Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
e4729fb
Add Label and Labeling models and migration
XanderVertegaal Nov 12, 2025
5ab479c
Refactor grey styling on ProblemDetail headers
XanderVertegaal Nov 12, 2025
7b47e74
Add mockLabels (for development) -- WIP
XanderVertegaal Nov 12, 2025
5d5b24e
Added ProblemLabelsComponent
XanderVertegaal Nov 12, 2025
0997c43
Add ManageLabelsModal
XanderVertegaal Nov 12, 2025
6c78d6f
Add and fix tests
XanderVertegaal Nov 12, 2025
97fba74
Cleanup
XanderVertegaal Nov 12, 2025
72b2d54
Update fixtures; create Admin interface
XanderVertegaal Nov 14, 2025
4a734f0
Formatting
XanderVertegaal Nov 14, 2025
973c4f5
Replace serialize methods with proper serializers
XanderVertegaal Nov 14, 2025
b1ef4dc
User serializers in view
XanderVertegaal Nov 14, 2025
02b7eb6
Show toasts on fetching errors
XanderVertegaal Nov 14, 2025
793ed9e
Query all labels
XanderVertegaal Nov 14, 2025
d131340
Show queried labels in frontend
XanderVertegaal Nov 14, 2025
d8d4b61
Expand label form
XanderVertegaal Nov 14, 2025
aa103ef
Fix typo
XanderVertegaal Nov 14, 2025
4e11806
Save labels in backend
XanderVertegaal Nov 14, 2025
9e8dac8
Handle dates
XanderVertegaal Nov 14, 2025
48f9292
Merge branch 'feature/user-models' into feature/problem-labels
XanderVertegaal Dec 11, 2025
16f2ff0
Restore missing imports
XanderVertegaal Dec 11, 2025
8bface7
Merge branch 'feature/user-models' into feature/problem-labels
XanderVertegaal Dec 11, 2025
8002cd2
Switch to ModelViewSet setup with Django permissions
XanderVertegaal Dec 11, 2025
98d27c7
Write tests for labeling
XanderVertegaal Dec 11, 2025
9018d5a
Merge branch 'develop' into feature/problem-labels
XanderVertegaal Dec 17, 2025
2eb8545
Removed useless log
XanderVertegaal Dec 17, 2025
61eed76
Wrap labels
XanderVertegaal Dec 17, 2025
3bbf47a
Add loading indicator; dismiss modals correctly
XanderVertegaal Dec 17, 2025
53c7140
Cleanup
XanderVertegaal Dec 17, 2025
534ed83
More cleanup
XanderVertegaal Dec 17, 2025
6516edc
Fix failing tests
XanderVertegaal Dec 23, 2025
849741a
Merge branch 'develop' into feature/problem-labels
XanderVertegaal Dec 23, 2025
da15e0d
Merge branch 'develop' into feature/problem-labels
XanderVertegaal Jan 15, 2026
e4f8bb5
Rely on DRF built-in permission handling
XanderVertegaal Jan 15, 2026
d457341
Move utils to util.ts
XanderVertegaal Jan 15, 2026
eafc758
Fix error message
XanderVertegaal Jan 15, 2026
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
7 changes: 7 additions & 0 deletions backend/annotation/admin.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
from django.contrib import admin

from annotation.models import Label


# Register your models here.
@admin.register(Label)
class LabelAdmin(admin.ModelAdmin):
list_display = ("text", "description")
search_fields = ("text",)
50 changes: 50 additions & 0 deletions backend/annotation/fixtures/initial.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
[
{
"model": "annotation.label",
"pk": 1,
"fields": {
"text": "Important",
"description": "This problem is particularly important."
}
},
{
"model": "annotation.label",
"pk": 2,
"fields": {
"text": "Review needed",
"description": "This problem needs to be reviewed by a master annotator."
}
},
{
"model": "annotation.label",
"pk": 3,
"fields": {
"text": "Ambiguous",
"description": "The problem statement is ambiguous and needs clarification."
}
},
{
"model": "annotation.label",
"pk": 4,
"fields": {
"text": "Incomplete knowledge",
"description": "Some knowledge base items are missing."
}
},
{
"model": "annotation.label",
"pk": 5,
"fields": {
"text": "Spelling",
"description": "The premises or hypothesis are misspelled, which may lead to wrong analyses."
}
},
{
"model": "annotation.label",
"pk": 6,
"fields": {
"text": "Wrong entailment label",
"description": "The entailment label is incorrect."
}
}
]
123 changes: 123 additions & 0 deletions backend/annotation/migrations/0002_label_labeling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Generated by Django 4.2.27 on 2025-12-11 15:10

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


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("problem", "0007_alter_problem_options"),
("annotation", "0001_initial"),
]

operations = [
migrations.CreateModel(
name="Label",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("text", models.CharField(max_length=255)),
(
"description",
models.TextField(
help_text="A detailed description of the label, indicating when it should be used."
),
),
],
options={
"ordering": ["text"],
},
),
migrations.CreateModel(
name="Labeling",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("attached_at", models.DateTimeField(auto_now_add=True)),
(
"removed_at",
models.DateTimeField(
blank=True,
help_text="When this label was removed from the problem.",
null=True,
),
),
(
"notes",
models.TextField(
blank=True,
help_text="Optional notes explaining why this label was added or removed.",
),
),
(
"attached_by",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="labelings_attached",
to=settings.AUTH_USER_MODEL,
),
),
(
"label",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="labelings",
to="annotation.label",
),
),
(
"problem",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="labelings",
to="problem.problem",
),
),
(
"removed_by",
models.ForeignKey(
blank=True,
help_text="User who removed this label.",
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="labelings_removed",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ["-attached_at"],
"permissions": [
("delete_own_labeling", "Can remove own labeling from problems"),
("delete_any_labeling", "Can remove any labeling from problems"),
],
"indexes": [
models.Index(
fields=["problem", "removed_at"],
name="annotation__problem_e7ac7b_idx",
),
models.Index(
fields=["label", "removed_at"],
name="annotation__label_i_a229d2_idx",
),
],
},
),
]
80 changes: 80 additions & 0 deletions backend/annotation/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,83 @@ class KnowledgeBaseAnnotation(models.Model):
)

