From dd300afd63c17a5a5dc1876b445303ad3b62747d Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 24 Jul 2023 11:56:12 +0100 Subject: [PATCH] some refactoring and atlas improvements --- ...didates_only_alter_exam_active_and_more.py | 43 +++++++++ ...asecollection_user_user_groups_and_more.py | 36 ++++++++ ...casecollection_candidates_only_and_more.py | 43 +++++++++ atlas/models.py | 57 +++--------- atlas/tables.py | 4 +- atlas/templates/atlas/base.html | 13 +-- atlas/templates/atlas/case_form.html | 2 +- .../atlas/collection_case_view_take.html | 2 +- atlas/templates/atlas/collection_headers.html | 4 +- .../atlas/collection_index_view.html | 2 +- .../atlas/collection_take_overview.html | 14 ++- .../atlas/collection_take_start.html | 12 ++- .../atlas/collection_update_form.html | 17 ++++ atlas/templates/atlas/exams.html | 4 +- atlas/urls.py | 21 ++++- atlas/views.py | 3 + generic/models.py | 88 ++++++++++++------ ...didates_only_alter_exam_active_and_more.py | 43 +++++++++ ...didates_only_alter_exam_active_and_more.py | 43 +++++++++ physics/views.py | 18 +--- rad/static/css/anatomy.css | 31 ++++++- rad/test_helpers.py | 2 +- ...didates_only_alter_exam_active_and_more.py | 43 +++++++++ .../rapids/question_display_block.html | 91 ++++++++++--------- ...didates_only_alter_exam_active_and_more.py | 43 +++++++++ sbas/views.py | 12 +-- 26 files changed, 521 insertions(+), 170 deletions(-) create mode 100644 anatomy/migrations/0004_exam_candidates_only_alter_exam_active_and_more.py create mode 100644 atlas/migrations/0005_casecollection_user_user_groups_and_more.py create mode 100644 atlas/migrations/0006_casecollection_candidates_only_and_more.py create mode 100644 atlas/templates/atlas/collection_update_form.html create mode 100644 longs/migrations/0003_exam_candidates_only_alter_exam_active_and_more.py create mode 100644 physics/migrations/0002_exam_candidates_only_alter_exam_active_and_more.py create mode 100644 rapids/migrations/0002_exam_candidates_only_alter_exam_active_and_more.py create mode 100644 sbas/migrations/0002_exam_candidates_only_alter_exam_active_and_more.py diff --git a/anatomy/migrations/0004_exam_candidates_only_alter_exam_active_and_more.py b/anatomy/migrations/0004_exam_candidates_only_alter_exam_active_and_more.py new file mode 100644 index 00000000..757b7341 --- /dev/null +++ b/anatomy/migrations/0004_exam_candidates_only_alter_exam_active_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 4.1.4 on 2023-07-24 09:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('anatomy', '0003_alter_anatomyquestion_modality_delete_modality'), + ] + + operations = [ + migrations.AddField( + model_name='exam', + name='candidates_only', + field=models.BooleanField(default=True, help_text='If the exam/collection is only available to candidates who have been added'), + ), + migrations.AlterField( + model_name='exam', + name='active', + field=models.BooleanField(default=False, help_text='If an exam/collection should be available to take'), + ), + migrations.AlterField( + model_name='exam', + name='archive', + field=models.BooleanField(default=False, help_text='Archived exams/collections will remain on the test system but will not be displayed by default'), + ), + migrations.AlterField( + model_name='exam', + name='name', + field=models.CharField(help_text='Name of the exam/collection', max_length=200), + ), + migrations.AlterField( + model_name='exam', + name='open_access', + field=models.BooleanField(default=False, help_text='If the exam/collection is freely accessible (to view and edit on the test system)'), + ), + migrations.AlterField( + model_name='exam', + name='publish_results', + field=models.BooleanField(default=False, help_text='If an exam/collections results should be available'), + ), + ] diff --git a/atlas/migrations/0005_casecollection_user_user_groups_and_more.py b/atlas/migrations/0005_casecollection_user_user_groups_and_more.py new file mode 100644 index 00000000..e7542028 --- /dev/null +++ b/atlas/migrations/0005_casecollection_user_user_groups_and_more.py @@ -0,0 +1,36 @@ +# Generated by Django 4.1.4 on 2023-07-24 09:06 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('generic', '0006_alter_ciduserexam_completed'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('atlas', '0004_casecollection_self_review'), + ] + + operations = [ + migrations.AddField( + model_name='casecollection', + name='user_user_groups', + field=models.ManyToManyField(blank=True, help_text='These groups define which candidates are able to be added to the exams/collection.', related_name='casecollection_user_user_groups', to='generic.userusergroup'), + ), + migrations.AddField( + model_name='casecollection', + name='valid_user_users', + field=models.ManyToManyField(blank=True, related_name='user_casecollection_exams', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='casecollection', + name='author', + field=models.ManyToManyField(blank=True, help_text='Author of the collection', related_name='casecollection_authored_cases', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='casecollection', + name='cid_user_groups', + field=models.ManyToManyField(blank=True, help_text='These groups define which candidates are able to be added to the exams/collection.', related_name='casecollection_cid_user_groups', to='generic.cidusergroup'), + ), + ] diff --git a/atlas/migrations/0006_casecollection_candidates_only_and_more.py b/atlas/migrations/0006_casecollection_candidates_only_and_more.py new file mode 100644 index 00000000..3a054af1 --- /dev/null +++ b/atlas/migrations/0006_casecollection_candidates_only_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 4.1.4 on 2023-07-24 09:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('atlas', '0005_casecollection_user_user_groups_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='casecollection', + name='candidates_only', + field=models.BooleanField(default=True, help_text='If the exam/collection is only available to candidates who have been added'), + ), + migrations.AlterField( + model_name='casecollection', + name='active', + field=models.BooleanField(default=False, help_text='If an exam/collection should be available to take'), + ), + migrations.AlterField( + model_name='casecollection', + name='archive', + field=models.BooleanField(default=False, help_text='Archived exams/collections will remain on the test system but will not be displayed by default'), + ), + migrations.AlterField( + model_name='casecollection', + name='name', + field=models.CharField(help_text='Name of the exam/collection', max_length=200), + ), + migrations.AlterField( + model_name='casecollection', + name='open_access', + field=models.BooleanField(default=False, help_text='If the exam/collection is freely accessible (to view and edit on the test system)'), + ), + migrations.AlterField( + model_name='casecollection', + name='publish_results', + field=models.BooleanField(default=False, help_text='If an exam/collections results should be available'), + ), + ] diff --git a/atlas/models.py b/atlas/models.py index 9b08d44c..4b29835f 100644 --- a/atlas/models.py +++ b/atlas/models.py @@ -2,7 +2,7 @@ import json import os import pathlib -from django.http import Http404 +from django.http import Http404, HttpRequest from rad.settings import REMOTE_URL from django.db.models.fields.files import ImageField from django.db.models.fields.related import ForeignKey @@ -46,6 +46,7 @@ from generic.models import ( SeriesBase, SeriesImageBase, Modality, + UserUserGroup, ) # from generic.models import Examination, Site, Condition, Sign @@ -413,18 +414,8 @@ class Series(SeriesBase): class CaseCollection(ExamCollectionGenericBase): app_name = "atlas" - name = models.CharField(max_length=255, unique=True) - cases = models.ManyToManyField(Case, through="CaseDetail") - publish_results = models.BooleanField( - help_text="If a collection should published", default=False - ) - - active = models.BooleanField( - help_text="If a collection should be available", default=True - ) - show_title_pre = models.BooleanField( default=False, help_text="Show the title of the cases (pre exam)" ) @@ -461,20 +452,31 @@ class CaseCollection(ExamCollectionGenericBase): settings.AUTH_USER_MODEL, blank=True, help_text="Author of the collection", - # related_name="atlas_authored_cases", + related_name="casecollection_authored_cases", ) valid_cid_users = models.ManyToManyField( CidUser, blank=True, related_name="casecollection_exams" ) + valid_user_users = models.ManyToManyField( + settings.AUTH_USER_MODEL, blank=True, related_name="user_casecollection_exams" + ) + cid_user_groups = models.ManyToManyField( CidUserGroup, blank=True, help_text="These groups define which candidates are able to be added to the exams/collection.", + related_name="casecollection_cid_user_groups", + ) + + user_user_groups = models.ManyToManyField( + UserUserGroup, + blank=True, + help_text="These groups define which candidates are able to be added to the exams/collection.", + related_name="casecollection_user_user_groups", ) - archive = models.BooleanField(default=False) exam_mode = models.BooleanField(default=False) # This should override the publish setting @@ -483,11 +485,6 @@ class CaseCollection(ExamCollectionGenericBase): help_text="If true allows users self complete and review cases in a self directed way.", ) - open_access = models.BooleanField( - help_text="If the exam is freely accessible (to view and edit on the test system)", - default=False, - ) - class COLLECTION_TYPE_CHOICES(models.TextChoices): REVIEW = ( "REV", @@ -525,30 +522,6 @@ class CaseCollection(ExamCollectionGenericBase): return False - def check_user_can_take(self, cid, passcode, request): - """ - Helper to check if a user is allowed to access a collection - - Args: - cid (_type_): _description_ - passcode (_type_): _description_ - request (_type_): _description_ - - Raises: - Http404: If user does not have access - """ - if not self.active: - raise Http404("Exam not found") - - #if self.collection_is_review(): - # return True - - #if not self.self_review: - - if not self.check_cid_user(cid, passcode, request): - raise Http404("Error accessing exam") - - class CaseDetail(models.Model): case = models.ForeignKey(Case, on_delete=models.CASCADE) collection = models.ForeignKey(CaseCollection, on_delete=models.CASCADE) diff --git a/atlas/tables.py b/atlas/tables.py index f95ac078..737ed241 100755 --- a/atlas/tables.py +++ b/atlas/tables.py @@ -273,7 +273,7 @@ class SubspecialtyTable(tables.Table): class CaseCollectionTable(tables.Table): edit = tables.LinkColumn( - "atlas:collection_update", text="Edit", args=[A("pk")], orderable=False + "atlas:exam_update", text="Edit", args=[A("pk")], orderable=False ) view = tables.LinkColumn( "atlas:collection_detail", text="View", args=[A("pk")], orderable=False @@ -282,7 +282,7 @@ class CaseCollectionTable(tables.Table): # "atlas:case_clone", text="Clone", args=[A("pk")], orderable=False #) delete = tables.LinkColumn( - "atlas:collection_delete", text="Delete", args=[A("pk")], orderable=False + "atlas:exam_deleted", text="Delete", args=[A("pk")], orderable=False ) #differential = tables.ManyToManyColumn(verbose_name="Differential") diff --git a/atlas/templates/atlas/base.html b/atlas/templates/atlas/base.html index f3d008ba..5d5d4c09 100755 --- a/atlas/templates/atlas/base.html +++ b/atlas/templates/atlas/base.html @@ -4,16 +4,6 @@ Atlas {% endblock %} -{% block css %} -{% endblock %} - -{% block js %} -{% endblock %} - - - -{% block content %} -{% endblock %} {% block navigation %} Atlas: {% if request.user.is_authenticated %} @@ -28,3 +18,6 @@ Questions by: author {% endcomment %} {% endblock %} + +{% block content %} +{% endblock %} \ No newline at end of file diff --git a/atlas/templates/atlas/case_form.html b/atlas/templates/atlas/case_form.html index 0e43584b..8a5b9f1a 100755 --- a/atlas/templates/atlas/case_form.html +++ b/atlas/templates/atlas/case_form.html @@ -53,7 +53,7 @@ Use this form to create a atlas case. Existing associated image sets can be added using this form. {% if form.collection %} - + {% endif %}
{% csrf_token %} diff --git a/atlas/templates/atlas/collection_case_view_take.html b/atlas/templates/atlas/collection_case_view_take.html index 71b7e5e6..7f8059ff 100644 --- a/atlas/templates/atlas/collection_case_view_take.html +++ b/atlas/templates/atlas/collection_case_view_take.html @@ -9,7 +9,7 @@ {% if collection.publish_results or cid_user_exam.completed %} - review + REVIEW {% endif %} diff --git a/atlas/templates/atlas/collection_headers.html b/atlas/templates/atlas/collection_headers.html index d1784444..936d9953 100644 --- a/atlas/templates/atlas/collection_headers.html +++ b/atlas/templates/atlas/collection_headers.html @@ -6,8 +6,8 @@ Candidates / Add New Case
- Edit - \ Delete + Edit + \ Delete {% comment %} \ Clone {% endcomment %} {% if request.user.is_superuser %} \ Admin Edit diff --git a/atlas/templates/atlas/collection_index_view.html b/atlas/templates/atlas/collection_index_view.html index da35ea7f..22bc84d7 100644 --- a/atlas/templates/atlas/collection_index_view.html +++ b/atlas/templates/atlas/collection_index_view.html @@ -1,7 +1,7 @@ {% extends 'atlas/base.html' %} {% block content %} - Create collection + Create collection
    {% for collection in collections %}
  • {{collection.name}}
  • diff --git a/atlas/templates/atlas/collection_take_overview.html b/atlas/templates/atlas/collection_take_overview.html index 46fa423f..ee28cfca 100644 --- a/atlas/templates/atlas/collection_take_overview.html +++ b/atlas/templates/atlas/collection_take_overview.html @@ -8,16 +8,15 @@

    Collection: {{collection.name}} {% if collection.publish_results or cid_user_exam.completed %} - review + REVIEW {% endif %}

    -

    Questions

    {{answer_count}} out of {{collection_length}} cases answered. Click to go to case.
    diff --git a/atlas/templates/atlas/collection_take_start.html b/atlas/templates/atlas/collection_take_start.html index 78497790..1e320866 100644 --- a/atlas/templates/atlas/collection_take_start.html +++ b/atlas/templates/atlas/collection_take_start.html @@ -1,18 +1,26 @@ {% extends 'atlas/base.html' %} {% block content %} -

    Start: {{collection.name}}

    +

    Start: {{collection.name}}{% if cid_exam.completed %} REVIEW{% endif %}

    {% if request.user.is_authenticated and valid_user %} User: {{request.user}}
    {% if cid_exam %} Started: {{cid_exam.start_time}}
    + + + {% if cid_exam.completed %} + Ended: {{cid_exam.end_time}}
    + {% endif %} {% endif %} - + {% if cid_exam.completed %} + + {% else %} + {% endif %} {% else %} Enter your CID and passcode in the below boxes.
    diff --git a/atlas/templates/atlas/collection_update_form.html b/atlas/templates/atlas/collection_update_form.html new file mode 100644 index 00000000..faeed783 --- /dev/null +++ b/atlas/templates/atlas/collection_update_form.html @@ -0,0 +1,17 @@ +{% extends "atlas/exams.html" %} + +{% block js %} + {{ form.media }} +{% endblock %} + +{% block content %} + +

    Update Exam

    + + {% csrf_token %} + + {{ form.as_table }} +
    + + +{% endblock %} \ No newline at end of file diff --git a/atlas/templates/atlas/exams.html b/atlas/templates/atlas/exams.html index c464c03a..ab7b202f 100644 --- a/atlas/templates/atlas/exams.html +++ b/atlas/templates/atlas/exams.html @@ -9,8 +9,8 @@ Scores / Candidates
    - Edit - \ Delete + Edit + \ Delete {% if request.user.is_superuser %} \ Admin Edit {% endif %} diff --git a/atlas/urls.py b/atlas/urls.py index 97895b66..47156951 100755 --- a/atlas/urls.py +++ b/atlas/urls.py @@ -21,17 +21,17 @@ urlpatterns = [ path( "collection/create", views.CaseCollectionCreate.as_view(), - name="collection_create", + name="exam_create", ), path( "collection//delete", views.CaseCollectionDelete.as_view(), - name="collection_delete", + name="exam_deleted", ), path( "collection//update", views.CaseCollectionUpdate.as_view(), - name="collection_update", + name="exam_update", ), path("collection/", views.collection_detail, name="collection_detail"), path( @@ -44,6 +44,16 @@ urlpatterns = [ views.GenericExamViews.exam_cids, name="exam_cids", ), + path( + "exam//cids/edit", + views.GenericExamViews.exam_cids_edit, + name="exam_cids_edit", + ), + path( + "exam//users/edit", + views.GenericExamViews.exam_users_edit, + name="exam_users_edit", + ), path( "collection//cids//delete_answers", views.delete_collection_cid_answers, @@ -59,6 +69,11 @@ urlpatterns = [ views.GenericExamViews.exam_cids_edit, name="exam_cids_edit", ), + path( + "collection//users/edit", + views.GenericExamViews.exam_users_edit, + name="exam_users_edit", + ), path( "collection//mark", views.collection_mark_overview, diff --git a/atlas/views.py b/atlas/views.py index 1c2f14f7..efb4f648 100755 --- a/atlas/views.py +++ b/atlas/views.py @@ -442,6 +442,7 @@ class SeriesCreate(RevisionMixin, LoginRequiredMixin, CreateView): class CaseCollectionUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateView): model = CaseCollection form_class = CaseCollectionForm + template_name = "atlas/collection_update_form.html" # fields = '__all__' # #fields = [ 'condition' ] @@ -467,6 +468,8 @@ class CaseCollectionUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateVi context["case_formset"] = CaseCollectionCaseFormSet( instance=self.object, form_kwargs={"user": self.request.user} ) + + context["collection"] = context["casecollection"] return context def form_valid(self, form): diff --git a/generic/models.py b/generic/models.py index 4a832679..6149380b 100644 --- a/generic/models.py +++ b/generic/models.py @@ -4,7 +4,7 @@ import os from typing import Self, Tuple from django.contrib.auth.mixins import LoginRequiredMixin from django.db import models -from django.http import HttpRequest +from django.http import Http404, HttpRequest from django.utils import timezone from smtplib import SMTPException @@ -393,6 +393,30 @@ class ExamCollectionGenericBase(models.Model): # Is this actually used? cid_users = GenericRelation("generic.CidUserExam") + name = models.CharField(max_length=200, help_text="Name of the exam/collection") + + active = models.BooleanField( + help_text="If an exam/collection should be available to take", default=False + ) + publish_results = models.BooleanField( + help_text="If an exam/collections results should be available", default=False + ) + + archive = models.BooleanField( + help_text="Archived exams/collections will remain on the test system but will not be displayed by default", + default=False, + ) + + open_access = models.BooleanField( + help_text="If the exam/collection is freely accessible (to view and edit on the test system)", + default=False, + ) + + candidates_only = models.BooleanField( + help_text="If the exam/collection is only available to candidates who have been added", + default=True, + ) + class Meta: abstract = True @@ -412,6 +436,42 @@ class ExamCollectionGenericBase(models.Model): """Helper to check if the logged in user can access the exam""" return self.check_cid_user(request=request, user_id=request.user.id) + def check_user_can_review( + self, cid, passcode, request: HttpRequest, active_only=False + ): + return self.check_user_can_take(cid, passcode, request, active_only) + + def check_user_can_take( + self, cid, passcode, request: HttpRequest, active_only=True + ): + """ + Helper to check if a user is allowed to access an exam/collection + + Args: + cid (_type_): _description_ + passcode (_type_): _description_ + request (_type_): _description_ + + Raises: + Http404: If user does not have access + """ + # If it is not active no one can take + print(0) + if active_only and not self.active: + raise Http404("Exam not found") + + print(1) + # If candidates only check they have access + if self.candidates_only: + if not self.check_cid_user(cid, passcode, request): + raise Http404("Error accessing exam") + return + + print(2) + # Otherwise check that they are a valid user. + if not request.user.is_active: + raise Http404("Error accessing exam") + def check_cid_user( self, cid: int | None = None, @@ -524,13 +584,6 @@ class ExamCollectionGenericBase(models.Model): class ExamBase(ExamCollectionGenericBase): - name = models.CharField(max_length=200, help_text="Name of the exam") - # exam_questions = SortedManyToManyField(Long, related_name="exams", blank="true") - - active = models.BooleanField( - help_text="If an exam should be available to take", default=False - ) - exam_mode = models.BooleanField( help_text="If an exam should be taken in exam mode (users results will be submited to the server for marking)", default=False, @@ -541,10 +594,6 @@ class ExamBase(ExamCollectionGenericBase): default=False, ) - publish_results = models.BooleanField( - help_text="If an exams results should be available", default=False - ) - recreate_json = models.BooleanField( help_text="If the json cache needs updating", default=False ) @@ -554,16 +603,6 @@ class ExamBase(ExamCollectionGenericBase): default=1, help_text="auto incrementing field when json recreated" ) - archive = models.BooleanField( - help_text="Archived exams will remain on the test system but will not be displayed by default", - default=False, - ) - - open_access = models.BooleanField( - help_text="If the exam is freely accessible (to view and edit on the test system)", - default=False, - ) - authors_only = models.BooleanField( help_text="If true only exam authors will be able to view.", default=False, @@ -1090,10 +1129,7 @@ class CidUserExam(models.Model): else: end_time = f"{ self.end_time:%Y-%m-%d %H:%M }" - - return ( - f"{user}: {start_time} {end_time}" - ) + return f"{user}: {start_time} {end_time}" CID_GROUP_EXAMS = ( diff --git a/longs/migrations/0003_exam_candidates_only_alter_exam_active_and_more.py b/longs/migrations/0003_exam_candidates_only_alter_exam_active_and_more.py new file mode 100644 index 00000000..60fec0b4 --- /dev/null +++ b/longs/migrations/0003_exam_candidates_only_alter_exam_active_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 4.1.4 on 2023-07-24 09:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('longs', '0002_alter_longseries_modality'), + ] + + operations = [ + migrations.AddField( + model_name='exam', + name='candidates_only', + field=models.BooleanField(default=True, help_text='If the exam/collection is only available to candidates who have been added'), + ), + migrations.AlterField( + model_name='exam', + name='active', + field=models.BooleanField(default=False, help_text='If an exam/collection should be available to take'), + ), + migrations.AlterField( + model_name='exam', + name='archive', + field=models.BooleanField(default=False, help_text='Archived exams/collections will remain on the test system but will not be displayed by default'), + ), + migrations.AlterField( + model_name='exam', + name='name', + field=models.CharField(help_text='Name of the exam/collection', max_length=200), + ), + migrations.AlterField( + model_name='exam', + name='open_access', + field=models.BooleanField(default=False, help_text='If the exam/collection is freely accessible (to view and edit on the test system)'), + ), + migrations.AlterField( + model_name='exam', + name='publish_results', + field=models.BooleanField(default=False, help_text='If an exam/collections results should be available'), + ), + ] diff --git a/physics/migrations/0002_exam_candidates_only_alter_exam_active_and_more.py b/physics/migrations/0002_exam_candidates_only_alter_exam_active_and_more.py new file mode 100644 index 00000000..6a634c03 --- /dev/null +++ b/physics/migrations/0002_exam_candidates_only_alter_exam_active_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 4.1.4 on 2023-07-24 09:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('physics', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='exam', + name='candidates_only', + field=models.BooleanField(default=True, help_text='If the exam/collection is only available to candidates who have been added'), + ), + migrations.AlterField( + model_name='exam', + name='active', + field=models.BooleanField(default=False, help_text='If an exam/collection should be available to take'), + ), + migrations.AlterField( + model_name='exam', + name='archive', + field=models.BooleanField(default=False, help_text='Archived exams/collections will remain on the test system but will not be displayed by default'), + ), + migrations.AlterField( + model_name='exam', + name='name', + field=models.CharField(help_text='Name of the exam/collection', max_length=200), + ), + migrations.AlterField( + model_name='exam', + name='open_access', + field=models.BooleanField(default=False, help_text='If the exam/collection is freely accessible (to view and edit on the test system)'), + ), + migrations.AlterField( + model_name='exam', + name='publish_results', + field=models.BooleanField(default=False, help_text='If an exam/collections results should be available'), + ), + ] diff --git a/physics/views.py b/physics/views.py index 18a18d22..f21b1413 100644 --- a/physics/views.py +++ b/physics/views.py @@ -103,12 +103,7 @@ def active_exams(request): def exam_scores_cid_user(request, pk, cid=None, passcode=None): exam = get_object_or_404(Exam, pk=pk) - if not exam.exam_mode: - raise Http404("Packet not in exam mode") - - print(cid, passcode, request.user) - if not exam.check_cid_user(cid, passcode, request): - raise Http404("Error accessing exam") + exam.check_user_can_review(cid, passcode, request) questions = exam.exam_questions.all() @@ -208,11 +203,7 @@ def exam_start(request, pk): def exam_take_overview(request, pk, cid=None, passcode=None): exam = get_object_or_404(Exam, pk=pk) - if not exam.active: - return exam_inactive(request, context={"exam": exam}) - - if not exam.check_cid_user(cid, passcode, request): - raise Http404("Error accessing exam") + exam.check_user_can_take(cid, passcode, request) questions = exam.exam_questions.all() @@ -258,10 +249,7 @@ def exam_take(request, pk: int, sk: int, cid: str| None = None, passcode: str| N if not exam.active: return exam_inactive(request, context={"exam": exam}) - if not exam.check_cid_user(cid, passcode, request): - raise Http404("Error accessing exam") - - print(cid, passcode, request.user) + exam.check_user_can_take(cid, passcode, request) cid_user_exam = exam.get_or_create_cid_user_exam(cid=cid, user_user=request.user) diff --git a/rad/static/css/anatomy.css b/rad/static/css/anatomy.css index 5272965a..caf687b3 100644 --- a/rad/static/css/anatomy.css +++ b/rad/static/css/anatomy.css @@ -750,10 +750,19 @@ input { min-width: 10px; } */ +.sba-finish-list a { + text-decoration: none; + color: unset; + +} + +.sba-finish-list span { + color: lightblue; +} + +.sba-finish-list span.unanswered { + color: gray; -.sba-finish-list a button.unanswered { - color: red; - border-style: dashed; } .no-select { @@ -975,3 +984,19 @@ h1, h2, h3, h4, h5, h6 { .cid-candidate-list div { } +.stamp-white { + display: inline-block; + z-index:1; + font-family:Arial,sans-serif; + transform: rotate(-15deg); + font-size:20px; + color:white; + border:solid 2px white; + padding:5px; + border-radius:5px; + zoom:1; + filter:alpha(opacity=20); + opacity:0.9; + text-shadow: 0 0 2px white; + box-shadow: 0 0 2px white; +} \ No newline at end of file diff --git a/rad/test_helpers.py b/rad/test_helpers.py index 4c202923..ed8f024d 100644 --- a/rad/test_helpers.py +++ b/rad/test_helpers.py @@ -9,7 +9,7 @@ from generic.models import CidUser, CidUserGroup, UserUserGroup, ExamBase from generic.views import ExamViews from django.contrib.auth.models import User from django.urls import reverse - +from rich.pretty import pprint def AssertNotFound(client, url): """Helper to quickly test for urls that should return a 404 (NOT FOUND) error""" diff --git a/rapids/migrations/0002_exam_candidates_only_alter_exam_active_and_more.py b/rapids/migrations/0002_exam_candidates_only_alter_exam_active_and_more.py new file mode 100644 index 00000000..004380cc --- /dev/null +++ b/rapids/migrations/0002_exam_candidates_only_alter_exam_active_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 4.1.4 on 2023-07-24 09:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rapids', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='exam', + name='candidates_only', + field=models.BooleanField(default=True, help_text='If the exam/collection is only available to candidates who have been added'), + ), + migrations.AlterField( + model_name='exam', + name='active', + field=models.BooleanField(default=False, help_text='If an exam/collection should be available to take'), + ), + migrations.AlterField( + model_name='exam', + name='archive', + field=models.BooleanField(default=False, help_text='Archived exams/collections will remain on the test system but will not be displayed by default'), + ), + migrations.AlterField( + model_name='exam', + name='name', + field=models.CharField(help_text='Name of the exam/collection', max_length=200), + ), + migrations.AlterField( + model_name='exam', + name='open_access', + field=models.BooleanField(default=False, help_text='If the exam/collection is freely accessible (to view and edit on the test system)'), + ), + migrations.AlterField( + model_name='exam', + name='publish_results', + field=models.BooleanField(default=False, help_text='If an exam/collections results should be available'), + ), + ] diff --git a/rapids/templates/rapids/question_display_block.html b/rapids/templates/rapids/question_display_block.html index 415da73c..fd31f8c1 100755 --- a/rapids/templates/rapids/question_display_block.html +++ b/rapids/templates/rapids/question_display_block.html @@ -1,5 +1,5 @@
    - + {% comment %} {% endcomment %}
    {{ question.created_date|date:"d/m/Y" }}
    @@ -9,7 +9,7 @@

    Rapid: {{ question }}

    Normal: {{ question.normal }}

    + class="toggle-button">toggle

    Region: {{ question.get_regions }}

    Examination: {{ question.get_examinations }}

    Laterality: {{ question.laterality }} @@ -24,7 +24,7 @@ Image {{ forloop.counter }}{% if image.description %} ({{image.description}}){% endif %}{% if image.feedback_image %} [feedback image]{% endif %}:

    + data-url="https://www.penracourses.org.uk{{ image.image.url}}">
    {% endfor %}
    @@ -33,11 +33,11 @@ {{ exam.name }}, {% endfor %} - - + {% endcomment %}

    Open Access: {{ question.open_access }}

    Feedback: {{ question.feedback }}

    @@ -59,7 +59,7 @@ + {% endif %}> {{ answer }} @@ -94,7 +94,7 @@ Image viewer
    + data-annotations='{{question.get_image_annotations}}'>
    @@ -110,7 +110,7 @@ {% endfor %} Anonymise dicoms
    + title="Anonymise dicom images">Anonymise dicoms