Skip to content

Commit 8002cd2

Browse files
Switch to ModelViewSet setup with Django permissions
1 parent 8bface7 commit 8002cd2

15 files changed

Lines changed: 360 additions & 256 deletions

File tree

backend/annotation/migrations/0002_label_labeling.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Generated by Django 4.2.20 on 2025-11-12 13:53
1+
# Generated by Django 4.2.27 on 2025-12-11 15:10
22

33
from django.conf import settings
44
from django.db import migrations, models
@@ -9,7 +9,7 @@ class Migration(migrations.Migration):
99

1010
dependencies = [
1111
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12-
("problem", "0004_sentence_remove_problem_premises_knowledgebase_and_more"),
12+
("problem", "0007_alter_problem_options"),
1313
("annotation", "0001_initial"),
1414
]
1515

@@ -34,6 +34,9 @@ class Migration(migrations.Migration):
3434
),
3535
),
3636
],
37+
options={
38+
"ordering": ["text"],
39+
},
3740
),
3841
migrations.CreateModel(
3942
name="Labeling",
@@ -101,6 +104,10 @@ class Migration(migrations.Migration):
101104
],
102105
options={
103106
"ordering": ["-attached_at"],
107+
"permissions": [
108+
("delete_own_labeling", "Can remove own labeling from problems"),
109+
("delete_any_labeling", "Can remove any labeling from problems"),
110+
],
104111
"indexes": [
105112
models.Index(
106113
fields=["problem", "removed_at"],

backend/annotation/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ class Label(models.Model):
8989
help_text="A detailed description of the label, indicating when it should be used."
9090
)
9191

92+
class Meta:
93+
ordering = ["text"]
94+
9295
def __str__(self):
9396
return self.text
9497

@@ -145,6 +148,11 @@ class Meta:
145148
models.Index(fields=["problem", "removed_at"]),
146149
models.Index(fields=["label", "removed_at"]),
147150
]
151+
permissions = [
152+
("delete_own_labeling", "Can remove own labeling from problems"),
153+
("delete_any_labeling", "Can remove any labeling from problems"),
154+
]
155+
148156

149157
def is_active(self) -> bool:
150158
"""Check if this labeling is currently active (not removed)."""

backend/annotation/serializers.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
from rest_framework import serializers
2+
3+
from django.contrib.auth.models import AnonymousUser
4+
5+
from annotation.models import Label, Labeling
6+
from problem.models import Problem
7+
from user.models import User
8+
9+
10+
class LabelSerializer(serializers.ModelSerializer):
11+
"""
12+
Serializer for Label model.
13+
"""
14+
15+
class Meta:
16+
model = Label
17+
fields = ["id", "text", "description"]
18+
19+
20+
class ActiveLabelSerializer(serializers.Serializer):
21+
"""
22+
Serializer for active labels attached to a problem.
23+
Includes attachedInfo and removable status based on current user.
24+
"""
25+
26+
id = serializers.IntegerField(source="label.id")
27+
text = serializers.CharField(source="label.text")
28+
description = serializers.CharField(source="label.description")
29+
attachedInfo = serializers.SerializerMethodField()
30+
removable = serializers.SerializerMethodField()
31+
32+
def get_attachedInfo(self, labeling: Labeling) -> dict:
33+
"""Get attachment information for the label."""
34+
request = self.context.get("request")
35+
user: User | AnonymousUser | None = request.user if request else None
36+
37+
if user and user.is_anonymous is False:
38+
attached_by_current_user = labeling.attached_by.pk == user.pk
39+
else:
40+
attached_by_current_user = False
41+
42+
return {
43+
"userName": labeling.attached_by.username,
44+
"date": labeling.attached_at.isoformat(),
45+
"attachedByCurrentUser": attached_by_current_user,
46+
}
47+
48+
def get_removable(self, labeling: Labeling) -> bool:
49+
"""Determine if the label is removable by the current user."""
50+
request = self.context.get("request")
51+
user: User | AnonymousUser | None = request.user if request else None
52+
53+
if user is None or user.is_anonymous:
54+
return False
55+
56+
if user.is_superuser:
57+
return True
58+
59+
if user.has_perm("annotation.delete_any_labeling"):
60+
return True
61+
62+
if user.has_perm("annotation.delete_own_labeling"):
63+
return labeling.attached_by.pk == user.pk
64+
65+
return False
66+
67+
68+
class LabelingSerializer(serializers.ModelSerializer):
69+
"""
70+
Serializer for Labeling model, including the full label details.
71+
"""
72+
73+
label = LabelSerializer(read_only=True)
74+
attachedAt = serializers.DateTimeField(source="attached_at")
75+
attachedBy = serializers.PrimaryKeyRelatedField(
76+
source="attached_by", read_only=True
77+
)
78+
removedAt = serializers.DateTimeField(source="removed_at", allow_null=True)
79+
removedBy = serializers.PrimaryKeyRelatedField(
80+
source="removed_by", allow_null=True, read_only=True
81+
)
82+
83+
class Meta:
84+
model = Labeling
85+
fields = [
86+
"id",
87+
"label",
88+
"attached_at",
89+
"attached_by",
90+
"removed_at",
91+
"removed_by",
92+
"notes",
93+
]
94+
95+
96+
class SelectedLabelSerializer(serializers.Serializer):
97+
"""Serializer for a selected label in the save labels input."""
98+
99+
id = serializers.IntegerField()
100+
101+
def validate_id(self, value):
102+
"""Validate that the label exists."""
103+
if not Label.objects.filter(id=value).exists():
104+
raise serializers.ValidationError(f"Label with ID {value} does not exist.")
105+
return value
106+
107+
108+
class SaveLabelsInputSerializer(serializers.Serializer):
109+
"""
110+
Serializer for validating save labels input data.
111+
Used when saving/updating labels for a problem.
112+
"""
113+
114+
problemId = serializers.IntegerField()
115+
selectedLabels = SelectedLabelSerializer(many=True, allow_empty=True)
116+
remarks = serializers.CharField(required=False, allow_blank=True, default="")
117+
118+
def validate_problemId(self, value):
119+
"""Validate that the problem exists."""
120+
if not Problem.objects.filter(id=value).exists():
121+
raise serializers.ValidationError(
122+
f"Problem with ID {value} does not exist."
123+
)
124+
return value
125+
126+
127+
class LabelingInputSerializer(serializers.Serializer):
128+
"""
129+
Serializer for creating a single labeling.
130+
"""
131+
132+
problemId = serializers.IntegerField()
133+
labelId = serializers.IntegerField()
134+
notes = serializers.CharField(required=False, allow_blank=True, default="")
135+
136+
def validate_problemId(self, value):
137+
"""Validate that the problem exists."""
138+
if not Problem.objects.filter(id=value).exists():
139+
raise serializers.ValidationError(
140+
f"Problem with ID {value} does not exist."
141+
)
142+
return value
143+
144+
def validate_labelId(self, value):
145+
"""Validate that the label exists."""
146+
if not Label.objects.filter(id=value).exists():
147+
raise serializers.ValidationError(f"Label with ID {value} does not exist.")
148+
return value

backend/annotation/urls.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1 @@
1-
from django.urls import path
2-
3-
from annotation.views import LabelsView
4-
5-
6-
urlpatterns = [
7-
path("", LabelsView.as_view(), name="labels"),
8-
]
1+
urlpatterns = []

0 commit comments

Comments
 (0)