Skip to content

Commit 0c1793a

Browse files
MarrekNozkaclaude
authored andcommitted
feat(review): nové škály známkování pro práce 2026+ (DMP/AP/VOŠ)
- Přidán model Category.grade_type (dmp/ap) + migrace - Nové třídy GradesDMPChoices, GradesAPChoices, DifficultyNewChoices - get_grade_config() vrací správnou škálu dle roku odevzdání a typu kategorie - Serializér dynamicky validuje hodnoty dle škály dané práce - Model fields choices rozšířeny na 1–6 (fix: grade 5 dříve zamítán jako 400) - PDF šablona používá dynamické difficulty_choices/difficulty_display z view - Frontend: nové tick labely (AP = A–F), obrácený difficulty slider pro nové práce - Barevné škály difficulty a grades dynamické dle gradeType Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 91a6fa1 commit 0c1793a

12 files changed

Lines changed: 255 additions & 28 deletions

File tree

django/apps/api/views/review.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class ReviewViewSet(ModelViewSet):
88
queryset = Review.objects.get_queryset().select_related(
99
'thesis__opponent',
1010
'thesis__supervisor',
11+
'thesis__category',
1112
).prefetch_related(
1213
'thesis__authors',
1314
)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import django.contrib.postgres.fields
2+
import django.contrib.postgres.validators
3+
import django.core.validators
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('review', '0008_auto_20231023_2110'),
11+
]
12+
13+
operations = [
14+
migrations.AlterField(
15+
model_name='review',
16+
name='grades',
17+
field=django.contrib.postgres.fields.ArrayField(
18+
base_field=models.PositiveSmallIntegerField(
19+
choices=[(1, 'A'), (2, 'B'), (3, 'C'), (4, 'D'), (5, 'E'), (6, 'F')],
20+
help_text='As value between 1 and 6 inclusive.',
21+
validators=[
22+
django.core.validators.MinValueValidator(1),
23+
django.core.validators.MaxValueValidator(6),
24+
],
25+
),
26+
size=None,
27+
validators=[
28+
django.contrib.postgres.validators.ArrayMinLengthValidator(5),
29+
django.contrib.postgres.validators.ArrayMaxLengthValidator(6),
30+
],
31+
verbose_name='Grades',
32+
),
33+
),
34+
migrations.AlterField(
35+
model_name='review',
36+
name='grade_proposal',
37+
field=models.PositiveSmallIntegerField(
38+
choices=[(1, 'A'), (2, 'B'), (3, 'C'), (4, 'D'), (5, 'E'), (6, 'F')],
39+
help_text='As value between 1 and 6 inclusive.',
40+
validators=[
41+
django.core.validators.MinValueValidator(1),
42+
django.core.validators.MaxValueValidator(6),
43+
],
44+
verbose_name='Proposed grade',
45+
),
46+
),
47+
]

django/apps/review/models/review.py

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,31 @@ class GradesChoices(IntegerChoices):
1919
VERY_WELL = 3, _('Very well')
2020
EXCELLENT = 4, _('Excellent')
2121

22+
class GradesDMPChoices(IntegerChoices):
23+
VYBORNY = 1, _('1 - výborný')
24+
CHVALITEBNY = 2, _('2 - chvalitebný')
25+
DOBRY = 3, _('3 - dobrý')
26+
DOSTATECNY = 4, _('4 - dostatečný')
27+
NEDOSTATECNY = 5, _('5 - nedostatečný')
28+
29+
class GradesAPChoices(IntegerChoices):
30+
A = 1, 'A'
31+
B = 2, 'B'
32+
C = 3, 'C'
33+
D = 4, 'D'
34+
E = 5, 'E'
35+
F = 6, 'F'
36+
2237
class DifficultyChoices(IntegerChoices):
2338
UNDER_AVERAGE = 1, _('Under average')
2439
AVERAGE = 2, _('Average')
2540
OVER_AVERAGE = 3, _('Over average')
2641

