From: olivier <> Date: Wed, 10 Feb 2010 16:42:16 +0000 (+0000) Subject: split models X-Git-Tag: 1.1~554 X-Git-Url: https://git.parisson.com/?a=commitdiff_plain;h=622588f8e15e8f6f5893e37995f342ceb53b055a;p=telemeta.git split models --- diff --git a/telemeta/models/__init__.py b/telemeta/models/__init__.py index 3b4367b0..678ec369 100644 --- a/telemeta/models/__init__.py +++ b/telemeta/models/__init__.py @@ -32,7 +32,11 @@ # # Author: Olivier Guilyardi -from telemeta.models.crem import * +from telemeta.models.media import * +from telemeta.models.location import * +from telemeta.models.instrument import * +from telemeta.models.enum import * +from telemeta.models.system import * #MediaCollection, MediaItem, MediaPart, Revision, \ # PhysicalFormat, PublishingStatus diff --git a/telemeta/models/core.py b/telemeta/models/core.py index 50455b76..9640ea48 100644 --- a/telemeta/models/core.py +++ b/telemeta/models/core.py @@ -1,21 +1,22 @@ # -*- coding: utf-8 -*- +# # Copyright (C) 2007-2010 Samalyse 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, @@ -26,16 +27,27 @@ # 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 +__all__ = ['ModelCore', 'MetaCore', 'DurationField', 'Duration', 'WeakForeignKey', + 'EnhancedModel', 'CharField', 'TextField', 'IntegerField', 'BooleanField', + 'DateTimeField', 'FileField', 'ForeignKey', 'FloatField', 'DateField', + 'RequiredFieldError', 'CoreQuerySet', 'CoreManager'] + +from django.core import exceptions +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.db import models import datetime from django.utils.translation import ugettext_lazy as _ import re +from django.core.exceptions import ObjectDoesNotExist class Duration(object): """Represent a time duration""" @@ -302,3 +314,140 @@ class DateField(models.DateField): def __init__(self, *args, **kwargs): super(DateField, self).__init__(*args, **normalize_field(kwargs, '0000-00-00')) +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 + def required_fields(cls): + required = [] + for field in cls._meta.fields: + if not field.blank: + required.append(field) + return required + + def save(self, force_insert=False, force_update=False, using=None): + required = self.required_fields() + for field in required: + if not getattr(self, field.name): + raise RequiredFieldError(self, field) + super(ModelCore, self).save(force_insert, force_update, using) + + @classmethod + def get_dom_name(cls): + "Convert the class name to a DOM element name" + clsname = cls.__name__ + return clsname[0].lower() + clsname[1:] + + @staticmethod + def get_dom_field_name(field_name): + "Convert the class name to a DOM element name" + tokens = field_name.split('_') + name = tokens[0] + for t in tokens[1:]: + name += t[0].upper() + t[1:] + return name + + def to_dom(self): + "Return the DOM representation of this media object" + impl = getDOMImplementation() + root = self.get_dom_name() + doc = impl.createDocument(None, root, None) + top = doc.documentElement + top.setAttribute("id", str(self.pk)) + fields = self.to_dict() + for name, value in fields.iteritems(): + element = doc.createElement(self.get_dom_field_name(name)) + if isinstance(value, EnhancedModel): + element.setAttribute('key', str(value.pk)) + value = unicode(value) + element.appendChild(doc.createTextNode(value)) + top.appendChild(element) + return doc + + def to_dict(self): + "Return model fields as a dict of name/value pairs" + fields_dict = {} + for field in self._meta.fields: + fields_dict[field.name] = getattr(self, field.name) + return fields_dict + + def to_list(self): + "Return model fields as a list" + fields_list = [] + for field in self._meta.fields: + fields_list.append({'name': field.name, 'value': getattr(self, field.name)}) + return fields_list + + @classmethod + def field_label(cls, field_name): + try: + return cls._meta.get_field(field_name).verbose_name + except FieldDoesNotExist: + try: + return getattr(cls, field_name).verbose_name + except AttributeError: + return field_name + + class Meta: + abstract = True + +class MetaCore: + app_label = 'telemeta' + +class CoreQuerySet(EnhancedQuerySet): + "Base class for all query sets" + + def none(self): # redundant with none() in recent Django svn + "Return an empty result set" + return self.extra(where = ["0 = 1"]) + + def word_search_q(self, 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 + + def word_search(self, field, pattern): + return self.filter(self.word_search_q(field, pattern)) + + def _by_change_time(self, type, from_time = None, until_time = None): + "Search between two revision dates" + where = ["element_type = '%s'" % type] + if from_time: + where.append("time >= '%s'" % from_time.strftime('%Y-%m-%d %H:%M:%S')) + if until_time: + where.append("time <= '%s'" % until_time.strftime('%Y-%m-%d %H:%M:%S')) + return self.extra( + where = ["id IN (SELECT DISTINCT element_id FROM revisions WHERE %s)" % " AND ".join(where)]); + +class CoreManager(EnhancedManager): + "Base class for all models managers" + + def none(self, *args, **kwargs): + "" + return self.get_query_set().none(*args, **kwargs) + + def get(self, **kwargs): + if kwargs.has_key('public_id'): + try: + args = kwargs.copy() + args['code'] = kwargs['public_id'] + args.pop('public_id') + return super(CoreManager, self).get(**args) + except ObjectDoesNotExist: + args = kwargs.copy() + args['id'] = kwargs['public_id'] + args.pop('public_id') + return super(CoreManager, self).get(**args) + + return super(CoreManager, self).get(**kwargs) + diff --git a/telemeta/models/crem.py b/telemeta/models/crem.py deleted file mode 100755 index 8dac2737..00000000 --- a/telemeta/models/crem.py +++ /dev/null @@ -1,792 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 2007-2010 Samalyse 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 - -from django.core.exceptions import ObjectDoesNotExist -import cremquery as query -from xml.dom.minidom import getDOMImplementation -from telemeta.util.unaccent import unaccent_icmp -import re -from django.db.models import FieldDoesNotExist, Q -from telemeta.models.core import DurationField, Duration, WeakForeignKey, EnhancedModel, \ - CharField, TextField, IntegerField, BooleanField, \ - DateTimeField, FileField, ForeignKey, FloatField, DateField -from telemeta.models import dublincore as dc -from django.utils.translation import ugettext_lazy as _ - -class ModelCore(EnhancedModel): - - @classmethod - def required_fields(cls): - required = [] - for field in cls._meta.fields: - if not field.blank: - required.append(field) - return required - - def save(self, force_insert=False, force_update=False, using=None): - required = self.required_fields() - for field in required: - if not getattr(self, field.name): - raise RequiredFieldError(self, field) - super(ModelCore, self).save(force_insert, force_update, using) - - def save_with_revision(self, user, force_insert=False, force_update=False, using=None): - "Save a media object and add a revision" - self.save(force_insert, force_update, using) - 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] - - @classmethod - def get_dom_name(cls): - "Convert the class name to a DOM element name" - clsname = cls.__name__ - return clsname[0].lower() + clsname[1:] - - @staticmethod - def get_dom_field_name(field_name): - "Convert the class name to a DOM element name" - tokens = field_name.split('_') - name = tokens[0] - for t in tokens[1:]: - name += t[0].upper() + t[1:] - return name - - def to_dom(self): - "Return the DOM representation of this media object" - impl = getDOMImplementation() - root = self.get_dom_name() - doc = impl.createDocument(None, root, None) - top = doc.documentElement - top.setAttribute("id", str(self.pk)) - fields = self.to_dict() - for name, value in fields.iteritems(): - element = doc.createElement(self.get_dom_field_name(name)) - if isinstance(value, EnhancedModel): - element.setAttribute('key', str(value.pk)) - value = unicode(value) - element.appendChild(doc.createTextNode(value)) - top.appendChild(element) - return doc - - def to_dict(self): - "Return model fields as a dict of name/value pairs" - fields_dict = {} - for field in self._meta.fields: - fields_dict[field.name] = getattr(self, field.name) - return fields_dict - - def to_list(self): - "Return model fields as a list" - fields_list = [] - for field in self._meta.fields: - fields_list.append({'name': field.name, 'value': getattr(self, field.name)}) - return fields_list - - @classmethod - def field_label(cls, field_name): - try: - return cls._meta.get_field(field_name).verbose_name - except FieldDoesNotExist: - try: - return getattr(cls, field_name).verbose_name - except AttributeError: - return field_name - - class Meta: - abstract = True - -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 = _('public access') - - class Meta: - abstract = True - -class MetaCore: - app_label = 'telemeta' - -class MediaCollection(MediaResource): - "Describe a collection of items" - element_type = 'collection' - PUBLIC_ACCESS_CHOICES = (('none', 'none'), ('metadata', 'metadata'), ('full', 'full')) - - published_code_regex = 'CNRSMH_E_[0-9]{4}(?:_[0-9]{3}){2}' - unpublished_code_regex = 'CNRSMH_I_[0-9]{4}_[0-9]{3}' - code_regex = '(?:%s|%s)' % (published_code_regex, unpublished_code_regex) - - reference = CharField(_('reference'), unique=True, null=True) - physical_format = WeakForeignKey('PhysicalFormat', related_name="collections", - verbose_name=_('archive format')) - old_code = CharField(_('old code'), unique=True, null=True) - code = CharField(_('code'), unique=True, required=True) - title = CharField(_('title'), required=True) - alt_title = CharField(_('original title / translation')) - physical_items_num = IntegerField(_('number of components (medium / piece)')) - publishing_status = WeakForeignKey('PublishingStatus', related_name="collections", - verbose_name=_('secondary edition')) - creator = CharField(_('depositor / contributor')) - booklet_author = CharField(_('author of published notice')) - booklet_description = TextField(_('related documentation')) - collector = CharField(_('recordist')) - collector_is_creator = BooleanField(_('recordist identical to depositor')) - publisher = WeakForeignKey('Publisher', related_name="collections", - verbose_name=_('publisher / status')) - is_published = BooleanField(_('published')) - year_published = IntegerField(_('year published')) - publisher_collection = WeakForeignKey('PublisherCollection', related_name="collections", - verbose_name=_('publisher collection')) - publisher_serial = CharField(_('publisher serial number')) - external_references = TextField(_('bibliographic references')) - acquisition_mode = WeakForeignKey('AcquisitionMode', related_name="collections", - verbose_name=_('mode of acquisition')) - comment = TextField(_('comment')) - metadata_author = WeakForeignKey('MetadataAuthor', related_name="collections", - verbose_name=_('record author')) - metadata_writer = WeakForeignKey('MetadataWriter', related_name="collections", - verbose_name=_('record writer')) - legal_rights = WeakForeignKey('LegalRight', related_name="collections", - verbose_name=_('legal rights')) - alt_ids = CharField(_('copies')) - recorded_from_year = IntegerField(_('recording year (from)')) - recorded_to_year = IntegerField(_('recording year (until)')) - recording_context = WeakForeignKey('RecordingContext', related_name="collections", - verbose_name=_('recording context')) - approx_duration = DurationField(_('approximative duration')) - doctype_code = IntegerField(_('document type')) - travail = CharField(_('archiver notes')) - state = TextField(_('status')) - cnrs_contributor = CharField(_('CNRS depositor')) - items_done = CharField(_('items finished')) - a_informer_07_03 = CharField(_('a_informer_07_03')) - ad_conversion = WeakForeignKey('AdConversion', related_name='collections', - verbose_name=_('A/D conversion')) - public_access = CharField(_('public access'), choices=PUBLIC_ACCESS_CHOICES, - max_length=16, default="metadata") - - objects = query.MediaCollectionManager() - - def __unicode__(self): - if self.title: - return self.title - - return self.code - - @property - def public_id(self): - return self.code - - 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 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) - - groups.sort(self.__name_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 is_valid_code(self, code): - "Check if the collection code is well formed" - if self.is_published: - regex = '^' + self.published_code_regex + '$' - else: - regex = '^' + self.unpublished_code_regex + '$' - - if re.match(regex, code): - return True - - return False - - def save(self, force_insert=False, force_update=False, using=None): - if not self.code: - raise RequiredFieldError(self, self._meta.get_field('code')) - if not self.is_valid_code(self.code): - raise MediaInvalidCodeError("%s is not a valid code for this collection" % self.code) - super(MediaCollection, self).save(force_insert, force_update, using) - - class Meta(MetaCore): - db_table = 'media_collections' - -class MediaItem(MediaResource): - "Describe an item" - element_type = 'item' - PUBLIC_ACCESS_CHOICES = (('none', 'none'), ('metadata', 'metadata'), ('full', 'full')) - - published_code_regex = MediaCollection.published_code_regex + '(?:_[0-9]{2}){1,2}' - unpublished_code_regex = MediaCollection.unpublished_code_regex + '_[0-9]{2,3}(?:_[0-9]{2}){0,2}' - code_regex = '(?:%s|%s)' % (published_code_regex, unpublished_code_regex) - - collection = ForeignKey('MediaCollection', related_name="items", - verbose_name=_('collection')) - track = CharField(_('item number')) - old_code = CharField(_('old code'), unique=True, null=True) - code = CharField(_('code'), unique=True, null=True) - approx_duration = DurationField(_('approximative duration')) - recorded_from_date = DateField(_('recording date (from)')) - recorded_to_date = DateField(_('recording date (until)')) - location = WeakForeignKey('Location', verbose_name=_('location')) - location_comment = CharField(_('location details')) - ethnic_group = WeakForeignKey('EthnicGroup', related_name="items", - verbose_name=_('population / social group')) - title = CharField(_('title'), required=True) - alt_title = CharField(_('original title / translation')) - author = CharField(_('author')) - vernacular_style = WeakForeignKey('VernacularStyle', related_name="items", - verbose_name=_('vernacular name')) - context_comment = TextField(_('comments')) - external_references = TextField(_('published reference')) - moda_execut = CharField(_('moda_execut')) - copied_from_item = WeakForeignKey('self', related_name="copies", verbose_name=_('copy of')) - collector = CharField(_('recordist')) - collector_from_collection = BooleanField(_('recordist as in collection')) - cultural_area = CharField(_('cultural area')) - generic_style = WeakForeignKey('GenericStyle', related_name="items", - verbose_name=_('generic name')) - collector_selection = CharField(_('recordist selection')) - creator_reference = CharField(_('reference')) - comment = TextField(_('comment')) - file = FileField(_('file'), upload_to='items/%Y/%m/%d', db_column="filename") - public_access = CharField(_('public access'), choices=PUBLIC_ACCESS_CHOICES, max_length=16, default="metadata") - - objects = query.MediaItemManager() - - 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 self.id - - class Meta(MetaCore): - db_table = 'media_items' - - 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 = '^' + self.published_code_regex + '$' - else: - regex = '^' + self.unpublished_code_regex + '$' - - if re.match(regex, code): - return True - - return False - - def save(self, force_insert=False, force_update=False, using=None): - if not self.code: - raise RequiredFieldError(self, self._meta.get_field('code')) - if not self.is_valid_code(self.code): - raise MediaInvalidCodeError("%s is not a valid item code for collection %s" - % (self.code, self.collection.code)) - super(MediaItem, self).save(force_insert, force_update, using) - - def computed_duration(self): - "Tell the length in seconds of this item media data" - # FIXME: use TimeSide? - seconds = 0 - if self.file: - import wave - media = wave.open(self.file.path, "rb") - seconds = media.getnframes() / media.getframerate() - media.close() - - return Duration(seconds=seconds) - - 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) - - if self.track: - title += ' ' + self.track - - return title - -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' - - def __unicode__(self): - return self.title - -class Enumeration(ModelCore): - "Abstract enumerations base class" - value = CharField(_('value'), required=True, unique=True) - - def __unicode__(self): - return self.value - - class Meta(MetaCore): - abstract = True - -class PhysicalFormat(Enumeration): - "Collection physical format" - - class Meta(MetaCore): - db_table = 'physical_formats' - -class PublishingStatus(Enumeration): - "Collection publishing status" - - class Meta(MetaCore): - db_table = 'publishing_status' - -class AcquisitionMode(Enumeration): - "Mode of acquisition of the collection" - - class Meta(MetaCore): - db_table = 'acquisition_modes' - -class MetadataAuthor(Enumeration): - "Collection metadata author" - - class Meta(MetaCore): - db_table = 'metadata_authors' - -class MetadataWriter(Enumeration): - "Collection metadata writer" - - class Meta(MetaCore): - db_table = 'metadata_writers' - -class LegalRight(Enumeration): - "Collection legal rights" - - class Meta(MetaCore): - db_table = 'legal_rights' - -class RecordingContext(Enumeration): - "Collection recording context" - - class Meta(MetaCore): - db_table = 'recording_contexts' - -class AdConversion(Enumeration): - "Collection digital to analog conversion status" - - class Meta(MetaCore): - db_table = 'ad_conversions' - -class VernacularStyle(Enumeration): - "Item vernacular style" - - class Meta(MetaCore): - db_table = 'vernacular_styles' - -class GenericStyle(Enumeration): - "Item generic style" - - class Meta(MetaCore): - db_table = 'generic_styles' - -class Instrument(ModelCore): - "Instrument used in the item" - name = CharField(_('name'), required=True) - - class Meta(MetaCore): - db_table = 'instruments' - - def __unicode__(self): - return self.name - -class InstrumentAlias(ModelCore): - "Instrument other name" - name = CharField(_('name'), required=True) - - class Meta(MetaCore): - db_table = 'instrument_aliases' - - def __unicode__(self): - return self.name - -class InstrumentRelation(ModelCore): - "Instrument family" - instrument = ForeignKey('Instrument', related_name="parent_relation", - verbose_name=_('instrument')) - parent_instrument = ForeignKey('Instrument', related_name="child_relation", - verbose_name=_('parent instrument')) - - class Meta(MetaCore): - db_table = 'instrument_relations' - unique_together = (('instrument', 'parent_instrument'),) - -class InstrumentAliasRelation(ModelCore): - "Instrument family other name" - alias = ForeignKey('InstrumentAlias', related_name="other_name", - verbose_name=_('alias')) - instrument = ForeignKey('InstrumentAlias', related_name="relation", - verbose_name=_('instrument')) - - class Meta(MetaCore): - db_table = 'instrument_alias_relations' - unique_together = (('alias', 'instrument'),) - -class MediaItemPerformance(ModelCore): - "Item performance" - media_item = ForeignKey('MediaItem', related_name="performances", - verbose_name=_('item')) - instrument = WeakForeignKey('Instrument', related_name="performances", - verbose_name=_('scientific instrument')) - alias = WeakForeignKey('InstrumentAlias', related_name="performances", - verbose_name=_('vernacular instrument')) - instruments_num = CharField(_('number')) - musicians = CharField(_('interprets')) - - class Meta(MetaCore): - db_table = 'media_item_performances' - -class User(ModelCore): - "Telemeta user" - LEVEL_CHOICES = (('user', 'user'), ('maintainer', 'maintainer'), ('admin', 'admin')) - - username = CharField(_('username'), primary_key=True, max_length=64, required=True) - level = CharField(_('level'), choices=LEVEL_CHOICES, max_length=32, required=True) - first_name = CharField(_('first name')) - last_name = CharField(_('last name')) - phone = CharField(_('phone')) - email = CharField(_('email')) - - class Meta(MetaCore): - db_table = 'users' - - def __unicode__(self): - return self.username - -class Playlist(ModelCore): - "Item or collection playlist" - owner_username = ForeignKey('User', related_name="playlists", db_column="owner_username") - name = CharField(_('name'), required=True) - - class Meta(MetaCore): - db_table = 'playlists' - - def __unicode__(self): - return self.name - -class PlaylistResource(ModelCore): - "Playlist components" - RESOURCE_TYPE_CHOICES = (('item', 'item'), ('collection', 'collection')) - - playlist = ForeignKey('Playlist', related_name="resources", verbose_name=_('playlist')) - resource_type = CharField(_('resource type'), choices=RESOURCE_TYPE_CHOICES, required=True) - resource = IntegerField(_('resource'), required=True) - - class Meta(MetaCore): - db_table = 'playlist_resources' - -class Location(ModelCore): - "Locations" - OTHER_TYPE = 0 - CONTINENT = 1 - COUNTRY = 2 - TYPE_CHOICES = ((COUNTRY, _('country')), (CONTINENT, _('continent')), (OTHER_TYPE, _('other'))) - - name = CharField(_('name'), unique=True, max_length=150, required=True) - type = IntegerField(_('type'), choices=TYPE_CHOICES, default=OTHER_TYPE, db_index=True) - complete_type = ForeignKey('LocationType', related_name="locations", verbose_name=_('complete type')) - current_location = WeakForeignKey('self', related_name="past_names", - verbose_name=_('current location')) - latitude = FloatField(null=True) - longitude = FloatField(null=True) - is_authoritative = BooleanField(_('authoritative')) - - objects = query.LocationManager() - - def items(self): - return MediaItem.objects.by_location(self) - - def collections(self): - return MediaCollection.objects.by_location(self) - - def ancestors(self, direct=False): - q = Q(descendant_relations__location=self) - if direct: - q &= Q(descendant_relations__is_direct=True) - return Location.objects.filter(q) - - def descendants(self, direct=False): - q = Q(ancestor_relations__ancestor_location=self) - if direct: - q &= Q(ancestor_relations__is_direct=True) - return Location.objects.filter(q) - - def add_child(self, other): - LocationRelation.objects.create(location=other, ancestor_location=self, is_direct=True) - for location in self.ancestors(): - #FIXME: might raise Duplicate Entry - LocationRelation.objects.create(location=other, ancestor_location=location) - - def add_parent(self, other): - LocationRelation.objects.create(location=self, ancestor_location=other, is_direct=True) - for location in self.descendants(): - #FIXME: might raise Duplicate Entry - LocationRelation.objects.create(location=location, ancestor_location=other) - - def countries(self): - if self.type == self.COUNTRY: - return Location.objects.filter(pk=self.id) - return self.ancestors().filter(type=self.COUNTRY) - - def continents(self): - if self.type == self.CONTINENT: - return Location.objects.filter(pk=self.id) - return self.ancestors().filter(type=self.CONTINENT) - - class Meta(MetaCore): - db_table = 'locations' - - def __unicode__(self): - return self.name - - def flatname(self): - if self.type != self.COUNTRY and self.type != self.CONTINENT: - raise Exceptions("Flat names are only supported for countries and continents") - - map = Location.objects.flatname_map() - for flatname in map: - if self.id == map[flatname]: - return flatname - - return None - - def paths(self): - #FIXME: need to handle multiple (polyhierarchical) paths - path = [] - location = self - while location: - path.append(location) - try: - location = location.ancestors(direct=True)[0] - except IndexError: - location = None - return [path] - - def fullnames(self): - names = [] - for path in self.paths(): - names.append(u', '.join([unicode(l) for l in path])) - return names - -class LocationType(ModelCore): - "Location types" - code = CharField(_('identifier'), max_length=64, unique=True, required=True) - name = CharField(_('name'), max_length=150, required=True) - - class Meta(MetaCore): - db_table = 'location_types' - -class LocationAlias(ModelCore): - "Location aliases" - location = ForeignKey('Location', related_name="aliases", verbose_name=_('location')) - alias = CharField(_('alias'), max_length=150, required=True) - is_authoritative = BooleanField(_('authoritative')) - - def __unicode__(self): - return self.alias - - class Meta(MetaCore): - db_table = 'location_aliases' - unique_together = (('location', 'alias'),) - -class LocationRelation(ModelCore): - "Location relations" - location = ForeignKey('Location', related_name="ancestor_relations", verbose_name=_('location')) - ancestor_location = ForeignKey('Location', related_name="descendant_relations", verbose_name=_('ancestor location')) - is_direct = BooleanField(db_index=True) - - class Meta(MetaCore): - db_table = 'location_relations' - unique_together = ('location', 'ancestor_location') - - def __unicode__(self): - sep = ' > ' - if not self.is_direct: - sep = ' >..> ' - return unicode(self.ancestor_location) + sep + unicode(self.location) - -class ContextKeyword(Enumeration): - "Keyword" - - class Meta(MetaCore): - db_table = 'context_keywords' - -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 Publisher(Enumeration): - "Collection publisher" - - class Meta(MetaCore): - db_table = 'publishers' - -class PublisherCollection(ModelCore): - "Collection which belongs to publisher" - publisher = ForeignKey('Publisher', related_name="publisher_collections", verbose_name=_('publisher')) - value = CharField(_('value'), required=True) - - def __unicode__(self): - return self.value - - class Meta(MetaCore): - db_table = 'publisher_collections' - -class Revision(ModelCore): - "Revision made by user" - ELEMENT_TYPE_CHOICES = (('collection', 'collection'), ('item', 'item'), ('part', 'part')) - CHANGE_TYPE_CHOICES = (('import', 'import'), ('create', 'create'), ('update', 'update'), ('delete','delete')) - - element_type = CharField(_('element type'), choices=ELEMENT_TYPE_CHOICES, max_length=16, required=True) - element_id = IntegerField(_('element identifier'), required=True) - change_type = CharField(_('modification type'), choices=CHANGE_TYPE_CHOICES, max_length=16, required=True) - time = DateTimeField(_('time'), auto_now_add=True) - user = ForeignKey('User', db_column='username', related_name="revisions", verbose_name=_('user')) - - @classmethod - def touch(cls, element, user): - "Create or update a revision" - revision = cls(element_type=element.element_type, element_id=element.pk, - user=user, change_type='create') - if element.pk: - try: - element.__class__.objects.get(pk=element.pk) - except ObjectDoesNotExist: - pass - else: - revision.change_type = 'update' - - revision.save() - return revision - - class Meta(MetaCore): - db_table = 'revisions' - -class EthnicGroup(ModelCore): - "Item ethnic group" - name = CharField(_('name'), required=True) - - class Meta(MetaCore): - db_table = 'ethnic_groups' - - def __unicode__(self): - return self.name - -class EthnicGroupAlias(ModelCore): - "Item ethnic group other name" - ethnic_group = ForeignKey('EthnicGroup', related_name="aliases", verbose_name=_('population / social group')) - name = CharField(_('name'), required=True) - - class Meta(MetaCore): - db_table = 'ethnic_group_aliases' - - -class MissingUserError(Exception): - pass - -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 MediaInvalidCodeError(Exception): - pass diff --git a/telemeta/models/cremquery.py b/telemeta/models/cremquery.py deleted file mode 100644 index 1165d7ba..00000000 --- a/telemeta/models/cremquery.py +++ /dev/null @@ -1,406 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 2007 Samalyse 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 - -from django import db -from django.db.models import Manager, Q -from telemeta.models.core import EnhancedQuerySet, EnhancedManager -import re -from django.core.exceptions import ObjectDoesNotExist -from django import db -import _mysql_exceptions -from telemeta.util.unaccent import unaccent_icmp, unaccent - -class CoreQuerySet(EnhancedQuerySet): - "Base class for all query sets" - - def none(self): # redundant with none() in recent Django svn - "Return an empty result set" - return self.extra(where = ["0 = 1"]) - - def word_search_q(self, 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 - - def word_search(self, field, pattern): - return self.filter(self.word_search_q(field, pattern)) - - def _by_change_time(self, type, from_time = None, until_time = None): - "Search between two revision dates" - where = ["element_type = '%s'" % type] - if from_time: - where.append("time >= '%s'" % from_time.strftime('%Y-%m-%d %H:%M:%S')) - if until_time: - where.append("time <= '%s'" % until_time.strftime('%Y-%m-%d %H:%M:%S')) - return self.extra( - where = ["id IN (SELECT DISTINCT element_id FROM revisions WHERE %s)" % " AND ".join(where)]); - -class CoreManager(EnhancedManager): - "Base class for all models managers" - - def none(self, *args, **kwargs): - "" - return self.get_query_set().none(*args, **kwargs) - - def get(self, **kwargs): - if kwargs.has_key('public_id'): - try: - args = kwargs.copy() - args['code'] = kwargs['public_id'] - args.pop('public_id') - return super(CoreManager, self).get(**args) - except ObjectDoesNotExist: - args = kwargs.copy() - args['id'] = kwargs['public_id'] - args.pop('public_id') - return super(CoreManager, self).get(**args) - - return super(CoreManager, self).get(**kwargs) - -class MediaCollectionQuerySet(CoreQuerySet): - - def quick_search(self, pattern): - "Perform a quick search on id, title and creator name" - return self.filter( - self.word_search_q('id', pattern) | - self.word_search_q('title', pattern) | - self.word_search_q('creator', pattern) - ) - - def by_location(self, location): - "Find collections by location" - return self.filter(Q(items__location=location) | Q(items__location__in=location.descendants())).distinct() - - def by_recording_year(self, from_year, to_year=None): - "Find collections by recording year" - if to_year is None: - return (self.filter(recorded_from_year__lte=from_year, recorded_to_year__gte=from_year)) - else: - return (self.filter(Q(recorded_from_year__range=(from_year, to_year)) | - Q(recorded_to_year__range=(from_year, to_year)))) - - def by_publish_year(self, from_year, to_year=None): - "Find collections by publishing year" - if to_year is None: - to_year = from_year - return self.filter(year_published__range=(from_year, to_year)) - - def by_ethnic_group(self, group): - "Find collections by ethnic group" - return self.filter(items__ethnic_group__name=group).distinct() - - def by_change_time(self, from_time=None, until_time=None): - "Find collections between two dates" - return self._by_change_time('collection', from_time, until_time) - - def virtual(self, *args): - qs = self - for f in args: - if f == 'apparent_collector': - qs = qs.extra(select={f: 'IF(media_collections.collector_is_creator, ' - 'media_collections.creator, media_collections.collector)'}) - else: - raise Exception("Unsupported virtual field: %s" % f) - - return qs - -class MediaCollectionManager(CoreManager): - "Manage collection queries" - - def get_query_set(self): - "Return the collection query" - return MediaCollectionQuerySet(self.model) - - def enriched(self): - "Query set with additional virtual fields such as apparent_collector" - return self.get_query_set().virtual('apparent_collector') - - def quick_search(self, *args, **kwargs): - return self.get_query_set().quick_search(*args, **kwargs) - quick_search.__doc__ = MediaCollectionQuerySet.quick_search.__doc__ - - def by_location(self, *args, **kwargs): - return self.get_query_set().by_location(*args, **kwargs) - by_location.__doc__ = MediaCollectionQuerySet.by_location.__doc__ - - def by_recording_year(self, *args, **kwargs): - return self.get_query_set().by_recording_year(*args, **kwargs) - by_recording_year.__doc__ = MediaCollectionQuerySet.by_recording_year.__doc__ - - def by_publish_year(self, *args, **kwargs): - return self.get_query_set().by_publish_year(*args, **kwargs) - by_publish_year.__doc__ = MediaCollectionQuerySet.by_publish_year.__doc__ - - def by_ethnic_group(self, *args, **kwargs): - return self.get_query_set().by_ethnic_group(*args, **kwargs) - by_ethnic_group.__doc__ = MediaCollectionQuerySet.by_ethnic_group.__doc__ - - def by_change_time(self, *args, **kwargs): - return self.get_query_set().by_change_time(*args, **kwargs) - by_change_time.__doc__ = MediaCollectionQuerySet.by_change_time.__doc__ - - @staticmethod - def __name_cmp(obj1, obj2): - return unaccent_icmp(obj1.name, obj2.name) - - def stat_continents(self, only_continent=None): - "Return the number of collections by continents and countries as a tree" - from telemeta.models import MediaItem, Location - - countries = [] - for lid in MediaItem.objects.filter(location__isnull=False).values_list('location', flat=True).distinct(): - location = Location.objects.get(pk=lid) - if not only_continent or (only_continent in location.continents()): - for l in location.countries(): - if not l in countries: - countries.append(l) - - stat = {} - - for country in countries: - count = country.collections().count() - for continent in country.continents(): - if not stat.has_key(continent): - stat[continent] = {} - - stat[continent][country] = count - - keys1 = stat.keys() - keys1.sort(self.__name_cmp) - ordered = [] - for c in keys1: - keys2 = stat[c].keys() - keys2.sort(self.__name_cmp) - sub = [{'location': d, 'count': stat[c][d]} for d in keys2] - ordered.append({'location': c, 'countries': sub}) - - return ordered - - -class MediaItemQuerySet(CoreQuerySet): - "Base class for all media item query sets" - - def quick_search(self, pattern): - "Perform a quick search on id and title" - return self.filter( - self.word_search_q('id', pattern) | - self.word_search_q('title', pattern) | - self.word_search_q('author', pattern) - ) - - def without_collection(self): - "Find items which do not belong to any collection" - return self.extra( - where = ["collection_id NOT IN (SELECT id FROM media_collections)"]); - - def by_recording_date(self, from_date, to_date = None): - "Find items by recording date" - if to_date is None: - return (self.filter(recorded_from_date__lte=from_date, recorded_to_date__gte=from_date)) - else : - return (self.filter(Q(recorded_from_date__range=(from_date, to_date)) - | Q(recorded_to_date__range=(from_date, to_date)))) - - def by_title(self, pattern): - "Find items by title" - # to (sort of) sync with models.media.MediaItem.get_title() - return self.filter(self.word_search_q("title", pattern) | self.word_search_q("collection__title", pattern)) - - def by_publish_year(self, from_year, to_year = None): - "Find items by publishing year" - if to_year is None: - to_year = from_year - return self.filter(collection__year_published__range=(from_year, to_year)) - - def by_change_time(self, from_time = None, until_time = None): - "Find items by last change time" - return self._by_change_time('item', from_time, until_time) - - def by_location(self, location): - "Find items by location" - from telemeta.models import LocationRelation - descendants = LocationRelation.objects.filter(ancestor_location=location) - return self.filter(Q(location=location) | Q(location__in=descendants)) - - @staticmethod - def __name_cmp(obj1, obj2): - return unaccent_icmp(obj1.name, obj2.name) - - def countries(self, group_by_continent=False): - from telemeta.models import Location - countries = [] - for id in self.filter(location__isnull=False).values_list('location', flat=True).distinct(): - location = Location.objects.get(pk=id) - for l in location.countries(): - if not l in countries: - countries.append(l) - - if group_by_continent: - grouped = {} - - for country in countries: - for continent in country.continents(): - if not grouped.has_key(continent): - grouped[continent] = [] - - grouped[continent].append(country) - - keys = grouped.keys() - keys.sort(self.__name_cmp) - ordered = [] - for c in keys: - grouped[c].sort(self.__name_cmp) - ordered.append({'continent': c, 'countries': grouped[c]}) - - countries = ordered - - return countries - - def virtual(self, *args): - qs = self - need_collection = False - related = [] - for f in args: - if f == 'apparent_collector': - related.append('collection') - qs = qs.extra(select={f: - 'IF(collector_from_collection, ' - 'IF(media_collections.collector_is_creator, ' - 'media_collections.creator, ' - 'media_collections.collector),' - 'media_items.collector)'}) - elif f == 'country_or_continent': - from telemeta.models import Location - related.append('location') - qs = qs.extra(select={f: - 'IF(locations.type = ' + str(Location.COUNTRY) + ' ' - 'OR locations.type = ' + str(Location.CONTINENT) + ',' - 'locations.name, ' - '(SELECT l2.name FROM location_relations AS r INNER JOIN locations AS l2 ' - 'ON r.ancestor_location_id = l2.id ' - 'WHERE r.location_id = media_items.location_id AND l2.type = ' + str(Location.COUNTRY) + ' ))' - }) - else: - raise Exception("Unsupported virtual field: %s" % f) - - if related: - qs = qs.select_related(*related) - - return qs - - def ethnic_groups(self): - return self.filter(ethnic_group__isnull=False) \ - .values_list('ethnic_group__name', flat=True) \ - .distinct().order_by('ethnic_group__name') - -class MediaItemManager(CoreManager): - "Manage media items queries" - - def get_query_set(self): - "Return media query sets" - return MediaItemQuerySet(self.model) - - def enriched(self): - "Query set with additional virtual fields such as apparent_collector and country_or_continent" - return self.get_query_set().virtual('apparent_collector', 'country_or_continent') - - def quick_search(self, *args, **kwargs): - return self.get_query_set().quick_search(*args, **kwargs) - quick_search.__doc__ = MediaItemQuerySet.quick_search.__doc__ - - def without_collection(self, *args, **kwargs): - return self.get_query_set().without_collection(*args, **kwargs) - without_collection.__doc__ = MediaItemQuerySet.without_collection.__doc__ - - def by_recording_date(self, *args, **kwargs): - return self.get_query_set().by_recording_date(*args, **kwargs) - by_recording_date.__doc__ = MediaItemQuerySet.by_recording_date.__doc__ - - def by_title(self, *args, **kwargs): - return self.get_query_set().by_title(*args, **kwargs) - by_title.__doc__ = MediaItemQuerySet.by_title.__doc__ - - def by_publish_year(self, *args, **kwargs): - return self.get_query_set().by_publish_year(*args, **kwargs) - by_publish_year.__doc__ = MediaItemQuerySet.by_publish_year.__doc__ - - def by_change_time(self, *args, **kwargs): - return self.get_query_set().by_change_time(*args, **kwargs) - by_change_time.__doc__ = MediaItemQuerySet.by_change_time.__doc__ - - def by_location(self, *args, **kwargs): - return self.get_query_set().by_location(*args, **kwargs) - by_location.__doc__ = MediaItemQuerySet.by_location.__doc__ - -class LocationQuerySet(CoreQuerySet): - def by_flatname(self, flatname): - map = LocationManager.flatname_map() - return self.filter(pk=map[flatname]) - -class LocationManager(CoreManager): - __flatname_map = None - - def get_query_set(self): - "Return location query set" - return LocationQuerySet(self.model) - - @classmethod - def flatname_map(cls): - if cls.__flatname_map: - return cls.__flatname_map - - from telemeta.models import Location - map = {} - locations = Location.objects.filter(Q(type=Location.COUNTRY) | Q(type=Location.CONTINENT)) - for l in locations: - flatname = unaccent(l.name).lower() - flatname = re.sub('[^a-z]', '_', flatname) - while map.has_key(flatname): - flatname = '_' + flatname - map[flatname] = l.id - - cls.__flatname_map = map - return map - - def by_flatname(self, *args, **kwargs): - return self.get_query_set().by_flatname(*args, **kwargs) - by_flatname.__doc__ = LocationQuerySet.by_flatname.__doc__ - - diff --git a/telemeta/models/enum.py b/telemeta/models/enum.py new file mode 100644 index 00000000..619a11d1 --- /dev/null +++ b/telemeta/models/enum.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2007-2010 Samalyse 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 + +from telemeta.models.core import * +from django.utils.translation import ugettext_lazy as _ + +class Enumeration(ModelCore): + "Abstract enumerations base class" + value = CharField(_('value'), required=True, unique=True) + + def __unicode__(self): + return self.value + + class Meta(MetaCore): + abstract = True + +class PhysicalFormat(Enumeration): + "Collection physical format" + + class Meta(MetaCore): + db_table = 'physical_formats' + +class PublishingStatus(Enumeration): + "Collection publishing status" + + class Meta(MetaCore): + db_table = 'publishing_status' + +class AcquisitionMode(Enumeration): + "Mode of acquisition of the collection" + + class Meta(MetaCore): + db_table = 'acquisition_modes' + +class MetadataAuthor(Enumeration): + "Collection metadata author" + + class Meta(MetaCore): + db_table = 'metadata_authors' + +class MetadataWriter(Enumeration): + "Collection metadata writer" + + class Meta(MetaCore): + db_table = 'metadata_writers' + +class LegalRight(Enumeration): + "Collection legal rights" + + class Meta(MetaCore): + db_table = 'legal_rights' + +class RecordingContext(Enumeration): + "Collection recording context" + + class Meta(MetaCore): + db_table = 'recording_contexts' + +class AdConversion(Enumeration): + "Collection digital to analog conversion status" + + class Meta(MetaCore): + db_table = 'ad_conversions' + +class VernacularStyle(Enumeration): + "Item vernacular style" + + class Meta(MetaCore): + db_table = 'vernacular_styles' + +class GenericStyle(Enumeration): + "Item generic style" + + class Meta(MetaCore): + db_table = 'generic_styles' + +class ContextKeyword(Enumeration): + "Keyword" + + class Meta(MetaCore): + db_table = 'context_keywords' + +class Publisher(Enumeration): + "Collection publisher" + + class Meta(MetaCore): + db_table = 'publishers' + +class PublisherCollection(ModelCore): + "Collection which belongs to publisher" + publisher = ForeignKey('Publisher', related_name="publisher_collections", verbose_name=_('publisher')) + value = CharField(_('value'), required=True) + + def __unicode__(self): + return self.value + + class Meta(MetaCore): + db_table = 'publisher_collections' + +class EthnicGroup(ModelCore): + "Item ethnic group" + name = CharField(_('name'), required=True) + + class Meta(MetaCore): + db_table = 'ethnic_groups' + + def __unicode__(self): + return self.name + +class EthnicGroupAlias(ModelCore): + "Item ethnic group other name" + ethnic_group = ForeignKey('EthnicGroup', related_name="aliases", verbose_name=_('population / social group')) + name = CharField(_('name'), required=True) + + class Meta(MetaCore): + db_table = 'ethnic_group_aliases' + + diff --git a/telemeta/models/instrument.py b/telemeta/models/instrument.py new file mode 100644 index 00000000..4e4b9837 --- /dev/null +++ b/telemeta/models/instrument.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2007-2010 Samalyse 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 + +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) + + class Meta(MetaCore): + db_table = 'instruments' + + def __unicode__(self): + return self.name + +class InstrumentAlias(ModelCore): + "Instrument other name" + name = CharField(_('name'), required=True) + + class Meta(MetaCore): + db_table = 'instrument_aliases' + + def __unicode__(self): + return self.name + +class InstrumentRelation(ModelCore): + "Instrument family" + instrument = ForeignKey('Instrument', related_name="parent_relation", + verbose_name=_('instrument')) + parent_instrument = ForeignKey('Instrument', related_name="child_relation", + verbose_name=_('parent instrument')) + + class Meta(MetaCore): + db_table = 'instrument_relations' + unique_together = (('instrument', 'parent_instrument'),) + +class InstrumentAliasRelation(ModelCore): + "Instrument family other name" + alias = ForeignKey('InstrumentAlias', related_name="other_name", + verbose_name=_('alias')) + instrument = ForeignKey('InstrumentAlias', related_name="relation", + verbose_name=_('instrument')) + + class Meta(MetaCore): + db_table = 'instrument_alias_relations' + unique_together = (('alias', 'instrument'),) + diff --git a/telemeta/models/location.py b/telemeta/models/location.py new file mode 100644 index 00000000..d5c660d5 --- /dev/null +++ b/telemeta/models/location.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2007 Samalyse 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 + +from telemeta.models.core import * +from telemeta.util.unaccent import unaccent +import re +from django.db.models import Q +from django.utils.translation import ugettext_lazy as _ +from telemeta.models import query + +class Location(ModelCore): + "Locations" + OTHER_TYPE = 0 + CONTINENT = 1 + COUNTRY = 2 + TYPE_CHOICES = ((COUNTRY, _('country')), (CONTINENT, _('continent')), (OTHER_TYPE, _('other'))) + + name = CharField(_('name'), unique=True, max_length=150, required=True) + type = IntegerField(_('type'), choices=TYPE_CHOICES, default=OTHER_TYPE, db_index=True) + complete_type = ForeignKey('LocationType', related_name="locations", verbose_name=_('complete type')) + current_location = WeakForeignKey('self', related_name="past_names", + verbose_name=_('current location')) + latitude = FloatField(null=True) + longitude = FloatField(null=True) + is_authoritative = BooleanField(_('authoritative')) + + objects = query.LocationManager() + + def items(self): + from telemeta.models import MediaItem + return MediaItem.objects.by_location(self) + + def collections(self): + from telemeta.models import MediaCollection + return MediaCollection.objects.by_location(self) + + def ancestors(self, direct=False): + q = Q(descendant_relations__location=self) + if direct: + q &= Q(descendant_relations__is_direct=True) + return Location.objects.filter(q) + + def descendants(self, direct=False): + q = Q(ancestor_relations__ancestor_location=self) + if direct: + q &= Q(ancestor_relations__is_direct=True) + return Location.objects.filter(q) + + def add_child(self, other): + LocationRelation.objects.create(location=other, ancestor_location=self, is_direct=True) + for location in self.ancestors(): + #FIXME: might raise Duplicate Entry + LocationRelation.objects.create(location=other, ancestor_location=location) + + def add_parent(self, other): + LocationRelation.objects.create(location=self, ancestor_location=other, is_direct=True) + for location in self.descendants(): + #FIXME: might raise Duplicate Entry + LocationRelation.objects.create(location=location, ancestor_location=other) + + def countries(self): + if self.type == self.COUNTRY: + return Location.objects.filter(pk=self.id) + return self.ancestors().filter(type=self.COUNTRY) + + def continents(self): + if self.type == self.CONTINENT: + return Location.objects.filter(pk=self.id) + return self.ancestors().filter(type=self.CONTINENT) + + class Meta(MetaCore): + db_table = 'locations' + + def __unicode__(self): + return self.name + + def flatname(self): + if self.type != self.COUNTRY and self.type != self.CONTINENT: + raise Exception("Flat names are only supported for countries and continents") + + map = Location.objects.flatname_map() + for flatname in map: + if self.id == map[flatname]: + return flatname + + return None + + def paths(self): + #FIXME: need to handle multiple (polyhierarchical) paths + path = [] + location = self + while location: + path.append(location) + try: + location = location.ancestors(direct=True)[0] + except IndexError: + location = None + return [path] + + def fullnames(self): + names = [] + for path in self.paths(): + names.append(u', '.join([unicode(l) for l in path])) + return names + +class LocationType(ModelCore): + "Location types" + code = CharField(_('identifier'), max_length=64, unique=True, required=True) + name = CharField(_('name'), max_length=150, required=True) + + class Meta(MetaCore): + db_table = 'location_types' + +class LocationAlias(ModelCore): + "Location aliases" + location = ForeignKey('Location', related_name="aliases", verbose_name=_('location')) + alias = CharField(_('alias'), max_length=150, required=True) + is_authoritative = BooleanField(_('authoritative')) + + def __unicode__(self): + return self.alias + + class Meta(MetaCore): + db_table = 'location_aliases' + unique_together = (('location', 'alias'),) + +class LocationRelation(ModelCore): + "Location relations" + location = ForeignKey('Location', related_name="ancestor_relations", verbose_name=_('location')) + ancestor_location = ForeignKey('Location', related_name="descendant_relations", verbose_name=_('ancestor location')) + is_direct = BooleanField(db_index=True) + + class Meta(MetaCore): + db_table = 'location_relations' + unique_together = ('location', 'ancestor_location') + + def __unicode__(self): + sep = ' > ' + if not self.is_direct: + sep = ' >..> ' + return unicode(self.ancestor_location) + sep + unicode(self.location) + diff --git a/telemeta/models/media.py b/telemeta/models/media.py new file mode 100644 index 00000000..5479c2b8 --- /dev/null +++ b/telemeta/models/media.py @@ -0,0 +1,370 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2007-2010 Samalyse 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 + +from django.utils.translation import ugettext_lazy as _ +from telemeta.models.core import * +from telemeta.models.enum import ContextKeyword +from telemeta.util.unaccent import unaccent_icmp +import re +from telemeta.models.location import LocationRelation, Location +from telemeta.models.system import Revision +from telemeta.models import query + +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 = _('public access') + + def save_with_revision(self, user, force_insert=False, force_update=False, using=None): + "Save a media object and add a revision" + self.save(force_insert, force_update, using) + 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 MediaCollection(MediaResource): + "Describe a collection of items" + element_type = 'collection' + PUBLIC_ACCESS_CHOICES = (('none', 'none'), ('metadata', 'metadata'), ('full', 'full')) + + published_code_regex = 'CNRSMH_E_[0-9]{4}(?:_[0-9]{3}){2}' + unpublished_code_regex = 'CNRSMH_I_[0-9]{4}_[0-9]{3}' + code_regex = '(?:%s|%s)' % (published_code_regex, unpublished_code_regex) + + reference = CharField(_('reference'), unique=True, null=True) + physical_format = WeakForeignKey('PhysicalFormat', related_name="collections", + verbose_name=_('archive format')) + old_code = CharField(_('old code'), unique=True, null=True) + code = CharField(_('code'), unique=True, required=True) + title = CharField(_('title'), required=True) + alt_title = CharField(_('original title / translation')) + physical_items_num = IntegerField(_('number of components (medium / piece)')) + publishing_status = WeakForeignKey('PublishingStatus', related_name="collections", + verbose_name=_('secondary edition')) + creator = CharField(_('depositor / contributor')) + booklet_author = CharField(_('author of published notice')) + booklet_description = TextField(_('related documentation')) + collector = CharField(_('recordist')) + collector_is_creator = BooleanField(_('recordist identical to depositor')) + publisher = WeakForeignKey('Publisher', related_name="collections", + verbose_name=_('publisher / status')) + is_published = BooleanField(_('published')) + year_published = IntegerField(_('year published')) + publisher_collection = WeakForeignKey('PublisherCollection', related_name="collections", + verbose_name=_('publisher collection')) + publisher_serial = CharField(_('publisher serial number')) + external_references = TextField(_('bibliographic references')) + acquisition_mode = WeakForeignKey('AcquisitionMode', related_name="collections", + verbose_name=_('mode of acquisition')) + comment = TextField(_('comment')) + metadata_author = WeakForeignKey('MetadataAuthor', related_name="collections", + verbose_name=_('record author')) + metadata_writer = WeakForeignKey('MetadataWriter', related_name="collections", + verbose_name=_('record writer')) + legal_rights = WeakForeignKey('LegalRight', related_name="collections", + verbose_name=_('legal rights')) + alt_ids = CharField(_('copies')) + recorded_from_year = IntegerField(_('recording year (from)')) + recorded_to_year = IntegerField(_('recording year (until)')) + recording_context = WeakForeignKey('RecordingContext', related_name="collections", + verbose_name=_('recording context')) + approx_duration = DurationField(_('approximative duration')) + doctype_code = IntegerField(_('document type')) + travail = CharField(_('archiver notes')) + state = TextField(_('status')) + cnrs_contributor = CharField(_('CNRS depositor')) + items_done = CharField(_('items finished')) + a_informer_07_03 = CharField(_('a_informer_07_03')) + ad_conversion = WeakForeignKey('AdConversion', related_name='collections', + verbose_name=_('A/D conversion')) + public_access = CharField(_('public access'), choices=PUBLIC_ACCESS_CHOICES, + max_length=16, default="metadata") + + objects = query.MediaCollectionManager() + + def __unicode__(self): + if self.title: + return self.title + + return self.code + + @property + def public_id(self): + return self.code + + 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 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) + + groups.sort(self.__name_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 is_valid_code(self, code): + "Check if the collection code is well formed" + if self.is_published: + regex = '^' + self.published_code_regex + '$' + else: + regex = '^' + self.unpublished_code_regex + '$' + + if re.match(regex, code): + return True + + return False + + def save(self, force_insert=False, force_update=False, using=None): + if not self.code: + raise RequiredFieldError(self, self._meta.get_field('code')) + if not self.is_valid_code(self.code): + raise MediaInvalidCodeError("%s is not a valid code for this collection" % self.code) + super(MediaCollection, self).save(force_insert, force_update, using) + + class Meta(MetaCore): + db_table = 'media_collections' + +class MediaItem(MediaResource): + "Describe an item" + element_type = 'item' + PUBLIC_ACCESS_CHOICES = (('none', 'none'), ('metadata', 'metadata'), ('full', 'full')) + + published_code_regex = MediaCollection.published_code_regex + '(?:_[0-9]{2}){1,2}' + unpublished_code_regex = MediaCollection.unpublished_code_regex + '_[0-9]{2,3}(?:_[0-9]{2}){0,2}' + code_regex = '(?:%s|%s)' % (published_code_regex, unpublished_code_regex) + + collection = ForeignKey('MediaCollection', related_name="items", + verbose_name=_('collection')) + track = CharField(_('item number')) + old_code = CharField(_('old code'), unique=True, null=True) + code = CharField(_('code'), unique=True, null=True) + approx_duration = DurationField(_('approximative duration')) + recorded_from_date = DateField(_('recording date (from)')) + recorded_to_date = DateField(_('recording date (until)')) + location = WeakForeignKey('Location', verbose_name=_('location')) + location_comment = CharField(_('location details')) + ethnic_group = WeakForeignKey('EthnicGroup', related_name="items", + verbose_name=_('population / social group')) + title = CharField(_('title'), required=True) + alt_title = CharField(_('original title / translation')) + author = CharField(_('author')) + vernacular_style = WeakForeignKey('VernacularStyle', related_name="items", + verbose_name=_('vernacular name')) + context_comment = TextField(_('comments')) + external_references = TextField(_('published reference')) + moda_execut = CharField(_('moda_execut')) + copied_from_item = WeakForeignKey('self', related_name="copies", verbose_name=_('copy of')) + collector = CharField(_('recordist')) + collector_from_collection = BooleanField(_('recordist as in collection')) + cultural_area = CharField(_('cultural area')) + generic_style = WeakForeignKey('GenericStyle', related_name="items", + verbose_name=_('generic name')) + collector_selection = CharField(_('recordist selection')) + creator_reference = CharField(_('reference')) + comment = TextField(_('comment')) + file = FileField(_('file'), upload_to='items/%Y/%m/%d', db_column="filename") + public_access = CharField(_('public access'), choices=PUBLIC_ACCESS_CHOICES, max_length=16, default="metadata") + + objects = query.MediaItemManager() + + 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 self.id + + class Meta(MetaCore): + db_table = 'media_items' + + 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 = '^' + self.published_code_regex + '$' + else: + regex = '^' + self.unpublished_code_regex + '$' + + if re.match(regex, code): + return True + + return False + + def save(self, force_insert=False, force_update=False, using=None): + if not self.code: + raise RequiredFieldError(self, self._meta.get_field('code')) + if not self.is_valid_code(self.code): + raise MediaInvalidCodeError("%s is not a valid item code for collection %s" + % (self.code, self.collection.code)) + super(MediaItem, self).save(force_insert, force_update, using) + + def computed_duration(self): + "Tell the length in seconds of this item media data" + # FIXME: use TimeSide? + seconds = 0 + if self.file: + import wave + media = wave.open(self.file.path, "rb") + seconds = media.getnframes() / media.getframerate() + media.close() + + return Duration(seconds=seconds) + + 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) + + if self.track: + title += ' ' + self.track + + return title + +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=_('scientific instrument')) + alias = WeakForeignKey('InstrumentAlias', related_name="performances", + verbose_name=_('vernacular instrument')) + instruments_num = CharField(_('number')) + musicians = CharField(_('interprets')) + + class Meta(MetaCore): + db_table = 'media_item_performances' + +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' + + def __unicode__(self): + return self.title + +class Playlist(ModelCore): + "Item or collection playlist" + owner_username = ForeignKey('User', related_name="playlists", db_column="owner_username") + name = CharField(_('name'), required=True) + + class Meta(MetaCore): + db_table = 'playlists' + + def __unicode__(self): + return self.name + +class PlaylistResource(ModelCore): + "Playlist components" + RESOURCE_TYPE_CHOICES = (('item', 'item'), ('collection', 'collection')) + + playlist = ForeignKey('Playlist', related_name="resources", verbose_name=_('playlist')) + resource_type = CharField(_('resource type'), choices=RESOURCE_TYPE_CHOICES, required=True) + resource = IntegerField(_('resource'), required=True) + + class Meta(MetaCore): + db_table = 'playlist_resources' + +class MediaInvalidCodeError(Exception): + pass + diff --git a/telemeta/models/query.py b/telemeta/models/query.py new file mode 100644 index 00000000..e753859b --- /dev/null +++ b/telemeta/models/query.py @@ -0,0 +1,350 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2007-2010 Samalyse 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 + +from django.db.models import Q +from telemeta.models.core import * +from telemeta.util.unaccent import unaccent, unaccent_icmp +import re + +class MediaItemQuerySet(CoreQuerySet): + "Base class for all media item query sets" + + def quick_search(self, pattern): + "Perform a quick search on id and title" + return self.filter( + self.word_search_q('id', pattern) | + self.word_search_q('title', pattern) | + self.word_search_q('author', pattern) + ) + + def without_collection(self): + "Find items which do not belong to any collection" + return self.extra( + where = ["collection_id NOT IN (SELECT id FROM media_collections)"]); + + def by_recording_date(self, from_date, to_date = None): + "Find items by recording date" + if to_date is None: + return (self.filter(recorded_from_date__lte=from_date, recorded_to_date__gte=from_date)) + else : + return (self.filter(Q(recorded_from_date__range=(from_date, to_date)) + | Q(recorded_to_date__range=(from_date, to_date)))) + + def by_title(self, pattern): + "Find items by title" + # to (sort of) sync with models.media.MediaItem.get_title() + return self.filter(self.word_search_q("title", pattern) | self.word_search_q("collection__title", pattern)) + + def by_publish_year(self, from_year, to_year = None): + "Find items by publishing year" + if to_year is None: + to_year = from_year + return self.filter(collection__year_published__range=(from_year, to_year)) + + def by_change_time(self, from_time = None, until_time = None): + "Find items by last change time" + return self._by_change_time('item', from_time, until_time) + + def by_location(self, location): + "Find items by location" + from telemeta.models import LocationRelation + descendants = LocationRelation.objects.filter(ancestor_location=location) + return self.filter(Q(location=location) | Q(location__in=descendants)) + + @staticmethod + def __name_cmp(obj1, obj2): + return unaccent_icmp(obj1.name, obj2.name) + + def countries(self, group_by_continent=False): + countries = [] + from telemeta.models import Location + for id in self.filter(location__isnull=False).values_list('location', flat=True).distinct(): + location = Location.objects.get(pk=id) + for l in location.countries(): + if not l in countries: + countries.append(l) + + if group_by_continent: + grouped = {} + + for country in countries: + for continent in country.continents(): + if not grouped.has_key(continent): + grouped[continent] = [] + + grouped[continent].append(country) + + keys = grouped.keys() + keys.sort(self.__name_cmp) + ordered = [] + for c in keys: + grouped[c].sort(self.__name_cmp) + ordered.append({'continent': c, 'countries': grouped[c]}) + + countries = ordered + + return countries + + def virtual(self, *args): + qs = self + need_collection = False + related = [] + from telemeta.models import Location + for f in args: + if f == 'apparent_collector': + related.append('collection') + qs = qs.extra(select={f: + 'IF(collector_from_collection, ' + 'IF(media_collections.collector_is_creator, ' + 'media_collections.creator, ' + 'media_collections.collector),' + 'media_items.collector)'}) + elif f == 'country_or_continent': + related.append('location') + qs = qs.extra(select={f: + 'IF(locations.type = ' + str(Location.COUNTRY) + ' ' + 'OR locations.type = ' + str(Location.CONTINENT) + ',' + 'locations.name, ' + '(SELECT l2.name FROM location_relations AS r INNER JOIN locations AS l2 ' + 'ON r.ancestor_location_id = l2.id ' + 'WHERE r.location_id = media_items.location_id AND l2.type = ' + str(Location.COUNTRY) + ' ))' + }) + else: + raise Exception("Unsupported virtual field: %s" % f) + + if related: + qs = qs.select_related(*related) + + return qs + + def ethnic_groups(self): + return self.filter(ethnic_group__isnull=False) \ + .values_list('ethnic_group__name', flat=True) \ + .distinct().order_by('ethnic_group__name') + +class MediaItemManager(CoreManager): + "Manage media items queries" + + def get_query_set(self): + "Return media query sets" + return MediaItemQuerySet(self.model) + + def enriched(self): + "Query set with additional virtual fields such as apparent_collector and country_or_continent" + return self.get_query_set().virtual('apparent_collector', 'country_or_continent') + + def quick_search(self, *args, **kwargs): + return self.get_query_set().quick_search(*args, **kwargs) + quick_search.__doc__ = MediaItemQuerySet.quick_search.__doc__ + + def without_collection(self, *args, **kwargs): + return self.get_query_set().without_collection(*args, **kwargs) + without_collection.__doc__ = MediaItemQuerySet.without_collection.__doc__ + + def by_recording_date(self, *args, **kwargs): + return self.get_query_set().by_recording_date(*args, **kwargs) + by_recording_date.__doc__ = MediaItemQuerySet.by_recording_date.__doc__ + + def by_title(self, *args, **kwargs): + return self.get_query_set().by_title(*args, **kwargs) + by_title.__doc__ = MediaItemQuerySet.by_title.__doc__ + + def by_publish_year(self, *args, **kwargs): + return self.get_query_set().by_publish_year(*args, **kwargs) + by_publish_year.__doc__ = MediaItemQuerySet.by_publish_year.__doc__ + + def by_change_time(self, *args, **kwargs): + return self.get_query_set().by_change_time(*args, **kwargs) + by_change_time.__doc__ = MediaItemQuerySet.by_change_time.__doc__ + + def by_location(self, *args, **kwargs): + return self.get_query_set().by_location(*args, **kwargs) + by_location.__doc__ = MediaItemQuerySet.by_location.__doc__ + +class MediaCollectionQuerySet(CoreQuerySet): + + def quick_search(self, pattern): + "Perform a quick search on id, title and creator name" + return self.filter( + self.word_search_q('id', pattern) | + self.word_search_q('title', pattern) | + self.word_search_q('creator', pattern) + ) + + def by_location(self, location): + "Find collections by location" + return self.filter(Q(items__location=location) | Q(items__location__in=location.descendants())).distinct() + + def by_recording_year(self, from_year, to_year=None): + "Find collections by recording year" + if to_year is None: + return (self.filter(recorded_from_year__lte=from_year, recorded_to_year__gte=from_year)) + else: + return (self.filter(Q(recorded_from_year__range=(from_year, to_year)) | + Q(recorded_to_year__range=(from_year, to_year)))) + + def by_publish_year(self, from_year, to_year=None): + "Find collections by publishing year" + if to_year is None: + to_year = from_year + return self.filter(year_published__range=(from_year, to_year)) + + def by_ethnic_group(self, group): + "Find collections by ethnic group" + return self.filter(items__ethnic_group__name=group).distinct() + + def by_change_time(self, from_time=None, until_time=None): + "Find collections between two dates" + return self._by_change_time('collection', from_time, until_time) + + def virtual(self, *args): + qs = self + for f in args: + if f == 'apparent_collector': + qs = qs.extra(select={f: 'IF(media_collections.collector_is_creator, ' + 'media_collections.creator, media_collections.collector)'}) + else: + raise Exception("Unsupported virtual field: %s" % f) + + return qs + +class MediaCollectionManager(CoreManager): + "Manage collection queries" + + def get_query_set(self): + "Return the collection query" + return MediaCollectionQuerySet(self.model) + + def enriched(self): + "Query set with additional virtual fields such as apparent_collector" + return self.get_query_set().virtual('apparent_collector') + + def quick_search(self, *args, **kwargs): + return self.get_query_set().quick_search(*args, **kwargs) + quick_search.__doc__ = MediaCollectionQuerySet.quick_search.__doc__ + + def by_location(self, *args, **kwargs): + return self.get_query_set().by_location(*args, **kwargs) + by_location.__doc__ = MediaCollectionQuerySet.by_location.__doc__ + + def by_recording_year(self, *args, **kwargs): + return self.get_query_set().by_recording_year(*args, **kwargs) + by_recording_year.__doc__ = MediaCollectionQuerySet.by_recording_year.__doc__ + + def by_publish_year(self, *args, **kwargs): + return self.get_query_set().by_publish_year(*args, **kwargs) + by_publish_year.__doc__ = MediaCollectionQuerySet.by_publish_year.__doc__ + + def by_ethnic_group(self, *args, **kwargs): + return self.get_query_set().by_ethnic_group(*args, **kwargs) + by_ethnic_group.__doc__ = MediaCollectionQuerySet.by_ethnic_group.__doc__ + + def by_change_time(self, *args, **kwargs): + return self.get_query_set().by_change_time(*args, **kwargs) + by_change_time.__doc__ = MediaCollectionQuerySet.by_change_time.__doc__ + + @staticmethod + def __name_cmp(obj1, obj2): + return unaccent_icmp(obj1.name, obj2.name) + + def stat_continents(self, only_continent=None): + "Return the number of collections by continents and countries as a tree" + + countries = [] + for lid in MediaItem.objects.filter(location__isnull=False).values_list('location', flat=True).distinct(): + location = Location.objects.get(pk=lid) + if not only_continent or (only_continent in location.continents()): + for l in location.countries(): + if not l in countries: + countries.append(l) + + stat = {} + + for country in countries: + count = country.collections().count() + for continent in country.continents(): + if not stat.has_key(continent): + stat[continent] = {} + + stat[continent][country] = count + + keys1 = stat.keys() + keys1.sort(self.__name_cmp) + ordered = [] + for c in keys1: + keys2 = stat[c].keys() + keys2.sort(self.__name_cmp) + sub = [{'location': d, 'count': stat[c][d]} for d in keys2] + ordered.append({'location': c, 'countries': sub}) + + return ordered + +class LocationQuerySet(CoreQuerySet): + __flatname_map = None + + def by_flatname(self, flatname): + map = self.flatname_map() + return self.filter(pk=map[flatname]) + + def flatname_map(self): + if self.__class__.__flatname_map: + return self.__class__.__flatname_map + + map = {} + locations = self.filter(Q(type=self.model.COUNTRY) | Q(type=self.model.CONTINENT)) + for l in locations: + flatname = unaccent(l.name).lower() + flatname = re.sub('[^a-z]', '_', flatname) + while map.has_key(flatname): + flatname = '_' + flatname + map[flatname] = l.id + + self.__class__.__flatname_map = map + return map + +class LocationManager(CoreManager): + + def get_query_set(self): + "Return location query set" + return LocationQuerySet(self.model) + + def by_flatname(self, *args, **kwargs): + return self.get_query_set().by_flatname(*args, **kwargs) + by_flatname.__doc__ = LocationQuerySet.by_flatname.__doc__ + + def flatname_map(self, *args, **kwargs): + return self.get_query_set().flatname_map(*args, **kwargs) + flatname_map.__doc__ = LocationQuerySet.flatname_map.__doc__ + diff --git a/telemeta/models/system.py b/telemeta/models/system.py new file mode 100644 index 00000000..4f6f327d --- /dev/null +++ b/telemeta/models/system.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2007-2010 Samalyse 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 + +from telemeta.models.core import * +from django.core.exceptions import ObjectDoesNotExist +from django.utils.translation import ugettext_lazy as _ + +class User(ModelCore): + "Telemeta user" + LEVEL_CHOICES = (('user', 'user'), ('maintainer', 'maintainer'), ('admin', 'admin')) + + username = CharField(_('username'), primary_key=True, max_length=64, required=True) + level = CharField(_('level'), choices=LEVEL_CHOICES, max_length=32, required=True) + first_name = CharField(_('first name')) + last_name = CharField(_('last name')) + phone = CharField(_('phone')) + email = CharField(_('email')) + + class Meta(MetaCore): + db_table = 'users' + + def __unicode__(self): + return self.username + +class Revision(ModelCore): + "Revision made by user" + ELEMENT_TYPE_CHOICES = (('collection', 'collection'), ('item', 'item'), ('part', 'part')) + CHANGE_TYPE_CHOICES = (('import', 'import'), ('create', 'create'), ('update', 'update'), ('delete','delete')) + + element_type = CharField(_('element type'), choices=ELEMENT_TYPE_CHOICES, max_length=16, required=True) + element_id = IntegerField(_('element identifier'), required=True) + change_type = CharField(_('modification type'), choices=CHANGE_TYPE_CHOICES, max_length=16, required=True) + time = DateTimeField(_('time'), auto_now_add=True) + user = ForeignKey('User', db_column='username', related_name="revisions", verbose_name=_('user')) + + @classmethod + def touch(cls, element, user): + "Create or update a revision" + revision = cls(element_type=element.element_type, element_id=element.pk, + user=user, change_type='create') + if element.pk: + try: + element.__class__.objects.get(pk=element.pk) + except ObjectDoesNotExist: + pass + else: + revision.change_type = 'update' + + revision.save() + return revision + + class Meta(MetaCore): + db_table = 'revisions' +