]> git.parisson.com Git - telemeta.git/commitdiff
split models
authorolivier <>
Wed, 10 Feb 2010 16:42:16 +0000 (16:42 +0000)
committerolivier <>
Wed, 10 Feb 2010 16:42:16 +0000 (16:42 +0000)
telemeta/models/__init__.py
telemeta/models/core.py
telemeta/models/crem.py [deleted file]
telemeta/models/cremquery.py [deleted file]
telemeta/models/enum.py [new file with mode: 0644]
telemeta/models/instrument.py [new file with mode: 0644]
telemeta/models/location.py [new file with mode: 0644]
telemeta/models/media.py [new file with mode: 0644]
telemeta/models/query.py [new file with mode: 0644]
telemeta/models/system.py [new file with mode: 0644]

index 3b4367b06c698b12df5842f28c727629716904f6..678ec369295f671c65f5dcd7fdfd584ef33fd659 100644 (file)
 #
 # Author: Olivier Guilyardi <olivier@samalyse.com>
 
-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
 
index 50455b769c9b8e5daa8f13a45d000e27876ec420..9640ea480c0864193e87123e5eb5d5d4a7d5b507 100644 (file)
@@ -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,
 # 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 <olivier@samalyse.com>
 
+__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 (executable)
index 8dac273..0000000
+++ /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 <olivier@samalyse.com>
-#          David LIPSZYC <davidlipszyc@gmail.com>
-
-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 (file)
index 1165d7b..0000000
+++ /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 <olivier@samalyse.com>
-#          David LIPSZYC <davidlipszyc@gmail.com>
-
-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 (file)
index 0000000..619a11d
--- /dev/null
@@ -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 <olivier@samalyse.com>
+#          David LIPSZYC <davidlipszyc@gmail.com>
+
+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 (file)
index 0000000..4e4b983
--- /dev/null
@@ -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 <olivier@samalyse.com>
+#          David LIPSZYC <davidlipszyc@gmail.com>
+
+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 (file)
index 0000000..d5c660d
--- /dev/null
@@ -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 <olivier@samalyse.com>
+#          David LIPSZYC <davidlipszyc@gmail.com>
+
+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 (file)
index 0000000..5479c2b
--- /dev/null
@@ -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 <olivier@samalyse.com>
+#          David LIPSZYC <davidlipszyc@gmail.com>
+
+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 (file)
index 0000000..e753859
--- /dev/null
@@ -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 <olivier@samalyse.com>
+#          David LIPSZYC <davidlipszyc@gmail.com>
+
+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 (file)
index 0000000..4f6f327
--- /dev/null
@@ -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 <olivier@samalyse.com>
+#          David LIPSZYC <davidlipszyc@gmail.com>
+
+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'
+