continue building examcollections
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.1.4 on 2024-02-05 10:23
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('anatomy', '0012_alter_exam_exam_collection'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='exam',
|
||||
old_name='exam_collection',
|
||||
new_name='examcollection',
|
||||
),
|
||||
]
|
||||
+1
-1
@@ -404,7 +404,7 @@ class Exam(ExamBase):
|
||||
|
||||
exam_user_status = GenericRelation(ExamUserStatus)
|
||||
|
||||
exam_collection = models.ForeignKey(ExamCollection, blank=True, null=True, on_delete=models.SET_NULL, related_name="anatomy_exams")
|
||||
examcollection = models.ForeignKey(ExamCollection, blank=True, null=True, on_delete=models.SET_NULL, related_name="anatomy_exams")
|
||||
|
||||
def get_exam_json(self, based=True):
|
||||
questions = self.exam_questions.all()
|
||||
|
||||
+84
-10
@@ -17,6 +17,7 @@ from generic.models import (
|
||||
CidUser,
|
||||
CidUserGroup,
|
||||
Examination,
|
||||
ExamCollection,
|
||||
QuestionNote,
|
||||
Supervisor,
|
||||
UserGrades,
|
||||
@@ -333,6 +334,7 @@ class UserGroupExamForm(ModelForm):
|
||||
"physics_user_user_groups",
|
||||
"sba_user_user_groups",
|
||||
]
|
||||
|
||||
# users = UserUserGroupModelChoiceField(
|
||||
# required=False,
|
||||
# queryset=User.objects.all(),
|
||||
@@ -346,10 +348,10 @@ class UserGroupExamForm(ModelForm):
|
||||
# The widget for a ModelMultipleChoiceField expects
|
||||
# a list of primary key for the selected data.
|
||||
for groups in self.GROUP_TYPES:
|
||||
#instance.anatomy_cid_user_groups.clear()
|
||||
#obj = getattr(instance,groups)
|
||||
# instance.anatomy_cid_user_groups.clear()
|
||||
# obj = getattr(instance,groups)
|
||||
initial[groups] = [
|
||||
t.pk for t in getattr(kwargs["instance"],groups).all()
|
||||
t.pk for t in getattr(kwargs["instance"], groups).all()
|
||||
]
|
||||
|
||||
ModelForm.__init__(self, *args, **kwargs)
|
||||
@@ -401,8 +403,8 @@ class UserGroupExamForm(ModelForm):
|
||||
old_save_m2m()
|
||||
|
||||
for groups in self.GROUP_TYPES:
|
||||
#instance.anatomy_cid_user_groups.clear()
|
||||
obj = getattr(instance,groups)
|
||||
# instance.anatomy_cid_user_groups.clear()
|
||||
obj = getattr(instance, groups)
|
||||
obj.clear()
|
||||
for exam in self.cleaned_data[groups]:
|
||||
obj.add(exam.pk)
|
||||
@@ -438,10 +440,10 @@ class CidGroupExamForm(ModelForm):
|
||||
# The widget for a ModelMultipleChoiceField expects
|
||||
# a list of primary key for the selected data.
|
||||
for groups in self.GROUP_TYPES:
|
||||
#instance.anatomy_cid_user_groups.clear()
|
||||
#obj = getattr(instance,groups)
|
||||
# instance.anatomy_cid_user_groups.clear()
|
||||
# obj = getattr(instance,groups)
|
||||
initial[groups] = [
|
||||
t.pk for t in getattr(kwargs["instance"],groups).all()
|
||||
t.pk for t in getattr(kwargs["instance"], groups).all()
|
||||
]
|
||||
|
||||
ModelForm.__init__(self, *args, **kwargs)
|
||||
@@ -493,8 +495,8 @@ class CidGroupExamForm(ModelForm):
|
||||
old_save_m2m()
|
||||
|
||||
for groups in self.GROUP_TYPES:
|
||||
#instance.anatomy_cid_user_groups.clear()
|
||||
obj = getattr(instance,groups)
|
||||
# instance.anatomy_cid_user_groups.clear()
|
||||
obj = getattr(instance, groups)
|
||||
obj.clear()
|
||||
for exam in self.cleaned_data[groups]:
|
||||
obj.add(exam.pk)
|
||||
@@ -549,3 +551,75 @@ class SupervisorForm(ModelForm):
|
||||
class Meta:
|
||||
model = Supervisor
|
||||
exclude = ("",)
|
||||
|
||||
|
||||
class ExamCollectionForm(ModelForm):
|
||||
GROUP_TYPES = (
|
||||
("Rapid Exams", "rapids_exams", RapidsExam),
|
||||
("Long Exams", "longs_exams", LongsExam),
|
||||
("SBA Exams", "sbas_exams", SbasExam),
|
||||
("Physic Exams", "physics_exams", PhysicsExam),
|
||||
("Anatomy Exams", "anatomy_exams", AnatomyExam),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ExamCollection
|
||||
exclude = ("",)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if kwargs.get("instance"):
|
||||
# We get the 'initial' keyword argument or initialize it
|
||||
# as a dict if it didn't exist.
|
||||
initial = kwargs.setdefault("initial", {})
|
||||
# The widget for a ModelMultipleChoiceField expects
|
||||
# a list of primary key for the selected data.
|
||||
for name, reverse_prop, exam_obj in self.GROUP_TYPES:
|
||||
# instance.anatomy_cid_user_groups.clear()
|
||||
# obj = getattr(instance,groups)
|
||||
initial[reverse_prop] = [
|
||||
t.pk for t in getattr(kwargs["instance"], reverse_prop).all()
|
||||
]
|
||||
|
||||
ModelForm.__init__(self, *args, **kwargs)
|
||||
|
||||
super(ExamCollectionForm, self).__init__(*args, **kwargs)
|
||||
|
||||
for name, reverse_prop, exam_obj in self.GROUP_TYPES:
|
||||
self.fields[reverse_prop] = ModelMultipleChoiceField(
|
||||
required=False,
|
||||
queryset=exam_obj.objects.filter(archive=False),
|
||||
widget=FilteredSelectMultiple(
|
||||
verbose_name=name, is_stacked=False
|
||||
),
|
||||
)
|
||||
|
||||
def save(self, commit=True):
|
||||
# Get the unsaved Long instance
|
||||
instance = ModelForm.save(self, False)
|
||||
|
||||
# Prepare a 'save_m2m' method for the form,
|
||||
old_save_m2m = self.save_m2m
|
||||
|
||||
def save_m2m():
|
||||
old_save_m2m()
|
||||
|
||||
for name, reverse_prop, exam_obj in self.GROUP_TYPES:
|
||||
# instance.anatomy_cid_user_groups.clear()
|
||||
obj = getattr(instance, reverse_prop)
|
||||
obj.clear()
|
||||
for exam in self.cleaned_data[reverse_prop]:
|
||||
obj.add(exam)
|
||||
|
||||
self.save_m2m = save_m2m
|
||||
|
||||
# Do we need to save all changes now?
|
||||
# if commit:
|
||||
instance.save()
|
||||
self.save_m2m()
|
||||
|
||||
return instance
|
||||
|
||||
class ExamCollectionCloneForm(ModelForm):
|
||||
class Meta:
|
||||
model = ExamCollection
|
||||
fields = ("name", "date")
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.1.4 on 2024-02-05 09:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('generic', '0012_examcollection_author'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='examcollection',
|
||||
name='archive',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
+82
-20
@@ -39,6 +39,7 @@ from easy_thumbnails.exceptions import InvalidImageFormatError
|
||||
import pydicom
|
||||
import dicognito.anonymizer
|
||||
from django.contrib.auth.models import User
|
||||
from django.forms.models import model_to_dict
|
||||
|
||||
|
||||
def findMiddle(input_list):
|
||||
@@ -49,8 +50,6 @@ def findMiddle(input_list):
|
||||
return input_list[int(middle)]
|
||||
|
||||
|
||||
|
||||
|
||||
class Modality(models.Model):
|
||||
modality = models.CharField(max_length=200, unique=True)
|
||||
short_code = models.CharField(max_length=5, unique=True, null=True)
|
||||
@@ -223,7 +222,6 @@ class SeriesImageBase(models.Model):
|
||||
help_text="Set to true if the file this object refers to has been removed (either from series truncation or merging)",
|
||||
)
|
||||
|
||||
|
||||
class Meta:
|
||||
ordering = ["position"]
|
||||
abstract = True
|
||||
@@ -244,7 +242,9 @@ class SeriesImageBase(models.Model):
|
||||
|
||||
def generate_md5_hash(self):
|
||||
if not self.removed:
|
||||
image_hash, is_dicom = get_image_hash(self.image, hash_type="md5", direct_pixel_data=False)
|
||||
image_hash, is_dicom = get_image_hash(
|
||||
self.image, hash_type="md5", direct_pixel_data=False
|
||||
)
|
||||
|
||||
self.is_dicom = is_dicom
|
||||
self.image_md5_hash = image_hash
|
||||
@@ -253,7 +253,9 @@ class SeriesImageBase(models.Model):
|
||||
|
||||
def generate_blake3_hash(self):
|
||||
if not self.removed:
|
||||
image_blake3_hash, is_dicom = get_image_hash(self.image, hash_type="blake3", direct_pixel_data=True)
|
||||
image_blake3_hash, is_dicom = get_image_hash(
|
||||
self.image, hash_type="blake3", direct_pixel_data=True
|
||||
)
|
||||
self.is_dicom = is_dicom
|
||||
self.image_blake3_hash = image_blake3_hash
|
||||
|
||||
@@ -262,19 +264,21 @@ class SeriesImageBase(models.Model):
|
||||
def save(self, *args, **kwargs):
|
||||
"""Override save method to add image hash"""
|
||||
if self.image:
|
||||
#if not self.image_md5_hash:
|
||||
# if not self.image_md5_hash:
|
||||
# image_hash, is_dicom = get_image_hash(self.image, hash_type="md5", direct_pixel_data=False)
|
||||
|
||||
# self.is_dicom = is_dicom
|
||||
# self.image_md5_hash = image_hash
|
||||
|
||||
if not self.image_blake3_hash:
|
||||
image_blake3_hash, is_dicom = get_image_hash(self.image, hash_type="blake3", direct_pixel_data=True)
|
||||
image_blake3_hash, is_dicom = get_image_hash(
|
||||
self.image, hash_type="blake3", direct_pixel_data=True
|
||||
)
|
||||
self.is_dicom = is_dicom
|
||||
self.image_blake3_hash = image_blake3_hash
|
||||
|
||||
## Hack for tests
|
||||
#if image_hash != "12345ABCD":
|
||||
# if image_hash != "12345ABCD":
|
||||
# super().save(*args, **kwargs) # Call the "real" save() method.
|
||||
|
||||
super().save(*args, **kwargs) # Call the "real" save() method.
|
||||
@@ -301,12 +305,24 @@ class SeriesBase(models.Model):
|
||||
modified_date = models.DateTimeField(auto_now=True)
|
||||
|
||||
class SeriesModifiedChocies(models.TextChoices):
|
||||
NO = "NO", "Not modified",
|
||||
TR = "TR", "Truncated",
|
||||
RE = "RE", "Resliced/resampled",
|
||||
NO = (
|
||||
"NO",
|
||||
"Not modified",
|
||||
)
|
||||
TR = (
|
||||
"TR",
|
||||
"Truncated",
|
||||
)
|
||||
RE = (
|
||||
"RE",
|
||||
"Resliced/resampled",
|
||||
)
|
||||
|
||||
|
||||
modified = models.CharField(max_length=2, default=SeriesModifiedChocies.NO, choices=SeriesModifiedChocies.choices)
|
||||
modified = models.CharField(
|
||||
max_length=2,
|
||||
default=SeriesModifiedChocies.NO,
|
||||
choices=SeriesModifiedChocies.choices,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@@ -317,7 +333,7 @@ class SeriesBase(models.Model):
|
||||
def check_user_can_edit(self, user: User):
|
||||
if user.is_superuser:
|
||||
return True
|
||||
|
||||
|
||||
if user in self.get_author_objects():
|
||||
return True
|
||||
|
||||
@@ -558,7 +574,7 @@ class ExamOrCollectionGenericBase(models.Model, AuthorMixin):
|
||||
def check_user_can_edit(self, user: User):
|
||||
if user.is_superuser:
|
||||
return True
|
||||
|
||||
|
||||
if user in self.get_author_objects():
|
||||
return True
|
||||
|
||||
@@ -720,6 +736,48 @@ class ExamOrCollectionGenericBase(models.Model, AuthorMixin):
|
||||
content_type=content_type, object_id=self.pk, user_user=user_user
|
||||
)
|
||||
|
||||
def clone_model(self):
|
||||
M2M_fields = (
|
||||
"exam_questions",
|
||||
"author",
|
||||
"valid_cid_users",
|
||||
"valid_user_users",
|
||||
"cid_user_groups",
|
||||
"user_user_groups",
|
||||
)
|
||||
FK_fields = (
|
||||
"examcollection",
|
||||
)
|
||||
|
||||
model_dict = model_to_dict(self)
|
||||
|
||||
m2m_to_update = {}
|
||||
for field in M2M_fields:
|
||||
m2m_to_update[field] = model_dict[field]
|
||||
del model_dict[field]
|
||||
fk_to_update = {}
|
||||
for field in FK_fields:
|
||||
fk_to_update[field] = model_dict[field]
|
||||
del model_dict[field]
|
||||
|
||||
cloned_model = self.__class__(**model_dict)
|
||||
cloned_model.pk = None
|
||||
|
||||
for field in fk_to_update:
|
||||
setattr(cloned_model, f"{field}_id", fk_to_update[field])
|
||||
|
||||
cloned_model.save()
|
||||
|
||||
for field in m2m_to_update:
|
||||
rel = getattr(cloned_model, field)
|
||||
rel.clear()
|
||||
for item in m2m_to_update[field]:
|
||||
rel.add(item)
|
||||
|
||||
|
||||
|
||||
return cloned_model
|
||||
|
||||
|
||||
class ExamBase(ExamOrCollectionGenericBase):
|
||||
exam_mode = models.BooleanField(
|
||||
@@ -831,7 +889,7 @@ class ExamBase(ExamOrCollectionGenericBase):
|
||||
def get_question_index(self, question):
|
||||
return list(self.exam_questions.all()).index(question)
|
||||
|
||||
def get_question_by_index(self, index:int):
|
||||
def get_question_by_index(self, index: int):
|
||||
# There must be a better way to do this
|
||||
return list(self.exam_questions.all())[index]
|
||||
|
||||
@@ -890,7 +948,7 @@ class ExamBase(ExamOrCollectionGenericBase):
|
||||
|
||||
"""
|
||||
if self.app_name == "rapids":
|
||||
html_msg =f"{html_msg}<br/>{rapids_extra}"
|
||||
html_msg = f"{html_msg}<br/>{rapids_extra}"
|
||||
|
||||
return msg, html_msg
|
||||
|
||||
@@ -914,7 +972,6 @@ class ExamBase(ExamOrCollectionGenericBase):
|
||||
else:
|
||||
extra = "No supervisor"
|
||||
|
||||
|
||||
if additional_emails is not None:
|
||||
emails.extend(additional_emails)
|
||||
|
||||
@@ -1505,10 +1562,13 @@ def create_user_profile(sender, instance, created, **kwargs):
|
||||
def save_user_profile(sender, instance, **kwargs):
|
||||
instance.userprofile.save()
|
||||
|
||||
|
||||
class ExamCollection(models.Model, AuthorMixin):
|
||||
name = models.CharField(max_length=255)
|
||||
|
||||
date = models.DateField(blank=True,null=True)
|
||||
date = models.DateField(blank=True, null=True)
|
||||
|
||||
archive = models.BooleanField(default=False)
|
||||
|
||||
author = models.ManyToManyField(
|
||||
settings.AUTH_USER_MODEL,
|
||||
@@ -1518,4 +1578,6 @@ class ExamCollection(models.Model, AuthorMixin):
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} [{self.date}]"
|
||||
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("generic:examcollection_detail", kwargs={"pk": self.pk})
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<a href="{% url 'generic:user_group_view' %}">User Groups</a> /
|
||||
<a href="{% url 'accounts_list' %}">Manage Users</a> /
|
||||
<a href="{% url 'generic:supervisor' %}">Manage Supervisors</a> /
|
||||
<a href="{% url 'generic:exam_collection_list' %}">Collections</a>
|
||||
<a href="{% url 'generic:examcollection_list' %}">Collections</a>
|
||||
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -49,9 +49,17 @@ Open access: {{ exam.open_access }}<br />
|
||||
{% if exam.exam_mode %}
|
||||
<div class="parent-help" title="Click to enable / disable the exam results">
|
||||
Publish results: <input type="checkbox" id="exam-publish-results-switch" data-posturl="{% url app_name|add:':exam_toggle_results_published' pk=exam.pk %}"
|
||||
{% if exam.publish_results %}checked{% endif %}> <span class="help-text">[When checked the exam results will be available to users on this site]</span>
|
||||
{% if exam.publish_results %}checked{% endif %}> <span class="help-text">[When checked the exam results will be available to users on this site]</span>
|
||||
</div>
|
||||
{% if app_name == "anatomy" or app_name == "rapids" or app_name == "longs" %}
|
||||
<p><a href="{% url app_name|add:':mark_overview' pk=exam.pk %}"><button>Mark exam</button></a></p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if exam.examcollection %}
|
||||
Exam Collection: <a href="{{ exam.examcollection.get_absolute_url }}">{{ exam.examcollection }}</a> <br/>
|
||||
{% endif %}
|
||||
Author(s): {% for author in exam.author.all %}
|
||||
{{ author }}{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}<br/>
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
{% block navigation %}
|
||||
{{block.super}}
|
||||
<br/>Test
|
||||
<br/>
|
||||
{% comment %} <a href="{% url 'generic:cid_group_detail' cidusergroup.pk %}">Collections</a> / {% endcomment %}
|
||||
<a href="{% url 'generic:examcollection_create' %}">Create Exam Collection</a>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,65 @@
|
||||
{% extends "generic/base.html" %}
|
||||
<!-- {% load static from static %} -->
|
||||
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block css %}
|
||||
{% endblock %}
|
||||
{% block js %}
|
||||
<!--<script type="text/javascript" src="/admin/jsi18n/"></script>-->
|
||||
{{form.media}}
|
||||
|
||||
<script type="text/javascript">
|
||||
</script>
|
||||
|
||||
<!-- {{ form.media }} -->
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
|
||||
<h2>Clone Exam Collection {{object.name}}</h2>
|
||||
Clones the collection. This will maintain the collection name but will create clones of all of the associated exams.
|
||||
<form action="" method="post" enctype="multipart/form-data" id="examcollection-form">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
|
||||
|
||||
<p>The following exams will be cloned<br/>
|
||||
<h3>Anatomy Exams:</h3>
|
||||
<ul>
|
||||
{% for exam in view.initial_object.anatomy_exams.all %}
|
||||
<li>{{ exam }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<h3>Longs Exams:</h3>
|
||||
<ul>
|
||||
{% for exam in view.initial_object.longs_exams.all %}
|
||||
<li>{{ exam }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<h3>SBAs Exams:</h3>
|
||||
<ul>
|
||||
{% for exam in view.initial_object.sbas_exams.all %}
|
||||
<li>{{ exam }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<h3>Physics Exams:</h3>
|
||||
<ul>
|
||||
{% for exam in view.initial_object.physics_exams.all %}
|
||||
<li>{{ exam }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<h3>Rapids Exams:</h3>
|
||||
<ul>
|
||||
{% for exam in view.initial_object.rapids_exams.all %}
|
||||
<li>{{ exam }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</p>
|
||||
<input type="submit" class="submit-button" value="Submit" name="submit">
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -4,7 +4,49 @@
|
||||
|
||||
<h1>{{ object.name }} [{{object.date}}] </h1>
|
||||
|
||||
{{object.anatomy_exams}}
|
||||
<p>
|
||||
<a href="{% url 'generic:examcollection_edit' object.pk %}">Edit</a>
|
||||
<a href="{% url 'generic:examcollection_clone' object.pk %}">Clone</a>
|
||||
<a href="{% url 'generic:examcollection_delete' object.pk %}">Delete</a>
|
||||
</p>
|
||||
|
||||
This collection contains the following exams
|
||||
|
||||
|
||||
<h2>Anatomy Exams:</h2>
|
||||
<ul>
|
||||
{% for exam in object.anatomy_exams.all %}
|
||||
<li><a href="{% url 'anatomy:exam_overview' exam.pk %}">{{ exam }}</a> - <a href="{% url 'anatomy:exam_update' exam.pk %}">(Edit)</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<h2>Longs Exams:</h2>
|
||||
<ul>
|
||||
{% for exam in object.longs_exams.all %}
|
||||
<li><a href="{% url 'longs:exam_overview' exam.pk %}">{{ exam }}</a> - <a href="{% url 'longs:exam_update' exam.pk %}">(Edit)</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<h2>Rapids Exams:</h2>
|
||||
<ul>
|
||||
{% for exam in object.rapids_exams.all %}
|
||||
<li><a href="{% url 'rapids:exam_overview' exam.pk %}">{{ exam }}</a> - <a href="{% url 'rapids:exam_update' exam.pk %}">(Edit)</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<h2>Physics Exams:</h2>
|
||||
<ul>
|
||||
{% for exam in object.physics_exams.all %}
|
||||
<li><a href="{% url 'physics:exam_overview' exam.pk %}">{{ exam }}</a> - <a href="{% url 'physics:exam_update' exam.pk %}">(Edit)</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<h2>SBAs Exams:</h2>
|
||||
<ul>
|
||||
{% for exam in object.sbas_exams.all %}
|
||||
<li><a href="{% url 'sbas:exam_overview' exam.pk %}">{{ exam }}</a> - <a href="{% url 'sbas:exam_update' exam.pk %}">(Edit)</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
{% extends "generic/base.html" %}
|
||||
<!-- {% load static from static %} -->
|
||||
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block css %}
|
||||
{% endblock %}
|
||||
{% block js %}
|
||||
<!--<script type="text/javascript" src="/admin/jsi18n/"></script>-->
|
||||
{{form.media}}
|
||||
|
||||
<script type="text/javascript">
|
||||
</script>
|
||||
|
||||
<!-- {{ form.media }} -->
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
|
||||
<h2>Edit Exam Collection {{object.name}}</h2>
|
||||
<form action="" method="post" enctype="multipart/form-data" id="examcollection-form">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<input type="submit" class="submit-button" value="Submit" name="submit">
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -5,7 +5,7 @@
|
||||
<h1>Exam Collections</h1>
|
||||
<ul class="collections">
|
||||
{% for collection in object_list %}
|
||||
<li class="collection"><a href="{% url 'generic:exam_collection_detail' collection.pk %}">{{ collection.name }} [{{ collection.date}}] (Authors: {{ collection.get_authors }})</a></li>
|
||||
<li class="collection"><a href="{% url 'generic:examcollection_detail' collection.pk %}">{{ collection.name }} [{{ collection.date}}] (Authors: {{ collection.get_authors }})</a></li>
|
||||
{% empty %}
|
||||
<li>No exam collections yet.</li>
|
||||
{% endfor %}
|
||||
|
||||
+4
-4
@@ -12,24 +12,24 @@ import datetime
|
||||
class TestExamCollections:
|
||||
def test_list_url(self, db, client):
|
||||
# Test with no content
|
||||
response = client.get(reverse("generic:exam_collection_list"))
|
||||
response = client.get(reverse("generic:examcollection_list"))
|
||||
assert response.status_code == 200
|
||||
soup = BeautifulSoup(response.content, "html.parser")
|
||||
assert "No exam collections yet" in soup.text
|
||||
|
||||
# No content yet so this should fail
|
||||
response = client.get(reverse("generic:exam_collection_detail", args=( 1, )))
|
||||
response = client.get(reverse("generic:examcollection_detail", args=( 1, )))
|
||||
assert response.status_code == 404
|
||||
|
||||
collection = ExamCollection.objects.create(name="test collection", date=datetime.date.today())
|
||||
|
||||
response = client.get(reverse("generic:exam_collection_list"))
|
||||
response = client.get(reverse("generic:examcollection_list"))
|
||||
assert response.status_code == 200
|
||||
soup = BeautifulSoup(response.content, "html.parser")
|
||||
assert collection.name in soup.find("li", class_="collection").text
|
||||
|
||||
|
||||
response = client.get(reverse("generic:exam_collection_detail", args=( collection.pk, )))
|
||||
response = client.get(reverse("generic:examcollection_detail", args=( collection.pk, )))
|
||||
assert response.status_code == 200
|
||||
soup = BeautifulSoup(response.content, "html.parser")
|
||||
assert collection.name in soup.find("h1").text
|
||||
+24
-4
@@ -163,14 +163,34 @@ urlpatterns = [
|
||||
name="supervisor_delete",
|
||||
),
|
||||
path(
|
||||
"exam_collection/",
|
||||
"examcollection/",
|
||||
views.ExamCollectionList.as_view(),
|
||||
name="exam_collection_list",
|
||||
name="examcollection_list",
|
||||
),
|
||||
path(
|
||||
"exam_collection/<int:pk>/",
|
||||
"examcollection/<int:pk>/",
|
||||
views.ExamCollectionDetail.as_view(),
|
||||
name="exam_collection_detail",
|
||||
name="examcollection_detail",
|
||||
),
|
||||
path(
|
||||
"examcollection/<int:pk>/edit",
|
||||
views.ExamCollectionEdit.as_view(),
|
||||
name="examcollection_edit",
|
||||
),
|
||||
path(
|
||||
"examcollection/create",
|
||||
views.ExamCollectionCreate.as_view(),
|
||||
name="examcollection_create",
|
||||
),
|
||||
path(
|
||||
"examcollection/<int:pk>/clone",
|
||||
views.ExamCollectionClone.as_view(),
|
||||
name="examcollection_clone",
|
||||
),
|
||||
path(
|
||||
"examcollection/<int:pk>/delete",
|
||||
views.ExamCollectionDelete.as_view(),
|
||||
name="examcollection_delete",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
+71
-8
@@ -59,6 +59,8 @@ from .forms import (
|
||||
CidGroupExamForm,
|
||||
CidUserForm,
|
||||
CidUserExamForm,
|
||||
ExamCollectionCloneForm,
|
||||
ExamCollectionForm,
|
||||
ExaminationForm,
|
||||
CidUserGroupForm,
|
||||
ExaminationMergeForm,
|
||||
@@ -85,9 +87,9 @@ from .models import (
|
||||
)
|
||||
|
||||
from rapids.models import Rapid as RapidQuestion, RapidImage
|
||||
from rapids.models import Exam as RapidExam
|
||||
from rapids.models import Exam as RapidsExam
|
||||
from longs.models import Long as LongQuestion, LongSeries
|
||||
from longs.models import Exam as LongExam
|
||||
from longs.models import Exam as LongsExam
|
||||
from anatomy.models import AnatomyQuestion as AnatomyQuestion
|
||||
from anatomy.models import Exam as AnatomyExam
|
||||
from sbas.models import Question as SbasQuestion
|
||||
@@ -260,10 +262,10 @@ def generic_exam_json_edit(request):
|
||||
question_type = request.POST.get("type")
|
||||
|
||||
if question_type == "rapids":
|
||||
Exam = RapidExam
|
||||
Exam = RapidsExam
|
||||
Question = RapidQuestion
|
||||
elif question_type == "longs":
|
||||
Exam = LongExam
|
||||
Exam = LongsExam
|
||||
Question = LongQuestion
|
||||
elif question_type == "anatomy":
|
||||
Exam = AnatomyExam
|
||||
@@ -917,6 +919,7 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
cid_candidates = exam.valid_cid_users.count()
|
||||
users_candidates = exam.valid_user_users.count()
|
||||
|
||||
|
||||
return render(
|
||||
request,
|
||||
"{}/exam_overview.html".format(self.app_name),
|
||||
@@ -2466,9 +2469,9 @@ class CidUserExamView(CidManagerRequiredMixin, SingleTableMixin, FilterView):
|
||||
filters = {"archive": False, "exam_mode": True}
|
||||
|
||||
physics_exams = [(i.name, i.pk) for i in PhysicsExam.objects.filter(**filters)]
|
||||
rapid_exams = [(i.name, i.pk) for i in RapidExam.objects.filter(**filters)]
|
||||
rapid_exams = [(i.name, i.pk) for i in RapidsExam.objects.filter(**filters)]
|
||||
sba_exams = [(i.name, i.pk) for i in SbasExam.objects.filter(**filters)]
|
||||
longs_exams = [(i.name, i.pk) for i in LongExam.objects.filter(**filters)]
|
||||
longs_exams = [(i.name, i.pk) for i in LongsExam.objects.filter(**filters)]
|
||||
anatomy_exams = [(i.name, i.pk) for i in AnatomyExam.objects.filter(**filters)]
|
||||
casecollection_exams = [
|
||||
(i.name, i.pk)
|
||||
@@ -2504,9 +2507,9 @@ class CidUserView(CidManagerRequiredMixin, SingleTableMixin, FilterView):
|
||||
filters = {"archive": False, "exam_mode": True}
|
||||
|
||||
physics_exams = [(i.name, i.pk) for i in PhysicsExam.objects.filter(**filters)]
|
||||
rapid_exams = [(i.name, i.pk) for i in RapidExam.objects.filter(**filters)]
|
||||
rapid_exams = [(i.name, i.pk) for i in RapidsExam.objects.filter(**filters)]
|
||||
sba_exams = [(i.name, i.pk) for i in SbasExam.objects.filter(**filters)]
|
||||
longs_exams = [(i.name, i.pk) for i in LongExam.objects.filter(**filters)]
|
||||
longs_exams = [(i.name, i.pk) for i in LongsExam.objects.filter(**filters)]
|
||||
anatomy_exams = [(i.name, i.pk) for i in AnatomyExam.objects.filter(**filters)]
|
||||
casecollection_exams = [
|
||||
(i.name, i.pk)
|
||||
@@ -3262,6 +3265,66 @@ class ExamCollectionList(ListView):
|
||||
class ExamCollectionDetail(DetailView, AuthorRequiredMixin):
|
||||
model = ExamCollection
|
||||
|
||||
class ExamCollectionEdit(UpdateView, AuthorRequiredMixin):
|
||||
model = ExamCollection
|
||||
form_class = ExamCollectionForm
|
||||
|
||||
class ExamCollectionCreate(CreateView, AuthorRequiredMixin):
|
||||
model = ExamCollection
|
||||
form_class = ExamCollectionForm
|
||||
|
||||
class ExamCollectionClone(CreateView, AuthorRequiredMixin):
|
||||
model = ExamCollection
|
||||
template_name = "generic/examcollection_clone_form.html"
|
||||
form_class = ExamCollectionCloneForm
|
||||
|
||||
def get_initial(self):
|
||||
old_object = get_object_or_404(self.model, pk=self.kwargs["pk"])
|
||||
self.initial_object = old_object
|
||||
|
||||
|
||||
initial_data = model_to_dict(old_object, exclude=["id", "date"])
|
||||
|
||||
## We manually transfer the forign keys / m2m relationships
|
||||
#questions = old_object.exam_questions.all().values_list("id", flat=True)
|
||||
authors = old_object.author.all().values_list("id", flat=True)
|
||||
|
||||
## clear the associated groups
|
||||
#initial_data["valid_user_users"] = []
|
||||
#initial_data["valid_cid_users"] = []
|
||||
#initial_data["cid_user_groups"] = []
|
||||
#initial_data["user_user_groups"] = []
|
||||
|
||||
#initial_data["active"] = False
|
||||
#initial_data["publish_results"] = False
|
||||
|
||||
#self.exam_questions = list(questions)
|
||||
self.author = list(authors)
|
||||
|
||||
return initial_data
|
||||
|
||||
def form_valid(self, form):
|
||||
object = form.save()
|
||||
# Reapply these otherwise they get lost?
|
||||
#object.exam_questions.set(self.exam_questions)
|
||||
|
||||
GROUP_TYPES = (
|
||||
("Rapid Exams", "rapids_exams", RapidsExam),
|
||||
("Long Exams", "longs_exams", LongsExam),
|
||||
("SBA Exams", "sbas_exams", SbasExam),
|
||||
("Physic Exams", "physics_exams", PhysicsExam),
|
||||
("Anatomy Exams", "anatomy_exams", AnatomyExam),
|
||||
)
|
||||
|
||||
object.author.set(self.author)
|
||||
object.save()
|
||||
return HttpResponseRedirect(object.get_absolute_url())
|
||||
|
||||
class ExamCollectionDelete(DeleteView, AuthorRequiredMixin):
|
||||
model = ExamCollection
|
||||
template_name = "confirm_delete.html"
|
||||
success_url = reverse_lazy("generic:examcollection_list")
|
||||
|
||||
class ExaminationView(SuperuserRequiredMixin, SingleTableMixin, FilterView):
|
||||
model = Examination
|
||||
table_class = ExaminationTable
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.1.4 on 2024-02-05 10:23
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('longs', '0015_exam_exam_collection'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='exam',
|
||||
old_name='exam_collection',
|
||||
new_name='examcollection',
|
||||
),
|
||||
]
|
||||
+1
-1
@@ -456,7 +456,7 @@ class Exam(ExamBase):
|
||||
|
||||
exam_user_status = GenericRelation(ExamUserStatus)
|
||||
|
||||
exam_collection = models.ForeignKey(ExamCollection, blank=True, null=True, on_delete=models.SET_NULL, related_name="longs_exams")
|
||||
examcollection = models.ForeignKey(ExamCollection, blank=True, null=True, on_delete=models.SET_NULL, related_name="longs_exams")
|
||||
|
||||
def get_exam_question_json(self, question_id):
|
||||
q = get_object_or_404(Long, pk=question_id)
|
||||
|
||||
@@ -14,7 +14,7 @@ from rich.pretty import pprint
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from longs.views import GenericExamViews as LongExamViews
|
||||
from longs.views import GenericExamViews as LongsExamViews
|
||||
|
||||
from longs.models import (
|
||||
AnswerMarks,
|
||||
@@ -39,7 +39,7 @@ APP_NAME = "longs"
|
||||
|
||||
|
||||
def create_exam(db):
|
||||
exam: Exam = LongExamViews.Exam.objects.create(
|
||||
exam: Exam = LongsExamViews.Exam.objects.create(
|
||||
name="Cid Exam", exam_mode=True, active=True
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.1.4 on 2024-02-05 10:23
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('physics', '0005_alter_exam_exam_collection'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='exam',
|
||||
old_name='exam_collection',
|
||||
new_name='examcollection',
|
||||
),
|
||||
]
|
||||
+1
-1
@@ -182,7 +182,7 @@ class Exam(ExamBase):
|
||||
|
||||
exam_user_status = GenericRelation(ExamUserStatus)
|
||||
|
||||
exam_collection = models.ForeignKey(ExamCollection, blank=True, null=True, on_delete=models.SET_NULL, related_name="physics_exams")
|
||||
examcollection = models.ForeignKey(ExamCollection, blank=True, null=True, on_delete=models.SET_NULL, related_name="physics_exams")
|
||||
|
||||
def get_take_url(self):
|
||||
return reverse("physics:exam_start", kwargs={"pk": self.pk})
|
||||
|
||||
+2
-2
@@ -571,9 +571,9 @@ class UserListTableView(CidManagerRequiredMixin, SingleTableMixin, FilterView):
|
||||
# filters = {"archive": False, "exam_mode": True}
|
||||
|
||||
# physics_exams = [(i.name, i.pk) for i in PhysicsExam.objects.filter(**filters)]
|
||||
# rapid_exams = [(i.name, i.pk) for i in RapidExam.objects.filter(**filters)]
|
||||
# rapid_exams = [(i.name, i.pk) for i in RapidsExam.objects.filter(**filters)]
|
||||
# sba_exams = [(i.name, i.pk) for i in SbasExam.objects.filter(**filters)]
|
||||
# longs_exams = [(i.name, i.pk) for i in LongExam.objects.filter(**filters)]
|
||||
# longs_exams = [(i.name, i.pk) for i in LongsExam.objects.filter(**filters)]
|
||||
# anatomy_exams = [(i.name, i.pk) for i in AnatomyExam.objects.filter(**filters)]
|
||||
# casecollection_exams = [
|
||||
# (i.name, i.pk)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.1.4 on 2024-02-05 10:23
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('rapids', '0004_exam_exam_collection'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='exam',
|
||||
old_name='exam_collection',
|
||||
new_name='examcollection',
|
||||
),
|
||||
]
|
||||
+1
-1
@@ -644,7 +644,7 @@ class Exam(ExamBase):
|
||||
|
||||
exam_user_status = GenericRelation(ExamUserStatus)
|
||||
|
||||
exam_collection = models.ForeignKey(ExamCollection, blank=True, null=True, on_delete=models.SET_NULL, related_name="rapids_exams")
|
||||
examcollection = models.ForeignKey(ExamCollection, blank=True, null=True, on_delete=models.SET_NULL, related_name="rapids_exams")
|
||||
|
||||
def get_normal_abnormal_breakdown(self):
|
||||
# Inefficient but more extendible
|
||||
|
||||
@@ -13,7 +13,7 @@ from rich.pretty import pprint
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from rapids.views import GenericExamViews as RapidExamViews
|
||||
from rapids.views import GenericExamViews as RapidsExamViews
|
||||
|
||||
from rapids.models import Answer, UserAnswer, Exam, Examination, Rapid as Question, RapidImage
|
||||
|
||||
@@ -27,7 +27,7 @@ APP_NAME = "rapids"
|
||||
|
||||
|
||||
def create_exam(db):
|
||||
exam: Exam = RapidExamViews.Exam.objects.create(
|
||||
exam: Exam = RapidsExamViews.Exam.objects.create(
|
||||
name="Cid Exam", exam_mode=True, active=True
|
||||
)
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@ from bs4 import BeautifulSoup
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
from rapids.views import GenericExamViews as RapidExamViews
|
||||
from rapids.views import GenericExamViews as LongExamViews
|
||||
from rapids.views import GenericExamViews as RapidsExamViews
|
||||
from rapids.views import GenericExamViews as LongsExamViews
|
||||
from sbas.views import GenericExamViews as SbaExamViews
|
||||
from longs.views import GenericExamViews as LongExamViews
|
||||
from longs.views import GenericExamViews as LongsExamViews
|
||||
from physics.views import GenericExamViews as PhysicExamViews
|
||||
from anatomy.views import GenericExamViews as AnatomyExamViews
|
||||
|
||||
@@ -22,16 +22,16 @@ APP_NAMES = ("rapids", "anatomy", "longs", "physics", "sbas")
|
||||
JSON_APPS = ("rapids", "anatomy", "longs")
|
||||
|
||||
EXAM_VIEWS = (
|
||||
RapidExamViews,
|
||||
LongExamViews,
|
||||
RapidsExamViews,
|
||||
LongsExamViews,
|
||||
SbaExamViews,
|
||||
PhysicExamViews,
|
||||
AnatomyExamViews,
|
||||
)
|
||||
|
||||
JSON_VIEWS = (
|
||||
RapidExamViews,
|
||||
LongExamViews,
|
||||
RapidsExamViews,
|
||||
LongsExamViews,
|
||||
AnatomyExamViews,
|
||||
)
|
||||
|
||||
|
||||
+2
-1
@@ -42,4 +42,5 @@ django-htmx-autocomplete
|
||||
faker
|
||||
pytest-faker
|
||||
blake3
|
||||
django-markdownify
|
||||
django-markdownify
|
||||
django-clone
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.1.4 on 2024-02-05 10:23
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('sbas', '0005_exam_exam_collection'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='exam',
|
||||
old_name='exam_collection',
|
||||
new_name='examcollection',
|
||||
),
|
||||
]
|
||||
+1
-1
@@ -198,7 +198,7 @@ class Exam(ExamBase):
|
||||
|
||||
exam_user_status = GenericRelation(ExamUserStatus)
|
||||
|
||||
exam_collection = models.ForeignKey(ExamCollection, blank=True, null=True, on_delete=models.SET_NULL, related_name="sbas_exams")
|
||||
examcollection = models.ForeignKey(ExamCollection, blank=True, null=True, on_delete=models.SET_NULL, related_name="sbas_exams")
|
||||
|
||||
def get_take_url(self):
|
||||
return reverse("sbas:exam_start", kwargs={"pk": self.pk})
|
||||
|
||||
@@ -53,6 +53,7 @@ urlpatterns = [
|
||||
"exam/<int:pk>/authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"
|
||||
),
|
||||
path("exam/<int:exam_id>/clone", views.ExamClone.as_view(), name="exam_clone"),
|
||||
path("exam/<int:exam_id>/clone2", views.exam_clone2, name="exam_clone2"),
|
||||
path("exam/available", views.active_exams, name="active_exams"),
|
||||
path("exam/create", views.ExamUpdate.as_view(), name="exam_create"),
|
||||
path("exam/<int:pk>/update", views.ExamUpdate.as_view(), name="exam_update"),
|
||||
|
||||
@@ -436,3 +436,8 @@ class ExamAuthorUpdate(
|
||||
|
||||
class ExamClone(ExamCloneMixin, ExamCreate):
|
||||
"""Clone exam view"""
|
||||
|
||||
def exam_clone2(request, exam_id):
|
||||
exam = get_object_or_404(Exam, pk=exam_id)
|
||||
new_exam = exam.clone_model()
|
||||
return redirect("sbas:exam_update", pk=new_exam.id)
|
||||
|
||||
Reference in New Issue
Block a user