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_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()
|
||||||
|
|||||||
@@ -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
@@ -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})
|
||||||
|
|||||||
@@ -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 %}
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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})
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user