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_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
View File
@@ -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
View File
@@ -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})
+1 -1
View File
@@ -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 %}
+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>
{{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>
<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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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
)
+7 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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})
+1
View File
@@ -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"),
+5
View File
@@ -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)