source: telemeta/models/media.py @ 03ca023

cremcrem2devdev2diademsfeature/breadcrumbsfeature/ts-0.5feature/ts-0.5.4feature/writecachegenericinstru_searchlamlam2mapsv3mergenlivemultirelease/1.4.4sabiodsecurityserversocialstoragetelecastertest
Last change on this file since 03ca023 was 03ca023, checked in by yomguy <yomguy@…>, 2 years ago

fix format

  • Property mode set to 100644
File size: 26.5 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    collector             = CharField(_('recordist'))
315    collection            = ForeignKey('MediaCollection', related_name="items",
316                                       verbose_name=_('collection'))
317    recorded_from_date    = DateField(_('recording date (from)'))
318    recorded_to_date      = DateField(_('recording date (until)'))
319
320    # Geographic and cultural informations
321    location              = WeakForeignKey('Location', verbose_name=_('location'))
322    location_comment      = CharField(_('location details'))
323    cultural_area         = CharField(_('cultural area'))
324    ethnic_group          = WeakForeignKey('EthnicGroup', related_name="items",
325                                           verbose_name=_('population / social group'))
326    language              = CharField(_('language'))
327    language_iso          = ForeignKey('Language', related_name="items",
328                                       verbose_name=_('ISO language'), blank=True,
329                                        null=True, on_delete=models.SET_NULL)
330    context_comment       = TextField(_('comments / ethnographic context'))
331    moda_execut           = CharField(_('moda_execut'))
332
333    # Musical informations
334    vernacular_style      = WeakForeignKey('VernacularStyle', related_name="items",
335                                           verbose_name=_('vernacular style'))
336    generic_style         = WeakForeignKey('GenericStyle', related_name="items",
337                                           verbose_name=_('generic style'))
338    author                = CharField(_('author / compositor'))
339
340    # General informations
341    comment               = TextField(_('remarks'))
342    collector_selection   = CharField(_('recordist selection'))
343    collector_from_collection = BooleanField(_('recordist as in collection'))
344
345    # Archiving data
346    code                  = CharField(_('code'), unique=True, blank=True)
347    old_code              = CharField(_('old code'), unique=False, blank=True)
348    track                 = CharField(_('item number'))
349    creator_reference     = CharField(_('reference'))
350    original_format       = ForeignKey(Format, related_name="item",
351                                       verbose_name=_('original format'), blank=True,
352                                        null=True, on_delete=models.SET_NULL)
353    external_references   = TextField(_('published references'))
354    copied_from_item      = WeakForeignKey('self', related_name="copies", verbose_name=_('copy of'))
355    public_access         = CharField(_('public access'), choices=PUBLIC_ACCESS_CHOICES, max_length=16, default="metadata")
356    file                  = FileField(_('file'), upload_to='items/%Y/%m/%d', db_column="filename")
357
358    # Technical data
359    approx_duration       = DurationField(_('approximative duration'))
360
361    # All
362    objects               = MediaItemManager()
363
364    def keywords(self):
365        return ContextKeyword.objects.filter(item_relations__item = self)
366    keywords.verbose_name = _('keywords')
367
368    @property
369    def public_id(self):
370        if self.code:
371            return self.code
372        return self.id
373
374    class Meta(MetaCore):
375        db_table = 'media_items'
376        permissions = (("can_play_all_items", "Can play all media items"),
377                       ("can_download_all_items", "Can download all media items"), )
378        verbose_name = _('item')
379
380    def is_valid_code(self, code):
381        "Check if the item code is well formed"
382        if not re.match('^' + self.collection.code, self.code):
383            return False
384        if self.collection.is_published:
385            regex = '^' + item_published_code_regex + '$'
386        else:
387            regex = '^' + item_unpublished_code_regex + '$'
388        if re.match(regex, code):
389            return True
390        return False
391
392    #def clean(self):
393        #if self.code and not self.is_valid_code(self.code):
394            #raise ValidationError("%s is not a valid item code for collection %s"
395                                        #% (self.code, self.collection.code))
396
397    def save(self, force_insert=False, force_update=False):
398        super(MediaItem, self).save(force_insert, force_update)
399
400    def computed_duration(self):
401        "Tell the length in seconds of this item media data"
402        return self.approx_duration
403
404    computed_duration.verbose_name = _('computed duration')
405
406    def __unicode__(self):
407        if self.title and not re.match('^ *N *$', self.title):
408            title = self.title
409        else:
410            title = unicode(self.collection)
411        if self.track:
412            title += ' ' + self.track
413        return title
414
415    @property
416    def instruments(self):
417        "Return the instruments of the item"
418        instruments = []
419        performances = MediaItemPerformance.objects.filter(media_item=self)
420        for performance in performances:
421            instrument = performance.instrument
422            alias = performance.alias
423            if not instrument in instruments:
424                instruments.append(instrument)
425            if not alias in instruments:
426                instruments.append(alias)
427
428        instruments.sort(self.__name_cmp)
429        return instruments
430
431        instruments.verbose_name = _("instruments")
432
433
434class MediaItemRelated(MediaRelated):
435    "Item related media"
436
437    item            = ForeignKey('MediaItem', related_name="related", verbose_name=_('item'))
438
439    class Meta(MetaCore):
440        db_table = 'media_item_related'
441        verbose_name = _('item related media')
442        verbose_name_plural = _('item related media')
443
444
445class MediaItemKeyword(ModelCore):
446    "Item keyword"
447    item    = ForeignKey('MediaItem', verbose_name=_('item'), related_name="keyword_relations")
448    keyword = ForeignKey('ContextKeyword', verbose_name=_('keyword'), related_name="item_relations")
449
450    class Meta(MetaCore):
451        db_table = 'media_item_keywords'
452        unique_together = (('item', 'keyword'),)
453
454
455class MediaItemPerformance(ModelCore):
456    "Item performance"
457    media_item      = ForeignKey('MediaItem', related_name="performances",
458                                 verbose_name=_('item'))
459    instrument      = WeakForeignKey('Instrument', related_name="performances",
460                                     verbose_name=_('composition'))
461    alias           = WeakForeignKey('InstrumentAlias', related_name="performances",
462                                     verbose_name=_('vernacular name'))
463    instruments_num = CharField(_('number'))
464    musicians       = CharField(_('interprets'))
465
466    class Meta(MetaCore):
467        db_table = 'media_item_performances'
468
469
470class MediaItemAnalysis(ModelCore):
471    "Item analysis result computed by TimeSide"
472
473    element_type = 'analysis'
474    item  = ForeignKey('MediaItem', related_name="analysis", verbose_name=_('item'))
475    analyzer_id = CharField(_('id'), required=True)
476    name = CharField(_('name'))
477    value = CharField(_('value'))
478    unit = CharField(_('unit'))
479
480    class Meta(MetaCore):
481        db_table = 'media_analysis'
482        ordering = ['name']
483
484    def to_dict(self):
485        if self.analyzer_id == 'duration':
486            if '.' in self.value:
487                value = self.value.split('.')
488                self.value = '.'.join([value[0], value[1][:2]])
489        return {'id': self.analyzer_id, 'name': self.name, 'value': self.value, 'unit': self.unit}
490
491
492class MediaPart(MediaResource):
493    "Describe an item part"
494    element_type = 'part'
495    item  = ForeignKey('MediaItem', related_name="parts", verbose_name=_('item'))
496    title = CharField(_('title'), required=True)
497    start = FloatField(_('start'), required=True)
498    end   = FloatField(_('end'), required=True)
499
500    class Meta(MetaCore):
501        db_table = 'media_parts'
502        verbose_name = _('item part')
503
504    def __unicode__(self):
505        return self.title
506
507class Playlist(ModelCore):
508    "Item, collection or marker playlist"
509    element_type = 'playlist'
510    public_id      = CharField(_('public_id'), required=True)
511    author         = ForeignKey(User, related_name="playlists", db_column="author")
512    title          = CharField(_('title'), required=True)
513    description    = TextField(_('description'))
514
515    class Meta(MetaCore):
516        db_table = 'playlists'
517
518    def __unicode__(self):
519        return self.title
520
521
522class PlaylistResource(ModelCore):
523    "Playlist components"
524    RESOURCE_TYPE_CHOICES = (('item', 'item'), ('collection', 'collection'), ('marker', 'marker'), ('fonds', 'fonds'), ('corpus', 'corpus'))
525    element_type = 'playlist_resource'
526    public_id          = CharField(_('public_id'), required=True)
527    playlist           = ForeignKey('Playlist', related_name="resources", verbose_name=_('playlist'))
528    resource_type      = CharField(_('resource_type'), choices=RESOURCE_TYPE_CHOICES, required=True)
529    resource_id        = CharField(_('resource_id'), required=True)
530
531    class Meta(MetaCore):
532        db_table = 'playlist_resources'
533
534
535class MediaItemMarker(MediaResource):
536    "2D marker object : text value vs. time"
537
538    element_type = 'marker'
539
540    item            = ForeignKey('MediaItem', related_name="markers", verbose_name=_('item'))
541    public_id       = CharField(_('public_id'), required=True)
542    time            = FloatField(_('time'))
543    title           = CharField(_('title'))
544    date            = DateTimeField(_('date'), auto_now=True)
545    description     = TextField(_('description'))
546    author          = ForeignKey(User, related_name="markers", verbose_name=_('author'))
547
548    class Meta(MetaCore):
549        db_table = 'media_markers'
550
551    def __unicode__(self):
552        if self.title:
553            return self.title
554        else:
555            return self.public_id
556
557
558class MediaItemTranscodingFlag(ModelCore):
559    "Item flag to know if the MediaItem has been transcoded to a given format"
560
561    item            = ForeignKey('MediaItem', related_name="transcoding", verbose_name=_('item'))
562    mime_type       = CharField(_('mime_type'), required=True)
563    date            = DateTimeField(_('date'), auto_now=True)
564    value           = BooleanField(_('transcoded'))
565
566    class Meta(MetaCore):
567        db_table = 'media_transcoding'
568
569
570class DublinCoreToFormatMetadata(object):
571    """ a mapping class to get item DublinCore metadata dictionaries
572    in various audio metadata format (MP3, OGG, etc...)"""
573
574    #FIXME: should be given by timeside
575    unavailable_extensions = ['wav', 'aiff', 'aif', 'flac', 'webm']
576
577    metadata_mapping = {
578                    'mp3' : {
579                         'title': 'TIT2', #title2
580                         'creator': 'TCOM', #composer
581                         'creator': 'TPE1', #lead
582                         'identifier': 'UFID', #unique ID
583                         'relation': 'TALB', #album
584                         'type': 'TCON', #genre
585                         'publisher': 'TPUB', #publisher
586                         'date': 'TDRC', #year
587#                         'coverage': 'COMM',  #comment
588                         },
589                    'ogg': {
590                        'creator': 'artist',
591                        'relation': 'album',
592                        'all': 'all',
593                       },
594                    'flac': {
595                        'creator': 'artist',
596                        'relation': 'album',
597                        'all': 'all',
598                       },
599                    'wav': {
600                        'creator': 'artist',
601                        'relation': 'album',
602                        'all': 'all',
603                       },
604                    'webm': {
605                        'creator': 'artist',
606                        'relation': 'album',
607                        'all': 'all',
608                       },
609                    }
610
611    def __init__(self, format):
612        self.format = format
613
614    def get_metadata(self, dc_metadata):
615        mapp = self.metadata_mapping[self.format]
616        metadata = {}
617        keys_done = []
618        for data in dc_metadata:
619            key = data[0]
620            value = data[1].encode('utf-8')
621            if value:
622                if key == 'date':
623                    value = value.split(';')[0].split('=')
624                    if len(value) > 1:
625                        value  = value[1]
626                        value = value.split('-')[0]
627                    else:
628                        value = value[0].split('-')[0]
629                if key in mapp:
630                    metadata[mapp[key]] = value.decode('utf-8')
631                elif 'all' in mapp.keys():
632                    metadata[key] = value.decode('utf-8')
633                keys_done.append(key)
634        return metadata
635
636
637class MediaCorpus(MediaBaseResource):
638    "Describe a corpus"
639
640    element_type = 'corpus'
641    children_type = 'collections'
642
643    children = models.ManyToManyField(MediaCollection, related_name="corpus", verbose_name=_('collections'),  blank=True, null=True)
644    recorded_from_year    = IntegerField(_('recording year (from)'))
645    recorded_to_year      = IntegerField(_('recording year (until)'))
646
647    objects = MediaCorpusManager()
648
649    @property
650    def public_id(self):
651        return self.code
652
653    class Meta(MetaCore):
654        db_table = 'media_corpus'
655        verbose_name = _('corpus')
656        verbose_name_plural = _('corpus')
657
658
659class MediaFonds(MediaBaseResource):
660    "Describe fonds"
661
662    element_type = 'fonds'
663    children_type = 'corpus'
664
665    children = models.ManyToManyField(MediaCorpus, related_name="fonds", verbose_name=_('corpus'), blank=True, null=True)
666
667    objects = MediaFondsManager()
668
669    @property
670    def public_id(self):
671        return self.code
672
673    class Meta(MetaCore):
674        db_table = 'media_fonds'
675        verbose_name = _('fonds')
676        verbose_name_plural = _('fonds')
677
678
679class MediaCorpusRelated(MediaRelated):
680    "Corpus related media"
681
682    resource = ForeignKey(MediaCorpus, related_name="related", verbose_name=_('corpus'))
683
684    class Meta(MetaCore):
685        db_table = 'media_corpus_related'
686        verbose_name = _('corpus related media')
687        verbose_name_plural = _('corpus related media')
688
689
690class MediaFondsRelated(MediaRelated):
691    "Fonds related media"
692
693    resource = ForeignKey(MediaFonds, related_name="related", verbose_name=_('fonds'))
694
695    class Meta(MetaCore):
696        db_table = 'media_fonds_related'
697        verbose_name = _('fonds related media')
698        verbose_name_plural = _('fonds related media')
699
Note: See TracBrowser for help on using the repository browser.