Skip to content

Commit cf4a202

Browse files
Replace manual serialization with ModelViewSet
1 parent a826af9 commit cf4a202

9 files changed

Lines changed: 133 additions & 102 deletions

File tree

backend/langpro_annotator/urls.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@
2121

2222
from rest_framework import routers
2323

24+
from problem.views.problem import ProblemView
25+
2426
from .index import index
2527
from .proxy_frontend import proxy_frontend
2628
from .i18n import i18n
2729

2830
api_router = routers.DefaultRouter() # register viewsets with this router
31+
api_router.register(r"problem", ProblemView, basename="problem")
2932

3033

3134
if settings.PROXY_FRONTEND:

backend/problem/models.py

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -57,33 +57,6 @@ def get_index(self, qs: QuerySet) -> int | None:
5757
logger.exception(f"Error getting index for problem {self.pk}: {e}")
5858
return None
5959

60-
def serialize(self) -> dict:
61-
"""
62-
Serialize the Problem instance to a dictionary.
63-
"""
64-
65-
match self.dataset:
66-
case self.Dataset.SICK:
67-
serialized_extra_data = SickData.serialize(self.extra_data)
68-
case self.Dataset.FRACAS:
69-
serialized_extra_data = FracasData.serialize(self.extra_data)
70-
case self.Dataset.SNLI:
71-
serialized_extra_data = SNLIData.serialize(self.extra_data)
72-
case _:
73-
serialized_extra_data = {}
74-
75-
kb_items = self.knowledge_bases.all() # type: ignore
76-
77-
return {
78-
"id": self.pk,
79-
"dataset": self.dataset,
80-
"premises": [premise.text for premise in self.premises.all()],
81-
"hypothesis": self.hypothesis.text,
82-
"entailmentLabel": self.entailment_label,
83-
"extraData": serialized_extra_data,
84-
"kbItems": [item.serialize() for item in kb_items],
85-
}
86-
8760

8861
class KnowledgeBase(models.Model):
8962
class Relationship(models.TextChoices):

backend/problem/problem_details.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from typing import Optional
21
from dataclasses import dataclass
32

43
from django.http import QueryDict

backend/problem/serializers.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from rest_framework import serializers
2+
from problem.services import FracasData, SNLIData, SickData
23
from problem.models import Problem, KnowledgeBase
34

45

@@ -21,6 +22,56 @@ def validate_id(self, value):
2122
return value
2223

2324

