From 6f59d3363c770c7815618ce1fd91bdf69afc9527 Mon Sep 17 00:00:00 2001 From: Elodie MORIN Date: Thu, 8 Oct 2026 10:20:46 +0200 Subject: [PATCH 1/2] fix(manage annotators): Cannot access with AnnotationFileRange change permission --- backend/api/models/annotation/annotation_file_range.py | 3 ++- backend/api/models/annotation/annotation_phase.py | 4 ++++ backend/api/models/common/__abstract_permission.py | 10 +++++++--- backend/api/schema/nodes/annotation_phase.py | 10 ++++++++++ .../AnnotationFileRange/FileRangeActionBar.tsx | 2 +- .../AnnotationPhase/api/annotation-phase.graphql | 1 + 6 files changed, 25 insertions(+), 5 deletions(-) diff --git a/backend/api/models/annotation/annotation_file_range.py b/backend/api/models/annotation/annotation_file_range.py index 84616a8cb..7279868ed 100644 --- a/backend/api/models/annotation/annotation_file_range.py +++ b/backend/api/models/annotation/annotation_file_range.py @@ -8,6 +8,7 @@ from backend.aplose.models import User from .annotation_task import AnnotationTask +from ..common.__abstract_permission import AbstractPermission from ..data import Spectrogram @@ -63,7 +64,7 @@ def filter_editable_by(self, user: User, **kwargs): ) -class AnnotationFileRange(models.Model): +class AnnotationFileRange(AbstractPermission, models.Model): """Gives a range of files to annotate by an annotator within a campaign""" objects = models.Manager.from_queryset(AnnotationFileRangeQuerySet)() diff --git a/backend/api/models/annotation/annotation_phase.py b/backend/api/models/annotation/annotation_phase.py index 0387741be..4e12b8618 100644 --- a/backend/api/models/annotation/annotation_phase.py +++ b/backend/api/models/annotation/annotation_phase.py @@ -4,6 +4,7 @@ from django.db.models import Q, CheckConstraint from django_extension.models import ExtendedEnum, ExtendedQuerySet +from .annotation_file_range import AnnotationFileRange from backend.api.models.common.__abstract_archivable import AbstractArchivable from backend.aplose.models import User @@ -99,3 +100,6 @@ def has_change_permission(self, user: "User") -> bool: or user.id == self.created_by_id or user.id == self.annotation_campaign.owner_id ) + + def has_file_range_change_permission(self, user: "User") -> bool: + return self.has_change_permission(user) and AnnotationFileRange.has_global_change_permission(user) diff --git a/backend/api/models/common/__abstract_permission.py b/backend/api/models/common/__abstract_permission.py index e12d2fecf..a967f4dbc 100644 --- a/backend/api/models/common/__abstract_permission.py +++ b/backend/api/models/common/__abstract_permission.py @@ -6,10 +6,14 @@ class AbstractPermission(models.Model): class Meta: abstract = True - def has_change_permission(self, user: "User") -> bool: + @classmethod + def has_global_change_permission(cls, user: "User") -> bool: edit_permissions = Permission.objects.filter( codename__startswith="change", - content_type__app_label=self._meta.app_label, - content_type__model=self._meta.model_name, + content_type__app_label=cls._meta.app_label, + content_type__model=cls._meta.model_name, ).first() return user.has_perm(edit_permissions) + + def has_change_permission(self, user: "User") -> bool: + return self.has_global_change_permission(user=user) diff --git a/backend/api/schema/nodes/annotation_phase.py b/backend/api/schema/nodes/annotation_phase.py index 57b451b62..34703be1a 100644 --- a/backend/api/schema/nodes/annotation_phase.py +++ b/backend/api/schema/nodes/annotation_phase.py @@ -2,6 +2,7 @@ import graphene_django_optimizer from django.db.models import Sum from django_extension.schema.types import ExtendedNode +from graphql import GraphQLResolveInfo from backend.api.models import AnnotationPhase, AnnotationTask from backend.api.schema.enums import AnnotationPhaseType @@ -78,3 +79,12 @@ def resolve_annotators(self: AnnotationPhase, info): ).distinct(), info, ) + + has_file_range_change_permission = graphene.Boolean(required=True) + + @graphene_django_optimizer.resolver_hints() + def resolve_has_file_range_change_permission( + self: AnnotationPhase, info: GraphQLResolveInfo + ): + # pylint: disable=not-callable + return self.has_file_range_change_permission(info.context.user) diff --git a/frontend/src/features/AnnotationFileRange/FileRangeActionBar.tsx b/frontend/src/features/AnnotationFileRange/FileRangeActionBar.tsx index d30d85e24..986b22e29 100644 --- a/frontend/src/features/AnnotationFileRange/FileRangeActionBar.tsx +++ b/frontend/src/features/AnnotationFileRange/FileRangeActionBar.tsx @@ -104,7 +104,7 @@ export const FileRangeActionBar: React.FC<{ isPending?: boolean }> = ({ isPendin - { phase?.hasChangePermission && + { phase?.hasFileRangeChangePermission && {/* Manage annotators */ } Date: Thu, 8 Oct 2026 10:31:41 +0200 Subject: [PATCH 2/2] fix CI --- .../api/models/annotation/annotation_phase.py | 6 +++-- backend/api/schema/nodes/annotation_phase.py | 2 +- frontend/tests/utils/mock/phase.ts | 6 ++++- frontend/tests/utils/mock/types/phase.ts | 24 +++++++++---------- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/backend/api/models/annotation/annotation_phase.py b/backend/api/models/annotation/annotation_phase.py index 4e12b8618..fa7cb69ef 100644 --- a/backend/api/models/annotation/annotation_phase.py +++ b/backend/api/models/annotation/annotation_phase.py @@ -4,9 +4,9 @@ from django.db.models import Q, CheckConstraint from django_extension.models import ExtendedEnum, ExtendedQuerySet -from .annotation_file_range import AnnotationFileRange from backend.api.models.common.__abstract_archivable import AbstractArchivable from backend.aplose.models import User +from .annotation_file_range import AnnotationFileRange class AnnotationPhaseQuerySet(ExtendedQuerySet): @@ -102,4 +102,6 @@ def has_change_permission(self, user: "User") -> bool: ) def has_file_range_change_permission(self, user: "User") -> bool: - return self.has_change_permission(user) and AnnotationFileRange.has_global_change_permission(user) + return self.has_change_permission( + user + ) and AnnotationFileRange.has_global_change_permission(user) diff --git a/backend/api/schema/nodes/annotation_phase.py b/backend/api/schema/nodes/annotation_phase.py index 34703be1a..9e1bd2695 100644 --- a/backend/api/schema/nodes/annotation_phase.py +++ b/backend/api/schema/nodes/annotation_phase.py @@ -84,7 +84,7 @@ def resolve_annotators(self: AnnotationPhase, info): @graphene_django_optimizer.resolver_hints() def resolve_has_file_range_change_permission( - self: AnnotationPhase, info: GraphQLResolveInfo + self: AnnotationPhase, info: GraphQLResolveInfo ): # pylint: disable=not-callable return self.has_file_range_change_permission(info.context.user) diff --git a/frontend/tests/utils/mock/phase.ts b/frontend/tests/utils/mock/phase.ts index 2c7b305f6..fd04f4656 100644 --- a/frontend/tests/utils/mock/phase.ts +++ b/frontend/tests/utils/mock/phase.ts @@ -27,7 +27,8 @@ export const PHASE_QUERIES: { userTasksCount, archived: phase.archived, hasAnnotations: phase.hasAnnotations, - hasChangePermission: true + hasChangePermission: true, + hasFileRangeChangePermission: true, }, }, managerVerification: { @@ -41,6 +42,7 @@ export const PHASE_QUERIES: { archived: phase.archived, hasAnnotations: phase.hasAnnotations, hasChangePermission: true, + hasFileRangeChangePermission: true, }, }, Annotation: { @@ -54,6 +56,7 @@ export const PHASE_QUERIES: { archived: phase.archived, hasAnnotations: phase.hasAnnotations, hasChangePermission: false, + hasFileRangeChangePermission: false, }, }, Verification: { @@ -67,6 +70,7 @@ export const PHASE_QUERIES: { archived: phase.archived, hasAnnotations: phase.hasAnnotations, hasChangePermission: false, + hasFileRangeChangePermission: false, }, }, }, diff --git a/frontend/tests/utils/mock/types/phase.ts b/frontend/tests/utils/mock/types/phase.ts index 614cde921..66bd4b413 100644 --- a/frontend/tests/utils/mock/types/phase.ts +++ b/frontend/tests/utils/mock/types/phase.ts @@ -1,22 +1,22 @@ import { type AnnotationPhaseNode } from '../../../../src/api/types.gql-generated'; export type Phase = - Omit + Omit export const phase: Phase = { - id: '1', - createdAt: new Date().toISOString(), - archived: false, - hasAnnotations: true, + id: '1', + createdAt: new Date().toISOString(), + archived: false, + hasAnnotations: true, } export const otherPhase: Phase = { - id: '2', - createdAt: new Date().toISOString(), - archived: false, - hasAnnotations: true, + id: '2', + createdAt: new Date().toISOString(), + archived: false, + hasAnnotations: true, } export const completedTasksCount = 50; export const tasksCount = 100;