This repository has been archived on 2026-03-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
docs/src/backend/core/choices.py
Samuel Paccoud - DINUM 611ba496d2 ♻️(backend) simplify roles by returning only the max role
We were returning the list of roles a user has on a document (direct
and inherited). Now that we introduced priority on roles, we are able
to determine what is the max role and return only this one.

This commit also changes the role that is returned for the restricted
reach: we now return None because the role is not relevant in this
case.
2025-07-08 13:51:26 +02:00

143 lines
4.7 KiB
Python

"""Declare and configure choices for Docs' core application."""
from django.db.models import TextChoices
from django.utils.translation import gettext_lazy as _
class PriorityTextChoices(TextChoices):
"""
This class inherits from Django's TextChoices and provides a method to get the priority
of a given value based on its position in the class.
"""
@classmethod
def get_priority(cls, role):
"""Returns the priority of the given role based on its order in the class."""
members = list(cls.__members__.values())
return members.index(role) + 1 if role in members else 0
@classmethod
def max(cls, *roles):
"""
Return the highest-priority role among the given roles, using get_priority().
If no valid roles are provided, returns None.
"""
valid_roles = [role for role in roles if cls.get_priority(role) is not None]
if not valid_roles:
return None
return max(valid_roles, key=cls.get_priority)
class LinkRoleChoices(PriorityTextChoices):
"""Defines the possible roles a link can offer on a document."""
READER = "reader", _("Reader") # Can read
EDITOR = "editor", _("Editor") # Can read and edit
class RoleChoices(PriorityTextChoices):
"""Defines the possible roles a user can have in a resource."""
READER = "reader", _("Reader") # Can read
EDITOR = "editor", _("Editor") # Can read and edit
ADMIN = "administrator", _("Administrator") # Can read, edit, delete and share
OWNER = "owner", _("Owner")
PRIVILEGED_ROLES = [RoleChoices.ADMIN, RoleChoices.OWNER]
class LinkReachChoices(PriorityTextChoices):
"""Defines types of access for links"""
RESTRICTED = (
"restricted",
_("Restricted"),
) # Only users with a specific access can read/edit the document
AUTHENTICATED = (
"authenticated",
_("Authenticated"),
) # Any authenticated user can access the document
PUBLIC = "public", _("Public") # Even anonymous users can access the document
@classmethod
def get_select_options(cls, link_reach, link_role):
"""
Determines the valid select options for link reach and link role depending on the
list of ancestors' link reach/role definitions.
Returns:
Dictionary mapping possible reach levels to their corresponding possible roles.
"""
# If no ancestors, return all options
if not link_reach:
return {
reach: LinkRoleChoices.values if reach != cls.RESTRICTED else None
for reach in cls.values
}
# Initialize the result for all reaches with possible roles
result = {
reach: set(LinkRoleChoices.values) if reach != cls.RESTRICTED else None
for reach in cls.values
}
# Handle special rules directly with early returns for efficiency
if link_role == LinkRoleChoices.EDITOR:
# Rule 1: public/editor → override everything
if link_reach == cls.PUBLIC:
return {cls.PUBLIC: [LinkRoleChoices.EDITOR]}
# Rule 2: authenticated/editor
if link_reach == cls.AUTHENTICATED:
result[cls.AUTHENTICATED].discard(LinkRoleChoices.READER)
result.pop(cls.RESTRICTED, None)
if link_role == LinkRoleChoices.READER:
# Rule 3: public/reader
if link_reach == cls.PUBLIC:
result.pop(cls.AUTHENTICATED, None)
result.pop(cls.RESTRICTED, None)
# Rule 4: authenticated/reader
if link_reach == cls.AUTHENTICATED:
result.pop(cls.RESTRICTED, None)
# Convert sets to ordered lists where applicable
return {
reach: sorted(roles, key=LinkRoleChoices.get_priority) if roles else roles
for reach, roles in result.items()
}
def get_equivalent_link_definition(ancestors_links):
"""
Return the (reach, role) pair with:
1. Highest reach
2. Highest role among links having that reach
"""
if not ancestors_links:
return {"link_reach": None, "link_role": None}
# 1) Find the highest reach
max_reach = max(
ancestors_links,
key=lambda link: LinkReachChoices.get_priority(link["link_reach"]),
)["link_reach"]
# 2) Among those, find the highest role (ignore role if RESTRICTED)
if max_reach == LinkReachChoices.RESTRICTED:
max_role = None
else:
max_role = max(
(
link["link_role"]
for link in ancestors_links
if link["link_reach"] == max_reach
),
key=LinkRoleChoices.get_priority,
)
return {"link_reach": max_reach, "link_role": max_role}