smoke test the rest of the apps
This commit is contained in:
+3
-2
@@ -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
|
||||
@@ -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"
|
||||
@@ -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():
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# Atlas tests package
|
||||
+20
-15
@@ -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 ""
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1 @@
|
||||
# Package marker for tests
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1 @@
|
||||
# OEF tests package
|
||||
@@ -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
@@ -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
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1 @@
|
||||
# Package marker for tests
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# Package marker for tests
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1 @@
|
||||
# RCR tests package
|
||||
@@ -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
|
||||
|
||||
@@ -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 %}
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1 @@
|
||||
# Rota tests package
|
||||
@@ -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
@@ -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 %}
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1 @@
|
||||
# Package marker for tests
|
||||
@@ -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"
|
||||
@@ -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
@@ -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 %}
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1 @@
|
||||
# Package marker for tests
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user