Skip to content

Commit 25b1feb

Browse files
FSadriehFSadriehjanno42
authored andcommitted
Add shortcut button to add exam evaluation (e-valuation#2050)
Co-authored-by: FSadrieh <frederic.sadrieh@student.hpi.de> Co-authored-by: Johannes Wolf <janno42@posteo.de>
1 parent 856022e commit 25b1feb

File tree

10 files changed

+194
-3
lines changed

10 files changed

+194
-3
lines changed

deployment/localsettings.template.py

+2
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,5 @@
5353
"de": mark_safe("Deine Teilnahme am Evaluationsprojekt wird helfen. Evaluiere also <b>jetzt</b>!"),
5454
"en": mark_safe("Your participation in the evaluation helps, so evaluate <b>now</b>!"),
5555
}
56+
# Questionnaires automatically added to exam evaluations
57+
EXAM_QUESTIONNAIRE_IDS = [111]

evap/development/fixtures/test_data.json

+30
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,24 @@
656656
"is_locked": false
657657
}
658658
},
659+
{
660+
"model": "evaluation.questionnaire",
661+
"pk": 111,
662+
"fields": {
663+
"type": 10,
664+
"name_de": "Klausur",
665+
"name_en": "Exam",
666+
"description_de": "",
667+
"description_en": "",
668+
"public_name_de": "Klausur",
669+
"public_name_en": "Exam",
670+
"teaser_de": "",
671+
"teaser_en": "",
672+
"order": 62,
673+
"visibility": 1,
674+
"is_locked": false
675+
}
676+
},
659677
{
660678
"model": "evaluation.program",
661679
"pk": 1,
@@ -21784,6 +21802,18 @@
2178421802
"type": 10
2178521803
}
2178621804
},
21805+
{
21806+
"model": "evaluation.question",
21807+
"pk": 478,
21808+
"fields": {
21809+
"order": 1,
21810+
"questionnaire": 111,
21811+
"text_de": "Wie fandest du die Klausur?",
21812+
"text_en": "How did you like the exam?",
21813+
"allows_additional_textanswers": true,
21814+
"type": 6
21815+
}
21816+
},
2178721817
{
2178821818
"model": "evaluation.ratinganswercounter",
2178921819
"pk": "0009be0e-4a00-4f89-82b7-9733ff0fe35f",

evap/evaluation/models.py

+30-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from collections import defaultdict
55
from collections.abc import Collection, Container, Iterable, Sequence
66
from dataclasses import dataclass
7-
from datetime import date, datetime, timedelta
7+
from datetime import date, datetime, time, timedelta
88
from enum import Enum, auto
99
from functools import partial
1010
from numbers import Real
@@ -470,6 +470,35 @@ class State(models.IntegerChoices):
470470
verbose_name=_("wait for grade upload before publishing"), default=True
471471
)
472472

473+
@property
474+
def has_exam_evaluation(self):
475+
return self.course.evaluations.filter(name_de="Klausur", name_en="Exam").exists()
476+
477+
@property
478+
def earliest_possible_exam_date(self):
479+
return self.vote_start_datetime.date() + timedelta(days=1)
480+
481+
@transaction.atomic
482+
def create_exam_evaluation(self, exam_date: date):
483+
self.weight = 9
484+
self.vote_end_date = exam_date - timedelta(days=1)
485+
self.save()
486+
exam_evaluation = Evaluation(
487+
course=self.course,
488+
name_de="Klausur",
489+
name_en="Exam",
490+
weight=1,
491+
is_rewarded=False,
492+
vote_start_datetime=datetime.combine(exam_date + timedelta(days=1), time(8, 0)),
493+
vote_end_date=exam_date + timedelta(days=3),
494+
)
495+
exam_evaluation.save()
496+
497+
exam_evaluation.participants.set(self.participants.all())
498+
for contribution in self.contributions.exclude(contributor=None):
499+
exam_evaluation.contributions.create(contributor=contribution.contributor)
500+
exam_evaluation.general_contribution.questionnaires.set(settings.EXAM_QUESTIONNAIRE_IDS)
501+
473502
class TextAnswerReviewState(Enum):
474503
NO_TEXTANSWERS = auto()
475504
NO_REVIEW_NEEDED = auto()

