diff --git a/anatomy/filters.py b/anatomy/filters.py
index a4748a71..ab71f568 100644
--- a/anatomy/filters.py
+++ b/anatomy/filters.py
@@ -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
\ No newline at end of file
diff --git a/anatomy/tests/test_url_smoke.py b/anatomy/tests/test_url_smoke.py
new file mode 100644
index 00000000..5dccddc7
--- /dev/null
+++ b/anatomy/tests/test_url_smoke.py
@@ -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"
diff --git a/anatomy/views.py b/anatomy/views.py
index 517ea135..9b9d7508 100644
--- a/anatomy/views.py
+++ b/anatomy/views.py
@@ -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():
diff --git a/atlas/tests/__init__.py b/atlas/tests/__init__.py
new file mode 100644
index 00000000..978024c1
--- /dev/null
+++ b/atlas/tests/__init__.py
@@ -0,0 +1 @@
+# Atlas tests package
diff --git a/generic/models.py b/generic/models.py
index e80228c4..5bc1286d 100644
--- a/generic/models.py
+++ b/generic/models.py
@@ -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 ""
diff --git a/generic/templates/generic/help.html b/generic/templates/generic/help.html
new file mode 100644
index 00000000..0554cc05
--- /dev/null
+++ b/generic/templates/generic/help.html
@@ -0,0 +1,14 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+
+
+
{{ app_name|title }} Help
+
Welcome to the radiology education platform. There is no custom help document for the {{ app_name }} application yet.
+
+
Go Back
+
+
+
+{% endblock %}
diff --git a/generic/tests/test_url_smoke.py b/generic/tests/test_url_smoke.py
new file mode 100644
index 00000000..32846525
--- /dev/null
+++ b/generic/tests/test_url_smoke.py
@@ -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"
diff --git a/generic/views.py b/generic/views.py
index 444758dc..c55d86c9 100644
--- a/generic/views.py
+++ b/generic/views.py
@@ -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
diff --git a/generic/widgets.py b/generic/widgets.py
index 10c682f1..d9ca3bfe 100644
--- a/generic/widgets.py
+++ b/generic/widgets.py
@@ -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}"
diff --git a/longs/filters.py b/longs/filters.py
index a93cc853..b0246010 100755
--- a/longs/filters.py
+++ b/longs/filters.py
@@ -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
\ No newline at end of file
diff --git a/longs/tests.py b/longs/tests.py
deleted file mode 100755
index 7ce503c2..00000000
--- a/longs/tests.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.test import TestCase
-
-# Create your tests here.
diff --git a/longs/tests/__init__.py b/longs/tests/__init__.py
new file mode 100644
index 00000000..b67e1bb2
--- /dev/null
+++ b/longs/tests/__init__.py
@@ -0,0 +1 @@
+# Package marker for tests
diff --git a/longs/tests/test_url_smoke.py b/longs/tests/test_url_smoke.py
new file mode 100644
index 00000000..49f9ce40
--- /dev/null
+++ b/longs/tests/test_url_smoke.py
@@ -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"
diff --git a/longs/views.py b/longs/views.py
index 86571649..024d4787 100755
--- a/longs/views.py
+++ b/longs/views.py
@@ -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)
diff --git a/media_test/tmpam34uuxi.jpg b/media_test/tmpam34uuxi.jpg
new file mode 100644
index 00000000..e69de29b
diff --git a/media_test/tmpn6geswcc.jpg b/media_test/tmpn6geswcc.jpg
new file mode 100644
index 00000000..e69de29b
diff --git a/media_test/tmpqmjkbq5c.jpg b/media_test/tmpqmjkbq5c.jpg
new file mode 100644
index 00000000..e69de29b
diff --git a/media_testanatomy/exam/1.json b/media_testanatomy/exam/1.json
new file mode 100644
index 00000000..cd65ff91
--- /dev/null
+++ b/media_testanatomy/exam/1.json
@@ -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}
\ No newline at end of file
diff --git a/oef/templates/oef/formats_update_form.html b/oef/templates/oef/formats_update_form.html
index 73695ac2..16994d83 100644
--- a/oef/templates/oef/formats_update_form.html
+++ b/oef/templates/oef/formats_update_form.html
@@ -6,7 +6,8 @@
-{% if request.user.is_superuser %}
+{% if object %}
+{% if request.user.is_superuser and object.id %}
Admin
{% endif %}
@@ -22,6 +23,8 @@
{% endif %}
">{{ entry.exam_code }} / {{ entry.exam_name }}
{% endfor %}
+
+{% endif %}
{% endblock content %}
diff --git a/oef/tests.py b/oef/tests.py
deleted file mode 100644
index 7ce503c2..00000000
--- a/oef/tests.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.test import TestCase
-
-# Create your tests here.
diff --git a/oef/tests/__init__.py b/oef/tests/__init__.py
new file mode 100644
index 00000000..41113998
--- /dev/null
+++ b/oef/tests/__init__.py
@@ -0,0 +1 @@
+# OEF tests package
diff --git a/oef/tests/test_url_smoke.py b/oef/tests/test_url_smoke.py
new file mode 100644
index 00000000..d9ef1f03
--- /dev/null
+++ b/oef/tests/test_url_smoke.py
@@ -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"
diff --git a/physics/filters.py b/physics/filters.py
index affa030f..42517305 100644
--- a/physics/filters.py
+++ b/physics/filters.py
@@ -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
\ No newline at end of file
diff --git a/physics/tests.py b/physics/tests.py
deleted file mode 100644
index 7ce503c2..00000000
--- a/physics/tests.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.test import TestCase
-
-# Create your tests here.
diff --git a/physics/tests/__init__.py b/physics/tests/__init__.py
new file mode 100644
index 00000000..b67e1bb2
--- /dev/null
+++ b/physics/tests/__init__.py
@@ -0,0 +1 @@
+# Package marker for tests
diff --git a/physics/tests/test_url_smoke.py b/physics/tests/test_url_smoke.py
new file mode 100644
index 00000000..4b54becf
--- /dev/null
+++ b/physics/tests/test_url_smoke.py
@@ -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"
diff --git a/physics/views.py b/physics/views.py
index ef1e59ec..ba3a5ab7 100644
--- a/physics/views.py
+++ b/physics/views.py
@@ -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
diff --git a/rad/settings_test.py b/rad/settings_test.py
index a200d53f..605aa94f 100644
--- a/rad/settings_test.py
+++ b/rad/settings_test.py
@@ -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)
+
diff --git a/rapids/filters.py b/rapids/filters.py
index c8e9fdd6..66cf8bb1 100755
--- a/rapids/filters.py
+++ b/rapids/filters.py
@@ -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
)
diff --git a/rapids/tests/__init__.py b/rapids/tests/__init__.py
new file mode 100644
index 00000000..b67e1bb2
--- /dev/null
+++ b/rapids/tests/__init__.py
@@ -0,0 +1 @@
+# Package marker for tests
diff --git a/rapids/tests/test_url_smoke.py b/rapids/tests/test_url_smoke.py
new file mode 100644
index 00000000..ad31a142
--- /dev/null
+++ b/rapids/tests/test_url_smoke.py
@@ -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"
diff --git a/rapids/views.py b/rapids/views.py
index 112a8797..9613ad3e 100755
--- a/rapids/views.py
+++ b/rapids/views.py
@@ -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,
diff --git a/rcr/tests.py b/rcr/tests.py
deleted file mode 100644
index 7ce503c2..00000000
--- a/rcr/tests.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.test import TestCase
-
-# Create your tests here.
diff --git a/rcr/tests/__init__.py b/rcr/tests/__init__.py
new file mode 100644
index 00000000..a1d258fa
--- /dev/null
+++ b/rcr/tests/__init__.py
@@ -0,0 +1 @@
+# RCR tests package
diff --git a/rcr/tests/conftest.py b/rcr/tests/conftest.py
index 41d83f8b..d18a12fe 100644
--- a/rcr/tests/conftest.py
+++ b/rcr/tests/conftest.py
@@ -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
diff --git a/rcr/tests/test_url_smoke.py b/rcr/tests/test_url_smoke.py
new file mode 100644
index 00000000..40b7bbe1
--- /dev/null
+++ b/rcr/tests/test_url_smoke.py
@@ -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"
diff --git a/rota/templates/rota/userrotadetail_form.html b/rota/templates/rota/userrotadetail_form.html
new file mode 100644
index 00000000..9544f656
--- /dev/null
+++ b/rota/templates/rota/userrotadetail_form.html
@@ -0,0 +1,14 @@
+{% extends 'base.html' %}
+{% load crispy_forms_tags %}
+
+{% block content %}
+
+
Update Rota Details
+
+
+{% block content_extra %}{% endblock %}
+{% endblock %}
diff --git a/rota/tests.py b/rota/tests.py
deleted file mode 100644
index 7ce503c2..00000000
--- a/rota/tests.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.test import TestCase
-
-# Create your tests here.
diff --git a/rota/tests/__init__.py b/rota/tests/__init__.py
new file mode 100644
index 00000000..54d1617b
--- /dev/null
+++ b/rota/tests/__init__.py
@@ -0,0 +1 @@
+# Rota tests package
diff --git a/rota/tests/test_url_smoke.py b/rota/tests/test_url_smoke.py
new file mode 100644
index 00000000..15e46796
--- /dev/null
+++ b/rota/tests/test_url_smoke.py
@@ -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"
diff --git a/sbas/filters.py b/sbas/filters.py
index 580346d5..3c39c280 100644
--- a/sbas/filters.py
+++ b/sbas/filters.py
@@ -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
\ No newline at end of file
diff --git a/sbas/templates/sbas/question_confirm_delete.html b/sbas/templates/sbas/question_confirm_delete.html
new file mode 100644
index 00000000..5120595b
--- /dev/null
+++ b/sbas/templates/sbas/question_confirm_delete.html
@@ -0,0 +1,7 @@
+{% extends 'sbas/base.html' %}
+{% block content %}
+
+{% endblock %}
diff --git a/sbas/tests.py b/sbas/tests.py
deleted file mode 100644
index 7ce503c2..00000000
--- a/sbas/tests.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.test import TestCase
-
-# Create your tests here.
diff --git a/sbas/tests/__init__.py b/sbas/tests/__init__.py
new file mode 100644
index 00000000..b67e1bb2
--- /dev/null
+++ b/sbas/tests/__init__.py
@@ -0,0 +1 @@
+# Package marker for tests
diff --git a/sbas/tests/test_url_smoke.py b/sbas/tests/test_url_smoke.py
new file mode 100644
index 00000000..7ef7470b
--- /dev/null
+++ b/sbas/tests/test_url_smoke.py
@@ -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"
diff --git a/sbas/views.py b/sbas/views.py
index 037f9d05..84547b99 100644
--- a/sbas/views.py
+++ b/sbas/views.py
@@ -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():
diff --git a/shorts/filters.py b/shorts/filters.py
index 8d1538f6..133100d5 100755
--- a/shorts/filters.py
+++ b/shorts/filters.py
@@ -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
)
diff --git a/shorts/templates/shorts/question_confirm_delete.html b/shorts/templates/shorts/question_confirm_delete.html
new file mode 100644
index 00000000..5e92fd70
--- /dev/null
+++ b/shorts/templates/shorts/question_confirm_delete.html
@@ -0,0 +1,7 @@
+{% extends 'shorts/base.html' %}
+{% block content %}
+
+{% endblock %}
diff --git a/shorts/templates/shorts/question_viewer.html b/shorts/templates/shorts/question_viewer.html
new file mode 100644
index 00000000..c6e63059
--- /dev/null
+++ b/shorts/templates/shorts/question_viewer.html
@@ -0,0 +1,8 @@
+
+
+
+
+
+{% for id in ids %}
+{{id}}
+{% endfor %}
diff --git a/shorts/templates/shorts/user_answer_question_view.html b/shorts/templates/shorts/user_answer_question_view.html
new file mode 100644
index 00000000..e0fbe01e
--- /dev/null
+++ b/shorts/templates/shorts/user_answer_question_view.html
@@ -0,0 +1,31 @@
+{% extends 'shorts/base.html' %}
+
+{% load render_table from django_tables2 %}
+{% block css %}
+{% endblock %}
+
+{% block content %}
+
+
+
Shorts User Answers
+
+
+{% render_table table %}
+
+
+{% endblock %}
+
+{% block js %}
+
+{% endblock %}
diff --git a/shorts/templates/shorts/useranswer_detail.html b/shorts/templates/shorts/useranswer_detail.html
new file mode 100644
index 00000000..560b57bf
--- /dev/null
+++ b/shorts/templates/shorts/useranswer_detail.html
@@ -0,0 +1,12 @@
+{% extends 'shorts/base.html' %}
+
+{% block content %}
+ CID: {{useranswer.cid}}
+ User: {{useranswer.user}}
+ Exam: {{useranswer.exam}}
+ Question: {{useranswer.question}}
+ Answer: {{useranswer.answer}}
+ Score: {{useranswer.get_answer_score}}
+ Delete answer
+ Admin Edit
+{% endblock content %}
diff --git a/shorts/tests.py b/shorts/tests.py
deleted file mode 100644
index 7ce503c2..00000000
--- a/shorts/tests.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.test import TestCase
-
-# Create your tests here.
diff --git a/shorts/tests/__init__.py b/shorts/tests/__init__.py
new file mode 100644
index 00000000..b67e1bb2
--- /dev/null
+++ b/shorts/tests/__init__.py
@@ -0,0 +1 @@
+# Package marker for tests
diff --git a/shorts/tests/test_url_smoke.py b/shorts/tests/test_url_smoke.py
new file mode 100644
index 00000000..6c5d6d25
--- /dev/null
+++ b/shorts/tests/test_url_smoke.py
@@ -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"
diff --git a/shorts/views.py b/shorts/views.py
index 1aefb9fd..00519960 100644
--- a/shorts/views.py
+++ b/shorts/views.py
@@ -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,