some refactoring and atlas improvements
This commit is contained in:
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
+15
-42
@@ -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)
|
||||
|
||||
+2
-2
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
<span id="authors-link"><a href="{% url 'atlas:author_list' %}">author</a></span> {% endcomment %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
@@ -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 %}
|
||||
<div class="alert alert-info" role="alert">Creating a case in the collection: <a href="{% url 'atlas:collection_detail' pk=form.collection.pk %}">{{form.collection.name}}</a></div>
|
||||
<div class="alert alert-info" role="alert">Creating a case in the collection: <a href="{% url 'atlas:collection_detail' pk=form.exam.pk %}">{{form.collection.name}}</a></div>
|
||||
{% endif %}
|
||||
<form action="" method="post" enctype="multipart/form-data" id="atlas-form">
|
||||
{% csrf_token %}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
|
||||
{% if collection.publish_results or cid_user_exam.completed %}
|
||||
review
|
||||
<span class="stamp-white">REVIEW</span>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<a href="{% url 'atlas:exam_cids' collection.pk %}">Candidates</a> /
|
||||
<a href="{% url 'atlas:atlas_create_exam' pk=collection.pk %}">Add New Case</a>
|
||||
<div class="floating-header">
|
||||
<a href="{% url 'atlas:collection_update' collection.id %}" title="Edit the Collection">Edit</a>
|
||||
\ <a href="{% url 'atlas:collection_delete' collection.id %}" title="Delete the Collection">Delete</a>
|
||||
<a href="{% url 'atlas:exam_update' collection.id %}" title="Edit the Collection">Edit</a>
|
||||
\ <a href="{% url 'atlas:exam_deleted' collection.id %}" title="Delete the Collection">Delete</a>
|
||||
{% comment %} \ <a href="{% url 'atlas:collection_clone' collection.id %}" title="Clone the Collection">Clone</a> {% endcomment %}
|
||||
{% if request.user.is_superuser %}
|
||||
\ <a href="{% url 'admin:atlas_casecollection_change' collection.id %}" title="Edit the Collection using the admin interface">Admin Edit</a>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends 'atlas/base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<a href="{% url 'atlas:collection_create' %}">Create collection</a>
|
||||
<a href="{% url 'atlas:exam_create' %}">Create collection</a>
|
||||
<ul>
|
||||
{% for collection in collections %}
|
||||
<li><a href='{{collection.get_absolute_url}}'>{{collection.name}}</a></li>
|
||||
|
||||
@@ -8,16 +8,15 @@
|
||||
<h2>Collection: {{collection.name}}
|
||||
|
||||
{% if collection.publish_results or cid_user_exam.completed %}
|
||||
review
|
||||
<span class="stamp-white">REVIEW</span>
|
||||
{% endif %}
|
||||
</h2>
|
||||
|
||||
<div><p>Questions</p></div>
|
||||
{{answer_count}} out of {{collection_length}} cases answered. Click to go to case.
|
||||
<div class="sba-finish-list">
|
||||
<ul>
|
||||
{% for question, answer in question_answer_tuples %}
|
||||
<li>
|
||||
<li> <span {% if not answer %}class="unanswered"{% endif %}>
|
||||
<a href="
|
||||
{% if cid is not None %}
|
||||
{% url 'atlas:collection_case_view_take' pk=collection.id case_number=forloop.counter0 cid=cid passcode=passcode %}
|
||||
@@ -25,10 +24,15 @@
|
||||
{% url 'atlas:collection_case_view_take_user' pk=collection.id case_number=forloop.counter0 %}
|
||||
{% endif %}
|
||||
">
|
||||
<span {% if not answer %}class="unanswered"{% endif %}>Case: {{forloop.counter}}</span>
|
||||
Case: {{forloop.counter}}
|
||||
</a>
|
||||
<br/>
|
||||
{{answer.answer}}
|
||||
{% if not answer %}
|
||||
No answer
|
||||
{% else %}
|
||||
{{answer.answer}}
|
||||
{% endif %}
|
||||
</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
{% extends 'atlas/base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Start: {{collection.name}}</h2>
|
||||
<h2>Start: {{collection.name}}{% if cid_exam.completed %} <span class="stamp-white">REVIEW</span>{% endif %}</h2>
|
||||
|
||||
{% if request.user.is_authenticated and valid_user %}
|
||||
User: {{request.user}}<br/>
|
||||
|
||||
{% if cid_exam %}
|
||||
Started: {{cid_exam.start_time}} <br/>
|
||||
|
||||
|
||||
{% if cid_exam.completed %}
|
||||
Ended: {{cid_exam.end_time}} <br/>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
<a href="{% url 'atlas:collection_case_view_take_user' pk=collection.pk case_number=0 %}">
|
||||
{% if cid_exam.completed %}
|
||||
<button>Review</button>
|
||||
{% else %}
|
||||
<button>Start</button>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% else %}
|
||||
Enter your CID and passcode in the below boxes.<br />
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{% extends "atlas/exams.html" %}
|
||||
|
||||
{% block js %}
|
||||
{{ form.media }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h2>Update Exam</h2>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<table>
|
||||
{{ form.as_table }}
|
||||
</table>
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -9,8 +9,8 @@
|
||||
<a href="{% url 'atlas:collection_scores_cid' exam.pk %}">Scores</a> /
|
||||
<a href="{% url 'atlas:exam_cids' exam.pk %}">Candidates</a>
|
||||
<div class="floating-header">
|
||||
<a href="{% url 'atlas:collection_update' exam.id %}" title="Edit the Collection">Edit</a>
|
||||
\ <a href="{% url 'atlas:collection_delete' exam.id %}" title="Delete the Collection">Delete</a>
|
||||
<a href="{% url 'atlas:exam_update' exam.id %}" title="Edit the Collection">Edit</a>
|
||||
\ <a href="{% url 'atlas:exam_deleted' exam.id %}" title="Delete the Collection">Delete</a>
|
||||
{% if request.user.is_superuser %}
|
||||
\ <a href="{% url 'admin:atlas_casecollection_change' exam.id %}" title="Edit the Collection using the admin interface">Admin Edit</a>
|
||||
{% endif %}
|
||||
|
||||
+18
-3
@@ -21,17 +21,17 @@ urlpatterns = [
|
||||
path(
|
||||
"collection/create",
|
||||
views.CaseCollectionCreate.as_view(),
|
||||
name="collection_create",
|
||||
name="exam_create",
|
||||
),
|
||||
path(
|
||||
"collection/<int:pk>/delete",
|
||||
views.CaseCollectionDelete.as_view(),
|
||||
name="collection_delete",
|
||||
name="exam_deleted",
|
||||
),
|
||||
path(
|
||||
"collection/<int:pk>/update",
|
||||
views.CaseCollectionUpdate.as_view(),
|
||||
name="collection_update",
|
||||
name="exam_update",
|
||||
),
|
||||
path("collection/<int:pk>", views.collection_detail, name="collection_detail"),
|
||||
path(
|
||||
@@ -44,6 +44,16 @@ urlpatterns = [
|
||||
views.GenericExamViews.exam_cids,
|
||||
name="exam_cids",
|
||||
),
|
||||
path(
|
||||
"exam/<int:exam_id>/cids/edit",
|
||||
views.GenericExamViews.exam_cids_edit,
|
||||
name="exam_cids_edit",
|
||||
),
|
||||
path(
|
||||
"exam/<int:exam_id>/users/edit",
|
||||
views.GenericExamViews.exam_users_edit,
|
||||
name="exam_users_edit",
|
||||
),
|
||||
path(
|
||||
"collection/<int:exam_id>/cids/<int:cid>/delete_answers",
|
||||
views.delete_collection_cid_answers,
|
||||
@@ -59,6 +69,11 @@ urlpatterns = [
|
||||
views.GenericExamViews.exam_cids_edit,
|
||||
name="exam_cids_edit",
|
||||
),
|
||||
path(
|
||||
"collection/<int:exam_id>/users/edit",
|
||||
views.GenericExamViews.exam_users_edit,
|
||||
name="exam_users_edit",
|
||||
),
|
||||
path(
|
||||
"collection/<int:pk>/mark",
|
||||
views.collection_mark_overview,
|
||||
|
||||
@@ -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):
|
||||
|
||||
+62
-26
@@ -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 = (
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
+3
-15
@@ -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)
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+1
-1
@@ -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"""
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="rapid {% if question.scrapped %}rapid-scrapped{% endif %}">
|
||||
<button id="edit-button">Inline Edit</button>
|
||||
{% comment %} <button id="edit-button">Inline Edit</button> {% endcomment %}
|
||||
<div class="date">
|
||||
{{ question.created_date|date:"d/m/Y" }}
|
||||
</div>
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<p class="pre-whitespace"><b>Rapid:</b> {{ question }}</p>
|
||||
<p class="pre-whitespace"><b>Normal:</b> {{ question.normal }} <button id="toggle-normal-button"
|
||||
class="toggle-button">toggle</button></p>
|
||||
class="toggle-button">toggle</button></p>
|
||||
<p class="pre-whitespace"><b>Region:</b> {{ question.get_regions }}</p>
|
||||
<p class="pre-whitespace"><b>Examination:</b> {{ question.get_examinations }}</p>
|
||||
<p class="pre-whitespace"><b>Laterality:</b> {{ question.laterality }}
|
||||
@@ -24,7 +24,7 @@
|
||||
<span class="image-block">
|
||||
Image {{ forloop.counter }}{% if image.description %} ({{image.description}}){% endif %}{% if image.feedback_image %} [feedback image]{% endif %}:
|
||||
<div class="dicom-image rapid-img {% if image.feedback_image %}feedback-img{% endif %}"
|
||||
data-url="https://www.penracourses.org.uk{{ image.image.url}}"></div>
|
||||
data-url="https://www.penracourses.org.uk{{ image.image.url}}"></div>
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -33,11 +33,11 @@
|
||||
<a href="{% url 'rapids:exam_overview' pk=exam.pk %}">{{ exam.name }}</a>,
|
||||
{% endfor %}
|
||||
|
||||
<button id="add-to-exam" data-exam_json_edit_url="{% url 'generic:generic_exam_json_edit' %}"
|
||||
{% comment %} <button id="add-to-exam" data-exam_json_edit_url="{% url 'generic:generic_exam_json_edit' %}"
|
||||
data-exam_list_url="{% url 'api-1:rapid_user_exams' %}" data-type="rapids" data-csrf="{{ csrf_token}}"
|
||||
data-qid="{{question.pk}}">Add to exam</button>
|
||||
<button id="cancel-add-to-exam" style="display:none">Cancel</button>
|
||||
<span id="exam-options"></span>
|
||||
<span id="exam-options"></span> {% endcomment %}
|
||||
</div>
|
||||
<p class="pre-whitespace"><b>Open Access:</b> {{ question.open_access }}</p>
|
||||
<p class="pre-whitespace"><b>Feedback:</b> {{ question.feedback }}</p>
|
||||
@@ -59,7 +59,7 @@
|
||||
<tr>
|
||||
<td>
|
||||
<span {% if answer.proposed %}class="proposed-answer" data-aid="{{answer.pk}}" data-question-type="rapid"
|
||||
{% endif %}>
|
||||
{% endif %}>
|
||||
{{ answer }}
|
||||
</span>
|
||||
<td>
|
||||
@@ -94,7 +94,7 @@
|
||||
Image viewer
|
||||
</summary>
|
||||
<div id="single-dicom-viewer" class="dicom-viewer" data-images="{{ question.get_image_url_array }}"
|
||||
data-annotations='{{question.get_image_annotations}}'>
|
||||
data-annotations='{{question.get_image_annotations}}'>
|
||||
</div>
|
||||
<button id="save-annotations" class="save-rapid-annotations">Save Annotations</button>
|
||||
</details>
|
||||
@@ -110,7 +110,7 @@
|
||||
{% endfor %}
|
||||
</details>
|
||||
<a href="{% url 'rapids:question_anonymise_dicom' pk=question.pk %}"
|
||||
title="Anonymise dicom images">Anonymise dicoms</a><br />
|
||||
title="Anonymise dicom images">Anonymise dicoms</a><br />
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$("#edit-button").click(() => {
|
||||
@@ -156,21 +156,21 @@
|
||||
})
|
||||
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||
.done(function (data) {
|
||||
console.log(data);
|
||||
console.log(data);
|
||||
// show some message according to the response.
|
||||
// For eg. A message box showing that the status has been changed
|
||||
if (data.status == "success") {
|
||||
toastr.info('Annotations saved')
|
||||
} else {
|
||||
toastr.warning('Error saving annotations')
|
||||
}
|
||||
})
|
||||
if (data.status == "success") {
|
||||
toastr.info('Annotations saved')
|
||||
} else {
|
||||
toastr.warning('Error saving annotations')
|
||||
}
|
||||
})
|
||||
.always(function () {
|
||||
console.log('[Done]');
|
||||
})
|
||||
console.log('[Done]');
|
||||
})
|
||||
})
|
||||
|
||||
$("#cancel-add-to-exam").click(function (evt) {
|
||||
{% comment %} $("#cancel-add-to-exam").click(function (evt) {
|
||||
$("#add-to-exam").toggle();
|
||||
$("#cancel-add-to-exam").toggle();
|
||||
$("#exam-options").empty();
|
||||
@@ -296,7 +296,8 @@
|
||||
alert('Error updating this model instance.');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
{% endcomment %}
|
||||
|
||||
$(".suggested_answers li").each((n, el) => {
|
||||
$(el).append($("<span class='correct'>[Add Correct]</span>").on("click", function () {
|
||||
@@ -315,18 +316,18 @@
|
||||
})
|
||||
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||
.done(function (data) {
|
||||
console.log(data);
|
||||
console.log(data);
|
||||
|
||||
if (data.success) {
|
||||
toastr.info('Answer saved')
|
||||
$(el).find(".correct").remove()
|
||||
}
|
||||
if (data.success) {
|
||||
toastr.info('Answer saved')
|
||||
$(el).find(".correct").remove()
|
||||
}
|
||||
// show some message according to the response.
|
||||
// For eg. A message box showing that the status has been changed
|
||||
})
|
||||
})
|
||||
.always(function () {
|
||||
console.log('[Done]');
|
||||
})
|
||||
console.log('[Done]');
|
||||
})
|
||||
}))
|
||||
|
||||
});
|
||||
@@ -350,19 +351,19 @@
|
||||
})
|
||||
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||
.done(function (data) {
|
||||
console.log(data);
|
||||
console.log(data);
|
||||
|
||||
if (data.success) {
|
||||
toastr.info('Answer saved')
|
||||
$(el).find(".confirm").remove()
|
||||
$(el).removeClass("proposed-answer")
|
||||
}
|
||||
if (data.success) {
|
||||
toastr.info('Answer saved')
|
||||
$(el).find(".confirm").remove()
|
||||
$(el).removeClass("proposed-answer")
|
||||
}
|
||||
// show some message according to the response.
|
||||
// For eg. A message box showing that the status has been changed
|
||||
})
|
||||
})
|
||||
.always(function () {
|
||||
console.log('[Done]');
|
||||
})
|
||||
console.log('[Done]');
|
||||
})
|
||||
}))
|
||||
|
||||
// Add button to confirm answer is incorrect
|
||||
@@ -381,19 +382,19 @@
|
||||
})
|
||||
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||
.done(function (data) {
|
||||
console.log(data);
|
||||
console.log(data);
|
||||
|
||||
if (data.success) {
|
||||
toastr.info('Answer saved')
|
||||
$(el).find(".confirm").remove()
|
||||
$(el).removeClass("proposed-answer")
|
||||
}
|
||||
if (data.success) {
|
||||
toastr.info('Answer saved')
|
||||
$(el).find(".confirm").remove()
|
||||
$(el).removeClass("proposed-answer")
|
||||
}
|
||||
// show some message according to the response.
|
||||
// For eg. A message box showing that the status has been changed
|
||||
})
|
||||
})
|
||||
.always(function () {
|
||||
console.log('[Done]');
|
||||
})
|
||||
console.log('[Done]');
|
||||
})
|
||||
}))
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 = [
|
||||
('sbas', '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'),
|
||||
),
|
||||
]
|
||||
+3
-9
@@ -102,8 +102,7 @@ def active_exams(request):
|
||||
def exam_scores_cid_user(request, pk, cid, passcode):
|
||||
exam = get_object_or_404(Exam, pk=pk)
|
||||
|
||||
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()
|
||||
|
||||
@@ -179,8 +178,7 @@ def exam_take_overview(request, pk, cid=None, passcode=None):
|
||||
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()
|
||||
|
||||
@@ -222,11 +220,7 @@ def exam_take_overview(request, pk, cid=None, passcode=None):
|
||||
def exam_take(request, pk: int, sk: int, cid: int = None, passcode: str = None):
|
||||
exam = get_object_or_404(Exam, pk=pk)
|
||||
|
||||
if not exam.active:
|
||||
raise Http404("Exam not found")
|
||||
|
||||
if not exam.check_cid_user(cid, passcode, request):
|
||||
raise Http404("Error accessing exam")
|
||||
exam.check_user_can_take(cid, passcode, request)
|
||||
|
||||
cid_user_exam = exam.get_or_create_cid_user_exam(cid = cid, user_user=request.user)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user