L'application mayan/apps/backends/ fournit l'infrastructure commune de backends pluggables utilisée partout dans Mayan EDMS : sources, storage, authentification, actions de workflow, etc. Elle définit le registre de classes, le mixin de modèle DB, et les formulaires/vues dynamiques associés.
Un backend est une implémentation interchangeable d'une fonctionnalité. Le pattern est toujours le même :
BaseBackend ou sous-classe) sert de registre et définit l'interface.backend_path (chemin Python) + backend_data (JSON) pour instancier le backend à la demande.BaseBackend — le registre# mayan/apps/backends/classes.py
class BaseBackend(AppsModuleLoaderMixin, metaclass=BackendMetaclass):
_loader_module_name = None # nom du module où chercher les backends
_registry = {} # {classe_parente: {backend_id: classe}}
is_visible = True # si False, masqué dans les sélections UI
@classproperty
def backend_class_path(cls): ... # chemin Python complet (ex. 'mayan.apps.sources...')
@classproperty
def backend_id(cls): ... # identifiant unique (par défaut = backend_class_path)
@classmethod
def get(cls, name): ... # récupère un backend par id
@classmethod
def get_all(cls): ... # liste tous les backends enregistrés
@classmethod
def get_choices(cls): ... # liste (id, label) triée pour les formulaires
@classmethod
def register(cls, klass): ... # enregistre manuellement un backend
BackendMetaclassLa métaclasse BackendMetaclass s'exécute à la création de chaque sous-classe. Elle :
_loader_module_name défini._loader_module_name, elle est enregistrée automatiquement via register().Cela signifie que déclarer une sous-classe suffit à l'enregistrer — pas d'appel explicite à register() nécessaire pour les backends définis dans le bon module.
class SourceBackend(BaseBackend):
_loader_module_name = 'source_backends' # cherche dans les modules nommés 'source_backends'
# Dans source_web_forms/source_backends.py :
class SourceBackendWebForm(SourceBackend):
label = 'Web Form'
# → enregistré automatiquement dans SourceBackend._registry
BaseBackend| Classe | Usage | Stockage |
|---|---|---|
BaseBackend |
Base pure, pas de modèle DB lié | Aucun |
ModelBaseBackend |
Backend lié à une instance de modèle | Instance passée par model_instance_id |
StoredBaseBackend |
Backend créé automatiquement en DB à l'enregistrement | get_or_create à l'import |
ModelBaseBackendLe backend reçoit l'id de l'instance de modèle à l'initialisation. get_model_instance() recharge l'objet depuis la DB.
class ModelBaseBackend(BaseBackend):
_backend_app_label = 'sources'
_backend_model_name = 'Source'
def __init__(self, model_instance_id, **kwargs):
self.model_instance_id = model_instance_id
self.kwargs = kwargs
def get_model_instance(self):
return Source.objects.get(pk=self.model_instance_id)
StoredBaseBackendÀ l'enregistrement de la classe (register()), une ligne est automatiquement créée en DB via get_or_create. L'id DB est stocké en attribut de classe (_backend_stored_model_id).
BackendModelMixin — le mixin de modèleÀ appliquer au modèle Django qui stocke la sélection de backend + ses données :
# mayan/apps/backends/model_mixins.py
class BackendModelMixin(models.Model):
backend_path = CharField(max_length=128) # ex. 'mayan.apps.sources..SourceBackendWebForm'
backend_data = TextField(blank=True) # JSON des paramètres du backend
class Meta:
abstract = True
def get_backend_class(self) -> type: ... # importe et retourne la classe
def get_backend_class_label(self) -> str: ... # label lisible de la classe
def get_backend_data(self) -> dict: ... # désérialise backend_data
def set_backend_data(self, obj: dict): ... # sérialise et stocke
def get_backend_instance(self): ... # instancie le backend avec ses kwargs
Si le module du backend est manquant (ex. : dépendance optionnelle non installée) :
_backend_model_null_backend est défini sur le modèle → retourne le backend nul au lieu de planter.class Source(BackendModelMixin, ...):
_backend_model_null_backend = SourceBackendNull
source = Source.objects.get(pk=1)
# Récupérer la classe (sans instancier)
klass = source.get_backend_class()
print(klass.label)
# Instancier le backend avec ses paramètres stockés
backend = source.get_backend_instance()
# équivaut à : SourceBackendWebForm(model_instance_id=1, **json.loads(source.backend_data))
backend.do_something()
DynamicFormBackendMixin — formulaires dynamiquesPermet à un backend de décrire ses propres champs de configuration via des attributs de classe. Ces champs sont assemblés en un schéma DynamicForm (cf. app forms).
# mayan/apps/backends/class_mixins.py
class DynamicFormBackendMixin:
form_fields = {} # {nom: {class, label, required, default, help_text, kwargs}}
form_field_widgets = {} # {nom: {class, kwargs}}
form_fieldsets = () # fieldsets pour regrouper les champs
form_fieldset_exclude_list = ('backend_data',)
form_media = {} # JS/CSS supplémentaires
form_field_order = () # ordre des champs
@classmethod
def get_form_schema(cls, **kwargs) -> dict: ...
@classmethod
def get_form_fields(cls) -> dict: ...
@classmethod
def get_form_field_widgets(cls) -> dict: ...
@classmethod
def get_form_fieldsets(cls) -> tuple: ...
@classmethod
def get_form_media(cls) -> dict: ...
@classmethod
def do_sanity_check(cls, attribute, name): ... # détecte la mutation directe du dict parent
class SourceBackendWatchFolder(DynamicFormBackendMixin, ModelBaseBackend):
label = 'Watch Folder'
form_fields = {
'folder_path': {
'class': 'django.forms.CharField',
'label': 'Folder path',
'required': True,
},
'include_regex': {
'class': 'django.forms.CharField',
'label': 'Include filter (regex)',
'required': False,
'default': '',
},
}
form_fieldsets = (
('Configuration', {'fields': ('folder_path', 'include_regex')}),
)
Attention : ne jamais modifier
form_fieldsdirectement dans une sous-classe — copier le dict d'abord (form_fields = ParentBackend.form_fields.copy()).do_sanity_check()détecte cette erreur à l'initialisation.
FormDynamicModelBackend — formulaire de saisieFormulaire Django généré automatiquement à partir du schéma du backend :
# mayan/apps/backends/forms.py
class FormDynamicModelBackend(DynamicModelForm, metaclass=BackendDynamicFormMetaclass):
def __init__(self, backend_path=None, user=None, *args, **kwargs): ...
def clean(self): ... # consolide les champs dynamiques → backend_data (JSON)
def get_backend_fields(self): ...
def get_field_reload_attributes(self): return {'user': self.user}
backend_data est caché (HiddenInput) et rempli automatiquement lors du clean().QuerySet et instances de modèle sont convertis en id / liste d'id pour la sérialisation JSON.FormFieldFilteredModelChoice) sont rechargés avec l'utilisateur courant.BackendDynamicFormMetaclassMétaclasse qui ajoute automatiquement backend_data à Meta.fields du ModelForm et le configure en widget HiddenInput.
# mayan/apps/backends/views.py
class ViewSingleObjectDynamicFormModelBackendCreate(
ViewMixinDynamicFormBackendClass, SingleObjectDynamicFormCreateView
):
# get_backend_class() → depuis self.kwargs['backend_path']
class ViewSingleObjectDynamicFormModelBackendEdit(
ViewMixinDynamicFormBackendClass, SingleObjectDynamicFormEditView
):
# get_backend_class() → depuis self.object.get_backend_class()
ViewMixinDynamicFormBackendClassclass ViewMixinDynamicFormBackendClass:
def get_form_schema(self):
return self.get_backend_class().get_form_schema(**self.get_form_schema_extra_kwargs())
def get_form_schema_extra_kwargs(self):
return {}
Le mixin interroge la classe backend pour obtenir le schéma de formulaire, qui est ensuite passé au DynamicForm de la vue.
Utilisateur choisit un backend (ex. WatchFolder)
│
▼
ViewSingleObjectDynamicFormModelBackendCreate
├── get_backend_class() → SourceBackendWatchFolder
├── get_form_schema() → {'fields': {...}, 'fieldsets': (...)}
└── FormDynamicModelBackend(schema=...) affiche le formulaire
│
▼ (POST)
FormDynamicModelBackend.clean()
→ consolide les champs dynamiques dans backend_data (JSON)
│
▼
Source.save()
→ backend_path = 'mayan.apps...SourceBackendWatchFolder'
→ backend_data = '{"folder_path": "/data/inbox", "include_regex": ""}'
│
▼ (utilisation)
source.get_backend_instance()
→ SourceBackendWatchFolder(model_instance_id=1, folder_path='/data/inbox', ...)
| App | Classe parente | _loader_module_name |
|---|---|---|
sources |
SourceBackend |
source_backends |
storage |
(DefinedStorage) | — |
authentication |
AuthenticationBackend |
authentication_backends |
document_states |
WorkflowAction |
workflow_actions |
lock_manager |
LockingBackend |
locking_backends |