Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
667a6ab
Reinterpret KB items and labelings as annotations
XanderVertegaal Jan 24, 2026
ae7e6b4
Update views and serializers
XanderVertegaal Jan 24, 2026
5108bb7
Use new annotations in frontend
XanderVertegaal Jan 25, 2026
868b6f6
Rename Labeling to LabelAnnotation in backend
XanderVertegaal Jan 25, 2026
ed923bf
Fix frontend tests
XanderVertegaal Jan 25, 2026
ab35841
Rename removeLabel
XanderVertegaal Jan 25, 2026
663bab7
Leave remarks out for now
XanderVertegaal Feb 1, 2026
4ec3e7a
Consistent permission naming
XanderVertegaal Feb 1, 2026
3df62b7
Update and expand backend tests
XanderVertegaal Feb 1, 2026
a6d5e32
Fix frontend tests
XanderVertegaal Feb 1, 2026
c64dbef
Fix frontend tests
XanderVertegaal Feb 1, 2026
7f15592
Handle en-US formatted date string
XanderVertegaal Feb 1, 2026
26f47e8
Un-nest KB ID validation logic
XanderVertegaal Feb 10, 2026
226894b
Use many=True for label/kb serializers
XanderVertegaal Feb 10, 2026
93aa6af
Use LabelAnnotationSerializer.create to create new LabelAnnotation data
XanderVertegaal Feb 10, 2026
760c710
Pre-refactor WIP
XanderVertegaal Feb 12, 2026
82ba2e0
Fix KB saving
XanderVertegaal Feb 12, 2026
2bacc67
Remove unnecessary test
XanderVertegaal Feb 12, 2026
90e71bb
Return documentation for LabelAnnotation
XanderVertegaal Feb 12, 2026
3ebbdf1
Remove AnnotationSerializer
XanderVertegaal Feb 12, 2026
0b9d349
Outfactor annotation user check
XanderVertegaal Feb 12, 2026
0f46ceb
Move and fix tests
XanderVertegaal Feb 12, 2026
778d028
Remove annotation properties from ProblemSerializer
XanderVertegaal Feb 12, 2026
55bb44b
Further cleanup
XanderVertegaal Feb 12, 2026
4166e1b
Merge branch 'develop' into feature/kb-and-labels-as-annotations
XanderVertegaal Feb 20, 2026
6cb981e
Remove unnecessary test
XanderVertegaal Feb 20, 2026
3810ce1
Rename annotator_session and master_annotator_session
XanderVertegaal Feb 20, 2026
3c2b6b5
Update backend tests
XanderVertegaal Feb 20, 2026
c7b2868
Fix en-GB formatting for dates
XanderVertegaal Feb 20, 2026
af84cae
Outfactor subscribing in tests
XanderVertegaal Feb 20, 2026
9df972a
Deforestation
XanderVertegaal Feb 20, 2026
395824f
Fix frontend tests
XanderVertegaal Feb 20, 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
183 changes: 183 additions & 0 deletions backend/annotation/migrations/0003_labelannotation_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# Generated by Django 4.2.27 on 2026-02-01 13:27

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", "0002_label_labeling"),
]