evap/evaluation/tests/tools.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -188,11 +188,12 @@ def create_evaluation_with_responsible_and_editor():
188188
}
189189

190190

191-
def make_manager():
191+
def make_manager(**kwargs):
192192
return baker.make(
193193
UserProfile,
194194
email="manager@institution.example.com",
195195
groups=[Group.objects.get(name="Manager")],
196+
**kwargs,
196197
)
197198

198199

@@ -263,6 +264,7 @@ def assert_no_database_modifications(*args, **kwargs):
263264
query["sql"].startswith('INSERT INTO "testing_cache_sessions"')
264265
or query["sql"].startswith('UPDATE "testing_cache_sessions"')
265266
or query["sql"].startswith('DELETE FROM "testing_cache_sessions"')
267+
or query["sql"].startswith('UPDATE "evaluation_userprofile" SET "last_login" = ')
266268
):
267269
# These queries are caused by interacting with the test-app (self.app.get()), since that opens a session.
268270
# That's not what we want to test for here

evap/settings.py

+2
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@
107107
# Amount of hours in which participant will be warned
108108
EVALUATION_END_WARNING_PERIOD = 5
109109

110+
# Questionnaires automatically added to exam evaluations
111+
EXAM_QUESTIONNAIRE_IDS: list[int] = []
110112

111113
### Installation specific settings
112114

evap/staff/templates/staff_semester_view.html

+9-1
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,13 @@ <h3 class="m-0 me-1">{{ semester.name }}</h3>
374374
</div>
375375
</div>
376376

