Skip to content

Commit

Permalink
Add shortcut button to add exam evaluation (#2050)
Browse files Browse the repository at this point in the history
Co-authored-by: FSadrieh <[email protected]>
Co-authored-by: Johannes Wolf <[email protected]>
  • Loading branch information
3 people authored Dec 16, 2024
1 parent 2c5f711 commit 80bd1c8
Show file tree
Hide file tree
Showing 10 changed files with 194 additions and 3 deletions.
2 changes: 2 additions & 0 deletions deployment/localsettings.template.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,5 @@
"de": mark_safe("Deine Teilnahme am Evaluationsprojekt wird helfen. Evaluiere also <b>jetzt</b>!"),
"en": mark_safe("Your participation in the evaluation helps, so evaluate <b>now</b>!"),
}
# Questionnaires automatically added to exam evaluations
EXAM_QUESTIONNAIRE_IDS = [111]
30 changes: 30 additions & 0 deletions evap/development/fixtures/test_data.json
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,24 @@
"is_locked": false
}
},
{
"model": "evaluation.questionnaire",
"pk": 111,
"fields": {
"type": 10,
"name_de": "Klausur",
"name_en": "Exam",
"description_de": "",
"description_en": "",
"public_name_de": "Klausur",
"public_name_en": "Exam",
"teaser_de": "",
"teaser_en": "",
"order": 62,
"visibility": 1,
"is_locked": false
}
},
{
"model": "evaluation.program",
"pk": 1,
Expand Down Expand Up @@ -21784,6 +21802,18 @@
"type": 10
}
},
{
"model": "evaluation.question",
"pk": 478,
"fields": {
"order": 1,
"questionnaire": 111,
"text_de": "Wie fandest du die Klausur?",
"text_en": "How did you like the exam?",
"allows_additional_textanswers": true,
"type": 6
}
},
{
"model": "evaluation.ratinganswercounter",
"pk": "0009be0e-4a00-4f89-82b7-9733ff0fe35f",
Expand Down
31 changes: 30 additions & 1 deletion evap/evaluation/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from collections import defaultdict
from collections.abc import Collection, Container, Iterable, Sequence
from dataclasses import dataclass
from datetime import date, datetime, timedelta
from datetime import date, datetime, time, timedelta
from enum import Enum, auto
from functools import partial
from numbers import Real
Expand Down Expand Up @@ -470,6 +470,35 @@ class State(models.IntegerChoices):
verbose_name=_("wait for grade upload before publishing"), default=True
)

@property
def has_exam_evaluation(self):
return self.course.evaluations.filter(name_de="Klausur", name_en="Exam").exists()

@property
def earliest_possible_exam_date(self):
return self.vote_start_datetime.date() + timedelta(days=1)

@transaction.atomic
def create_exam_evaluation(self, exam_date: date):
self.weight = 9
self.vote_end_date = exam_date - timedelta(days=1)
self.save()
exam_evaluation = Evaluation(
course=self.course,
name_de="Klausur",
name_en="Exam",
weight=1,
is_rewarded=False,
vote_start_datetime=datetime.combine(exam_date + timedelta(days=1), time(8, 0)),
vote_end_date=exam_date + timedelta(days=3),
)
exam_evaluation.save()

exam_evaluation.participants.set(self.participants.all())
for contribution in self.contributions.exclude(contributor=None):
exam_evaluation.contributions.create(contributor=contribution.contributor)
exam_evaluation.general_contribution.questionnaires.set(settings.EXAM_QUESTIONNAIRE_IDS)

class TextAnswerReviewState(Enum):
NO_TEXTANSWERS = auto()
NO_REVIEW_NEEDED = auto()
Expand Down
4 changes: 3 additions & 1 deletion evap/evaluation/tests/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,11 +188,12 @@ def create_evaluation_with_responsible_and_editor():
}


def make_manager():
def make_manager(**kwargs):
return baker.make(
UserProfile,
email="[email protected]",
groups=[Group.objects.get(name="Manager")],
**kwargs,
)


Expand Down Expand Up @@ -263,6 +264,7 @@ def assert_no_database_modifications(*args, **kwargs):
query["sql"].startswith('INSERT INTO "testing_cache_sessions"')
or query["sql"].startswith('UPDATE "testing_cache_sessions"')
or query["sql"].startswith('DELETE FROM "testing_cache_sessions"')
or query["sql"].startswith('UPDATE "evaluation_userprofile" SET "last_login" = ')
):
# These queries are caused by interacting with the test-app (self.app.get()), since that opens a session.
# That's not what we want to test for here
Expand Down
2 changes: 2 additions & 0 deletions evap/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@
# Amount of hours in which participant will be warned
EVALUATION_END_WARNING_PERIOD = 5

