import os import datetime from math import ceil 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.utils import timezone 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') ) PLACE_MODES = ( ('live', 'live'), ('draft', 'draft'), ('review', 'review'), ('archive', 'archive'), ('imported', 'imported') ) PLACE_IMPORT_TYPES = ( ('kml', 'KML-File import'), ) class PlaceImport(models.Model): imported_when = models.DateTimeField( auto_now_add=True, verbose_name=_('When the imported has taken place') ) description = models.TextField( default=None, null=True, verbose_name=_('Description of the import') ) explorer = models.ForeignKey( 'Explorer', null=True, on_delete=models.SET_NULL, related_name='place_imports' ) import_type = models.TextField( default='kml', choices=PLACE_IMPORT_TYPES, verbose_name=_('What kind of import this is') ) 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 ) mode = models.TextField( default='live', choices=PLACE_MODES, verbose_name=_('Mode of Place Editing') ) place_import = models.ForeignKey( PlaceImport, null=True, on_delete=models.SET_NULL, related_name='place_list' ) def is_imported(self): return self.place_import != None 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) if amount > 0: latitude = 0 longitude = 0 for place in place_list: longitude += place.longitude latitude += place.latitude return {'latitude': latitude / amount, 'longitude': longitude / amount} else: # Location of China Corner in Münster # Where I almost always eat lunch # (Does'nt help losing wheight, tho) return {'latitude': 51.961922091398904, 'longitude': 7.6295628132604385} def calculate_place_level(self): if self.placevotings.count() == 0: self.level = 5 self.save() return level = 0 for vote in self.placevotings.all(): level += vote.vote self.level = round(level / self.placevotings.count()) self.save() def calculate_voting_accuracy(self): place_age = timezone.now() - self.submitted_when; accuaries = []; for vote in self.placevotings.all(): vote_age = timezone.now() - vote.submitted_when; accuracy = 100 - (100 / (place_age / vote_age)) accuaries.append(accuracy) if len(accuaries) > 0: return ceil(sum(accuaries) / len(accuaries)) else: return 0 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): 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)