Skip to content

✨Ask for access #1081

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ and this project adheres to
- 📝(project) add troubleshoot doc #1066
- 📝(project) add system-requirement doc #1066
- 🔧(front) configure x-frame-options to DENY in nginx conf #1084
- (doc) add documentation to install with compose #855
- ✨Ask for access #1081
- 📝(doc) add documentation to install with compose #855

### Changed

Expand All @@ -26,11 +27,12 @@ and this project adheres to

### Fixed

-🐛(frontend) table of content disappearing #982
-🐛(frontend) fix multiple EmojiPicker #1012
-🐛(frontend) fix meta title #1017
-🔧(git) set LF line endings for all text files #1032
-📝(docs) minor fixes to docs/env.md
- 🐛(frontend) table of content disappearing #982
- 🐛(frontend) fix multiple EmojiPicker #1012
- 🐛(frontend) fix meta title #1017
- 🔧(git) set LF line endings for all text files #1032
- 📝(docs) minor fixes to docs/env.md


## [3.3.0] - 2025-05-06

Expand Down
44 changes: 44 additions & 0 deletions src/backend/core/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,50 @@ def validate_role(self, role):
return role


class RoleSerializer(serializers.Serializer):
"""Serializer validating role choices."""

role = serializers.ChoiceField(
choices=models.RoleChoices.choices, required=False, allow_null=True
)


class DocumentAskForAccessCreateSerializer(serializers.Serializer):
"""Serializer for creating a document ask for access."""

role = serializers.ChoiceField(
choices=models.RoleChoices.choices,
required=False,
default=models.RoleChoices.READER,
)


class DocumentAskForAccessSerializer(serializers.ModelSerializer):
"""Serializer for document ask for access model"""

abilities = serializers.SerializerMethodField(read_only=True)
user = UserSerializer(read_only=True)

class Meta:
model = models.DocumentAskForAccess
fields = [
"id",
"document",
"user",
"role",
"created_at",
"abilities",
]
read_only_fields = ["id", "document", "user", "role", "created_at", "abilities"]

def get_abilities(self, invitation) -> dict:
"""Return abilities of the logged-in user on the instance."""
request = self.context.get("request")
if request:
return invitation.get_abilities(request.user)
return {}


class VersionFilterSerializer(serializers.Serializer):
"""Validate version filters applied to the list endpoint."""

Expand Down
78 changes: 78 additions & 0 deletions src/backend/core/api/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from core import authentication, enums, models
from core.services.ai_services import AIService
from core.services.collaboration_services import CollaborationService
from core.tasks.mail import send_ask_for_access_mail
from core.utils import extract_attachments, filter_descendants

from . import permissions, serializers, utils
Expand Down Expand Up @@ -1772,6 +1773,83 @@ def perform_create(self, serializer):
)


class DocumentAskForAccessViewSet(
drf.mixins.ListModelMixin,
drf.mixins.RetrieveModelMixin,
drf.mixins.DestroyModelMixin,
viewsets.GenericViewSet,
):
"""API ViewSet for asking for access to a document."""

lookup_field = "id"
pagination_class = Pagination
permission_classes = [permissions.IsAuthenticated, permissions.AccessPermission]
queryset = models.DocumentAskForAccess.objects.all()
serializer_class = serializers.DocumentAskForAccessSerializer
_document = None

def get_document_or_404(self):
"""Get the document related to the viewset or raise a 404 error."""
if self._document is None:
try:
self._document = models.Document.objects.get(
pk=self.kwargs["resource_id"]
)
except models.Document.DoesNotExist as e:
raise drf.exceptions.NotFound("Document not found.") from e
return self._document

def get_queryset(self):
"""Return the queryset according to the action."""
document = self.get_document_or_404()

queryset = super().get_queryset()
queryset = queryset.filter(document=document)

roles = set(document.get_roles(self.request.user))
is_owner_or_admin = bool(roles.intersection(set(models.PRIVILEGED_ROLES)))
if not is_owner_or_admin:
queryset = queryset.filter(user=self.request.user)

return queryset

def create(self, request, *args, **kwargs):
"""Create a document ask for access resource."""
document = self.get_document_or_404()

serializer = serializers.DocumentAskForAccessCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)

queryset = self.get_queryset()

if queryset.filter(user=request.user).exists():
return drf.response.Response(
{"detail": "You already ask to access to this document."},
status=drf.status.HTTP_400_BAD_REQUEST,
)

ask_for_access = models.DocumentAskForAccess.objects.create(
document=document,
user=request.user,
role=serializer.validated_data["role"],
)

send_ask_for_access_mail.delay(ask_for_access.id)

return drf.response.Response(status=drf.status.HTTP_201_CREATED)

@drf.decorators.action(detail=True, methods=["post"])
def accept(self, request, *args, **kwargs):
"""Accept a document ask for access resource."""
document_ask_for_access = self.get_object()

serializer = serializers.RoleSerializer(data=request.data)
serializer.is_valid(raise_exception=True)

document_ask_for_access.accept(role=serializer.validated_data.get("role"))
return drf.response.Response(status=drf.status.HTTP_204_NO_CONTENT)


class ConfigView(drf.views.APIView):
"""API ViewSet for sharing some public settings."""

Expand Down
11 changes: 11 additions & 0 deletions src/backend/core/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,17 @@ class Meta:
role = factory.fuzzy.FuzzyChoice([r[0] for r in models.RoleChoices.choices])


class DocumentAskForAccessFactory(factory.django.DjangoModelFactory):
"""Create fake document ask for access for testing."""

class Meta:
model = models.DocumentAskForAccess

document = factory.SubFactory(DocumentFactory)
user = factory.SubFactory(UserFactory)
role = factory.fuzzy.FuzzyChoice([r[0] for r in models.RoleChoices.choices])


class TemplateFactory(factory.django.DjangoModelFactory):
"""A factory to create templates"""

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Generated by Django 5.2.3 on 2025-06-18 10:02

import uuid

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("core", "0021_activate_unaccent_extension"),
]

operations = [
migrations.CreateModel(
name="DocumentAskForAccess",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="primary key for the record as UUID",
primary_key=True,
serialize=False,
verbose_name="id",
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True,
help_text="date and time at which a record was created",
verbose_name="created on",
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True,
help_text="date and time at which a record was last updated",
verbose_name="updated on",
),
),
(
"role",
models.CharField(
choices=[
("reader", "Reader"),
("editor", "Editor"),
("administrator", "Administrator"),
("owner", "Owner"),
],
default="reader",
max_length=20,
),
),
(
"document",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="ask_for_accesses",
to="core.document",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="ask_for_accesses",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "Document ask for access",
"verbose_name_plural": "Document ask for accesses",
"db_table": "impress_document_ask_for_access",
"constraints": [
models.UniqueConstraint(
fields=("user", "document"),
name="unique_document_ask_for_access_user",
violation_error_message="This user has already asked for access to this document.",
)
],
},
),
]
Loading
Loading