From: Guillaume Pellerin Date: Tue, 20 Jan 2015 16:07:30 +0000 (+0100) Subject: Refactoring / splitting models X-Git-Tag: 1.5~8 X-Git-Url: https://git.parisson.com/?a=commitdiff_plain;h=7fc3149c7a07558c77c080165d434c78cb2ea0ed;p=telemeta.git Refactoring / splitting models --- diff --git a/telemeta/admin.py b/telemeta/admin.py index 4bcb3805..8988f4e2 100644 --- a/telemeta/admin.py +++ b/telemeta/admin.py @@ -1,9 +1,13 @@ # -*- coding: utf-8 -*- -from telemeta.models.media import * +from telemeta.models.fonds import * +from telemeta.models.corpus import * +from telemeta.models.collection import * +from telemeta.models.item import * from telemeta.models.instrument import * from telemeta.models.location import * from telemeta.models.language import * from telemeta.models.system import * +from telemeta.models.format import * from django.contrib import admin from django.contrib.auth.models import User from django.contrib.auth.admin import UserAdmin diff --git a/telemeta/models/__init__.py b/telemeta/models/__init__.py index e4e118c5..99f228e8 100644 --- a/telemeta/models/__init__.py +++ b/telemeta/models/__init__.py @@ -34,7 +34,8 @@ # Author: Olivier Guilyardi # Guillaume Pellerin -from media import * + +from identifier import * from location import * from instrument import * from enum import * @@ -43,4 +44,10 @@ from query import * from dublincore import * from language import * from format import * - +from identifier import * +from fonds import * +from corpus import * +from collection import * +from item import * +from resource import * +from playlist import * diff --git a/telemeta/models/collection.py b/telemeta/models/collection.py new file mode 100644 index 00000000..e3d640c7 --- /dev/null +++ b/telemeta/models/collection.py @@ -0,0 +1,288 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2010 Samalyse SARL +# Copyright (C) 2010-2014 Parisson SARL + +# This software is a computer program whose purpose is to backup, analyse, +# transcode and stream any audio content with its metadata over a web frontend. + +# This software is governed by the CeCILL license under French law and +# abiding by the rules of distribution of free software. You can use, +# modify and/ or redistribute the software under the terms of the CeCILL +# license as circulated by CEA, CNRS and INRIA at the following URL +# "http://www.cecill.info". + +# As a counterpart to the access to the source code and rights to copy, +# modify and redistribute granted by the license, users are provided only +# with a limited warranty and the software's author, the holder of the +# economic rights, and the successive licensors have only limited +# liability. + +# In this respect, the user's attention is drawn to the risks associated +# with loading, using, modifying and/or developing or reproducing the +# software by the user in light of its specific status of free software, +# that may mean that it is complicated to manipulate, and that also +# therefore means that it is reserved for developers and experienced +# professionals having in-depth computer knowledge. Users are therefore +# encouraged to load and test the software's suitability as regards their +# requirements in conditions enabling the security of their systems and/or +# data to be ensured and, more generally, to use and operate it in the +# same conditions as regards security. + +# The fact that you are presently reading this means that you have had +# knowledge of the CeCILL license and that you accept its terms. +# +# Authors: Olivier Guilyardi +# David LIPSZYC +# Guillaume Pellerin + + +from __future__ import division +from django.utils.translation import ugettext_lazy as _ +from telemeta.models.core import * +from telemeta.models.query import * +from telemeta.models.identifier import * +from telemeta.models.resource import * + +# Special code regex of collections for the branch +collection_published_code_regex = getattr(settings, 'COLLECTION_PUBLISHED_CODE_REGEX', '[A-Za-z0-9._-]*') +collection_unpublished_code_regex = getattr(settings, 'COLLECTION_UNPUBLISHED_CODE_REGEX', '[A-Za-z0-9._-]*') + +# CREM +#collection_published_code_regex = 'CNRSMH_E_[0-9]{4}(?:_[0-9]{3}){2}' +#collection_unpublished_code_regex = 'CNRSMH_I_[0-9]{4}_[0-9]{3}' + +collection_code_regex = '(?:%s|%s)' % (collection_published_code_regex, + collection_unpublished_code_regex) + + +class MediaCollection(MediaResource): + "Describe a collection of items" + + element_type = 'collection' + + def is_valid_collection_code(value): + "Check if the collection code is well formed" + regex = '^' + collection_code_regex + '$' + if not re.match(regex, value): + raise ValidationError(u'%s is not a valid collection code' % value) + + # General informations + title = CharField(_('title'), required=True) + alt_title = CharField(_('original title / translation')) + creator = CharField(_('depositor / contributor'), help_text=_('First name, Last name ; First name, Last name')) + description = TextField(_('description')) + recording_context = WeakForeignKey('RecordingContext', related_name="collections", verbose_name=_('recording context')) + recorded_from_year = IntegerField(_('recording year (from)'), help_text=_('YYYY')) + recorded_to_year = IntegerField(_('recording year (until)'), help_text=_('YYYY')) + year_published = IntegerField(_('year published'), help_text=_('YYYY')) + public_access = CharField(_('access type'), choices=PUBLIC_ACCESS_CHOICES, max_length=16, default="metadata") + + # Geographic and cultural informations + # See "countries" and "ethnic_groups" methods below + + # Legal notices + collector = CharField(_('recordist'), help_text=_('First name, Last name ; First name, Last name')) + publisher = WeakForeignKey('Publisher', related_name="collections", verbose_name=_('publisher')) + publisher_collection = WeakForeignKey('PublisherCollection', related_name="collections", verbose_name=_('publisher collection')) + publisher_serial = CharField(_('publisher serial number')) + booklet_author = CharField(_('booklet author'), blank=True) + reference = CharField(_('publisher reference')) + external_references = TextField(_('bibliographic references')) + + auto_period_access = BooleanField(_('automatic access after a rolling period'), default=True) + legal_rights = WeakForeignKey('LegalRight', related_name="collections", verbose_name=_('legal rights')) + + # Archiving data + code = CharField(_('code'), unique=True, required=True, validators=[is_valid_collection_code]) + old_code = CharField(_('old code'), unique=False, null=True, blank=True) + acquisition_mode = WeakForeignKey('AcquisitionMode', related_name="collections", verbose_name=_('mode of acquisition')) + cnrs_contributor = CharField(_('CNRS depositor')) + copy_type = WeakForeignKey('CopyType', related_name="collections", verbose_name=_('copy type')) + metadata_author = WeakForeignKey('MetadataAuthor', related_name="collections", verbose_name=_('record author')) + booklet_description = TextField(_('related documentation')) + publishing_status = WeakForeignKey('PublishingStatus', related_name="collections", verbose_name=_('secondary edition')) + status = WeakForeignKey('Status', related_name="collections", verbose_name=_('collection status')) + alt_copies = TextField(_('copies')) + comment = TextField(_('comment')) + metadata_writer = WeakForeignKey('MetadataWriter', related_name="collections", verbose_name=_('record writer')) + archiver_notes = TextField(_('archiver notes')) + items_done = CharField(_('items finished')) + collector_is_creator = BooleanField(_('recordist identical to depositor')) + is_published = BooleanField(_('published')) + conservation_site = CharField(_('conservation site')) + + # Technical data + media_type = WeakForeignKey('MediaType', related_name="collections", verbose_name=_('media type')) + approx_duration = DurationField(_('estimated duration'), help_text='hh:mm:ss') + physical_items_num = IntegerField(_('number of components (medium / piece)')) + original_format = WeakForeignKey('OriginalFormat', related_name="collections", verbose_name=_('original format')) + physical_format = WeakForeignKey('PhysicalFormat', related_name="collections", verbose_name=_('archive format')) + ad_conversion = WeakForeignKey('AdConversion', related_name='collections', verbose_name=_('digitization')) + + # No more used old fields + alt_ids = CharField(_('copies (obsolete field)')) + travail = CharField(_('archiver notes (obsolete field)')) + + # All + objects = MediaCollectionManager() + + exclude = ['alt_ids', 'travail'] + + class Meta(MetaCore): + db_table = 'media_collections' + ordering = ['code'] + verbose_name = _('collection') + + def __unicode__(self): + return self.code + + def save(self, force_insert=False, force_update=False, user=None, code=None): + super(MediaCollection, self).save(force_insert, force_update) + + @property + def public_id(self): + return self.code + + @property + def has_mediafile(self): + "Tell wether this collection has any media files attached to its items" + items = self.items.all() + for item in items: + if item.file: + return True + return False + + def __name_cmp(self, obj1, obj2): + return unaccent_icmp(obj1.name, obj2.name) + + def countries(self): + "Return the countries of the items" + countries = [] + for item in self.items.filter(location__isnull=False): + for country in item.location.countries(): + if not country in countries: + countries.append(country) + countries.sort(self.__name_cmp) + return countries + countries.verbose_name = _("states / nations") + + def main_countries(self): + "Return the main countries of the items (no aliases or ancestors)" + countries = [] + for item in self.items.filter(location__isnull=False): + if not item.location in countries: + countries.append(item.location) + countries.sort(self.__name_cmp) + return countries + main_countries.verbose_name = _("states / nations") + + def ethnic_groups(self): + "Return the ethnic groups of the items" + groups = [] + items = self.items.all() + for item in items: + if item.ethnic_group and not item.ethnic_group in groups: + groups.append(item.ethnic_group) + + cmp = lambda a, b: unaccent_icmp(a.value, b.value) + groups.sort(cmp) + + return groups + ethnic_groups.verbose_name = _('populations / social groups') + + def computed_duration(self): + duration = Duration() + for item in self.items.all(): + duration += item.computed_duration() + return duration + computed_duration.verbose_name = _('computed duration') + + def computed_size(self): + "Return the total size of a collection" + size = 0 + for item in self.items.all(): + size += item.size() + return size + computed_size.verbose_name = _('collection size') + + def document_status(self): + if '_I_' in self.public_id: + return ugettext('Unpublished') + elif '_E_' in self.public_id: + return ugettext('Published') + else: + return '' + + def get_url(self): + return get_full_url(reverse('telemeta-collection-detail', kwargs={'public_id':self.pk})) + + def to_dict_with_more(self): + # metadata = model_to_dict(self, fields=[], exclude=self.exclude) + metadata = self.to_dict() + for key in self.exclude: + if key in metadata.keys(): + del metadata[key] + + metadata['url'] = get_full_url(reverse('telemeta-collection-detail', kwargs={'public_id':self.pk})) + metadata['doc_status'] = self.document_status() + metadata['countries'] = ';'.join([location.name for location in self.main_countries()]) + metadata['ethnic_groups'] = ';'.join([group.value for group in self.ethnic_groups()]) + metadata['last_modification_date'] = unicode(self.get_revision().time) + metadata['computed_duration'] = unicode(self.computed_duration()) + metadata['computed_size'] = unicode(self.computed_size()) + metadata['number_of_items'] = unicode(self.items.all().count()) + + i = 0 + for media in self.related.all(): + metadata['related_media_title' + '_' + str(i)] = media.title + if media.url: + tag = 'related_media_url' + '_' + str(i) + metadata[tag] = media.url + elif media.url: + metadata[tag] = get_full_url(reverse('telemeta-collection-related', + kwargs={'public_id': self.public_id, 'media_id': media.id})) + i += 1 + + # One ID only + identifiers = self.identifiers.all() + if identifiers: + identifier = identifiers[0] + metadata['identifier_id'] = identifier.identifier + metadata['identifier_type'] = identifier.type + metadata['identifier_date'] = unicode(identifier.date_last) + metadata['identifier_note'] = identifier.notes + + # All IDs + # i = 0 + # for indentifier in self.identifiers.all(): + # metadata['identifier' + '_' + str(i)] = identifier.identifier + # metadata['identifier_type' + '_' + str(i)] = identifier.type + # metadata['identifier_date_last' + '_' + str(i)] = unicode(identifier.date_last) + # metadata['identifier_notes' + '_' + str(i)] = identifier.notes + # i += 1 + + return metadata + + +class MediaCollectionRelated(MediaRelated): + "Collection related media" + + collection = ForeignKey('MediaCollection', related_name="related", verbose_name=_('collection')) + + class Meta(MetaCore): + db_table = 'media_collection_related' + verbose_name = _('collection related media') + verbose_name_plural = _('collection related media') + + +class MediaCollectionIdentifier(Identifier): + """Collection identifier""" + + collection = ForeignKey(MediaCollection, related_name="identifiers", verbose_name=_('collection')) + + class Meta(MetaCore): + db_table = 'media_collection_identifier' + verbose_name = _('collection identifier') + verbose_name_plural = _('collection identifiers') + unique_together = ('identifier', 'collection') + diff --git a/telemeta/models/core.py b/telemeta/models/core.py index c92ebf89..7500bf42 100644 --- a/telemeta/models/core.py +++ b/telemeta/models/core.py @@ -35,202 +35,41 @@ # Authors: Olivier Guilyardi # Guillaume Pellerin -__all__ = ['ModelCore', 'MetaCore', 'DurationField', 'Duration', 'WeakForeignKey', - 'EnhancedModel', 'CharField', 'TextField', 'IntegerField', 'BooleanField', - 'DateTimeField', 'FileField', 'ForeignKey', 'FloatField', 'DateField', - 'RequiredFieldError', 'CoreQuerySet', 'CoreManager', 'word_search_q'] -from django.core import exceptions +import datetime +import mimetypes +import re, os, random + from django import forms -from xml.dom.minidom import getDOMImplementation -from django.db.models.fields import FieldDoesNotExist -from django.db.models import Q +from django.conf import settings +from django.contrib.auth.models import User +from django.contrib.sites.models import Site +from django.core import exceptions +from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.core.urlresolvers import reverse, reverse_lazy from django.db import models -import datetime +from django.db.models import Q, URLField +from django.db.models.fields import FieldDoesNotExist +from django.forms.models import model_to_dict +from django.utils.translation import ugettext from django.utils.translation import ugettext_lazy as _ -import re -from django.core.exceptions import ObjectDoesNotExist -from south.modelsinspector import add_introspection_rules - - -class Duration(object): - """Represent a time duration""" - def __init__(self, *args, **kwargs): - if len(args) and isinstance(args[0], datetime.timedelta): - self._delta = datetime.timedelta(days=args[0].days, seconds=args[0].seconds) - else: - self._delta = datetime.timedelta(*args, **kwargs) - - def __decorate(self, method, other): - if isinstance(other, Duration): - res = method(other._delta) - else: - res = method(other) - if type(res) == datetime.timedelta: - return Duration(res) - - return res - - def __add__(self, other): - return self.__decorate(self._delta.__add__, other) - - def __nonzero__(self): - return self._delta.__nonzero__() - - def __str__(self): - hours = self._delta.days * 24 + self._delta.seconds / 3600 - minutes = (self._delta.seconds % 3600) / 60 - seconds = self._delta.seconds % 60 - - return "%.2d:%.2d:%.2d" % (hours, minutes, seconds) - - @staticmethod - def fromstr(str): - if not str: - return Duration() - - test = re.match('^([0-9]+)(?::([0-9]+)(?::([0-9]+))?)?$', str) - if test: - groups = test.groups() - try: - hours = minutes = seconds = 0 - if groups[0]: - hours = int(groups[0]) - if groups[1]: - minutes = int(groups[1]) - if groups[2]: - seconds = int(groups[2]) - - return Duration(hours=hours, minutes=minutes, seconds=seconds) - except TypeError: - print groups - raise - else: - raise ValueError("Malformed duration string: " + str) - - def as_seconds(self): - return self._delta.days * 24 * 3600 + self._delta.seconds - - -def normalize_field(args, default_value=None): - """Normalize field constructor arguments, so that the field is marked blank=True - and has a default value by default. - - This behaviour can be disabled by passing the special argument required=True. - - The default value can also be overriden with the default=value argument. - """ - required = False - if args.has_key('required'): - required = args['required'] - args.pop('required') - - args['blank'] = not required - - if not required: - if not args.has_key('default'): - if args.get('null'): - args['default'] = None - elif default_value is not None: - args['default'] = default_value - - return args - - -class DurationField(models.Field): - """Duration Django model field based on Django TimeField. - Essentially the same as a TimeField, but with values over 24h allowed. - - The constructor arguments are also normalized with normalize_field(). - """ - - description = _("Duration") - - __metaclass__ = models.SubfieldBase - - default_error_messages = { - 'invalid': _('Enter a valid duration in HH:MM[:ss] format.'), - } - - def __init__(self, *args, **kwargs): - super(DurationField, self).__init__(*args, **normalize_field(kwargs, '0')) - - def db_type(self): - return 'int' - - def to_python(self, value): - if value is None: - return None - if isinstance(value, int) or isinstance(value, long): - return Duration(seconds=value) - if isinstance(value, datetime.time): - return Duration(hours=value.hour, minutes=value.minute, seconds=value.second) - if isinstance(value, datetime.datetime): - # Not usually a good idea to pass in a datetime here (it loses - # information), but this can be a side-effect of interacting with a - # database backend (e.g. Oracle), so we'll be accommodating. - return self.to_python(value.time()) - else: - value = str(value) - try: - return Duration.fromstr(value) - except ValueError: - raise exceptions.ValidationError(self.error_messages['invalid']) - - def get_prep_value(self, value): - return self.to_python(value) - - def get_db_prep_value(self, value, connection=None, prepared=False): - # Casts times into the format expected by the backend - try: - return value.as_seconds() - except: - return value - - def value_to_string(self, obj): - val = self._get_val_from_obj(obj) - if val is None: - data = '' - else: - data = unicode(val) - return data - - def formfield(self, **kwargs): - defaults = {'form_class': forms.CharField} - defaults.update(kwargs) - return super(DurationField, self).formfield(**defaults) - - -class ForeignKey(models.ForeignKey): - """The constructor arguments of this ForeignKey are normalized - with normalize_field(), however the field is marked required by default - unless it is allowed to be null.""" +from telemeta.models.utils import * +from telemeta.models.fields import * +from telemeta.util.kdenlive.session import * +from telemeta.util.unaccent import unaccent_icmp +from xml.dom.minidom import getDOMImplementation - def __init__(self, to, **kwargs): - if not kwargs.has_key('required'): - if not kwargs.get('null'): - kwargs['required'] = True - super(ForeignKey, self).__init__(to, **normalize_field(kwargs, 0)) +PUBLIC_ACCESS_CHOICES = (('none', _('none')), ('metadata', _('metadata')), + ('mixed', _('mixed')), ('full', _('full'))) +mimetypes.add_type('video/webm','.webm') -class WeakForeignKey(ForeignKey): - """A weak foreign key is the same as foreign key but without cascading - delete. Instead the reference is set to null when the referenced record - get deleted. This emulates the ON DELETE SET NULL sql behaviour. +app_name = 'telemeta' - This field is automatically allowed to be null, there's no need to pass - null=True. +strict_code = getattr(settings, 'TELEMETA_STRICT_CODE', False) - The constructor arguments are normalized with normalize_field() by the - parent ForeignKey - Warning: must be used in conjunction with EnhancedQuerySet, EnhancedManager, - and EnhancedModel - """ - def __init__(self, to, **kwargs): - kwargs['null'] = True - super(WeakForeignKey, self).__init__(to, **kwargs) class EnhancedQuerySet(models.query.QuerySet): @@ -278,79 +117,6 @@ class EnhancedModel(models.Model): abstract = True -class CharField(models.CharField): - """This is a CharField with a default max_length of 250. - - The arguments are also normalized with normalize_field()""" - - def __init__(self, *args, **kwargs): - if not kwargs.has_key('max_length'): - kwargs['max_length'] = 250 - super(CharField, self).__init__(*args, **normalize_field(kwargs, '')) - - -class IntegerField(models.IntegerField): - """IntegerField normalized with normalize_field()""" - - def __init__(self, *args, **kwargs): - super(IntegerField, self).__init__(*args, **normalize_field(kwargs, 0)) - - -class BooleanField(models.BooleanField): - """BooleanField normalized with normalize_field()""" - - def __init__(self, *args, **kwargs): - super(BooleanField, self).__init__(*args, **normalize_field(kwargs, False)) - - -class TextField(models.TextField): - """TextField normalized with normalize_field()""" - - def __init__(self, *args, **kwargs): - super(TextField, self).__init__(*args, **normalize_field(kwargs, '')) - - -class DateTimeField(models.DateTimeField): - """DateTimeField normalized with normalize_field(). This field is allowed to - be null by default unless null=False is passed""" - - def __init__(self, *args, **kwargs): - if not kwargs.has_key('null'): - kwargs['null'] = True - super(DateTimeField, self).__init__(*args, **normalize_field(kwargs)) - - -class FileField(models.FileField): - """FileField normalized with normalize_field()""" - - def __init__(self, *args, **kwargs): - super(FileField, self).__init__(*args, **normalize_field(kwargs, '')) - - -class FloatField(models.FloatField): - """FloatField normalized with normalize_field()""" - - def __init__(self, *args, **kwargs): - super(FloatField, self).__init__(*args, **normalize_field(kwargs, 0)) - - -class DateField(models.DateField): - """DateField normalized with normalize_field(). This field is allowed to - be null by default unless null=False is passed""" - - def __init__(self, *args, **kwargs): - if not kwargs.has_key('null'): - kwargs['null'] = True - super(DateField, self).__init__(*args, **normalize_field(kwargs)) - - -class RequiredFieldError(Exception): - def __init__(self, model, field): - self.model = model - self.field = field - super(Exception, self).__init__('%s.%s is required' % (model._meta.object_name, field.name)) - - class ModelCore(EnhancedModel): @classmethod @@ -435,16 +201,6 @@ class MetaCore: app_label = 'telemeta' -def word_search_q(field, pattern): - words = re.split("[ .*-]+", pattern) - q = Q() - for w in words: - if len(w) >= 3: - kwargs = {field + '__icontains': w} - q &= Q(**kwargs) - - return q - class CoreQuerySet(EnhancedQuerySet): "Base class for all query sets" @@ -496,16 +252,3 @@ class CoreManager(EnhancedManager): return super(CoreManager, self).get(**kwargs) -# South introspection rules -add_introspection_rules([], ["^telemeta\.models\.core\.CharField"]) -add_introspection_rules([], ["^telemeta\.models\.core\.TextField"]) -add_introspection_rules([], ["^telemeta\.models\.core\.FileField"]) -add_introspection_rules([], ["^telemeta\.models\.core\.IntegerField"]) -add_introspection_rules([], ["^telemeta\.models\.core\.BooleanField"]) -add_introspection_rules([], ["^telemeta\.models\.core\.DateTimeField"]) -add_introspection_rules([], ["^telemeta\.models\.core\.DateField"]) -add_introspection_rules([], ["^telemeta\.models\.core\.FloatField"]) -add_introspection_rules([], ["^telemeta\.models\.core\.DurationField"]) -add_introspection_rules([], ["^telemeta\.models\.core\.ForeignKey"]) -add_introspection_rules([], ["^telemeta\.models\.core\.WeakForeignKey"]) - diff --git a/telemeta/models/corpus.py b/telemeta/models/corpus.py new file mode 100644 index 00000000..067b82a5 --- /dev/null +++ b/telemeta/models/corpus.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2010 Samalyse SARL +# Copyright (C) 2010-2014 Parisson SARL + +# This software is a computer program whose purpose is to backup, analyse, +# transcode and stream any audio content with its metadata over a web frontend. + +# This software is governed by the CeCILL license under French law and +# abiding by the rules of distribution of free software. You can use, +# modify and/ or redistribute the software under the terms of the CeCILL +# license as circulated by CEA, CNRS and INRIA at the following URL +# "http://www.cecill.info". + +# As a counterpart to the access to the source code and rights to copy, +# modify and redistribute granted by the license, users are provided only +# with a limited warranty and the software's author, the holder of the +# economic rights, and the successive licensors have only limited +# liability. + +# In this respect, the user's attention is drawn to the risks associated +# with loading, using, modifying and/or developing or reproducing the +# software by the user in light of its specific status of free software, +# that may mean that it is complicated to manipulate, and that also +# therefore means that it is reserved for developers and experienced +# professionals having in-depth computer knowledge. Users are therefore +# encouraged to load and test the software's suitability as regards their +# requirements in conditions enabling the security of their systems and/or +# data to be ensured and, more generally, to use and operate it in the +# same conditions as regards security. + +# The fact that you are presently reading this means that you have had +# knowledge of the CeCILL license and that you accept its terms. +# +# Authors: Olivier Guilyardi +# David LIPSZYC +# Guillaume Pellerin + +from __future__ import division +from django.utils.translation import ugettext_lazy as _ +from telemeta.models.core import * +from telemeta.models.resource import * +from telemeta.models.collection import * + + +class MediaCorpus(MediaBaseResource): + "Describe a corpus" + + element_type = 'corpus' + children_type = 'collections' + + children = models.ManyToManyField(MediaCollection, related_name="corpus", + verbose_name=_('collections'), blank=True, null=True) + recorded_from_year = IntegerField(_('recording year (from)'), help_text=_('YYYY')) + recorded_to_year = IntegerField(_('recording year (until)'), help_text=_('YYYY')) + + objects = MediaCorpusManager() + + @property + def public_id(self): + return self.code + + @property + def has_mediafile(self): + for child in self.children.all(): + if child.has_mediafile: + return True + return False + + def computed_duration(self): + duration = Duration() + for child in self.children.all(): + duration += child.computed_duration() + return duration + computed_duration.verbose_name = _('total available duration') + + class Meta(MetaCore): + db_table = 'media_corpus' + verbose_name = _('corpus') + verbose_name_plural = _('corpus') + ordering = ['code'] + + +class MediaCorpusRelated(MediaRelated): + "Corpus related media" + + resource = ForeignKey(MediaCorpus, related_name="related", verbose_name=_('corpus')) + + class Meta(MetaCore): + db_table = 'media_corpus_related' + verbose_name = _('corpus related media') + verbose_name_plural = _('corpus related media') + diff --git a/telemeta/models/dublincore.py b/telemeta/models/dublincore.py index 504121c0..964162d7 100644 --- a/telemeta/models/dublincore.py +++ b/telemeta/models/dublincore.py @@ -32,8 +32,9 @@ # # Author: Olivier Guilyardi -from telemeta.models.core import Duration -from telemeta.models.media import * +from telemeta.models.core import * +from telemeta.models.item import * +from telemeta.models.collection import * from django.contrib.sites.models import Site from django.conf import settings diff --git a/telemeta/models/fields.py b/telemeta/models/fields.py new file mode 100644 index 00000000..9752e2ab --- /dev/null +++ b/telemeta/models/fields.py @@ -0,0 +1,319 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007-2010 Samalyse SARL +# Copyright (C) 2010-2015 Parisson SARL +# +# This software is a computer program whose purpose is to backup, analyse, +# transcode and stream any audio content with its metadata over a web frontend. +# +# This software is governed by the CeCILL license under French law and +# abiding by the rules of distribution of free software. You can use, +# modify and/ or redistribute the software under the terms of the CeCILL +# license as circulated by CEA, CNRS and INRIA at the following URL +# "http://www.cecill.info". +# +# As a counterpart to the access to the source code and rights to copy, +# modify and redistribute granted by the license, users are provided only +# with a limited warranty and the software's author, the holder of the +# economic rights, and the successive licensors have only limited +# liability. +# +# In this respect, the user's attention is drawn to the risks associated +# with loading, using, modifying and/or developing or reproducing the +# software by the user in light of its specific status of free software, +# that may mean that it is complicated to manipulate, and that also +# therefore means that it is reserved for developers and experienced +# professionals having in-depth computer knowledge. Users are therefore +# encouraged to load and test the software's suitability as regards their +# requirements in conditions enabling the security of their systems and/or +# data to be ensured and, more generally, to use and operate it in the +# same conditions as regards security. +# +# The fact that you are presently reading this means that you have had +# knowledge of the CeCILL license and that you accept its terms. +# +# Authors: Olivier Guilyardi +# Guillaume Pellerin + +from __future__ import division + +__all__ = ['DurationField', 'Duration', 'WeakForeignKey', + 'CharField', 'TextField', 'IntegerField', 'BooleanField', + 'DateTimeField', 'FileField', 'ForeignKey', 'FloatField', 'DateField', + 'RequiredFieldError',] + +import datetime +from django import forms +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from south.modelsinspector import add_introspection_rules + + +class Duration(object): + """Represent a time duration""" + def __init__(self, *args, **kwargs): + if len(args) and isinstance(args[0], datetime.timedelta): + self._delta = datetime.timedelta(days=args[0].days, seconds=args[0].seconds) + else: + self._delta = datetime.timedelta(*args, **kwargs) + + def __decorate(self, method, other): + if isinstance(other, Duration): + res = method(other._delta) + else: + res = method(other) + if type(res) == datetime.timedelta: + return Duration(res) + + return res + + def __add__(self, other): + return self.__decorate(self._delta.__add__, other) + + def __nonzero__(self): + return self._delta.__nonzero__() + + def __str__(self): + hours = self._delta.days * 24 + self._delta.seconds / 3600 + minutes = (self._delta.seconds % 3600) / 60 + seconds = self._delta.seconds % 60 + + return "%.2d:%.2d:%.2d" % (hours, minutes, seconds) + + @staticmethod + def fromstr(str): + if not str: + return Duration() + + test = re.match('^([0-9]+)(?::([0-9]+)(?::([0-9]+))?)?$', str) + if test: + groups = test.groups() + try: + hours = minutes = seconds = 0 + if groups[0]: + hours = int(groups[0]) + if groups[1]: + minutes = int(groups[1]) + if groups[2]: + seconds = int(groups[2]) + + return Duration(hours=hours, minutes=minutes, seconds=seconds) + except TypeError: + print groups + raise + else: + raise ValueError("Malformed duration string: " + str) + + def as_seconds(self): + return self._delta.days * 24 * 3600 + self._delta.seconds + + +def normalize_field(args, default_value=None): + """Normalize field constructor arguments, so that the field is marked blank=True + and has a default value by default. + + This behaviour can be disabled by passing the special argument required=True. + + The default value can also be overriden with the default=value argument. + """ + required = False + if args.has_key('required'): + required = args['required'] + args.pop('required') + + args['blank'] = not required + + if not required: + if not args.has_key('default'): + if args.get('null'): + args['default'] = None + elif default_value is not None: + args['default'] = default_value + + return args + + +class DurationField(models.Field): + """Duration Django model field based on Django TimeField. + Essentially the same as a TimeField, but with values over 24h allowed. + + The constructor arguments are also normalized with normalize_field(). + """ + + description = _("Duration") + + __metaclass__ = models.SubfieldBase + + default_error_messages = { + 'invalid': _('Enter a valid duration in HH:MM[:ss] format.'), + } + + def __init__(self, *args, **kwargs): + super(DurationField, self).__init__(*args, **normalize_field(kwargs, '0')) + + def db_type(self): + return 'int' + + def to_python(self, value): + if value is None: + return None + if isinstance(value, int) or isinstance(value, long): + return Duration(seconds=value) + if isinstance(value, datetime.time): + return Duration(hours=value.hour, minutes=value.minute, seconds=value.second) + if isinstance(value, datetime.datetime): + # Not usually a good idea to pass in a datetime here (it loses + # information), but this can be a side-effect of interacting with a + # database backend (e.g. Oracle), so we'll be accommodating. + return self.to_python(value.time()) + else: + value = str(value) + try: + return Duration.fromstr(value) + except ValueError: + raise exceptions.ValidationError(self.error_messages['invalid']) + + def get_prep_value(self, value): + return self.to_python(value) + + def get_db_prep_value(self, value, connection=None, prepared=False): + # Casts times into the format expected by the backend + try: + return value.as_seconds() + except: + return value + + def value_to_string(self, obj): + val = self._get_val_from_obj(obj) + if val is None: + data = '' + else: + data = unicode(val) + return data + + def formfield(self, **kwargs): + defaults = {'form_class': forms.CharField} + defaults.update(kwargs) + return super(DurationField, self).formfield(**defaults) + + +class ForeignKey(models.ForeignKey): + """The constructor arguments of this ForeignKey are normalized + with normalize_field(), however the field is marked required by default + unless it is allowed to be null.""" + + def __init__(self, to, **kwargs): + if not kwargs.has_key('required'): + if not kwargs.get('null'): + kwargs['required'] = True + + super(ForeignKey, self).__init__(to, **normalize_field(kwargs, 0)) + + +class WeakForeignKey(ForeignKey): + """A weak foreign key is the same as foreign key but without cascading + delete. Instead the reference is set to null when the referenced record + get deleted. This emulates the ON DELETE SET NULL sql behaviour. + + This field is automatically allowed to be null, there's no need to pass + null=True. + + The constructor arguments are normalized with normalize_field() by the + parent ForeignKey + + Warning: must be used in conjunction with EnhancedQuerySet, EnhancedManager, + and EnhancedModel + """ + def __init__(self, to, **kwargs): + kwargs['null'] = True + super(WeakForeignKey, self).__init__(to, **kwargs) + + + +class CharField(models.CharField): + """This is a CharField with a default max_length of 250. + + The arguments are also normalized with normalize_field()""" + + def __init__(self, *args, **kwargs): + if not kwargs.has_key('max_length'): + kwargs['max_length'] = 250 + super(CharField, self).__init__(*args, **normalize_field(kwargs, '')) + + +class IntegerField(models.IntegerField): + """IntegerField normalized with normalize_field()""" + + def __init__(self, *args, **kwargs): + super(IntegerField, self).__init__(*args, **normalize_field(kwargs, 0)) + + +class BooleanField(models.BooleanField): + """BooleanField normalized with normalize_field()""" + + def __init__(self, *args, **kwargs): + super(BooleanField, self).__init__(*args, **normalize_field(kwargs, False)) + + +class TextField(models.TextField): + """TextField normalized with normalize_field()""" + + def __init__(self, *args, **kwargs): + super(TextField, self).__init__(*args, **normalize_field(kwargs, '')) + + +class DateTimeField(models.DateTimeField): + """DateTimeField normalized with normalize_field(). This field is allowed to + be null by default unless null=False is passed""" + + def __init__(self, *args, **kwargs): + if not kwargs.has_key('null'): + kwargs['null'] = True + super(DateTimeField, self).__init__(*args, **normalize_field(kwargs)) + + +class FileField(models.FileField): + """FileField normalized with normalize_field()""" + + def __init__(self, *args, **kwargs): + super(FileField, self).__init__(*args, **normalize_field(kwargs, '')) + + +class FloatField(models.FloatField): + """FloatField normalized with normalize_field()""" + + def __init__(self, *args, **kwargs): + super(FloatField, self).__init__(*args, **normalize_field(kwargs, 0)) + + +class DateField(models.DateField): + """DateField normalized with normalize_field(). This field is allowed to + be null by default unless null=False is passed""" + + def __init__(self, *args, **kwargs): + if not kwargs.has_key('null'): + kwargs['null'] = True + super(DateField, self).__init__(*args, **normalize_field(kwargs)) + + +class RequiredFieldError(Exception): + def __init__(self, model, field): + self.model = model + self.field = field + super(Exception, self).__init__('%s.%s is required' % (model._meta.object_name, field.name)) + + + +# South introspection rules +add_introspection_rules([], ["^telemeta\.models\.core\.CharField"]) +add_introspection_rules([], ["^telemeta\.models\.core\.TextField"]) +add_introspection_rules([], ["^telemeta\.models\.core\.FileField"]) +add_introspection_rules([], ["^telemeta\.models\.core\.IntegerField"]) +add_introspection_rules([], ["^telemeta\.models\.core\.BooleanField"]) +add_introspection_rules([], ["^telemeta\.models\.core\.DateTimeField"]) +add_introspection_rules([], ["^telemeta\.models\.core\.DateField"]) +add_introspection_rules([], ["^telemeta\.models\.core\.FloatField"]) +add_introspection_rules([], ["^telemeta\.models\.core\.DurationField"]) +add_introspection_rules([], ["^telemeta\.models\.core\.ForeignKey"]) +add_introspection_rules([], ["^telemeta\.models\.core\.WeakForeignKey"]) + diff --git a/telemeta/models/fonds.py b/telemeta/models/fonds.py new file mode 100644 index 00000000..69faa6bd --- /dev/null +++ b/telemeta/models/fonds.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2010 Samalyse SARL +# Copyright (C) 2010-2014 Parisson SARL + +# This software is a computer program whose purpose is to backup, analyse, +# transcode and stream any audio content with its metadata over a web frontend. + +# This software is governed by the CeCILL license under French law and +# abiding by the rules of distribution of free software. You can use, +# modify and/ or redistribute the software under the terms of the CeCILL +# license as circulated by CEA, CNRS and INRIA at the following URL +# "http://www.cecill.info". + +# As a counterpart to the access to the source code and rights to copy, +# modify and redistribute granted by the license, users are provided only +# with a limited warranty and the software's author, the holder of the +# economic rights, and the successive licensors have only limited +# liability. + +# In this respect, the user's attention is drawn to the risks associated +# with loading, using, modifying and/or developing or reproducing the +# software by the user in light of its specific status of free software, +# that may mean that it is complicated to manipulate, and that also +# therefore means that it is reserved for developers and experienced +# professionals having in-depth computer knowledge. Users are therefore +# encouraged to load and test the software's suitability as regards their +# requirements in conditions enabling the security of their systems and/or +# data to be ensured and, more generally, to use and operate it in the +# same conditions as regards security. + +# The fact that you are presently reading this means that you have had +# knowledge of the CeCILL license and that you accept its terms. +# +# Authors: Olivier Guilyardi +# David LIPSZYC +# Guillaume Pellerin + +from __future__ import division +from django.utils.translation import ugettext_lazy as _ +from telemeta.models.core import * +from telemeta.models.resource import * +from telemeta.models.corpus import * + + +class MediaFonds(MediaBaseResource): + "Describe fonds" + + element_type = 'fonds' + children_type = 'corpus' + + children = models.ManyToManyField(MediaCorpus, related_name="fonds", + verbose_name=_('corpus'), blank=True, null=True) + + objects = MediaFondsManager() + + @property + def public_id(self): + return self.code + + @property + def has_mediafile(self): + for child in self.children.all(): + if child.has_mediafile: + return True + return False + + def computed_duration(self): + duration = Duration() + for child in self.children.all(): + duration += child.computed_duration() + return duration + computed_duration.verbose_name = _('total available duration') + + class Meta(MetaCore): + db_table = 'media_fonds' + verbose_name = _('fonds') + verbose_name_plural = _('fonds') + ordering = ['code'] + + +class MediaFondsRelated(MediaRelated): + "Fonds related media" + + resource = ForeignKey(MediaFonds, related_name="related", verbose_name=_('fonds')) + + class Meta(MetaCore): + db_table = 'media_fonds_related' + verbose_name = _('fonds related media') + verbose_name_plural = _('fonds related media') + + diff --git a/telemeta/models/format.py b/telemeta/models/format.py index e4b4a991..dc050995 100644 --- a/telemeta/models/format.py +++ b/telemeta/models/format.py @@ -40,13 +40,8 @@ from django.utils.translation import ugettext_lazy as _ from django.core.exceptions import ValidationError from telemeta.models.core import * from telemeta.util.unaccent import unaccent_icmp -from telemeta.models.location import * -from telemeta.models.system import * -from telemeta.models.query import * -from telemeta.models.instrument import * from telemeta.models.enum import * from telemeta.models.language import * -from telemeta.models.media import * from django.db import models diff --git a/telemeta/models/identifier.py b/telemeta/models/identifier.py new file mode 100644 index 00000000..47fc3155 --- /dev/null +++ b/telemeta/models/identifier.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2010 Samalyse SARL +# Copyright (C) 2010-2014 Parisson SARL + +# This software is a computer program whose purpose is to backup, analyse, +# transcode and stream any audio content with its metadata over a web frontend. + +# This software is governed by the CeCILL license under French law and +# abiding by the rules of distribution of free software. You can use, +# modify and/ or redistribute the software under the terms of the CeCILL +# license as circulated by CEA, CNRS and INRIA at the following URL +# "http://www.cecill.info". + +# As a counterpart to the access to the source code and rights to copy, +# modify and redistribute granted by the license, users are provided only +# with a limited warranty and the software's author, the holder of the +# economic rights, and the successive licensors have only limited +# liability. + +# In this respect, the user's attention is drawn to the risks associated +# with loading, using, modifying and/or developing or reproducing the +# software by the user in light of its specific status of free software, +# that may mean that it is complicated to manipulate, and that also +# therefore means that it is reserved for developers and experienced +# professionals having in-depth computer knowledge. Users are therefore +# encouraged to load and test the software's suitability as regards their +# requirements in conditions enabling the security of their systems and/or +# data to be ensured and, more generally, to use and operate it in the +# same conditions as regards security. + +# The fact that you are presently reading this means that you have had +# knowledge of the CeCILL license and that you accept its terms. +# +# Authors: Olivier Guilyardi +# David LIPSZYC +# Guillaume Pellerin + + +from telemeta.models.core import * +from telemeta.models.fields import * +from django.utils.translation import ugettext_lazy as _ + + +class Identifier(ModelCore): + """Resource identifier""" + + identifier = CharField(_('identifier'), max_length=255, blank=True, unique=True) + type = WeakForeignKey('IdentifierType', verbose_name=_('type')) + date_add = DateTimeField(_('date added'), auto_now_add=True) + date_first = DateTimeField(_('date of first attribution')) + date_last = DateTimeField(_('date of last attribution')) + date_modified = DateTimeField(_('date modified'), auto_now=True) + notes = TextField(_('notes')) + + class Meta(MetaCore): + abstract = True + ordering = ['-date_last'] diff --git a/telemeta/models/instrument.py b/telemeta/models/instrument.py index b93db1d8..8de13fdf 100644 --- a/telemeta/models/instrument.py +++ b/telemeta/models/instrument.py @@ -38,6 +38,7 @@ from telemeta.models.core import * from django.utils.translation import ugettext_lazy as _ + class Instrument(ModelCore): "Instrument used in the item" name = CharField(_('name'), required=True) diff --git a/telemeta/models/item.py b/telemeta/models/item.py new file mode 100644 index 00000000..db2417af --- /dev/null +++ b/telemeta/models/item.py @@ -0,0 +1,492 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2010 Samalyse SARL +# Copyright (C) 2010-2014 Parisson SARL + +# This software is a computer program whose purpose is to backup, analyse, +# transcode and stream any audio content with its metadata over a web frontend. + +# This software is governed by the CeCILL license under French law and +# abiding by the rules of distribution of free software. You can use, +# modify and/ or redistribute the software under the terms of the CeCILL +# license as circulated by CEA, CNRS and INRIA at the following URL +# "http://www.cecill.info". + +# As a counterpart to the access to the source code and rights to copy, +# modify and redistribute granted by the license, users are provided only +# with a limited warranty and the software's author, the holder of the +# economic rights, and the successive licensors have only limited +# liability. + +# In this respect, the user's attention is drawn to the risks associated +# with loading, using, modifying and/or developing or reproducing the +# software by the user in light of its specific status of free software, +# that may mean that it is complicated to manipulate, and that also +# therefore means that it is reserved for developers and experienced +# professionals having in-depth computer knowledge. Users are therefore +# encouraged to load and test the software's suitability as regards their +# requirements in conditions enabling the security of their systems and/or +# data to be ensured and, more generally, to use and operate it in the +# same conditions as regards security. + +# The fact that you are presently reading this means that you have had +# knowledge of the CeCILL license and that you accept its terms. +# +# Authors: Olivier Guilyardi +# David LIPSZYC +# Guillaume Pellerin + +from __future__ import division +from django.utils.translation import ugettext_lazy as _ +from telemeta.models.core import * +from telemeta.models.resource import * +from telemeta.models.query import * +from telemeta.models.identifier import * +from telemeta.models.resource import * +from telemeta.models.enum import * + + +item_published_code_regex = getattr(settings, 'ITEM_PUBLISHED_CODE_REGEX', '[A-Za-z0-9._-]*') +item_unpublished_code_regex = getattr(settings, 'ITEM_UNPUBLISHED_CODE_REGEX', '[A-Za-z0-9._-]*') + +# CREM +# item_published_code_regex = collection_published_code_regex + '(?:_[0-9]{2,3}){1,2}' +# item_unpublished_code_regex = collection_unpublished_code_regex + '_[0-9]{2,3}(?:_[0-9]{2,3}){0,2}' + +item_code_regex = '(?:%s|%s)' % (item_published_code_regex, item_unpublished_code_regex) + +ITEM_PUBLIC_ACCESS_CHOICES = (('none', _('none')), ('metadata', _('metadata')), + ('full', _('full'))) + +ITEM_TRANSODING_STATUS = ((0, _('broken')), (1, _('pending')), (2, _('processing')), + (3, _('done')), (5, _('ready'))) + + +class MediaItem(MediaResource): + "Describe an item" + + element_type = 'item' + + # Main Informations + title = CharField(_('title')) + alt_title = CharField(_('original title / translation')) + collector = CharField(_('collector'), help_text=_('First name, Last name ; First name, Last name')) + collection = ForeignKey('MediaCollection', related_name="items", verbose_name=_('collection')) + recorded_from_date = DateField(_('recording date (from)'), help_text=_('YYYY-MM-DD')) + recorded_to_date = DateField(_('recording date (until)'), help_text=_('YYYY-MM-DD')) + public_access = CharField(_('access type'), choices=ITEM_PUBLIC_ACCESS_CHOICES, max_length=16, default="metadata") + + # Geographic and cultural informations + location = WeakForeignKey('Location', verbose_name=_('location')) + location_comment = CharField(_('location details')) + cultural_area = CharField(_('cultural area')) + language = CharField(_('language')) + language_iso = ForeignKey('Language', related_name="items", verbose_name=_('Language (ISO norm)'), blank=True, null=True, on_delete=models.SET_NULL) + ethnic_group = WeakForeignKey('EthnicGroup', related_name="items", verbose_name=_('population / social group')) + context_comment = TextField(_('Ethnographic context')) + + # Musical informations + moda_execut = CharField(_('implementing rules')) + vernacular_style = WeakForeignKey('VernacularStyle', related_name="items", verbose_name=_('vernacular style')) + generic_style = WeakForeignKey('GenericStyle', related_name="items", verbose_name=_('generic style')) + author = CharField(_('author / compositor'), help_text=_('First name, Last name ; First name, Last name')) + + # Legal mentions + organization = WeakForeignKey('Organization', verbose_name=_('organization')) + depositor = CharField(_('depositor')) + rights = WeakForeignKey('Rights', verbose_name=_('rights')) + + # Archiving data + code = CharField(_('code'), unique=True, blank=True, required=True, help_text=_('CollectionCode_ItemCode')) + old_code = CharField(_('original code'), unique=False, blank=True) + track = CharField(_('item number')) + collector_selection = CharField(_('collector selection')) + collector_from_collection = BooleanField(_('collector as in collection')) + creator_reference = CharField(_('creator reference')) + external_references = TextField(_('published references')) + auto_period_access = BooleanField(_('automatic access after a rolling period'), default=True) + comment = TextField(_('remarks')) + + # Technical data + media_type = WeakForeignKey('MediaType', related_name="items", verbose_name=_('media type')) + approx_duration = DurationField(_('approximative duration'), blank=True, help_text=_('hh:mm:ss')) + mimetype = CharField(_('mime type'), max_length=255, blank=True) + file = FileField(_('file'), upload_to='items/%Y/%m/%d', db_column="filename", max_length=1024) + url = URLField(_('URL'), max_length=512, blank=True) + + # LAM + recordist = CharField(_('recordist')) + digitalist = CharField(_('digitalist')) + digitization_date = DateField(_('digitization date')) + publishing_date = DateField(_('publishing date')) + scientist = CharField(_('scientist'), help_text=_('First name, Last name ; First name, Last name')) + topic = WeakForeignKey('Topic', verbose_name=_('topic')) + summary = TextField(_('summary')) + contributor = CharField(_('contributor')) + + # Manager + objects = MediaItemManager() + + exclude = ['copied_from_item', 'mimetype', 'url', + 'organization', 'depositor', 'rights', + 'recordist', 'digitalist', 'digitization_date', + 'publishing_date', 'scientist', 'topic', + 'summary', 'contributor', ] + + def keywords(self): + return ContextKeyword.objects.filter(item_relations__item = self) + keywords.verbose_name = _('keywords') + + @property + def public_id(self): + if self.code: + return self.code + return str(self.id) + + @property + def mime_type(self): + if not self.mimetype: + if self.file: + if os.path.exists(self.file.path): + self.mimetype = mimetypes.guess_type(self.file.path)[0] + self.save() + return self.mimetype + else: + return 'none' + else: + return 'none' + else: + return _('none') + + + class Meta(MetaCore): + db_table = 'media_items' + permissions = (("can_play_all_items", "Can play all media items"), + ("can_download_all_items", "Can download all media items"), ) + verbose_name = _('item') + + def is_valid_code(self, code): + "Check if the item code is well formed" + if not re.match('^' + self.collection.code, self.code): + return False + if self.collection.is_published: + regex = '^' + item_published_code_regex + '$' + else: + regex = '^' + item_unpublished_code_regex + '$' + if re.match(regex, code): + return True + return False + + def clean(self): + if strict_code: + if self.code and not self.is_valid_code(self.code): + raise ValidationError("%s is not a valid item code for collection %s" + % (self.code, self.collection.code)) + + def save(self, force_insert=False, force_update=False): + super(MediaItem, self).save(force_insert, force_update) + + def computed_duration(self): + "Tell the length in seconds of this item media data" + return self.approx_duration + + computed_duration.verbose_name = _('computed duration') + + def __unicode__(self): + if self.title and not re.match('^ *N *$', self.title): + title = self.title + else: + title = unicode(self.collection.title) + if self.track: + title += ' ' + self.track + return title + + def get_source(self): + source = None + if self.file and os.path.exists(self.file.path): + source = self.file.path + elif self.url: + source = self.url + return source + + @property + def instruments(self): + "Return the instruments of the item" + instruments = [] + performances = MediaItemPerformance.objects.filter(media_item=self) + for performance in performances: + instrument = performance.instrument + alias = performance.alias + if not instrument in instruments: + instruments.append(instrument) + if not alias in instruments: + instruments.append(alias) + + instruments.sort(self.__name_cmp) + return instruments + + instruments.verbose_name = _("instruments") + + def size(self): + if self.file and os.path.exists(self.file.path): + return self.file.size + else: + return 0 + size.verbose_name = _('item size') + + def get_url(self): + return get_full_url(reverse('telemeta-item-detail', kwargs={'public_id':self.pk})) + + def to_dict_with_more(self): + # metadata = model_to_dict(self, fields=[], exclude=self.exclude) + metadata = self.to_dict() + for key in self.exclude: + if key in metadata.keys(): + del metadata[key] + + metadata['url'] = self.get_url() + metadata['last_modification_date'] = unicode(self.get_revision().time) + metadata['collection'] = self.collection.get_url() + + keywords = [] + for keyword in self.keywords(): + keywords.append(keyword.value) + metadata['keywords'] = ';'.join(keywords) + + i = 0 + for media in self.related.all(): + if media.title: + tag = 'related_media_title' + '_' + str(i) + metadata[tag] = media.title + else: + metadata[tag] = '' + if media.url: + tag = 'related_media_url' + '_' + str(i) + metadata[tag] = media.url + elif media.url: + metadata[tag] = get_full_url(reverse('telemeta-collection-related', + kwargs={'public_id': self.public_id, 'media_id': media.id})) + i += 1 + + + instruments = [] + instrument_vernacular_names = [] + performers = [] + + for performance in self.performances.all(): + if performance.instrument: + instruments.append(performance.instrument.name) + if performance.alias: + instrument_vernacular_names.append(performance.alias.name) + if performance.musicians: + performers.append(performance.musicians.replace(' et ', ';')) + + metadata['instruments'] = ';'.join(instruments) + metadata['instrument_vernacular_names'] = ';'.join(instrument_vernacular_names) + metadata['performers'] = ';'.join(performers) + + i = 0 + for indentifier in self.identifiers.all(): + metadata['identifier' + '_' + str(i)] = identifier.identifier + metadata['identifier_type' + '_' + str(i)] = identifier.type + metadata['identifier_date_last' + '_' + str(i)] = unicode(identifier.date_last) + metadata['identifier_notes' + '_' + str(i)] = identifier.notes + i += 1 + + analyzers = ['channels', 'samplerate', 'duration', 'resolution', 'mime_type'] + for analyzer_id in analyzers: + analysis = MediaItemAnalysis.objects.filter(item=self, analyzer_id=analyzer_id) + if analysis: + metadata[analyzer_id] = analysis[0].value + + metadata['file_size'] = unicode(self.size()) + metadata['thumbnail'] = get_full_url(reverse('telemeta-item-visualize', + kwargs={'public_id': self.public_id, + 'grapher_id': 'waveform_centroid', + 'width': 346, + 'height': 130})) + + # One ID only + identifiers = self.identifiers.all() + if identifiers: + identifier = identifiers[0] + metadata['identifier_id'] = identifier.identifier + metadata['identifier_type'] = identifier.type + metadata['identifier_date'] = unicode(identifier.date_last) + metadata['identifier_note'] = identifier.notes + + return metadata + + +class MediaItemRelated(MediaRelated): + "Item related media" + + item = ForeignKey('MediaItem', related_name="related", verbose_name=_('item')) + + def save(self, force_insert=False, force_update=False, using=False): + super(MediaItemRelated, self).save(force_insert, force_update) + + def parse_markers(self, **kwargs): + # Parse KDEnLive session + if self.file: + if self.is_kdenlive_session(): + session = KDEnLiveSession(self.file.path) + markers = session.markers(**kwargs) + for marker in markers: + m = MediaItemMarker(item=self.item) + m.public_id = get_random_hash() + m.time = float(marker['time']) + m.title = marker['comment'] + m.save() + return markers + + class Meta(MetaCore): + db_table = 'media_item_related' + verbose_name = _('item related media') + verbose_name_plural = _('item related media') + + +class MediaItemKeyword(ModelCore): + "Item keyword" + item = ForeignKey('MediaItem', verbose_name=_('item'), related_name="keyword_relations") + keyword = ForeignKey('ContextKeyword', verbose_name=_('keyword'), related_name="item_relations") + + class Meta(MetaCore): + db_table = 'media_item_keywords' + unique_together = (('item', 'keyword'),) + + +class MediaItemPerformance(ModelCore): + "Item performance" + media_item = ForeignKey('MediaItem', related_name="performances", verbose_name=_('item')) + instrument = WeakForeignKey('Instrument', related_name="performances", verbose_name=_('composition')) + alias = WeakForeignKey('InstrumentAlias', related_name="performances", verbose_name=_('vernacular name')) + instruments_num = CharField(_('number')) + musicians = CharField(_('interprets')) + + class Meta(MetaCore): + db_table = 'media_item_performances' + + +class MediaItemAnalysis(ModelCore): + "Item analysis result computed by TimeSide" + + element_type = 'analysis' + item = ForeignKey('MediaItem', related_name="analysis", verbose_name=_('item')) + analyzer_id = CharField(_('id'), required=True) + name = CharField(_('name')) + value = CharField(_('value')) + unit = CharField(_('unit')) + + class Meta(MetaCore): + db_table = 'media_analysis' + ordering = ['name'] + + def to_dict(self): + if self.analyzer_id == 'duration': + if '.' in self.value: + value = self.value.split('.') + self.value = '.'.join([value[0], value[1][:2]]) + return {'id': self.analyzer_id, 'name': self.name, 'value': self.value, 'unit': self.unit} + + + +class MediaItemMarker(MediaResource): + "2D marker object : text value vs. time (in seconds)" + + element_type = 'marker' + + item = ForeignKey('MediaItem', related_name="markers", verbose_name=_('item')) + public_id = CharField(_('public_id'), required=True) + time = FloatField(_('time (s)')) + title = CharField(_('title')) + date = DateTimeField(_('date'), auto_now=True) + description = TextField(_('description')) + author = ForeignKey(User, related_name="markers", verbose_name=_('author'), + blank=True, null=True) + + class Meta(MetaCore): + db_table = 'media_markers' + ordering = ['time'] + + def __unicode__(self): + if self.title: + return self.title + else: + return self.public_id + + +class MediaItemTranscoded(MediaResource): + "Item file transcoded" + + element_type = 'transcoded item' + + item = models.ForeignKey('MediaItem', related_name="transcoded", verbose_name=_('item')) + mimetype = models.CharField(_('mime_type'), max_length=255, blank=True) + date_added = DateTimeField(_('date'), auto_now_add=True) + status = models.IntegerField(_('status'), choices=ITEM_TRANSODING_STATUS, default=1) + file = models.FileField(_('file'), upload_to='items/%Y/%m/%d', max_length=1024, blank=True) + + @property + def mime_type(self): + if not self.mimetype: + if self.file: + if os.path.exists(self.file.path): + self.mimetype = mimetypes.guess_type(self.file.path)[0] + self.save() + return self.mimetype + else: + return 'none' + else: + return 'none' + else: + return self.mimetype + + def __unicode__(self): + if self.item.title: + return self.item.title + ' - ' + self.mime_type + else: + return self.item.public_id + ' - ' + self.mime_type + + class Meta(MetaCore): + db_table = app_name + '_media_transcoded' + + +class MediaItemTranscodingFlag(ModelCore): + "Item flag to know if the MediaItem has been transcoded to a given format" + + item = ForeignKey('MediaItem', related_name="transcoding", verbose_name=_('item')) + mime_type = CharField(_('mime_type'), required=True) + date = DateTimeField(_('date'), auto_now=True) + value = BooleanField(_('transcoded')) + + class Meta(MetaCore): + db_table = 'media_transcoding' + + +class MediaItemIdentifier(Identifier): + """Item identifier""" + + item = ForeignKey(MediaItem, related_name="identifiers", verbose_name=_('item')) + + class Meta(MetaCore): + db_table = 'media_item_identifier' + verbose_name = _('item identifier') + verbose_name_plural = _('item identifiers') + unique_together = ('identifier', 'item') + + + +class MediaPart(MediaResource): + "Describe an item part" + element_type = 'part' + item = ForeignKey('MediaItem', related_name="parts", verbose_name=_('item')) + title = CharField(_('title'), required=True) + start = FloatField(_('start'), required=True) + end = FloatField(_('end'), required=True) + + class Meta(MetaCore): + db_table = 'media_parts' + verbose_name = _('item part') + + def __unicode__(self): + return self.title diff --git a/telemeta/models/media.py b/telemeta/models/media.py deleted file mode 100644 index 32bcb9a1..00000000 --- a/telemeta/models/media.py +++ /dev/null @@ -1,1069 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 2010 Samalyse SARL -# Copyright (C) 2010-2014 Parisson SARL - -# This software is a computer program whose purpose is to backup, analyse, -# transcode and stream any audio content with its metadata over a web frontend. - -# This software is governed by the CeCILL license under French law and -# abiding by the rules of distribution of free software. You can use, -# modify and/ or redistribute the software under the terms of the CeCILL -# license as circulated by CEA, CNRS and INRIA at the following URL -# "http://www.cecill.info". - -# As a counterpart to the access to the source code and rights to copy, -# modify and redistribute granted by the license, users are provided only -# with a limited warranty and the software's author, the holder of the -# economic rights, and the successive licensors have only limited -# liability. - -# In this respect, the user's attention is drawn to the risks associated -# with loading, using, modifying and/or developing or reproducing the -# software by the user in light of its specific status of free software, -# that may mean that it is complicated to manipulate, and that also -# therefore means that it is reserved for developers and experienced -# professionals having in-depth computer knowledge. Users are therefore -# encouraged to load and test the software's suitability as regards their -# requirements in conditions enabling the security of their systems and/or -# data to be ensured and, more generally, to use and operate it in the -# same conditions as regards security. - -# The fact that you are presently reading this means that you have had -# knowledge of the CeCILL license and that you accept its terms. -# -# Authors: Olivier Guilyardi -# David LIPSZYC -# Guillaume Pellerin - -from __future__ import division -import re, os, random -import mimetypes -from django.contrib.auth.models import User -from django.utils.translation import ugettext_lazy as _ -from django.utils.translation import ugettext -from django.core.exceptions import ValidationError -from telemeta.models.core import * -from telemeta.models.enum import ContextKeyword -from telemeta.util.unaccent import unaccent_icmp -from telemeta.models.location import LocationRelation, Location -from telemeta.models.system import Revision -from telemeta.models.query import * -from telemeta.models.instrument import * -from telemeta.models.enum import * -from telemeta.models.language import * -from telemeta.models.format import * -from telemeta.util.kdenlive.session import * -from django.db import models -from django.db.models import URLField -from django.conf import settings -from django.core.urlresolvers import reverse, reverse_lazy -from django.contrib.sites.models import Site -from django.forms.models import model_to_dict - - -strict_code = getattr(settings, 'TELEMETA_STRICT_CODE', False) - -# Special code regex of collections for the branch -collection_published_code_regex = getattr(settings, 'COLLECTION_PUBLISHED_CODE_REGEX', '[A-Za-z0-9._-]*') -collection_unpublished_code_regex = getattr(settings, 'COLLECTION_UNPUBLISHED_CODE_REGEX', '[A-Za-z0-9._-]*') - -# CREM -#collection_published_code_regex = 'CNRSMH_E_[0-9]{4}(?:_[0-9]{3}){2}' -#collection_unpublished_code_regex = 'CNRSMH_I_[0-9]{4}_[0-9]{3}' - -collection_code_regex = '(?:%s|%s)' % (collection_published_code_regex, - collection_unpublished_code_regex) - -item_published_code_regex = getattr(settings, 'ITEM_PUBLISHED_CODE_REGEX', '[A-Za-z0-9._-]*') -item_unpublished_code_regex = getattr(settings, 'ITEM_UNPUBLISHED_CODE_REGEX', '[A-Za-z0-9._-]*') - -# CREM -# item_published_code_regex = collection_published_code_regex + '(?:_[0-9]{2,3}){1,2}' -# item_unpublished_code_regex = collection_unpublished_code_regex + '_[0-9]{2,3}(?:_[0-9]{2,3}){0,2}' - -item_code_regex = '(?:%s|%s)' % (item_published_code_regex, item_unpublished_code_regex) - -PUBLIC_ACCESS_CHOICES = (('none', _('none')), ('metadata', _('metadata')), - ('mixed', _('mixed')), ('full', _('full'))) - -ITEM_PUBLIC_ACCESS_CHOICES = (('none', _('none')), ('metadata', _('metadata')), - ('full', _('full'))) - -ITEM_TRANSODING_STATUS = ((0, _('broken')), (1, _('pending')), (2, _('processing')), - (3, _('done')), (5, _('ready'))) - -mimetypes.add_type('video/webm','.webm') - -app_name = 'telemeta' - - -def get_random_hash(): - hash = random.getrandbits(64) - return "%016x" % hash - -def get_full_url(path): - return 'http://' + Site.objects.get_current().domain + path - - -class MediaResource(ModelCore): - "Base class of all media objects" - - def public_access_label(self): - if self.public_access == 'metadata': - return _('Metadata only') - elif self.public_access == 'full': - return _('Sound and metadata') - - return _('Private data') - public_access_label.verbose_name = _('access type') - - def set_revision(self, user): - "Save a media object and add a revision" - Revision.touch(self, user) - - def get_revision(self): - return Revision.objects.filter(element_type=self.element_type, element_id=self.id).order_by('-time')[0] - - class Meta: - abstract = True - - -class MediaBaseResource(MediaResource): - "Describe a media base resource" - - title = CharField(_('title'), required=True) - description = CharField(_('description_old')) - descriptions = TextField(_('description')) - code = CharField(_('code'), unique=True, required=True) - public_access = CharField(_('public access'), choices=PUBLIC_ACCESS_CHOICES, max_length=16, default="metadata") - - def __unicode__(self): - return self.code - - @property - def public_id(self): - return self.code - - def save(self, force_insert=False, force_update=False, user=None, code=None): - super(MediaBaseResource, self).save(force_insert, force_update) - - def get_fields(self): - return self._meta.fields - - class Meta(MetaCore): - abstract = True - - -class MediaRelated(MediaResource): - "Related media" - - element_type = 'media' - - title = CharField(_('title')) - date = DateTimeField(_('date'), auto_now=True) - description = TextField(_('description')) - mime_type = CharField(_('mime_type')) - url = CharField(_('url'), max_length=500) - credits = CharField(_('credits')) - file = FileField(_('file'), upload_to='items/%Y/%m/%d', db_column="filename", max_length=255) - - def is_image(self): - is_url_image = False - if self.url: - url_types = ['.png', '.jpg', '.gif', '.jpeg'] - for type in url_types: - if type in self.url or type.upper() in self.url: - is_url_image = True - return 'image' in self.mime_type or is_url_image - - def save(self, force_insert=False, force_update=False, author=None): - super(MediaRelated, self).save(force_insert, force_update) - - def set_mime_type(self): - if self.file: - self.mime_type = mimetypes.guess_type(self.file.path)[0] - - def is_kdenlive_session(self): - if self.file: - return '.kdenlive' in self.file.path - else: - return False - - def __unicode__(self): - if self.title and not re.match('^ *N *$', self.title): - title = self.title - else: - title = unicode(self.item) - return title - - class Meta: - abstract = True - - -class MediaCollection(MediaResource): - "Describe a collection of items" - - element_type = 'collection' - - def is_valid_collection_code(value): - "Check if the collection code is well formed" - regex = '^' + collection_code_regex + '$' - if not re.match(regex, value): - raise ValidationError(u'%s is not a valid collection code' % value) - - # General informations - title = CharField(_('title'), required=True) - alt_title = CharField(_('original title / translation')) - creator = CharField(_('depositor / contributor'), help_text=_('First name, Last name ; First name, Last name')) - description = TextField(_('description')) - recording_context = WeakForeignKey('RecordingContext', related_name="collections", verbose_name=_('recording context')) - recorded_from_year = IntegerField(_('recording year (from)'), help_text=_('YYYY')) - recorded_to_year = IntegerField(_('recording year (until)'), help_text=_('YYYY')) - year_published = IntegerField(_('year published'), help_text=_('YYYY')) - public_access = CharField(_('access type'), choices=PUBLIC_ACCESS_CHOICES, max_length=16, default="metadata") - - # Geographic and cultural informations - # See "countries" and "ethnic_groups" methods below - - # Legal notices - collector = CharField(_('recordist'), help_text=_('First name, Last name ; First name, Last name')) - publisher = WeakForeignKey('Publisher', related_name="collections", verbose_name=_('publisher')) - publisher_collection = WeakForeignKey('PublisherCollection', related_name="collections", verbose_name=_('publisher collection')) - publisher_serial = CharField(_('publisher serial number')) - booklet_author = CharField(_('booklet author'), blank=True) - reference = CharField(_('publisher reference')) - external_references = TextField(_('bibliographic references')) - - auto_period_access = BooleanField(_('automatic access after a rolling period'), default=True) - legal_rights = WeakForeignKey('LegalRight', related_name="collections", verbose_name=_('legal rights')) - - # Archiving data - code = CharField(_('code'), unique=True, required=True, validators=[is_valid_collection_code]) - old_code = CharField(_('old code'), unique=False, null=True, blank=True) - acquisition_mode = WeakForeignKey('AcquisitionMode', related_name="collections", verbose_name=_('mode of acquisition')) - cnrs_contributor = CharField(_('CNRS depositor')) - copy_type = WeakForeignKey('CopyType', related_name="collections", verbose_name=_('copy type')) - metadata_author = WeakForeignKey('MetadataAuthor', related_name="collections", verbose_name=_('record author')) - booklet_description = TextField(_('related documentation')) - publishing_status = WeakForeignKey('PublishingStatus', related_name="collections", verbose_name=_('secondary edition')) - status = WeakForeignKey('Status', related_name="collections", verbose_name=_('collection status')) - alt_copies = TextField(_('copies')) - comment = TextField(_('comment')) - metadata_writer = WeakForeignKey('MetadataWriter', related_name="collections", verbose_name=_('record writer')) - archiver_notes = TextField(_('archiver notes')) - items_done = CharField(_('items finished')) - collector_is_creator = BooleanField(_('recordist identical to depositor')) - is_published = BooleanField(_('published')) - conservation_site = CharField(_('conservation site')) - - # Technical data - media_type = WeakForeignKey('MediaType', related_name="collections", verbose_name=_('media type')) - approx_duration = DurationField(_('estimated duration'), help_text='hh:mm:ss') - physical_items_num = IntegerField(_('number of components (medium / piece)')) - original_format = WeakForeignKey('OriginalFormat', related_name="collections", verbose_name=_('original format')) - physical_format = WeakForeignKey('PhysicalFormat', related_name="collections", verbose_name=_('archive format')) - ad_conversion = WeakForeignKey('AdConversion', related_name='collections', verbose_name=_('digitization')) - - # No more used old fields - alt_ids = CharField(_('copies (obsolete field)')) - travail = CharField(_('archiver notes (obsolete field)')) - - # All - objects = MediaCollectionManager() - - exclude = ['alt_ids', 'travail'] - - class Meta(MetaCore): - db_table = 'media_collections' - ordering = ['code'] - verbose_name = _('collection') - - def __unicode__(self): - return self.code - - def save(self, force_insert=False, force_update=False, user=None, code=None): - super(MediaCollection, self).save(force_insert, force_update) - - @property - def public_id(self): - return self.code - - @property - def has_mediafile(self): - "Tell wether this collection has any media files attached to its items" - items = self.items.all() - for item in items: - if item.file: - return True - return False - - def __name_cmp(self, obj1, obj2): - return unaccent_icmp(obj1.name, obj2.name) - - def countries(self): - "Return the countries of the items" - countries = [] - for item in self.items.filter(location__isnull=False): - for country in item.location.countries(): - if not country in countries: - countries.append(country) - countries.sort(self.__name_cmp) - return countries - countries.verbose_name = _("states / nations") - - def main_countries(self): - "Return the main countries of the items (no aliases or ancestors)" - countries = [] - for item in self.items.filter(location__isnull=False): - if not item.location in countries: - countries.append(item.location) - countries.sort(self.__name_cmp) - return countries - main_countries.verbose_name = _("states / nations") - - def ethnic_groups(self): - "Return the ethnic groups of the items" - groups = [] - items = self.items.all() - for item in items: - if item.ethnic_group and not item.ethnic_group in groups: - groups.append(item.ethnic_group) - - cmp = lambda a, b: unaccent_icmp(a.value, b.value) - groups.sort(cmp) - - return groups - ethnic_groups.verbose_name = _('populations / social groups') - - def computed_duration(self): - duration = Duration() - for item in self.items.all(): - duration += item.computed_duration() - return duration - computed_duration.verbose_name = _('computed duration') - - def computed_size(self): - "Return the total size of a collection" - size = 0 - for item in self.items.all(): - size += item.size() - return size - computed_size.verbose_name = _('collection size') - - def document_status(self): - if '_I_' in self.public_id: - return ugettext('Unpublished') - elif '_E_' in self.public_id: - return ugettext('Published') - else: - return '' - - def get_url(self): - return get_full_url(reverse('telemeta-collection-detail', kwargs={'public_id':self.pk})) - - def to_dict_with_more(self): - # metadata = model_to_dict(self, fields=[], exclude=self.exclude) - metadata = self.to_dict() - for key in self.exclude: - if key in metadata.keys(): - del metadata[key] - - metadata['url'] = get_full_url(reverse('telemeta-collection-detail', kwargs={'public_id':self.pk})) - metadata['doc_status'] = self.document_status() - metadata['countries'] = ';'.join([location.name for location in self.main_countries()]) - metadata['ethnic_groups'] = ';'.join([group.value for group in self.ethnic_groups()]) - metadata['last_modification_date'] = unicode(self.get_revision().time) - metadata['computed_duration'] = unicode(self.computed_duration()) - metadata['computed_size'] = unicode(self.computed_size()) - metadata['number_of_items'] = unicode(self.items.all().count()) - - i = 0 - for media in self.related.all(): - metadata['related_media_title' + '_' + str(i)] = media.title - if media.url: - tag = 'related_media_url' + '_' + str(i) - metadata[tag] = media.url - elif media.url: - metadata[tag] = get_full_url(reverse('telemeta-collection-related', - kwargs={'public_id': self.public_id, 'media_id': media.id})) - i += 1 - - # One ID only - identifiers = self.identifiers.all() - if identifiers: - identifier = identifiers[0] - metadata['identifier_id'] = identifier.identifier - metadata['identifier_type'] = identifier.type - metadata['identifier_date'] = unicode(identifier.date_last) - metadata['identifier_note'] = identifier.notes - - # All IDs - # i = 0 - # for indentifier in self.identifiers.all(): - # metadata['identifier' + '_' + str(i)] = identifier.identifier - # metadata['identifier_type' + '_' + str(i)] = identifier.type - # metadata['identifier_date_last' + '_' + str(i)] = unicode(identifier.date_last) - # metadata['identifier_notes' + '_' + str(i)] = identifier.notes - # i += 1 - - return metadata - - -class MediaCollectionRelated(MediaRelated): - "Collection related media" - - collection = ForeignKey('MediaCollection', related_name="related", verbose_name=_('collection')) - - class Meta(MetaCore): - db_table = 'media_collection_related' - verbose_name = _('collection related media') - verbose_name_plural = _('collection related media') - - -class MediaItem(MediaResource): - "Describe an item" - - element_type = 'item' - - # Main Informations - title = CharField(_('title')) - alt_title = CharField(_('original title / translation')) - collector = CharField(_('collector'), help_text=_('First name, Last name ; First name, Last name')) - collection = ForeignKey('MediaCollection', related_name="items", verbose_name=_('collection')) - recorded_from_date = DateField(_('recording date (from)'), help_text=_('YYYY-MM-DD')) - recorded_to_date = DateField(_('recording date (until)'), help_text=_('YYYY-MM-DD')) - public_access = CharField(_('access type'), choices=ITEM_PUBLIC_ACCESS_CHOICES, max_length=16, default="metadata") - - # Geographic and cultural informations - location = WeakForeignKey('Location', verbose_name=_('location')) - location_comment = CharField(_('location details')) - cultural_area = CharField(_('cultural area')) - language = CharField(_('language')) - language_iso = ForeignKey('Language', related_name="items", verbose_name=_('Language (ISO norm)'), blank=True, null=True, on_delete=models.SET_NULL) - ethnic_group = WeakForeignKey('EthnicGroup', related_name="items", verbose_name=_('population / social group')) - context_comment = TextField(_('Ethnographic context')) - - # Musical informations - moda_execut = CharField(_('implementing rules')) - vernacular_style = WeakForeignKey('VernacularStyle', related_name="items", verbose_name=_('vernacular style')) - generic_style = WeakForeignKey('GenericStyle', related_name="items", verbose_name=_('generic style')) - author = CharField(_('author / compositor'), help_text=_('First name, Last name ; First name, Last name')) - - # Legal mentions - organization = WeakForeignKey('Organization', verbose_name=_('organization')) - depositor = CharField(_('depositor')) - rights = WeakForeignKey('Rights', verbose_name=_('rights')) - - # Archiving data - code = CharField(_('code'), unique=True, blank=True, required=True, help_text=_('CollectionCode_ItemCode')) - old_code = CharField(_('original code'), unique=False, blank=True) - track = CharField(_('item number')) - collector_selection = CharField(_('collector selection')) - collector_from_collection = BooleanField(_('collector as in collection')) - creator_reference = CharField(_('creator reference')) - external_references = TextField(_('published references')) - auto_period_access = BooleanField(_('automatic access after a rolling period'), default=True) - comment = TextField(_('remarks')) - - # Technical data - media_type = WeakForeignKey('MediaType', related_name="items", verbose_name=_('media type')) - approx_duration = DurationField(_('approximative duration'), blank=True, help_text=_('hh:mm:ss')) - mimetype = CharField(_('mime type'), max_length=255, blank=True) - file = FileField(_('file'), upload_to='items/%Y/%m/%d', db_column="filename", max_length=1024) - url = URLField(_('URL'), max_length=512, blank=True) - - # LAM - recordist = CharField(_('recordist')) - digitalist = CharField(_('digitalist')) - digitization_date = DateField(_('digitization date')) - publishing_date = DateField(_('publishing date')) - scientist = CharField(_('scientist'), help_text=_('First name, Last name ; First name, Last name')) - topic = WeakForeignKey('Topic', verbose_name=_('topic')) - summary = TextField(_('summary')) - contributor = CharField(_('contributor')) - - # Manager - objects = MediaItemManager() - - exclude = ['copied_from_item', 'mimetype', 'url', - 'organization', 'depositor', 'rights', - 'recordist', 'digitalist', 'digitization_date', - 'publishing_date', 'scientist', 'topic', - 'summary', 'contributor', ] - - def keywords(self): - return ContextKeyword.objects.filter(item_relations__item = self) - keywords.verbose_name = _('keywords') - - @property - def public_id(self): - if self.code: - return self.code - return str(self.id) - - @property - def mime_type(self): - if not self.mimetype: - if self.file: - if os.path.exists(self.file.path): - self.mimetype = mimetypes.guess_type(self.file.path)[0] - self.save() - return self.mimetype - else: - return 'none' - else: - return 'none' - else: - return _('none') - - - class Meta(MetaCore): - db_table = 'media_items' - permissions = (("can_play_all_items", "Can play all media items"), - ("can_download_all_items", "Can download all media items"), ) - verbose_name = _('item') - - def is_valid_code(self, code): - "Check if the item code is well formed" - if not re.match('^' + self.collection.code, self.code): - return False - if self.collection.is_published: - regex = '^' + item_published_code_regex + '$' - else: - regex = '^' + item_unpublished_code_regex + '$' - if re.match(regex, code): - return True - return False - - def clean(self): - if strict_code: - if self.code and not self.is_valid_code(self.code): - raise ValidationError("%s is not a valid item code for collection %s" - % (self.code, self.collection.code)) - - def save(self, force_insert=False, force_update=False): - super(MediaItem, self).save(force_insert, force_update) - - def computed_duration(self): - "Tell the length in seconds of this item media data" - return self.approx_duration - - computed_duration.verbose_name = _('computed duration') - - def __unicode__(self): - if self.title and not re.match('^ *N *$', self.title): - title = self.title - else: - title = unicode(self.collection.title) - if self.track: - title += ' ' + self.track - return title - - def get_source(self): - source = None - if self.file and os.path.exists(self.file.path): - source = self.file.path - elif self.url: - source = self.url - return source - - @property - def instruments(self): - "Return the instruments of the item" - instruments = [] - performances = MediaItemPerformance.objects.filter(media_item=self) - for performance in performances: - instrument = performance.instrument - alias = performance.alias - if not instrument in instruments: - instruments.append(instrument) - if not alias in instruments: - instruments.append(alias) - - instruments.sort(self.__name_cmp) - return instruments - - instruments.verbose_name = _("instruments") - - def size(self): - if self.file and os.path.exists(self.file.path): - return self.file.size - else: - return 0 - size.verbose_name = _('item size') - - def get_url(self): - return get_full_url(reverse('telemeta-item-detail', kwargs={'public_id':self.pk})) - - def to_dict_with_more(self): - # metadata = model_to_dict(self, fields=[], exclude=self.exclude) - metadata = self.to_dict() - for key in self.exclude: - if key in metadata.keys(): - del metadata[key] - - metadata['url'] = self.get_url() - metadata['last_modification_date'] = unicode(self.get_revision().time) - metadata['collection'] = self.collection.get_url() - - keywords = [] - for keyword in self.keywords(): - keywords.append(keyword.value) - metadata['keywords'] = ';'.join(keywords) - - i = 0 - for media in self.related.all(): - if media.title: - tag = 'related_media_title' + '_' + str(i) - metadata[tag] = media.title - else: - metadata[tag] = '' - if media.url: - tag = 'related_media_url' + '_' + str(i) - metadata[tag] = media.url - elif media.url: - metadata[tag] = get_full_url(reverse('telemeta-collection-related', - kwargs={'public_id': self.public_id, 'media_id': media.id})) - i += 1 - - - instruments = [] - instrument_vernacular_names = [] - performers = [] - - for performance in self.performances.all(): - if performance.instrument: - instruments.append(performance.instrument.name) - if performance.alias: - instrument_vernacular_names.append(performance.alias.name) - if performance.musicians: - performers.append(performance.musicians.replace(' et ', ';')) - - metadata['instruments'] = ';'.join(instruments) - metadata['instrument_vernacular_names'] = ';'.join(instrument_vernacular_names) - metadata['performers'] = ';'.join(performers) - - i = 0 - for indentifier in self.identifiers.all(): - metadata['identifier' + '_' + str(i)] = identifier.identifier - metadata['identifier_type' + '_' + str(i)] = identifier.type - metadata['identifier_date_last' + '_' + str(i)] = unicode(identifier.date_last) - metadata['identifier_notes' + '_' + str(i)] = identifier.notes - i += 1 - - analyzers = ['channels', 'samplerate', 'duration', 'resolution', 'mime_type'] - for analyzer_id in analyzers: - analysis = MediaItemAnalysis.objects.filter(item=self, analyzer_id=analyzer_id) - if analysis: - metadata[analyzer_id] = analysis[0].value - - metadata['file_size'] = unicode(self.size()) - metadata['thumbnail'] = get_full_url(reverse('telemeta-item-visualize', - kwargs={'public_id': self.public_id, - 'grapher_id': 'waveform_centroid', - 'width': 346, - 'height': 130})) - - # One ID only - identifiers = self.identifiers.all() - if identifiers: - identifier = identifiers[0] - metadata['identifier_id'] = identifier.identifier - metadata['identifier_type'] = identifier.type - metadata['identifier_date'] = unicode(identifier.date_last) - metadata['identifier_note'] = identifier.notes - - return metadata - - -class MediaItemRelated(MediaRelated): - "Item related media" - - item = ForeignKey('MediaItem', related_name="related", verbose_name=_('item')) - - def save(self, force_insert=False, force_update=False, using=False): - super(MediaItemRelated, self).save(force_insert, force_update) - - def parse_markers(self, **kwargs): - # Parse KDEnLive session - if self.file: - if self.is_kdenlive_session(): - session = KDEnLiveSession(self.file.path) - markers = session.markers(**kwargs) - for marker in markers: - m = MediaItemMarker(item=self.item) - m.public_id = get_random_hash() - m.time = float(marker['time']) - m.title = marker['comment'] - m.save() - return markers - - class Meta(MetaCore): - db_table = 'media_item_related' - verbose_name = _('item related media') - verbose_name_plural = _('item related media') - - -class MediaItemKeyword(ModelCore): - "Item keyword" - item = ForeignKey('MediaItem', verbose_name=_('item'), related_name="keyword_relations") - keyword = ForeignKey('ContextKeyword', verbose_name=_('keyword'), related_name="item_relations") - - class Meta(MetaCore): - db_table = 'media_item_keywords' - unique_together = (('item', 'keyword'),) - - -class MediaItemPerformance(ModelCore): - "Item performance" - media_item = ForeignKey('MediaItem', related_name="performances", verbose_name=_('item')) - instrument = WeakForeignKey('Instrument', related_name="performances", verbose_name=_('composition')) - alias = WeakForeignKey('InstrumentAlias', related_name="performances", verbose_name=_('vernacular name')) - instruments_num = CharField(_('number')) - musicians = CharField(_('interprets')) - - class Meta(MetaCore): - db_table = 'media_item_performances' - - -class MediaItemAnalysis(ModelCore): - "Item analysis result computed by TimeSide" - - element_type = 'analysis' - item = ForeignKey('MediaItem', related_name="analysis", verbose_name=_('item')) - analyzer_id = CharField(_('id'), required=True) - name = CharField(_('name')) - value = CharField(_('value')) - unit = CharField(_('unit')) - - class Meta(MetaCore): - db_table = 'media_analysis' - ordering = ['name'] - - def to_dict(self): - if self.analyzer_id == 'duration': - if '.' in self.value: - value = self.value.split('.') - self.value = '.'.join([value[0], value[1][:2]]) - return {'id': self.analyzer_id, 'name': self.name, 'value': self.value, 'unit': self.unit} - - -class MediaPart(MediaResource): - "Describe an item part" - element_type = 'part' - item = ForeignKey('MediaItem', related_name="parts", verbose_name=_('item')) - title = CharField(_('title'), required=True) - start = FloatField(_('start'), required=True) - end = FloatField(_('end'), required=True) - - class Meta(MetaCore): - db_table = 'media_parts' - verbose_name = _('item part') - - def __unicode__(self): - return self.title - -class Playlist(ModelCore): - "Item, collection or marker playlist" - element_type = 'playlist' - public_id = CharField(_('public_id'), required=True) - author = ForeignKey(User, related_name="playlists", db_column="author") - title = CharField(_('title'), required=True) - description = TextField(_('description')) - - class Meta(MetaCore): - db_table = 'playlists' - - def __unicode__(self): - return self.title - - -class PlaylistResource(ModelCore): - "Playlist components" - RESOURCE_TYPE_CHOICES = (('item', 'item'), ('collection', 'collection'), - ('marker', 'marker'), ('fonds', 'fonds'), ('corpus', 'corpus')) - element_type = 'playlist_resource' - public_id = CharField(_('public_id'), required=True) - playlist = ForeignKey('Playlist', related_name="resources", verbose_name=_('playlist')) - resource_type = CharField(_('resource_type'), choices=RESOURCE_TYPE_CHOICES, required=True) - resource_id = CharField(_('resource_id'), required=True) - - class Meta(MetaCore): - db_table = 'playlist_resources' - - -class MediaItemMarker(MediaResource): - "2D marker object : text value vs. time (in seconds)" - - element_type = 'marker' - - item = ForeignKey('MediaItem', related_name="markers", verbose_name=_('item')) - public_id = CharField(_('public_id'), required=True) - time = FloatField(_('time (s)')) - title = CharField(_('title')) - date = DateTimeField(_('date'), auto_now=True) - description = TextField(_('description')) - author = ForeignKey(User, related_name="markers", verbose_name=_('author'), - blank=True, null=True) - - class Meta(MetaCore): - db_table = 'media_markers' - ordering = ['time'] - - def __unicode__(self): - if self.title: - return self.title - else: - return self.public_id - - -class MediaItemTranscoded(MediaResource): - "Item file transcoded" - - element_type = 'transcoded item' - - item = models.ForeignKey('MediaItem', related_name="transcoded", verbose_name=_('item')) - mimetype = models.CharField(_('mime_type'), max_length=255, blank=True) - date_added = DateTimeField(_('date'), auto_now_add=True) - status = models.IntegerField(_('status'), choices=ITEM_TRANSODING_STATUS, default=1) - file = models.FileField(_('file'), upload_to='items/%Y/%m/%d', max_length=1024, blank=True) - - @property - def mime_type(self): - if not self.mimetype: - if self.file: - if os.path.exists(self.file.path): - self.mimetype = mimetypes.guess_type(self.file.path)[0] - self.save() - return self.mimetype - else: - return 'none' - else: - return 'none' - else: - return self.mimetype - - def __unicode__(self): - if self.item.title: - return self.item.title + ' - ' + self.mime_type - else: - return self.item.public_id + ' - ' + self.mime_type - - class Meta(MetaCore): - db_table = app_name + '_media_transcoded' - - -class MediaItemTranscodingFlag(ModelCore): - "Item flag to know if the MediaItem has been transcoded to a given format" - - item = ForeignKey('MediaItem', related_name="transcoding", verbose_name=_('item')) - mime_type = CharField(_('mime_type'), required=True) - date = DateTimeField(_('date'), auto_now=True) - value = BooleanField(_('transcoded')) - - class Meta(MetaCore): - db_table = 'media_transcoding' - - -class DublinCoreToFormatMetadata(object): - """ a mapping class to get item DublinCore metadata dictionaries - in various audio metadata format (MP3, OGG, etc...)""" - - #FIXME: should be given by timeside - unavailable_extensions = ['wav', 'aiff', 'aif', 'flac', 'webm'] - - metadata_mapping = { - 'mp3' : { - 'title': 'TIT2', #title2 - 'creator': 'TCOM', #composer - 'creator': 'TPE1', #lead - 'identifier': 'UFID', #unique ID - 'relation': 'TALB', #album - 'type': 'TCON', #genre - 'publisher': 'TPUB', #publisher - 'date': 'TDRC', #year -# 'coverage': 'COMM', #comment - }, - 'ogg': { - 'creator': 'artist', - 'relation': 'album', - 'all': 'all', - }, - 'flac': { - 'creator': 'artist', - 'relation': 'album', - 'all': 'all', - }, - 'wav': { - 'creator': 'artist', - 'relation': 'album', - 'all': 'all', - }, - 'webm': { - 'creator': 'artist', - 'relation': 'album', - 'all': 'all', - }, - } - - def __init__(self, format): - self.format = format - - def get_metadata(self, dc_metadata): - mapp = self.metadata_mapping[self.format] - metadata = {} - keys_done = [] - for data in dc_metadata: - key = data[0] - value = data[1].encode('utf-8') - if value: - if key == 'date': - value = value.split(';')[0].split('=') - if len(value) > 1: - value = value[1] - value = value.split('-')[0] - else: - value = value[0].split('-')[0] - if key in mapp: - metadata[mapp[key]] = value.decode('utf-8') - elif 'all' in mapp.keys(): - metadata[key] = value.decode('utf-8') - keys_done.append(key) - return metadata - - -class MediaCorpus(MediaBaseResource): - "Describe a corpus" - - element_type = 'corpus' - children_type = 'collections' - - children = models.ManyToManyField(MediaCollection, related_name="corpus", - verbose_name=_('collections'), blank=True, null=True) - recorded_from_year = IntegerField(_('recording year (from)'), help_text=_('YYYY')) - recorded_to_year = IntegerField(_('recording year (until)'), help_text=_('YYYY')) - - objects = MediaCorpusManager() - - @property - def public_id(self): - return self.code - - @property - def has_mediafile(self): - for child in self.children.all(): - if child.has_mediafile: - return True - return False - - def computed_duration(self): - duration = Duration() - for child in self.children.all(): - duration += child.computed_duration() - return duration - computed_duration.verbose_name = _('total available duration') - - class Meta(MetaCore): - db_table = 'media_corpus' - verbose_name = _('corpus') - verbose_name_plural = _('corpus') - ordering = ['code'] - - -class MediaFonds(MediaBaseResource): - "Describe fonds" - - element_type = 'fonds' - children_type = 'corpus' - - children = models.ManyToManyField(MediaCorpus, related_name="fonds", - verbose_name=_('corpus'), blank=True, null=True) - - objects = MediaFondsManager() - - @property - def public_id(self): - return self.code - - @property - def has_mediafile(self): - for child in self.children.all(): - if child.has_mediafile: - return True - return False - - def computed_duration(self): - duration = Duration() - for child in self.children.all(): - duration += child.computed_duration() - return duration - computed_duration.verbose_name = _('total available duration') - - class Meta(MetaCore): - db_table = 'media_fonds' - verbose_name = _('fonds') - verbose_name_plural = _('fonds') - ordering = ['code'] - - -class MediaCorpusRelated(MediaRelated): - "Corpus related media" - - resource = ForeignKey(MediaCorpus, related_name="related", verbose_name=_('corpus')) - - class Meta(MetaCore): - db_table = 'media_corpus_related' - verbose_name = _('corpus related media') - verbose_name_plural = _('corpus related media') - - -class MediaFondsRelated(MediaRelated): - "Fonds related media" - - resource = ForeignKey(MediaFonds, related_name="related", verbose_name=_('fonds')) - - class Meta(MetaCore): - db_table = 'media_fonds_related' - verbose_name = _('fonds related media') - verbose_name_plural = _('fonds related media') - - -class Identifier(ModelCore): - """Resource identifier""" - - identifier = CharField(_('identifier'), max_length=255, blank=True, unique=True) - type = WeakForeignKey('IdentifierType', verbose_name=_('type')) - date_add = DateTimeField(_('date added'), auto_now_add=True) - date_first = DateTimeField(_('date of first attribution')) - date_last = DateTimeField(_('date of last attribution')) - date_modified = DateTimeField(_('date modified'), auto_now=True) - notes = TextField(_('notes')) - - class Meta(MetaCore): - abstract = True - ordering = ['-date_last'] - - -class MediaItemIdentifier(Identifier): - """Item identifier""" - - item = ForeignKey(MediaItem, related_name="identifiers", verbose_name=_('item')) - - class Meta(MetaCore): - db_table = 'media_item_identifier' - verbose_name = _('item identifier') - verbose_name_plural = _('item identifiers') - unique_together = ('identifier', 'item') - - -class MediaCollectionIdentifier(Identifier): - """Collection identifier""" - - collection = ForeignKey(MediaCollection, related_name="identifiers", verbose_name=_('collection')) - - class Meta(MetaCore): - db_table = 'media_collection_identifier' - verbose_name = _('collection identifier') - verbose_name_plural = _('collection identifiers') - unique_together = ('identifier', 'collection') - diff --git a/telemeta/models/playlist.py b/telemeta/models/playlist.py new file mode 100644 index 00000000..fbae06bf --- /dev/null +++ b/telemeta/models/playlist.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2010 Samalyse SARL +# Copyright (C) 2010-2014 Parisson SARL + +# This software is a computer program whose purpose is to backup, analyse, +# transcode and stream any audio content with its metadata over a web frontend. + +# This software is governed by the CeCILL license under French law and +# abiding by the rules of distribution of free software. You can use, +# modify and/ or redistribute the software under the terms of the CeCILL +# license as circulated by CEA, CNRS and INRIA at the following URL +# "http://www.cecill.info". + +# As a counterpart to the access to the source code and rights to copy, +# modify and redistribute granted by the license, users are provided only +# with a limited warranty and the software's author, the holder of the +# economic rights, and the successive licensors have only limited +# liability. + +# In this respect, the user's attention is drawn to the risks associated +# with loading, using, modifying and/or developing or reproducing the +# software by the user in light of its specific status of free software, +# that may mean that it is complicated to manipulate, and that also +# therefore means that it is reserved for developers and experienced +# professionals having in-depth computer knowledge. Users are therefore +# encouraged to load and test the software's suitability as regards their +# requirements in conditions enabling the security of their systems and/or +# data to be ensured and, more generally, to use and operate it in the +# same conditions as regards security. + +# The fact that you are presently reading this means that you have had +# knowledge of the CeCILL license and that you accept its terms. +# +# Authors: Olivier Guilyardi +# David LIPSZYC +# Guillaume Pellerin + + +from __future__ import division +from django.utils.translation import ugettext_lazy as _ +from telemeta.models.core import * + + +class Playlist(ModelCore): + "Item, collection or marker playlist" + element_type = 'playlist' + public_id = CharField(_('public_id'), required=True) + author = ForeignKey(User, related_name="playlists", db_column="author") + title = CharField(_('title'), required=True) + description = TextField(_('description')) + + class Meta(MetaCore): + db_table = 'playlists' + + def __unicode__(self): + return self.title + + +class PlaylistResource(ModelCore): + "Playlist components" + RESOURCE_TYPE_CHOICES = (('item', 'item'), ('collection', 'collection'), + ('marker', 'marker'), ('fonds', 'fonds'), ('corpus', 'corpus')) + element_type = 'playlist_resource' + public_id = CharField(_('public_id'), required=True) + playlist = ForeignKey('Playlist', related_name="resources", verbose_name=_('playlist')) + resource_type = CharField(_('resource_type'), choices=RESOURCE_TYPE_CHOICES, required=True) + resource_id = CharField(_('resource_id'), required=True) + + class Meta(MetaCore): + db_table = 'playlist_resources' + + diff --git a/telemeta/models/resource.py b/telemeta/models/resource.py new file mode 100644 index 00000000..3939254a --- /dev/null +++ b/telemeta/models/resource.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2010 Samalyse SARL +# Copyright (C) 2010-2014 Parisson SARL + +# This software is a computer program whose purpose is to backup, analyse, +# transcode and stream any audio content with its metadata over a web frontend. + +# This software is governed by the CeCILL license under French law and +# abiding by the rules of distribution of free software. You can use, +# modify and/ or redistribute the software under the terms of the CeCILL +# license as circulated by CEA, CNRS and INRIA at the following URL +# "http://www.cecill.info". + +# As a counterpart to the access to the source code and rights to copy, +# modify and redistribute granted by the license, users are provided only +# with a limited warranty and the software's author, the holder of the +# economic rights, and the successive licensors have only limited +# liability. + +# In this respect, the user's attention is drawn to the risks associated +# with loading, using, modifying and/or developing or reproducing the +# software by the user in light of its specific status of free software, +# that may mean that it is complicated to manipulate, and that also +# therefore means that it is reserved for developers and experienced +# professionals having in-depth computer knowledge. Users are therefore +# encouraged to load and test the software's suitability as regards their +# requirements in conditions enabling the security of their systems and/or +# data to be ensured and, more generally, to use and operate it in the +# same conditions as regards security. + +# The fact that you are presently reading this means that you have had +# knowledge of the CeCILL license and that you accept its terms. +# +# Authors: Olivier Guilyardi +# David LIPSZYC +# Guillaume Pellerin + + +from django.utils.translation import ugettext_lazy as _ +from telemeta.models.core import * + + +class MediaResource(ModelCore): + "Base class of all media objects" + + def public_access_label(self): + if self.public_access == 'metadata': + return _('Metadata only') + elif self.public_access == 'full': + return _('Sound and metadata') + + return _('Private data') + public_access_label.verbose_name = _('access type') + + def set_revision(self, user): + "Save a media object and add a revision" + Revision.touch(self, user) + + def get_revision(self): + return Revision.objects.filter(element_type=self.element_type, element_id=self.id).order_by('-time')[0] + + class Meta: + abstract = True + + +class MediaBaseResource(MediaResource): + "Describe a media base resource" + + title = CharField(_('title'), required=True) + description = CharField(_('description_old')) + descriptions = TextField(_('description')) + code = CharField(_('code'), unique=True, required=True) + public_access = CharField(_('public access'), choices=PUBLIC_ACCESS_CHOICES, max_length=16, default="metadata") + + def __unicode__(self): + return self.code + + @property + def public_id(self): + return self.code + + def save(self, force_insert=False, force_update=False, user=None, code=None): + super(MediaBaseResource, self).save(force_insert, force_update) + + def get_fields(self): + return self._meta.fields + + class Meta(MetaCore): + abstract = True + + +class MediaRelated(MediaResource): + "Related media" + + element_type = 'media' + + title = CharField(_('title')) + date = DateTimeField(_('date'), auto_now=True) + description = TextField(_('description')) + mime_type = CharField(_('mime_type')) + url = CharField(_('url'), max_length=500) + credits = CharField(_('credits')) + file = FileField(_('file'), upload_to='items/%Y/%m/%d', db_column="filename", max_length=255) + + def is_image(self): + is_url_image = False + if self.url: + url_types = ['.png', '.jpg', '.gif', '.jpeg'] + for type in url_types: + if type in self.url or type.upper() in self.url: + is_url_image = True + return 'image' in self.mime_type or is_url_image + + def save(self, force_insert=False, force_update=False, author=None): + super(MediaRelated, self).save(force_insert, force_update) + + def set_mime_type(self): + if self.file: + self.mime_type = mimetypes.guess_type(self.file.path)[0] + + def is_kdenlive_session(self): + if self.file: + return '.kdenlive' in self.file.path + else: + return False + + def __unicode__(self): + if self.title and not re.match('^ *N *$', self.title): + title = self.title + else: + title = unicode(self.item) + return title + + class Meta: + abstract = True + + diff --git a/telemeta/models/utils.py b/telemeta/models/utils.py new file mode 100644 index 00000000..70e71f36 --- /dev/null +++ b/telemeta/models/utils.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2010-2014 Parisson SARL + + +def get_random_hash(): + hash = random.getrandbits(64) + return "%016x" % hash + + +def get_full_url(path): + return 'http://' + Site.objects.get_current().domain + path + + +def word_search_q(field, pattern): + words = re.split("[ .*-]+", pattern) + q = Q() + for w in words: + if len(w) >= 3: + kwargs = {field + '__icontains': w} + q &= Q(**kwargs) + + return q