# Questionnaires automatically added to exam evaluations
EXAM_QUESTIONNAIRE_IDS: list[int] = []

### Installation specific settings

Expand Down
10 changes: 9 additions & 1 deletion evap/staff/templates/staff_semester_view.html
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,13 @@ <h3 class="m-0 me-1">{{ semester.name }}</h3>
</div>
</div>

{% for evaluation in evaluations %}
{# separate forms for each modal since we want separate date-selection inputs because each exam_creation_modal needs its own exam date input field. #}
<form id="exam_creation_form_{{ evaluation.id }}" reload-on-success method="post" action="{% url 'staff:create_exam_evaluation' %}">
{% csrf_token %}
</form>
{% endfor %}

<form id="evaluation-deletion-form" custom-success method="POST" action="{% url 'staff:evaluation_delete' %}">
{% csrf_token %}
</form>
Expand Down Expand Up @@ -466,6 +473,7 @@ <h3 class="m-0 me-1">{{ semester.name }}</h3>
{% endif %}
</form>
</div>

<div class="tab-pane" id="courses" role="tabpanel">
<div class="row align-items-center mb-3">
<div class="col-9">
Expand Down Expand Up @@ -546,7 +554,7 @@ <h3 class="m-0 me-1">{{ semester.name }}</h3>
href="{% url 'staff:course_copy' course.id %}"
title="{% translate 'Copy course' %}">
<span class="fas fa-copy"></span>
</a>
</a>
{% endif %}
{% if course.can_be_deleted_by_manager %}
<confirmation-modal type="submit" form="course-deletion-form" name="course_id" value="{{ course.id }}" confirm-button-class="btn-danger">
Expand Down
19 changes: 19 additions & 0 deletions evap/staff/templates/staff_semester_view_evaluation.html
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,25 @@
<a href="{% url 'staff:evaluation_copy' evaluation.id %}" class="btn btn-sm btn-light" data-bs-toggle="tooltip" title="{% translate 'Copy' %}">
<span class="fas fa-fw fa-copy"></span>
</a>
{% if not evaluation.has_exam_evaluation %}
<confirmation-modal type="submit" name="evaluation_id" value="{{ evaluation.id }}" confirm-button-class="btn-primary" form="exam_creation_form_{{ evaluation.id }}">
<span slot="title">{% translate 'Create exam evaluation' %}</span>
<span slot="action-text">{% translate 'Create exam evaluation' %}</span>
<span slot="question">
{% blocktranslate trimmed %}
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.
{% endblocktranslate %}
</span>
<div slot="extra-inputs">
<label>
{% translate 'Exam Date:' %}
<input type="date" name="exam_date" min="{{ evaluation.earliest_possible_exam_date }}" class="form-control" required form="exam_creation_form_{{ evaluation.id }}"/>
</label>
</div>

<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>
</confirmation-modal>
{% endif %}
{% endif %}
{% if request.user.is_manager %}
<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' %}">
Expand Down
70 changes: 70 additions & 0 deletions evap/staff/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1960,6 +1960,76 @@ def test_evaluation_copy(self):
self.assertEqual(copied_evaluation.contributions.count(), 4)


@override_settings(EXAM_QUESTIONNAIRE_IDS=[111])
class TestEvaluationExamCreation(WebTestStaffMode):
csrf_checks = False
url = reverse("staff:create_exam_evaluation")

@classmethod
def setUpTestData(cls):
# We need to set the managers language to avoid a database update, when no language is set
cls.manager = make_manager(language="en")
cls.course = baker.make(Course)
vote_start_datetime = datetime.datetime.now() - datetime.timedelta(days=50)
cls.evaluation = baker.make(Evaluation, course=cls.course, vote_start_datetime=vote_start_datetime)
cls.evaluation.participants.set(baker.make(UserProfile, _quantity=3))
cls.contributions = baker.make(
Contribution, evaluation=cls.evaluation, _fill_optional=["contributor"], _quantity=3, _bulk_create=True
)
cls.exam_date = datetime.date.today() + datetime.timedelta(days=10)
cls.params = {"evaluation_id": cls.evaluation.pk, "exam_date": cls.exam_date}
cls.exam_questionnaire = baker.make(Questionnaire, pk=111)

def test_create_exam_evaluation(self):
self.app.post(self.url, user=self.manager, status=200, params=self.params)
self.assertEqual(Evaluation.objects.count(), 2)
exam_evaluation = Evaluation.objects.exclude(pk=self.evaluation.pk).get()
self.assertEqual(exam_evaluation.contributions.count(), self.evaluation.contributions.count())
self.assertEqual(
exam_evaluation.vote_start_datetime,
datetime.datetime.combine(self.exam_date + datetime.timedelta(days=1), datetime.time(8, 0)),
)
self.assertEqual(exam_evaluation.vote_end_date, self.exam_date + datetime.timedelta(days=3))
self.assertEqual(exam_evaluation.name_de, "Klausur")
self.assertEqual(exam_evaluation.name_en, "Exam")
self.assertEqual(exam_evaluation.course, self.evaluation.course)
self.assertQuerySetEqual(exam_evaluation.participants.all(), self.evaluation.participants.all())
self.assertEqual(exam_evaluation.weight, 1)

evaluation = Evaluation.objects.get(pk=self.evaluation.pk)
self.assertEqual(evaluation.weight, 9)
self.assertEqual(evaluation.vote_end_date, self.exam_date - datetime.timedelta(days=1))

def test_exam_evaluation_for_single_result(self):
self.evaluation.is_single_result = True
self.evaluation.save()
with assert_no_database_modifications():
self.app.post(self.url, user=self.manager, status=400, params=self.params)

def test_exam_evaluation_for_already_existing_exam_evaluation(self):
baker.make(Evaluation, course=self.course, name_en="Exam", name_de="Klausur")
self.assertTrue(self.evaluation.has_exam_evaluation)
with assert_no_database_modifications():
self.app.post(self.url, user=self.manager, status=400, params=self.params)

def test_exam_evaluation_with_wrong_date(self):
self.evaluation.vote_start_datetime = datetime.datetime.now() + datetime.timedelta(days=100)
self.evaluation.vote_end_date = datetime.date.today() + datetime.timedelta(days=150)
self.evaluation.save()
with assert_no_database_modifications():
self.app.post(self.url, user=self.manager, status=400, params=self.params)

def test_exam_evaluation_with_missing_date(self):
self.params.pop("exam_date")
with assert_no_database_modifications():
self.app.post(self.url, user=self.manager, status=400, params=self.params)

def test_exam_evaluation_with_wrongly_formatted_date(self):
self.params["exam_date"] = ""
with assert_no_database_modifications():
self.app.post(self.url, user=self.manager, status=400, params=self.params)


class TestCourseCopyView(WebTestStaffMode):
@classmethod
def setUpTestData(cls):
Expand Down
1 change: 1 addition & 0 deletions evap/staff/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
path("evaluation/<int:evaluation_id>/copy", views.evaluation_copy, name="evaluation_copy"),
path("evaluation/<int:evaluation_id>/email", views.evaluation_email, name="evaluation_email"),
path("evaluation/<int:evaluation_id>/preview", views.evaluation_preview, name="evaluation_preview"),
path("evaluation/create_exam_evaluation", views.create_exam_evaluation, name="create_exam_evaluation"),
path("evaluation/<int:evaluation_id>/person_management", views.evaluation_person_management, name="evaluation_person_management"),
path("evaluation/<int:evaluation_id>/login_key_export", views.evaluation_login_key_export, name="evaluation_login_key_export"),
path("semester/<int:semester_id>/evaluation/operation", views.evaluation_operation, name="evaluation_operation"),
Expand Down
28 changes: 28 additions & 0 deletions evap/staff/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1093,6 +1093,34 @@ def course_copy(request, course_id):
)


@require_POST
@manager_required
def create_exam_evaluation(request: HttpRequest) -> HttpResponse:
evaluation = get_object_from_dict_pk_entry_or_logged_40x(Evaluation, request.POST, "evaluation_id")
if evaluation.is_single_result:
raise SuspiciousOperation("Creating an exam evaluation for a single result evaluation is not allowed.")

if evaluation.has_exam_evaluation:
raise SuspiciousOperation("An exam evaluation already exists for this course.")

exam_date_string = request.POST.get("exam_date")
if not exam_date_string:
return HttpResponseBadRequest("Exam date missing.")
try:
exam_date = datetime.strptime(exam_date_string, "%Y-%m-%d").date()
except ValueError:
return HttpResponseBadRequest("Exam date invalid.")

if exam_date < evaluation.earliest_possible_exam_date:
raise SuspiciousOperation(
"The end date of the main evaluation would be before its start date. No exam evaluation was created."
)

evaluation.create_exam_evaluation(exam_date)
messages.success(request, _("Successfully created exam evaluation."))
return HttpResponse() # 200 OK


@manager_required
class CourseEditView(SuccessMessageMixin, UpdateView):
model = Course
Expand Down

0 comments on commit 80bd1c8

Please sign in to comment.