25+
class ProblemSerializer(serializers.ModelSerializer):
26+
"""
27+
Serializer for Problem model output.
28+
Handles serialization of problems with all related data including labels.
29+
"""
30+
31+
premises = serializers.SerializerMethodField()
32+
hypothesis = serializers.SerializerMethodField()
33+
entailmentLabel = serializers.CharField(source="entailment_label")
34+
extraData = serializers.SerializerMethodField()
35+
kbItems = serializers.SerializerMethodField()
36+
37+
class Meta:
38+
model = Problem
39+
fields = [
40+
"id",
41+
"dataset",
42+
"premises",
43+
"hypothesis",
44+
"entailmentLabel",
45+
"extraData",
46+
"kbItems",
47+
]
48+
49+
def get_premises(self, problem):
50+
"""Get list of premise texts."""
51+
return [premise.text for premise in problem.premises.all()]
52+
53+
def get_hypothesis(self, problem):
54+
"""Get hypothesis text."""
55+
return problem.hypothesis.text
56+
57+
def get_extraData(self, problem):
58+
"""Get dataset-specific extra data."""
59+
match problem.dataset:
60+
case Problem.Dataset.SICK:
61+
return SickData.serialize(problem.extra_data)
62+
case Problem.Dataset.FRACAS:
63+
return FracasData.serialize(problem.extra_data)
64+
case Problem.Dataset.SNLI:
65+
return SNLIData.serialize(problem.extra_data)
66+
case _:
67+
return {}
68+
69+
def get_kbItems(self, problem):
70+
"""Get knowledge base items."""
71+
kb_items = problem.knowledge_bases.all()
72+
return KnowledgeBaseSerializer(kb_items, many=True).data
73+
74+
2475
class ProblemInputSerializer(serializers.Serializer):
2576
"""
2677
Serializer for validating problem input data.

backend/problem/urls.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
from django.urls import path
22

33
from problem.views.parse import ParseView
4-
from problem.views.problem import ProblemView
5-
64

75
urlpatterns = [
8-
path("<int:problem_id>", ProblemView.as_view(), name="problem_detail_view"),
96
path("parse", ParseView.as_view(), name="parse_view"),
10-
path("", ProblemView.as_view(), name="problem_view"),
117
]

backend/problem/views/problem.py

Lines changed: 59 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,18 @@
22

33
from django.db import DatabaseError
44
from django.http import JsonResponse
5-
from rest_framework.views import APIView
5+
from rest_framework.decorators import action
66
from rest_framework.request import Request
7+
from rest_framework.response import Response
8+
from rest_framework.viewsets import ModelViewSet
79

810
from langpro_annotator.logger import logger
9-
from problem.problem_details import get_filters, get_related_problem_ids
11+
from problem.problem_details import (
12+
get_filters,
13+
get_related_problem_ids,
14+
)
1015
from problem.models import KnowledgeBase, Problem, Sentence
11-
from problem.serializers import ProblemInputSerializer
12-
13-
14-
@dataclass
15-
class ProblemResponse:
16-
problem: Problem | None = None
17-
index: int | None = None
18-
error: str | None = None
19-
20-
first: int | None = None
21-
previous: int | None = None
22-
next: int | None = None
23-
last: int | None = None
24-
total: int | None = None
25-
26-
def json_response(self, status=200) -> JsonResponse:
27-
return JsonResponse(
28-
{
29-
"index": self.index,
30-
"problem": self.problem.serialize() if self.problem else None,
31-
"error": self.error,
32-
"firstProblemId": self.first,
33-
"previousProblemId": self.previous,
34-
"nextProblemId": self.next,
35-
"lastProblemId": self.last,
36-
"totalProblems": self.total if self.total is not None else 0,
37-
},
38-
status=status,
39-
)
40-
16+
from problem.serializers import ProblemSerializer
4117

4218
@dataclass
4319
class SaveProblemResponse:
@@ -48,23 +24,54 @@ def json_response(self, status=200) -> JsonResponse:
4824
return JsonResponse(asdict(self), status=status)
4925

5026

51-
class ProblemView(APIView):
52-
def get(self, request: Request, problem_id: int | None = None) -> JsonResponse:
27+
class ProblemView(ModelViewSet):
28+
queryset = Problem.objects.all()
29+
serializer_class = ProblemSerializer
30+
31+
def list(self, request: Request) -> Response:
32+
"""
33+
Lists all Problems in the database, with optional filtering.
34+
"""
35+
filters = get_filters(request.query_params)
36+
37+
qs = self.get_queryset()
38+
39+
if filters is not None:
40+
qs = qs.filter(filters)
41+
42+
serializer = self.get_serializer(qs, many=True)
43+
return Response(serializer.data, status=200)
44+
45+
46+
@action(detail=False, methods=['get'], url_path='first')
47+
def first(self, request: Request) -> Response:
48+
"""
49+
Retrieves the first problem from the queryset.
50+
"""
51+
return self._get_problem_response(request, pk=None)
52+
53+
def retrieve(self, request: Request, pk: int | None = None) -> Response:
54+
"""
55+
Retrieves the requested Problem by ID.
56+
"""
57+
return self._get_problem_response(request, pk=pk)
58+
59+
def _get_problem_response(self, request: Request, pk: int | None) -> Response:
5360
"""
54-
If a Problem ID is provided, retrieves the requested Problem.
55-
Otherwise, simply returns the first problem of the QS.
61+
Helper method to build the problem response.
62+
If pk is provided, retrieves that problem; otherwise returns the first problem.
5663
"""
5764
filters = get_filters(request.query_params)
5865

59-
qs = Problem.objects.all()
66+
qs = self.get_queryset()
6067

6168
if filters is not None:
6269
qs = qs.filter(filters)
6370

6471
problem = None
65-
if problem_id is not None:
72+
if pk is not None:
6673
try:
67-
problem = qs.get(id=problem_id)
74+
problem = qs.get(id=pk)
6875
except Problem.DoesNotExist:
6976
# The selected problem may not be part of the selected filters.
7077
# In that case, we simply take the first problem from the queryset.
@@ -74,17 +81,19 @@ def get(self, request: Request, problem_id: int | None = None) -> JsonResponse:
7481
problem = qs.first()
7582

7683
problem_index = problem.get_index(qs) if problem else None
77-
related_problem_ids = get_related_problem_ids(qs, problem_id)
78-
79-
return ProblemResponse(
80-
problem=problem,
81-
index=problem_index,
82-
first=related_problem_ids.first,
83-
previous=related_problem_ids.previous,
84-
next=related_problem_ids.next,
85-
last=related_problem_ids.last,
86-
total=related_problem_ids.total,
87-
).json_response(status=200)
84+
related_problem_ids = get_related_problem_ids(qs, pk)
85+
86+
serializer = self.get_serializer(problem)
87+
88+
return Response({
89+
"problem": serializer.data,
90+
"index": problem_index,
91+
"first": related_problem_ids.first,
92+
"previous": related_problem_ids.previous,
93+
"next": related_problem_ids.next,
94+
"last": related_problem_ids.last,
95+
"total": related_problem_ids.total,
96+
}, status=200)
8897

8998
def post(self, request: Request, problem_id: int | None = None) -> JsonResponse:
9099
"""

