Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
70 changes: 68 additions & 2 deletions course/exam.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@
from django.core.exceptions import (
ObjectDoesNotExist,
PermissionDenied,
SuspiciousOperation,
)
from django.db import transaction
from django.db.models import Q
from django.shortcuts import redirect, render
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.html import escape
from django.utils.safestring import mark_safe
Expand Down Expand Up @@ -66,6 +67,7 @@
HTML5DateTimeInput,
RelateHttpRequest,
StyledForm,
StyledModelForm,
is_authed,
string_concat,
)
Expand Down Expand Up @@ -927,7 +929,71 @@
# }}}


# {{{ lockdown context processor
# {{{ edit exam

class EditExamForm(StyledModelForm):
def __init__(self, add_new: bool, *args, **kwargs) -> None:

Check warning on line 935 in course/exam.py

View workflow job for this annotation

GitHub Actions / Lint and typecheck Python

Type annotation is missing for parameter "kwargs" (reportMissingParameterType)

Check warning on line 935 in course/exam.py

View workflow job for this annotation

GitHub Actions / Lint and typecheck Python

Type of parameter "kwargs" is unknown (reportUnknownParameterType)

Check warning on line 935 in course/exam.py

View workflow job for this annotation

GitHub Actions / Lint and typecheck Python

Type annotation is missing for parameter "args" (reportMissingParameterType)

Check warning on line 935 in course/exam.py

View workflow job for this annotation

GitHub Actions / Lint and typecheck Python

Type of parameter "args" is unknown (reportUnknownParameterType)
super().__init__(*args, **kwargs)

Check warning on line 936 in course/exam.py

View workflow job for this annotation

GitHub Actions / Lint and typecheck Python

Argument type is unknown   Argument corresponds to parameter "kwargs" in function "__init__" (reportUnknownArgumentType)

Check warning on line 936 in course/exam.py

View workflow job for this annotation

GitHub Actions / Lint and typecheck Python

Argument type is unknown   Argument corresponds to parameter "args" in function "__init__" (reportUnknownArgumentType)

Check warning on line 936 in course/exam.py

View workflow job for this annotation

GitHub Actions / Lint and typecheck Python

Type of "__init__" is partially unknown   Type of "__init__" is "(...) -> None" (reportUnknownMemberType)

self.fields["course"].disabled = True

self.helper.add_input(

Check warning on line 940 in course/exam.py

View workflow job for this annotation

GitHub Actions / Lint and typecheck Python

Type of "add_input" is partially unknown   Type of "add_input" is "(input_object: Unknown) -> None" (reportUnknownMemberType)
Submit("submit", _("Create") if add_new else _("Update")))

class Meta:
model = Exam

Check warning on line 944 in course/exam.py

View workflow job for this annotation

GitHub Actions / Lint and typecheck Python

Type annotation for attribute `model` is required because this class is not decorated with `@final` (reportUnannotatedClassAttribute)
fields = [

Check warning on line 945 in course/exam.py

View workflow job for this annotation

GitHub Actions / Lint and typecheck Python

Type annotation for attribute `fields` is required because this class is not decorated with `@final` (reportUnannotatedClassAttribute)
"course",
"description",
"flow_id",
"active",
"listed",
"no_exams_before",
"no_exams_after",
]
widgets = {
"no_exams_before": HTML5DateTimeInput(),
"no_exams_after": HTML5DateTimeInput(),
}


@course_view
def edit_exam(pctx: CoursePageContext, exam_id: int) -> http.HttpResponse:
if not pctx.has_permission(PPerm.edit_exam):
raise PermissionDenied()

request = pctx.request

num_exam_id = int(exam_id)
if num_exam_id == -1:
exam = Exam(course=pctx.course)
add_new = True
else:
exam = get_object_or_404(Exam, id=num_exam_id)
add_new = False

if exam.course.id != pctx.course.id:
raise SuspiciousOperation(
"may not edit exam in a different course")

if request.method == "POST":
form = EditExamForm(add_new, request.POST, instance=exam)

if form.is_valid():
form.save()
return redirect("relate-edit_exam",
pctx.course.identifier, form.instance.id)

else:
form = EditExamForm(add_new, instance=exam)

return render_course_page(pctx, "course/generic-course-form.html", {
"form_description": _("Create Exam") if add_new else _("Edit Exam"),
"form": form
})

# }}}


