Source code for fairdm.utils.utils

"""Small helpers for models, settings, documentation URLs and crispy layouts."""

from __future__ import annotations

from typing import TYPE_CHECKING

from crispy_forms.layout import HTML, Column, Fieldset, Layout, Row
from django.apps import apps
from django.conf import settings
from django.shortcuts import get_object_or_404
from django.utils.text import slugify

from fairdm.contrib import CORE_MAPPING

if TYPE_CHECKING:
    pass

DOCUMENTATION_BASE_URL = "https://docs.fairdm.org/latest/users/"


[docs] def user_guide(name: str) -> str: """Return the documentation URL for a given name. Args: name: The name of the documentation section. Returns: The URL to the documentation section. """ return f"{DOCUMENTATION_BASE_URL}{name}.html"
[docs] def get_setting(name: str, key: str): """Return a value from a FAIRDM dictionary setting. Args: name: The name of the FAIRDM setting (without FAIRDM_ prefix). key: The key within the setting dictionary. Returns: The setting value or None if the key is not found. """ settings_dict = getattr(settings, f"FAIRDM_{name}") return settings_dict.get(key)
[docs] def get_subclasses(model): """Return the registered Django model subclasses of a given model. Iterates the app registry's models and keeps those that are subclasses of ``model``, excluding the model itself. Args: model: The Django model class for which to find subclasses. Returns: The direct and indirect subclasses of ``model``. Example: >>> class A(models.Model): ... pass >>> class B(A): ... pass >>> class C(B): ... pass >>> get_subclasses(A) [B, C] """ models = apps.get_models() return [m for m in models if issubclass(m, model) and m != model]
[docs] def get_inheritance_chain(model, base_model): """Return the inheritance chain of a model up to a base model. Walks the model's method resolution order and collects every model class that is a subclass of ``base_model``. Args: model: The Django model class for which to determine the inheritance chain. base_model: The base model class where the chain ends. Returns: The model classes in the chain, starting from ``model`` and ending at ``base_model``. Example: >>> class A(models.Model): ... pass >>> class B(A): ... pass >>> class C(B): ... pass >>> get_inheritance_chain(C, A) [C, B, A] """ chain = [] for base in model.__mro__: if hasattr(base, "_meta") and issubclass(base, base_model): chain.append(base) return chain
[docs] def get_model_class(uuid: str): """Return the core model class a UUID belongs to. The UUID's first character identifies the model. Args: uuid: A core object's shortuuid primary key. Returns: The model class. """ return apps.get_model(CORE_MAPPING[uuid[0]])
[docs] def get_core_object_or_none(uuid: str) -> tuple: """Return the model class and the object matching a UUID. Args: uuid: The UUID of the object to retrieve. Returns: A tuple of the model class and the first object with that UUID, or ``None`` in place of the object when none exists. """ model = get_model_class(uuid) return model, model.objects.filter(uuid=uuid).first()
[docs] def get_core_object_or_404(uuid: str): """Return the core object with a shortuuid primary key, or raise a 404. Args: uuid: A core object's shortuuid primary key. Returns: The matching object. ``Http404`` is raised when no object has that UUID. """ model = get_model_class(uuid) return get_object_or_404(model, uuid=uuid)
[docs] def default_image_path(instance, filename: str) -> str: """Generate the upload path for an image. Args: instance: The model instance the image is being uploaded to. filename: The original filename of the uploaded image. Returns: A relative file path for storing the image. """ model_name = slugify(instance._meta.verbose_name_plural) return f"{model_name}/{instance.uuid}/{filename}"
[docs] def fieldsets_to_crispy_layout(fieldsets): """Convert Django fieldsets into a crispy-forms Layout. Takes fieldsets typically defined in Django's `admin.py` and transforms them into a crispy-forms `Layout`, grouping fields into `Fieldset` containers and organizing grouped fields into `Row` and `Column` structures. The ``fields`` and ``help_text`` entries are popped from each options dict, so the input is modified. Args: fieldsets: A list of tuples, each containing: - `legend` (str or None): The title of the fieldset. - `options` (dict): A dictionary holding `"fields"`, a list of field names or tuples/lists of field names to be grouped, and optionally `"help_text"`. Returns: A crispy-forms `Layout` representing the given fieldsets. Example: >>> fieldsets = [ >>> ("Personal Info", {"fields": ["first_name", "last_name"]}), >>> ("Contact", {"fields": [("email", "phone")]}), >>> ] >>> fieldsets_to_crispy_layout(fieldsets) Layout( Fieldset("Personal Info", "first_name", "last_name"), Fieldset("Contact", Row(Column("email"), Column("phone"))) ) """ crispy_layout = [] for legend, options in fieldsets: layout = [legend] help_text = options.pop("help_text", None) if help_text: layout.append(HTML(f"<p class='help-text'>{help_text}</p>")) fields = options.pop("fields", []) field_layout = fields_to_crispy_layout(fields) layout.extend(field_layout.fields) crispy_layout.append(Fieldset(*layout, **options)) return Layout(*crispy_layout)
[docs] def fields_to_crispy_layout(fields): """Convert a flat list of fields, or tuples and lists of fields, into a crispy-forms Layout. Single field names are added directly. Tuples and lists of field names are wrapped in Columns inside a Row. Args: fields: Field names, or tuples or lists of field names to place side by side. Returns: A crispy-forms `Layout`. """ layout = [] for field in fields: if isinstance(field, (tuple, list)): columns = [Column(f) for f in field] layout.append(Row(*columns)) else: layout.append(field) return Layout(*layout)
[docs] def fairdm_fieldsets_to_django(fieldsets): """Convert FairDM-style fieldsets to Django-style fieldsets. Args: fieldsets: A dict mapping each legend to its options dict. Returns: A tuple of ``(legend, options)`` pairs. """ return tuple((key, value) for key, value in fieldsets.items())