Source code for ferrosoft.apps.ferrobase.views.files
# Copyright (c) 2025 Ferrosoft GmbH. All rights reserved.
from urllib.parse import urljoin
from django.contrib.postgres.aggregates import ArrayAgg
from django.utils.translation import gettext_lazy as _
from ferrosoft.apps.ferrobase.filter import StoredFileFilterSet
from ferrosoft.apps.ferrobase.models import (
Directory,
StoredFile,
User,
LocalUser,
AccessSubject,
AccessMode,
FileAccessGrant,
)
from ferrosoft.apps.ferrobase.tables.models import StoredFileTable
from ferrosoft.apps.ferrobase.views.media import FORCE_DOWNLOAD_PARAM
from ferrosoft.apps.ferrobase.views.generic import ListView, TableView, UpdateView
[docs]
class DirectoryRootView(ListView):
"""Top-level directory browser showing only root directories."""
activity_title = _("Files")
help_topic = "files"
model = Directory
ordering = ["name"]
[docs]
def get_queryset(self):
"""Restrict to directories without a parent (i.e. roots)."""
return super().get_queryset().filter(parent_directory=None)
[docs]
class DirectoryListingView(TableView):
"""List files within a directory filtered by per-user access grants."""
activity_title = _("Files")
filterset_class = StoredFileFilterSet
help_topic = "files"
model = StoredFile
ordering = ["name"]
table_class = StoredFileTable
[docs]
def get_queryset(self):
"""Union three access scopes (owner, organisation, public) for the directory.
The UNION is built from three filtered querysets — owner files
bypass access grants entirely, while organisation- and public-scoped
files require an explicit READ grant. ``.distinct()`` collapses
duplicates introduced by the UNION.
"""
directory_id = self.request.resolver_match.kwargs.get("directory")
# Files are selected differently for each access subject and returned in
# one big, beautiful UNION query.
#
# Owner files are a simple query, bypassing access grants since owner
# must be able to manage permissions.
owner_files = (
super()
.get_queryset()
.filter(
directory_id=directory_id,
owner=LocalUser.load(self.request.user),
)
)
org_files = (
super()
.get_queryset()
.filter(
directory_id=directory_id,
owner__username__in=User.objects.filter(
organization_id=self.request.user.organization_id
).values_list("username", flat=True),
access_grants__subject=AccessSubject.ORGANIZATION,
access_grants__mode=AccessMode.READ,
)
)
public_files = (
super()
.get_queryset()
.filter(
directory_id=directory_id,
access_grants__subject=AccessSubject.PUBLIC,
access_grants__mode=AccessMode.READ,
)
)
return (owner_files | org_files | public_files).distinct()
[docs]
class FileUpdateView(UpdateView):
"""Rename a file and edit its access grants matrix."""
activity_title = _("File")
help_topic = "files"
model = StoredFile
fields = ["name"]
template_name = "ferrobase/storedfile/update.html"
[docs]
def get_context_data(self, **kwargs):
"""Build the ``{subject: {mode: bool}}`` access matrix and download URL."""
grants = {
AccessSubject(grant["subject"]): [
AccessMode(mode) for mode in grant["modes"]
]
for grant in self.object.access_grants.values("subject").annotate(
modes=ArrayAgg("mode")
)
}
grants_matrix = {}
for subject in AccessSubject:
grants_matrix[subject] = {
mode: mode in grants.get(subject, []) for mode in AccessMode
}
return super().get_context_data(**kwargs) | {
"download_url": urljoin(
self.object.get_absolute_url(), f"?{FORCE_DOWNLOAD_PARAM}=1"
),
"grants": grants_matrix,
"access_modes": AccessMode,
}