42+
class DifficultyNewChoices(IntegerChoices):
43+
OVER_AVERAGE = 1, _('Over average')
44+
AVERAGE = 2, _('Average')
45+
UNDER_AVERAGE = 3, _('Under average')
46+
2747
thesis = models.ForeignKey(
2848
to='thesis.Thesis',
2949
on_delete=models.CASCADE,
@@ -51,18 +71,18 @@ class DifficultyChoices(IntegerChoices):
5171
)
5272
grades = ArrayField(
5373
models.PositiveSmallIntegerField(
54-
validators=[MinValueValidator(1), MaxValueValidator(4)],
55-
choices=GradesChoices.choices,
56-
help_text=_('As value between 1 and 4 inclusive, higher is better.'),
74+
validators=[MinValueValidator(1), MaxValueValidator(6)],
75+
choices=GradesAPChoices.choices,
76+
help_text=_('As value between 1 and 6 inclusive.'),
5777
),
5878
validators=[ArrayMinLengthValidator(5), ArrayMaxLengthValidator(6)],
5979
verbose_name=_('Grades'),
6080
)
6181
grade_proposal = models.PositiveSmallIntegerField(
6282
verbose_name=_('Proposed grade'),
63-
validators=[MinValueValidator(1), MaxValueValidator(4)],
64-
help_text=_('As value between 1 and 4 inclusive, higher is better.'),
65-
choices=GradesChoices.choices,
83+
validators=[MinValueValidator(1), MaxValueValidator(6)],
84+
help_text=_('As value between 1 and 6 inclusive.'),
85+
choices=GradesAPChoices.choices,
6686
)
6787

6888
class Meta:
@@ -73,6 +93,30 @@ class Meta:
7393
def __str__(self):
7494
return _('Review on {} from {}').format(self.thesis, self.user)
7595

