Skip to content

Commit 5f39776

Browse files
committed
Merge branch 'main' into type-results-exporter
2 parents de9e078 + c749b9c commit 5f39776

21 files changed

+293
-108
lines changed

evap/development/fixtures/test_data.json

+76-4
Original file line numberDiff line numberDiff line change
@@ -132980,7 +132980,7 @@
132980132980
"title": "",
132981132981
"first_name_given": "",
132982132982
"first_name_chosen": "",
132983-
"last_name": "",
132983+
"last_name": "reviewer",
132984132984
"language": "",
132985132985
"is_proxy_user": false,
132986132986
"login_key": null,
@@ -133008,7 +133008,7 @@
133008133008
"title": "",
133009133009
"first_name_given": "",
133010133010
"first_name_chosen": "",
133011-
"last_name": "",
133011+
"last_name": "proxy",
133012133012
"language": "",
133013133013
"is_proxy_user": true,
133014133014
"login_key": null,
@@ -133043,7 +133043,7 @@
133043133043
"title": "",
133044133044
"first_name_given": "",
133045133045
"first_name_chosen": "",
133046-
"last_name": "",
133046+
"last_name": "proxy_delegate",
133047133047
"language": "",
133048133048
"is_proxy_user": false,
133049133049
"login_key": null,
@@ -133071,7 +133071,79 @@
133071133071
"title": "",
133072133072
"first_name_given": "",
133073133073
"first_name_chosen": "",
133074-
"last_name": "",
133074+
"last_name": "proxy_delegate_2",
133075+
"language": "",
133076+
"is_proxy_user": false,
133077+
"login_key": null,
133078+
"login_key_valid_until": null,
133079+
"is_active": true,
133080+
"notes": "",
133081+
"startpage": "DE",
133082+
"groups": [],
133083+
"user_permissions": [],
133084+
"delegates": [],
133085+
"cc_users": []
133086+
}
133087+
},
133088+
{
133089+
"model": "evaluation.userprofile",
133090+
"fields": {
133091+
"password": "eZAyFmtqHydCIFtGdbevAxiVjiRpqMtmaVUCrmkcfXdoJDigmGWPVNHeoYYyRojokKUJjsgPSPvZkjiiIHSIQlBfOKtQFDbZlPEyKnrQRrHdPtEhUYHqJauIlyIkYpBM",
133092+
"last_login": null,
133093+
"is_superuser": false,
133094+
"email": "[email protected]",
133095+
"title": "",
133096+
"first_name_given": "Vincenzo Alfredo",
133097+
"first_name_chosen": "",
133098+
"last_name": "Boston",
133099+
"language": "",
133100+
"is_proxy_user": false,
133101+
"login_key": null,
133102+
"login_key_valid_until": null,
133103+
"is_active": true,
133104+
"notes": "",
133105+
"startpage": "DE",
133106+
"groups": [],
133107+
"user_permissions": [],
133108+
"delegates": [],
133109+
"cc_users": []
133110+
}
133111+
},
133112+
{
133113+
"model": "evaluation.userprofile",
133114+
"fields": {
133115+
"password": "utAhMBbTpirVqtaoPpadEHdamaehnXWbEsliMMSnwDBYJcTnHluinAxkTeEupPoBzpuDBMYeXbpwmockMtQNYegbMuxkUBEBKqWGkOEFAWxzUFjdxevtIwYzvAgHCAwD",
133116+
"last_login": null,
133117+
"is_superuser": false,
133118+
"email": "[email protected]",
133119+
"title": "",
133120+
"first_name_given": "Bud",
133121+
"first_name_chosen": "",
133122+
"last_name": "LedBetter",
133123+
"language": "",
133124+
"is_proxy_user": false,
133125+
"login_key": null,
133126+
"login_key_valid_until": null,
133127+
"is_active": true,
133128+
"notes": "",
133129+
"startpage": "DE",
133130+
"groups": [],
133131+
"user_permissions": [],
133132+
"delegates": [],
133133+
"cc_users": []
133134+
}
133135+
},
133136+
{
133137+
"model": "evaluation.userprofile",
133138+
"fields": {
133139+
"password": "naFmzOVrFhXrVVLsIGFYceDAarTGwDRFZKGJwBvKhNFCpupezBrwhorUHsyQSpUxLFKSQuOurcIyoBBYRjARXjzcJCbqYRiKRMOwvdTqwNjAbYDhUKbopBPDYhANXUkI",
133140+
"last_login": null,
133141+
"is_superuser": false,
133142+
"email": "[email protected]",
133143+
"title": "",
133144+
"first_name_given": "Melody",
133145+
"first_name_chosen": "",
133146+
"last_name": "Large",
133075133147
"language": "",
133076133148
"is_proxy_user": false,
133077133149
"login_key": null,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 4.2.7 on 2023-11-13 20:59
2+
3+
from django.db import migrations
4+
import django_fsm
5+
6+
7+
class Migration(migrations.Migration):
8+
dependencies = [
9+
("evaluation", "0141_userprofile_notes"),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name="evaluation",
15+
name="state",
16+
field=django_fsm.FSMIntegerField(default=10, protected=True, verbose_name="state"),
17+
),
18+
]

evap/evaluation/models.py

+21-23
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
import secrets
33
import uuid
44
from collections import defaultdict
5+
from dataclasses import dataclass
56
from datetime import date, datetime, timedelta
67
from enum import Enum, auto
7-
from numbers import Number
8-
from typing import NamedTuple
8+
from numbers import Real
99

1010
from django.conf import settings
1111
from django.contrib import messages
@@ -389,7 +389,7 @@ class State:
389389
REVIEWED = 70
390390
PUBLISHED = 80
391391

392-
state = FSMIntegerField(default=State.NEW, protected=True)
392+
state = FSMIntegerField(default=State.NEW, protected=True, verbose_name=_("state"))
393393

394394
course = models.ForeignKey(Course, models.PROTECT, verbose_name=_("course"), related_name="evaluations")
395395

@@ -779,14 +779,14 @@ def unpublish(self):
779779
self._participant_count = None
780780

781781
STATE_STR_CONVERSION = {
782-
State.NEW: "new",
783-
State.PREPARED: "prepared",
784-
State.EDITOR_APPROVED: "editor_approved",
785-
State.APPROVED: "approved",
786-
State.IN_EVALUATION: "in_evaluation",
787-
State.EVALUATED: "evaluated",
788-
State.REVIEWED: "reviewed",
789-
State.PUBLISHED: "published",
782+
State.NEW: _("new"),
783+
State.PREPARED: _("prepared"),
784+
State.EDITOR_APPROVED: _("editor_approved"),
785+
State.APPROVED: _("approved"),
786+
State.IN_EVALUATION: _("in_evaluation"),
787+
State.EVALUATED: _("evaluated"),
788+
State.REVIEWED: _("reviewed"),
789+
State.PUBLISHED: _("published"),
790790
}
791791

792792
@classmethod
@@ -994,7 +994,7 @@ def unlogged_fields(self):
994994

995995
@classmethod
996996
def transform_log_action(cls, field_action):
997-
if field_action.label == "State":
997+
if field_action.label.lower() == Evaluation.state.field.verbose_name.lower():
998998
return FieldAction(
999999
field_action.label, field_action.type, [cls.state_to_str(state) for state in field_action.items]
10001000
)
@@ -1236,25 +1236,23 @@ def can_have_textanswers(self):
12361236
return self.is_text_question or self.is_rating_question and self.allows_additional_textanswers
12371237

12381238

1239-
# Let's deduplicate the fields here once mypy is smart enough to keep up with us :)
1240-
class Choices(NamedTuple):
1239+
@dataclass
1240+
class Choices:
12411241
css_class: str
1242-
values: tuple[Number]
1242+
values: tuple[Real]
12431243
colors: tuple[str]
1244-
grades: tuple[Number]
1244+
grades: tuple[Real]
12451245
names: list[StrOrPromise]
12461246
is_inverted: bool
12471247

1248+
def as_name_color_value_tuples(self):
1249+
return zip(self.names, self.colors, self.values, strict=True)
12481250

1249-
class BipolarChoices(NamedTuple):
1250-
css_class: str
1251-
values: tuple[Number]
1252-
colors: tuple[str]
1253-
grades: tuple[Number]
1254-
names: list[StrOrPromise]
1251+
1252+
@dataclass
1253+
class BipolarChoices(Choices):
12551254
plus_name: StrOrPromise
12561255
minus_name: StrOrPromise
1257-
is_inverted: bool
12581256

12591257

12601258
NO_ANSWER = 6

evap/evaluation/templates/base.html

-11
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,6 @@
4747

4848
{% if user.is_authenticated %}
4949
{% include 'notebook.html' %}
50-
51-
<div class="collapse show" tabindex="-1" id="notebookButton">
52-
<button class="btn btn-outline-secondary button-open-notebook" type="button" data-bs-toggle="collapse" data-bs-target="#notebook">
53-
<span class="fas fa-pen-to-square"></span>
54-
</button>
55-
</div>
5650
{% endif %}
5751

5852
<div class="p-3 mb-4" id="evapContent">
@@ -207,11 +201,6 @@
207201
};
208202

