#!/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. ''' 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 )