created_at = models.DateTimeField(auto_now_add=True)


class Label(models.Model):
text = models.CharField(max_length=255)
description = models.TextField(
help_text="A detailed description of the label, indicating when it should be used."
)

class Meta:
ordering = ["text"]

def __str__(self):
return self.text


class Labeling(models.Model):
"""
The attachment of a label to a problem.

Each time a label is attached to a problem, a new Labeling record is created.
When removed, the record is marked as removed (not deleted), so the history of labelings is preserved.
"""

problem = models.ForeignKey(
Problem,
on_delete=models.CASCADE,
related_name="labelings",
)

label = models.ForeignKey(
Label,
on_delete=models.CASCADE,
related_name="labelings",
)

attached_at = models.DateTimeField(auto_now_add=True)
attached_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="labelings_attached",
)

removed_at = models.DateTimeField(
null=True,
blank=True,
help_text="When this label was removed from the problem.",
)
removed_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="labelings_removed",
null=True,
blank=True,
help_text="User who removed this label.",
)
Comment on lines +119 to +138

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a request for change, just a remark: another way to organize this would be to create a separate Event model for changes made by users. It would have a type (such as "deleted"), a link to the affected model, a user and a date/time. Maybe not worth worrying about for this project, but when there many models that can be edited, this could help to keep the models leaner.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds very scalable! 👍


notes = models.TextField(
blank=True,
help_text="Optional notes explaining why this label was added or removed.",
)

class Meta:
ordering = ["-attached_at"]
indexes = [
models.Index(fields=["problem", "removed_at"]),
models.Index(fields=["label", "removed_at"]),
]
permissions = [
("delete_own_labeling", "Can remove own labeling from problems"),
("delete_any_labeling", "Can remove any labeling from problems"),
]


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

def __str__(self):
status = "active" if self.is_active() else f"removed at {self.removed_at}"
return f"Label '{self.label.text}' on Problem {self.problem.pk} ({status})"
121 changes: 121 additions & 0 deletions backend/annotation/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
from rest_framework import serializers

from django.contrib.auth.models import AnonymousUser

from annotation.models import Label, Labeling
from problem.models import Problem
from user.models import User


class LabelSerializer(serializers.ModelSerializer):
"""
Serializer for Label model.
"""

class Meta:
model = Label
fields = ["id", "text", "description"]


class ActiveLabelSerializer(serializers.Serializer):
"""
Serializer for active labels attached to a problem.
Includes attachedInfo and removable status based on current user.
"""

id = serializers.IntegerField(source="label.id")
text = serializers.CharField(source="label.text")
description = serializers.CharField(source="label.description")
attachedInfo = serializers.SerializerMethodField()
removable = serializers.SerializerMethodField()

def get_attachedInfo(self, labeling: Labeling) -> dict:
"""Get attachment information for the label."""
request = self.context.get("request")
user: User | AnonymousUser | None = request.user if request else None

if user and user.is_anonymous is False:
attached_by_current_user = labeling.attached_by.pk == user.pk
else:
attached_by_current_user = False

return {
"userName": labeling.attached_by.username,
"date": labeling.attached_at.isoformat(),
"attachedByCurrentUser": attached_by_current_user,
}

def get_removable(self, labeling: Labeling) -> bool:
"""Determine if the label is removable by the current user."""
request = self.context.get("request")
user: User | AnonymousUser | None = request.user if request else None

if user is None or user.is_anonymous:
return False

if user.is_superuser or user.has_perm("annotation.delete_any_labeling"):
return True

if user.has_perm("annotation.delete_own_labeling"):
return labeling.attached_by.pk == user.pk

return False


class LabelingSerializer(serializers.ModelSerializer):
"""
Serializer for Labeling model, including the full label details.
"""

label = LabelSerializer(read_only=True)
attachedAt = serializers.DateTimeField(source="attached_at")
attachedBy = serializers.PrimaryKeyRelatedField(
source="attached_by", read_only=True
)
removedAt = serializers.DateTimeField(source="removed_at", allow_null=True)
removedBy = serializers.PrimaryKeyRelatedField(
source="removed_by", allow_null=True, read_only=True
)

class Meta:
model = Labeling
fields = [
"id",
"label",
"attached_at",
"attached_by",
"removed_at",
"removed_by",
"notes",
]


class SelectedLabelSerializer(serializers.Serializer):
"""Serializer for a selected label in the save labels input."""

id = serializers.IntegerField()

def validate_id(self, value):
"""Validate that the label exists."""
if not Label.objects.filter(id=value).exists():
raise serializers.ValidationError(f"Label with ID {value} does not exist.")
return value


class SaveLabelsInputSerializer(serializers.Serializer):
"""
Serializer for validating save labels input data.
Used when saving/updating labels for a problem.
"""

problemId = serializers.IntegerField()
selectedLabels = SelectedLabelSerializer(many=True, allow_empty=True)
remarks = serializers.CharField(required=False, allow_blank=True, default="")

def validate_problemId(self, value):
"""Validate that the problem exists."""
if not Problem.objects.filter(id=value).exists():
raise serializers.ValidationError(
f"Problem with ID {value} does not exist."
)
return value
Loading