def exam_lockdown_context_processor(request):
return {
Expand Down
7 changes: 7 additions & 0 deletions relate/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,13 @@
"/$",
course.exam.batch_issue_exam_tickets,
name="relate-batch_issue_exam_tickets"),
re_path(r"^course"
"/" + COURSE_ID_REGEX
+ "/edit-exam"
"/(?P<exam_id>[-0-9]+)"
"/$",
course.exam.edit_exam,
name="relate-edit_exam"),
path("exam-check-in/",
course.exam.check_in_for_exam,
name="relate-check_in_for_exam"),
Expand Down
107 changes: 106 additions & 1 deletion tests/test_exam.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from django.utils.timezone import now, timedelta

from course import constants, exam
from course.models import ExamTicket, FlowSession
from course.models import Exam, ExamTicket, FlowSession
from tests import factories
from tests.base_test_mixins import (
MockAddMessageMixing,
Expand Down Expand Up @@ -1071,4 +1071,109 @@ def test_start_flow_not_ok(self):
"RELATE is not currently allowed. "
"To exit this exam, log out.")


# {{{ edit exam tests

class EditExamTest(ExamTestMixin, TestCase):
"""Tests for exam.edit_exam view."""

def get_edit_exam_url(self, exam_id, course_identifier=None):
course_identifier = course_identifier or self.get_default_course_identifier()
kwargs = {"course_identifier": course_identifier,
"exam_id": exam_id}
return reverse("relate-edit_exam", kwargs=kwargs)

def get_edit_exam_view(self, exam_id, course_identifier=None,
use_instructor=True):
course_identifier = course_identifier or self.get_default_course_identifier()
user = (self.instructor_participation.user if use_instructor
else self.student_participation.user)
with self.temporarily_switch_to_user(user):
return self.client.get(
self.get_edit_exam_url(exam_id, course_identifier))

def post_edit_exam_view(self, exam_id, data, course_identifier=None,
use_instructor=True):
course_identifier = course_identifier or self.get_default_course_identifier()
user = (self.instructor_participation.user if use_instructor
else self.student_participation.user)
with self.temporarily_switch_to_user(user):
return self.client.post(
self.get_edit_exam_url(exam_id, course_identifier), data)

def get_exam_post_data(self, **kwargs):
data = {
"course": self.course.pk,
"description": "Test Exam",
"flow_id": "quiz-test",
"active": True,
"listed": True,
"no_exams_before": datetime.datetime(
2019, 1, 1, tzinfo=UTC).strftime(DATE_TIME_PICKER_TIME_FORMAT),
"no_exams_after": datetime.datetime(
2019, 3, 1, tzinfo=UTC).strftime(DATE_TIME_PICKER_TIME_FORMAT),
}
data.update(kwargs)
return data

def test_get_create_new(self):
resp = self.get_edit_exam_view(-1)
self.assertEqual(resp.status_code, 200)

def test_get_edit_existing(self):
resp = self.get_edit_exam_view(self.exam.pk)
self.assertEqual(resp.status_code, 200)

def test_no_permission(self):
resp = self.get_edit_exam_view(-1, use_instructor=False)
self.assertEqual(resp.status_code, 403)

resp = self.post_edit_exam_view(-1, data={}, use_instructor=False)
self.assertEqual(resp.status_code, 403)

def test_not_authenticated(self):
with self.temporarily_switch_to_user(None):
resp = self.client.get(
self.get_edit_exam_url(-1))
self.assertEqual(resp.status_code, 403)

def test_post_create_new(self):
initial_count = Exam.objects.count()
data = self.get_exam_post_data()
resp = self.post_edit_exam_view(-1, data=data)
self.assertEqual(Exam.objects.count(), initial_count + 1)
new_exam = Exam.objects.order_by("id").last()
self.assertEqual(new_exam.description, "Test Exam")
self.assertRedirects(
resp, self.get_edit_exam_url(new_exam.pk),
fetch_redirect_response=False)

def test_post_edit_existing(self):
data = self.get_exam_post_data(description="Updated Description")
resp = self.post_edit_exam_view(self.exam.pk, data=data)
self.assertRedirects(
resp, self.get_edit_exam_url(self.exam.pk),
fetch_redirect_response=False)
self.exam.refresh_from_db()
self.assertEqual(self.exam.description, "Updated Description")

def test_post_form_invalid(self):
with mock.patch("course.exam.EditExamForm.is_valid") as mock_is_valid:
mock_is_valid.return_value = False
resp = self.post_edit_exam_view(
self.exam.pk, data=self.get_exam_post_data())
self.assertEqual(resp.status_code, 200)

def test_course_not_match(self):
another_course = factories.CourseFactory(identifier="another-course")
another_exam = factories.ExamFactory(course=another_course)

resp = self.get_edit_exam_view(
another_exam.pk,
course_identifier=self.course.identifier)
self.assertEqual(resp.status_code, 400)

# }}}


# vim: fdm=marker
Loading