source: telemeta/models/media.py @ c2c51a8

cremcrem2crem3devdev2diademsdj1.6feature/breadcrumbsfeature/ts-0.5feature/ts-0.5.4feature/writecachegenericlamlam2mapsv3mergenlivemultirelease/1.4.4sabiodsearchserverstoragetelecaster
Last change on this file since c2c51a8 was c2c51a8, checked in by yomguy <yomguy@…>, 2 years ago

add domain enum

  • Property mode set to 100644
File size: 27.1 KB
Line 
1# -*- coding: utf-8 -*-
2# Copyright (C) 2007-2010 Samalyse SARL
3# Copyright (C) 2010-2011 Parisson SARL
4
5# This software is a computer program whose purpose is to backup, analyse,
6# transcode and stream any audio content with its metadata over a web frontend.
7
8# This software is governed by the CeCILL  license under French law and
9# abiding by the rules of distribution of free software.  You can  use,
10# modify and/ or redistribute the software under the terms of the CeCILL
11# license as circulated by CEA, CNRS and INRIA at the following URL
12# "http://www.cecill.info".
13
14# As a counterpart to the access to the source code and  rights to copy,
15# modify and redistribute granted by the license, users are provided only
16# with a limited warranty  and the software's author,  the holder of the
17# economic rights,  and the successive licensors  have only  limited
18# liability.
19
20# In this respect, the user's attention is drawn to the risks associated
21# with loading,  using,  modifying and/or developing or reproducing the
22# software by the user in light of its specific status of free software,
23# that may mean  that it is complicated to manipulate,  and  that  also
24# therefore means  that it is reserved for developers  and  experienced
25# professionals having in-depth computer knowledge. Users are therefore
26# encouraged to load and test the software's suitability as regards their
27# requirements in conditions enabling the security of their systems and/or
28# data to be ensured and,  more generally, to use and operate it in the
29# same conditions as regards security.
30
31# The fact that you are presently reading this means that you have had
32# knowledge of the CeCILL license and that you accept its terms.
33#
34# Authors: Olivier Guilyardi <olivier@samalyse.com>
35#          David LIPSZYC <davidlipszyc@gmail.com>
36#          Guillaume Pellerin <yomguy@parisson.com>
37
38import re
39import mimetypes
40from django.contrib.auth.models import User
41from django.utils.translation import ugettext_lazy as _
42from django.core.exceptions import ValidationError
43from telemeta.models.core import *
44from telemeta.models.enum import ContextKeyword
45from telemeta.util.unaccent import unaccent_icmp
46from telemeta.models.location import LocationRelation, Location
47from telemeta.models.system import Revision
48from telemeta.models.query import *
49from telemeta.models.instrument import *
50from telemeta.models.enum import *
51from telemeta.models.language import *
52from telemeta.models.format import *
53from django.db import models
54
55collection_published_code_regex   = '[A-Za-z0-9._-]*'
56collection_unpublished_code_regex = '[A-Za-z0-9._-]*'
57collection_code_regex             = '(?:%s|%s)' % (collection_published_code_regex,
58                                                    collection_unpublished_code_regex)
59
60item_published_code_regex    = '[A-Za-z0-9._-]*'
61item_unpublished_code_regex  = '[A-Za-z0-9._-]*'
62item_code_regex              = '(?:%s|%s)' % (item_published_code_regex, item_unpublished_code_regex)
63
64PUBLIC_ACCESS_CHOICES = (('none', 'none'), ('metadata', 'metadata'), ('full', 'full'))
65
66
67class MediaResource(ModelCore):
68    "Base class of all media objects"
69
70    def public_access_label(self):
71        if self.public_access == 'metadata':
72            return _('Metadata only')
73        elif self.public_access == 'full':
74            return _('Sound and metadata')
75
76        return _('Private data')
77    public_access_label.verbose_name = _('public access')
78
79    def set_revision(self, user):
80        "Save a media object and add a revision"
81        Revision.touch(self, user)
82
83    def get_revision(self):
84        return Revision.objects.filter(element_type=self.element_type, element_id=self.id).order_by('-time')[0]
85
86    class Meta:
87        abstract = True
88
89
90class MediaBaseResource(MediaResource):
91    "Describe a media base resource"
92
93    title                 = CharField(_('title'), required=True)
94    description           = CharField(_('description'))
95    code                  = CharField(_('code'), unique=True, required=True)
96    reference             = CharField(_('reference'), unique=True, null=True)
97    public_access         = CharField(_('public access'), choices=PUBLIC_ACCESS_CHOICES,
98                                      max_length=16, default="metadata")
99
100    def __unicode__(self):
101        return self.code
102
103    @property
104    def public_id(self):
105        return self.code
106
107    def save(self, force_insert=False, force_update=False, user=None, code=None):
108        super(MediaBaseResource, self).save(force_insert, force_update)
109
110    def get_fields(self):
111        return self._meta.fields
112
113    class Meta(MetaCore):
114        abstract = True
115        ordering = ['code']
116
117
118class MediaRelated(MediaResource):
119    "Related media"
120
121    element_type = 'media'
122
123    title           = CharField(_('title'))
124    date            = DateTimeField(_('date'), auto_now=True)
125    description     = TextField(_('description'))
126    mime_type       = CharField(_('mime_type'), null=True)
127    url             = CharField(_('url'), max_length=500)
128    credits         = CharField(_('credits'))
129    file            = FileField(_('file'), upload_to='items/%Y/%m/%d', db_column="filename")
130
131    def is_image(self):
132        is_url_image = False
133        if self.url:
134            url_types = ['.png', '.jpg', '.gif', '.jpeg']
135            for type in url_types:
136                if type in self.url or type.upper() in self.url:
137                    is_url_image = True
138        return 'image' in self.mime_type or is_url_image
139
140    def save(self, force_insert=False, force_update=False):
141        super(MediaRelated, self).save(force_insert, force_update)
142
143    def set_mime_type(self):
144        if self.file:
145            self.mime_type = mimetypes.guess_type(self.file.path)[0]
146
147    def __unicode__(self):
148        if self.title and not re.match('^ *N *$', self.title):
149            title = self.title
150        else:
151            title = unicode(self.item)
152        return title
153
154    class Meta:
155        abstract = True
156
157
158class MediaCollection(MediaResource):
159    "Describe a collection of items"
160
161    element_type = 'collection'
162
163    def is_valid_collection_code(value):
164        "Check if the collection code is well formed"
165        regex = '^' + collection_code_regex + '$'
166        if not re.match(regex, value):
167            raise ValidationError(u'%s is not a valid collection code' % value)
168
169    # General informations
170    reference             = CharField(_('reference'), unique=True, null=True)
171    title                 = CharField(_('title'), required=True)
172    alt_title             = CharField(_('original title / translation'))
173    creator               = CharField(_('depositor / contributor'))
174    recording_context     = WeakForeignKey('RecordingContext', related_name="collections",
175                                           verbose_name=_('recording context'))
176    recorded_from_year    = IntegerField(_('recording year (from)'))
177    recorded_to_year      = IntegerField(_('recording year (until)'))
178    year_published        = IntegerField(_('year published'))
179
180    # Geographic and cultural informations
181    ## See "countries" and "ethnic_groups" methods below
182
183    # Legal notices
184    collector             = CharField(_('recordist'))
185    publisher             = WeakForeignKey('Publisher', related_name="collections",
186                                           verbose_name=_('publisher / status'))
187    publisher_collection  = WeakForeignKey('PublisherCollection', related_name="collections",
188                                            verbose_name=_('publisher collection'))
189    publisher_serial      = CharField(_('publisher serial number'))
190    booklet_author        = CharField(_('author of published notice'))
191    external_references   = TextField(_('bibliographic references'))
192    doctype_code          = IntegerField(_('document type'))
193    public_access         = CharField(_('public access'), choices=PUBLIC_ACCESS_CHOICES,
194                                      max_length=16, default="metadata")
195    legal_rights          = WeakForeignKey('LegalRight', related_name="collections",
196                                           verbose_name=_('legal rights'))
197
198    # Archiving data
199    acquisition_mode      = WeakForeignKey('AcquisitionMode', related_name="collections",
200                                            verbose_name=_('mode of acquisition'))
201    cnrs_contributor      = CharField(_('CNRS depositor'))
202    metadata_author       = WeakForeignKey('MetadataAuthor', related_name="collections",
203                                           verbose_name=_('record author'))
204    booklet_description   = TextField(_('related documentation'))
205    publishing_status     = WeakForeignKey('PublishingStatus', related_name="collections",
206                                           verbose_name=_('secondary edition'))
207    alt_ids               = CharField(_('copies'))
208    comment               = TextField(_('comment'))
209    metadata_writer       = WeakForeignKey('MetadataWriter', related_name="collections",
210                                           verbose_name=_('record writer'))
211    travail               = CharField(_('archiver notes'))
212    items_done            = CharField(_('items finished'))
213    collector_is_creator  = BooleanField(_('recordist identical to depositor'))
214    is_published          = BooleanField(_('published'))
215    conservation_site     = CharField(_('conservation site'))
216
217    # Technical data
218    code                  = CharField(_('code'), unique=True, required=True, validators=[is_valid_collection_code])
219    old_code              = CharField(_('old code'), unique=False, null=True, blank=True)
220    approx_duration       = DurationField(_('approximative duration'))
221    physical_items_num    = IntegerField(_('number of components (medium / piece)'))
222    physical_format       = WeakForeignKey('PhysicalFormat', related_name="collections",
223                                           verbose_name=_('archive format'))
224    ad_conversion         = WeakForeignKey('AdConversion', related_name='collections',
225                                           verbose_name=_('digitization'))
226    state                 = TextField(_('status'))
227    a_informer_07_03      = CharField(_('a_informer_07_03'))
228
229    # All
230    objects               = MediaCollectionManager()
231
232    def __unicode__(self):
233        return self.code
234
235    @property
236    def public_id(self):
237        return self.code
238
239    @property
240    def has_mediafile(self):
241        "Tell wether this collection has any media files attached to its items"
242        items = self.items.all()
243        for item in items:
244            if item.file:
245                return True
246        return False
247
248    def __name_cmp(self, obj1, obj2):
249        return unaccent_icmp(obj1.name, obj2.name)
250
251    def countries(self):
252        "Return the countries of the items"
253        countries = []
254        for item in self.items.filter(location__isnull=False):
255            for country in item.location.countries():
256                if not country in countries:
257                    countries.append(country)
258
259        countries.sort(self.__name_cmp)
260
261        return countries
262    countries.verbose_name = _("states / nations")
263
264    def ethnic_groups(self):
265        "Return the ethnic groups of the items"
266        groups = []
267        items = self.items.all()
268        for item in items:
269            if item.ethnic_group and not item.ethnic_group in groups:
270                groups.append(item.ethnic_group)
271
272        cmp = lambda a, b: unaccent_icmp(a.value, b.value)
273        groups.sort(cmp)
274
275        return groups
276    ethnic_groups.verbose_name = _('populations / social groups')
277
278    def computed_duration(self):
279        duration = Duration()
280        for item in self.items.all():
281            duration += item.computed_duration()
282        return duration
283
284    computed_duration.verbose_name = _('computed duration')
285
286    def save(self, force_insert=False, force_update=False, user=None, code=None):
287        super(MediaCollection, self).save(force_insert, force_update)
288
289    class Meta(MetaCore):
290        db_table = 'media_collections'
291        ordering = ['code']
292        verbose_name = _('collection')
293
294
295class MediaCollectionRelated(MediaRelated):
296    "Collection related media"
297
298    collection      = ForeignKey('MediaCollection', related_name="related", verbose_name=_('collection'))
299
300    class Meta(MetaCore):
301        db_table = 'media_collection_related'
302        verbose_name = _('collection related media')
303        verbose_name_plural = _('collection related media')
304
305
306class MediaItem(MediaResource):
307    "Describe an item"
308
309    element_type = 'item'
310
311    # Main Informations
312    title                 = CharField(_('title'))
313    alt_title             = CharField(_('original title / translation'))
314    collection            = ForeignKey('MediaCollection', related_name="items",
315                                       verbose_name=_('collection'))
316    recorded_from_date    = DateField(_('recording date (from)'))
317    recorded_to_date      = DateField(_('recording date (until)'))
318
319    scientist             = CharField(_('scientist'))
320    domain                = WeakForeignKey('Domain', verbose_name=_('domain'))
321    summary               = TextField(_('summary'))
322    comment               = TextField(_('remarks'))
323
324    # Geographic and cultural informations
325    location              = WeakForeignKey('Location', verbose_name=_('location'))
326    location_comment      = CharField(_('location details'))
327    cultural_area         = CharField(_('cultural area'))
328    ethnic_group          = WeakForeignKey('EthnicGroup', related_name="items",
329                                           verbose_name=_('population / social group'))
330    language              = CharField(_('language'))
331    language_iso          = ForeignKey('Language', related_name="items",
332                                       verbose_name=_('ISO language'), blank=True,
333                                        null=True, on_delete=models.SET_NULL)
334    context_comment       = TextField(_('comments / ethnographic context'))
335    moda_execut           = CharField(_('moda_execut'))
336
337    # Musical informations
338    vernacular_style      = WeakForeignKey('VernacularStyle', related_name="items",
339                                           verbose_name=_('vernacular style'))
340    generic_style         = WeakForeignKey('GenericStyle', related_name="items",
341                                           verbose_name=_('generic style'))
342    author                = CharField(_('author / compositor'))
343    contributor           = CharField(_('contributor'))
344
345    # Legal mentions
346    organization          = WeakForeignKey('Organization', verbose_name=_('organization'))
347    public_access         = CharField(_('public access'), choices=PUBLIC_ACCESS_CHOICES,
348                                      max_length=16, default="metadata")
349    depositor             = CharField(_('depositor'))
350    rights                = WeakForeignKey('Rights', verbose_name=_('rights'))
351
352    # Archiving data
353    code                  = CharField(_('code'), unique=True, blank=True)
354    old_code              = CharField(_('original code'), unique=False, blank=True)
355    track                 = CharField(_('item number'))
356    recordist             = CharField(_('recordist'))
357    digitalist            = CharField(_('digitalist'))
358    collector             = CharField(_('collector'))
359    collector_selection   = CharField(_('collector selection'))
360    collector_from_collection = BooleanField(_('collector as in collection'))
361    digitization_date        = DateField(_('digitization date'))
362    publishing_date       = DateField(_('publishing date'))
363    creator_reference     = CharField(_('creator reference'))
364    external_references   = TextField(_('published references'))
365    copied_from_item      = WeakForeignKey('self', related_name="copies", verbose_name=_('copy of'))
366
367    # Media
368    file                  = FileField(_('file'), upload_to='items/%Y/%m/%d', db_column="filename")
369
370    # Technical data
371    approx_duration       = DurationField(_('approximative duration'))
372
373    # Manager
374    objects               = MediaItemManager()
375
376    def keywords(self):
377        return ContextKeyword.objects.filter(item_relations__item = self)
378    keywords.verbose_name = _('keywords')
379
380    @property
381    def public_id(self):
382        if self.code:
383            return self.code
384        return self.id
385
386    class Meta(MetaCore):
387        db_table = 'media_items'
388        permissions = (("can_play_all_items", "Can play all media items"),
389                       ("can_download_all_items", "Can download all media items"), )
390        verbose_name = _('item')
391
392    def is_valid_code(self, code):
393        "Check if the item code is well formed"
394        if not re.match('^' + self.collection.code, self.code):
395            return False
396        if self.collection.is_published:
397            regex = '^' + item_published_code_regex + '$'
398        else:
399            regex = '^' + item_unpublished_code_regex + '$'
400        if re.match(regex, code):
401            return True
402        return False
403
404    #def clean(self):
405        #if self.code and not self.is_valid_code(self.code):
406            #raise ValidationError("%s is not a valid item code for collection %s"
407                                        #% (self.code, self.collection.code))
408
409    def save(self, force_insert=False, force_update=False):
410        super(MediaItem, self).save(force_insert, force_update)
411
412    def computed_duration(self):
413        "Tell the length in seconds of this item media data"
414        return self.approx_duration
415
416    computed_duration.verbose_name = _('computed duration')
417
418    def __unicode__(self):
419        if self.title and not re.match('^ *N *$', self.title):
420            title = self.title
421        else:
422            title = unicode(self.collection)
423        if self.track:
424            title += ' ' + self.track
425        return title
426
427    @property
428    def instruments(self):
429        "Return the instruments of the item"
430        instruments = []
431        performances = MediaItemPerformance.objects.filter(media_item=self)
432        for performance in performances:
433            instrument = performance.instrument
434            alias = performance.alias
435            if not instrument in instruments:
436                instruments.append(instrument)
437            if not alias in instruments:
438                instruments.append(alias)
439
440        instruments.sort(self.__name_cmp)
441        return instruments
442
443        instruments.verbose_name = _("instruments")
444
445
446class MediaItemRelated(MediaRelated):
447    "Item related media"
448
449    item            = ForeignKey('MediaItem', related_name="related", verbose_name=_('item'))
450
451    class Meta(MetaCore):
452        db_table = 'media_item_related'
453        verbose_name = _('item related media')
454        verbose_name_plural = _('item related media')
455
456
457class MediaItemKeyword(ModelCore):
458    "Item keyword"
459    item    = ForeignKey('MediaItem', verbose_name=_('item'), related_name="keyword_relations")
460    keyword = ForeignKey('ContextKeyword', verbose_name=_('keyword'), related_name="item_relations")
461
462    class Meta(MetaCore):
463        db_table = 'media_item_keywords'
464        unique_together = (('item', 'keyword'),)
465
466
467class MediaItemPerformance(ModelCore):
468    "Item performance"
469    media_item      = ForeignKey('MediaItem', related_name="performances",
470                                 verbose_name=_('item'))
471    instrument      = WeakForeignKey('Instrument', related_name="performances",
472                                     verbose_name=_('composition'))
473    alias           = WeakForeignKey('InstrumentAlias', related_name="performances",
474                                     verbose_name=_('vernacular name'))
475    instruments_num = CharField(_('number'))
476    musicians       = CharField(_('interprets'))
477
478    class Meta(MetaCore):
479        db_table = 'media_item_performances'
480
481
482class MediaItemAnalysis(ModelCore):
483    "Item analysis result computed by TimeSide"
484
485    element_type = 'analysis'
486    item  = ForeignKey('MediaItem', related_name="analysis", verbose_name=_('item'))
487    analyzer_id = CharField(_('id'), required=True)
488    name = CharField(_('name'))
489    value = CharField(_('value'))
490    unit = CharField(_('unit'))
491
492    class Meta(MetaCore):
493        db_table = 'media_analysis'
494        ordering = ['name']
495
496    def to_dict(self):
497        if self.analyzer_id == 'duration':
498            if '.' in self.value:
499                value = self.value.split('.')
500                self.value = '.'.join([value[0], value[1][:2]])
501        return {'id': self.analyzer_id, 'name': self.name, 'value': self.value, 'unit': self.unit}
502
503
504class MediaPart(MediaResource):
505    "Describe an item part"
506    element_type = 'part'
507    item  = ForeignKey('MediaItem', related_name="parts", verbose_name=_('item'))
508    title = CharField(_('title'), required=True)
509    start = FloatField(_('start'), required=True)
510    end   = FloatField(_('end'), required=True)
511
512    class Meta(MetaCore):
513        db_table = 'media_parts'
514        verbose_name = _('item part')
515
516    def __unicode__(self):
517        return self.title
518
519class Playlist(ModelCore):
520    "Item, collection or marker playlist"
521    element_type = 'playlist'
522    public_id      = CharField(_('public_id'), required=True)
523    author         = ForeignKey(User, related_name="playlists", db_column="author")
524    title          = CharField(_('title'), required=True)
525    description    = TextField(_('description'))
526
527    class Meta(MetaCore):
528        db_table = 'playlists'
529
530    def __unicode__(self):
531        return self.title
532
533
534class PlaylistResource(ModelCore):
535    "Playlist components"
536    RESOURCE_TYPE_CHOICES = (('item', 'item'), ('collection', 'collection'),
537                             ('marker', 'marker'), ('fonds', 'fonds'), ('corpus', 'corpus'))
538    element_type = 'playlist_resource'
539    public_id          = CharField(_('public_id'), required=True)
540    playlist           = ForeignKey('Playlist', related_name="resources", verbose_name=_('playlist'))
541    resource_type      = CharField(_('resource_type'), choices=RESOURCE_TYPE_CHOICES, required=True)
542    resource_id        = CharField(_('resource_id'), required=True)
543
544    class Meta(MetaCore):
545        db_table = 'playlist_resources'
546
547
548class MediaItemMarker(MediaResource):
549    "2D marker object : text value vs. time"
550
551    element_type = 'marker'
552
553    item            = ForeignKey('MediaItem', related_name="markers", verbose_name=_('item'))
554    public_id       = CharField(_('public_id'), required=True)
555    time            = FloatField(_('time'))
556    title           = CharField(_('title'))
557    date            = DateTimeField(_('date'), auto_now=True)
558    description     = TextField(_('description'))
559    author          = ForeignKey(User, related_name="markers", verbose_name=_('author'))
560
561    class Meta(MetaCore):
562        db_table = 'media_markers'
563
564    def __unicode__(self):
565        if self.title:
566            return self.title
567        else:
568            return self.public_id
569
570
571class MediaItemTranscodingFlag(ModelCore):
572    "Item flag to know if the MediaItem has been transcoded to a given format"
573
574    item            = ForeignKey('MediaItem', related_name="transcoding", verbose_name=_('item'))
575    mime_type       = CharField(_('mime_type'), required=True)
576    date            = DateTimeField(_('date'), auto_now=True)
577    value           = BooleanField(_('transcoded'))
578
579    class Meta(MetaCore):
580        db_table = 'media_transcoding'
581
582
583class DublinCoreToFormatMetadata(object):
584    """ a mapping class to get item DublinCore metadata dictionaries
585    in various audio metadata format (MP3, OGG, etc...)"""
586
587    #FIXME: should be given by timeside
588    unavailable_extensions = ['wav', 'aiff', 'aif', 'flac', 'webm']
589
590    metadata_mapping = {
591                    'mp3' : {
592                         'title': 'TIT2', #title2
593                         'creator': 'TCOM', #composer
594                         'creator': 'TPE1', #lead
595                         'identifier': 'UFID', #unique ID
596                         'relation': 'TALB', #album
597                         'type': 'TCON', #genre
598                         'publisher': 'TPUB', #publisher
599                         'date': 'TDRC', #year
600#                         'coverage': 'COMM',  #comment
601                         },
602                    'ogg': {
603                        'creator': 'artist',
604                        'relation': 'album',
605                        'all': 'all',
606                       },
607                    'flac': {
608                        'creator': 'artist',
609                        'relation': 'album',
610                        'all': 'all',
611                       },
612                    'wav': {
613                        'creator': 'artist',
614                        'relation': 'album',
615                        'all': 'all',
616                       },
617                    'webm': {
618                        'creator': 'artist',
619                        'relation': 'album',
620                        'all': 'all',
621                       },
622                    }
623
624    def __init__(self, format):
625        self.format = format
626
627    def get_metadata(self, dc_metadata):
628        mapp = self.metadata_mapping[self.format]
629        metadata = {}
630        keys_done = []
631        for data in dc_metadata:
632            key = data[0]
633            value = data[1].encode('utf-8')
634            if value:
635                if key == 'date':
636                    value = value.split(';')[0].split('=')
637                    if len(value) > 1:
638                        value  = value[1]
639                        value = value.split('-')[0]
640                    else:
641                        value = value[0].split('-')[0]
642                if key in mapp:
643                    metadata[mapp[key]] = value.decode('utf-8')
644                elif 'all' in mapp.keys():
645                    metadata[key] = value.decode('utf-8')
646                keys_done.append(key)
647        return metadata
648
649
650class MediaCorpus(MediaBaseResource):
651    "Describe a corpus"
652
653    element_type = 'corpus'
654    children_type = 'collections'
655
656    children = models.ManyToManyField(MediaCollection, related_name="corpus",
657                                      verbose_name=_('collections'),  blank=True, null=True)
658    recorded_from_year    = IntegerField(_('recording year (from)'))
659    recorded_to_year      = IntegerField(_('recording year (until)'))
660
661    objects = MediaCorpusManager()
662
663    @property
664    def public_id(self):
665        return self.code
666
667    class Meta(MetaCore):
668        db_table = 'media_corpus'
669        verbose_name = _('corpus')
670        verbose_name_plural = _('corpus')
671
672
673class MediaFonds(MediaBaseResource):
674    "Describe fonds"
675
676    element_type = 'fonds'
677    children_type = 'corpus'
678
679    children = models.ManyToManyField(MediaCorpus, related_name="fonds",
680                                      verbose_name=_('corpus'), blank=True, null=True)
681
682    objects = MediaFondsManager()
683
684    @property
685    def public_id(self):
686        return self.code
687
688    class Meta(MetaCore):
689        db_table = 'media_fonds'
690        verbose_name = _('fonds')
691        verbose_name_plural = _('fonds')
692
693
694class MediaCorpusRelated(MediaRelated):
695    "Corpus related media"
696
697    resource = ForeignKey(MediaCorpus, related_name="related", verbose_name=_('corpus'))
698
699    class Meta(MetaCore):
700        db_table = 'media_corpus_related'
701        verbose_name = _('corpus related media')
702        verbose_name_plural = _('corpus related media')
703
704
705class MediaFondsRelated(MediaRelated):
706    "Fonds related media"
707
708    resource = ForeignKey(MediaFonds, related_name="related", verbose_name=_('fonds'))
709
710    class Meta(MetaCore):
711        db_table = 'media_fonds_related'
712        verbose_name = _('fonds related media')
713        verbose_name_plural = _('fonds related media')
714
Note: See TracBrowser for help on using the repository browser.