import os from math import floor 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 django.utils.translation import gettext as _ from django.conf import settings from lostplaces.models.abstract_models import Submittable, Taggable, Mapable, Expireable from easy_thumbnails.fields import ThumbnailerImageField from easy_thumbnails.files import get_thumbnailer PLACE_LEVELS = ( (1, 'Ruin'), (2, 'Vandalized'), (3, 'Natures Treasure'), (4, 'Lost in History'), (5, 'Time Capsule') ) class Place(Submittable, Taggable, Mapable): """ Place defines a lost place (location, name, description etc.). """ location = models.CharField( max_length=50, verbose_name=_('Location'), ) description = models.TextField( help_text=_('Description of the place: e.g. how to get there, where to be careful, the place\'s history...'), verbose_name=_('Description'), ) hero = models.ForeignKey( 'PlaceImage', on_delete=models.SET_NULL, null=True, blank=True, related_name='place_heros' ) level = models.IntegerField( default=5, choices=PLACE_LEVELS ) def get_hero_image(self): if self.hero: return self.hero elif len(self.placeimages.all()) > 0: return self.placeimages.first() else: return None def get_absolute_url(self): return reverse('place_detail', kwargs={'pk': self.pk}) def get_hero_index_in_queryset(self): ''' Calculates the index of the hero image within the list / queryset of images. Necessary for the lightbox. ''' for i in range(0, len(self.placeimages.all())): image = self.placeimages.all()[i] if image == self.hero: return i return None @classmethod # Get center position of LP-geocoordinates. def average_latlon(cls, place_list): amount = len(place_list) # Init fill values to prevent None # China Corner in Münster # Where I almost always eat lunch # (Does'nt help losing wheight, tho) longitude = 7.6295628132604385 latitude = 51.961922091398904 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 calculate_place_level(self): self.remove_expired_votes() if self.placevotings.count() == 0: self.level = 5 self.save() return level = 0 for vote in self.placevotings.all(): level += vote.vote self.level = floor(level / self.placevotings.count()) self.save() def remove_expired_votes(self): for vote in self.placevotings.all(): if vote.is_expired: vote.delete() def __str__(self): return self.name def generate_place_image_filename(instance, filename): """ Callback for generating filename for uploaded place images. Returns filename as: place_pk-placename{-number}.jpg """ return settings.RELATIVE_THUMBNAIL_PATH + str(instance.place.pk) + '-' + str(instance.place.name) + '.' + filename.split('.')[-1] def generate_image_upload_path(instance, filename): return generate_place_image_filename(instance, filename) 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 DummyAsset(PlaceAsset): name = models.CharField(max_length=50) class PlaceImage(PlaceAsset): """ 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, verbose_name=_('Description'), ) filename = ThumbnailerImageField( upload_to=generate_place_image_filename, resize_source=dict(size=(2560, 2560), sharpen=True), verbose_name=_('Images'), help_text=_('Optional: One or more images to upload') ) 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.place.name) # 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: old_file.delete(save=False) class PlaceVoting(PlaceAsset, Expireable): vote = models.IntegerField(choices=PLACE_LEVELS) def get_human_readable_level(self): return PLACE_LEVELS[self.vote - 1][1] def get_all_choices(self): return reversed(PLACE_LEVELS)