import os import uuid from typing import List, cast from django.conf import settings from django.db import models from ..agenda.mixins import ListOfSpeakersMixin from ..core.config import config from ..utils.models import RESTModelMixin from ..utils.rest_api import ValidationError from .access_permissions import MediafileAccessPermissions class MediafileManager(models.Manager): """ Customized model manager to support our get_full_queryset method. """ def get_full_queryset(self): """ Returns the normal queryset with all mediafiles. In the background all related list of speakers are prefetched from the database. """ return self.get_queryset().prefetch_related( "lists_of_speakers", "parent", "access_groups" ) def delete(self, *args, **kwargs): raise RuntimeError( "Do not use the querysets delete function. Please delete every mediafile on it's own." ) def get_file_path(mediafile, filename): mediafile.original_filename = filename ext = filename.split(".")[-1] filename = "%s.%s" % (uuid.uuid4(), ext) return os.path.join("file", filename) class Mediafile(RESTModelMixin, ListOfSpeakersMixin, models.Model): """ Class for uploaded files which can be delivered under a certain url. """ objects = MediafileManager() access_permissions = MediafileAccessPermissions() can_see_permission = "mediafiles.can_see" mediafile = models.FileField(upload_to=get_file_path, null=True) """ See https://docs.djangoproject.com/en/dev/ref/models/fields/#filefield for more information. """ title = models.CharField(max_length=255) """A string representing the title of the file.""" original_filename = models.CharField(max_length=255) create_timestamp = models.DateTimeField(auto_now_add=True) """A DateTimeField to save the upload date and time.""" is_directory = models.BooleanField(default=False) parent = models.ForeignKey( "self", # The on_delete should be CASCADE_AND_AUTOUPDATE, but we do # have to delete the actual file from every mediafile to ensure # cleaning up the server files. This is ensured by the custom delete # method of every mediafile. Do not use the delete method of the # mediafile manager. on_delete=models.SET_NULL, null=True, blank=True, related_name="children", ) access_groups = models.ManyToManyField(settings.AUTH_GROUP_MODEL, blank=True) class Meta: """ Meta class for the mediafile model. """ ordering = ("title",) default_permissions = () permissions = ( ("can_see", "Can see the list of files"), ("can_manage", "Can manage files"), ) def create(self, *args, **kwargs): self.validate_unique() return super().create(*args, **kwargs) def save(self, *args, **kwargs): self.validate_unique() return super().save(*args, **kwargs) def validate_unique(self): """ `unique_together` is not working with foreign keys with possible `null` values. So we do need to check this here. self.original_filename is not yet set, but if is_file is True, the actual filename is self.mediafile.file """ title_or_original_filename = models.Q(title=self.title) if self.is_file: title_or_original_filename = title_or_original_filename | models.Q( original_filename=self.mediafile.name ) if ( Mediafile.objects.exclude( pk=self.pk ) # self.pk is None on creation, but this does not invalidate the exclude statement. .filter(title_or_original_filename, parent=self.parent) .exists() ): raise ValidationError( { "detail": "A mediafile with this title or filename already exists in this folder." } ) def __str__(self): """ Method for representation. """ return self.title def delete(self, skip_autoupdate=False): mediafiles_to_delete = self.get_children_deep() mediafiles_to_delete.append(self) for mediafile in mediafiles_to_delete: if mediafile.is_file: # To avoid Django calling save() and triggering autoupdate we do not # use the builtin method mediafile.mediafile.delete() but call # mediafile.mediafile.storage.delete(...) directly. This may have # unattended side effects so be careful especially when accessing files # on server via Django methods (file, open(), save(), ...). mediafile.mediafile.storage.delete(mediafile.mediafile.name) mediafile._db_delete(skip_autoupdate=skip_autoupdate) def _db_delete(self, *args, **kwargs): """ Captures the original .delete() method. """ return super().delete(*args, **kwargs) def get_children_deep(self): """ Returns all children and all children of childrens and so forth. """ children = [] for child in self.children.all(): children.append(child) children.extend(child.get_children_deep()) return children @property def path(self): name = (self.title + "/") if self.is_directory else self.original_filename if self.parent: return self.parent.path + name else: return name @property def url(self): return settings.MEDIA_URL + self.path @property def inherited_access_groups_id(self): """ True: all groups False: no permissions List[int]: Groups with permissions """ own_access_groups = [group.id for group in self.access_groups.all()] if not self.parent: return own_access_groups or True # either some groups or all access_groups = self.parent.inherited_access_groups_id if len(own_access_groups) > 0: if isinstance(access_groups, bool) and access_groups: return own_access_groups elif isinstance(access_groups, bool) and not access_groups: return False else: # List[int] access_groups = [ id for id in cast(List[int], access_groups) if id in own_access_groups ] return access_groups or False else: return access_groups # We do not have restrictions, copy from parent. def get_filesize(self): """ Transforms bytes to kilobytes or megabytes. Returns the size as string. """ # TODO: Read http://stackoverflow.com/a/1094933 and think about it. try: size = self.mediafile.size except OSError: size_string = "unknown" except ValueError: # happens, if this is a directory and no file exists return None else: if size < 1024: size_string = "< 1 kB" elif size >= 1024 * 1024: mB = size / 1024 / 1024 size_string = "%d MB" % mB else: kB = size / 1024 size_string = "%d kB" % kB return size_string @property def is_logo(self): if self.is_directory: return False for key in config["logos_available"]: if config[key]["path"] == self.url: return True return False @property def is_font(self): if self.is_directory: return False for key in config["fonts_available"]: if config[key]["path"] == self.url: return True return False @property def is_special_file(self): return self.is_logo or self.is_font @property def is_file(self): return not self.is_directory def get_list_of_speakers_title_information(self): return {"title": self.title}