2019-06-28 07:24:28 +02:00
|
|
|
import os
|
|
|
|
import uuid
|
|
|
|
from typing import List, cast
|
|
|
|
|
2015-09-17 22:58:24 +02:00
|
|
|
from django.conf import settings
|
2019-12-04 14:24:30 +01:00
|
|
|
from django.db import connections, models
|
|
|
|
from jsonfield import JSONField
|
|
|
|
|
|
|
|
from openslides.utils import logging
|
2013-02-16 16:19:20 +01:00
|
|
|
|
2019-04-23 16:57:35 +02:00
|
|
|
from ..agenda.mixins import ListOfSpeakersMixin
|
2017-09-26 09:00:34 +02:00
|
|
|
from ..core.config import config
|
2019-06-28 07:24:28 +02:00
|
|
|
from ..utils.models import RESTModelMixin
|
|
|
|
from ..utils.rest_api import ValidationError
|
2019-03-06 14:53:24 +01:00
|
|
|
from .access_permissions import MediafileAccessPermissions
|
2019-12-04 14:24:30 +01:00
|
|
|
from .utils import bytes_to_human
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
if "mediafiles" in connections:
|
|
|
|
use_mediafile_database = True
|
|
|
|
logger.info("Using a standalone mediafile database")
|
2013-02-16 16:19:20 +01:00
|
|
|
|
|
|
|
|
2019-04-23 16:57:35 +02:00
|
|
|
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.
|
|
|
|
"""
|
2019-06-28 07:24:28 +02:00
|
|
|
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):
|
|
|
|
ext = filename.split(".")[-1]
|
|
|
|
filename = "%s.%s" % (uuid.uuid4(), ext)
|
|
|
|
return os.path.join("file", filename)
|
2019-04-23 16:57:35 +02:00
|
|
|
|
|
|
|
|
|
|
|
class Mediafile(RESTModelMixin, ListOfSpeakersMixin, models.Model):
|
2013-03-19 00:51:52 +01:00
|
|
|
"""
|
2013-02-16 16:19:20 +01:00
|
|
|
Class for uploaded files which can be delivered under a certain url.
|
2019-12-04 14:24:30 +01:00
|
|
|
|
|
|
|
This model encapsulte directories and mediafiles. If is_directory is True, the `title` is
|
|
|
|
the directory name. Else, there might be a mediafile, except when using a mediafile DB. In
|
|
|
|
this case, also mediafile is None.
|
2013-02-16 16:19:20 +01:00
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
|
2019-04-23 16:57:35 +02:00
|
|
|
objects = MediafileManager()
|
2016-02-11 22:58:32 +01:00
|
|
|
access_permissions = MediafileAccessPermissions()
|
2019-01-06 16:22:33 +01:00
|
|
|
can_see_permission = "mediafiles.can_see"
|
2016-02-11 22:58:32 +01:00
|
|
|
|
2019-06-28 07:24:28 +02:00
|
|
|
mediafile = models.FileField(upload_to=get_file_path, null=True)
|
2013-03-19 00:51:52 +01:00
|
|
|
"""
|
2013-02-16 16:19:20 +01:00
|
|
|
See https://docs.djangoproject.com/en/dev/ref/models/fields/#filefield
|
|
|
|
for more information.
|
|
|
|
"""
|
2013-03-19 00:51:52 +01:00
|
|
|
|
2019-12-04 14:24:30 +01:00
|
|
|
filesize = models.CharField(max_length=255, default="")
|
|
|
|
|
|
|
|
mimetype = models.CharField(max_length=255, default="")
|
|
|
|
|
|
|
|
pdf_information = JSONField(default=dict)
|
|
|
|
|
2019-06-28 07:24:28 +02:00
|
|
|
title = models.CharField(max_length=255)
|
2013-02-16 16:19:20 +01:00
|
|
|
"""A string representing the title of the file."""
|
|
|
|
|
2019-06-28 07:24:28 +02:00
|
|
|
original_filename = models.CharField(max_length=255)
|
2016-05-14 14:49:24 +02:00
|
|
|
|
2019-06-28 07:24:28 +02:00
|
|
|
create_timestamp = models.DateTimeField(auto_now_add=True)
|
2013-02-16 16:19:20 +01:00
|
|
|
"""A DateTimeField to save the upload date and time."""
|
|
|
|
|
2019-06-28 07:24:28 +02:00
|
|
|
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)
|
|
|
|
|
2013-02-16 16:19:20 +01:00
|
|
|
class Meta:
|
2013-03-19 00:51:52 +01:00
|
|
|
"""
|
|
|
|
Meta class for the mediafile model.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
|
2019-06-28 07:24:28 +02:00
|
|
|
ordering = ("title",)
|
2015-12-10 00:20:59 +01:00
|
|
|
default_permissions = ()
|
2013-02-16 16:19:20 +01:00
|
|
|
permissions = (
|
2019-01-06 16:22:33 +01:00
|
|
|
("can_see", "Can see the list of files"),
|
|
|
|
("can_manage", "Can manage files"),
|
|
|
|
)
|
2013-02-16 16:19:20 +01:00
|
|
|
|
2019-06-28 07:24:28 +02:00
|
|
|
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.
|
|
|
|
"""
|
2019-11-08 13:09:07 +01:00
|
|
|
title_or_original_filename = models.Q(title=self.title)
|
|
|
|
if self.is_file:
|
|
|
|
title_or_original_filename = title_or_original_filename | models.Q(
|
2019-12-04 14:24:30 +01:00
|
|
|
original_filename=self.original_filename
|
2019-11-08 13:09:07 +01:00
|
|
|
)
|
|
|
|
|
2019-06-28 07:24:28 +02:00
|
|
|
if (
|
2019-11-08 13:09:07 +01:00
|
|
|
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)
|
2019-06-28 07:24:28 +02:00
|
|
|
.exists()
|
|
|
|
):
|
|
|
|
raise ValidationError(
|
2019-11-08 13:09:07 +01:00
|
|
|
{
|
|
|
|
"detail": "A mediafile with this title or filename already exists in this folder."
|
|
|
|
}
|
2019-06-28 07:24:28 +02:00
|
|
|
)
|
|
|
|
|
2014-08-16 09:25:18 +02:00
|
|
|
def __str__(self):
|
2013-03-19 00:51:52 +01:00
|
|
|
"""
|
|
|
|
Method for representation.
|
|
|
|
"""
|
2013-02-16 16:19:20 +01:00
|
|
|
return self.title
|
|
|
|
|
2019-06-28 07:24:28 +02:00
|
|
|
def delete(self, skip_autoupdate=False):
|
|
|
|
mediafiles_to_delete = self.get_children_deep()
|
|
|
|
mediafiles_to_delete.append(self)
|
2019-12-04 14:24:30 +01:00
|
|
|
deleted_ids = []
|
2019-06-28 07:24:28 +02:00
|
|
|
for mediafile in mediafiles_to_delete:
|
2019-12-04 14:24:30 +01:00
|
|
|
deleted_ids.append(mediafile.id)
|
|
|
|
# Do not check for is_file, this might be wrong to delete the actual mediafile
|
|
|
|
if mediafile.mediafile:
|
2019-06-28 07:24:28 +02:00
|
|
|
# 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)
|
|
|
|
|
2019-12-04 14:24:30 +01:00
|
|
|
return deleted_ids
|
|
|
|
|
2019-06-28 07:24:28 +02:00
|
|
|
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):
|
2017-04-28 22:10:18 +02:00
|
|
|
"""
|
2019-06-28 07:24:28 +02:00
|
|
|
True: all groups
|
|
|
|
False: no permissions
|
|
|
|
List[int]: Groups with permissions
|
2017-04-28 22:10:18 +02:00
|
|
|
"""
|
2019-06-28 07:24:28 +02:00
|
|
|
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.
|
2017-04-28 22:10:18 +02:00
|
|
|
|
2013-02-16 16:19:20 +01:00
|
|
|
def get_filesize(self):
|
2013-03-19 00:51:52 +01:00
|
|
|
"""
|
2013-12-23 18:46:04 +01:00
|
|
|
Transforms bytes to kilobytes or megabytes. Returns the size as string.
|
2013-03-19 00:51:52 +01:00
|
|
|
"""
|
2013-12-23 18:46:04 +01:00
|
|
|
try:
|
|
|
|
size = self.mediafile.size
|
|
|
|
except OSError:
|
2019-12-04 14:24:30 +01:00
|
|
|
return "unknown"
|
2019-06-28 07:24:28 +02:00
|
|
|
except ValueError:
|
|
|
|
# happens, if this is a directory and no file exists
|
|
|
|
return None
|
2013-02-16 16:19:20 +01:00
|
|
|
else:
|
2019-12-04 14:24:30 +01:00
|
|
|
return bytes_to_human(size)
|
2017-09-26 09:00:34 +02:00
|
|
|
|
2019-06-28 07:24:28 +02:00
|
|
|
@property
|
2017-09-26 09:00:34 +02:00
|
|
|
def is_logo(self):
|
2019-06-28 07:24:28 +02:00
|
|
|
if self.is_directory:
|
|
|
|
return False
|
2019-01-06 16:22:33 +01:00
|
|
|
for key in config["logos_available"]:
|
2019-06-28 07:24:28 +02:00
|
|
|
if config[key]["path"] == self.url:
|
2017-09-26 09:00:34 +02:00
|
|
|
return True
|
|
|
|
return False
|
2018-04-04 11:25:45 +02:00
|
|
|
|
2019-06-28 07:24:28 +02:00
|
|
|
@property
|
2018-04-04 11:25:45 +02:00
|
|
|
def is_font(self):
|
2019-06-28 07:24:28 +02:00
|
|
|
if self.is_directory:
|
|
|
|
return False
|
2019-01-06 16:22:33 +01:00
|
|
|
for key in config["fonts_available"]:
|
2019-06-28 07:24:28 +02:00
|
|
|
if config[key]["path"] == self.url:
|
2018-04-04 11:25:45 +02:00
|
|
|
return True
|
|
|
|
return False
|
2019-04-23 16:57:35 +02:00
|
|
|
|
2019-06-28 07:24:28 +02:00
|
|
|
@property
|
|
|
|
def is_special_file(self):
|
|
|
|
return self.is_logo or self.is_font
|
|
|
|
|
|
|
|
@property
|
|
|
|
def is_file(self):
|
2019-12-04 14:24:30 +01:00
|
|
|
""" Do not check the self.mediafile, becuase this is not a valid indicator. """
|
2019-06-28 07:24:28 +02:00
|
|
|
return not self.is_directory
|
|
|
|
|
2019-04-23 16:57:35 +02:00
|
|
|
def get_list_of_speakers_title_information(self):
|
|
|
|
return {"title": self.title}
|