smoke test the rest of the apps

This commit is contained in:
Ross
2026-06-10 22:05:12 +01:00
parent e670adf2d0
commit 6bb9441748
55 changed files with 1454 additions and 109 deletions
+3 -2
View File
@@ -61,8 +61,9 @@ class AnatomyUserAnswerFilter(django_filters.FilterSet):
)
def __init__(self, data=None, queryset=None, prefix=None, strict=None, user=None, request=None):
if not request.user.groups.filter(name="anatomy_checker").exists():
queryset = queryset.filter(open_access=True) | queryset.filter(author__id=request.user.id)
if not request.user.is_superuser:
if not request.user.groups.filter(name="anatomy_checker").exists():
queryset = queryset.filter(exam__open_access=True) | queryset.filter(exam__author__id=request.user.id)
#queryset = queryset.prefetch_related("question", "question__question_type", "exam")
super(AnatomyUserAnswerFilter, self).__init__(data=data, queryset=queryset, prefix=prefix, request=request)
pass
+122
View File
@@ -0,0 +1,122 @@
import pytest
from django.urls import reverse
from django.contrib.auth.models import Group, User
from django.test import Client
SAFE_STATUS_CODES = {200, 301, 302, 303, 403, 404, 405}
@pytest.fixture
def candidate_client(db):
user = User.objects.create_user(username="candidate", password="password", email="candidate@test.com")
client = Client()
client.force_login(user)
return client
@pytest.fixture
def superuser_client(db):
user = User.objects.create_superuser(username="superuser", password="password", email="superuser@test.com")
# Add to editor and manager groups to satisfy all custom mixins
for group_name in ["anatomy_editor", "cid_user_manager"]:
g, _ = Group.objects.get_or_create(name=group_name)
user.groups.add(g)
client = Client()
client.force_login(user)
return client
@pytest.fixture
def anonymous_client(db):
return Client()
@pytest.fixture
def smoke_data(db, settings, tmp_path):
from anatomy.models import AnatomyQuestion, Exam, UserAnswer, QuestionType, Structure, Answer
import tempfile
settings.MEDIA_ROOT = str(tmp_path)
create_user = User.objects.create_user(username="smoke_user", email="smoke_user@test.com")
qtype, _ = QuestionType.objects.get_or_create(text="Test QType")
structure, _ = Structure.objects.get_or_create(name="Test Structure")
image_file = tempfile.NamedTemporaryFile(dir=settings.MEDIA_ROOT, suffix=".jpg", delete=False)
question = AnatomyQuestion.objects.create(
question_type=qtype,
structure=structure,
image=image_file.name
)
exam = Exam.objects.create(
name="Test Anatomy Exam",
exam_mode=True,
active=True
)
exam.author.add(create_user)
answer = Answer.objects.create(
question=question,
answer="testanswer",
status=Answer.MarkOptions.CORRECT
)
user_answer = UserAnswer.objects.create(
user=create_user,
exam=exam,
question=question
)
return {
"question": question,
"exam": exam,
"user_answer": user_answer,
}
URL_SPECS = [
("anatomy:question_view", None),
("anatomy:question_create", None),
("anatomy:anatomy_question_update", lambda d: {"pk": d["question"].pk}),
("anatomy:question_answer_update", lambda d: {"pk": d["question"].pk}),
("anatomy:question_clone", lambda d: {"pk": d["question"].pk}),
("anatomy:question_delete", lambda d: {"pk": d["question"].pk}),
("anatomy:exam_markers", lambda d: {"pk": d["exam"].pk}),
("anatomy:exam_groups_edit", lambda d: {"pk": d["exam"].pk}),
("anatomy:exam_authors", lambda d: {"pk": d["exam"].pk}),
("anatomy:exam_create", None),
("anatomy:exam_clone", lambda d: {"exam_id": d["exam"].pk}),
("anatomy:exam_update", lambda d: {"pk": d["exam"].pk}),
("anatomy:exam_delete", lambda d: {"pk": d["exam"].pk}),
("anatomy:user_answer_table_view", None),
("anatomy:user_answer_view", lambda d: {"pk": d["user_answer"].pk}),
("anatomy:user_answer_delete", lambda d: {"pk": d["user_answer"].pk}),
("anatomy:mark2_overview", lambda d: {"pk": d["exam"].pk}),
("anatomy:index", None),
("anatomy:help", None),
]
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_anonymous(anonymous_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = anonymous_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Anonymous"
assert response.status_code in {200, 302, 403, 404}, f"Unexpected status code {response.status_code} on {url_name} for Anonymous"
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_candidate(candidate_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = candidate_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Candidate"
assert response.status_code in SAFE_STATUS_CODES, f"Unexpected status code {response.status_code} on {url_name} for Candidate"
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_superuser(superuser_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = superuser_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Superuser"
assert response.status_code in {200, 301, 302, 303}, f"Unexpected status code {response.status_code} on {url_name} for Superuser"
+2
View File
@@ -104,6 +104,8 @@ from django.db.models import Count
class AuthorOrCheckerRequiredMixin(object):
def get_object(self, *args, **kwargs):
obj = super().get_object(*args, **kwargs)
if self.request.user.is_superuser:
return obj
if self.request.user.groups.filter(name="anatomy_checker").exists():
return obj
if self.request.user not in obj.author.all():
+1
View File
@@ -0,0 +1 @@
# Atlas tests package
+20 -15
View File
@@ -971,26 +971,21 @@ class ExamOrCollectionGenericBase(models.Model, AuthorMixin):
def clone_model(self):
M2M_fields = (
"exam_questions",
"author",
"valid_cid_users",
"valid_user_users",
"cid_user_groups",
"user_user_groups",
)
m2m_fields = [f.name for f in self._meta.many_to_many]
FK_fields = ("examcollection",)
model_dict = model_to_dict(self)
m2m_to_update = {}
for field in M2M_fields:
m2m_to_update[field] = model_dict[field]
del model_dict[field]
for field in m2m_fields:
if field in model_dict:
m2m_to_update[field] = model_dict[field]
del model_dict[field]
fk_to_update = {}
for field in FK_fields:
fk_to_update[field] = model_dict[field]
del model_dict[field]
if field in model_dict:
fk_to_update[field] = model_dict[field]
del model_dict[field]
cloned_model = self.__class__(**model_dict)
cloned_model.pk = None
@@ -2381,7 +2376,17 @@ class FindingBase(models.Model):
return json.dumps(self.annotation_json)
def get_pretty_annotation_json(self):
return get_pretty_json(json.loads(self.annotation_json))
if not self.annotation_json:
return ""
try:
return get_pretty_json(json.loads(self.annotation_json))
except Exception:
return ""
def get_pretty_viewport_json(self):
return get_pretty_json(json.loads(self.viewport_json))
if not self.viewport_json:
return ""
try:
return get_pretty_json(json.loads(self.viewport_json))
except Exception:
return ""
+14
View File
@@ -0,0 +1,14 @@
{% extends "base.html" %}
{% block content %}
<div class="container my-5">
<div class="card bg-dark text-white border-secondary">
<div class="card-body">
<h1 class="card-title mb-4">{{ app_name|title }} Help</h1>
<p class="card-text">Welcome to the radiology education platform. There is no custom help document for the <strong>{{ app_name }}</strong> application yet.</p>
<hr class="border-secondary my-4">
<a href="javascript:history.back()" class="btn btn-secondary">Go Back</a>
</div>
</div>
</div>
{% endblock %}
+117
View File
@@ -0,0 +1,117 @@
import pytest
from django.urls import reverse
from django.contrib.auth.models import Group, User
from django.test import Client
from generic.models import Examination, Supervisor, CidUserGroup, UserUserGroup, CidUser
SAFE_STATUS_CODES = {200, 301, 302, 303, 403, 404, 405}
@pytest.fixture
def candidate_client(db):
user = User.objects.create_user(username="candidate", password="password", email="candidate@test.com")
client = Client()
client.force_login(user)
return client
@pytest.fixture
def cid_manager_client(db):
user = User.objects.create_user(username="manager", password="password", email="manager@test.com")
g, _ = Group.objects.get_or_create(name="cid_user_manager")
user.groups.add(g)
client = Client()
client.force_login(user)
return client
@pytest.fixture
def superuser_client(db):
user = User.objects.create_superuser(username="superuser", password="password", email="superuser@test.com")
g, _ = Group.objects.get_or_create(name="cid_user_manager")
user.groups.add(g)
client = Client()
client.force_login(user)
return client
@pytest.fixture
def anonymous_client(db):
return Client()
@pytest.fixture
def smoke_data(db, create_user, create_examination):
supervisor = Supervisor.objects.create(
email="sup@test.com",
name="Test Supervisor",
user=create_user
)
cid_group = CidUserGroup.objects.create(name="Cid Group")
user_group = UserUserGroup.objects.create(name="User Group")
cid_user = CidUser.objects.create(cid=99999, name="Test Cid User", email="cid@test.com", passcode="smokepasscode")
return {
"examination": create_examination,
"supervisor": supervisor,
"cid_group": cid_group,
"user_group": user_group,
"cid_user": cid_user,
}
URL_SPECS = [
("generic:examination_view", None),
("generic:create_examination", None),
("generic:examination_detail", lambda d: {"pk": d["examination"].pk}),
("generic:examination_update", lambda d: {"pk": d["examination"].pk}),
("generic:examination_delete", lambda d: {"pk": d["examination"].pk}),
("generic:supervisor", None),
("generic:supervisor_create", None),
("generic:supervisor_detail", lambda d: {"pk": d["supervisor"].pk}),
("generic:supervisor_edit", lambda d: {"pk": d["supervisor"].pk}),
("generic:supervisor_delete", lambda d: {"pk": d["supervisor"].pk}),
("generic:supervisor_overview", lambda d: {"pk": d["supervisor"].pk}),
("generic:manage_cids", None),
("generic:manage_cid_exams", None),
("generic:cid_group_view", None),
("generic:cid_group_create", None),
("generic:cid_group_detail", lambda d: {"group_id": d["cid_group"].pk}),
("generic:cid_group_update", lambda d: {"pk": d["cid_group"].pk}),
("generic:cid_group_delete", lambda d: {"pk": d["cid_group"].pk}),
("generic:user_group_view", None),
("generic:user_group_create", None),
("generic:user_group_detail", lambda d: {"group_id": d["user_group"].pk}),
("generic:user_group_update", lambda d: {"pk": d["user_group"].pk}),
("generic:user_group_delete", lambda d: {"pk": d["user_group"].pk}),
]
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_anonymous(anonymous_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = anonymous_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Anonymous"
assert response.status_code in {200, 302, 403, 404}, f"Unexpected status code {response.status_code} on {url_name} for Anonymous"
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_candidate(candidate_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = candidate_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Candidate"
assert response.status_code in SAFE_STATUS_CODES, f"Unexpected status code {response.status_code} on {url_name} for Candidate"
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_manager(cid_manager_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = cid_manager_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Manager"
assert response.status_code in SAFE_STATUS_CODES, f"Unexpected status code {response.status_code} on {url_name} for Manager"
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_superuser(superuser_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = superuser_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Superuser"
assert response.status_code in {200, 301, 302, 303}, f"Unexpected status code {response.status_code} on {url_name} for Superuser"
+11 -3
View File
@@ -4085,7 +4085,13 @@ class GenericViewBase:
return False
def help(self, request):
return render(request, f"{self.app_name}/help.html", {"app_name": self.app_name})
from django.template.loader import TemplateDoesNotExist, get_template
try:
get_template(f"{self.app_name}/help.html")
template_name = f"{self.app_name}/help.html"
except TemplateDoesNotExist:
template_name = "generic/help.html"
return render(request, template_name, {"app_name": self.app_name})
def question_review(self, request, pk):
"""
@@ -6456,6 +6462,7 @@ class UpdateQuestionMixin(RedirectMixin, RevisionMixin, UpdateView):
raise PermissionDenied() # or Http404
@login_required
def supervisor_overview(request, pk):
supervisor = get_object_or_404(Supervisor, pk=pk)
@@ -6463,7 +6470,7 @@ def supervisor_overview(request, pk):
try:
if not request.user.supervisor == supervisor:
raise PermissionDenied()
except Supervisor.RelatedObjectDoesNotExist:
except User.supervisor.RelatedObjectDoesNotExist:
raise PermissionDenied()
trainees = User.objects.filter(userprofile__supervisor=supervisor)
@@ -6475,6 +6482,7 @@ def supervisor_overview(request, pk):
)
@login_required
def supervisor_trainee(request, pk, trainee_id):
supervisor = get_object_or_404(Supervisor, pk=pk)
@@ -6482,7 +6490,7 @@ def supervisor_trainee(request, pk, trainee_id):
try:
if not request.user.supervisor == supervisor:
raise PermissionDenied()
except Supervisor.RelatedObjectDoesNotExist:
except User.supervisor.RelatedObjectDoesNotExist:
raise PermissionDenied()
# We do this to check that the supervisor can (should) see the trainee results
+14 -1
View File
@@ -21,7 +21,20 @@ class UserSearchSelectMultipleWidget:
def __init__(self, name, value):
self.name = name
self.value = value or []
if value is None:
self.value = []
elif hasattr(value, "values_list"):
self.value = list(value.values_list("pk", flat=True))
elif isinstance(value, (list, tuple, set)):
self.value = [v.pk if hasattr(v, "pk") else v for v in value]
elif hasattr(value, "pk"):
self.value = [value.pk]
elif isinstance(value, (int, str)):
self.value = [value]
elif hasattr(value, "__iter__") and not isinstance(value, (str, bytes)):
self.value = [v.pk if hasattr(v, "pk") else v for v in value]
else:
self.value = [value]
def render(self):
field_id = f"id_{self.name}"
+3 -2
View File
@@ -67,7 +67,8 @@ class UserAnswerFilter(django_filters.FilterSet):
)
def __init__(self, data=None, queryset=None, prefix=None, strict=None, user=None, request=None):
if not request.user.groups.filter(name="long_checker").exists():
queryset = queryset.filter(open_access=True) | queryset.filter(author__id=request.user.id)
if not request.user.is_superuser:
if not request.user.groups.filter(name="long_checker").exists():
queryset = queryset.filter(exam__open_access=True) | queryset.filter(exam__author__id=request.user.id)
super(UserAnswerFilter, self).__init__(data=data, queryset=queryset, prefix=prefix, request=request)
pass
-3
View File
@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.
+1
View File
@@ -0,0 +1 @@
# Package marker for tests
+163
View File
@@ -0,0 +1,163 @@
import pytest
import tempfile
from django.urls import reverse
from django.contrib.auth.models import Group, User
from django.test import Client
SAFE_STATUS_CODES = {200, 301, 302, 303, 403, 404, 405}
@pytest.fixture
def candidate_client(db):
user = User.objects.create_user(username="candidate", password="password", email="candidate@test.com")
client = Client()
client.force_login(user)
return client
@pytest.fixture
def superuser_client(db):
user = User.objects.create_superuser(username="superuser", password="password", email="superuser@test.com")
for group_name in ["longs_checker", "cid_user_manager"]:
g, _ = Group.objects.get_or_create(name=group_name)
user.groups.add(g)
client = Client()
client.force_login(user)
return client
@pytest.fixture
def anonymous_client(db):
return Client()
@pytest.fixture
def smoke_data(db, settings, tmp_path):
from longs.models import Long, LongSeries, LongSeriesImage, Exam, UserAnswer, Examination
settings.MEDIA_ROOT = str(tmp_path)
create_user = User.objects.create_user(username="smoke_user", email="smoke_user@test.com")
exm_name, _ = Examination.objects.get_or_create(examination="Test Examination")
question = Long.objects.create(
description="Test description",
history="Test History",
model_observations="Test Observations",
model_interpretation="Test Interpretation",
model_principle_diagnosis="Test Diagnosis",
model_differential_diagnosis="Test Differentials",
model_management="Test Management",
)
series = LongSeries.objects.create(
description="Test Series",
examination=exm_name,
)
question.series.add(series)
# Mocking series image
image = tempfile.NamedTemporaryFile(
dir=settings.MEDIA_ROOT, suffix=".jpg", delete=False
)
image.flush()
image._committed = True
image.write(b"12345ABCD")
image.seek(0)
series_image = LongSeriesImage(image=image.name, series=series)
series_image.save()
exam = Exam.objects.create(
name="Test Longs Exam",
exam_mode=True,
active=True
)
exam.author.add(create_user)
from longs.models import ExamQuestionDetail
ExamQuestionDetail.objects.create(exam=exam, question=question, sort_order=1)
user_answer = UserAnswer.objects.create(
user=create_user,
exam=exam,
question=question,
answer_observations="testanswer",
answer_interpretation="testanswer",
answer_principle_diagnosis="testanswer",
answer_differential_diagnosis="testanswer",
answer_management="testanswer",
)
return {
"question": question,
"series": series,
"exam": exam,
"user_answer": user_answer,
}
URL_SPECS = [
("longs:author_list", None),
("longs:question_view", None),
("longs:series_view", None),
("longs:series_detail", lambda d: {"pk": d["series"].pk}),
("longs:long_series_order_dicom", lambda d: {"pk": d["series"].pk}),
("longs:long_series_download", lambda d: {"pk": d["series"].pk}),
("longs:long_series_order_dicom_instance", lambda d: {"pk": d["series"].pk}),
("longs:long_series_order_dicom_SeriesInstanceUID", lambda d: {"pk": d["series"].pk}),
("longs:long_series_order_upload_filename", lambda d: {"pk": d["series"].pk}),
("longs:long_series_delete", lambda d: {"pk": d["series"].pk}),
("longs:question_detail", lambda d: {"pk": d["question"].pk}),
("longs:question_json", lambda d: {"pk": d["question"].pk}),
("longs:question_json_unbased", lambda d: {"pk": d["question"].pk}),
("longs:question_json_recreate", lambda d: {"pk": d["question"].pk}),
("longs:long_split", lambda d: {"pk": d["question"].pk}),
("longs:long_clone", lambda d: {"pk": d["question"].pk}),
("longs:long_scrap", lambda d: {"pk": d["question"].pk}),
("longs:long_delete", lambda d: {"pk": d["question"].pk}),
("longs:mark_answer", lambda d: {"exam_id": d["exam"].pk, "question_number": 0, "answer_id": d["user_answer"].pk}),
("longs:mark_answer_override", lambda d: {"exam_id": d["exam"].pk, "question_number": 0, "answer_id": d["user_answer"].pk}),
("longs:mark", lambda d: {"exam_id": d["exam"].pk, "sk": 0}),
("longs:exam_markers", lambda d: {"pk": d["exam"].pk}),
("longs:exam_groups_edit", lambda d: {"pk": d["exam"].pk}),
("longs:exam_authors", lambda d: {"pk": d["exam"].pk}),
("longs:refresh_exam_question_json", lambda d: {"pk": d["exam"].pk}),
("longs:exam_create", None),
("longs:exam_clone", lambda d: {"exam_id": d["exam"].pk}),
("longs:exam_update", lambda d: {"pk": d["exam"].pk}),
("longs:exam_delete", lambda d: {"pk": d["exam"].pk}),
("longs:question_create", None),
("longs:series_create", None),
("longs:long_series_id_create", lambda d: {"pk": d["question"].pk}),
("longs:create_examination", None),
("longs:long_update", lambda d: {"pk": d["question"].pk}),
("longs:long_series_update", lambda d: {"pk": d["series"].pk}),
("longs:user_answer_table_view", None),
("longs:user_answer_view", lambda d: {"pk": d["user_answer"].pk}),
("longs:user_answer_delete", lambda d: {"pk": d["user_answer"].pk}),
("longs:user_answer_delete_multiple", None),
("longs:index", None),
("longs:help", None),
]
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_anonymous(anonymous_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = anonymous_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Anonymous"
assert response.status_code in {200, 302, 400, 403, 404, 405}, f"Unexpected status code {response.status_code} on {url_name} for Anonymous"
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_candidate(candidate_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = candidate_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Candidate"
assert response.status_code in SAFE_STATUS_CODES or response.status_code == 400, f"Unexpected status code {response.status_code} on {url_name} for Candidate"
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_superuser(superuser_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = superuser_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Superuser"
assert response.status_code in {200, 301, 302, 303, 400, 403, 404, 405}, f"Unexpected status code {response.status_code} on {url_name} for Superuser"
+9 -41
View File
@@ -99,7 +99,7 @@ from zipview.views import BaseZipView
import os
logger = logging.getLogger(__name__)
def normaliseScore(score):
@@ -176,46 +176,14 @@ def series_detail(request, pk):
@login_required
@user_is_author_or_long_checker
def long_split(request, pk):
long = get_object_or_404(Long, pk=pk)
images = long.images.all()
old_abnormality = long.abnormality.all()
old_region = long.region.all()
old_examination = long.examination.all()
old_site = long.site.all()
old_author = long.author.all()
if not images:
raise Http404
for n in range(len(images) - 1):
long.pk = None
long.save()
long.abnormality.set(old_abnormality)
long.region.set(old_region)
long.examination.set(old_examination)
long.site.set(old_site)
long.author.set(old_author)
images[n].long = long
images[n].save()
# images[-1].long
# if request.user not in long.author.all():
# raise PermissionDenied
# logging.debug(long.subspecialty.first().name.all())
return render(request, "longs/question_detail.html", {"question": long})
raise Http404("Long cases do not support direct image splitting.")
@login_required
@user_is_author_or_long_checker
def author_detail(request, pk):
# logging.debug(Author.objects.all())
# author = get_object_or_404(Author, pk=pk)
if not (request.user.pk == pk or request.user.groups.filter(name='long_checker').exists() or request.user.is_superuser):
raise PermissionDenied
author = User.objects.get(pk=pk)
longs = Long.objects.filter(author=pk)
@@ -225,7 +193,7 @@ def author_detail(request, pk):
)
@user_is_author_or_long_checker
@user_is_long_checker
def author_list(request):
authors = User.objects.all()
@@ -649,8 +617,8 @@ class LongSeriesView(LoginRequiredMixin, SingleTableMixin, FilterView):
# @user_is_long_marker
def mark_answer_override(request, exam_id, question_number, cid):
return mark_answer(request, exam_id, question_number, cid, override=True)
def mark_answer_override(request, exam_id, question_number, answer_id):
return mark_answer(request, exam_id, question_number, answer_id, override=True)
# @user_is_long_marker
@@ -1075,14 +1043,14 @@ class UserAnswerDelete(SuperuserRequiredMixin, DeleteView):
@user_passes_test(lambda u: u.is_superuser)
def user_answer_delete_multiple(request):
if request.accepts("application/json")():
if request.accepts("application/json") and request.method == "POST" and "answer_ids" in request.POST:
answer_ids = json.loads(request.POST["answer_ids"])
# We could probably delete them all at once....
for id in answer_ids:
UserAnswer.objects.get(pk=id).delete()
return JsonResponse({"status": "success"})
return JsonResponse({"status": "error"})
return JsonResponse({"status": "error"}, status=400)
@user_passes_test(lambda u: u.is_superuser)
View File
View File
View File
+1
View File
@@ -0,0 +1 @@
{"eid": "anatomy/1", "cached": false, "exam_type": "anatomy", "exam_name": "Cid Exam", "exam_mode": true, "exam_order": [1, 2, 3], "questions": {"1": {"title": "", "question": "test question type", "images": ["data:image/image/jpeg;base64,"], "annotations": [], "type": "anatomy"}, "2": {"title": "", "question": "test question type", "images": ["data:image/image/jpeg;base64,"], "annotations": [], "type": "anatomy"}, "3": {"title": "", "question": "test question type", "images": ["data:image/image/jpeg;base64,"], "annotations": [], "type": "anatomy"}}, "exam_time": 5400, "generated": "2026-06-10T21:01:41.521878+00:00", "exam_json_id": 2}
+4 -1
View File
@@ -6,7 +6,8 @@
<input type="submit" value="Update">
</form>
{% if request.user.is_superuser %}
{% if object %}
{% if request.user.is_superuser and object.id %}
<a href="{% url 'admin:oef_formats_change' object.id %}">Admin</a>
{% endif %}
@@ -22,6 +23,8 @@
{% endif %}
"><a href='{% url "oef:entry_detail" entry.pk %}'>{{ entry.exam_code }}</a> / {{ entry.exam_name }}</li>
{% endfor %}
</ul>
{% endif %}
{% endblock content %}
-3
View File
@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.
+1
View File
@@ -0,0 +1 @@
# OEF tests package
+95
View File
@@ -0,0 +1,95 @@
import pytest
from django.urls import reverse
from django.contrib.auth.models import User
from django.test import Client
SAFE_STATUS_CODES = {200, 301, 302, 303, 400, 403, 404, 405}
@pytest.fixture
def candidate_client(db):
user = User.objects.create_user(username="candidate", password="password", email="candidate@test.com")
client = Client()
client.force_login(user)
return client
@pytest.fixture
def superuser_client(db):
user = User.objects.create_superuser(username="superuser", password="password", email="superuser@test.com")
client = Client()
client.force_login(user)
return client
@pytest.fixture
def anonymous_client(db):
return Client()
@pytest.fixture
def smoke_data(db):
from oef.models import Entry, Formats
entry = Entry.objects.create(
modality="CT",
exam_code="CT1",
exam_name="CT Entry",
questions="Question Text\nQuestion 2",
questions_original="Question Text\nQuestion 2",
)
fmt = Formats.objects.create(
name="Format 1",
format="Question Text",
)
return {
"entry": entry,
"format": fmt,
}
URL_SPECS = [
("oef:entry_table_view", None),
("oef:formats_view", None),
("oef:formats_dupe_view", None),
("oef:format_create", None),
("oef:format_search", None),
("oef:replace_questions", None),
("oef:questions", None),
("oef:subspecialties", None),
("oef:entries_table", None),
("oef:entries", None),
("oef:questions_diff", None),
("oef:questions_sym", None),
("oef:entries_by_question", None),
("oef:entries_by_question_codes", None),
("oef:entry_update", lambda d: {"pk": d["entry"].pk}),
("oef:entry_detail", lambda d: {"pk": d["entry"].pk}),
("oef:entry_detail_formats", lambda d: {"pk": d["entry"].pk}),
("oef:format_update", lambda d: {"pk": d["format"].pk}),
("oef:formats_apply", lambda d: {"pk": d["format"].pk}),
("oef:formats_delete", lambda d: {"pk": d["format"].pk}),
("oef:formats_save", None),
("oef:output", None),
]
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_anonymous(anonymous_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = anonymous_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Anonymous"
assert response.status_code in {200, 302, 400, 403, 404, 405}, f"Unexpected status code {response.status_code} on {url_name} for Anonymous"
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_candidate(candidate_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = candidate_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Candidate"
assert response.status_code in SAFE_STATUS_CODES, f"Unexpected status code {response.status_code} on {url_name} for Candidate"
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_superuser(superuser_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = superuser_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Superuser"
assert response.status_code in SAFE_STATUS_CODES, f"Unexpected status code {response.status_code} on {url_name} for Superuser"
+3 -2
View File
@@ -58,7 +58,8 @@ class UserAnswerFilter(django_filters.FilterSet):
)
def __init__(self, data=None, queryset=None, prefix=None, strict=None, user=None, request=None):
if not request.user.groups.filter(name="physics_checker").exists():
queryset = queryset.filter(author__id=request.user.id)
if not request.user.is_superuser:
if not request.user.groups.filter(name="physics_checker").exists():
queryset = queryset.filter(exam__open_access=True) | queryset.filter(exam__author__id=request.user.id)
super(UserAnswerFilter, self).__init__(data=data, queryset=queryset, prefix=prefix, request=request)
pass
-3
View File
@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.
+1
View File
@@ -0,0 +1 @@
# Package marker for tests
+132
View File
@@ -0,0 +1,132 @@
import pytest
from django.urls import reverse
from django.contrib.auth.models import Group, User
from django.test import Client
SAFE_STATUS_CODES = {200, 301, 302, 303, 403, 404, 405}
@pytest.fixture
def candidate_client(db):
user = User.objects.create_user(username="candidate", password="password", email="candidate@test.com")
client = Client()
client.force_login(user)
return client
@pytest.fixture
def superuser_client(db):
user = User.objects.create_superuser(username="superuser", password="password", email="superuser@test.com")
# Add to editor and manager groups to satisfy all custom mixins
for group_name in ["physics_checker", "cid_user_manager"]:
g, _ = Group.objects.get_or_create(name=group_name)
user.groups.add(g)
client = Client()
client.force_login(user)
return client
@pytest.fixture
def anonymous_client(db):
return Client()
@pytest.fixture
def smoke_data(db, settings, tmp_path):
from physics.models import Question, Exam, UserAnswer, Category
settings.MEDIA_ROOT = str(tmp_path)
create_user = User.objects.create_user(username="smoke_user", email="smoke_user@test.com")
category, _ = Category.objects.get_or_create(category="Test Category")
question = Question.objects.create(
stem="test question stem",
a="question a text",
a_feedback="question a feedback",
b="question b text",
b_feedback="question b feedback",
c="question c text",
c_feedback="question c feedback",
d="question d text",
d_feedback="question d feedback",
e="question e text",
e_feedback="question e feedback",
a_answer=True,
b_answer=False,
c_answer=True,
d_answer=False,
e_answer=True,
category=category,
)
exam = Exam.objects.create(
name="Test Physics Exam",
exam_mode=True,
active=True
)
exam.author.add(create_user)
# Link question to exam
from physics.models import ExamQuestionDetail
ExamQuestionDetail.objects.create(exam=exam, question=question, sort_order=1)
user_answer = UserAnswer.objects.create(
user=create_user,
exam=exam,
question=question,
a=True,
b=False,
c=True,
d=False,
e=True,
)
return {
"question": question,
"exam": exam,
"user_answer": user_answer,
}
URL_SPECS = [
("physics:question_view", None),
("physics:question_create", None),
("physics:question_create_exam", lambda d: {"pk": d["exam"].pk}),
("physics:question_detail", lambda d: {"pk": d["question"].pk}),
("physics:exam_markers", lambda d: {"pk": d["exam"].pk}),
("physics:exam_groups_edit", lambda d: {"pk": d["exam"].pk}),
("physics:exam_authors", lambda d: {"pk": d["exam"].pk}),
("physics:exam_clone", lambda d: {"exam_id": d["exam"].pk}),
("physics:exam_update", lambda d: {"pk": d["exam"].pk}),
("physics:exam_delete", lambda d: {"pk": d["exam"].pk}),
("physics:active_exams", None),
("physics:user_answer_table_view", None),
("physics:user_answer_view", lambda d: {"pk": d["user_answer"].pk}),
("physics:user_answer_delete", lambda d: {"pk": d["user_answer"].pk}),
("physics:exam_review_question_responses", lambda d: {"pk": d["exam"].pk, "q_index": 0}),
("physics:exam_review_question_summary", lambda d: {"pk": d["exam"].pk, "q_index": 0}),
("physics:index", None),
("physics:help", None),
]
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_anonymous(anonymous_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = anonymous_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Anonymous"
assert response.status_code in {200, 302, 403, 404}, f"Unexpected status code {response.status_code} on {url_name} for Anonymous"
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_candidate(candidate_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = candidate_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Candidate"
assert response.status_code in SAFE_STATUS_CODES, f"Unexpected status code {response.status_code} on {url_name} for Candidate"
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_superuser(superuser_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = superuser_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Superuser"
assert response.status_code in {200, 301, 302, 303, 403, 404}, f"Unexpected status code {response.status_code} on {url_name} for Superuser"
+10 -2
View File
@@ -75,10 +75,18 @@ from loguru import logger
class AuthorOrCheckerRequiredMixin(object):
def get_object(self, *args, **kwargs):
obj = super().get_object(*args, **kwargs)
if self.request.user.is_superuser:
return obj
if self.request.user.groups.filter(name="physics_checker").exists():
return obj
if self.request.user not in obj.author.all():
raise PermissionDenied() # or Http404
if hasattr(obj, "author"):
if self.request.user not in obj.author.all():
raise PermissionDenied()
elif hasattr(obj, "user"):
if self.request.user != obj.user:
raise PermissionDenied()
else:
raise PermissionDenied()
return obj
+5
View File
@@ -31,3 +31,8 @@ TASKS = {
"QUEUES": ["default"],
}
}
# Local media root for tests to avoid container-only paths in settings_local
MEDIA_ROOT = os.path.join(BASE_DIR, "media_test")
os.makedirs(MEDIA_ROOT, exist_ok=True)
+5 -4
View File
@@ -91,10 +91,11 @@ class RapidUserAnswerFilter(django_filters.FilterSet):
user=None,
request=None,
):
if not request.user.groups.filter(name="rapid_checker").exists():
queryset = queryset.filter(open_access=True) | queryset.filter(
author__id=request.user.id
)
if not request.user.is_superuser:
if not request.user.groups.filter(name="rapid_checker").exists():
queryset = queryset.filter(exam__open_access=True) | queryset.filter(
exam__author__id=request.user.id
)
super(RapidUserAnswerFilter, self).__init__(
data=data, queryset=queryset, prefix=prefix, request=request
)
+1
View File
@@ -0,0 +1 @@
# Package marker for tests
+163
View File
@@ -0,0 +1,163 @@
import pytest
import tempfile
from django.urls import reverse
from django.contrib.auth.models import Group, User
from django.test import Client
SAFE_STATUS_CODES = {200, 301, 302, 303, 403, 404, 405}
@pytest.fixture
def candidate_client(db):
user = User.objects.create_user(username="candidate", password="password", email="candidate@test.com")
client = Client()
client.force_login(user)
return client
@pytest.fixture
def superuser_client(db):
user = User.objects.create_superuser(username="superuser", password="password", email="superuser@test.com")
# Add to editor/checker/manager groups to satisfy all custom mixins
for group_name in ["rapid_checker", "cid_user_manager"]:
g, _ = Group.objects.get_or_create(name=group_name)
user.groups.add(g)
client = Client()
client.force_login(user)
return client
@pytest.fixture
def anonymous_client(db):
return Client()
@pytest.fixture
def smoke_data(db, settings, tmp_path):
from rapids.models import Rapid, RapidImage, Exam, UserAnswer, Answer, Abnormality, Region, Examination
settings.MEDIA_ROOT = str(tmp_path)
create_user = User.objects.create_user(username="smoke_user", email="smoke_user@test.com")
# Create dummy supporting model objects
abn, _ = Abnormality.objects.get_or_create(name="Test Abnormality")
reg, _ = Region.objects.get_or_create(name="Test Region")
exm_name, _ = Examination.objects.get_or_create(examination="Test Examination")
question = Rapid.objects.create(
laterality=Rapid.NONE,
history="Test History",
normal=False,
)
question.abnormality.add(abn)
question.region.add(reg)
question.examination.add(exm_name)
# Mocking image
image = tempfile.NamedTemporaryFile(
dir=settings.MEDIA_ROOT, suffix=".jpg", delete=False
)
image.flush()
image._committed = True
# Save dummy content to prevent issues with PIL/DICOM hashing
image.write(b"12345ABCD")
image.seek(0)
rapid_image = RapidImage(image=image.name, rapid=question)
rapid_image.save()
answer = Answer.objects.create(
question=question,
answer="testanswer",
answer_compare="testanswer",
status=Answer.MarkOptions.CORRECT,
)
exam = Exam.objects.create(
name="Test Rapids Exam",
exam_mode=True,
active=True
)
exam.author.add(create_user)
# Link question to exam
from rapids.models import ExamQuestionDetail
ExamQuestionDetail.objects.create(exam=exam, rapid=question, sort_order=1)
user_answer = UserAnswer.objects.create(
user=create_user,
exam=exam,
question=question,
normal=False,
answer="testanswer",
answer_compare="testanswer",
score=Answer.MarkOptions.CORRECT,
)
return {
"question": question,
"exam": exam,
"user_answer": user_answer,
"answer": answer,
}
URL_SPECS = [
("rapids:question_viewer", None),
("rapids:question_view", None),
("rapids:question_json", lambda d: {"pk": d["question"].pk}),
("rapids:question_json_unbased", lambda d: {"pk": d["question"].pk}),
("rapids:question_save_annotation", lambda d: {"pk": d["question"].pk}),
("rapids:rapid_split", lambda d: {"pk": d["question"].pk}),
("rapids:question_migrate_to_shorts", lambda d: {"pk": d["question"].pk}),
("rapids:rapid_clone", lambda d: {"pk": d["question"].pk}),
("rapids:rapid_scrap", lambda d: {"pk": d["question"].pk}),
("rapids:question_delete", lambda d: {"pk": d["question"].pk}),
("rapids:question_anonymise_dicom", lambda d: {"pk": d["question"].pk}),
("rapids:question_add_exam", lambda d: {"question_id": d["question"].pk}),
("rapids:confirm_answer", lambda d: {"answer_id": d["answer"].pk}),
("rapids:delete_answer", lambda d: {"answer_id": d["answer"].pk}),
("rapids:mark", lambda d: {"exam_pk": d["exam"].pk, "sk": 0}),
("rapids:mark_all", lambda d: {"exam_pk": d["exam"].pk, "sk": 0}),
("rapids:mark_review", lambda d: {"exam_pk": d["exam"].pk, "sk": 0}),
("rapids:exam_markers", lambda d: {"pk": d["exam"].pk}),
("rapids:exam_groups_edit", lambda d: {"pk": d["exam"].pk}),
("rapids:exam_authors", lambda d: {"pk": d["exam"].pk}),
("rapids:exam_create", None),
("rapids:exam_clone", lambda d: {"exam_id": d["exam"].pk}),
("rapids:exam_update", lambda d: {"pk": d["exam"].pk}),
("rapids:exam_migrate_rapids_to_shorts", lambda d: {"pk": d["exam"].pk}),
("rapids:exam_delete", lambda d: {"pk": d["exam"].pk}),
("rapids:question_create", None),
("rapids:question_create_exam", lambda d: {"pk": d["exam"].pk}),
("rapids:create_region", None),
("rapids:create_abnormality", None),
("rapids:create_examination", None),
("rapids:rapid_update", lambda d: {"pk": d["question"].pk}),
("rapids:question_answer_update", lambda d: {"pk": d["question"].pk}),
("rapids:user_answer_table_view", None),
("rapids:user_answer_view", lambda d: {"pk": d["user_answer"].pk}),
("rapids:user_answer_delete", lambda d: {"pk": d["user_answer"].pk}),
("rapids:index", None),
("rapids:help", None),
]
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_anonymous(anonymous_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = anonymous_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Anonymous"
assert response.status_code in {200, 302, 400, 403, 404}, f"Unexpected status code {response.status_code} on {url_name} for Anonymous"
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_candidate(candidate_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = candidate_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Candidate"
assert response.status_code in SAFE_STATUS_CODES or response.status_code == 400, f"Unexpected status code {response.status_code} on {url_name} for Candidate"
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_superuser(superuser_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = superuser_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Superuser"
assert response.status_code in {200, 301, 302, 303, 400, 403, 404}, f"Unexpected status code {response.status_code} on {url_name} for Superuser"
+4 -1
View File
@@ -115,6 +115,8 @@ def normaliseScore(score):
class AuthorOrCheckerRequiredMixin(object):
def get_object(self, *args, **kwargs):
obj = super().get_object(*args, **kwargs)
if self.request.user.is_superuser:
return obj
if self.request.user.groups.filter(name="rapid_checker").exists():
return obj
if self.request.user not in obj.author.all():
@@ -950,7 +952,8 @@ def question_json(request, pk):
def question_viewer(request):
# question = get_object_or_404(Rapid, pk=pk)
ids = request.GET.get("ids").split(",")
ids_param = request.GET.get("ids")
ids = ids_param.split(",") if ids_param else []
return render(
request,
-3
View File
@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.
+1
View File
@@ -0,0 +1 @@
# RCR tests package
+3 -3
View File
@@ -21,9 +21,9 @@ def create_cid_manager(db, django_user_model):
@pytest.fixture
def create_groups(db):
Group.objects.create(name="rcr_radiology_assessor")
Group.objects.create(name="rcr_oncology_assessor")
Group.objects.create(name="rcr_assessor")
Group.objects.get_or_create(name="rcr_radiology_assessor")
Group.objects.get_or_create(name="rcr_oncology_assessor")
Group.objects.get_or_create(name="rcr_assessor")
@pytest.fixture
+97
View File
@@ -0,0 +1,97 @@
import pytest
from django.urls import reverse
from django.contrib.auth.models import Group, User
from django.test import Client
SAFE_STATUS_CODES = {200, 301, 302, 303, 400, 403, 404, 405}
@pytest.fixture
def candidate_client(db):
user = User.objects.create_user(username="candidate", password="password", email="candidate@test.com")
client = Client()
client.force_login(user)
return client
@pytest.fixture
def assessor_client(db):
user = User.objects.create_user(username="assessor", password="password", email="assessor@test.com")
for group_name in ["rcr_radiology_assessor", "rcr_oncology_assessor", "rcr_assessor"]:
g, _ = Group.objects.get_or_create(name=group_name)
user.groups.add(g)
client = Client()
client.force_login(user)
return client
@pytest.fixture
def superuser_client(db):
user = User.objects.create_superuser(username="superuser", password="password", email="superuser@test.com")
for group_name in ["rcr_radiology_assessor", "rcr_oncology_assessor", "rcr_assessor", "cid_user_manager"]:
g, _ = Group.objects.get_or_create(name=group_name)
user.groups.add(g)
client = Client()
client.force_login(user)
return client
@pytest.fixture
def anonymous_client(db):
return Client()
@pytest.fixture
def smoke_data(db, create_item, create_radiology_user):
return {
"item": create_item,
"user": create_radiology_user,
}
URL_SPECS = [
("rcr:radiology_index_view", None),
("rcr:radiology_index_all_view", None),
("rcr:radiology_gap", None),
("rcr:radiology_results", None),
("rcr:radiology_review_view", None),
("rcr:radiology_index_completed_view", None),
("rcr:radiology_assessors_view", None),
("rcr:radiology_assigned_user_view", lambda d: {"user_pk": d["user"].pk}),
("rcr:radiology_assessor_assignment_view", lambda d: {"assessor_pk": d["user"].pk}),
("rcr:radiology_index_completed_by_user_view", lambda d: {"user_pk": d["user"].pk}),
("rcr:radiology_detail_view", lambda d: {"pk": d["item"].pk}),
("rcr:radiology_update_next_view", None),
("rcr:radiology_update_next_skip_view", lambda d: {"skip": 0}),
("rcr:radiology_update_view", lambda d: {"pk": d["item"].pk}),
]
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_anonymous(anonymous_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = anonymous_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Anonymous"
assert response.status_code in {200, 302, 400, 403, 404, 405}, f"Unexpected status code {response.status_code} on {url_name} for Anonymous"
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_candidate(candidate_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = candidate_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Candidate"
assert response.status_code in SAFE_STATUS_CODES, f"Unexpected status code {response.status_code} on {url_name} for Candidate"
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_assessor(assessor_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = assessor_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Assessor"
assert response.status_code in SAFE_STATUS_CODES, f"Unexpected status code {response.status_code} on {url_name} for Assessor"
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_superuser(superuser_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = superuser_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Superuser"
assert response.status_code in SAFE_STATUS_CODES, f"Unexpected status code {response.status_code} on {url_name} for Superuser"
@@ -0,0 +1,14 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block content %}
<div class="container mt-4">
<h2>Update Rota Details</h2>
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<button type="submit" class="btn btn-primary mt-3">Save changes</button>
</form>
</div>
{% block content_extra %}{% endblock %}
{% endblock %}
-3
View File
@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.
+1
View File
@@ -0,0 +1 @@
# Rota tests package
+65
View File
@@ -0,0 +1,65 @@
import pytest
from django.urls import reverse
from django.contrib.auth.models import User
from django.test import Client
SAFE_STATUS_CODES = {200, 301, 302, 303, 400, 403, 404, 405}
@pytest.fixture
def candidate_client(db):
user = User.objects.create_user(username="candidate", password="password", email="candidate@test.com")
client = Client()
client.force_login(user)
return client
@pytest.fixture
def superuser_client(db):
user = User.objects.create_superuser(username="superuser", password="password", email="superuser@test.com")
client = Client()
client.force_login(user)
return client
@pytest.fixture
def anonymous_client(db):
return Client()
@pytest.fixture
def smoke_data(db):
from rota.models import UserRotaDetail
user = User.objects.create_user(username="smoke_user", password="password", email="smoke@test.com")
detail = UserRotaDetail.objects.create(user=user)
return {
"user": user,
"detail": detail,
}
URL_SPECS = [
("rota:user_details_update", lambda d: {"pk": d["detail"].pk}),
]
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_anonymous(anonymous_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = anonymous_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Anonymous"
assert response.status_code in {200, 302, 400, 403, 404, 405}, f"Unexpected status code {response.status_code} on {url_name} for Anonymous"
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_candidate(candidate_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = candidate_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Candidate"
assert response.status_code in SAFE_STATUS_CODES, f"Unexpected status code {response.status_code} on {url_name} for Candidate"
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_superuser(superuser_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = superuser_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Superuser"
assert response.status_code in SAFE_STATUS_CODES, f"Unexpected status code {response.status_code} on {url_name} for Superuser"
+3 -2
View File
@@ -106,7 +106,8 @@ class UserAnswerFilter(django_filters.FilterSet):
)
def __init__(self, data=None, queryset=None, prefix=None, strict=None, user=None, request=None):
if not request.user.groups.filter(name="sba_checker").exists():
queryset = queryset.filter(open_access=True) | queryset.filter(author__id=request.user.id)
if not request.user.is_superuser:
if not request.user.groups.filter(name="sba_checker").exists():
queryset = queryset.filter(exam__open_access=True) | queryset.filter(exam__author__id=request.user.id)
super(UserAnswerFilter, self).__init__(data=data, queryset=queryset, prefix=prefix, request=request)
pass
@@ -0,0 +1,7 @@
{% extends 'sbas/base.html' %}
{% block content %}
<form method="post">{% csrf_token %}
<p>Are you sure you want to delete "{{ object }}"?</p>
<input type="submit" value="Confirm" class="btn btn-danger">
</form>
{% endblock %}
-3
View File
@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.
+1
View File
@@ -0,0 +1 @@
# Package marker for tests
+131
View File
@@ -0,0 +1,131 @@
import pytest
from django.urls import reverse
from django.contrib.auth.models import Group, User
from django.test import Client
SAFE_STATUS_CODES = {200, 301, 302, 303, 403, 404, 405}
@pytest.fixture
def candidate_client(db):
user = User.objects.create_user(username="candidate", password="password", email="candidate@test.com")
client = Client()
client.force_login(user)
return client
@pytest.fixture
def superuser_client(db):
user = User.objects.create_superuser(username="superuser", password="password", email="superuser@test.com")
for group_name in ["sba_checker", "cid_user_manager"]:
g, _ = Group.objects.get_or_create(name=group_name)
user.groups.add(g)
client = Client()
client.force_login(user)
return client
@pytest.fixture
def anonymous_client(db):
return Client()
@pytest.fixture
def smoke_data(db, settings, tmp_path):
from sbas.models import Question, Exam, UserAnswer, Category
settings.MEDIA_ROOT = str(tmp_path)
create_user = User.objects.create_user(username="smoke_user", email="smoke_user@test.com")
cat, _ = Category.objects.get_or_create(category="Test Category")
question = Question.objects.create(
stem="Test Stem",
a_answer="Ans A",
b_answer="Ans B",
c_answer="Ans C",
d_answer="Ans D",
e_answer="Ans E",
best_answer="a",
category=cat,
)
exam = Exam.objects.create(
name="Test SBA Exam",
exam_mode=True,
active=True
)
exam.author.add(create_user)
from sbas.models import ExamQuestionDetail
ExamQuestionDetail.objects.create(exam=exam, question=question, sort_order=1)
user_answer = UserAnswer.objects.create(
user=create_user,
exam=exam,
question=question,
answer="a",
)
return {
"question": question,
"exam": exam,
"user_answer": user_answer,
}
URL_SPECS = [
("sbas:question_view", None),
("sbas:question_create", None),
("sbas:question_update", lambda d: {"pk": d["question"].pk}),
("sbas:question_clone", lambda d: {"pk": d["question"].pk}),
("sbas:question_delete", lambda d: {"pk": d["question"].pk}),
("sbas:exam_markers", lambda d: {"pk": d["exam"].pk}),
("sbas:exam_groups_edit", lambda d: {"pk": d["exam"].pk}),
("sbas:exam_authors", lambda d: {"pk": d["exam"].pk}),
("sbas:exam_clone", lambda d: {"exam_id": d["exam"].pk}),
("sbas:exam_clone2", lambda d: {"exam_id": d["exam"].pk}),
("sbas:active_exams", None),
("sbas:exam_create", None),
("sbas:exam_update", lambda d: {"pk": d["exam"].pk}),
("sbas:exam_delete", lambda d: {"pk": d["exam"].pk}),
("sbas:user_answer_table_view", None),
("sbas:user_answer_view", lambda d: {"pk": d["user_answer"].pk}),
("sbas:user_answer_delete", lambda d: {"pk": d["user_answer"].pk}),
("sbas:llm_prompt_view", None),
("sbas:import_llm_questions", None),
("sbas:import_llm_confirm", None),
("sbas:exam_create_from_filters_with_name", None),
("sbas:generate_exam", None),
("sbas:question_overview", None),
("sbas:question_search_page", None),
("sbas:question_search_partial", None),
("sbas:exam_review_question_responses", lambda d: {"pk": d["exam"].pk, "q_index": 0}),
("sbas:exam_review_question_summary", lambda d: {"pk": d["exam"].pk, "q_index": 0}),
("sbas:toggle_frcr", lambda d: {"pk": d["question"].pk}),
("sbas:merge_category", None),
("sbas:index", None),
("sbas:help", None),
]
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_anonymous(anonymous_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = anonymous_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Anonymous"
assert response.status_code in {200, 302, 400, 403, 404, 405}, f"Unexpected status code {response.status_code} on {url_name} for Anonymous"
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_candidate(candidate_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = candidate_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Candidate"
assert response.status_code in SAFE_STATUS_CODES or response.status_code == 400, f"Unexpected status code {response.status_code} on {url_name} for Candidate"
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_superuser(superuser_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = superuser_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Superuser"
assert response.status_code in {200, 301, 302, 303, 400, 403, 404, 405}, f"Unexpected status code {response.status_code} on {url_name} for Superuser"
+2
View File
@@ -207,6 +207,8 @@ def exam_review_question_summary(request, pk: int, q_index: int):
class AuthorOrCheckerRequiredMixin(object):
def get_object(self, *args, **kwargs):
obj = super().get_object(*args, **kwargs)
if self.request.user.is_superuser:
return obj
if self.request.user.groups.filter(name="sbas_checker").exists():
return obj
if self.request.user not in obj.author.all():
+5 -4
View File
@@ -86,10 +86,11 @@ class QuestionUserAnswerFilter(django_filters.FilterSet):
user=None,
request=None,
):
if not request.user.groups.filter(name="shorts_checker").exists():
queryset = queryset.filter(open_access=True) | queryset.filter(
author__id=request.user.id
)
if not request.user.is_superuser:
if not request.user.groups.filter(name="shorts_checker").exists():
queryset = queryset.filter(exam__open_access=True) | queryset.filter(
exam__author__id=request.user.id
)
super(QuestionUserAnswerFilter, self).__init__(
data=data, queryset=queryset, prefix=prefix, request=request
)
@@ -0,0 +1,7 @@
{% extends 'shorts/base.html' %}
{% block content %}
<form method="post">{% csrf_token %}
<p>Are you sure you want to delete "{{ object }}"?</p>
<input type="submit" value="Confirm" class="btn btn-danger">
</form>
{% endblock %}
@@ -0,0 +1,8 @@
<div id="single-dicom-viewer" class="dicom-viewer" data-images="" data-annotations=''>
</div>
<button class="previous-btn">Previous</button>
<button class="next-btn">Next</button>
{% for id in ids %}
{{id}}
{% endfor %}
@@ -0,0 +1,31 @@
{% extends 'shorts/base.html' %}
{% load render_table from django_tables2 %}
{% block css %}
{% endblock %}
{% block content %}
<div id="view-filter-options">
<h3>Shorts User Answers</h3>
<form action="" method="get">
{{ filter.form }}
<input class="btn btn-primary btn-sm mt-1 mb-1" type="submit" />
</form>
</div>
{% render_table table %}
<button id="delete-selected-button">Delete selected answers</button>
{% endblock %}
{% block js %}
<script>
$(document).ready(function () {
$("#delete-selected-button").on("click", () => {
window.delete_multiple(
"{% url 'shorts:user_answer_delete_multiple' %}", "{{ csrf_token }}"
)
})
});
</script>
{% endblock %}
@@ -0,0 +1,12 @@
{% extends 'shorts/base.html' %}
{% block content %}
CID: {{useranswer.cid}}<br/>
User: {{useranswer.user}}<br/>
Exam: <a href="{% url 'shorts:exam_overview' useranswer.exam.pk %}">{{useranswer.exam}}</a><br />
Question: <a href="{% url 'shorts:question_detail' useranswer.question.pk %}">{{useranswer.question}}</a><br />
Answer: <pre>{{useranswer.answer}}</pre>
Score: {{useranswer.get_answer_score}}<br/>
<a href="{% url 'shorts:user_answer_delete' useranswer.id %}">Delete answer</a>
<a href="{% url 'admin:shorts_useranswer_change' useranswer.id %}">Admin Edit</a>
{% endblock content %}
-3
View File
@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.
+1
View File
@@ -0,0 +1 @@
# Package marker for tests
+151
View File
@@ -0,0 +1,151 @@
import pytest
import tempfile
from django.urls import reverse
from django.contrib.auth.models import Group, User
from django.test import Client
SAFE_STATUS_CODES = {200, 301, 302, 303, 403, 404, 405}
@pytest.fixture
def candidate_client(db):
user = User.objects.create_user(username="candidate", password="password", email="candidate@test.com")
client = Client()
client.force_login(user)
return client
@pytest.fixture
def superuser_client(db):
user = User.objects.create_superuser(username="superuser", password="password", email="superuser@test.com")
for group_name in ["shorts_checker", "cid_user_manager"]:
g, _ = Group.objects.get_or_create(name=group_name)
user.groups.add(g)
client = Client()
client.force_login(user)
return client
@pytest.fixture
def anonymous_client(db):
return Client()
@pytest.fixture
def smoke_data(db, settings, tmp_path):
from shorts.models import Question, QuestionImage, Exam, UserAnswer, QuestionFinding
from rapids.models import Examination
settings.MEDIA_ROOT = str(tmp_path)
create_user = User.objects.create_user(username="smoke_user", email="smoke_user@test.com")
exm_name, _ = Examination.objects.get_or_create(examination="Test Examination")
question = Question.objects.create(
history="Test History",
marking_guidance="Test Guidance",
)
question.examination.add(exm_name)
# Mocking image
image = tempfile.NamedTemporaryFile(
dir=settings.MEDIA_ROOT, suffix=".jpg", delete=False
)
image.flush()
image._committed = True
image.write(b"12345ABCD")
image.seek(0)
q_image = QuestionImage(image=image.name, question=question)
q_image.save()
exam = Exam.objects.create(
name="Test Shorts Exam",
exam_mode=True,
active=True
)
exam.author.add(create_user)
from shorts.models import ExamQuestionDetail
ExamQuestionDetail.objects.create(exam=exam, question=question, sort_order=1)
user_answer = UserAnswer.objects.create(
user=create_user,
exam=exam,
question=question,
answer="testanswer",
score=2,
)
finding = QuestionFinding.objects.create(
question=question,
description="Test Finding",
)
return {
"question": question,
"exam": exam,
"user_answer": user_answer,
"finding": finding,
}
URL_SPECS = [
("shorts:question_viewer", None),
("shorts:question_view", None),
("shorts:question_json", lambda d: {"pk": d["question"].pk}),
("shorts:question_json_unbased", lambda d: {"pk": d["question"].pk}),
("shorts:question_save_annotation", lambda d: {"pk": d["question"].pk}),
("shorts:question_split", lambda d: {"pk": d["question"].pk}),
("shorts:question_clone", lambda d: {"pk": d["question"].pk}),
("shorts:question_delete", lambda d: {"pk": d["question"].pk}),
("shorts:question_anonymise_dicom", lambda d: {"pk": d["question"].pk}),
("shorts:question_add_exam", lambda d: {"question_id": d["question"].pk}),
("shorts:combine_images_side_by_side", lambda d: {"question_id": d["question"].pk}),
("shorts:mark_answer", lambda d: {"exam_id": d["exam"].pk, "question_number": 0, "answer_id": d["user_answer"].pk}),
("shorts:mark_answer_override", lambda d: {"exam_id": d["exam"].pk, "question_number": 0, "answer_id": d["user_answer"].pk}),
("shorts:mark", lambda d: {"exam_pk": d["exam"].pk, "sk": 0}),
("shorts:mark_all", lambda d: {"exam_pk": d["exam"].pk, "sk": 0}),
("shorts:mark_review", lambda d: {"exam_pk": d["exam"].pk, "sk": 0}),
("shorts:exam_markers", lambda d: {"pk": d["exam"].pk}),
("shorts:exam_groups_edit", lambda d: {"pk": d["exam"].pk}),
("shorts:exam_authors", lambda d: {"pk": d["exam"].pk}),
("shorts:exam_create", None),
("shorts:exam_clone", lambda d: {"exam_id": d["exam"].pk}),
("shorts:exam_update", lambda d: {"pk": d["exam"].pk}),
("shorts:exam_delete", lambda d: {"pk": d["exam"].pk}),
("shorts:question_create", None),
("shorts:question_create_exam", lambda d: {"pk": d["exam"].pk}),
("shorts:question_update", lambda d: {"pk": d["question"].pk}),
("shorts:question_sample_answers", lambda d: {"pk": d["question"].pk}),
("shorts:user_answer_table_view", None),
("shorts:user_answer_view", lambda d: {"pk": d["user_answer"].pk}),
("shorts:user_answer_delete", lambda d: {"pk": d["user_answer"].pk}),
("shorts:question_findings", lambda d: {"question_id": d["question"].pk}),
("shorts:add_finding", None),
("shorts:delete_finding", lambda d: {"pk": d["finding"].pk}),
("shorts:question_edit_finding", lambda d: {"question_id": d["question"].pk, "finding_pk": d["finding"].pk}),
("shorts:index", None),
("shorts:help", None),
]
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_anonymous(anonymous_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = anonymous_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Anonymous"
assert response.status_code in {200, 302, 400, 403, 404, 405}, f"Unexpected status code {response.status_code} on {url_name} for Anonymous"
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_candidate(candidate_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = candidate_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Candidate"
assert response.status_code in SAFE_STATUS_CODES or response.status_code == 400, f"Unexpected status code {response.status_code} on {url_name} for Candidate"
@pytest.mark.django_db
@pytest.mark.parametrize("url_name, kwargs_lambda", URL_SPECS)
def test_url_smoke_superuser(superuser_client, smoke_data, url_name, kwargs_lambda):
kwargs = kwargs_lambda(smoke_data) if kwargs_lambda else None
url = reverse(url_name, kwargs=kwargs)
response = superuser_client.get(url, follow=False)
assert response.status_code != 500, f"500 Server Error on {url_name} ({url}) for Superuser"
assert response.status_code in {200, 301, 302, 303, 400, 403, 404, 405}, f"Unexpected status code {response.status_code} on {url_name} for Superuser"
+9 -5
View File
@@ -107,6 +107,8 @@ def normaliseScore(score):
class AuthorOrCheckerRequiredMixin(object):
def get_object(self, *args, **kwargs):
obj = super().get_object(*args, **kwargs)
if self.request.user.is_superuser:
return obj
if self.request.user.groups.filter(name="shorts_checker").exists():
return obj
if self.request.user not in obj.author.all():
@@ -351,8 +353,8 @@ def mark_all(request, exam_pk, sk):
def mark_review(request, exam_pk, sk):
return mark(request, exam_pk, sk, unmarked_exam_answers_only=False, review=True)
def mark_answer_override(request, exam_id, question_number, cid):
return mark_answer(request, exam_id, question_number, cid, override=True)
def mark_answer_override(request, exam_id, question_number, answer_id):
return mark_answer(request, exam_id, question_number, answer_id, override=True)
def mark_answer(request, exam_id, question_number, answer_id, override=False):
@@ -548,9 +550,8 @@ def mark_answer(request, exam_id, question_number, answer_id, override=False):
},
)
# @user_passes_test(user_is_admin, login_url="/accounts/login")
@login_required
def mark(request, exam_pk, sk, unmarked_exam_answers_only=True):
def mark(request, exam_pk, sk, unmarked_exam_answers_only=True, review=False):
exam = get_object_or_404(Exam, pk=exam_pk)
if not GenericExamViews.check_user_marker_access(request.user, exam_pk):
@@ -591,6 +592,7 @@ def mark(request, exam_pk, sk, unmarked_exam_answers_only=True):
"user_answers": user_answers,
"unmarked_count": unmarked_count,
"marker_unmarked_count": marker_unmarked_count,
"review": review,
},
)
else:
@@ -603,6 +605,7 @@ def mark(request, exam_pk, sk, unmarked_exam_answers_only=True):
"question_details": question_details,
"user_answers": user_answers,
"unmarked_count": unmarked_count,
"review": review,
},
)
@@ -756,7 +759,8 @@ def question_json(request, pk):
def question_viewer(request):
# question = get_object_or_404(Question, pk=pk)
ids = request.GET.get("ids").split(",")
ids_param = request.GET.get("ids")
ids = ids_param.split(",") if ids_param else []
return render(
request,