209203
</script>
210-
<script type="module">
211-
import { NotebookLogic } from "{% static 'js/notebook.js' %}"
212-
213-
new NotebookLogic("#notebook").attach();
214-
</script>
215204

216205
{% block additional_javascript %}{% endblock %}
217206
</body>

evap/evaluation/templates/notebook.html

+13
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
{% load static %}
12
<div class="notebook">
23
<div class="collapse position-fixed collapse-horizontal notebook-animation mt-0 d-print-none" tabindex="-1" id="notebook" aria-expanded="false">
34
<div class="card">
@@ -27,3 +28,15 @@ <h5 class="card-title m-0">
2728
</div>
2829
</div>
2930
</div>
31+
32+
<div class="collapse show" tabindex="-1" id="notebookButton">
33+
<button class="btn btn-outline-secondary button-open-notebook" type="button" data-bs-toggle="collapse" data-bs-target="#notebook">
34+
<span class="fas fa-pen-to-square"></span>
35+
</button>
36+
</div>
37+
38+
<script type="module">
39+
import { NotebookLogic } from "{% static 'js/notebook.js' %}";
40+
import { selectOrError } from "{% static 'js/utils.js' %}";
41+
new NotebookLogic(selectOrError("#notebook"), selectOrError("#notebook-form"), selectOrError("#evapContent"), selectOrError("#notebookButton"), "evap_notebook_open").attach();
42+
</script>

