mayan/apps/common/ — boîte à outils partagée par toutes les autres apps. Ce n'est pas une fonctionnalité métier : c'est la plomberie interne de Mayan.
Common fournit cinq familles de choses :
ModelCopy — copie générique d'instances Django (tous types de champs)resolve_attribute / ResolverPipeline — résolution unifiée de chemins pointés sur n'importe quel objetpre_initial_setup, pre_upgrade, pre_save…COMMON_EXTRA_APPS, COMMON_DISABLE_LOCAL_STORAGE, COMMON_HOME_VIEW…yaml_load/dump, validateurs JSON/YAML, MissingItem, PropertyHelperModelCopy — copier un objet et toute sa relationCopier un objet Django avec toutes ses FK, M2M, relations inverses et one-to-one demande du code répétitif et fragile. ModelCopy encapsule cette logique une fois pour toutes, déclarativement.
# Dans apps.py d'une app tierce, dans ready()
from mayan.apps.common.classes import ModelCopy
ModelCopy(
model=DocumentType,
bind_link=True, # ajoute le lien "Copier" dans menu_object
register_permission=True # crée une permission "Copier les types de documents"
).add_fields(
field_names=(
'label', # champ scalaire unique → renommé _1, _2…
'filename_generator_backend', # FK
'labels', # M2M
'document_stubs', # relation inverse
),
field_value_templates={
'label': '{instance.label}_copy' # valeur calculée
}
)
ModelCopy.copy(instance)copy(instance)
├── champs scalaires (fields_copy) → copiés tels quels
├── champs unique (fields_unique) → suffixe _1, _2… jusqu'à trouver libre
├── foreign keys (fields_foreign_keys) → copiés (même référence)
├── M2M (fields_many_to_many) → .set(original.all())
├── relations inverses (reverse_related) → chaque objet lié appelle copy_instance()
├── one-to-one inverse → copy_instance() sur l'objet lié
└── generic relations (GenericRelation) → copy_instance() sur chaque objet lié
Cas spécial : si le modèle est un arbre MPTT, copy() parcourt les descendants dans l'ordre et reconstruit la hiérarchie dans la copie.
ModelCopy.__init__ appelle model.add_to_class('copy_instance', ...) — après enregistrement, tout objet dispose de :
doc_type.copy_instance(values={'label': 'Copie factures'})
resolve_attribute — chemin pointé universelPartout dans Mayan (SourceColumn, lien REST API, vérification ACL, formulaires…), il faut accéder à une valeur sur un objet dont on ne connaît pas à l'avance le type exact : propriété, méthode, clé de dictionnaire, attribut d'un objet lié… resolve_attribute unifie tout ça en un seul appel.
resolve_attribute(attribute, obj, kwargs=None)
attribute : chaîne de caractères (chemin pointé) ou callableobj : l'objet sourcekwargs : arguments optionnels passés si l'attribut est une méthode# SourceColumn dans documents/apps.py
SourceColumn(source=DocumentFile, attribute='filename', is_identifier=True)
# Ce que fait column.resolve(context) en interne :
resolve_attribute(attribute='filename', obj=document_file)
# → document_file.filename
# → "rapport-annuel-2024.pdf"
SourceColumn(source=DocumentFile, attribute='get_size_display', is_sortable=True, sort_field='size')
resolve_attribute(attribute='get_size_display', obj=document_file)
# → document_file.get_size_display() ← appelée automatiquement
# → "1.2 Mo"
resolve_attribute détecte que get_size_display est une méthode (callable) et l'appelle sans argument.
SourceColumn(
source=DocumentFile,
attribute='get_page_count',
kwargs={'user': 'request.user'}, # ← résolu depuis le contexte Django template
)
resolve_attribute(attribute='get_page_count', obj=document_file, kwargs={'user': marie})
# → document_file.get_page_count(user=marie)
# → 12
L'argument user permet à la méthode de filtrer les pages selon les permissions de l'utilisateur courant.
# On veut afficher le label du type de document dans la liste des fichiers
SourceColumn(source=DocumentFile, attribute='document.document_type.label')
resolve_attribute(attribute='document.document_type.label', obj=document_file)
# Étape 1 : document_file.document → objet Document
# Étape 2 : document.document_type → objet DocumentType
# Étape 3 : document_type.label → "Factures fournisseurs"
Chaque segment du chemin est résolu indépendamment par le pipeline de résolveurs.
# mayan/apps/acls/managers.py
# Quand un modèle hérite d'un autre (ex : DocumentFile hérite de Document),
# il faut remonter à l'objet parent pour vérifier les ACL.
parent_object = resolve_attribute(
obj=obj,
attribute=inheritance['field_name'] # ex : 'document'
)
La même fonction sert à traverser l'héritage de modèle pour atteindre l'objet parent sur lequel vérifier les permissions.
# mayan/apps/rest_api/relations.py
# Pour construire l'URL d'un objet dans l'API, on résout le lookup_field
# (ex : 'document.pk', 'pk') dynamiquement sur l'objet sérialisé.
kwargs[entry['lookup_url_kwarg']] = resolve_attribute(
attribute=entry['lookup_field'], # ex : 'document_id' ou 'document.pk'
obj=obj
)
# → construit reverse('api:document-detail', kwargs={'document_id': 42})
# mayan/apps/forms/forms.py
# Un formulaire générique construit ses champs à partir d'une liste d'attributs.
# resolve_attribute récupère la valeur initiale pour pré-remplir le champ.
value = resolve_attribute(attribute=field, obj=obj)
# Si field='document_type', récupère obj.document_type
# Si field='get_date_added', appelle obj.get_date_added()
ResolverPipelineObjectAttribute)Pour chaque segment du chemin, les résolveurs sont essayés dans l'ordre jusqu'à ce que l'un réussisse :
| Résolveur | Tente | Échoue si… |
|---|---|---|
ResolverDictionary |
obj[attribute] |
obj n'est pas un dict |
ResolverList |
itère et résout sur chaque élément | obj n'est pas une liste |
ResolverFunction |
getattr(obj, attr)(**kwargs) |
attr n'est pas une méthode |
ResolverObjectAttribute |
attribute(obj, **kwargs) |
attribute n'est pas un callable |
ResolverGetattr |
getattr(obj, attr) |
attribut absent |
ResolverPipelineModelAttribute ajoute ResolverRelatedManager pour gérer les FK/M2M Django avec double underscore (document__type__label).
Common définit des signaux Django pour les événements d'infrastructure de Mayan :
| Signal | Émis quand |
|---|---|
signal_pre_initial_setup |
Avant la première installation (→ lance migrate) |
signal_post_initial_setup |
Après la première installation |
signal_pre_upgrade |
Avant une mise à jour (→ lance migrate --fake-initial) |
signal_post_upgrade |
Après une mise à jour |
signal_perform_upgrade |
Pendant la mise à jour |
signal_mayan_pre_save |
Avant la sauvegarde d'un objet Mayan (hook global) |
# Dans apps.py de n'importe quelle app
from mayan.apps.common.signals import signal_post_upgrade
signal_post_upgrade.connect(
receiver=my_post_upgrade_handler,
dispatch_uid='myapp_post_upgrade'
)
| Clé | Défaut | Rôle |
|---|---|---|
COMMON_EXTRA_APPS |
[] |
Apps supplémentaires chargées après les apps Mayan (ex : image_barcode_extract) |
COMMON_EXTRA_APPS_PRE |
[] |
Apps supplémentaires chargées avant |
COMMON_DISABLED_APPS |
[] |
Apps Mayan à retirer de INSTALLED_APPS |
COMMON_DISABLE_LOCAL_STORAGE |
false |
Interdit l'usage du dossier media/ local — force l'usage de backends S3/cloud |
COMMON_HOME_VIEW |
common:home |
Vue affichée après connexion et au clic sur le logo |
COMMON_HOME_VIEW_DASHBOARD_NAME |
user |
Nom du dashboard affiché sur la page d'accueil |
COMMON_PROJECT_TITLE |
null |
Titre affiché dans l'interface (remplace "Mayan EDMS") |
COMMON_DISABLE_LOCAL_STORAGECe setting est une sécurité pour les déploiements cloud. Quand il vaut true, Mayan refuse de démarrer si un storage backend pointe encore vers le filesystem local. Utile pour éviter qu'un worker isolé écrive silencieusement dans son propre media/ sans que les autres workers y aient accès.
MissingItem — vérifications de santéRegistre d'éléments manquants affichés dans l'interface Admin :
MissingItem(
label=_('Index de recherche Elasticsearch'),
condition=lambda: not ElasticSearchBackend.is_available(),
description=_('L\'index n\'est pas joignable.'),
view='search:backend_reindex'
)
Les items dont condition() retourne True sont affichés sur la page d'accueil admin avec un lien vers la vue corrective.
PropertyHelper — propriétés dynamiquesClasse de base pour injecter des propriétés calculées sur un modèle via add_to_class :
class DocumentPropertyHelper(PropertyHelper):
@staticmethod
@property
def constructor(source_object):
return DocumentPropertyHelper(source_object)
def get_result(self, name):
return compute_something(self.instance, name)
Document.add_to_class('my_props', DocumentPropertyHelper.constructor)
# → doc.my_props.whatever appelle get_result('whatever')
| Classe/objet | Usage |
|---|---|
JSONValidator |
Valide qu'un champ de formulaire contient du JSON valide |
YAMLValidator |
Valide qu'un champ contient du YAML valide |
validate_internal_name |
Regex ^[a-zA-Z0-9_]+$ — noms internes Mayan |
yaml_load / yaml_dumpWrappers autour de PyYAML qui forcent CSafeLoader/CSafeDumper (version C plus rapide si disponible) et gèrent la conversion des SafeString Django.
Utilisés partout où Mayan sérialise/désérialise des settings ou des données de configuration.
utils.py| Fonction | Rôle |
|---|---|
resolve_attribute(attr, obj, kwargs) |
Chemin pointé universel (voir §2) |
get_related_field(model, field_name) |
Remonte les FK pour trouver un champ dans un modèle lié |
flatten_list(value) |
Aplati une liste imbriquée arbitrairement |
flatten_object(obj) |
Dict/liste → liste de (clé_aplatie, valeur) |
parse_range('1,3-5,8') |
Générateur d'entiers depuis une chaîne de plages |
group_iterator(it, n) |
Découpe un itérable en chunks de taille n |
comma_splitter(str) |
Split CSV avec gestion des guillemets (shlex) |
convert_to_internal_name(str) |
Slugify → remplace - par _ |
any_to_bool(val) |
Convertit 'true'/'false'/1/0 en bool |
DocumentsApp.ready()
└── ModelCopy(model=DocumentType, bind_link=True)
└── menu_object.bind_links([link_object_copy], sources=[DocumentType])
└── model.add_to_class('copy_instance', method_instance_copy)
Vue "Copier un type de document"
└── doc_type.copy_instance(values={'label': 'Copie'})
└── ModelCopy.copy(instance=doc_type)
├── copie champs scalaires
├── appelle copy_instance() sur les objets liés
└── retourne le nouveau DocumentType
SourceColumn(attribute='document.document_type.label', source=DocumentFile)
└── column.resolve(context) → pour chaque fichier dans la liste
└── resolve_attribute('document.document_type.label', obj=document_file)
├── document_file.document → Document(id=42)
├── document.document_type → DocumentType(id=3)
└── document_type.label → "Factures fournisseurs"
| Fichier | Contenu |
|---|---|
classes.py |
ModelCopy, MissingItem, PropertyHelper, UpstreamSetting |
utils.py |
resolve_attribute, ResolverPipeline*, utilitaires divers |
signals.py |
Signaux de cycle de vie |
settings.py |
COMMON_EXTRA_APPS, COMMON_DISABLE_LOCAL_STORAGE, etc. |
validators.py |
JSONValidator, YAMLValidator, validate_internal_name |
serialization.py |
yaml_load, yaml_dump |
class_mixins.py |
AppsModuleLoaderMixin (chargement automatique de modules par app) |
handlers.py |
Handlers migrate sur signal_pre_initial_setup / signal_pre_upgrade |