Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds shortcut button to add exam evaluation #2050

Merged
merged 46 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
573114d
Adds shortcut button to add exam evaluation
Oct 23, 2023
816fe4d
Made buttons look real noice. Fit to fit
Oct 23, 2023
ca803ec
PLS fix richard
Oct 30, 2023
11ca4bc
Made button work. Now need a check that the exam is not already there…
Nov 6, 2023
bcf9442
Now have success messages (oh the sweet sweet memories)
Nov 6, 2023
20b6592
Solved
Nov 20, 2023
763ad70
Fixed isort
Nov 20, 2023
1677125
Fix comments
Jan 15, 2024
8c5bc1f
Refactor exam evaluation generation
Feb 19, 2024
0352986
fix linter
Feb 19, 2024
2956e56
Added tests
Feb 19, 2024
f046757
Update exam modals to new modals
Apr 15, 2024
af94612
Add exam date field
Apr 15, 2024
36f1cad
Add exam date to test
Apr 15, 2024
7738272
Applied black
Apr 15, 2024
958fb34
Use confirmation modals now correctly
Apr 22, 2024
ed97d28
Add typing2
Apr 22, 2024
5f77e9b
Nicer user messages
Apr 29, 2024
def9fe8
Reformat
Apr 29, 2024
a79767a
Solved
May 6, 2024
9174caf
Fix
Jun 3, 2024
4d1dab8
Fix
Jun 3, 2024
1674ce4
Include strptime into bad request
Jun 3, 2024
fbac0d0
Fix
Jun 10, 2024
4d85af3
[WIP]: Start with date checker
Jul 1, 2024
8f09132
Better datechecker
Aug 12, 2024
ce74982
[WIP]: Better white space detection
Aug 12, 2024
618f23c
Working again
Aug 12, 2024
2818a36
Fixed format and lint
Aug 12, 2024
a98f963
Some minor refactors
Sep 30, 2024
d469b1c
Fix tests
FSadrieh Sep 30, 2024
77072b9
Nice things
FSadrieh Oct 7, 2024
92aa552
[WIP]: Better no database modification
FSadrieh Oct 21, 2024
82fe385
[Feat]: Better assert_no_database_modifications and exam date checking
FSadrieh Oct 21, 2024
a8d4421
Exchanged legal with possible
FSadrieh Oct 28, 2024
abbe926
Fixed has_exam
FSadrieh Oct 28, 2024
30d5311
Fixed second has_exam
FSadrieh Oct 28, 2024
bf6a95b
Remove unused import
FSadrieh Oct 29, 2024
00e2daa
Check missing exam date and wrong format exam date seperatly
FSadrieh Nov 4, 2024
66873c1
Renamed variable
FSadrieh Nov 4, 2024
995774c
More consistent naming
FSadrieh Nov 11, 2024
1001374
Some minor improvements
FSadrieh Nov 12, 2024
de25f67
Minor formating
FSadrieh Nov 18, 2024
2e5fdcb
Added test for wrong evaluation date
FSadrieh Nov 18, 2024
884b501
Add min date
FSadrieh Nov 25, 2024
c6435db
Apply message suggestions from Janno
FSadrieh Dec 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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="manager@institution.example.com",
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 @@
)


@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.")

Check warning on line 1112 in evap/staff/views.py

View check run for this annotation

Codecov / codecov/patch

evap/staff/views.py#L1111-L1112

Added lines #L1111 - L1112 were not covered by tests

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
Loading