evap/evaluation/templatetags/evaluation_filters.py

+6-10
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from django.utils.translation import gettext_lazy as _
66

77
from evap.evaluation.models import BASE_UNIPOLAR_CHOICES, Contribution, Evaluation
8+
from evap.results.tools import RatingResult
89
from evap.rewards.tools import can_reward_points_be_used_by
910
from evap.student.forms import HeadingField
1011

@@ -76,12 +77,7 @@
7677

7778
@register.filter(name="zip")
7879
def _zip(a, b):
79-
return zip(a, b)
80-
81-
82-
@register.filter()
83-
def zip_choices(counts, choices):
84-
return zip(counts, choices.names, choices.colors, choices.values)
80+
return zip(a, b, strict=True)
8581

8682

8783
@register.filter
@@ -115,12 +111,12 @@ def percentage_one_decimal(fraction, population):
115111

116112

117113
@register.filter
118-
def to_colors(choices):
119-
if not choices:
114+
def to_colors(question_result: RatingResult | None):
115+
if question_result is None:
120116
# When displaying the course distribution, there are no associated voting choices.
121117
# In that case, we just use the colors of a unipolar scale.
122-
return BASE_UNIPOLAR_CHOICES["colors"]
123-
return choices.colors
118+
return BASE_UNIPOLAR_CHOICES["colors"][:-1]
119+
return question_result.colors
124120

125121

126122
@register.filter

evap/evaluation/tests/test_models.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,10 @@ def test_in_evaluation_to_published(self):
127127
wait_for_grade_upload_before_publishing=False,
128128
)
129129

130-
with patch(
131-
"evap.evaluation.models.EmailTemplate.send_participant_publish_notifications"
132-
) as participant_mock, patch(
133-
"evap.evaluation.models.EmailTemplate.send_contributor_publish_notifications"
134-
) as contributor_mock:
130+
with (
131+
patch("evap.evaluation.models.EmailTemplate.send_participant_publish_notifications") as participant_mock,
132+
patch("evap.evaluation.models.EmailTemplate.send_contributor_publish_notifications") as contributor_mock,
133+
):
135134
Evaluation.update_evaluations()
136135

137136
participant_mock.assert_called_once_with([evaluation])