operations = [
migrations.CreateModel(
name="LabelAnnotation",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"removed_at",
models.DateTimeField(
blank=True,
help_text="When this annotation was removed from the problem.",
null=True,
),
),
(
"notes",
models.TextField(
blank=True,
help_text="Optional notes explaining why this annotation was added or removed.",
),
),
(
"created_by",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)ss_created",
to=settings.AUTH_USER_MODEL,
),
),
(
"label",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="label_annotations",
to="annotation.label",
),
),
(
"problem",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)ss",
to="problem.problem",
),
),
(
"removed_by",
models.ForeignKey(
blank=True,
help_text="User who removed this annotation.",
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)ss_removed",
to=settings.AUTH_USER_MODEL,
),
),
(
"session",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)ss",
to="annotation.annotationsession",
),
),
],
options={
"ordering": ["-created_at"],
"permissions": [
(
"delete_own_labelannotation",
"Can remove own label annotation from problems",
),
(
"delete_any_labelannotation",
"Can remove any label annotation from problems",
),
],
"abstract": False,
},
),
migrations.RemoveField(
model_name="knowledgebaseannotation",
name="knowledge_base",
),
migrations.AddField(
model_name="knowledgebaseannotation",
name="created_by",
field=models.ForeignKey(
default=0,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)ss_created",
to=settings.AUTH_USER_MODEL,
),
preserve_default=False,
),
migrations.AddField(
model_name="knowledgebaseannotation",
name="notes",
field=models.TextField(
blank=True,
help_text="Optional notes explaining why this annotation was added or removed.",
),
),
migrations.AddField(
model_name="knowledgebaseannotation",
name="problem",
field=models.ForeignKey(
default=0,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)ss",
to="problem.problem",
),
preserve_default=False,
),
migrations.AddField(
model_name="knowledgebaseannotation",
name="removed_at",
field=models.DateTimeField(
blank=True,
help_text="When this annotation was removed from the problem.",
null=True,
),
),
migrations.AddField(
model_name="knowledgebaseannotation",
name="removed_by",
field=models.ForeignKey(
blank=True,
help_text="User who removed this annotation.",
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)ss_removed",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterField(
model_name="knowledgebaseannotation",
name="session",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)ss",
to="annotation.annotationsession",
),
),
migrations.DeleteModel(
name="Labeling",
),
migrations.AddIndex(
model_name="labelannotation",
index=models.Index(
fields=["problem", "removed_at"], name="annotation__problem_7b6d4f_idx"
),
),
migrations.AddIndex(
model_name="labelannotation",
index=models.Index(
fields=["label", "removed_at"], name="annotation__label_i_243aa0_idx"
),
),
]
126 changes: 71 additions & 55 deletions backend/annotation/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.conf import settings
from django.db import models

from problem.models import KnowledgeBase, Problem, Sentence
from problem.models import Problem, Sentence


class AnnotationSession(models.Model):
Expand Down Expand Up @@ -57,30 +57,73 @@ class ProblemAnnotation(models.Model):
created_at = models.DateTimeField(auto_now_add=True)


class KnowledgeBaseAnnotation(models.Model):
class BaseAnnotation(models.Model):
session = models.ForeignKey(
AnnotationSession,
on_delete=models.CASCADE,
related_name="kb_annotations",
related_name="%(class)ss",
)

knowledge_base = models.ForeignKey(
KnowledgeBase,
problem = models.ForeignKey(
Problem,
on_delete=models.CASCADE,
related_name="%(class)ss",
)

created_at = models.DateTimeField(auto_now_add=True)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="%(class)ss_created",
)

removed_at = models.DateTimeField(
null=True,
blank=True,
help_text="When this annotation was removed from the problem.",
)
removed_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="kb_annotations",
related_name="%(class)ss_removed",
null=True,
blank=True,
help_text="User who removed this annotation.",
)

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

class Meta:
abstract = True

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


class KnowledgeBaseAnnotation(BaseAnnotation):
class Relationship(models.TextChoices):
EQUAL = "equal", "Equal"
NOT_EQUAL = "not_equal", "Not Equal"
SUBSET = "subset", "Subset"
SUPERSET = "superset", "Superset"

entity1 = models.CharField(max_length=255)

entity2 = models.CharField(max_length=255)

relationship = models.CharField(
max_length=255,
choices=KnowledgeBase.Relationship.choices,
default=KnowledgeBase.Relationship.EQUAL,
choices=Relationship.choices,
default=Relationship.EQUAL,
)

created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
status = "active" if self.is_active() else f"removed at {self.removed_at}"
return f"KB Annotation ({self.entity1} {self.relationship} {self.entity2}) on Problem {self.problem.pk} ({status})"


class Label(models.Model):
Expand All @@ -96,68 +139,41 @@ def __str__(self):
return self.text


class Labeling(models.Model):
class LabelAnnotation(BaseAnnotation):
"""
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.
Each time a label is attached to a problem, a new LabelAnnotation 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",
related_name="label_annotations",
)

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.",
)

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

class Meta:
ordering = ["-attached_at"]
class Meta(BaseAnnotation.Meta):
ordering = ["-created_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"),
(
"delete_own_labelannotation",
"Can remove own label annotation from problems",
),
(
"delete_any_labelannotation",
"Can remove any label annotation 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})"

def is_attached_by_user(self, user) -> bool:
"""Check if this label annotation was created by the given user."""
return self.created_by == user
Loading