96+
GRADE_TYPE_CUTOFF_YEAR = 2026
97+
98+
@staticmethod
99+
def get_grade_config(category_grade_type, published_at):
100+
"""Return (choices_class, max_value, lower_is_better) for given category and publish date."""
101+
use_new = published_at and published_at.year >= Review.GRADE_TYPE_CUTOFF_YEAR
102+
if not use_new:
103+
return Review.GradesChoices, 4, False
104+
if category_grade_type == 'ap':
105+
return Review.GradesAPChoices, 6, True
106+
return Review.GradesDMPChoices, 5, True
107+
108+
def get_grades_choices(self):
109+
choices_class, _, _ = self.get_grade_config(
110+
self.thesis.category.grade_type, self.thesis.published_at
111+
)
112+
return choices_class
113+
114+
def get_difficulty_choices(self):
115+
use_new = self.thesis.published_at and self.thesis.published_at.year >= self.GRADE_TYPE_CUTOFF_YEAR
116+
if use_new:
117+
return self.DifficultyNewChoices
118+
return self.DifficultyChoices
119+
76120
@property
77121
def gradings(self):
78122
return tuple(filter(None, (

django/apps/review/serializers.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,25 @@ def validate(self, attrs):
5252
raise ValidationError(_('Review has been already posted by this user or this user is not allowed to post '
5353
'review for this thesis.'))
5454

55+
choices_class, max_value, _ = Review.get_grade_config(
56+
thesis.category.grade_type, thesis.published_at
57+
)
58+
valid_values = set(choices_class.values)
59+
60+
for g in attrs.get('grades', []):
61+
if g not in valid_values:
62+
raise ValidationError(
63+
_('Invalid grade value %(value)s. Allowed values: %(allowed)s.')
64+
% {'value': g, 'allowed': ', '.join(str(v) for v in sorted(valid_values))}
65+
)
66+
67+
grade_proposal = attrs.get('grade_proposal')
68+
if grade_proposal is not None and grade_proposal not in valid_values:
69+
raise ValidationError(
70+
_('Invalid grade proposal value %(value)s. Allowed values: %(allowed)s.')
71+
% {'value': grade_proposal, 'allowed': ', '.join(str(v) for v in sorted(valid_values))}
72+
)
73+
5574
return attrs
5675

5776

django/apps/review/templates/review/review_detail.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -145,22 +145,22 @@ <h1>{% trans "Thesis review" %}</h1>
145145
<td><strong>{% trans "Criteriums of thesis grading" %}</strong></td>
146146
<td>
147147
<strong>{% trans "Grade" %}</strong>
148-
<small class="right">({% for l in review.GradesChoices.labels %}{{ l.lower }}{% if not forloop.last %},
148+
<small class="right">({% for l in grades_choices.labels %}{{ l.lower }}{% if not forloop.last %},
149149
{% endif %}{% endfor %})</small>
150150
</td>
151151
</tr>
152152
<tr>
153153
<td>{% trans "Difficulty of selected topic" %}</td>
154154
<td>
155-
<strong>{{ review.get_difficulty_display }}</strong>
156-
<small class="right">({% for l in review.DifficultyChoices.labels %}{{ l.lower }}{% if not forloop.last %},
155+
<strong>{{ difficulty_display }}</strong>
156+
<small class="right">({% for l in difficulty_choices.labels %}{{ l.lower }}{% if not forloop.last %},
157157
{% endif %}{% endfor %})</small>
158158
</td>
159159
</tr>
160160
{% for grading, grade in review.gradings|zip:review.grades %}
161161
<tr{% if forloop.last %} class="row-bottom-divider"{% endif %}>
162162
<td>{{ grading }}</td>
163-
<td><strong>{{ grade|get_choices_display:review.GradesChoices }}</strong></td>
163+
<td><strong>{{ grade|get_choices_display:grades_choices }}</strong></td>
164164
</tr>
165165
{% endfor %}
166166
<tr class="row-bottom-divider comment-and-questions-row">
@@ -182,7 +182,7 @@ <h1>{% trans "Thesis review" %}</h1>
182182
</tr>
183183
<tr class="row-bottom-divider">
184184
<td>{% get_verbose_field_name review "grade_proposal" %}</td>
185-
<td>{{ review.get_grade_proposal_display }}</td>
185+
<td>{{ grade_proposal_display }}</td>
186186
</tr>
187187
<tr class="row-bottom-divider row-higher">
188188
<td>

django/apps/review/views.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ class ReviewPdfView(WeasyTemplateResponseMixin, PermissionRequiredMixin, DetailV
1212
model = Review
1313
template_name = 'review/review_detail.html'
1414

15+
def get_queryset(self):
16+
return super().get_queryset().select_related('thesis__category')
17+
1518
def has_permission(self):
1619
review: Review = self.get_object()
1720

@@ -20,3 +23,14 @@ def has_permission(self):
2023
view=self,
2124
thesis=review.thesis,
2225
)
26+
27+
def get_context_data(self, **kwargs):
28+
context = super().get_context_data(**kwargs)
29+
review = self.get_object()
30+
grades_choices = review.get_grades_choices()
31+
context['grades_choices'] = grades_choices
32+
context['grade_proposal_display'] = grades_choices(review.grade_proposal).label
33+
difficulty_choices = review.get_difficulty_choices()
34+
context['difficulty_choices'] = difficulty_choices
35+
context['difficulty_display'] = difficulty_choices(review.difficulty).label
36+
return context

django/apps/thesis/admin.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,5 @@ class ReservationAdmin(ModelAdmin):
5757

5858
@register(Category)
5959
class CategoryAdmin(ModelAdmin):
60-
pass
60+
list_display = ('title', 'order', 'grade_type')
61+
list_editable = ('grade_type',)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from django.db import migrations, models
2+
3+
4+
def set_ap_grade_type(apps, schema_editor):
5+
Category = apps.get_model('thesis', 'Category')
6+
Category.objects.filter(title__in=['Vt', 'Ve', 'Dt', 'De']).update(grade_type='ap')
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
dependencies = [
12+
('thesis', '0030_alter_thesis_registration_number'),
13+
]
14+
15+
operations = [
16+
migrations.AddField(
17+
model_name='category',
18+
name='grade_type',
19+
field=models.CharField(
20+
choices=[('dmp', 'DMP (SPŠ)'), ('ap', 'AP (VOŠ)')],
21+
default='dmp',
22+
max_length=3,
23+
verbose_name='Grade type',
24+
),
25+
),
26+
migrations.RunPython(set_ap_grade_type, migrations.RunPython.noop),
27+
]

django/apps/thesis/models/category.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77

88
class Category(BaseModel):
99
"""Category of submitted thesis."""
10+
11+
class GradeType(models.TextChoices):
12+
DMP = 'dmp', _('DMP (SPŠ)')
13+
AP = 'ap', _('AP (VOŠ)')
14+
1015
title = models.CharField(
1116
verbose_name=_('Title'),
1217
max_length=128,
@@ -16,6 +21,13 @@ class Category(BaseModel):
1621
verbose_name=_('Order'),
1722
)
1823

24+
grade_type = models.CharField(
25+
max_length=3,
26+
choices=GradeType.choices,
27+
default=GradeType.DMP,
28+
verbose_name=_('Grade type'),
29+
)
30+
1931
class Meta:
2032
ordering = ['order', 'title']
2133
verbose_name = _('Category')

django/apps/thesis/serializers/category.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class Meta:
1010
fields = (
1111
'id',
1212
'title',
13+
'grade_type',
1314
)
1415

1516

@@ -23,4 +24,5 @@ class Meta:
2324
'id',
2425
'value',
2526
'text',
27+
'grade_type',
2628
)

0 commit comments

Comments
 (0)