This commit is contained in:
Ross
2022-05-31 23:40:50 +01:00
parent 4969723de9
commit 6d3cdedb95
8 changed files with 228 additions and 60 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
<input type="submit" /> <input type="submit" />
</form> </form>
</details> </details>
<h4>{{filter.qs|length}} exams found.</h4> <h4 class="exam-number-title">{{filter.qs|length}} exams found.</h4>
{% for exam in filter.qs %} {% for exam in filter.qs %}
<div class=""> <div class="">
<h1><a href="{% url app_name|add:':exam_overview' pk=exam.pk %}">Exam: {{ exam.name }} </a></h1> <h1><a href="{% url app_name|add:':exam_overview' pk=exam.pk %}">Exam: {{ exam.name }} </a></h1>
+37 -4
View File
@@ -318,14 +318,15 @@ class ExamViews(View, LoginRequiredMixin):
# }, # },
# }, # },
# } # }
def __init__(self, data=None, *args, **kwargs): def __init__(self, data=None, *args, **kwargs):
# if filterset is bound, use initial values as defaults # if filterset is bound, use initial values as defaults
if data is not None: if data is not None:
# get a mutable copy of the QueryDict # get a mutable copy of the QueryDict
data = data.copy() data = data.copy()
#for name, f in self.base_filters.items(): # for name, f in self.base_filters.items():
# filter param is either missing or empty, use initial as default # filter param is either missing or empty, use initial as default
if not data.get("archive"): if not data.get("archive"):
data["archive"] = False data["archive"] = False
@@ -796,6 +797,8 @@ class ExamViews(View, LoginRequiredMixin):
app_exam_map = {} app_exam_map = {}
app_exam_map["rapids"] = cid_user.rapid_exams 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 app_exam_map["atlas"] = cid_user.casecollection_exams
if add: if add:
@@ -815,6 +818,8 @@ class ExamViews(View, LoginRequiredMixin):
app_exam_map = {} app_exam_map = {}
app_exam_map["rapids"] = user_user.user_rapid_exams 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 # app_exam_map["atlas"] = user_user.casecollection_exams
if add: if add:
@@ -967,7 +972,8 @@ class ExamViews(View, LoginRequiredMixin):
# self.check_user_access(request.user, exam.pk) # self.check_user_access(request.user, exam.pk)
if ( if (
( request.user.is_superuser
or (
self.app_name == "rapids" self.app_name == "rapids"
and request.user.groups.filter(name="rapid_checker").exists() and request.user.groups.filter(name="rapid_checker").exists()
) )
@@ -986,7 +992,11 @@ class ExamViews(View, LoginRequiredMixin):
filter = self.ExamFilter(request.GET, queryset=exams) 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) @method_decorator(login_required)
def exam_question_detail(self, request, pk, sk): def exam_question_detail(self, request, pk, sk):
@@ -2064,3 +2074,26 @@ class UserUserGroupDelete(RevisionMixin, CidManagerRequiredMixin, DeleteView):
model = UserUserGroup model = UserUserGroup
template_name = "confirm_delete.html" template_name = "confirm_delete.html"
success_url = reverse_lazy("generic:user_group_view") 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)
+9 -9
View File
@@ -1,4 +1,4 @@
{% extends "rapids/exams.html" %} {% extends "rapids/base.html" %}
{% block js %} {% block js %}
{{ form.media }} {{ form.media }}
@@ -6,12 +6,12 @@
{% block content %} {% block content %}
<h2>Add Exam</h2> <h2>Add Exam</h2>
<form action="" method="post"> <form action="" method="post">
{% csrf_token %} {% csrf_token %}
<table> <table>
{{ form.as_table }} {{ form.as_table }}
</table> </table>
<input type="submit" value="Submit"> <input type="submit" value="Submit">
</form> </form>
{% endblock %} {% endblock %}
+109 -11
View File
@@ -1,24 +1,122 @@
import json
import pytest import pytest
#from django.contrib.auth.models import User
# from django.contrib.auth.models import User
from django.urls import reverse 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") APP_NAMES = ("rapids", "anatomy", "longs", "physics", "sbas")
JSON_APPS = ("rapids", "anatomy", "longs")
EXAM_VIEWS = (
RapidExamViews,
SbaExamViews,
LongExamViews,
PhysicExamViews,
AnatomyExamViews,
)
@pytest.fixture @pytest.fixture
def create_superuser(db, django_user_model): 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) # return Exam.objects.create(name="test exam", exam_mode=True)
# Very basic tests, make sure we can hit key urls without error # 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") client.login(username="admin", password="adminpassword")
response = client.get(reverse(f'{app_name}:index')) response = client.get(reverse(f"{exam_views.app_name}:index"))
print(reverse(f'{app_name}:index')) assert response.status_code == 200
print(response.status_code)
print(response.content) assert (
assert response.status_code == 200 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
+31 -34
View File
@@ -42,7 +42,14 @@ from .models import (
CidUserAnswer, CidUserAnswer,
) )
from generic.views import AuthorRequiredMixin, ExamCloneMixin, ExamViews, GenericViewBase from generic.views import (
AuthorRequiredMixin,
ExamCloneMixin,
ExamCreateBase,
ExamUpdateBase,
ExamViews,
GenericViewBase,
)
from reversion.views import RevisionMixin from reversion.views import RevisionMixin
@@ -350,8 +357,8 @@ class RapidUpdate(
def form_valid(self, form): def form_valid(self, form):
# save exam orders (there must be a better way to do this) # save exam orders (there must be a better way to do this)
#exam_orders = {} # exam_orders = {}
#for exam in self.object.exams.all(): # for exam in self.object.exams.all():
# exam_orders[exam] = list(exam.exam_questions.all()) # exam_orders[exam] = list(exam.exam_questions.all())
self.object = form.save(commit=False) self.object = form.save(commit=False)
@@ -371,8 +378,8 @@ class RapidUpdate(
answer_formset.save() answer_formset.save()
# restore exam orders # restore exam orders
#wanted_exams = form["exams"].data # wanted_exams = form["exams"].data
#for exam in self.object.exams.all(): # for exam in self.object.exams.all():
# if exam in exam_orders and self.object in exam_orders[exam]: # if exam in exam_orders and self.object in exam_orders[exam]:
# print(exam_orders[exam]) # print(exam_orders[exam])
# exam.exam_questions.set(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): def exam_scores_cid_user(request, pk, cid, passcode):
exam = get_object_or_404(Exam, pk=pk) 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 # 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 # cid_user_answers_q_map[ans.question] = ans
answers_and_marks = [] answers_and_marks = []
@@ -706,7 +714,7 @@ def exam_scores_cid_user(request, pk, cid, passcode):
for q in questions: for q in questions:
# Get user answer # Get user answer
user_answer = q.cid_user_answers.filter(cid=cid, exam__id=pk).first() 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: if not user_answer or user_answer is None:
# skip if no answer # skip if no answer
@@ -766,49 +774,36 @@ class QuestionDelete(AuthorOrCheckerRequiredMixin, DeleteView):
success_url = reverse_lazy("rapids:rapid_view") success_url = reverse_lazy("rapids:rapid_view")
GenericExamViews = ExamViews( GenericExamViews = ExamViews(Exam, Rapid, Answer, CidUserAnswer, "rapids", "rapid")
Exam, Rapid, Answer, CidUserAnswer, "rapids", "rapid"
)
class ExamCreate(RevisionMixin, LoginRequiredMixin, CreateView): class ExamCreate(ExamCreateBase):
model = Exam model = Exam
form_class = ExamForm 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) class ExamUpdate(ExamUpdateBase, AuthorOrCheckerRequiredMixin):
return super().form_valid(form) model = Exam
form_class = ExamForm
class ExamUpdate( class ExamAuthorUpdate(
RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView 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 model = Exam
form_class = ExamAuthorForm form_class = ExamAuthorForm
class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView): class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView):
model = Exam model = Exam
template_name = "exam_confirm_delete.html" template_name = "exam_confirm_delete.html"
success_url = reverse_lazy("rapids:index") success_url = reverse_lazy("rapids:index")
class UserAnswerView(LoginRequiredMixin, DetailView): class UserAnswerView(LoginRequiredMixin, DetailView):
model = CidUserAnswer model = CidUserAnswer
class UserAnswerTableView(LoginRequiredMixin, SingleTableMixin, FilterView): class UserAnswerTableView(LoginRequiredMixin, SingleTableMixin, FilterView):
model = CidUserAnswer model = CidUserAnswer
table_class = RapidUserAnswerTable table_class = RapidUserAnswerTable
@@ -925,6 +920,7 @@ class RapidLateralityViewSet(
GenericViews = GenericViewBase("rapids", Rapid, CidUserAnswer, Exam) GenericViews = GenericViewBase("rapids", Rapid, CidUserAnswer, Exam)
@user_is_author_or_rapid_checker @user_is_author_or_rapid_checker
def question_anonymise_dicom(request, pk): def question_anonymise_dicom(request, pk):
question = get_object_or_404(Rapid, pk=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) return redirect("rapids:question_detail", pk=pk)
@user_is_author_or_rapid_checker @user_is_author_or_rapid_checker
def question_save_annotation(request, pk): def question_save_annotation(request, pk):
if request.is_ajax() and request.method == "POST": if request.is_ajax() and request.method == "POST":
+3 -1
View File
@@ -26,4 +26,6 @@ django-querysetsequence
dicognito dicognito
django_unused_media django_unused_media
pytest-django pytest-django
gunicorn gunicorn
rich
bs4
+20
View File
@@ -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}}
<h2>Add Exam</h2>
<form action="" method="post">
{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<input type="submit" value="Submit">
</form>
{% endblock %}
+18
View File
@@ -0,0 +1,18 @@
{% extends exam.app_name|add:"/exams.html" %}
{% block js %}
{{ form.media }}
{% endblock %}
{% block content %}
<h2>Update Exam</h2>
<form action="" method="post">
{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<input type="submit" value="Submit">
</form>
{% endblock %}