mirror of
https://github.com/suitenumerique/docs.git
synced 2026-05-06 23:22:15 +02:00
Compare commits
4 Commits
make-title
...
documentat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e1a9b6a27 | ||
|
|
ecd06560c6 | ||
|
|
e9ab099ce0 | ||
|
|
67b69d05f7 |
@@ -8,11 +8,15 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## Added
|
||||
|
||||
- 🚩 add homepage feature flag #861
|
||||
|
||||
|
||||
## [3.1.0] - 2025-04-07
|
||||
|
||||
## Added
|
||||
|
||||
- ✨(backend) add ancestors links definitions to document abilities #846
|
||||
- 🚩(backend) add feature flag for the footer #841
|
||||
- 🔧(backend) add view to manage footer json #841
|
||||
- ✨(frontend) add custom css style #771
|
||||
@@ -24,7 +28,6 @@ and this project adheres to
|
||||
|
||||
## Fixed
|
||||
|
||||
- 🐛(backend) fix link definition select options linked to ancestors #846
|
||||
- 🐛(back) validate document content in serializer #822
|
||||
- 🐛(frontend) fix selection click past end of content #840
|
||||
|
||||
|
||||
@@ -48,6 +48,10 @@ All commit messages must adhere to the following format:
|
||||
Implemented login and signup features, and integrated OAuth2 for social login.
|
||||
```
|
||||
|
||||
## Signing commits
|
||||
|
||||
Only signed commits are accepted. They can be signed using a SSH or GPG key. Github documentation about signing commits contains all the information you need : https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification#about-commit-signature-verification
|
||||
|
||||
## Changelog Update
|
||||
|
||||
Please add a line to the changelog describing your development. The changelog entry should include a brief summary of the changes, this helps in tracking changes effectively and keeping everyone informed. We usually include the title of the pull request, followed by the pull request ID to finish the log entry. The changelog line should be less than 80 characters in total.
|
||||
|
||||
@@ -47,6 +47,11 @@ These are the environmental variables you can set for the impress-backend contai
|
||||
| COLLABORATION_API_URL | collaboration api host | |
|
||||
| COLLABORATION_SERVER_SECRET | collaboration api secret | |
|
||||
| COLLABORATION_WS_URL | collaboration websocket url | |
|
||||
| FRONTEND_CSS_URL | To add a external css file to the app | |
|
||||
| FRONTEND_HOMEPAGE_FEATURE_ENABLED | frontend feature flag to display the homepage | false |
|
||||
| FRONTEND_FOOTER_FEATURE_ENABLED | frontend feature flag to display the footer | false |
|
||||
| FRONTEND_FOOTER_VIEW_CACHE_TIMEOUT | Cache duration of the json footer | 86400 |
|
||||
| FRONTEND_URL_JSON_FOOTER | Url with a json to configure the footer | |
|
||||
| FRONTEND_THEME | frontend theme to use | |
|
||||
| POSTHOG_KEY | posthog key for analytics | |
|
||||
| CRISP_WEBSITE_ID | crisp website id for support | |
|
||||
|
||||
@@ -64,5 +64,6 @@ COLLABORATION_WS_URL=ws://localhost:4444/collaboration/ws/
|
||||
|
||||
# Frontend
|
||||
FRONTEND_THEME=default
|
||||
FRONTEND_HOMEPAGE_FEATURE_ENABLED=True
|
||||
FRONTEND_FOOTER_FEATURE_ENABLED=True
|
||||
FRONTEND_URL_JSON_FOOTER=http://frontend:3000/contents/footer-demo.json
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
"""API filters for Impress' core application."""
|
||||
|
||||
import unicodedata
|
||||
|
||||
from django.db.models import CharField, Func
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import django_filters
|
||||
@@ -10,64 +7,12 @@ import django_filters
|
||||
from core import models
|
||||
|
||||
|
||||
def remove_accents(value):
|
||||
"""Remove accents from a string (vélo -> velo)."""
|
||||
return "".join(
|
||||
c
|
||||
for c in unicodedata.normalize("NFD", value)
|
||||
if unicodedata.category(c) != "Mn"
|
||||
)
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class Unaccent(Func):
|
||||
"""
|
||||
PostgreSQL unaccent function wrapper for use in Django ORM queries.
|
||||
|
||||
This allows you to annotate a field using the unaccented version of a
|
||||
text column, enabling accent-insensitive filtering.
|
||||
"""
|
||||
|
||||
function = "unaccent"
|
||||
template = "unaccent(%(expressions)s::text)"
|
||||
output_field = CharField()
|
||||
|
||||
|
||||
class AccentInsensitiveCharFilter(django_filters.CharFilter):
|
||||
"""
|
||||
A custom CharFilter that performs case-insensitive and accent-insensitive filtering.
|
||||
|
||||
This filter uses PostgreSQL's extension `unaccent` function to remove diacritics (accents)
|
||||
from characters before applying the lookup expression (e.g., `icontains`).
|
||||
"""
|
||||
|
||||
def filter(self, qs, value):
|
||||
"""
|
||||
Apply the filter to the queryset using the unaccented version of the field.
|
||||
|
||||
Args:
|
||||
qs: The queryset to filter.
|
||||
value: The value to search for in the unaccented field.
|
||||
|
||||
Returns:
|
||||
A filtered queryset.
|
||||
"""
|
||||
if value:
|
||||
value = remove_accents(value)
|
||||
field_name = self.field_name
|
||||
annotated_field = f"unaccented_{field_name}"
|
||||
return qs.annotate(**{annotated_field: Unaccent(field_name)}).filter(
|
||||
**{f"{annotated_field}__{self.lookup_expr}": value}
|
||||
)
|
||||
return qs
|
||||
|
||||
|
||||
class DocumentFilter(django_filters.FilterSet):
|
||||
"""
|
||||
Custom filter for filtering documents on title (accent and case insensitive).
|
||||
Custom filter for filtering documents.
|
||||
"""
|
||||
|
||||
title = AccentInsensitiveCharFilter(
|
||||
title = django_filters.CharFilter(
|
||||
field_name="title", lookup_expr="icontains", label=_("Title")
|
||||
)
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ class BaseAccessSerializer(serializers.ModelSerializer):
|
||||
|
||||
if not self.Meta.model.objects.filter( # pylint: disable=no-member
|
||||
Q(user=user) | Q(team__in=user.teams),
|
||||
role__in=models.PRIVILEGED_ROLES,
|
||||
role__in=[models.RoleChoices.OWNER, models.RoleChoices.ADMIN],
|
||||
**{self.Meta.resource_field_name: resource_id}, # pylint: disable=no-member
|
||||
).exists():
|
||||
raise exceptions.PermissionDenied(
|
||||
@@ -124,10 +124,6 @@ class BaseAccessSerializer(serializers.ModelSerializer):
|
||||
class DocumentAccessSerializer(BaseAccessSerializer):
|
||||
"""Serialize document accesses."""
|
||||
|
||||
document_id = serializers.PrimaryKeyRelatedField(
|
||||
read_only=True,
|
||||
source="document",
|
||||
)
|
||||
user_id = serializers.PrimaryKeyRelatedField(
|
||||
queryset=models.User.objects.all(),
|
||||
write_only=True,
|
||||
@@ -140,11 +136,11 @@ class DocumentAccessSerializer(BaseAccessSerializer):
|
||||
class Meta:
|
||||
model = models.DocumentAccess
|
||||
resource_field_name = "document"
|
||||
fields = ["id", "document_id", "user", "user_id", "team", "role", "abilities"]
|
||||
read_only_fields = ["id", "document_id", "abilities"]
|
||||
fields = ["id", "user", "user_id", "team", "role", "abilities"]
|
||||
read_only_fields = ["id", "abilities"]
|
||||
|
||||
|
||||
class DocumentAccessLightSerializer(BaseAccessSerializer):
|
||||
class DocumentAccessLightSerializer(DocumentAccessSerializer):
|
||||
"""Serialize document accesses with limited fields."""
|
||||
|
||||
user = UserLightSerializer(read_only=True)
|
||||
|
||||
@@ -7,6 +7,7 @@ from urllib.parse import unquote, urlparse
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.contrib.postgres.search import TrigramSimilarity
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.storage import default_storage
|
||||
@@ -218,10 +219,14 @@ class UserViewSet(
|
||||
class ResourceAccessViewsetMixin:
|
||||
"""Mixin with methods common to all access viewsets."""
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
"""Override to filter on related resource."""
|
||||
queryset = super().filter_queryset(queryset)
|
||||
return queryset.filter(**{self.resource_field_name: self.kwargs["resource_id"]})
|
||||
def get_permissions(self):
|
||||
"""User only needs to be authenticated to list resource accesses"""
|
||||
if self.action == "list":
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
else:
|
||||
return super().get_permissions()
|
||||
|
||||
return [permission() for permission in permission_classes]
|
||||
|
||||
def get_serializer_context(self):
|
||||
"""Extra context provided to the serializer class."""
|
||||
@@ -229,6 +234,43 @@ class ResourceAccessViewsetMixin:
|
||||
context["resource_id"] = self.kwargs["resource_id"]
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
"""Return the queryset according to the action."""
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.filter(
|
||||
**{self.resource_field_name: self.kwargs["resource_id"]}
|
||||
)
|
||||
|
||||
if self.action == "list":
|
||||
user = self.request.user
|
||||
teams = user.teams
|
||||
user_roles_query = (
|
||||
queryset.filter(
|
||||
db.Q(user=user) | db.Q(team__in=teams),
|
||||
**{self.resource_field_name: self.kwargs["resource_id"]},
|
||||
)
|
||||
.values(self.resource_field_name)
|
||||
.annotate(roles_array=ArrayAgg("role"))
|
||||
.values("roles_array")
|
||||
)
|
||||
|
||||
# Limit to resource access instances related to a resource THAT also has
|
||||
# a resource access
|
||||
# instance for the logged-in user (we don't want to list only the resource
|
||||
# access instances pointing to the logged-in user)
|
||||
queryset = (
|
||||
queryset.filter(
|
||||
db.Q(**{f"{self.resource_field_name}__accesses__user": user})
|
||||
| db.Q(
|
||||
**{f"{self.resource_field_name}__accesses__team__in": teams}
|
||||
),
|
||||
**{self.resource_field_name: self.kwargs["resource_id"]},
|
||||
)
|
||||
.annotate(user_roles=db.Subquery(user_roles_query))
|
||||
.distinct()
|
||||
)
|
||||
return queryset
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
"""Forbid deleting the last owner access"""
|
||||
instance = self.get_object()
|
||||
@@ -399,6 +441,44 @@ class DocumentViewSet(
|
||||
trashbin_serializer_class = serializers.ListDocumentSerializer
|
||||
tree_serializer_class = serializers.ListDocumentSerializer
|
||||
|
||||
def annotate_is_favorite(self, queryset):
|
||||
"""
|
||||
Annotate document queryset with the favorite status for the current user.
|
||||
"""
|
||||
user = self.request.user
|
||||
|
||||
if user.is_authenticated:
|
||||
favorite_exists_subquery = models.DocumentFavorite.objects.filter(
|
||||
document_id=db.OuterRef("pk"), user=user
|
||||
)
|
||||
return queryset.annotate(is_favorite=db.Exists(favorite_exists_subquery))
|
||||
|
||||
return queryset.annotate(is_favorite=db.Value(False))
|
||||
|
||||
def annotate_user_roles(self, queryset):
|
||||
"""
|
||||
Annotate document queryset with the roles of the current user
|
||||
on the document or its ancestors.
|
||||
"""
|
||||
user = self.request.user
|
||||
output_field = ArrayField(base_field=db.CharField())
|
||||
|
||||
if user.is_authenticated:
|
||||
user_roles_subquery = models.DocumentAccess.objects.filter(
|
||||
db.Q(user=user) | db.Q(team__in=user.teams),
|
||||
document__path=Left(db.OuterRef("path"), Length("document__path")),
|
||||
).values_list("role", flat=True)
|
||||
|
||||
return queryset.annotate(
|
||||
user_roles=db.Func(
|
||||
user_roles_subquery, function="ARRAY", output_field=output_field
|
||||
)
|
||||
)
|
||||
|
||||
return queryset.annotate(
|
||||
user_roles=db.Value([], output_field=output_field),
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
"""Get queryset performing all annotation and filtering on the document tree structure."""
|
||||
user = self.request.user
|
||||
@@ -434,9 +514,8 @@ class DocumentViewSet(
|
||||
def filter_queryset(self, queryset):
|
||||
"""Override to apply annotations to generic views."""
|
||||
queryset = super().filter_queryset(queryset)
|
||||
user = self.request.user
|
||||
queryset = queryset.annotate_is_favorite(user)
|
||||
queryset = queryset.annotate_user_roles(user)
|
||||
queryset = self.annotate_is_favorite(queryset)
|
||||
queryset = self.annotate_user_roles(queryset)
|
||||
return queryset
|
||||
|
||||
def get_response_for_queryset(self, queryset):
|
||||
@@ -460,10 +539,9 @@ class DocumentViewSet(
|
||||
Additional annotations (e.g., `is_highest_ancestor_for_user`, favorite status) are
|
||||
applied before ordering and returning the response.
|
||||
"""
|
||||
user = self.request.user
|
||||
|
||||
# Not calling filter_queryset. We do our own cooking.
|
||||
queryset = self.get_queryset()
|
||||
queryset = (
|
||||
self.get_queryset()
|
||||
) # Not calling filter_queryset. We do our own cooking.
|
||||
|
||||
filterset = ListDocumentFilter(
|
||||
self.request.GET, queryset=queryset, request=self.request
|
||||
@@ -476,7 +554,7 @@ class DocumentViewSet(
|
||||
for field in ["is_creator_me", "title"]:
|
||||
queryset = filterset.filters[field].filter(queryset, filter_data[field])
|
||||
|
||||
queryset = queryset.annotate_user_roles(user)
|
||||
queryset = self.annotate_user_roles(queryset)
|
||||
|
||||
# Among the results, we may have documents that are ancestors/descendants
|
||||
# of each other. In this case we want to keep only the highest ancestors.
|
||||
@@ -493,7 +571,7 @@ class DocumentViewSet(
|
||||
)
|
||||
|
||||
# Annotate favorite status and filter if applicable as late as possible
|
||||
queryset = queryset.annotate_is_favorite(user)
|
||||
queryset = self.annotate_is_favorite(queryset)
|
||||
queryset = filterset.filters["is_favorite"].filter(
|
||||
queryset, filter_data["is_favorite"]
|
||||
)
|
||||
@@ -576,7 +654,7 @@ class DocumentViewSet(
|
||||
deleted_at__isnull=False,
|
||||
deleted_at__gte=models.get_trashbin_cutoff(),
|
||||
)
|
||||
queryset = queryset.annotate_user_roles(self.request.user)
|
||||
queryset = self.annotate_user_roles(queryset)
|
||||
queryset = queryset.filter(user_roles__contains=[models.RoleChoices.OWNER])
|
||||
|
||||
return self.get_response_for_queryset(queryset)
|
||||
@@ -756,8 +834,6 @@ class DocumentViewSet(
|
||||
List ancestors tree above the document.
|
||||
What we need to display is the tree structure opened for the current document.
|
||||
"""
|
||||
user = self.request.user
|
||||
|
||||
try:
|
||||
current_document = self.queryset.only("depth", "path").get(pk=pk)
|
||||
except models.Document.DoesNotExist as excpt:
|
||||
@@ -812,8 +888,8 @@ class DocumentViewSet(
|
||||
output_field=db.BooleanField(),
|
||||
)
|
||||
)
|
||||
queryset = queryset.annotate_user_roles(user)
|
||||
queryset = queryset.annotate_is_favorite(user)
|
||||
queryset = self.annotate_user_roles(queryset)
|
||||
queryset = self.annotate_is_favorite(queryset)
|
||||
|
||||
# Pass ancestors' links definitions to the serializer as a context variable
|
||||
# in order to allow saving time while computing abilities on the instance
|
||||
@@ -1297,11 +1373,7 @@ class DocumentViewSet(
|
||||
|
||||
class DocumentAccessViewSet(
|
||||
ResourceAccessViewsetMixin,
|
||||
drf.mixins.CreateModelMixin,
|
||||
drf.mixins.RetrieveModelMixin,
|
||||
drf.mixins.UpdateModelMixin,
|
||||
drf.mixins.DestroyModelMixin,
|
||||
viewsets.GenericViewSet,
|
||||
viewsets.ModelViewSet,
|
||||
):
|
||||
"""
|
||||
API ViewSet for all interactions with document accesses.
|
||||
@@ -1328,52 +1400,37 @@ class DocumentAccessViewSet(
|
||||
"""
|
||||
|
||||
lookup_field = "pk"
|
||||
pagination_class = Pagination
|
||||
permission_classes = [permissions.IsAuthenticated, permissions.AccessPermission]
|
||||
queryset = models.DocumentAccess.objects.select_related("user").all()
|
||||
resource_field_name = "document"
|
||||
serializer_class = serializers.DocumentAccessSerializer
|
||||
is_current_user_owner_or_admin = False
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""Return accesses for the current document with filters and annotations."""
|
||||
user = self.request.user
|
||||
def get_queryset(self):
|
||||
"""Return the queryset according to the action."""
|
||||
queryset = super().get_queryset()
|
||||
|
||||
try:
|
||||
document = models.Document.objects.get(pk=self.kwargs["resource_id"])
|
||||
except models.Document.DoesNotExist:
|
||||
return drf.response.Response([])
|
||||
if self.action == "list":
|
||||
try:
|
||||
document = models.Document.objects.get(pk=self.kwargs["resource_id"])
|
||||
except models.Document.DoesNotExist:
|
||||
return queryset.none()
|
||||
|
||||
roles = set(document.get_roles(user))
|
||||
if not roles:
|
||||
return drf.response.Response([])
|
||||
roles = set(document.get_roles(self.request.user))
|
||||
is_owner_or_admin = bool(roles.intersection(set(models.PRIVILEGED_ROLES)))
|
||||
self.is_current_user_owner_or_admin = is_owner_or_admin
|
||||
if not is_owner_or_admin:
|
||||
# Return only the document owner access
|
||||
queryset = queryset.filter(role__in=models.PRIVILEGED_ROLES)
|
||||
|
||||
ancestors = (
|
||||
(document.get_ancestors() | models.Document.objects.filter(pk=document.pk))
|
||||
.filter(ancestors_deleted_at__isnull=True)
|
||||
.order_by("path")
|
||||
)
|
||||
highest_readable = ancestors.readable_per_se(user).only("depth").first()
|
||||
return queryset
|
||||
|
||||
if highest_readable is None:
|
||||
return drf.response.Response([])
|
||||
def get_serializer_class(self):
|
||||
if self.action == "list" and not self.is_current_user_owner_or_admin:
|
||||
return serializers.DocumentAccessLightSerializer
|
||||
|
||||
queryset = self.get_queryset()
|
||||
queryset = queryset.filter(
|
||||
document__in=ancestors.filter(depth__gte=highest_readable.depth)
|
||||
)
|
||||
|
||||
is_privileged = bool(roles.intersection(set(models.PRIVILEGED_ROLES)))
|
||||
if is_privileged:
|
||||
serializer_class = serializers.DocumentAccessSerializer
|
||||
else:
|
||||
# Return only the document's privileged accesses
|
||||
queryset = queryset.filter(role__in=models.PRIVILEGED_ROLES)
|
||||
serializer_class = serializers.DocumentAccessLightSerializer
|
||||
|
||||
queryset = queryset.distinct()
|
||||
serializer = serializer_class(
|
||||
queryset, many=True, context=self.get_serializer_context()
|
||||
)
|
||||
return drf.response.Response(serializer.data)
|
||||
return super().get_serializer_class()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""Add a new access to the document and send an email to the new added user."""
|
||||
@@ -1485,6 +1542,7 @@ class TemplateAccessViewSet(
|
||||
ResourceAccessViewsetMixin,
|
||||
drf.mixins.CreateModelMixin,
|
||||
drf.mixins.DestroyModelMixin,
|
||||
drf.mixins.ListModelMixin,
|
||||
drf.mixins.RetrieveModelMixin,
|
||||
drf.mixins.UpdateModelMixin,
|
||||
viewsets.GenericViewSet,
|
||||
@@ -1514,28 +1572,12 @@ class TemplateAccessViewSet(
|
||||
"""
|
||||
|
||||
lookup_field = "pk"
|
||||
pagination_class = Pagination
|
||||
permission_classes = [permissions.IsAuthenticated, permissions.AccessPermission]
|
||||
queryset = models.TemplateAccess.objects.select_related("user").all()
|
||||
resource_field_name = "template"
|
||||
serializer_class = serializers.TemplateAccessSerializer
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""Restrict templates returned by the list endpoint"""
|
||||
user = self.request.user
|
||||
teams = user.teams
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
# Limit to resource access instances related to a resource THAT also has
|
||||
# a resource access instance for the logged-in user (we don't want to list
|
||||
# only the resource access instances pointing to the logged-in user)
|
||||
queryset = queryset.filter(
|
||||
db.Q(template__accesses__user=user)
|
||||
| db.Q(template__accesses__team__in=teams),
|
||||
).distinct()
|
||||
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return drf.response.Response(serializer.data)
|
||||
|
||||
|
||||
class InvitationViewset(
|
||||
drf.mixins.CreateModelMixin,
|
||||
@@ -1650,6 +1692,7 @@ class ConfigView(drf.views.APIView):
|
||||
"CRISP_WEBSITE_ID",
|
||||
"ENVIRONMENT",
|
||||
"FRONTEND_CSS_URL",
|
||||
"FRONTEND_HOMEPAGE_FEATURE_ENABLED",
|
||||
"FRONTEND_FOOTER_FEATURE_ENABLED",
|
||||
"FRONTEND_THEME",
|
||||
"MEDIA_BASE_URL",
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("core", "0020_remove_is_public_add_field_attachments_and_duplicated_from"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunSQL(
|
||||
"CREATE EXTENSION IF NOT EXISTS unaccent;",
|
||||
reverse_sql="DROP EXTENSION IF EXISTS unaccent;",
|
||||
),
|
||||
]
|
||||
@@ -87,61 +87,49 @@ class LinkReachChoices(models.TextChoices):
|
||||
"""
|
||||
Determines the valid select options for link reach and link role depending on the
|
||||
list of ancestors' link reach/role.
|
||||
|
||||
Args:
|
||||
ancestors_links: List of dictionaries, each with 'link_reach' and 'link_role' keys
|
||||
representing the reach and role of ancestors links.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping possible reach levels to their corresponding possible roles.
|
||||
"""
|
||||
# If no ancestors, return all options
|
||||
if not ancestors_links:
|
||||
return {
|
||||
reach: LinkRoleChoices.values if reach != cls.RESTRICTED else None
|
||||
for reach in cls.values
|
||||
}
|
||||
return dict.fromkeys(cls.values, LinkRoleChoices.values)
|
||||
|
||||
# Initialize result with all possible reaches and role options as sets
|
||||
result = {
|
||||
reach: set(LinkRoleChoices.values) if reach != cls.RESTRICTED else None
|
||||
for reach in cls.values
|
||||
}
|
||||
result = {reach: set(LinkRoleChoices.values) for reach in cls.values}
|
||||
|
||||
# Group roles by reach level
|
||||
reach_roles = defaultdict(set)
|
||||
for link in ancestors_links:
|
||||
reach_roles[link["link_reach"]].add(link["link_role"])
|
||||
|
||||
# Rule 1: public/editor → override everything
|
||||
if LinkRoleChoices.EDITOR in reach_roles.get(cls.PUBLIC, set()):
|
||||
return {cls.PUBLIC: [LinkRoleChoices.EDITOR]}
|
||||
# Apply constraints based on ancestor links
|
||||
if LinkRoleChoices.EDITOR in reach_roles[cls.RESTRICTED]:
|
||||
result[cls.RESTRICTED].discard(LinkRoleChoices.READER)
|
||||
|
||||
# Rule 2: authenticated/editor
|
||||
if LinkRoleChoices.EDITOR in reach_roles.get(cls.AUTHENTICATED, set()):
|
||||
if LinkRoleChoices.EDITOR in reach_roles[cls.AUTHENTICATED]:
|
||||
result[cls.AUTHENTICATED].discard(LinkRoleChoices.READER)
|
||||
result.pop(cls.RESTRICTED, None)
|
||||
elif LinkRoleChoices.READER in reach_roles[cls.AUTHENTICATED]:
|
||||
result[cls.RESTRICTED].discard(LinkRoleChoices.READER)
|
||||
|
||||
# Rule 3: public/reader
|
||||
if LinkRoleChoices.READER in reach_roles.get(cls.PUBLIC, set()):
|
||||
if LinkRoleChoices.EDITOR in reach_roles[cls.PUBLIC]:
|
||||
result[cls.PUBLIC].discard(LinkRoleChoices.READER)
|
||||
result.pop(cls.AUTHENTICATED, None)
|
||||
result.pop(cls.RESTRICTED, None)
|
||||
elif LinkRoleChoices.READER in reach_roles[cls.PUBLIC]:
|
||||
result[cls.AUTHENTICATED].discard(LinkRoleChoices.READER)
|
||||
result.get(cls.RESTRICTED, set()).discard(LinkRoleChoices.READER)
|
||||
|
||||
# Rule 4: authenticated/reader
|
||||
if LinkRoleChoices.READER in reach_roles.get(cls.AUTHENTICATED, set()):
|
||||
result.pop(cls.RESTRICTED, None)
|
||||
# Convert roles sets to lists while maintaining the order from LinkRoleChoices
|
||||
for reach, roles in result.items():
|
||||
result[reach] = [role for role in LinkRoleChoices.values if role in roles]
|
||||
|
||||
# Clean up: remove empty entries and convert sets to ordered lists
|
||||
cleaned = {}
|
||||
for reach in cls.values:
|
||||
if reach in result:
|
||||
if result[reach]:
|
||||
cleaned[reach] = [
|
||||
r for r in LinkRoleChoices.values if r in result[reach]
|
||||
]
|
||||
else:
|
||||
# Could be [] or None (for RESTRICTED reach)
|
||||
cleaned[reach] = result[reach]
|
||||
|
||||
return cleaned
|
||||
return result
|
||||
|
||||
|
||||
class DuplicateEmailError(Exception):
|
||||
@@ -464,41 +452,6 @@ class DocumentQuerySet(MP_NodeQuerySet):
|
||||
|
||||
return self.filter(link_reach=LinkReachChoices.PUBLIC)
|
||||
|
||||
def annotate_is_favorite(self, user):
|
||||
"""
|
||||
Annotate document queryset with the favorite status for the current user.
|
||||
"""
|
||||
if user.is_authenticated:
|
||||
favorite_exists_subquery = DocumentFavorite.objects.filter(
|
||||
document_id=models.OuterRef("pk"), user=user
|
||||
)
|
||||
return self.annotate(is_favorite=models.Exists(favorite_exists_subquery))
|
||||
|
||||
return self.annotate(is_favorite=models.Value(False))
|
||||
|
||||
def annotate_user_roles(self, user):
|
||||
"""
|
||||
Annotate document queryset with the roles of the current user
|
||||
on the document or its ancestors.
|
||||
"""
|
||||
output_field = ArrayField(base_field=models.CharField())
|
||||
|
||||
if user.is_authenticated:
|
||||
user_roles_subquery = DocumentAccess.objects.filter(
|
||||
models.Q(user=user) | models.Q(team__in=user.teams),
|
||||
document__path=Left(models.OuterRef("path"), Length("document__path")),
|
||||
).values_list("role", flat=True)
|
||||
|
||||
return self.annotate(
|
||||
user_roles=models.Func(
|
||||
user_roles_subquery, function="ARRAY", output_field=output_field
|
||||
)
|
||||
)
|
||||
|
||||
return self.annotate(
|
||||
user_roles=models.Value([], output_field=output_field),
|
||||
)
|
||||
|
||||
|
||||
class DocumentManager(MP_NodeManager.from_queryset(DocumentQuerySet)):
|
||||
"""
|
||||
@@ -784,16 +737,17 @@ class Document(MP_Node, BaseModel):
|
||||
roles = []
|
||||
return roles
|
||||
|
||||
def get_ancestors_links_definitions(self, ancestors_links):
|
||||
"""Get links reach/role definitions for ancestors of the current document."""
|
||||
def get_links_definitions(self, ancestors_links):
|
||||
"""Get links reach/role definitions for the current document and its ancestors."""
|
||||
|
||||
ancestors_links_definitions = defaultdict(set)
|
||||
links_definitions = defaultdict(set)
|
||||
links_definitions[self.link_reach].add(self.link_role)
|
||||
|
||||
# Merge ancestor link definitions
|
||||
for ancestor in ancestors_links:
|
||||
ancestors_links_definitions[ancestor["link_reach"]].add(
|
||||
ancestor["link_role"]
|
||||
)
|
||||
links_definitions[ancestor["link_reach"]].add(ancestor["link_role"])
|
||||
|
||||
return ancestors_links_definitions
|
||||
return dict(links_definitions) # Convert defaultdict back to a normal dict
|
||||
|
||||
def compute_ancestors_links(self, user):
|
||||
"""
|
||||
@@ -849,20 +803,10 @@ class Document(MP_Node, BaseModel):
|
||||
) and not is_deleted
|
||||
|
||||
# Add roles provided by the document link, taking into account its ancestors
|
||||
ancestors_links_definitions = self.get_ancestors_links_definitions(
|
||||
ancestors_links
|
||||
)
|
||||
|
||||
public_roles = ancestors_links_definitions.get(
|
||||
LinkReachChoices.PUBLIC, set()
|
||||
) | ({self.link_role} if self.link_reach == LinkReachChoices.PUBLIC else set())
|
||||
links_definitions = self.get_links_definitions(ancestors_links)
|
||||
public_roles = links_definitions.get(LinkReachChoices.PUBLIC, set())
|
||||
authenticated_roles = (
|
||||
ancestors_links_definitions.get(LinkReachChoices.AUTHENTICATED, set())
|
||||
| (
|
||||
{self.link_role}
|
||||
if self.link_reach == LinkReachChoices.AUTHENTICATED
|
||||
else set()
|
||||
)
|
||||
links_definitions.get(LinkReachChoices.AUTHENTICATED, set())
|
||||
if user.is_authenticated
|
||||
else set()
|
||||
)
|
||||
@@ -906,9 +850,6 @@ class Document(MP_Node, BaseModel):
|
||||
"restore": is_owner,
|
||||
"retrieve": can_get,
|
||||
"media_auth": can_get,
|
||||
"ancestors_links_definitions": {
|
||||
k: list(v) for k, v in ancestors_links_definitions.items()
|
||||
},
|
||||
"link_select_options": LinkReachChoices.get_select_options(ancestors_links),
|
||||
"tree": can_get,
|
||||
"update": can_update,
|
||||
|
||||
@@ -51,7 +51,12 @@ def test_api_document_accesses_list_authenticated_unrelated():
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
assert response.json() == {
|
||||
"count": 0,
|
||||
"next": None,
|
||||
"previous": None,
|
||||
"results": [],
|
||||
}
|
||||
|
||||
|
||||
def test_api_document_accesses_list_unexisting_document():
|
||||
@@ -65,7 +70,12 @@ def test_api_document_accesses_list_unexisting_document():
|
||||
|
||||
response = client.get(f"/api/v1.0/documents/{uuid4()!s}/accesses/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
assert response.json() == {
|
||||
"count": 0,
|
||||
"next": None,
|
||||
"previous": None,
|
||||
"results": [],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
@@ -76,30 +86,22 @@ def test_api_document_accesses_list_authenticated_related_non_privileged(
|
||||
via, role, mock_user_teams
|
||||
):
|
||||
"""
|
||||
Authenticated users with no privileged role should only be able to list document
|
||||
accesses associated with privileged roles for a document, including from ancestors.
|
||||
Authenticated users should be able to list document accesses for a document
|
||||
to which they are directly related, whatever their role in the document.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Create documents structured as a tree
|
||||
unreadable_ancestor = factories.DocumentFactory(link_reach="restricted")
|
||||
# make all documents below the grand parent readable without a specific access for the user
|
||||
grand_parent = factories.DocumentFactory(
|
||||
parent=unreadable_ancestor, link_reach="authenticated"
|
||||
owner = factories.UserFactory()
|
||||
accesses = []
|
||||
|
||||
document_access = factories.UserDocumentAccessFactory(
|
||||
user=owner, role=models.RoleChoices.OWNER
|
||||
)
|
||||
parent = factories.DocumentFactory(parent=grand_parent)
|
||||
document = factories.DocumentFactory(parent=parent)
|
||||
child = factories.DocumentFactory(parent=document)
|
||||
|
||||
# Create accesses related to each document
|
||||
factories.UserDocumentAccessFactory(document=unreadable_ancestor)
|
||||
grand_parent_access = factories.UserDocumentAccessFactory(document=grand_parent)
|
||||
parent_access = factories.UserDocumentAccessFactory(document=parent)
|
||||
document_access = factories.UserDocumentAccessFactory(document=document)
|
||||
factories.UserDocumentAccessFactory(document=child)
|
||||
|
||||
accesses.append(document_access)
|
||||
document = document_access.document
|
||||
if via == USER:
|
||||
models.DocumentAccess.objects.create(
|
||||
document=document,
|
||||
@@ -116,6 +118,8 @@ def test_api_document_accesses_list_authenticated_related_non_privileged(
|
||||
|
||||
access1 = factories.TeamDocumentAccessFactory(document=document)
|
||||
access2 = factories.UserDocumentAccessFactory(document=document)
|
||||
accesses.append(access1)
|
||||
accesses.append(access2)
|
||||
|
||||
# Accesses for other documents to which the user is related should not be listed either
|
||||
other_access = factories.UserDocumentAccessFactory(user=user)
|
||||
@@ -125,17 +129,14 @@ def test_api_document_accesses_list_authenticated_related_non_privileged(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||
)
|
||||
|
||||
# Return only owners
|
||||
owners_accesses = [
|
||||
access for access in accesses if access.role in models.PRIVILEGED_ROLES
|
||||
]
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
|
||||
# Make sure only privileged roles are returned
|
||||
accesses = [grand_parent_access, parent_access, document_access, access1, access2]
|
||||
privileged_accesses = [
|
||||
acc for acc in accesses if acc.role in models.PRIVILEGED_ROLES
|
||||
]
|
||||
assert len(content) == len(privileged_accesses)
|
||||
|
||||
assert sorted(content, key=lambda x: x["id"]) == sorted(
|
||||
assert content["count"] == len(owners_accesses)
|
||||
assert sorted(content["results"], key=lambda x: x["id"]) == sorted(
|
||||
[
|
||||
{
|
||||
"id": str(access.id),
|
||||
@@ -151,44 +152,38 @@ def test_api_document_accesses_list_authenticated_related_non_privileged(
|
||||
"role": access.role,
|
||||
"abilities": access.get_abilities(user),
|
||||
}
|
||||
for access in privileged_accesses
|
||||
for access in owners_accesses
|
||||
],
|
||||
key=lambda x: x["id"],
|
||||
)
|
||||
|
||||
for access in content["results"]:
|
||||
assert access["role"] in models.PRIVILEGED_ROLES
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
@pytest.mark.parametrize(
|
||||
"role", [role for role in models.RoleChoices if role in models.PRIVILEGED_ROLES]
|
||||
)
|
||||
def test_api_document_accesses_list_authenticated_related_privileged(
|
||||
@pytest.mark.parametrize("role", models.PRIVILEGED_ROLES)
|
||||
def test_api_document_accesses_list_authenticated_related_privileged_roles(
|
||||
via, role, mock_user_teams
|
||||
):
|
||||
"""
|
||||
Authenticated users with a privileged role should be able to list all
|
||||
document accesses whatever the role, including from ancestors.
|
||||
Authenticated users should be able to list document accesses for a document
|
||||
to which they are directly related, whatever their role in the document.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Create documents structured as a tree
|
||||
unreadable_ancestor = factories.DocumentFactory(link_reach="restricted")
|
||||
# make all documents below the grand parent readable without a specific access for the user
|
||||
grand_parent = factories.DocumentFactory(
|
||||
parent=unreadable_ancestor, link_reach="authenticated"
|
||||
owner = factories.UserFactory()
|
||||
accesses = []
|
||||
|
||||
document_access = factories.UserDocumentAccessFactory(
|
||||
user=owner, role=models.RoleChoices.OWNER
|
||||
)
|
||||
parent = factories.DocumentFactory(parent=grand_parent)
|
||||
document = factories.DocumentFactory(parent=parent)
|
||||
child = factories.DocumentFactory(parent=document)
|
||||
|
||||
# Create accesses related to each document
|
||||
factories.UserDocumentAccessFactory(document=unreadable_ancestor)
|
||||
grand_parent_access = factories.UserDocumentAccessFactory(document=grand_parent)
|
||||
parent_access = factories.UserDocumentAccessFactory(document=parent)
|
||||
document_access = factories.UserDocumentAccessFactory(document=document)
|
||||
factories.UserDocumentAccessFactory(document=child)
|
||||
|
||||
accesses.append(document_access)
|
||||
document = document_access.document
|
||||
user_access = None
|
||||
if via == USER:
|
||||
user_access = models.DocumentAccess.objects.create(
|
||||
document=document,
|
||||
@@ -202,11 +197,11 @@ def test_api_document_accesses_list_authenticated_related_privileged(
|
||||
team="lasuite",
|
||||
role=role,
|
||||
)
|
||||
else:
|
||||
raise RuntimeError()
|
||||
|
||||
access1 = factories.TeamDocumentAccessFactory(document=document)
|
||||
access2 = factories.UserDocumentAccessFactory(document=document)
|
||||
accesses.append(access1)
|
||||
accesses.append(access2)
|
||||
|
||||
# Accesses for other documents to which the user is related should not be listed either
|
||||
other_access = factories.UserDocumentAccessFactory(user=user)
|
||||
@@ -216,39 +211,42 @@ def test_api_document_accesses_list_authenticated_related_privileged(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||
)
|
||||
|
||||
access2_user = serializers.UserSerializer(instance=access2.user).data
|
||||
base_user = serializers.UserSerializer(instance=user).data
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
|
||||
# Make sure all expected accesses are returned
|
||||
accesses = [
|
||||
user_access,
|
||||
grand_parent_access,
|
||||
parent_access,
|
||||
document_access,
|
||||
access1,
|
||||
access2,
|
||||
]
|
||||
assert len(content) == 6
|
||||
|
||||
assert sorted(content, key=lambda x: x["id"]) == sorted(
|
||||
assert len(content["results"]) == 4
|
||||
assert sorted(content["results"], key=lambda x: x["id"]) == sorted(
|
||||
[
|
||||
{
|
||||
"id": str(access.id),
|
||||
"document_id": str(access.document_id),
|
||||
"user": {
|
||||
"id": str(access.user.id),
|
||||
"email": access.user.email,
|
||||
"language": access.user.language,
|
||||
"full_name": access.user.full_name,
|
||||
"short_name": access.user.short_name,
|
||||
}
|
||||
if access.user
|
||||
else None,
|
||||
"team": access.team,
|
||||
"role": access.role,
|
||||
"abilities": access.get_abilities(user),
|
||||
}
|
||||
for access in accesses
|
||||
"id": str(user_access.id),
|
||||
"user": base_user if via == "user" else None,
|
||||
"team": "lasuite" if via == "team" else "",
|
||||
"role": user_access.role,
|
||||
"abilities": user_access.get_abilities(user),
|
||||
},
|
||||
{
|
||||
"id": str(access1.id),
|
||||
"user": None,
|
||||
"team": access1.team,
|
||||
"role": access1.role,
|
||||
"abilities": access1.get_abilities(user),
|
||||
},
|
||||
{
|
||||
"id": str(access2.id),
|
||||
"user": access2_user,
|
||||
"team": "",
|
||||
"role": access2.role,
|
||||
"abilities": access2.get_abilities(user),
|
||||
},
|
||||
{
|
||||
"id": str(document_access.id),
|
||||
"user": serializers.UserSerializer(instance=owner).data,
|
||||
"team": "",
|
||||
"role": models.RoleChoices.OWNER,
|
||||
"abilities": document_access.get_abilities(user),
|
||||
},
|
||||
],
|
||||
key=lambda x: x["id"],
|
||||
)
|
||||
@@ -343,7 +341,6 @@ def test_api_document_accesses_retrieve_authenticated_related(
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"id": str(access.id),
|
||||
"document_id": str(access.document_id),
|
||||
"user": access_user,
|
||||
"team": "",
|
||||
"role": access.role,
|
||||
|
||||
@@ -165,7 +165,6 @@ def test_api_document_accesses_create_authenticated_administrator(via, mock_user
|
||||
other_user = serializers.UserSerializer(instance=other_user).data
|
||||
assert response.json() == {
|
||||
"abilities": new_document_access.get_abilities(user),
|
||||
"document_id": str(new_document_access.document_id),
|
||||
"id": str(new_document_access.id),
|
||||
"team": "",
|
||||
"role": role,
|
||||
@@ -223,7 +222,6 @@ def test_api_document_accesses_create_authenticated_owner(via, mock_user_teams):
|
||||
new_document_access = models.DocumentAccess.objects.filter(user=other_user).get()
|
||||
other_user = serializers.UserSerializer(instance=other_user).data
|
||||
assert response.json() == {
|
||||
"document_id": str(new_document_access.document_id),
|
||||
"id": str(new_document_access.id),
|
||||
"user": other_user,
|
||||
"team": "",
|
||||
@@ -288,7 +286,6 @@ def test_api_document_accesses_create_email_in_receivers_language(via, mock_user
|
||||
).get()
|
||||
other_user_data = serializers.UserSerializer(instance=other_user).data
|
||||
assert response.json() == {
|
||||
"document_id": str(new_document_access.document_id),
|
||||
"id": str(new_document_access.id),
|
||||
"user": other_user_data,
|
||||
"team": "",
|
||||
|
||||
@@ -7,7 +7,6 @@ from faker import Faker
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories
|
||||
from core.api.filters import remove_accents
|
||||
|
||||
fake = Faker()
|
||||
pytestmark = pytest.mark.django_db
|
||||
@@ -50,16 +49,14 @@ def test_api_documents_descendants_filter_unknown_field():
|
||||
[
|
||||
("Project Alpha", 1), # Exact match
|
||||
("project", 2), # Partial match (case-insensitive)
|
||||
("Guide", 2), # Word match within a title
|
||||
("Guide", 1), # Word match within a title
|
||||
("Special", 0), # No match (nonexistent keyword)
|
||||
("2024", 2), # Match by numeric keyword
|
||||
("", 6), # Empty string
|
||||
("velo", 1), # Accent-insensitive match (velo vs vélo)
|
||||
("bêta", 1), # Accent-insensitive match (bêta vs beta)
|
||||
("", 5), # Empty string
|
||||
],
|
||||
)
|
||||
def test_api_documents_descendants_filter_title(query, nb_results):
|
||||
"""Authenticated users should be able to search documents by their unaccented title."""
|
||||
"""Authenticated users should be able to search documents by their title."""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
@@ -73,7 +70,6 @@ def test_api_documents_descendants_filter_title(query, nb_results):
|
||||
"User Guide",
|
||||
"Financial Report 2024",
|
||||
"Annual Review 2024",
|
||||
"Guide du vélo urbain", # <-- Title with accent for accent-insensitive test
|
||||
]
|
||||
for title in titles:
|
||||
factories.DocumentFactory(title=title, parent=document)
|
||||
@@ -89,7 +85,4 @@ def test_api_documents_descendants_filter_title(query, nb_results):
|
||||
|
||||
# Ensure all results contain the query in their title
|
||||
for result in results:
|
||||
assert (
|
||||
remove_accents(query).lower().strip()
|
||||
in remove_accents(result["title"]).lower()
|
||||
)
|
||||
assert query.lower().strip() in result["title"].lower()
|
||||
|
||||
@@ -42,11 +42,10 @@ def test_api_documents_retrieve_anonymous_public_standalone():
|
||||
"favorite": False,
|
||||
"invite_owner": False,
|
||||
"link_configuration": False,
|
||||
"ancestors_links_definitions": {},
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
"restricted": ["reader", "editor"],
|
||||
},
|
||||
"media_auth": True,
|
||||
"move": False,
|
||||
@@ -98,10 +97,6 @@ def test_api_documents_retrieve_anonymous_public_parent():
|
||||
"accesses_view": False,
|
||||
"ai_transform": False,
|
||||
"ai_translate": False,
|
||||
"ancestors_links_definitions": {
|
||||
"public": [grand_parent.link_role],
|
||||
parent.link_reach: [parent.link_role],
|
||||
},
|
||||
"attachment_upload": grand_parent.link_role == "editor",
|
||||
"children_create": False,
|
||||
"children_list": True,
|
||||
@@ -198,7 +193,6 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
|
||||
"accesses_view": False,
|
||||
"ai_transform": document.link_role == "editor",
|
||||
"ai_translate": document.link_role == "editor",
|
||||
"ancestors_links_definitions": {},
|
||||
"attachment_upload": document.link_role == "editor",
|
||||
"children_create": document.link_role == "editor",
|
||||
"children_list": True,
|
||||
@@ -213,7 +207,7 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
"restricted": ["reader", "editor"],
|
||||
},
|
||||
"media_auth": True,
|
||||
"move": False,
|
||||
@@ -273,10 +267,6 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
|
||||
"accesses_view": False,
|
||||
"ai_transform": grand_parent.link_role == "editor",
|
||||
"ai_translate": grand_parent.link_role == "editor",
|
||||
"ancestors_links_definitions": {
|
||||
grand_parent.link_reach: [grand_parent.link_role],
|
||||
"restricted": [parent.link_role],
|
||||
},
|
||||
"attachment_upload": grand_parent.link_role == "editor",
|
||||
"children_create": grand_parent.link_role == "editor",
|
||||
"children_list": True,
|
||||
@@ -450,7 +440,6 @@ def test_api_documents_retrieve_authenticated_related_parent():
|
||||
)
|
||||
assert response.status_code == 200
|
||||
links = document.get_ancestors().values("link_reach", "link_role")
|
||||
ancestors_roles = list({grand_parent.link_role, parent.link_role})
|
||||
assert response.json() == {
|
||||
"id": str(document.id),
|
||||
"abilities": {
|
||||
@@ -458,7 +447,6 @@ def test_api_documents_retrieve_authenticated_related_parent():
|
||||
"accesses_view": True,
|
||||
"ai_transform": access.role != "reader",
|
||||
"ai_translate": access.role != "reader",
|
||||
"ancestors_links_definitions": {"restricted": ancestors_roles},
|
||||
"attachment_upload": access.role != "reader",
|
||||
"children_create": access.role != "reader",
|
||||
"children_list": True,
|
||||
|
||||
@@ -74,7 +74,6 @@ def test_api_documents_trashbin_format():
|
||||
"accesses_view": True,
|
||||
"ai_transform": True,
|
||||
"ai_translate": True,
|
||||
"ancestors_links_definitions": {},
|
||||
"attachment_upload": True,
|
||||
"children_create": True,
|
||||
"children_list": True,
|
||||
@@ -89,7 +88,7 @@ def test_api_documents_trashbin_format():
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
"restricted": ["reader", "editor"],
|
||||
},
|
||||
"media_auth": True,
|
||||
"move": False, # Can't move a deleted document
|
||||
|
||||
@@ -48,7 +48,12 @@ def test_api_template_accesses_list_authenticated_unrelated():
|
||||
f"/api/v1.0/templates/{template.id!s}/accesses/",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
assert response.json() == {
|
||||
"count": 0,
|
||||
"next": None,
|
||||
"previous": None,
|
||||
"results": [],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
@@ -91,8 +96,8 @@ def test_api_template_accesses_list_authenticated_related(via, mock_user_teams):
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
assert len(content) == 3
|
||||
assert sorted(content, key=lambda x: x["id"]) == sorted(
|
||||
assert len(content["results"]) == 3
|
||||
assert sorted(content["results"], key=lambda x: x["id"]) == sorted(
|
||||
[
|
||||
{
|
||||
"id": str(user_access.id),
|
||||
|
||||
@@ -19,6 +19,7 @@ pytestmark = pytest.mark.django_db
|
||||
COLLABORATION_WS_URL="http://testcollab/",
|
||||
CRISP_WEBSITE_ID="123",
|
||||
FRONTEND_CSS_URL="http://testcss/",
|
||||
FRONTEND_HOMEPAGE_FEATURE_ENABLED=True,
|
||||
FRONTEND_FOOTER_FEATURE_ENABLED=True,
|
||||
FRONTEND_THEME="test-theme",
|
||||
MEDIA_BASE_URL="http://testserver/",
|
||||
@@ -41,6 +42,7 @@ def test_api_config(is_authenticated):
|
||||
"CRISP_WEBSITE_ID": "123",
|
||||
"ENVIRONMENT": "test",
|
||||
"FRONTEND_CSS_URL": "http://testcss/",
|
||||
"FRONTEND_HOMEPAGE_FEATURE_ENABLED": True,
|
||||
"FRONTEND_FOOTER_FEATURE_ENABLED": True,
|
||||
"FRONTEND_THEME": "test-theme",
|
||||
"LANGUAGES": [
|
||||
|
||||
@@ -154,7 +154,6 @@ def test_models_documents_get_abilities_forbidden(
|
||||
"accesses_view": False,
|
||||
"ai_transform": False,
|
||||
"ai_translate": False,
|
||||
"ancestors_links_definitions": {},
|
||||
"attachment_upload": False,
|
||||
"children_create": False,
|
||||
"children_list": False,
|
||||
@@ -171,7 +170,7 @@ def test_models_documents_get_abilities_forbidden(
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
"restricted": ["reader", "editor"],
|
||||
},
|
||||
"partial_update": False,
|
||||
"restore": False,
|
||||
@@ -215,7 +214,6 @@ def test_models_documents_get_abilities_reader(
|
||||
"accesses_view": False,
|
||||
"ai_transform": False,
|
||||
"ai_translate": False,
|
||||
"ancestors_links_definitions": {},
|
||||
"attachment_upload": False,
|
||||
"children_create": False,
|
||||
"children_list": True,
|
||||
@@ -230,7 +228,7 @@ def test_models_documents_get_abilities_reader(
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
"restricted": ["reader", "editor"],
|
||||
},
|
||||
"media_auth": True,
|
||||
"move": False,
|
||||
@@ -252,7 +250,7 @@ def test_models_documents_get_abilities_reader(
|
||||
assert all(
|
||||
value is False
|
||||
for key, value in document.get_abilities(user).items()
|
||||
if key not in ["link_select_options", "ancestors_links_definitions"]
|
||||
if key != "link_select_options"
|
||||
)
|
||||
|
||||
|
||||
@@ -278,7 +276,6 @@ def test_models_documents_get_abilities_editor(
|
||||
"accesses_view": False,
|
||||
"ai_transform": is_authenticated,
|
||||
"ai_translate": is_authenticated,
|
||||
"ancestors_links_definitions": {},
|
||||
"attachment_upload": True,
|
||||
"children_create": is_authenticated,
|
||||
"children_list": True,
|
||||
@@ -293,7 +290,7 @@ def test_models_documents_get_abilities_editor(
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
"restricted": ["reader", "editor"],
|
||||
},
|
||||
"media_auth": True,
|
||||
"move": False,
|
||||
@@ -314,7 +311,7 @@ def test_models_documents_get_abilities_editor(
|
||||
assert all(
|
||||
value is False
|
||||
for key, value in document.get_abilities(user).items()
|
||||
if key not in ["link_select_options", "ancestors_links_definitions"]
|
||||
if key != "link_select_options"
|
||||
)
|
||||
|
||||
|
||||
@@ -330,7 +327,6 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
|
||||
"accesses_view": True,
|
||||
"ai_transform": True,
|
||||
"ai_translate": True,
|
||||
"ancestors_links_definitions": {},
|
||||
"attachment_upload": True,
|
||||
"children_create": True,
|
||||
"children_list": True,
|
||||
@@ -345,7 +341,7 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
"restricted": ["reader", "editor"],
|
||||
},
|
||||
"media_auth": True,
|
||||
"move": True,
|
||||
@@ -379,7 +375,6 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries)
|
||||
"accesses_view": True,
|
||||
"ai_transform": True,
|
||||
"ai_translate": True,
|
||||
"ancestors_links_definitions": {},
|
||||
"attachment_upload": True,
|
||||
"children_create": True,
|
||||
"children_list": True,
|
||||
@@ -394,7 +389,7 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries)
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
"restricted": ["reader", "editor"],
|
||||
},
|
||||
"media_auth": True,
|
||||
"move": True,
|
||||
@@ -415,7 +410,7 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries)
|
||||
assert all(
|
||||
value is False
|
||||
for key, value in document.get_abilities(user).items()
|
||||
if key not in ["link_select_options", "ancestors_links_definitions"]
|
||||
if key != "link_select_options"
|
||||
)
|
||||
|
||||
|
||||
@@ -431,7 +426,6 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
|
||||
"accesses_view": True,
|
||||
"ai_transform": True,
|
||||
"ai_translate": True,
|
||||
"ancestors_links_definitions": {},
|
||||
"attachment_upload": True,
|
||||
"children_create": True,
|
||||
"children_list": True,
|
||||
@@ -446,7 +440,7 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
"restricted": ["reader", "editor"],
|
||||
},
|
||||
"media_auth": True,
|
||||
"move": False,
|
||||
@@ -467,7 +461,7 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
|
||||
assert all(
|
||||
value is False
|
||||
for key, value in document.get_abilities(user).items()
|
||||
if key not in ["link_select_options", "ancestors_links_definitions"]
|
||||
if key != "link_select_options"
|
||||
)
|
||||
|
||||
|
||||
@@ -490,7 +484,6 @@ def test_models_documents_get_abilities_reader_user(
|
||||
# You should not access AI if it's restricted to users with specific access
|
||||
"ai_transform": access_from_link and ai_access_setting != "restricted",
|
||||
"ai_translate": access_from_link and ai_access_setting != "restricted",
|
||||
"ancestors_links_definitions": {},
|
||||
"attachment_upload": access_from_link,
|
||||
"children_create": access_from_link,
|
||||
"children_list": True,
|
||||
@@ -505,7 +498,7 @@ def test_models_documents_get_abilities_reader_user(
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
"restricted": ["reader", "editor"],
|
||||
},
|
||||
"media_auth": True,
|
||||
"move": False,
|
||||
@@ -528,7 +521,7 @@ def test_models_documents_get_abilities_reader_user(
|
||||
assert all(
|
||||
value is False
|
||||
for key, value in document.get_abilities(user).items()
|
||||
if key not in ["link_select_options", "ancestors_links_definitions"]
|
||||
if key != "link_select_options"
|
||||
)
|
||||
|
||||
|
||||
@@ -547,7 +540,6 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
|
||||
"accesses_view": True,
|
||||
"ai_transform": False,
|
||||
"ai_translate": False,
|
||||
"ancestors_links_definitions": {},
|
||||
"attachment_upload": False,
|
||||
"children_create": False,
|
||||
"children_list": True,
|
||||
@@ -562,7 +554,7 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
"restricted": ["reader", "editor"],
|
||||
},
|
||||
"media_auth": True,
|
||||
"move": False,
|
||||
@@ -1182,6 +1174,8 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
|
||||
(
|
||||
[{"link_reach": "public", "link_role": "reader"}],
|
||||
{
|
||||
"restricted": ["editor"],
|
||||
"authenticated": ["editor"],
|
||||
"public": ["reader", "editor"],
|
||||
},
|
||||
),
|
||||
@@ -1189,6 +1183,7 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
|
||||
(
|
||||
[{"link_reach": "authenticated", "link_role": "reader"}],
|
||||
{
|
||||
"restricted": ["editor"],
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
},
|
||||
@@ -1200,7 +1195,7 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
|
||||
(
|
||||
[{"link_reach": "restricted", "link_role": "reader"}],
|
||||
{
|
||||
"restricted": None,
|
||||
"restricted": ["reader", "editor"],
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
},
|
||||
@@ -1208,7 +1203,7 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
|
||||
(
|
||||
[{"link_reach": "restricted", "link_role": "editor"}],
|
||||
{
|
||||
"restricted": None,
|
||||
"restricted": ["editor"],
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
},
|
||||
@@ -1234,7 +1229,7 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
|
||||
{"link_reach": "restricted", "link_role": "editor"},
|
||||
],
|
||||
{
|
||||
"restricted": None,
|
||||
"restricted": ["editor"],
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
},
|
||||
@@ -1246,6 +1241,8 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
|
||||
{"link_reach": "public", "link_role": "reader"},
|
||||
],
|
||||
{
|
||||
"restricted": ["editor"],
|
||||
"authenticated": ["editor"],
|
||||
"public": ["reader", "editor"],
|
||||
},
|
||||
),
|
||||
@@ -1256,6 +1253,8 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
|
||||
{"link_reach": "public", "link_role": "reader"},
|
||||
],
|
||||
{
|
||||
"restricted": ["editor"],
|
||||
"authenticated": ["editor"],
|
||||
"public": ["reader", "editor"],
|
||||
},
|
||||
),
|
||||
@@ -1265,7 +1264,7 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
|
||||
{"link_reach": "authenticated", "link_role": "editor"},
|
||||
{"link_reach": "public", "link_role": "reader"},
|
||||
],
|
||||
{"public": ["reader", "editor"]},
|
||||
{"authenticated": ["editor"], "public": ["reader", "editor"]},
|
||||
),
|
||||
(
|
||||
[
|
||||
@@ -1280,6 +1279,7 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
|
||||
{"link_reach": "authenticated", "link_role": "reader"},
|
||||
],
|
||||
{
|
||||
"restricted": ["editor"],
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
},
|
||||
@@ -1297,7 +1297,7 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
|
||||
{
|
||||
"public": ["reader", "editor"],
|
||||
"authenticated": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
"restricted": ["reader", "editor"],
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -410,6 +410,11 @@ class Base(Configuration):
|
||||
FRONTEND_THEME = values.Value(
|
||||
None, environ_name="FRONTEND_THEME", environ_prefix=None
|
||||
)
|
||||
FRONTEND_HOMEPAGE_FEATURE_ENABLED = values.BooleanValue(
|
||||
default=False,
|
||||
environ_name="FRONTEND_HOMEPAGE_FEATURE_ENABLED",
|
||||
environ_prefix=None,
|
||||
)
|
||||
FRONTEND_URL_JSON_FOOTER = values.Value(
|
||||
None, environ_name="FRONTEND_URL_JSON_FOOTER", environ_prefix=None
|
||||
)
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
import { Page, expect } from '@playwright/test';
|
||||
|
||||
export const CONFIG = {
|
||||
AI_FEATURE_ENABLED: true,
|
||||
CRISP_WEBSITE_ID: null,
|
||||
COLLABORATION_WS_URL: 'ws://localhost:4444/collaboration/ws/',
|
||||
ENVIRONMENT: 'development',
|
||||
FRONTEND_CSS_URL: null,
|
||||
FRONTEND_HOMEPAGE_FEATURE_ENABLED: true,
|
||||
FRONTEND_FOOTER_FEATURE_ENABLED: true,
|
||||
FRONTEND_THEME: 'default',
|
||||
MEDIA_BASE_URL: 'http://localhost:8083',
|
||||
LANGUAGES: [
|
||||
['en-us', 'English'],
|
||||
['fr-fr', 'Français'],
|
||||
['de-de', 'Deutsch'],
|
||||
['nl-nl', 'Nederlands'],
|
||||
],
|
||||
LANGUAGE_CODE: 'en-us',
|
||||
POSTHOG_KEY: {},
|
||||
SENTRY_DSN: null,
|
||||
};
|
||||
|
||||
export const keyCloakSignIn = async (
|
||||
page: Page,
|
||||
browserName: string,
|
||||
|
||||
@@ -2,27 +2,7 @@ import path from 'path';
|
||||
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDoc } from './common';
|
||||
|
||||
const config = {
|
||||
AI_FEATURE_ENABLED: true,
|
||||
CRISP_WEBSITE_ID: null,
|
||||
COLLABORATION_WS_URL: 'ws://localhost:4444/collaboration/ws/',
|
||||
ENVIRONMENT: 'development',
|
||||
FRONTEND_CSS_URL: null,
|
||||
FRONTEND_FOOTER_FEATURE_ENABLED: true,
|
||||
FRONTEND_THEME: 'default',
|
||||
MEDIA_BASE_URL: 'http://localhost:8083',
|
||||
LANGUAGES: [
|
||||
['en-us', 'English'],
|
||||
['fr-fr', 'Français'],
|
||||
['de-de', 'Deutsch'],
|
||||
['nl-nl', 'Nederlands'],
|
||||
],
|
||||
LANGUAGE_CODE: 'en-us',
|
||||
POSTHOG_KEY: {},
|
||||
SENTRY_DSN: null,
|
||||
};
|
||||
import { CONFIG, createDoc } from './common';
|
||||
|
||||
test.describe('Config', () => {
|
||||
test('it checks the config api is called', async ({ page }) => {
|
||||
@@ -36,7 +16,7 @@ test.describe('Config', () => {
|
||||
const response = await responsePromise;
|
||||
expect(response.ok()).toBeTruthy();
|
||||
|
||||
expect(await response.json()).toStrictEqual(config);
|
||||
expect(await response.json()).toStrictEqual(CONFIG);
|
||||
});
|
||||
|
||||
test('it checks that sentry is trying to init from config endpoint', async ({
|
||||
@@ -47,7 +27,7 @@ test.describe('Config', () => {
|
||||
if (request.method().includes('GET')) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
...config,
|
||||
...CONFIG,
|
||||
SENTRY_DSN: 'https://sentry.io/123',
|
||||
},
|
||||
});
|
||||
@@ -120,7 +100,7 @@ test.describe('Config', () => {
|
||||
if (request.method().includes('GET')) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
...config,
|
||||
...CONFIG,
|
||||
AI_FEATURE_ENABLED: false,
|
||||
},
|
||||
});
|
||||
@@ -151,7 +131,7 @@ test.describe('Config', () => {
|
||||
if (request.method().includes('GET')) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
...config,
|
||||
...CONFIG,
|
||||
CRISP_WEBSITE_ID: '1234',
|
||||
},
|
||||
});
|
||||
@@ -173,7 +153,7 @@ test.describe('Config', () => {
|
||||
if (request.method().includes('GET')) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
...config,
|
||||
...CONFIG,
|
||||
FRONTEND_CSS_URL: 'http://localhost:123465/css/style.css',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { CONFIG } from './common';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/docs/');
|
||||
});
|
||||
@@ -50,4 +52,27 @@ test.describe('Home page', () => {
|
||||
|
||||
await expect(footer).toBeVisible();
|
||||
});
|
||||
|
||||
test('it checks the homepage feature flag', async ({ page }) => {
|
||||
await page.route('**/api/v1.0/config/', async (route) => {
|
||||
const request = route.request();
|
||||
if (request.method().includes('GET')) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
...CONFIG,
|
||||
FRONTEND_HOMEPAGE_FEATURE_ENABLED: false,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
// Keyclock login page
|
||||
await expect(
|
||||
page.locator('.login-pf-page-header').getByText('impress'),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,17 +5,18 @@ import { Theme } from '@/cunningham/';
|
||||
import { PostHogConf } from '@/services';
|
||||
|
||||
interface ConfigResponse {
|
||||
LANGUAGES: [string, string][];
|
||||
LANGUAGE_CODE: string;
|
||||
ENVIRONMENT: string;
|
||||
AI_FEATURE_ENABLED?: boolean;
|
||||
COLLABORATION_WS_URL?: string;
|
||||
CRISP_WEBSITE_ID?: string;
|
||||
FRONTEND_THEME?: Theme;
|
||||
ENVIRONMENT: string;
|
||||
FRONTEND_CSS_URL?: string;
|
||||
FRONTEND_HOMEPAGE_FEATURE_ENABLED?: boolean;
|
||||
FRONTEND_THEME?: Theme;
|
||||
LANGUAGES: [string, string][];
|
||||
LANGUAGE_CODE: string;
|
||||
MEDIA_BASE_URL?: string;
|
||||
POSTHOG_KEY?: PostHogConf;
|
||||
SENTRY_DSN?: string;
|
||||
AI_FEATURE_ENABLED?: boolean;
|
||||
}
|
||||
|
||||
export const getConfig = async (): Promise<ConfigResponse> => {
|
||||
|
||||
@@ -3,14 +3,16 @@ import { useRouter } from 'next/router';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { useConfig } from '@/core';
|
||||
|
||||
import { useAuth } from '../hooks';
|
||||
import { getAuthUrl } from '../utils';
|
||||
import { getAuthUrl, gotoLogin } from '../utils';
|
||||
|
||||
export const Auth = ({ children }: PropsWithChildren) => {
|
||||
const { isLoading, pathAllowed, isFetchedAfterMount, authenticated } =
|
||||
useAuth();
|
||||
const { replace, pathname } = useRouter();
|
||||
const { data: config } = useConfig();
|
||||
|
||||
if (isLoading && !isFetchedAfterMount) {
|
||||
return (
|
||||
@@ -40,7 +42,11 @@ export const Auth = ({ children }: PropsWithChildren) => {
|
||||
* If the user is not authenticated and the path is not allowed, we redirect to the login page.
|
||||
*/
|
||||
if (!authenticated && !pathAllowed) {
|
||||
void replace('/login');
|
||||
if (config?.FRONTEND_HOMEPAGE_FEATURE_ENABLED) {
|
||||
void replace('/home');
|
||||
} else {
|
||||
gotoLogin();
|
||||
}
|
||||
return (
|
||||
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
|
||||
<Loader />
|
||||
@@ -49,9 +55,9 @@ export const Auth = ({ children }: PropsWithChildren) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* If the user is authenticated and the path is the login page, we redirect to the home page.
|
||||
* If the user is authenticated and the path is the home page, we redirect to the index.
|
||||
*/
|
||||
if (pathname === '/login' && authenticated) {
|
||||
if (pathname === '/home' && authenticated) {
|
||||
void replace('/');
|
||||
return (
|
||||
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
|
||||
|
||||
8
src/frontend/apps/impress/src/pages/home/index.tsx
Normal file
8
src/frontend/apps/impress/src/pages/home/index.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { HomeContent } from '@/features/home';
|
||||
import { NextPageWithLayout } from '@/types/next';
|
||||
|
||||
const Page: NextPageWithLayout = () => {
|
||||
return <HomeContent />;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -50,6 +50,7 @@ backend:
|
||||
DB_USER: dinum
|
||||
DB_PASSWORD: pass
|
||||
DB_PORT: 5432
|
||||
FRONTEND_HOMEPAGE_FEATURE_ENABLED: true
|
||||
FRONTEND_FOOTER_FEATURE_ENABLED: true
|
||||
FRONTEND_URL_JSON_FOOTER: https://impress.127.0.0.1.nip.io/contents/footer-demo.json
|
||||
POSTGRES_DB: impress
|
||||
|
||||
Reference in New Issue
Block a user