377+
{% for evaluation in evaluations %}
378+
{# separate forms for each modal since we want separate date-selection inputs because each exam_creation_modal needs its own exam date input field. #}
379+
<form id="exam_creation_form_{{ evaluation.id }}" reload-on-success method="post" action="{% url 'staff:create_exam_evaluation' %}">
380+
{% csrf_token %}
381+
</form>
382+
{% endfor %}
383+
377384
<form id="evaluation-deletion-form" custom-success method="POST" action="{% url 'staff:evaluation_delete' %}">
378385
{% csrf_token %}
379386
</form>
@@ -466,6 +473,7 @@ <h3 class="m-0 me-1">{{ semester.name }}</h3>
466473
{% endif %}
467474
</form>
468475
</div>
476+
469477
<div class="tab-pane" id="courses" role="tabpanel">
470478
<div class="row align-items-center mb-3">
471479
<div class="col-9">
@@ -546,7 +554,7 @@ <h3 class="m-0 me-1">{{ semester.name }}</h3>
546554
href="{% url 'staff:course_copy' course.id %}"
547555
title="{% translate 'Copy course' %}">
548556
<span class="fas fa-copy"></span>
549-
</a>
557+
</a>
550558
{% endif %}
551559
{% if course.can_be_deleted_by_manager %}
552560
<confirmation-modal type="submit" form="course-deletion-form" name="course_id" value="{{ course.id }}" confirm-button-class="btn-danger">

evap/staff/templates/staff_semester_view_evaluation.html

+19
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,25 @@
180180
<a href="{% url 'staff:evaluation_copy' evaluation.id %}" class="btn btn-sm btn-light" data-bs-toggle="tooltip" title="{% translate 'Copy' %}">
181181
<span class="fas fa-fw fa-copy"></span>
182182
</a>
183+
{% if not evaluation.has_exam_evaluation %}
184+
<confirmation-modal type="submit" name="evaluation_id" value="{{ evaluation.id }}" confirm-button-class="btn-primary" form="exam_creation_form_{{ evaluation.id }}">
185+
<span slot="title">{% translate 'Create exam evaluation' %}</span>
186+
<span slot="action-text">{% translate 'Create exam evaluation' %}</span>
187+
<span slot="question">
188+
{% blocktranslate trimmed %}
189+
Create an exam evaluation based on this evaluation. This will copy all the participants and contributors from the original evaluation. It will set the weight of the original evaluation to 9 and its end date will be set to the day before the exam.
190+
{% endblocktranslate %}
191+
</span>
192+
<div slot="extra-inputs">
193+
<label>
194+
{% translate 'Exam Date:' %}
195+
<input type="date" name="exam_date" min="{{ evaluation.earliest_possible_exam_date }}" class="form-control" required form="exam_creation_form_{{ evaluation.id }}"/>
196+
</label>
197+
</div>
198+
199+
<button slot="show-button" type="button" class="btn btn-sm btn-light" title="{% translate 'Create exam evaluation' %}" data-bs-placement="top" data-bs-toggle="tooltip"><span class="fas fa-file-pen fa-fw"></span></button>
200+
</confirmation-modal>
201+
{% endif %}
183202
{% endif %}
184203
{% if request.user.is_manager %}
185204
<a href="{% url 'staff:evaluation_email' evaluation.id %}" class="btn btn-sm btn-light" data-bs-toggle="tooltip" data-bs-placement="top" title="{% translate 'Send email' %}">

evap/staff/tests/test_views.py

+70
Original file line numberDiff line numberDiff line change
@@ -1969,6 +1969,76 @@ def test_evaluation_copy(self):
19691969
self.assertEqual(copied_evaluation.contributions.count(), 4)
19701970

19711971

1972+
@override_settings(EXAM_QUESTIONNAIRE_IDS=[111])
1973+
class TestEvaluationExamCreation(WebTestStaffMode):
1974+
csrf_checks = False
1975+
url = reverse("staff:create_exam_evaluation")
1976+
1977+
@classmethod
1978+
def setUpTestData(cls):
1979+
# We need to set the managers language to avoid a database update, when no language is set
1980+
cls.manager = make_manager(language="en")
1981+
cls.course = baker.make(Course)
1982+
vote_start_datetime = datetime.datetime.now() - datetime.timedelta(days=50)
1983+
cls.evaluation = baker.make(Evaluation, course=cls.course, vote_start_datetime=vote_start_datetime)
1984+
cls.evaluation.participants.set(baker.make(UserProfile, _quantity=3))
1985+
cls.contributions = baker.make(
1986+
Contribution, evaluation=cls.evaluation, _fill_optional=["contributor"], _quantity=3, _bulk_create=True
1987+
)
1988+
cls.exam_date = datetime.date.today() + datetime.timedelta(days=10)
1989+
cls.params = {"evaluation_id": cls.evaluation.pk, "exam_date": cls.exam_date}
1990+
cls.exam_questionnaire = baker.make(Questionnaire, pk=111)
1991+
1992+
def test_create_exam_evaluation(self):
1993+
self.app.post(self.url, user=self.manager, status=200, params=self.params)
1994+
self.assertEqual(Evaluation.objects.count(), 2)
1995+
exam_evaluation = Evaluation.objects.exclude(pk=self.evaluation.pk).get()
1996+
self.assertEqual(exam_evaluation.contributions.count(), self.evaluation.contributions.count())
1997+
self.assertEqual(
1998+
exam_evaluation.vote_start_datetime,
1999+
datetime.datetime.combine(self.exam_date + datetime.timedelta(days=1), datetime.time(8, 0)),
2000+
)
2001+
self.assertEqual(exam_evaluation.vote_end_date, self.exam_date + datetime.timedelta(days=3))
2002+
self.assertEqual(exam_evaluation.name_de, "Klausur")
2003+
self.assertEqual(exam_evaluation.name_en, "Exam")
2004+
self.assertEqual(exam_evaluation.course, self.evaluation.course)
2005+
self.assertQuerySetEqual(exam_evaluation.participants.all(), self.evaluation.participants.all())
2006+
self.assertEqual(exam_evaluation.weight, 1)
2007+
2008+
evaluation = Evaluation.objects.get(pk=self.evaluation.pk)
2009+
self.assertEqual(evaluation.weight, 9)
2010+
self.assertEqual(evaluation.vote_end_date, self.exam_date - datetime.timedelta(days=1))
2011+
2012+
def test_exam_evaluation_for_single_result(self):
2013+
self.evaluation.is_single_result = True
2014+
self.evaluation.save()
2015+
with assert_no_database_modifications():
2016+
self.app.post(self.url, user=self.manager, status=400, params=self.params)
2017+
2018+
def test_exam_evaluation_for_already_existing_exam_evaluation(self):
2019+
baker.make(Evaluation, course=self.course, name_en="Exam", name_de="Klausur")
2020+
self.assertTrue(self.evaluation.has_exam_evaluation)
2021+
with assert_no_database_modifications():
2022+
self.app.post(self.url, user=self.manager, status=400, params=self.params)
2023+
2024+
def test_exam_evaluation_with_wrong_date(self):
2025+
self.evaluation.vote_start_datetime = datetime.datetime.now() + datetime.timedelta(days=100)
2026+
self.evaluation.vote_end_date = datetime.date.today() + datetime.timedelta(days=150)
2027+
self.evaluation.save()
2028+
with assert_no_database_modifications():
2029+
self.app.post(self.url, user=self.manager, status=400, params=self.params)
2030+
2031+
def test_exam_evaluation_with_missing_date(self):
2032+
self.params.pop("exam_date")
2033+
with assert_no_database_modifications():
2034+
self.app.post(self.url, user=self.manager, status=400, params=self.params)
2035+
2036+
def test_exam_evaluation_with_wrongly_formatted_date(self):
2037+
self.params["exam_date"] = ""
2038+
with assert_no_database_modifications():
2039+
self.app.post(self.url, user=self.manager, status=400, params=self.params)
2040+
2041+
19722042
class TestCourseCopyView(WebTestStaffMode):
19732043
@classmethod
19742044
def setUpTestData(cls):

evap/staff/urls.py

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
path("evaluation/<int:evaluation_id>/copy", views.evaluation_copy, name="evaluation_copy"),
3535
path("evaluation/<int:evaluation_id>/email", views.evaluation_email, name="evaluation_email"),
3636
path("evaluation/<int:evaluation_id>/preview", views.evaluation_preview, name="evaluation_preview"),
37+
path("evaluation/create_exam_evaluation", views.create_exam_evaluation, name="create_exam_evaluation"),
3738
path("evaluation/<int:evaluation_id>/person_management", views.evaluation_person_management, name="evaluation_person_management"),
3839
path("evaluation/<int:evaluation_id>/login_key_export", views.evaluation_login_key_export, name="evaluation_login_key_export"),
3940
path("semester/<int:semester_id>/evaluation/operation", views.evaluation_operation, name="evaluation_operation"),

evap/staff/views.py

+28
Original file line numberDiff line numberDiff line change
@@ -1096,6 +1096,34 @@ def course_copy(request, course_id):
10961096
)
10971097

