diff --git a/generic/templates/generic/exam_index.html b/generic/templates/generic/exam_index.html index 6b94e95d..09246716 100644 --- a/generic/templates/generic/exam_index.html +++ b/generic/templates/generic/exam_index.html @@ -10,7 +10,7 @@ -

{{filter.qs|length}} exams found.

+

{{filter.qs|length}} exams found.

{% for exam in filter.qs %}

Exam: {{ exam.name }}

diff --git a/generic/views.py b/generic/views.py index 351ee52d..39aa8d58 100644 --- a/generic/views.py +++ b/generic/views.py @@ -318,14 +318,15 @@ class ExamViews(View, LoginRequiredMixin): # }, # }, # } + def __init__(self, data=None, *args, **kwargs): # if filterset is bound, use initial values as defaults if data is not None: # get a mutable copy of the QueryDict data = data.copy() - #for name, f in self.base_filters.items(): - # filter param is either missing or empty, use initial as default + # for name, f in self.base_filters.items(): + # filter param is either missing or empty, use initial as default if not data.get("archive"): data["archive"] = False @@ -796,6 +797,8 @@ class ExamViews(View, LoginRequiredMixin): app_exam_map = {} app_exam_map["rapids"] = cid_user.rapid_exams + app_exam_map["anatomy"] = cid_user.anatomy_exams + app_exam_map["longs"] = cid_user.long_exams app_exam_map["atlas"] = cid_user.casecollection_exams if add: @@ -815,6 +818,8 @@ class ExamViews(View, LoginRequiredMixin): app_exam_map = {} app_exam_map["rapids"] = user_user.user_rapid_exams + app_exam_map["anatomy"] = user_user.user_anatomy_exams + app_exam_map["longs"] = user_user.user_longs_exams # app_exam_map["atlas"] = user_user.casecollection_exams if add: @@ -967,7 +972,8 @@ class ExamViews(View, LoginRequiredMixin): # self.check_user_access(request.user, exam.pk) if ( - ( + request.user.is_superuser + or ( self.app_name == "rapids" and request.user.groups.filter(name="rapid_checker").exists() ) @@ -986,7 +992,11 @@ class ExamViews(View, LoginRequiredMixin): filter = self.ExamFilter(request.GET, queryset=exams) - return render(request, "generic/exam_index.html", {"filter": filter, "app_name": self.app_name}) + return render( + request, + "generic/exam_index.html", + {"filter": filter, "app_name": self.app_name}, + ) @method_decorator(login_required) def exam_question_detail(self, request, pk, sk): @@ -2064,3 +2074,26 @@ class UserUserGroupDelete(RevisionMixin, CidManagerRequiredMixin, DeleteView): model = UserUserGroup template_name = "confirm_delete.html" success_url = reverse_lazy("generic:user_group_view") + + +class ExamCreateBase(RevisionMixin, LoginRequiredMixin, CreateView): + template_name = "exam_create_form.html" + + def form_valid(self, form): + self.object = form.save(commit=False) + self.object.save() + + form.instance.author.add(self.request.user.id) + return super().form_valid(form) + + +class ExamUpdateBase(RevisionMixin, LoginRequiredMixin, UpdateView): + template_name = "exam_update_form.html" + + def form_valid(self, form): + self.object = form.save(commit=False) + self.object.save() + + form.instance.author.add(self.request.user.id) + + return super().form_valid(form) diff --git a/rapids/templates/rapids/exam_form.html b/rapids/templates/rapids/exam_form.html index 72bca3df..9901ffac 100755 --- a/rapids/templates/rapids/exam_form.html +++ b/rapids/templates/rapids/exam_form.html @@ -1,4 +1,4 @@ -{% extends "rapids/exams.html" %} +{% extends "rapids/base.html" %} {% block js %} {{ form.media }} @@ -6,12 +6,12 @@ {% block content %} -

Add Exam

-
- {% csrf_token %} - - {{ form.as_table }} -
- -
+

Add Exam

