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 @@