10981098

1099+
@require_POST
1100+
@manager_required
1101+
def create_exam_evaluation(request: HttpRequest) -> HttpResponse:
1102+
evaluation = get_object_from_dict_pk_entry_or_logged_40x(Evaluation, request.POST, "evaluation_id")
1103+
if evaluation.is_single_result:
1104+
raise SuspiciousOperation("Creating an exam evaluation for a single result evaluation is not allowed.")
1105+
1106+
if evaluation.has_exam_evaluation:
1107+
raise SuspiciousOperation("An exam evaluation already exists for this course.")
1108+
1109+
exam_date_string = request.POST.get("exam_date")
1110+
if not exam_date_string:
1111+
return HttpResponseBadRequest("Exam date missing.")
1112+
try:
1113+
exam_date = datetime.strptime(exam_date_string, "%Y-%m-%d").date()
1114+
except ValueError:
1115+
return HttpResponseBadRequest("Exam date invalid.")
1116+
1117+
if exam_date < evaluation.earliest_possible_exam_date:
1118+
raise SuspiciousOperation(
1119+
"The end date of the main evaluation would be before its start date. No exam evaluation was created."
1120+
)
1121+
1122+
evaluation.create_exam_evaluation(exam_date)
1123+
messages.success(request, _("Successfully created exam evaluation."))
1124+
return HttpResponse() # 200 OK
1125+
1126+
10991127
@manager_required
11001128
class CourseEditView(SuccessMessageMixin, UpdateView):
11011129
model = Course

0 commit comments

Comments
 (0)