+
+ {% csrf_token %} + + {{ form.as_table }} +
+ +
{% endblock %} diff --git a/rapids/tests/test_urls.py b/rapids/tests/test_urls.py index ffb93b99..0fe027d8 100644 --- a/rapids/tests/test_urls.py +++ b/rapids/tests/test_urls.py @@ -1,24 +1,122 @@ +import json import pytest -#from django.contrib.auth.models import User + +# from django.contrib.auth.models import User from django.urls import reverse +from rich.pretty import pprint + +from bs4 import BeautifulSoup + +from rapids.views import GenericExamViews as RapidExamViews +from sbas.views import GenericExamViews as SbaExamViews +from longs.views import GenericExamViews as LongExamViews +from physics.views import GenericExamViews as PhysicExamViews +from anatomy.views import GenericExamViews as AnatomyExamViews + APP_NAMES = ("rapids", "anatomy", "longs", "physics", "sbas") +JSON_APPS = ("rapids", "anatomy", "longs") + +EXAM_VIEWS = ( + RapidExamViews, + SbaExamViews, + LongExamViews, + PhysicExamViews, + AnatomyExamViews, +) + + @pytest.fixture def create_superuser(db, django_user_model): - return django_user_model.objects.create_superuser("admin", "ross@xkjq.uk", "adminpassword") + return django_user_model.objects.create_superuser( + "admin", "ross@xkjq.uk", "adminpassword" + ) -#@pytest.fixture -#def create_exam(db): + +# @pytest.fixture +# def create_exam(db): # return Exam.objects.create(name="test exam", exam_mode=True) # Very basic tests, make sure we can hit key urls without error -@pytest.mark.parametrize("app_name", APP_NAMES) -def test_index(client, create_superuser, app_name): + +@pytest.mark.parametrize("exam_views", EXAM_VIEWS) +def test_index(client, create_superuser, exam_views, django_user_model): + + basic_user = django_user_model.objects.create_user( + "user1", "user@user.net", "pass2" + ) + + e1 = exam_views.Exam.objects.create(name="test exam", exam_mode=True) + e = exam_views.Exam.objects.create(name="test exam 2", exam_mode=True) + e.author.add(basic_user) + client.login(username="admin", password="adminpassword") - response = client.get(reverse(f'{app_name}:index')) - print(reverse(f'{app_name}:index')) - print(response.status_code) - print(response.content) - assert response.status_code == 200 \ No newline at end of file + response = client.get(reverse(f"{exam_views.app_name}:index")) + assert response.status_code == 200 + + assert ( + BeautifulSoup(response.content, "html.parser") + .find("h4", {"class": "exam-number-title"}) + .getText() + == "2 exams found." + ) + + if exam_views.app_name in JSON_APPS: + response = client.get(reverse(f"{exam_views.app_name}:active_exams")) + + json_response = json.loads(response.content)["exams"] + + assert len(json_response) == 2 + + for json_exam in json_response: + eid_app_type, eid = json_exam["eid"].split("/") + # hack for long / longs mismathc + assert ( + eid_app_type == exam_views.app_name + or eid_app_type == exam_views.app_name[:-1] + ) + + assert exam_views.Exam.objects.get(pk=int(eid)).name == json_exam["name"] + + add_user_response = client.post( + reverse(f"{exam_views.app_name}:exam_json_edit", args=(e1.pk,)), + {"edit_user_user": basic_user.pk, "add": "true"}, + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + + print(f"{e1.valid_user_users.all()=}") + + assert json.loads(add_user_response.content)["status"] == "success" + + client.logout() + + client.force_login(basic_user) + + response = client.get(reverse(f"{exam_views.app_name}:index")) + assert response.status_code == 200 + assert ( + BeautifulSoup(response.content, "html.parser") + .find("h4", {"class": "exam-number-title"}) + .getText() + == "1 exams found." + ) + + if exam_views.app_name in JSON_APPS: + response = client.get(reverse(f"{exam_views.app_name}:active_exams")) + + json_response = json.loads(response.content)["exams"] + + assert len(json_response) == 1 + + e1.active = True + e1.save() + + response = client.get(reverse(f"{exam_views.app_name}:active_exams")) + + json_response = json.loads(response.content)["exams"] + + print(json_response) + + assert len(json_response) == 2 diff --git a/rapids/views.py b/rapids/views.py index b289b520..5f3cec54 100755 --- a/rapids/views.py +++ b/rapids/views.py @@ -42,7 +42,14 @@ from .models import ( CidUserAnswer, ) -from generic.views import AuthorRequiredMixin, ExamCloneMixin, ExamViews, GenericViewBase +from generic.views import ( + AuthorRequiredMixin, + ExamCloneMixin, + ExamCreateBase, + ExamUpdateBase, + ExamViews, + GenericViewBase, +) from reversion.views import RevisionMixin @@ -350,8 +357,8 @@ class RapidUpdate( def form_valid(self, form): # save exam orders (there must be a better way to do this) - #exam_orders = {} - #for exam in self.object.exams.all(): + # exam_orders = {} + # for exam in self.object.exams.all(): # exam_orders[exam] = list(exam.exam_questions.all()) self.object = form.save(commit=False) @@ -371,8 +378,8 @@ class RapidUpdate( answer_formset.save() # restore exam orders - #wanted_exams = form["exams"].data - #for exam in self.object.exams.all(): + # wanted_exams = form["exams"].data + # for exam in self.object.exams.all(): # if exam in exam_orders and self.object in exam_orders[exam]: # print(exam_orders[exam]) # exam.exam_questions.set(exam_orders[exam]) @@ -677,7 +684,6 @@ def mark(request, exam_pk, sk, unmarked_exam_answers_only=True, review=False): ) - def exam_scores_cid_user(request, pk, cid, passcode): exam = get_object_or_404(Exam, pk=pk) @@ -686,13 +692,15 @@ def exam_scores_cid_user(request, pk, cid, passcode): # TODO:Need some kind of test for cid - questions = exam.exam_questions.all()#.prefetch_related("cid_user_answers", "answers") + questions = ( + exam.exam_questions.all() + ) # .prefetch_related("cid_user_answers", "answers") - #cid_user_answers = list(CidUserAnswer.objects.filter(cid=cid, exam__id=pk).prefetch_related("question")) + # cid_user_answers = list(CidUserAnswer.objects.filter(cid=cid, exam__id=pk).prefetch_related("question")) - #cid_user_answers_q_map = {} + # cid_user_answers_q_map = {} - #for ans in cid_user_answers: + # for ans in cid_user_answers: # cid_user_answers_q_map[ans.question] = ans answers_and_marks = [] @@ -706,7 +714,7 @@ def exam_scores_cid_user(request, pk, cid, passcode): for q in questions: # Get user answer user_answer = q.cid_user_answers.filter(cid=cid, exam__id=pk).first() - #user_answer = cid_user_answers_q_map[q] + # user_answer = cid_user_answers_q_map[q] if not user_answer or user_answer is None: # skip if no answer @@ -766,49 +774,36 @@ class QuestionDelete(AuthorOrCheckerRequiredMixin, DeleteView): success_url = reverse_lazy("rapids:rapid_view") -GenericExamViews = ExamViews( - Exam, Rapid, Answer, CidUserAnswer, "rapids", "rapid" -) +GenericExamViews = ExamViews(Exam, Rapid, Answer, CidUserAnswer, "rapids", "rapid") -class ExamCreate(RevisionMixin, LoginRequiredMixin, CreateView): +class ExamCreate(ExamCreateBase): model = Exam form_class = ExamForm - def form_valid(self, form): - self.object = form.save(commit=False) - self.object.save() - form.instance.author.add(self.request.user.id) - return super().form_valid(form) +class ExamUpdate(ExamUpdateBase, AuthorOrCheckerRequiredMixin): + model = Exam + form_class = ExamForm -class ExamUpdate( - RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView +class ExamAuthorUpdate( + RevisionMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView ): - model = Exam - form_class = ExamForm - - def form_valid(self, form): - self.object = form.save(commit=False) - self.object.save() - - form.instance.author.add(self.request.user.id) - - return super().form_valid(form) - -class ExamAuthorUpdate(RevisionMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView): model = Exam form_class = ExamAuthorForm + class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView): model = Exam template_name = "exam_confirm_delete.html" success_url = reverse_lazy("rapids:index") + class UserAnswerView(LoginRequiredMixin, DetailView): model = CidUserAnswer + class UserAnswerTableView(LoginRequiredMixin, SingleTableMixin, FilterView): model = CidUserAnswer table_class = RapidUserAnswerTable @@ -925,6 +920,7 @@ class RapidLateralityViewSet( GenericViews = GenericViewBase("rapids", Rapid, CidUserAnswer, Exam) + @user_is_author_or_rapid_checker def question_anonymise_dicom(request, pk): question = get_object_or_404(Rapid, pk=pk) @@ -933,6 +929,7 @@ def question_anonymise_dicom(request, pk): return redirect("rapids:question_detail", pk=pk) + @user_is_author_or_rapid_checker def question_save_annotation(request, pk): if request.is_ajax() and request.method == "POST": diff --git a/requirements.txt b/requirements.txt index e2066674..46fdc8fc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,4 +26,6 @@ django-querysetsequence dicognito django_unused_media pytest-django -gunicorn \ No newline at end of file +gunicorn +rich +bs4 \ No newline at end of file diff --git a/templates/exam_create_form.html b/templates/exam_create_form.html new file mode 100644 index 00000000..887f9ed1 --- /dev/null +++ b/templates/exam_create_form.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} +{% comment %} {% extends exam.app_name|add:"/base.html" %} {% endcomment %} + +{% block js %} + {{ form.media }} +{% endblock %} + +{% block content %} +{{form.instance}} + +

Add Exam

+
+ {% csrf_token %} + + {{ form.as_table }} +
+ +
+{% endblock %} + diff --git a/templates/exam_update_form.html b/templates/exam_update_form.html new file mode 100644 index 00000000..79aea4d2 --- /dev/null +++ b/templates/exam_update_form.html @@ -0,0 +1,18 @@ +{% extends exam.app_name|add:"/exams.html" %} + +{% block js %} + {{ form.media }} +{% endblock %} + +{% block content %} + +

Update Exam

+
+ {% csrf_token %} + + {{ form.as_table }} +
+ +
+{% endblock %} +