evap/results/templates/distribution_with_grade.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<div class="distribution-bar {{ question_result.choices.css_class }}"
66
{% if question_result.question.is_bipolar_likert_question %} style="left: {{ question_result.minus_balance_count|percentage_one_decimal:question_result.count_sum }}"{% endif %}
77
>
8-
{% with colors=question_result.choices|to_colors %}
8+
{% with colors=question_result|to_colors %}
99
{% for value, color in distribution|zip:colors %}
1010
{% if value != 0 %}
1111
<div class="vote-bg-{{ color }}" style="width: {{ value|percentage_one_decimal:1 }};">&nbsp;</div>

evap/results/templates/evaluation_result_widget.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
data-bs-toggle="tooltip" data-bs-placement="left" data-fallback-placement='["left", "bottom"]'
88
title="{% spaceless %}{% include 'result_widget_tooltip.html' with question_result=course_or_evaluation.single_result_rating_result weight_info=course_or_evaluation|weight_info %}{% endspaceless %}"
99
>
10-
{% include "distribution_with_grade.html" with distribution=course_or_evaluation.distribution average=course_or_evaluation.avg_grade weight_info=course_or_evaluation|weight_info %}
10+
{% include "distribution_with_grade.html" with distribution=course_or_evaluation.distribution average=course_or_evaluation.avg_grade weight_info=course_or_evaluation|weight_info question_result=None %}
1111
</div>
1212
{% else %}
1313
<div class="d-flex" data-bs-toggle="tooltip" data-bs-placement="left" title="{% trans 'Not enough answers were given.' %}">

evap/results/templates/result_widget_tooltip.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
{% endif %}
1414
{% with question_result.question.is_bipolar_likert_question as is_bipolar %}
1515
<p>
16-
{% for count, name, color, value in question_result.counts|zip_choices:question_result.choices %}
16+
{% for count, name, color, value in question_result.zipped_choices %}
1717
<span class='vote-text-{{ color }} fas fa-fw-absolute
1818
{% if is_bipolar and value < 0 %}fa-caret-down pole-icon
1919
{% elif is_bipolar and value > 0 %}fa-caret-up pole-icon

evap/results/tools.py

+13-5
Original file line numberDiff line numberDiff line change
@@ -58,23 +58,31 @@ def is_published(cls, rating_result) -> TypeGuard["PublishedRatingResult"]:
5858
def has_answers(cls, rating_result) -> TypeGuard["AnsweredRatingResult"]:
5959
return isinstance(rating_result, AnsweredRatingResult)
6060

61-
def __init__(self, question, additional_text_result=None):
61+
def __init__(self, question, additional_text_result=None) -> None:
6262
assert question.is_rating_question
6363
self.question = discard_cached_related_objects(copy(question))
6464
self.additional_text_result = additional_text_result
65+
self.colors = tuple(
66+
color for _, color, value in self.choices.as_name_color_value_tuples() if value != NO_ANSWER
67+
)
6568

6669
@property
6770
def choices(self):
6871
return CHOICES[self.question.type]
6972

7073

7174
class PublishedRatingResult(RatingResult):
72-
def __init__(self, question, answer_counters, additional_text_result=None):
75+
def __init__(self, question, answer_counters, additional_text_result=None) -> None:
7376
super().__init__(question, additional_text_result)
74-
counts = OrderedDict((value, 0) for value in self.choices.values if value != NO_ANSWER)
77+
counts = OrderedDict(
78+
(value, [0, name, color, value]) for (name, color, value) in self.choices.as_name_color_value_tuples()
79+
)
80+
counts.pop(NO_ANSWER)
7581
for answer_counter in answer_counters:
76-
counts[answer_counter.answer] = answer_counter.count
77-
self.counts = tuple(counts.values())
82+
assert counts[answer_counter.answer][0] == 0
83+
counts[answer_counter.answer][0] = answer_counter.count
84+
self.counts = tuple(count for count, _, _, _ in counts.values())
85+
self.zipped_choices = tuple(counts.values())
7886

7987
@property
8088
def count_sum(self) -> int:

evap/settings.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767

6868
# email domains for the internal users of the hosting institution used to
6969
# figure out who is an internal user
70-
INSTITUTION_EMAIL_DOMAINS = ["institution.example.com"]
70+
INSTITUTION_EMAIL_DOMAINS = ["institution.example.com", "student.institution.example.com"]
7171

7272
# List of tuples defining email domains that should be replaced on saving UserProfiles.
7373
# Emails ending on the first value will have this part replaced by the second value.

0 commit comments

Comments
 (0)