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 os
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
from django.http import Http404
|
from django.http import Http404, HttpRequest
|
||||||
from rad.settings import REMOTE_URL
|
from rad.settings import REMOTE_URL
|
||||||
from django.db.models.fields.files import ImageField
|
from django.db.models.fields.files import ImageField
|
||||||
from django.db.models.fields.related import ForeignKey
|
from django.db.models.fields.related import ForeignKey
|
||||||
@@ -46,6 +46,7 @@ from generic.models import (
|
|||||||
SeriesBase,
|
SeriesBase,
|
||||||
SeriesImageBase,
|
SeriesImageBase,
|
||||||
Modality,
|
Modality,
|
||||||
|
UserUserGroup,
|
||||||
)
|
)
|
||||||
|
|
||||||
# from generic.models import Examination, Site, Condition, Sign
|
# from generic.models import Examination, Site, Condition, Sign
|
||||||
@@ -413,18 +414,8 @@ class Series(SeriesBase):
|
|||||||
class CaseCollection(ExamCollectionGenericBase):
|
class CaseCollection(ExamCollectionGenericBase):
|
||||||
app_name = "atlas"
|
app_name = "atlas"
|
||||||
|
|
||||||
name = models.CharField(max_length=255, unique=True)
|
|
||||||
|
|
||||||
cases = models.ManyToManyField(Case, through="CaseDetail")
|
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(
|
show_title_pre = models.BooleanField(
|
||||||
default=False, help_text="Show the title of the cases (pre exam)"
|
default=False, help_text="Show the title of the cases (pre exam)"
|
||||||
)
|
)
|
||||||
@@ -461,20 +452,31 @@ class CaseCollection(ExamCollectionGenericBase):
|
|||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Author of the collection",
|
help_text="Author of the collection",
|
||||||
# related_name="atlas_authored_cases",
|
related_name="casecollection_authored_cases",
|
||||||
)
|
)
|
||||||
|
|
||||||
valid_cid_users = models.ManyToManyField(
|
valid_cid_users = models.ManyToManyField(
|
||||||
CidUser, blank=True, related_name="casecollection_exams"
|
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(
|
cid_user_groups = models.ManyToManyField(
|
||||||
CidUserGroup,
|
CidUserGroup,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="These groups define which candidates are able to be added to the exams/collection.",
|
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)
|
exam_mode = models.BooleanField(default=False)
|
||||||
|
|
||||||
# This should override the publish setting
|
# 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.",
|
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):
|
class COLLECTION_TYPE_CHOICES(models.TextChoices):
|
||||||
REVIEW = (
|
REVIEW = (
|
||||||
"REV",
|
"REV",
|
||||||
@@ -525,30 +522,6 @@ class CaseCollection(ExamCollectionGenericBase):
|
|||||||
return False
|
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):
|
class CaseDetail(models.Model):
|
||||||
case = models.ForeignKey(Case, on_delete=models.CASCADE)
|
case = models.ForeignKey(Case, on_delete=models.CASCADE)
|
||||||
collection = models.ForeignKey(CaseCollection, 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):
|
class CaseCollectionTable(tables.Table):
|
||||||
edit = tables.LinkColumn(
|
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(
|
view = tables.LinkColumn(
|
||||||
"atlas:collection_detail", text="View", args=[A("pk")], orderable=False
|
"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
|
# "atlas:case_clone", text="Clone", args=[A("pk")], orderable=False
|
||||||
#)
|
#)
|
||||||
delete = tables.LinkColumn(
|
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")
|
#differential = tables.ManyToManyColumn(verbose_name="Differential")
|
||||||
|
|||||||
@@ -4,16 +4,6 @@
|
|||||||
Atlas
|
Atlas
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block css %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block js %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% endblock %}
|
|
||||||
{% block navigation %}
|
{% block navigation %}
|
||||||
Atlas:
|
Atlas:
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
@@ -28,3 +18,6 @@
|
|||||||
Questions by:
|
Questions by:
|
||||||
<span id="authors-link"><a href="{% url 'atlas:author_list' %}">author</a></span> {% endcomment %}
|
<span id="authors-link"><a href="{% url 'atlas:author_list' %}">author</a></span> {% endcomment %}
|
||||||
{% endblock %}
|
{% 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.
|
Use this form to create a atlas case. Existing associated image sets can be added using this form.
|
||||||
|
|
||||||
{% if form.collection %}
|
{% 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 %}
|
{% endif %}
|
||||||
<form action="" method="post" enctype="multipart/form-data" id="atlas-form">
|
<form action="" method="post" enctype="multipart/form-data" id="atlas-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
|
|
||||||
{% if collection.publish_results or cid_user_exam.completed %}
|
{% if collection.publish_results or cid_user_exam.completed %}
|
||||||
review
|
<span class="stamp-white">REVIEW</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
<a href="{% url 'atlas:exam_cids' collection.pk %}">Candidates</a> /
|
<a href="{% url 'atlas:exam_cids' collection.pk %}">Candidates</a> /
|
||||||
<a href="{% url 'atlas:atlas_create_exam' pk=collection.pk %}">Add New Case</a>
|
<a href="{% url 'atlas:atlas_create_exam' pk=collection.pk %}">Add New Case</a>
|
||||||
<div class="floating-header">
|
<div class="floating-header">
|
||||||
<a href="{% url 'atlas:collection_update' collection.id %}" title="Edit the Collection">Edit</a>
|
<a href="{% url 'atlas:exam_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_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 %}
|
{% comment %} \ <a href="{% url 'atlas:collection_clone' collection.id %}" title="Clone the Collection">Clone</a> {% endcomment %}
|
||||||
{% if request.user.is_superuser %}
|
{% 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>
|
\ <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' %}
|
{% extends 'atlas/base.html' %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<a href="{% url 'atlas:collection_create' %}">Create collection</a>
|
<a href="{% url 'atlas:exam_create' %}">Create collection</a>
|
||||||
<ul>
|
<ul>
|
||||||
{% for collection in collections %}
|
{% for collection in collections %}
|
||||||
<li><a href='{{collection.get_absolute_url}}'>{{collection.name}}</a></li>
|
<li><a href='{{collection.get_absolute_url}}'>{{collection.name}}</a></li>
|
||||||
|
|||||||
@@ -8,16 +8,15 @@
|
|||||||
<h2>Collection: {{collection.name}}
|
<h2>Collection: {{collection.name}}
|
||||||
|
|
||||||
{% if collection.publish_results or cid_user_exam.completed %}
|
{% if collection.publish_results or cid_user_exam.completed %}
|
||||||
review
|
<span class="stamp-white">REVIEW</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div><p>Questions</p></div>
|
|
||||||
{{answer_count}} out of {{collection_length}} cases answered. Click to go to case.
|
{{answer_count}} out of {{collection_length}} cases answered. Click to go to case.
|
||||||
<div class="sba-finish-list">
|
<div class="sba-finish-list">
|
||||||
<ul>
|
<ul>
|
||||||
{% for question, answer in question_answer_tuples %}
|
{% for question, answer in question_answer_tuples %}
|
||||||
<li>
|
<li> <span {% if not answer %}class="unanswered"{% endif %}>
|
||||||
<a href="
|
<a href="
|
||||||
{% if cid is not None %}
|
{% if cid is not None %}
|
||||||
{% url 'atlas:collection_case_view_take' pk=collection.id case_number=forloop.counter0 cid=cid passcode=passcode %}
|
{% 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 %}
|
{% url 'atlas:collection_case_view_take_user' pk=collection.id case_number=forloop.counter0 %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
">
|
">
|
||||||
<span {% if not answer %}class="unanswered"{% endif %}>Case: {{forloop.counter}}</span>
|
Case: {{forloop.counter}}
|
||||||
</a>
|
</a>
|
||||||
<br/>
|
<br/>
|
||||||
{{answer.answer}}
|
{% if not answer %}
|
||||||
|
No answer
|
||||||
|
{% else %}
|
||||||
|
{{answer.answer}}
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
{% extends 'atlas/base.html' %}
|
{% extends 'atlas/base.html' %}
|
||||||
|
|
||||||
{% block content %}
|
{% 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 %}
|
{% if request.user.is_authenticated and valid_user %}
|
||||||
User: {{request.user}}<br/>
|
User: {{request.user}}<br/>
|
||||||
|
|
||||||
{% if cid_exam %}
|
{% if cid_exam %}
|
||||||
Started: {{cid_exam.start_time}} <br/>
|
Started: {{cid_exam.start_time}} <br/>
|
||||||
|
|
||||||
|
|
||||||
|
{% if cid_exam.completed %}
|
||||||
|
Ended: {{cid_exam.end_time}} <br/>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
<a href="{% url 'atlas:collection_case_view_take_user' pk=collection.pk case_number=0 %}">
|
<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>
|
<button>Start</button>
|
||||||
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
Enter your CID and passcode in the below boxes.<br />
|
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:collection_scores_cid' exam.pk %}">Scores</a> /
|
||||||
<a href="{% url 'atlas:exam_cids' exam.pk %}">Candidates</a>
|
<a href="{% url 'atlas:exam_cids' exam.pk %}">Candidates</a>
|
||||||
<div class="floating-header">
|
<div class="floating-header">
|
||||||
<a href="{% url 'atlas:collection_update' exam.id %}" title="Edit the Collection">Edit</a>
|
<a href="{% url 'atlas:exam_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_deleted' exam.id %}" title="Delete the Collection">Delete</a>
|
||||||
{% if request.user.is_superuser %}
|
{% 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>
|
\ <a href="{% url 'admin:atlas_casecollection_change' exam.id %}" title="Edit the Collection using the admin interface">Admin Edit</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
+18
-3
@@ -21,17 +21,17 @@ urlpatterns = [
|
|||||||
path(
|
path(
|
||||||
"collection/create",
|
"collection/create",
|
||||||
views.CaseCollectionCreate.as_view(),
|
views.CaseCollectionCreate.as_view(),
|
||||||
name="collection_create",
|
name="exam_create",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"collection/<int:pk>/delete",
|
"collection/<int:pk>/delete",
|
||||||
views.CaseCollectionDelete.as_view(),
|
views.CaseCollectionDelete.as_view(),
|
||||||
name="collection_delete",
|
name="exam_deleted",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"collection/<int:pk>/update",
|
"collection/<int:pk>/update",
|
||||||
views.CaseCollectionUpdate.as_view(),
|
views.CaseCollectionUpdate.as_view(),
|
||||||
name="collection_update",
|
name="exam_update",
|
||||||
),
|
),
|
||||||
path("collection/<int:pk>", views.collection_detail, name="collection_detail"),
|
path("collection/<int:pk>", views.collection_detail, name="collection_detail"),
|
||||||
path(
|
path(
|
||||||
@@ -44,6 +44,16 @@ urlpatterns = [
|
|||||||
views.GenericExamViews.exam_cids,
|
views.GenericExamViews.exam_cids,
|
||||||
name="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(
|
path(
|
||||||
"collection/<int:exam_id>/cids/<int:cid>/delete_answers",
|
"collection/<int:exam_id>/cids/<int:cid>/delete_answers",
|
||||||
views.delete_collection_cid_answers,
|
views.delete_collection_cid_answers,
|
||||||
@@ -59,6 +69,11 @@ urlpatterns = [
|
|||||||
views.GenericExamViews.exam_cids_edit,
|
views.GenericExamViews.exam_cids_edit,
|
||||||
name="exam_cids_edit",
|
name="exam_cids_edit",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"collection/<int:exam_id>/users/edit",
|
||||||
|
views.GenericExamViews.exam_users_edit,
|
||||||
|
name="exam_users_edit",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"collection/<int:pk>/mark",
|
"collection/<int:pk>/mark",
|
||||||
views.collection_mark_overview,
|
views.collection_mark_overview,
|
||||||
|
|||||||
@@ -442,6 +442,7 @@ class SeriesCreate(RevisionMixin, LoginRequiredMixin, CreateView):
|
|||||||
class CaseCollectionUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateView):
|
class CaseCollectionUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateView):
|
||||||
model = CaseCollection
|
model = CaseCollection
|
||||||
form_class = CaseCollectionForm
|
form_class = CaseCollectionForm
|
||||||
|
template_name = "atlas/collection_update_form.html"
|
||||||
|
|
||||||
# fields = '__all__'
|
# fields = '__all__'
|
||||||
# #fields = [ 'condition' ]
|
# #fields = [ 'condition' ]
|
||||||
@@ -467,6 +468,8 @@ class CaseCollectionUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateVi
|
|||||||
context["case_formset"] = CaseCollectionCaseFormSet(
|
context["case_formset"] = CaseCollectionCaseFormSet(
|
||||||
instance=self.object, form_kwargs={"user": self.request.user}
|
instance=self.object, form_kwargs={"user": self.request.user}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
context["collection"] = context["casecollection"]
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
|
|||||||
+62
-26
@@ -4,7 +4,7 @@ import os
|
|||||||
from typing import Self, Tuple
|
from typing import Self, Tuple
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.http import HttpRequest
|
from django.http import Http404, HttpRequest
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from smtplib import SMTPException
|
from smtplib import SMTPException
|
||||||
|
|
||||||
@@ -393,6 +393,30 @@ class ExamCollectionGenericBase(models.Model):
|
|||||||
# Is this actually used?
|
# Is this actually used?
|
||||||
cid_users = GenericRelation("generic.CidUserExam")
|
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:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
@@ -412,6 +436,42 @@ class ExamCollectionGenericBase(models.Model):
|
|||||||
"""Helper to check if the logged in user can access the exam"""
|
"""Helper to check if the logged in user can access the exam"""
|
||||||
return self.check_cid_user(request=request, user_id=request.user.id)
|
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(
|
def check_cid_user(
|
||||||
self,
|
self,
|
||||||
cid: int | None = None,
|
cid: int | None = None,
|
||||||
@@ -524,13 +584,6 @@ class ExamCollectionGenericBase(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class ExamBase(ExamCollectionGenericBase):
|
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(
|
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)",
|
help_text="If an exam should be taken in exam mode (users results will be submited to the server for marking)",
|
||||||
default=False,
|
default=False,
|
||||||
@@ -541,10 +594,6 @@ class ExamBase(ExamCollectionGenericBase):
|
|||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
publish_results = models.BooleanField(
|
|
||||||
help_text="If an exams results should be available", default=False
|
|
||||||
)
|
|
||||||
|
|
||||||
recreate_json = models.BooleanField(
|
recreate_json = models.BooleanField(
|
||||||
help_text="If the json cache needs updating", default=False
|
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"
|
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(
|
authors_only = models.BooleanField(
|
||||||
help_text="If true only exam authors will be able to view.",
|
help_text="If true only exam authors will be able to view.",
|
||||||
default=False,
|
default=False,
|
||||||
@@ -1090,10 +1129,7 @@ class CidUserExam(models.Model):
|
|||||||
else:
|
else:
|
||||||
end_time = f"{ self.end_time:%Y-%m-%d %H:%M }"
|
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 = (
|
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):
|
def exam_scores_cid_user(request, pk, cid=None, passcode=None):
|
||||||
exam = get_object_or_404(Exam, pk=pk)
|
exam = get_object_or_404(Exam, pk=pk)
|
||||||
|
|
||||||
if not exam.exam_mode:
|
exam.check_user_can_review(cid, passcode, request)
|
||||||
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")
|
|
||||||
|
|
||||||
questions = exam.exam_questions.all()
|
questions = exam.exam_questions.all()
|
||||||
|
|
||||||
@@ -208,11 +203,7 @@ def exam_start(request, pk):
|
|||||||
def exam_take_overview(request, pk, cid=None, passcode=None):
|
def exam_take_overview(request, pk, cid=None, passcode=None):
|
||||||
exam = get_object_or_404(Exam, pk=pk)
|
exam = get_object_or_404(Exam, pk=pk)
|
||||||
|
|
||||||
if not exam.active:
|
exam.check_user_can_take(cid, passcode, request)
|
||||||
return exam_inactive(request, context={"exam": exam})
|
|
||||||
|
|
||||||
if not exam.check_cid_user(cid, passcode, request):
|
|
||||||
raise Http404("Error accessing exam")
|
|
||||||
|
|
||||||
questions = exam.exam_questions.all()
|
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:
|
if not exam.active:
|
||||||
return exam_inactive(request, context={"exam": exam})
|
return exam_inactive(request, context={"exam": exam})
|
||||||
|
|
||||||
if not exam.check_cid_user(cid, passcode, request):
|
exam.check_user_can_take(cid, passcode, request)
|
||||||
raise Http404("Error accessing exam")
|
|
||||||
|
|
||||||
print(cid, passcode, request.user)
|
|
||||||
|
|
||||||
cid_user_exam = exam.get_or_create_cid_user_exam(cid=cid, user_user=request.user)
|
cid_user_exam = exam.get_or_create_cid_user_exam(cid=cid, user_user=request.user)
|
||||||
|
|
||||||
|
|||||||
@@ -750,10 +750,19 @@ input {
|
|||||||
min-width: 10px;
|
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 {
|
.no-select {
|
||||||
@@ -975,3 +984,19 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
.cid-candidate-list div {
|
.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 generic.views import ExamViews
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from rich.pretty import pprint
|
||||||
|
|
||||||
def AssertNotFound(client, url):
|
def AssertNotFound(client, url):
|
||||||
"""Helper to quickly test for urls that should return a 404 (NOT FOUND) error"""
|
"""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 %}">
|
<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">
|
<div class="date">
|
||||||
{{ question.created_date|date:"d/m/Y" }}
|
{{ question.created_date|date:"d/m/Y" }}
|
||||||
</div>
|
</div>
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
<p class="pre-whitespace"><b>Rapid:</b> {{ question }}</p>
|
<p class="pre-whitespace"><b>Rapid:</b> {{ question }}</p>
|
||||||
<p class="pre-whitespace"><b>Normal:</b> {{ question.normal }} <button id="toggle-normal-button"
|
<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>Region:</b> {{ question.get_regions }}</p>
|
||||||
<p class="pre-whitespace"><b>Examination:</b> {{ question.get_examinations }}</p>
|
<p class="pre-whitespace"><b>Examination:</b> {{ question.get_examinations }}</p>
|
||||||
<p class="pre-whitespace"><b>Laterality:</b> {{ question.laterality }}
|
<p class="pre-whitespace"><b>Laterality:</b> {{ question.laterality }}
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<span class="image-block">
|
<span class="image-block">
|
||||||
Image {{ forloop.counter }}{% if image.description %} ({{image.description}}){% endif %}{% if image.feedback_image %} [feedback image]{% endif %}:
|
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 %}"
|
<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>
|
</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
@@ -33,11 +33,11 @@
|
|||||||
<a href="{% url 'rapids:exam_overview' pk=exam.pk %}">{{ exam.name }}</a>,
|
<a href="{% url 'rapids:exam_overview' pk=exam.pk %}">{{ exam.name }}</a>,
|
||||||
{% endfor %}
|
{% 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-exam_list_url="{% url 'api-1:rapid_user_exams' %}" data-type="rapids" data-csrf="{{ csrf_token}}"
|
||||||
data-qid="{{question.pk}}">Add to exam</button>
|
data-qid="{{question.pk}}">Add to exam</button>
|
||||||
<button id="cancel-add-to-exam" style="display:none">Cancel</button>
|
<button id="cancel-add-to-exam" style="display:none">Cancel</button>
|
||||||
<span id="exam-options"></span>
|
<span id="exam-options"></span> {% endcomment %}
|
||||||
</div>
|
</div>
|
||||||
<p class="pre-whitespace"><b>Open Access:</b> {{ question.open_access }}</p>
|
<p class="pre-whitespace"><b>Open Access:</b> {{ question.open_access }}</p>
|
||||||
<p class="pre-whitespace"><b>Feedback:</b> {{ question.feedback }}</p>
|
<p class="pre-whitespace"><b>Feedback:</b> {{ question.feedback }}</p>
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span {% if answer.proposed %}class="proposed-answer" data-aid="{{answer.pk}}" data-question-type="rapid"
|
<span {% if answer.proposed %}class="proposed-answer" data-aid="{{answer.pk}}" data-question-type="rapid"
|
||||||
{% endif %}>
|
{% endif %}>
|
||||||
{{ answer }}
|
{{ answer }}
|
||||||
</span>
|
</span>
|
||||||
<td>
|
<td>
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
Image viewer
|
Image viewer
|
||||||
</summary>
|
</summary>
|
||||||
<div id="single-dicom-viewer" class="dicom-viewer" data-images="{{ question.get_image_url_array }}"
|
<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>
|
</div>
|
||||||
<button id="save-annotations" class="save-rapid-annotations">Save Annotations</button>
|
<button id="save-annotations" class="save-rapid-annotations">Save Annotations</button>
|
||||||
</details>
|
</details>
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</details>
|
</details>
|
||||||
<a href="{% url 'rapids:question_anonymise_dicom' pk=question.pk %}"
|
<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>
|
<script>
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
$("#edit-button").click(() => {
|
$("#edit-button").click(() => {
|
||||||
@@ -156,21 +156,21 @@
|
|||||||
})
|
})
|
||||||
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||||
.done(function (data) {
|
.done(function (data) {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
// show some message according to the response.
|
// show some message according to the response.
|
||||||
// For eg. A message box showing that the status has been changed
|
// For eg. A message box showing that the status has been changed
|
||||||
if (data.status == "success") {
|
if (data.status == "success") {
|
||||||
toastr.info('Annotations saved')
|
toastr.info('Annotations saved')
|
||||||
} else {
|
} else {
|
||||||
toastr.warning('Error saving annotations')
|
toastr.warning('Error saving annotations')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.always(function () {
|
.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();
|
$("#add-to-exam").toggle();
|
||||||
$("#cancel-add-to-exam").toggle();
|
$("#cancel-add-to-exam").toggle();
|
||||||
$("#exam-options").empty();
|
$("#exam-options").empty();
|
||||||
@@ -296,7 +296,8 @@
|
|||||||
alert('Error updating this model instance.');
|
alert('Error updating this model instance.');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
$(".suggested_answers li").each((n, el) => {
|
$(".suggested_answers li").each((n, el) => {
|
||||||
$(el).append($("<span class='correct'>[Add Correct]</span>").on("click", function () {
|
$(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
|
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||||
.done(function (data) {
|
.done(function (data) {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
toastr.info('Answer saved')
|
toastr.info('Answer saved')
|
||||||
$(el).find(".correct").remove()
|
$(el).find(".correct").remove()
|
||||||
}
|
}
|
||||||
// show some message according to the response.
|
// show some message according to the response.
|
||||||
// For eg. A message box showing that the status has been changed
|
// For eg. A message box showing that the status has been changed
|
||||||
})
|
})
|
||||||
.always(function () {
|
.always(function () {
|
||||||
console.log('[Done]');
|
console.log('[Done]');
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -350,19 +351,19 @@
|
|||||||
})
|
})
|
||||||
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||||
.done(function (data) {
|
.done(function (data) {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
toastr.info('Answer saved')
|
toastr.info('Answer saved')
|
||||||
$(el).find(".confirm").remove()
|
$(el).find(".confirm").remove()
|
||||||
$(el).removeClass("proposed-answer")
|
$(el).removeClass("proposed-answer")
|
||||||
}
|
}
|
||||||
// show some message according to the response.
|
// show some message according to the response.
|
||||||
// For eg. A message box showing that the status has been changed
|
// For eg. A message box showing that the status has been changed
|
||||||
})
|
})
|
||||||
.always(function () {
|
.always(function () {
|
||||||
console.log('[Done]');
|
console.log('[Done]');
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Add button to confirm answer is incorrect
|
// Add button to confirm answer is incorrect
|
||||||
@@ -381,19 +382,19 @@
|
|||||||
})
|
})
|
||||||
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||||
.done(function (data) {
|
.done(function (data) {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
toastr.info('Answer saved')
|
toastr.info('Answer saved')
|
||||||
$(el).find(".confirm").remove()
|
$(el).find(".confirm").remove()
|
||||||
$(el).removeClass("proposed-answer")
|
$(el).removeClass("proposed-answer")
|
||||||
}
|
}
|
||||||
// show some message according to the response.
|
// show some message according to the response.
|
||||||
// For eg. A message box showing that the status has been changed
|
// For eg. A message box showing that the status has been changed
|
||||||
})
|
})
|
||||||
.always(function () {
|
.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):
|
def exam_scores_cid_user(request, pk, cid, passcode):
|
||||||
exam = get_object_or_404(Exam, pk=pk)
|
exam = get_object_or_404(Exam, pk=pk)
|
||||||
|
|
||||||
if not exam.check_cid_user(cid, passcode, request):
|
exam.check_user_can_review(cid, passcode, request)
|
||||||
raise Http404("Error accessing exam")
|
|
||||||
|
|
||||||
questions = exam.exam_questions.all()
|
questions = exam.exam_questions.all()
|
||||||
|
|
||||||
@@ -179,8 +178,7 @@ def exam_take_overview(request, pk, cid=None, passcode=None):
|
|||||||
if not exam.active:
|
if not exam.active:
|
||||||
return exam_inactive(request, context={"exam": exam})
|
return exam_inactive(request, context={"exam": exam})
|
||||||
|
|
||||||
if not exam.check_cid_user(cid, passcode, request):
|
exam.check_user_can_take(cid, passcode, request)
|
||||||
raise Http404("Error accessing exam")
|
|
||||||
|
|
||||||
questions = exam.exam_questions.all()
|
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):
|
def exam_take(request, pk: int, sk: int, cid: int = None, passcode: str = None):
|
||||||
exam = get_object_or_404(Exam, pk=pk)
|
exam = get_object_or_404(Exam, pk=pk)
|
||||||
|
|
||||||
if not exam.active:
|
exam.check_user_can_take(cid, passcode, request)
|
||||||
raise Http404("Exam not found")
|
|
||||||
|
|
||||||
if not exam.check_cid_user(cid, passcode, request):
|
|
||||||
raise Http404("Error accessing exam")
|
|
||||||
|
|
||||||
cid_user_exam = exam.get_or_create_cid_user_exam(cid = cid, user_user=request.user)
|
cid_user_exam = exam.get_or_create_cid_user_exam(cid = cid, user_user=request.user)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user