Squashed commit of the following:

commit 0d62e72d72922a84e41c9f2cc21977b794784d1c
Merge: 79fee63 85f2a81
Author: reverend <reverend@reverend2048.de>
Date:   Tue Sep 22 21:55:18 2020 +0200

    Merge branch 'develop' into refactor/models

commit 79fee631d7ac28509067ecdd74078f1a2f6e0be2
Author: reverend <reverend@reverend2048.de>
Date:   Tue Sep 22 21:54:32 2020 +0200

    Updating references for related name

commit 8e07e79df2de2601f2e2eadfdd37eb7c719c51b0
Author: reverend <reverend@reverend2048.de>
Date:   Tue Sep 22 21:53:31 2020 +0200

    Generating of related names fix

commit 5fd804f37a805ae4707e13c3d941bdde3660afea
Merge: 8cc1d3e 3b526c9
Author: reverend <reverend@reverend2048.de>
Date:   Tue Sep 22 21:01:48 2020 +0200

    Merge branch 'develop' into refactor/models

commit 8cc1d3e690211dba6451e86569f00078b23e0621
Author: reverend <reverend@reverend2048.de>
Date:   Tue Sep 22 20:21:08 2020 +0200

    Tests

commit 7c0591e5397f892b1f6fb80725a693c21f90468a
Author: reverend <reverend@reverend2048.de>
Date:   Fri Sep 18 23:53:39 2020 +0200

    Testing PlaceAsset

commit 2e7b49ad1a15173565c81e7eb8bb3f35b9f622a6
Author: reverend <reverend@reverend2048.de>
Date:   Fri Sep 18 22:25:08 2020 +0200

    Restructuring models

commit eb7d03b08b326f9115e70d0fd9ed5d0fc229a362
Author: reverend <reverend@reverend2048.de>
Date:   Fri Sep 18 22:01:54 2020 +0200

    Abstract class Expireable

commit 2b51e741bb5734c5a578beeadef7819fe58b2223
Author: reverend <reverend@reverend2048.de>
Date:   Fri Sep 18 21:54:07 2020 +0200

    Abstract Model for PlaceAsset (i.e. Photoalbums)
This commit is contained in:
reverend 2020-09-22 21:56:51 +02:00
parent 85f2a81ebb
commit 5f304b91f3
10 changed files with 291 additions and 249 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -57,7 +57,7 @@
<h1 class="LP-Headline">Photo albums</h1>
<div class="LP-LinkList">
<ul class="LP-LinkList__Container">
{% for photo_album in place.photo_albums.all %}
{% for photo_album in place.photoalbums.all %}
<li class="LP-LinkList__Item">
<a target="_blank" href="{{photo_album.url}}" class="LP-Link">
<span class="LP-Text">{{photo_album.label}}</span>

View File

@ -8,7 +8,9 @@ from django.contrib.auth.models import User
from lostplaces.models import (
Taggable,
Mapable,
Submittable
Submittable,
PlaceAsset,
Expireable
)
from lostplaces.tests.models import ModelTestCase
@ -65,8 +67,8 @@ class SubmittableTestCase(ModelTestCase):
)
self.assertEqual(
submitted_by.remote_field.related_name,
'%(class)s',
msg='Expecting the related_name of %s to be \'%%(class)s\', got %s' % (
'%(class)ss',
msg='Expecting the related_name of %s to be \'%%(class)ss\', got %s' % (
str(submitted_by),
submitted_by.remote_field.related_name
)
@ -91,3 +93,23 @@ class SubmittableTestCase(ModelTestCase):
)
)
class PlaceAssetTestCase(ModelTestCase):
model = PlaceAsset
def test_place(self):
field = self.assertField('place', models.ForeignKey)
self.assertEqual(field.remote_field.on_delete, models.CASCADE,
msg='Expecting the deletion of %s to be cascading' % (
str(field)
)
)
expected_related_name = '%(class)ss'
self.assertEqual(field.remote_field.related_name, expected_related_name,
msg='Expecting the related name of %s to be %s' % (
str(field),
expected_related_name
)
)
class ExpireableTestCase(ModelTestCase):
model = Expireable

View File

@ -69,7 +69,7 @@ class PhotoAlbumTestCase(ModelTestCase):
str(field)
)
)
expected_related_name = 'photo_albums'
expected_related_name = 'photoalbums'
self.assertEqual(field.remote_field.related_name, expected_related_name,
msg='Expecting the related name of %s to be %s' % (
str(field),