From 5f304b91f37373850a825ed9b4ac360a27bdf109 Mon Sep 17 00:00:00 2001 From: reverend Date: Tue, 22 Sep 2020 21:56:51 +0200 Subject: [PATCH] Squashed commit of the following: commit 0d62e72d72922a84e41c9f2cc21977b794784d1c Merge: 79fee63 85f2a81 Author: reverend Date: Tue Sep 22 21:55:18 2020 +0200 Merge branch 'develop' into refactor/models commit 79fee631d7ac28509067ecdd74078f1a2f6e0be2 Author: reverend Date: Tue Sep 22 21:54:32 2020 +0200 Updating references for related name commit 8e07e79df2de2601f2e2eadfdd37eb7c719c51b0 Author: reverend Date: Tue Sep 22 21:53:31 2020 +0200 Generating of related names fix commit 5fd804f37a805ae4707e13c3d941bdde3660afea Merge: 8cc1d3e 3b526c9 Author: reverend Date: Tue Sep 22 21:01:48 2020 +0200 Merge branch 'develop' into refactor/models commit 8cc1d3e690211dba6451e86569f00078b23e0621 Author: reverend Date: Tue Sep 22 20:21:08 2020 +0200 Tests commit 7c0591e5397f892b1f6fb80725a693c21f90468a Author: reverend Date: Fri Sep 18 23:53:39 2020 +0200 Testing PlaceAsset commit 2e7b49ad1a15173565c81e7eb8bb3f35b9f622a6 Author: reverend Date: Fri Sep 18 22:25:08 2020 +0200 Restructuring models commit eb7d03b08b326f9115e70d0fd9ed5d0fc229a362 Author: reverend Date: Fri Sep 18 22:01:54 2020 +0200 Abstract class Expireable commit 2b51e741bb5734c5a578beeadef7819fe58b2223 Author: reverend Date: Fri Sep 18 21:54:07 2020 +0200 Abstract Model for PlaceAsset (i.e. Photoalbums) --- django_lostplaces/docs/developer.md | 2 +- django_lostplaces/lostplaces/models.py | 243 ------------------ .../lostplaces/models/__init__.py | 4 + .../lostplaces/models/abstract_models.py | 61 +++++ .../lostplaces/models/external_links.py | 14 + django_lostplaces/lostplaces/models/models.py | 55 ++++ django_lostplaces/lostplaces/models/place.py | 129 ++++++++++ .../templates/place/place_detail.html | 2 +- .../tests/models/test_abstract_models.py | 28 +- .../tests/models/test_link_model.py | 2 +- 10 files changed, 291 insertions(+), 249 deletions(-) delete mode 100644 django_lostplaces/lostplaces/models.py create mode 100644 django_lostplaces/lostplaces/models/__init__.py create mode 100644 django_lostplaces/lostplaces/models/abstract_models.py create mode 100644 django_lostplaces/lostplaces/models/external_links.py create mode 100644 django_lostplaces/lostplaces/models/models.py create mode 100644 django_lostplaces/lostplaces/models/place.py diff --git a/django_lostplaces/docs/developer.md b/django_lostplaces/docs/developer.md index 8e759b5..dce56e2 100644 --- a/django_lostplaces/docs/developer.md +++ b/django_lostplaces/docs/developer.md @@ -47,7 +47,7 @@ A mapable model has to provide its own get_absolute_url, in order to provide a l ### Submittable The abstract model Submittable represents an model that can be submitted by an user. It knows who submitted something and when: `submitted_by` -Referencing the explorer profile, see [Explorer](##explorer-user-profile). If the explorer profile is deleted, this instance is kept (on_delete=models.SET_NULL). The related_name is set to the class name, lower case appending an s (%(class)s) +Referencing the explorer profile, see [Explorer](##explorer-user-profile). If the explorer profile is deleted, this instance is kept (on_delete=models.SET_NULL). The related_name is set to the class name, lower case appending an s (%(class)ss) `submitted_when` When the object was submitted, automatically set by django (auto_now_add=True) diff --git a/django_lostplaces/lostplaces/models.py b/django_lostplaces/lostplaces/models.py deleted file mode 100644 index 152cc09..0000000 --- a/django_lostplaces/lostplaces/models.py +++ /dev/null @@ -1,243 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -''' -(Data)models which describe the structure of data to be saved into -database. -''' - -import os -import uuid - -from django.urls import reverse -from django.db import models -from django.contrib.auth.models import User -from django.db.models.signals import post_save -from django.dispatch import receiver -from django.core.validators import MaxValueValidator, MinValueValidator -from django.utils import timezone -from easy_thumbnails.fields import ThumbnailerImageField -from easy_thumbnails.files import get_thumbnailer -from taggit.managers import TaggableManager - -# Create your models here. - -class Explorer(models.Model): - """ - Profile that is linked to the a User. - Every user has a profile. - """ - - user = models.OneToOneField( - User, - on_delete=models.CASCADE, - related_name='explorer' - ) - - def __str__(self): - return self.user.username - -@receiver(post_save, sender=User) -def create_user_profile(sender, instance, created, **kwargs): - if created: - Explorer.objects.create(user=instance) - -@receiver(post_save, sender=User) -def save_user_profile(sender, instance, **kwargs): - instance.explorer.save() - -class Taggable(models.Model): - ''' - This abstract model represtens an object that is taggable - using django-taggit - ''' - class Meta: - abstract = True - - tags = TaggableManager(blank=True) - -class Mapable(models.Model): - ''' - This abstract model class represents an object that can be - displayed on a map. Subclasses have to provide absolute urls, - see https://docs.djangoproject.com/en/3.1/ref/models/instances/#get-absolute-url - ''' - class Meta: - abstract = True - - name = models.CharField(max_length=50) - latitude = models.FloatField( - validators=[ - MinValueValidator(-90), - MaxValueValidator(90) - ] - ) - longitude = models.FloatField( - validators=[ - MinValueValidator(-180), - MaxValueValidator(180) - ] - ) - -class Submittable(models.Model): - ''' - This abstract model class represents an object that can be submitted by - an explorer. - ''' - class Meta: - abstract = True - - submitted_when = models.DateTimeField(auto_now_add=True, null=True) - submitted_by = models.ForeignKey( - Explorer, - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name='%(class)s' - ) - -class Voucher(models.Model): - """ - Vouchers are authorization tokens to allow the registration of new users. - A voucher has a code, a creation and a deletion date, which are all - positional. Creation date is being set automatically during voucher - creation. - """ - - code = models.CharField(unique=True, max_length=30) - created_when = models.DateTimeField(auto_now_add=True) - expires_when = models.DateTimeField() - - @property - def valid(self): - return timezone.now() <= self.expires_when - - def __str__(self): - return "Voucher " + str(self.pk) - - -class Place(Submittable, Taggable, Mapable): - """ - Place defines a lost place (location, name, description etc.). - """ - - location = models.CharField(max_length=50) - description = models.TextField() - - def get_absolute_url(self): - return reverse('place_detail', kwargs={'pk': self.pk}) - - - @classmethod - # Get center position of LP-geocoordinates. - def average_latlon(cls, place_list): - amount = len(place_list) - # Init fill values to prevent None - longitude = 0 - latitude = 0 - - if amount > 0: - for place in place_list: - longitude += place.longitude - latitude += place.latitude - return {'latitude':latitude / amount, 'longitude': longitude / amount} - - return {'latitude': latitude, 'longitude': longitude} - - def __str__(self): - return self.name - - -def generate_image_upload_path(instance, filename): - """ - Callback for generating path for uploaded images. - Returns filename as: place_pk-placename{-rnd_string}.jpg - """ - - return 'places/' + str(instance.place.pk) + '-' + str(instance.place.name) + '.' + filename.split('.')[-1] - - -class PlaceImage (Submittable): - """ - PlaceImage defines an image file object that points to a file in uploads/. - Intermediate image sizes are generated as defined in THUMBNAIL_ALIASES. - PlaceImage references a Place to which it belongs. - """ - - description = models.TextField(blank=True) - filename = ThumbnailerImageField( - upload_to=generate_image_upload_path, - resize_source=dict(size=(2560, 2560), - sharpen=True) - ) - place = models.ForeignKey( - Place, - on_delete=models.CASCADE, - related_name='placeimages' - ) - - def __str__(self): - """ - Returning the name of the corresponding place + id - of this image as textual representation of this instance - """ - - return 'Image ' + str(self.pk) - - -# These two auto-delete files from filesystem when they are unneeded: - -@receiver(models.signals.post_delete, sender=PlaceImage) -def auto_delete_file_on_delete(sender, instance, **kwargs): - """ - Deletes file (including thumbnails) from filesystem - when corresponding `PlaceImage` object is deleted. - """ - if instance.filename: - # Get and delete all files and thumbnails from instance - thumbmanager = get_thumbnailer(instance.filename) - thumbmanager.delete(save=False) - - -@receiver(models.signals.pre_save, sender=PlaceImage) -def auto_delete_file_on_change(sender, instance, **kwargs): - """ - Deletes old file from filesystem - when corresponding `PlaceImage` object is updated - with new file. - """ - if not instance.pk: - return False - - try: - old_file = PlaceImage.objects.get(pk=instance.pk).filename - except PlaceImage.DoesNotExist: - return False - - # No need to delete thumbnails, as they will be overwritten on regeneration. - new_file = instance.filename - if not old_file == new_file: - if os.path.isfile(old_file.path): - os.remove(old_file.path) - - -class ExternalLink(models.Model): - url = models.URLField(max_length=200) - label = models.CharField(max_length=100) - submitted_by = models.ForeignKey( - Explorer, - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name='external_links' - ) - submitted_when = models.DateTimeField(auto_now_add=True, null=True) - - -class PhotoAlbum(ExternalLink): - place = models.ForeignKey( - Place, - on_delete=models.CASCADE, - related_name='photo_albums', - null=True - ) diff --git a/django_lostplaces/lostplaces/models/__init__.py b/django_lostplaces/lostplaces/models/__init__.py new file mode 100644 index 0000000..db32f55 --- /dev/null +++ b/django_lostplaces/lostplaces/models/__init__.py @@ -0,0 +1,4 @@ +from lostplaces.models.abstract_models import * +from lostplaces.models.place import * +from lostplaces.models.external_links import * +from lostplaces.models.models import * \ No newline at end of file diff --git a/django_lostplaces/lostplaces/models/abstract_models.py b/django_lostplaces/lostplaces/models/abstract_models.py new file mode 100644 index 0000000..c95ce61 --- /dev/null +++ b/django_lostplaces/lostplaces/models/abstract_models.py @@ -0,0 +1,61 @@ + +from django.db import models +from django.core.validators import MaxValueValidator, MinValueValidator + +from taggit.managers import TaggableManager + +class Taggable(models.Model): + ''' + This abstract model represtens an object that is taggalble + using django-taggit + ''' + class Meta: + abstract = True + + tags = TaggableManager(blank=True) + +class Mapable(models.Model): + ''' + This abstract model class represents an object that can be + displayed on a map. + ''' + class Meta: + abstract = True + + name = models.CharField(max_length=50) + latitude = models.FloatField( + validators=[ + MinValueValidator(-90), + MaxValueValidator(90) + ] + ) + longitude = models.FloatField( + validators=[ + MinValueValidator(-180), + MaxValueValidator(180) + ] + ) + +class Submittable(models.Model): + ''' + This abstract model class represents an object that can be submitted by + an explorer. + ''' + class Meta: + abstract = True + + submitted_when = models.DateTimeField(auto_now_add=True, null=True) + submitted_by = models.ForeignKey( + 'Explorer', + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='%(class)ss' + ) + +class Expireable(models.Model): + """ + Base class for things that can expire, i.e. VouchersAv + """ + created_when = models.DateTimeField(auto_now_add=True) + expires_when = models.DateTimeField() \ No newline at end of file diff --git a/django_lostplaces/lostplaces/models/external_links.py b/django_lostplaces/lostplaces/models/external_links.py new file mode 100644 index 0000000..b7bd5dc --- /dev/null +++ b/django_lostplaces/lostplaces/models/external_links.py @@ -0,0 +1,14 @@ +from django.db import models + +from lostplaces.models.place import PlaceAsset + +class ExternalLink(PlaceAsset): + + class Meta: + abstract = True + + url = models.URLField(max_length=200) + label = models.CharField(max_length=100) + +class PhotoAlbum(ExternalLink): + pass \ No newline at end of file diff --git a/django_lostplaces/lostplaces/models/models.py b/django_lostplaces/lostplaces/models/models.py new file mode 100644 index 0000000..a545fe0 --- /dev/null +++ b/django_lostplaces/lostplaces/models/models.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +''' +(Data)models which describe the structure of data to be saved into +database. +''' + +import uuid + +from django.db import models +from django.contrib.auth.models import User +from django.db.models.signals import post_save +from django.dispatch import receiver + +from lostplaces.models.abstract_models import Expireable + +class Explorer(models.Model): + """ + Profile that is linked to the a User. + Every user has a profile. + """ + + user = models.OneToOneField( + User, + on_delete=models.CASCADE, + related_name='explorer' + ) + + def __str__(self): + return self.user.username + +@receiver(post_save, sender=User) +def create_user_profile(sender, instance, created, **kwargs): + if created: + Explorer.objects.create(user=instance) + +@receiver(post_save, sender=User) +def save_user_profile(sender, instance, **kwargs): + instance.explorer.save() + +class Voucher(Expireable): + """ + Vouchers are authorization to created_when = models.DateTimeField(auto_now_add=True) + expires_when = models.DateTimeField()kens to allow the registration of new users. + A voucher has a code, a creation and a deletion date, which are all + positional. Creation date is being set automatically during voucher + creation. + """ + + code = models.CharField(unique=True, max_length=30) + + def __str__(self): + return "Voucher " + str(self.code) + diff --git a/django_lostplaces/lostplaces/models/place.py b/django_lostplaces/lostplaces/models/place.py new file mode 100644 index 0000000..402afdb --- /dev/null +++ b/django_lostplaces/lostplaces/models/place.py @@ -0,0 +1,129 @@ +import os + +from django.db import models +from django.urls import reverse +from django.dispatch import receiver +from django.db.models.signals import post_delete, pre_save + +from lostplaces.models.abstract_models import Submittable, Taggable, Mapable + +from easy_thumbnails.fields import ThumbnailerImageField +from easy_thumbnails.files import get_thumbnailer + +class Place(Submittable, Taggable, Mapable): + """ + Place defines a lost place (location, name, description etc.). + """ + + location = models.CharField(max_length=50) + description = models.TextField() + + def get_absolute_url(self): + return reverse('place_detail', kwargs={'pk': self.pk}) + + + @classmethod + # Get center position of LP-geocoordinates. + def average_latlon(cls, place_list): + amount = len(place_list) + # Init fill values to prevent None + longitude = 0 + latitude = 0 + + if amount > 0: + for place in place_list: + longitude += place.longitude + latitude += place.latitude + return {'latitude':latitude / amount, 'longitude': longitude / amount} + + return {'latitude': latitude, 'longitude': longitude} + + def __str__(self): + return self.name + + +def generate_image_upload_path(instance, filename): + """ + Callback for generating path for uploaded images. + Returns filename as: place_pk-placename{-rnd_string}.jpg + """ + + return 'places/' + str(instance.place.pk) + '-' + str(instance.place.name) + '.' + filename.split('.')[-1] + +class PlaceAsset(Submittable): + """ + Assets to a place, i.e. images + """ + + class Meta: + abstract = True + + place = models.ForeignKey( + Place, + on_delete=models.CASCADE, + related_name='%(class)ss', + null=True + ) + +class PlaceImage (Submittable): + """ + PlaceImage defines an image file object that points to a file in uploads/. + Intermediate image sizes are generated as defined in THUMBNAIL_ALIASES. + PlaceImage references a Place to which it belongs. + """ + + description = models.TextField(blank=True) + filename = ThumbnailerImageField( + upload_to=generate_image_upload_path, + resize_source=dict(size=(2560, 2560), + sharpen=True) + ) + place = models.ForeignKey( + Place, + on_delete=models.CASCADE, + related_name='placeimages' + ) + + def __str__(self): + """ + Returning the name of the corresponding place + id + of this image as textual representation of this instance + """ + + return 'Image ' + str(self.pk) + + +# These two auto-delete files from filesystem when they are unneeded: + +@receiver(post_delete, sender=PlaceImage) +def auto_delete_file_on_delete(sender, instance, **kwargs): + """ + Deletes file (including thumbnails) from filesystem + when corresponding `PlaceImage` object is deleted. + """ + if instance.filename: + # Get and delete all files and thumbnails from instance + thumbmanager = get_thumbnailer(instance.filename) + thumbmanager.delete(save=False) + + +@receiver(pre_save, sender=PlaceImage) +def auto_delete_file_on_change(sender, instance, **kwargs): + """ + Deletes old file from filesystem + when corresponding `PlaceImage` object is updated + with new file. + """ + if not instance.pk: + return False + + try: + old_file = PlaceImage.objects.get(pk=instance.pk).filename + except PlaceImage.DoesNotExist: + return False + + # No need to delete thumbnails, as they will be overwritten on regeneration. + new_file = instance.filename + if not old_file == new_file: + if os.path.isfile(old_file.path): + os.remove(old_file.path) \ No newline at end of file diff --git a/django_lostplaces/lostplaces/templates/place/place_detail.html b/django_lostplaces/lostplaces/templates/place/place_detail.html index 480d676..b471dd5 100644 --- a/django_lostplaces/lostplaces/templates/place/place_detail.html +++ b/django_lostplaces/lostplaces/templates/place/place_detail.html @@ -57,7 +57,7 @@

Photo albums