fargo/fargo/fargo/models.py

276 lines
8.9 KiB
Python

# -*- coding: utf-8 -*-
# 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 subprocess
import os
import re
import threading
from django.conf import settings
from django.core.urlresolvers import reverse
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import force_text
from django.utils.text import slugify
from django.utils.http import urlquote
from django.utils.html import format_html
from django.dispatch import receiver
from django.db.models.signals import post_save, post_delete
from django.core.files.storage import default_storage
from sorl.thumbnail import get_thumbnail, delete
from sorl.thumbnail.conf import settings as thumbnail_settings
from jsonfield import JSONField
from . import utils, managers
@python_2_unicode_compatible
class Origin(models.Model):
label = models.CharField(_('Label'), max_length=80)
slug = models.SlugField(_('Slug'))
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.label)
return super(Origin, self).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')
document = models.ForeignKey(
'Document',
related_name='user_documents',
verbose_name=_('document'))
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)
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('[/\.+-]', '-', self.document.mime_type))
@python_2_unicode_compatible
class Validation(models.Model):
'''Validation of a document as special kind for an user,
the data field contains metadata extracted from the document.
'''
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('user'))
content_hash = models.CharField(
max_length=128,
verbose_name=_('content hash'),
blank=True,
null=True)
origin = models.ForeignKey(
Origin,
verbose_name=_('origin'),
null=True)
document_type = models.CharField(max_length=256, verbose_name=_('document type'))
data = JSONField(null=True, verbose_name=_('data'))
start = models.DateField(verbose_name=_('start date'))
end = models.DateField(verbose_name=_('end date'))
creator = models.CharField(max_length=256, verbose_name=_('creator'))
created = models.DateTimeField(verbose_name=_('creation date'))
@property
def document_type_schema(self):
return utils.get_document_type_schema(settings, self.document_type) or {}
@property
def metadata(self):
return self.document_type_schema.get('metadata', [])
def display(self):
template = self.document_type_schema.get('display_template', '')
if template:
try:
return force_text(template.format(**self.data))
except KeyError:
pass
l = []
for meta_field in self.metadata:
l.append(_(u'%(label)s: %(value)s')
% {
'label': meta_field['label'],
'value': self.data.get(meta_field['varname'], ''),
})
return force_text(u'; '.join(l))
display.short_description = _('description')
@property
def user_document(self):
if self.content_hash:
try:
return UserDocument.objects.get(document__content_hash=self.content_hash)
except UserDocument.DoesNotExist:
pass
def __str__(self):
return self.display()
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(Document, self).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: # sorl-thumbnail can crash in unexpected ways
return None
try:
# check file exists and is readable
default_storage.open(thumbnail.name)
return thumbnail
except IOError:
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}
@classmethod
def occupancy_for_user(cls, user):
return float(sum(document.content.size for document in cls.objects.filter(user_documents__user=user).distinct()))
def __unicode__(self):
return u'%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)