Пример #1
0
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',
Пример #2
0
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',
Пример #3
0
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',
        )
Пример #4
0
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',
        )