frontend/src/app/annotate/navigator/navigator.component.html

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
<button
77
class="btn btn-outline-primary btn-sm"
88
type="button"
9-
(click)="navigateToProblem(problemResponse?.firstProblemId)"
10-
[disabled]="problemResponse?.problem?.id === problemResponse?.firstProblemId"
9+
(click)="navigateToProblem(problemResponse?.first)"
10+
[disabled]="problemResponse?.problem?.id === problemResponse?.first"
1111
aria-labelledby="label-first"
1212
title="First problem"
1313
title-i18n
@@ -24,8 +24,8 @@
2424
<button
2525
class="btn btn-outline-primary btn-sm"
2626
type="button"
27-
(click)="navigateToProblem(problemResponse?.previousProblemId)"
28-
[disabled]="problemResponse?.previousProblemId === null"
27+
(click)="navigateToProblem(problemResponse?.previous)"
28+
[disabled]="problemResponse?.previous === null"
2929
aria-labelledby="label-previous"
3030
title="Previous problem"
3131
title-i18n
@@ -47,16 +47,16 @@
4747
{{ problemResponse?.index ?? '-' }}
4848
</span>
4949
<hr class="my-1">
50-
<span class="small" i18n>{{ problemResponse.totalProblems }}</span>
50+
<span class="small" i18n>{{ problemResponse.total }}</span>
5151
}
5252
</div>
5353

5454
<div class="responsive-button-grid">
5555
<button
5656
class="btn btn-outline-primary btn-sm"
5757
type="button"
58-
(click)="navigateToProblem(problemResponse?.nextProblemId)"
59-
[disabled]="problemResponse?.nextProblemId === null"
58+
(click)="navigateToProblem(problemResponse?.next)"
59+
[disabled]="problemResponse?.next === null"
6060
title="Next problem"
6161
title-i18n
6262
>
@@ -69,8 +69,8 @@
6969
</button>
7070
<button
7171
class="btn btn-outline-primary btn-sm"
72-
(click)="navigateToProblem(problemResponse?.lastProblemId)"
73-
[disabled]="problemResponse?.problem?.id === problemResponse?.lastProblemId"
72+
(click)="navigateToProblem(problemResponse?.last)"
73+
[disabled]="problemResponse?.problem?.id === problemResponse?.last"
7474
title="Last problem"
7575
title-i18n
7676
>

frontend/src/app/services/problem.service.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,11 @@ export class ProblemService {
7676
private newProblem$(): Observable<ProblemResponse> {
7777
return of<ProblemResponse>({
7878
index: null,
79-
firstProblemId: null,
80-
lastProblemId: null,
81-
nextProblemId: null,
82-
previousProblemId: null,
83-
totalProblems: 0,
79+
first: null,
80+
last: null,
81+
next: null,
82+
previous: null,
83+
total: 0,
8484
error: null,
8585
problem: {
8686
id: null,
@@ -98,7 +98,7 @@ export class ProblemService {
9898
private existingProblem$(problemId?: string, queryParams?: ParamMap): Observable<ProblemResponse | null> {
9999
const httpParams = queryParams ? this.extractSearchParams(queryParams) : undefined;
100100

101-
return this.http.get<ProblemResponse>(`/api/problem/${problemId ?? ""}`, { params: httpParams }).pipe(
101+
return this.http.get<ProblemResponse>(`/api/problem/${problemId ?? "first"}/`, { params: httpParams }).pipe(
102102
catchError((error) => {
103103
const message = `Error fetching ${problemId ? `problem ${problemId}` : "first problem"}`;
104104
console.error(message, error);

frontend/src/app/types.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,11 @@ interface BaseResponse {
7474
export interface ProblemResponse extends BaseResponse {
7575
index: number | null;
7676
problem: Problem | null;
77-
firstProblemId: number | null;
78-
previousProblemId: number | null;
79-
nextProblemId: number | null;
80-
lastProblemId: number | null;
81-
totalProblems: number;
77+
first: number | null;
78+
previous: number | null;
79+
next: number | null;
80+
last: number | null;
81+
total: number;
8282
}
8383

8484
export interface SaveProblemResponse extends BaseResponse {

0 commit comments

Comments
 (0)