fargo/fargo/fargo/models.py

196 lines
6.7 KiB
Python

# fargo - document box
# Copyright (C) 2016-2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import base64
import hashlib
import os
import re
import subprocess
import threading
from django.conf import settings
from django.contrib.postgres.fields import JSONField
from django.core.files.storage import default_storage
from django.db import models
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from django.urls import reverse
from django.utils.encoding import force_text, python_2_unicode_compatible
from django.utils.html import format_html
from django.utils.http import urlquote
from django.utils.text import slugify
from django.utils.translation import ugettext_lazy as _
from sorl.thumbnail import delete, get_thumbnail
from sorl.thumbnail.conf import settings as thumbnail_settings
from . import managers, utils
def slug_truncate(label, length=256):
slug = slugify(label)
if len(slug) < length:
return slug
return slug[: length - 5] + '-%4s' % hashlib.md5(label.encode()).hexdigest()[:4]
@python_2_unicode_compatible
class Origin(models.Model):
label = models.TextField(_('Label'))
slug = models.SlugField(_('Slug'), max_length=256)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slug_truncate(self.label)
return super().save(*args, **kwargs)
def __str__(self):
return self.label
@python_2_unicode_compatible
class UserDocument(models.Model):
'''Document uploaded by an user or an agent'''
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('user'),
related_name='user_documents',
on_delete=models.CASCADE,
)
document = models.ForeignKey(
'Document', related_name='user_documents', verbose_name=_('document'), on_delete=models.CASCADE
)
filename = models.CharField(verbose_name=_('filename'), max_length=512)
created = models.DateTimeField(verbose_name=_('creation date'), auto_now_add=True)
origin = models.ForeignKey(Origin, verbose_name=_('origin'), null=True, on_delete=models.CASCADE)
deletable_by_user = models.BooleanField(verbose_name=_('deletable by user'), default=True)
title = models.CharField(verbose_name=_('title'), max_length=200, blank=True)
description = models.TextField(verbose_name=_('description'), blank=True)
expiration_date = models.DateField(verbose_name=_('expiration date'), blank=True, null=True)
class Meta:
verbose_name = _('user document')
verbose_name_plural = _('user documents')
ordering = ('-created', 'user')
unique_together = ('user', 'filename', 'document', 'origin', 'deletable_by_user')
@property
def filename_encoded(self):
return urlquote(self.filename, safe='')
def __str__(self):
return self.title or self.filename
def get_download_url(self):
return reverse('download', kwargs={'pk': self.id, 'filename': self.filename_encoded})
@property
def thumbnail_image(self):
thumbnail = self.document.thumbnail
if not thumbnail:
return ''
src = reverse('thumbnail', kwargs={'pk': self.id, 'filename': self.filename_encoded})
return {'src': src, 'width': thumbnail.width, 'height': thumbnail.height}
@property
def css_classes(self):
if not self.document.mime_type:
return ''
return 'mime-%s mime-%s' % (
self.document.mime_type.split('/')[0],
re.sub(r'[/\.+-]', '-', self.document.mime_type),
)
class Document(models.Model):
'''Content indexed documents'''
content_hash = models.CharField(primary_key=True, max_length=128, verbose_name=_('content hash'))
content = models.FileField(upload_to='uploads/', max_length=300, verbose_name=_('file'))
mime_type = models.CharField(max_length=256, blank=True)
creation_date = models.DateTimeField(auto_now_add=True)
objects = managers.DocumentManager()
def save(self, *args, **kwargs):
'''Create content_hash if new'''
if not self.content_hash:
self.content_hash = utils.sha256_of_file(self.content)
if not self.mime_type:
self.mime_type = utils.get_mime_type(self.content.file.name) or ''
super().save(*args, **kwargs)
@property
def thumbnail(self):
if not (self.mime_type.startswith('image/') or self.mime_type == 'application/pdf'):
return None
try:
thumbnail = get_thumbnail(self.content, '200x200')
except Exception: # sorl-thumbnail can crash in unexpected ways
return None
try:
# check file exists and is readable
default_storage.open(thumbnail.name)
return thumbnail
except OSError:
pass
return None
@property
def thumbnail_data_url(self):
thumbnail = self.thumbnail
if not thumbnail:
return ''
mime_type = 'image/' + thumbnail_settings.THUMBNAIL_FORMAT.lower()
return 'data:%s;base64,%s' % (mime_type, base64.b64encode(thumbnail.read()))
@property
def thumbnail_img_tag(self):
thumbnail = self.thumbnail
if not thumbnail:
return ''
return format_html(
'<img width="{}" height="{}" src="{}"/>',
thumbnail.width,
thumbnail.height,
self.thumbnail_data_url,
)
@property
def thumbnail_image(self):
thumbnail = self.thumbnail
if not thumbnail:
return ''
return {'src': self.thumbnail_data_url, 'width': thumbnail.width, 'height': thumbnail.height}
def __unicode__(self):
return '%s %s' % (os.path.basename(self.content.name), self.content_hash[:6])
class Meta:
verbose_name = _('document')
verbose_name_plural = _('documents')
ordering = ('creation_date',)
@receiver(post_delete, sender=Document)
def delete_file(sender, instance, **kwargs):
if instance.content:
if os.path.isfile(instance.content.path):
os.remove(instance.content.path)
delete(instance.content)