class TypeDeCaracteristiqueDeProgramme(CommonModel): nom = CharField(_('nom'), max_length=200, help_text=ex(_('tonalité')), unique=True, db_index=True) nom_pluriel = CharField(_('nom (au pluriel)'), max_length=230, blank=True, help_text=PLURAL_MSG) classement = SmallIntegerField(_('classement'), default=1) class Meta(object): verbose_name = _('type de caractéristique de programme') verbose_name_plural = _('types de caractéristique de programme') ordering = ('classement', ) @staticmethod def invalidated_relations_when_saved(all_relations=False): return ('caracteristiques', ) def pluriel(self): return calc_pluriel(self) def __str__(self): return self.nom @staticmethod def autocomplete_search_fields(): return 'nom__unaccent__icontains', 'nom_pluriel__unaccent__icontains',
class CaracteristiqueDeProgramme(CommonModel): type = ForeignKey('TypeDeCaracteristiqueDeProgramme', null=True, blank=True, on_delete=PROTECT, related_name='caracteristiques', verbose_name=_('type')) valeur = CharField(_('valeur'), max_length=400, help_text=ex(_('en trois actes'))) classement = SmallIntegerField( _('classement'), default=1, db_index=True, help_text=_('Par exemple, on peut choisir de classer ' 'les découpages par nombre d’actes.')) objects = CaracteristiqueManager() class Meta(object): unique_together = ('type', 'valeur') verbose_name = _('caractéristique de programme') verbose_name_plural = _('caractéristiques de programme') ordering = ('type', 'classement', 'valeur') @staticmethod def invalidated_relations_when_saved(all_relations=False): return ('elements_de_programme', ) def html(self, tags=True, caps=False): value = self.valeur if caps: value = capfirst(self.valeur) value = mark_safe(value) if self.type: return hlp(value, self.type, tags=tags) return value html.allow_tags = True def __str__(self): valeur = strip_tags(self.valeur) if self.type: return f'{self.type} : {valeur}' return valeur @staticmethod def autocomplete_search_fields(): return 'type__nom__unaccent__icontains', 'valeur__unaccent__icontains',
class Source(AutoriteModel): parent = ForeignKey( 'self', related_name='children', verbose_name=_('parent'), null=True, blank=True, on_delete=CASCADE, help_text=_( 'À remplir par exemple si la source est une page d’un recueil ' 'déjà existant ou un tome d’une série.'), ) position = PositiveIntegerField( _('position'), null=True, blank=True, help_text=_('Position au sein de son parent.')) est_promu = BooleanField(_('est dans la bibliothèque'), default=False) type = ForeignKey('TypeDeSource', related_name='sources', help_text=ex(_('compte rendu')), verbose_name=_('type'), on_delete=PROTECT) titre = CharField(_('titre'), max_length=200, blank=True, db_index=True, help_text=ex(_('Journal de Rouen'))) legende = CharField(_('légende'), max_length=600, blank=True, help_text=_('Recommandée pour les images.')) ancrage = AncrageSpatioTemporel(has_heure=False, has_lieu=False) numero = NumberCharField( _('numéro'), max_length=50, blank=True, db_index=True, help_text=_('Sans « № ». Exemple : « 52 »'), ) folio = CharField(_('folio'), max_length=15, blank=True, help_text=_('Sans « f. ». Exemple : « 3 ».')) page = CharField(_('page'), max_length=15, blank=True, db_index=True, help_text=_('Sans « p. ». Exemple : « 3 »')) lieu_conservation = CharField(_('lieu de conservation'), max_length=75, blank=True, db_index=True) cote = CharField(_('cote'), max_length=60, blank=True, db_index=True) url = URLField(_('URL'), blank=True, help_text=_('Uniquement un permalien extérieur à Dezède.')) transcription = HTMLField( _('transcription'), blank=True, help_text=_('Recopier la source ou un extrait en suivant les règles ' 'définies dans ' # FIXME: Don’t hardcode the URL. '<a href="/examens/source">le didacticiel.</a>'), ) fichier = FileField(_('fichier'), upload_to='files/', blank=True) TYPES = ( (FileAnalyzer.OTHER, _('autre')), (FileAnalyzer.IMAGE, _('image')), (FileAnalyzer.AUDIO, _('audio')), (FileAnalyzer.VIDEO, _('vidéo')), ) type_fichier = PositiveSmallIntegerField( choices=TYPES, null=True, blank=True, editable=False, db_index=True, ) telechargement_autorise = BooleanField( _('téléchargement autorisé'), default=True, ) evenements = ManyToManyField('Evenement', through='SourceEvenement', related_name='sources', verbose_name=_('événements')) oeuvres = ManyToManyField('Oeuvre', through='SourceOeuvre', related_name='sources', verbose_name=_('œuvres')) individus = ManyToManyField('Individu', through='SourceIndividu', related_name='sources', verbose_name=_('individus')) ensembles = ManyToManyField('Ensemble', through='SourceEnsemble', related_name='sources', verbose_name=_('ensembles')) lieux = ManyToManyField('Lieu', through='SourceLieu', related_name='sources', verbose_name=_('lieux')) parties = ManyToManyField('Partie', through='SourcePartie', related_name='sources', verbose_name=_('sources')) # # Dossier # # Métadonnées editeurs_scientifiques = ManyToManyField( 'accounts.HierarchicUser', related_name='sources_editees', verbose_name=_('éditeurs scientifiques'), blank=True, ) date_publication = DateField(_('date de publication'), default=datetime.datetime.now) publications = TextField(_('publication(s) associée(s)'), blank=True) developpements = TextField(_('développements envisagés'), blank=True) # Article presentation = TextField(_('présentation'), blank=True) contexte = TextField(_('contexte historique'), blank=True) sources_et_protocole = TextField(_('sources et protocole'), blank=True) bibliographie = TextField(_('bibliographie indicative'), blank=True) objects = SourceManager() class Meta: verbose_name = _('source') verbose_name_plural = _('sources') ordering = ( 'date', 'titre', 'numero', 'parent__date', 'parent__titre', 'parent__numero', 'position', 'page', 'lieu_conservation', 'cote', ) permissions = (('can_change_status', _('Peut changer l’état')), ) def __str__(self): return strip_tags(self.html(False)) def has_presentation_tab(self): def iterator(): yield self.editeurs_scientifiques.exists() yield self.publications yield self.developpements yield self.presentation yield self.contexte yield self.sources_et_protocole yield self.bibliographie return any(iterator()) def has_index_tab(self): def iterator(): yield self.parent yield self.auteurs_html() yield self.nested_individus() yield self.nested_oeuvres() yield self.nested_parties() yield self.nested_evenements() yield self.nested_ensembles() yield self.notes_publiques return any(iterator()) @cached_property def specific(self): if self.type_fichier == FileAnalyzer.AUDIO: return Audio.objects.get(pk=self.pk) if self.type_fichier == FileAnalyzer.VIDEO: return Video.objects.get(pk=self.pk) return self @permalink def get_absolute_url(self): return 'source_permanent_detail', (self.pk, ) @permalink def get_change_url(self): meta = self.specific._meta return f'admin:{meta.app_label}_{meta.model_name}_change', (self.pk, ) def permalien(self): return self.get_absolute_url() def link(self): return self.html() link.short_description = _('Lien') link.allow_tags = True def auteurs_html(self, tags=True): return self.auteurs.html(tags) def no(self): return ugettext('n° %s') % self.numero def f(self): return ugettext('f. %s') % self.folio def p(self): return ugettext('p. %s') % self.page def html(self, tags=True, pretty_title=False, link=True): url = None if not tags else self.get_absolute_url() conservation = hlp(self.lieu_conservation, ugettext('Lieu de conservation'), tags) if self.ancrage.date or self.ancrage.date_approx: ancrage = hlp(self.ancrage.html(tags, caps=False), ugettext('date')) else: ancrage = None if self.cote: conservation += f", {hlp(self.cote, 'cote', tags)}" if self.titre: l = [cite(self.titre, tags)] if self.numero: l.append(self.no()) if ancrage is not None: l.append(ancrage) if self.lieu_conservation: l[-1] += f' ({conservation})' else: l = [conservation] if ancrage is not None: l.append(ancrage) if self.folio: l.append(hlp(self.f(), ugettext('folio'), tags)) if self.page: l.append(hlp(self.p(), ugettext('page'), tags)) if self.parent is not None: l.insert( 0, self.parent.html(tags=tags, pretty_title=pretty_title, link=pretty_title)) l = (l[0], small(str_list(l[1:]), tags=tags)) if pretty_title else l out = str_list(l) if link: return mark_safe(href(url, out, tags)) return out html.short_description = _('rendu HTML') html.allow_tags = True def pretty_title(self): return self.html(pretty_title=True, link=False) def has_events(self): if hasattr(self, '_has_events'): return self._has_events return self.evenements.exists() has_events.short_description = _('événements') has_events.boolean = True has_events.admin_order_field = 'evenements' def has_program(self): if hasattr(self, '_has_program'): return self._has_program return self.evenements.with_program().exists() has_program.short_description = _('programme') has_program.boolean = True def is_other(self): return self.type_fichier == FileAnalyzer.OTHER def is_pdf(self): return self.is_other() and self.fichier.name.endswith('.pdf') def is_image(self): return self.type_fichier == FileAnalyzer.IMAGE def is_audio(self): return self.type_fichier == FileAnalyzer.AUDIO def is_video(self): return self.type_fichier == FileAnalyzer.VIDEO def has_children_images(self): return self.children.filter(type_fichier=FileAnalyzer.IMAGE).exists() def has_images(self): return (self.type_fichier == FileAnalyzer.IMAGE or self.has_children_images()) def has_fichiers(self): return (self.is_other() or self.is_audio() or self.is_video() or self.has_images()) def images_iterator(self): if self.is_image(): yield self for child in Source.objects.filter( Q(parent=self) | Q(parent__parent=self), type_fichier=FileAnalyzer.IMAGE, ).order_by('position', 'page'): yield child @cached_property def images(self): return list(self.images_iterator()) @cached_property def preview_image(self): return next(self.images_iterator()) @cached_property def is_collection(self): return Source.objects.filter(parent__parent=self).exists() def is_empty(self): return not (self.transcription or self.url or self.has_fichiers()) DATA_TYPES = ('video', 'audio', 'image', 'other', 'text', 'link') VIDEO, AUDIO, IMAGE, OTHER, TEXT, LINK = DATA_TYPES @property def data_types(self): data_types = [] if self.is_video(): data_types.append(self.VIDEO) if self.is_audio(): data_types.append(self.AUDIO) if self.has_images(): data_types.append(self.IMAGE) if self.is_other(): data_types.append(self.OTHER) if self.transcription: data_types.append(self.TEXT) if self.url: data_types.append(self.LINK) return data_types ICONS = { VIDEO: '<i class="fa fa-fw fa-video-camera"></i>', AUDIO: '<i class="fa fa-fw fa-volume-up"></i>', IMAGE: '<i class="fa fa-fw fa-photo"></i>', OTHER: '<i class="fa fa-fw fa-paperclip"></i>', TEXT: '<i class="fa fa-fw fa-file-text-o"></i>', LINK: '<i class="fa fa-fw fa-external-link"></i>', } DATA_TYPES_WITH_ICONS = ( (VIDEO, _(f'{ICONS[VIDEO]} Vidéo')), (AUDIO, _(f'{ICONS[AUDIO]} Audio')), (IMAGE, _(f'{ICONS[IMAGE]} Image')), (OTHER, _(f'{ICONS[OTHER]} Autre')), (TEXT, _(f'{ICONS[TEXT]} Texte')), (LINK, _(f'{ICONS[LINK]} Lien')), ) @property def icons(self): return ''.join( [self.ICONS[data_type] for data_type in self.data_types]) def update_media_info(self): if self.fichier: file_analyzer = FileAnalyzer(self, 'fichier') self.type_fichier = file_analyzer.type else: self.type_fichier = None def clean(self): super().clean() if not getattr(self, 'updated_media_info', False): self.update_media_info() self.updated_media_info = True @cached_property def first_page(self): return self.children.order_by('position').first() @cached_property def prev_page(self): if self.parent is not None: return self.parent.children.exclude(pk=self.pk).filter( position__lte=self.position, ).order_by('-position').first() @cached_property def next_page(self): if self.parent is not None: return self.parent.children.exclude(pk=self.pk).filter( position__gte=self.position, ).order_by('position').first() @property def filename(self): return Path(self.fichier.path).name @cached_property def linked_individus(self): return self.individus.distinct() @cached_property def linked_evenements(self): return self.evenements.distinct() @cached_property def linked_oeuvres(self): return self.oeuvres.distinct() @cached_property def linked_ensembles(self): return self.ensembles.distinct() @cached_property def linked_lieux(self): return self.lieux.distinct() @cached_property def linked_parties(self): return self.parties.distinct() def get_linked_objects(self): return [ *self.auteurs.all(), *self.linked_individus, *self.linked_evenements, *self.linked_oeuvres, *self.linked_ensembles, *self.linked_lieux, *self.linked_parties, ] def get_linked_objects_json(self): return json.dumps([{ 'url': obj.get_absolute_url(), 'label': str(obj), 'model': obj.class_name().lower(), } for obj in self.get_linked_objects()]) def nested_evenements(self): return apps.get_model('libretto.Evenement').objects.filter( sources__in=self.children.all() | Source.objects.filter(pk=self.pk)).distinct() def nested_oeuvres(self): return apps.get_model('libretto.Oeuvre').objects.filter( sources__in=self.children.all() | Source.objects.filter(pk=self.pk)).distinct() def nested_individus(self): return apps.get_model('libretto.Individu').objects.filter( sources__in=self.children.all() | Source.objects.filter(pk=self.pk)).distinct() def nested_ensembles(self): return apps.get_model('libretto.Ensemble').objects.filter( sources__in=self.children.all() | Source.objects.filter(pk=self.pk)).distinct() def nested_lieux(self): return apps.get_model('libretto.Lieu').objects.filter( sources__in=self.children.all() | Source.objects.filter(pk=self.pk)).distinct() def nested_parties(self): return apps.get_model('libretto.Partie').objects.filter( sources__in=self.children.all() | Source.objects.filter(pk=self.pk)).distinct() @property def small_thumbnail(self): if self.is_image(): thumbnailer = get_thumbnailer(self.fichier) return thumbnailer.get_thumbnail(aliases.get('small')).url @property def medium_thumbnail(self): if self.is_image(): thumbnailer = get_thumbnailer(self.fichier) return thumbnailer.get_thumbnail(aliases.get('medium')).url @staticmethod def autocomplete_search_fields(): return ( 'type__nom__unaccent__icontains', 'titre__unaccent__icontains', 'date__icontains', 'date_approx__unaccent__icontains', 'numero__unaccent__icontains', 'lieu_conservation__unaccent__icontains', 'cote__unaccent__icontains', )
class Individu(AutoriteModel, UniqueSlugModel): particule_nom = CharField( _('particule du nom d’usage'), max_length=10, blank=True, db_index=True) # TODO: rendre le champ nom 'blank' nom = CharField(_('nom d’usage'), max_length=200, db_index=True) particule_nom_naissance = CharField( _('particule du nom de naissance'), max_length=10, blank=True, db_index=True) nom_naissance = CharField( _('nom de naissance'), max_length=200, blank=True, db_index=True, help_text=_('Ne remplir que s’il est différent du nom d’usage.')) prenoms = CharField(_('prénoms'), max_length=50, blank=True, db_index=True, help_text=ex('Antonio')) prenoms_complets = CharField( _('prénoms complets'), max_length=100, blank=True, db_index=True, help_text= ex('Antonio Lucio', post=' Ne remplir que s’il existe un ou des prénoms ' 'peu usités pour cet individu.')) pseudonyme = CharField(_('pseudonyme'), max_length=200, blank=True, db_index=True) DESIGNATIONS = ( ('S', _('Standard (nom, prénoms et pseudonyme)')), ('P', _('Pseudonyme (uniquement)')), ('L', _('Nom d’usage (uniquement)')), # L pour Last name ('B', _('Nom de naissance (standard)')), # B pour Birth name ('F', _('Prénom(s) (uniquement)')), # F pour First name ) designation = CharField(_('affichage'), max_length=1, choices=DESIGNATIONS, default='S') TITRES = ( ('M', _('M.')), ('J', _('Mlle')), # J pour Jouvencelle ('F', _('Mme')), ) titre = CharField(pgettext_lazy('individu', 'titre'), max_length=1, choices=TITRES, blank=True, db_index=True) naissance = AncrageSpatioTemporel(has_heure=False, verbose_name=_('naissance')) deces = AncrageSpatioTemporel(has_heure=False, verbose_name=_('décès')) professions = ManyToManyField( 'Profession', related_name='individus', blank=True, verbose_name=_('professions')) enfants = ManyToManyField( 'self', through='ParenteDIndividus', related_name='parents', symmetrical=False, verbose_name=_('enfants')) biographie = HTMLField(_('biographie'), blank=True) isni = CharField( _('Identifiant ISNI'), max_length=16, blank=True, validators=ISNI_VALIDATORS, help_text=_('Exemple : « 0000000121269154 » pour Mozart.')) sans_isni = BooleanField(_('sans ISNI'), default=False) objects = IndividuManager() class Meta(object): verbose_name = _('individu') verbose_name_plural = _('individus') ordering = ('nom',) permissions = (('can_change_status', _('Peut changer l’état')),) @staticmethod def invalidated_relations_when_saved(all_relations=False): relations = ('auteurs', 'elements_de_distribution',) if all_relations: relations += ('enfants', 'dossiers',) return relations def get_slug(self): parent = super(Individu, self).get_slug() return slugify_unicode(self.nom) or parent @permalink def get_absolute_url(self): return 'individu_detail', (self.slug,) @permalink def permalien(self): return 'individu_permanent_detail', (self.pk,) def link(self): return self.html() link.short_description = _('lien') link.allow_tags = True def oeuvres(self): oeuvres = self.auteurs.oeuvres() return oeuvres.exclude(extrait_de__in=oeuvres) def oeuvres_with_descendants(self): return self.auteurs.oeuvres() def publications(self): return self.auteurs.sources() def apparitions(self): # FIXME: Gérer la période d’activité des membres d’un groupe. sql = """ SELECT DISTINCT COALESCE(distribution.evenement_id, programme.evenement_id) FROM libretto_elementdedistribution AS distribution LEFT JOIN libretto_elementdeprogramme AS programme ON (programme.id = distribution.element_de_programme_id) WHERE distribution.individu_id = %s """ with connection.cursor() as cursor: cursor.execute(sql, (self.pk,)) evenement_ids = [t[0] for t in cursor.fetchall()] return Evenement.objects.filter(id__in=evenement_ids) def evenements_referents(self): return Evenement.objects.filter( programme__oeuvre__auteurs__individu=self).distinct() def membre_de(self): return self.membres.order_by('-debut', 'instrument', 'classement') def calc_titre(self, tags=False): titre = self.titre if not titre: return '' if tags: if titre == 'M': return hlp(ugettext('M.'), 'Monsieur') elif titre == 'J': return hlp(ugettext('M<sup>lle</sup>'), 'Mademoiselle') elif titre == 'F': return hlp(ugettext('M<sup>me</sup>'), 'Madame') if titre == 'M': return ugettext('Monsieur') elif titre == 'J': return ugettext('Mademoiselle') elif titre == 'F': return ugettext('Madame') raise ValueError('Type de titre inconnu, il devrait être M, J, ou F.') def is_feminin(self): return self.titre in ('J', 'F',) def get_particule(self, naissance=False, lon=True): particule = (self.particule_nom_naissance if naissance else self.particule_nom) if lon and particule and particule[-1] not in "'’": return f'{particule} ' return particule def calc_professions(self, tags=True): if not self.pk: return '' return str_list_w_last( p.html(feminin=self.is_feminin(), tags=tags, caps=i == 0) for i, p in enumerate(self.professions.all())) calc_professions.short_description = _('professions') calc_professions.admin_order_field = 'professions__nom' calc_professions.allow_tags = True def html(self, tags=True, lon=False, show_prenoms=True, designation=None, abbr=True, links=True): if designation is None: designation = self.designation titre = self.calc_titre(tags) prenoms = (self.prenoms_complets if lon and self.prenoms_complets else self.prenoms) nom = self.nom if lon: nom = f'{self.get_particule()}{nom}' pseudonyme = self.pseudonyme def standard(main, prenoms): particule = self.get_particule(naissance=(designation == 'B'), lon=lon) l = [] if nom and not prenoms: l.append(titre) l.append(main) if show_prenoms and (prenoms or particule and not lon): if lon: l.insert(max(len(l) - 1, 0), prenoms) else: if prenoms: prenoms = abbreviate(prenoms, tags=tags, enabled=abbr) if particule: particule = sc(particule, tags) prenom_and_particule = (f'{prenoms} {particule}' if prenoms and particule else (prenoms or particule)) l.append(f'({prenom_and_particule})') out = str_list(l, ' ') if pseudonyme: alias = (ugettext('dite') if self.is_feminin() else ugettext('dit')) out += f' {alias}\u00A0{pseudonyme}' return out if designation in 'SL': main = nom elif designation == 'F': main = prenoms elif designation == 'P': main = pseudonyme elif designation == 'B': nom_naissance = self.nom_naissance if lon: nom_naissance = f'{self.get_particule(True)}{nom_naissance}' main = nom_naissance main = sc(main, tags) out = standard(main, prenoms) if designation in 'SB' else main if tags: return href(self.get_absolute_url(), out, links) return out html.short_description = _('rendu HTML') html.allow_tags = True def nom_seul(self, tags=False, abbr=False, links=False): return self.html(tags=tags, lon=False, show_prenoms=False, abbr=abbr, links=links) def nom_complet(self, tags=True, designation='S', abbr=False, links=True): return self.html(tags=tags, lon=True, designation=designation, abbr=abbr, links=links) def related_label(self, tags=False): return self.html(tags=tags, abbr=False) related_label.short_description = _('individu') def related_label_html(self): return self.related_label(tags=True) def clean(self): naissance = self.naissance.date deces = self.deces.date if naissance and deces and deces < naissance: message = _('Le décès ne peut précéder la naissance.') raise ValidationError({'naissance_date': message, 'deces_date': message}) if self.isni and self.sans_isni: message = _('« ISNI » ne peut être rempli ' 'lorsque « Sans ISNI » est coché.') raise ValidationError({'isni': message, 'sans_isni': message}) def __str__(self): return strip_tags(self.html(tags=False)) @staticmethod def autocomplete_search_fields(): return ( 'nom__unaccent__icontains', 'nom_naissance__unaccent__icontains', 'pseudonyme__unaccent__icontains', 'prenoms__unaccent__icontains', )