continue building examcollections

This commit is contained in:
Ross
2024-02-05 18:30:35 +00:00
parent 269a86c25b
commit b1da429036
30 changed files with 546 additions and 70 deletions
@@ -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
View File
@@ -404,7 +404,7 @@ class Exam(ExamBase):
exam_user_status = GenericRelation(ExamUserStatus) 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): def get_exam_json(self, based=True):
questions = self.exam_questions.all() questions = self.exam_questions.all()
+74
View File
@@ -17,6 +17,7 @@ from generic.models import (
CidUser, CidUser,
CidUserGroup, CidUserGroup,
Examination, Examination,
ExamCollection,
QuestionNote, QuestionNote,
Supervisor, Supervisor,
UserGrades, UserGrades,
@@ -333,6 +334,7 @@ class UserGroupExamForm(ModelForm):
"physics_user_user_groups", "physics_user_user_groups",
"sba_user_user_groups", "sba_user_user_groups",
] ]
# users = UserUserGroupModelChoiceField( # users = UserUserGroupModelChoiceField(
# required=False, # required=False,
# queryset=User.objects.all(), # queryset=User.objects.all(),
@@ -549,3 +551,75 @@ class SupervisorForm(ModelForm):
class Meta: class Meta:
model = Supervisor model = Supervisor
exclude = ("",) 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),
),
]
+74 -12
View File
@@ -39,6 +39,7 @@ from easy_thumbnails.exceptions import InvalidImageFormatError
import pydicom import pydicom
import dicognito.anonymizer import dicognito.anonymizer
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.forms.models import model_to_dict
def findMiddle(input_list): def findMiddle(input_list):
@@ -49,8 +50,6 @@ def findMiddle(input_list):
return input_list[int(middle)] return input_list[int(middle)]
class Modality(models.Model): class Modality(models.Model):
modality = models.CharField(max_length=200, unique=True) modality = models.CharField(max_length=200, unique=True)
short_code = models.CharField(max_length=5, unique=True, null=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)", help_text="Set to true if the file this object refers to has been removed (either from series truncation or merging)",
) )
class Meta: class Meta:
ordering = ["position"] ordering = ["position"]
abstract = True abstract = True
@@ -244,7 +242,9 @@ class SeriesImageBase(models.Model):
def generate_md5_hash(self): def generate_md5_hash(self):
if not self.removed: 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.is_dicom = is_dicom
self.image_md5_hash = image_hash self.image_md5_hash = image_hash
@@ -253,7 +253,9 @@ class SeriesImageBase(models.Model):
def generate_blake3_hash(self): def generate_blake3_hash(self):
if not self.removed: 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.is_dicom = is_dicom
self.image_blake3_hash = image_blake3_hash self.image_blake3_hash = image_blake3_hash
@@ -269,7 +271,9 @@ class SeriesImageBase(models.Model):
# self.image_md5_hash = image_hash # self.image_md5_hash = image_hash
if not self.image_blake3_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.is_dicom = is_dicom
self.image_blake3_hash = image_blake3_hash self.image_blake3_hash = image_blake3_hash
@@ -301,12 +305,24 @@ class SeriesBase(models.Model):
modified_date = models.DateTimeField(auto_now=True) modified_date = models.DateTimeField(auto_now=True)
class SeriesModifiedChocies(models.TextChoices): class SeriesModifiedChocies(models.TextChoices):
NO = "NO", "Not modified", NO = (
TR = "TR", "Truncated", "NO",
RE = "RE", "Resliced/resampled", "Not modified",
)
TR = (
"TR",
"Truncated",
)
RE = (
"RE",
"Resliced/resampled",
)
modified = models.CharField(
modified = models.CharField(max_length=2, default=SeriesModifiedChocies.NO, choices=SeriesModifiedChocies.choices) max_length=2,
default=SeriesModifiedChocies.NO,
choices=SeriesModifiedChocies.choices,
)
class Meta: class Meta:
abstract = True abstract = True
@@ -720,6 +736,48 @@ class ExamOrCollectionGenericBase(models.Model, AuthorMixin):
content_type=content_type, object_id=self.pk, user_user=user_user 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): class ExamBase(ExamOrCollectionGenericBase):
exam_mode = models.BooleanField( exam_mode = models.BooleanField(
@@ -914,7 +972,6 @@ class ExamBase(ExamOrCollectionGenericBase):
else: else:
extra = "No supervisor" extra = "No supervisor"
if additional_emails is not None: if additional_emails is not None:
emails.extend(additional_emails) emails.extend(additional_emails)
@@ -1505,11 +1562,14 @@ def create_user_profile(sender, instance, created, **kwargs):
def save_user_profile(sender, instance, **kwargs): def save_user_profile(sender, instance, **kwargs):
instance.userprofile.save() instance.userprofile.save()
class ExamCollection(models.Model, AuthorMixin): class ExamCollection(models.Model, AuthorMixin):
name = models.CharField(max_length=255) 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( author = models.ManyToManyField(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
blank=True, blank=True,
@@ -1519,3 +1579,5 @@ class ExamCollection(models.Model, AuthorMixin):
def __str__(self): def __str__(self):
return f"{self.name} [{self.date}]" return f"{self.name} [{self.date}]"
def get_absolute_url(self):
return reverse("generic:examcollection_detail", kwargs={"pk": self.pk})
+1 -1
View File
@@ -23,7 +23,7 @@
<a href="{% url 'generic:user_group_view' %}">User Groups</a> / <a href="{% url 'generic:user_group_view' %}">User Groups</a> /
<a href="{% url 'accounts_list' %}">Manage Users</a> / <a href="{% url 'accounts_list' %}">Manage Users</a> /
<a href="{% url 'generic:supervisor' %}">Manage Supervisors</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 %} {% endif %}
{% endblock %} {% endblock %}
@@ -55,3 +55,11 @@ Open access: {{ exam.open_access }}<br />
<p><a href="{% url app_name|add:':mark_overview' pk=exam.pk %}"><button>Mark exam</button></a></p> <p><a href="{% url app_name|add:':mark_overview' pk=exam.pk %}"><button>Mark exam</button></a></p>
{% endif %} {% endif %}
{% 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 navigation %}
{{block.super}} {{block.super}}
<br/>Test <br/>
{% comment %} <a href="{% url 'generic:cid_group_detail' cidusergroup.pk %}">Collections</a> / {% endcomment %} {% comment %} <a href="{% url 'generic:cid_group_detail' cidusergroup.pk %}">Collections</a> / {% endcomment %}
<a href="{% url 'generic:examcollection_create' %}">Create Exam Collection</a>
{% endblock %} {% endblock %}
+65
View File
@@ -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> <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
View File
@@ -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> <h1>Exam Collections</h1>
<ul class="collections"> <ul class="collections">
{% for collection in object_list %} {% 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 %} {% empty %}
<li>No exam collections yet.</li> <li>No exam collections yet.</li>
{% endfor %} {% endfor %}
+4 -4
View File
@@ -12,24 +12,24 @@ import datetime
class TestExamCollections: class TestExamCollections:
def test_list_url(self, db, client): def test_list_url(self, db, client):
# Test with no content # Test with no content
response = client.get(reverse("generic:exam_collection_list")) response = client.get(reverse("generic:examcollection_list"))
assert response.status_code == 200 assert response.status_code == 200
soup = BeautifulSoup(response.content, "html.parser") soup = BeautifulSoup(response.content, "html.parser")
assert "No exam collections yet" in soup.text assert "No exam collections yet" in soup.text
# No content yet so this should fail # 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 assert response.status_code == 404
collection = ExamCollection.objects.create(name="test collection", date=datetime.date.today()) 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 assert response.status_code == 200
soup = BeautifulSoup(response.content, "html.parser") soup = BeautifulSoup(response.content, "html.parser")
assert collection.name in soup.find("li", class_="collection").text 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 assert response.status_code == 200
soup = BeautifulSoup(response.content, "html.parser") soup = BeautifulSoup(response.content, "html.parser")
assert collection.name in soup.find("h1").text assert collection.name in soup.find("h1").text
+24 -4
View File
@@ -163,14 +163,34 @@ urlpatterns = [
name="supervisor_delete", name="supervisor_delete",
), ),
path( path(
"exam_collection/", "examcollection/",
views.ExamCollectionList.as_view(), views.ExamCollectionList.as_view(),
name="exam_collection_list", name="examcollection_list",
), ),
path( path(
"exam_collection/<int:pk>/", "examcollection/<int:pk>/",
views.ExamCollectionDetail.as_view(), 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
View File
@@ -59,6 +59,8 @@ from .forms import (
CidGroupExamForm, CidGroupExamForm,
CidUserForm, CidUserForm,
CidUserExamForm, CidUserExamForm,
ExamCollectionCloneForm,
ExamCollectionForm,
ExaminationForm, ExaminationForm,
CidUserGroupForm, CidUserGroupForm,
ExaminationMergeForm, ExaminationMergeForm,
@@ -85,9 +87,9 @@ from .models import (
) )
from rapids.models import Rapid as RapidQuestion, RapidImage 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 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 AnatomyQuestion as AnatomyQuestion
from anatomy.models import Exam as AnatomyExam from anatomy.models import Exam as AnatomyExam
from sbas.models import Question as SbasQuestion from sbas.models import Question as SbasQuestion
@@ -260,10 +262,10 @@ def generic_exam_json_edit(request):
question_type = request.POST.get("type") question_type = request.POST.get("type")
if question_type == "rapids": if question_type == "rapids":
Exam = RapidExam Exam = RapidsExam
Question = RapidQuestion Question = RapidQuestion
elif question_type == "longs": elif question_type == "longs":
Exam = LongExam Exam = LongsExam
Question = LongQuestion Question = LongQuestion
elif question_type == "anatomy": elif question_type == "anatomy":
Exam = AnatomyExam Exam = AnatomyExam
@@ -917,6 +919,7 @@ class ExamViews(View, LoginRequiredMixin):
cid_candidates = exam.valid_cid_users.count() cid_candidates = exam.valid_cid_users.count()
users_candidates = exam.valid_user_users.count() users_candidates = exam.valid_user_users.count()
return render( return render(
request, request,
"{}/exam_overview.html".format(self.app_name), "{}/exam_overview.html".format(self.app_name),
@@ -2466,9 +2469,9 @@ class CidUserExamView(CidManagerRequiredMixin, SingleTableMixin, FilterView):
filters = {"archive": False, "exam_mode": True} filters = {"archive": False, "exam_mode": True}
physics_exams = [(i.name, i.pk) for i in PhysicsExam.objects.filter(**filters)] 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)] 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)] anatomy_exams = [(i.name, i.pk) for i in AnatomyExam.objects.filter(**filters)]
casecollection_exams = [ casecollection_exams = [
(i.name, i.pk) (i.name, i.pk)
@@ -2504,9 +2507,9 @@ class CidUserView(CidManagerRequiredMixin, SingleTableMixin, FilterView):
filters = {"archive": False, "exam_mode": True} filters = {"archive": False, "exam_mode": True}
physics_exams = [(i.name, i.pk) for i in PhysicsExam.objects.filter(**filters)] 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)] 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)] anatomy_exams = [(i.name, i.pk) for i in AnatomyExam.objects.filter(**filters)]
casecollection_exams = [ casecollection_exams = [
(i.name, i.pk) (i.name, i.pk)
@@ -3262,6 +3265,66 @@ class ExamCollectionList(ListView):
class ExamCollectionDetail(DetailView, AuthorRequiredMixin): class ExamCollectionDetail(DetailView, AuthorRequiredMixin):
model = ExamCollection 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): class ExaminationView(SuperuserRequiredMixin, SingleTableMixin, FilterView):
model = Examination model = Examination
table_class = ExaminationTable 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
View File
@@ -456,7 +456,7 @@ class Exam(ExamBase):
exam_user_status = GenericRelation(ExamUserStatus) 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): def get_exam_question_json(self, question_id):
q = get_object_or_404(Long, pk=question_id) q = get_object_or_404(Long, pk=question_id)
+2 -2
View File
@@ -14,7 +14,7 @@ from rich.pretty import pprint
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from longs.views import GenericExamViews as LongExamViews from longs.views import GenericExamViews as LongsExamViews
from longs.models import ( from longs.models import (
AnswerMarks, AnswerMarks,
@@ -39,7 +39,7 @@ APP_NAME = "longs"
def create_exam(db): def create_exam(db):
exam: Exam = LongExamViews.Exam.objects.create( exam: Exam = LongsExamViews.Exam.objects.create(
name="Cid Exam", exam_mode=True, active=True 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
View File
@@ -182,7 +182,7 @@ class Exam(ExamBase):
exam_user_status = GenericRelation(ExamUserStatus) 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): def get_take_url(self):
return reverse("physics:exam_start", kwargs={"pk": self.pk}) return reverse("physics:exam_start", kwargs={"pk": self.pk})
+2 -2
View File
@@ -571,9 +571,9 @@ class UserListTableView(CidManagerRequiredMixin, SingleTableMixin, FilterView):
# filters = {"archive": False, "exam_mode": True} # filters = {"archive": False, "exam_mode": True}
# physics_exams = [(i.name, i.pk) for i in PhysicsExam.objects.filter(**filters)] # 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)] # 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)] # anatomy_exams = [(i.name, i.pk) for i in AnatomyExam.objects.filter(**filters)]
# casecollection_exams = [ # casecollection_exams = [
# (i.name, i.pk) # (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
View File
@@ -644,7 +644,7 @@ class Exam(ExamBase):
exam_user_status = GenericRelation(ExamUserStatus) 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): def get_normal_abnormal_breakdown(self):
# Inefficient but more extendible # Inefficient but more extendible
+2 -2
View File
@@ -13,7 +13,7 @@ from rich.pretty import pprint
from bs4 import BeautifulSoup 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 from rapids.models import Answer, UserAnswer, Exam, Examination, Rapid as Question, RapidImage
@@ -27,7 +27,7 @@ APP_NAME = "rapids"
def create_exam(db): def create_exam(db):
exam: Exam = RapidExamViews.Exam.objects.create( exam: Exam = RapidsExamViews.Exam.objects.create(
name="Cid Exam", exam_mode=True, active=True name="Cid Exam", exam_mode=True, active=True
) )
+7 -7
View File
@@ -8,10 +8,10 @@ from bs4 import BeautifulSoup
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from rapids.views import GenericExamViews as RapidExamViews from rapids.views import GenericExamViews as RapidsExamViews
from rapids.views import GenericExamViews as LongExamViews from rapids.views import GenericExamViews as LongsExamViews
from sbas.views import GenericExamViews as SbaExamViews 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 physics.views import GenericExamViews as PhysicExamViews
from anatomy.views import GenericExamViews as AnatomyExamViews from anatomy.views import GenericExamViews as AnatomyExamViews
@@ -22,16 +22,16 @@ APP_NAMES = ("rapids", "anatomy", "longs", "physics", "sbas")
JSON_APPS = ("rapids", "anatomy", "longs") JSON_APPS = ("rapids", "anatomy", "longs")
EXAM_VIEWS = ( EXAM_VIEWS = (
RapidExamViews, RapidsExamViews,
LongExamViews, LongsExamViews,
SbaExamViews, SbaExamViews,
PhysicExamViews, PhysicExamViews,
AnatomyExamViews, AnatomyExamViews,
) )
JSON_VIEWS = ( JSON_VIEWS = (
RapidExamViews, RapidsExamViews,
LongExamViews, LongsExamViews,
AnatomyExamViews, AnatomyExamViews,
) )
+1
View File
@@ -43,3 +43,4 @@ faker
pytest-faker pytest-faker
blake3 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
View File
@@ -198,7 +198,7 @@ class Exam(ExamBase):
exam_user_status = GenericRelation(ExamUserStatus) 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): def get_take_url(self):
return reverse("sbas:exam_start", kwargs={"pk": self.pk}) return reverse("sbas:exam_start", kwargs={"pk": self.pk})
+1
View File
@@ -53,6 +53,7 @@ urlpatterns = [
"exam/<int:pk>/authors", views.ExamAuthorUpdate.as_view(), name="exam_authors" "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>/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/available", views.active_exams, name="active_exams"),
path("exam/create", views.ExamUpdate.as_view(), name="exam_create"), path("exam/create", views.ExamUpdate.as_view(), name="exam_create"),
path("exam/<int:pk>/update", views.ExamUpdate.as_view(), name="exam_update"), path("exam/<int:pk>/update", views.ExamUpdate.as_view(), name="exam_update"),
+5
View File
@@ -436,3 +436,8 @@ class ExamAuthorUpdate(
class ExamClone(ExamCloneMixin, ExamCreate): class ExamClone(ExamCloneMixin, ExamCreate):
"""Clone exam view""" """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)