Skip to content

Commit dc23d3d

Browse files
authored
Merge pull request #301 from spseol/fix-grades
2 parents 4d707f2 + 8ed7a27 commit dc23d3d

22 files changed

Lines changed: 293 additions & 49 deletions

File tree

.env.local.template

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ ALLOWED_HOSTS=domain.com www.domain.com
66
SECRET_KEY=...
77

88
# for ldap remote login
9+
# LDAP_DISABLE=True
910
LDAP_HOST=localhost
1011
LDAP_PORT=389
1112
LDAP_USERNAME=user

django/.env.base

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
THESAURUS_VERSION=1.0.3
1+
THESAURUS_VERSION=1.1.0
22

33
SQL_ENGINE=django.db.backends.postgresql
44
SQL_DATABASE=thesaurus

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
)

django/apps/review/admin.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
11
from django.contrib.admin import ModelAdmin, register, RelatedOnlyFieldListFilter
2+
from django.utils.translation import gettext_lazy as _
23

34
from apps.review.models import Review
45

56

67
@register(Review)
78
class ReviewAdmin(ModelAdmin):
8-
date_hierarchy = 'thesis__published_at'
9-
list_display = ['thesis', 'user', 'created', 'grade_proposal']
9+
date_hierarchy = "thesis__published_at"
10+
list_display = ["thesis", "user", "created", "grade_proposal_display"]
1011

1112
list_filter = (
12-
('user', RelatedOnlyFieldListFilter),
13-
'grade_proposal',
13+
("user", RelatedOnlyFieldListFilter),
14+
"grade_proposal",
1415
)
16+
17+
@staticmethod
18+
def grade_proposal_display(obj: Review) -> str:
19+
choices_class = obj.get_grades_choices()
20+
try:
21+
return choices_class(obj.grade_proposal).label
22+
except ValueError:
23+
return str(obj.grade_proposal)
24+
25+
grade_proposal_display.short_description = _("Proposed grade")
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: 51 additions & 7 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,
@@ -45,24 +65,24 @@ class DifficultyChoices(IntegerChoices):
4565
)
4666
difficulty = models.PositiveSmallIntegerField(
4767
verbose_name=_('Difficulty'),
48-
help_text=_('As value between 1 and 3 inclusive, higher is harder.'),
68+
help_text=_('As value between 1 and 3 inclusive.'),
4969
validators=[MinValueValidator(1), MaxValueValidator(3)],
5070
choices=DifficultyChoices.choices,
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',)

0 commit comments

Comments
 (0)