.
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
+36
-3
@@ -318,13 +318,14 @@ 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)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{% extends "rapids/exams.html" %}
|
{% extends "rapids/base.html" %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
{{ form.media }}
|
{{ form.media }}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
{% 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>
|
||||||
|
|||||||
+108
-10
@@ -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'))
|
|
||||||
print(response.status_code)
|
|
||||||
print(response.content)
|
|
||||||
assert response.status_code == 200
|
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
|
||||||
|
|||||||
+31
-34
@@ -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":
|
||||||
|
|||||||
@@ -27,3 +27,5 @@ dicognito
|
|||||||
django_unused_media
|
django_unused_media
|
||||||
pytest-django
|
pytest-django
|
||||||
gunicorn
|
gunicorn
|
||||||
|
rich
|
||||||
|
bs4
|
||||||
@@ -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 %}
|
||||||
|
|
||||||
@@ -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 %}
|
||||||
|
|
||||||
Reference in New Issue
Block a user