From b1da42903673dde7211731832b3c5ed0285188af Mon Sep 17 00:00:00 2001
From: Ross
Date: Mon, 5 Feb 2024 18:30:35 +0000
Subject: [PATCH] continue building examcollections
---
...ame_exam_collection_exam_examcollection.py | 18 ++++
anatomy/models.py | 2 +-
generic/forms.py | 94 ++++++++++++++--
.../migrations/0013_examcollection_archive.py | 18 ++++
generic/models.py | 102 ++++++++++++++----
generic/templates/generic/base.html | 2 +-
.../generic/exam_overview_headers.html | 10 +-
.../generic/examcollection_base.html | 3 +-
.../generic/examcollection_clone_form.html | 65 +++++++++++
.../generic/examcollection_detail.html | 44 +++++++-
.../generic/examcollection_form.html | 26 +++++
.../generic/examcollection_list.html | 2 +-
generic/tests.py | 8 +-
generic/urls.py | 28 ++++-
generic/views.py | 79 ++++++++++++--
...ame_exam_collection_exam_examcollection.py | 18 ++++
longs/models.py | 2 +-
longs/tests/test_longs_exams.py | 4 +-
...ame_exam_collection_exam_examcollection.py | 18 ++++
physics/models.py | 2 +-
rad/views.py | 4 +-
...ame_exam_collection_exam_examcollection.py | 18 ++++
rapids/models.py | 2 +-
rapids/tests/test_rapids_exams.py | 4 +-
rapids/tests/test_urls.py | 14 +--
requirements.txt | 3 +-
...ame_exam_collection_exam_examcollection.py | 18 ++++
sbas/models.py | 2 +-
sbas/urls.py | 1 +
sbas/views.py | 5 +
30 files changed, 546 insertions(+), 70 deletions(-)
create mode 100644 anatomy/migrations/0013_rename_exam_collection_exam_examcollection.py
create mode 100644 generic/migrations/0013_examcollection_archive.py
create mode 100755 generic/templates/generic/examcollection_clone_form.html
create mode 100755 generic/templates/generic/examcollection_form.html
create mode 100644 longs/migrations/0016_rename_exam_collection_exam_examcollection.py
create mode 100644 physics/migrations/0006_rename_exam_collection_exam_examcollection.py
create mode 100644 rapids/migrations/0005_rename_exam_collection_exam_examcollection.py
create mode 100644 sbas/migrations/0006_rename_exam_collection_exam_examcollection.py
diff --git a/anatomy/migrations/0013_rename_exam_collection_exam_examcollection.py b/anatomy/migrations/0013_rename_exam_collection_exam_examcollection.py
new file mode 100644
index 00000000..89110e86
--- /dev/null
+++ b/anatomy/migrations/0013_rename_exam_collection_exam_examcollection.py
@@ -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',
+ ),
+ ]
diff --git a/anatomy/models.py b/anatomy/models.py
index 6ae5699f..4f1b6f5a 100644
--- a/anatomy/models.py
+++ b/anatomy/models.py
@@ -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()
diff --git a/generic/forms.py b/generic/forms.py
index 2457c733..69cdb7d7 100755
--- a/generic/forms.py
+++ b/generic/forms.py
@@ -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")
diff --git a/generic/migrations/0013_examcollection_archive.py b/generic/migrations/0013_examcollection_archive.py
new file mode 100644
index 00000000..c83acb1b
--- /dev/null
+++ b/generic/migrations/0013_examcollection_archive.py
@@ -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),
+ ),
+ ]
diff --git a/generic/models.py b/generic/models.py
index 45ac6d75..b2a26a16 100644
--- a/generic/models.py
+++ b/generic/models.py
@@ -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} {rapids_extra}"
+ html_msg = f"{html_msg} {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}]"
-
\ No newline at end of file
+
+ def get_absolute_url(self):
+ return reverse("generic:examcollection_detail", kwargs={"pk": self.pk})
diff --git a/generic/templates/generic/base.html b/generic/templates/generic/base.html
index 559b5139..bde61e8c 100755
--- a/generic/templates/generic/base.html
+++ b/generic/templates/generic/base.html
@@ -23,7 +23,7 @@
User Groups /
Manage Users /
Manage Supervisors /
- Collections
+ Collections
{% endif %}
{% endblock %}
diff --git a/generic/templates/generic/exam_overview_headers.html b/generic/templates/generic/exam_overview_headers.html
index eeac7ca4..dbe291cf 100644
--- a/generic/templates/generic/exam_overview_headers.html
+++ b/generic/templates/generic/exam_overview_headers.html
@@ -49,9 +49,17 @@ Open access: {{ exam.open_access }}
{% if exam.exam_mode %}
Publish results: [When checked the exam results will be available to users on this site]
+ {% if exam.publish_results %}checked{% endif %}> [When checked the exam results will be available to users on this site]
{% if app_name == "anatomy" or app_name == "rapids" or app_name == "longs" %}