improve exam group management

This commit is contained in:
Ross
2024-08-12 10:33:15 +01:00
parent bd1bb54f9f
commit 6fe2bae8a8
22 changed files with 352 additions and 155 deletions
+59 -45
View File
@@ -437,13 +437,12 @@ class SeriesBase(models.Model):
if series_number is not None:
series_html = format_html(
"<span class='series-block-series-number'>Series {}</span>{}<br>",
series_number
series_number,
)
description = ""
if self.description:
description = format_html("{}<br/>", self.description)
return format_html(
"""{}{}
<span>
@@ -554,9 +553,16 @@ class ExamOrCollectionGenericBase(models.Model, AuthorMixin):
name = models.CharField(max_length=200, help_text="Name of the exam/collection")
start_date = models.DateTimeField(help_text="Date (and time) the exam/collection starts", null=True, blank=True)
end_date = models.DateTimeField(help_text="Date (and time) the exam/collection ends", null=True, blank=True)
restrict_to_dates = models.BooleanField(default=False, help_text="If the exam/collection should only be taken between the start and end date times")
start_date = models.DateTimeField(
help_text="Date (and time) the exam/collection starts", null=True, blank=True
)
end_date = models.DateTimeField(
help_text="Date (and time) the exam/collection ends", null=True, blank=True
)
restrict_to_dates = models.BooleanField(
default=False,
help_text="If the exam/collection should only be taken between the start and end date times",
)
active = models.BooleanField(
help_text="If an exam/collection should be available to take", default=False
@@ -595,15 +601,19 @@ class ExamOrCollectionGenericBase(models.Model, AuthorMixin):
def clean(self, *args, **kwargs):
if self.restrict_to_dates:
if self.start_date is None:
raise ValidationError("If restrict to dates is set, a start date must be set")
raise ValidationError(
"If restrict to dates is set, a start date must be set"
)
if self.end_date is not None and self.end_date <= self.start_date:
raise ValidationError({"end_date" : "End date must be after start date"})
raise ValidationError({"end_date": "End date must be after start date"})
return super().clean(*args, **kwargs)
def get_app_name(self):
raise NotImplementedError("You must implement get_app_name in the derived class")
raise NotImplementedError(
"You must implement get_app_name in the derived class"
)
def get_base_template(self):
return f"{self.get_app_name()}/exams.html"
@@ -629,9 +639,7 @@ class ExamOrCollectionGenericBase(models.Model, AuthorMixin):
):
return self.check_user_can_take(cid, passcode, request.user, active_only)
def check_user_can_take(
self, cid, passcode, user = None, active_only=True
):
def check_user_can_take(self, cid, passcode, user=None, active_only=True):
"""
Helper to check if a user is allowed to access an exam/collection
@@ -645,7 +653,9 @@ class ExamOrCollectionGenericBase(models.Model, AuthorMixin):
"""
# If we are limiting by dates check that the current date is within the range
if self.restrict_to_dates:
if self.start_date >= timezone.now() or (self.end_date is not None and self.end_date <= timezone.now()):
if self.start_date >= timezone.now() or (
self.end_date is not None and self.end_date <= timezone.now()
):
raise Http404("Exam not found")
# If it is not active no one can take
elif active_only and not self.active:
@@ -663,7 +673,7 @@ class ExamOrCollectionGenericBase(models.Model, AuthorMixin):
if not user.is_active:
raise Http404("Error accessing exam")
return
def check_cid_user(
@@ -671,7 +681,7 @@ class ExamOrCollectionGenericBase(models.Model, AuthorMixin):
cid: int | None = None,
passcode: str | None = None,
user: User | None = None,
#request: HttpRequest | None = None,
# request: HttpRequest | None = None,
user_id: int | None = None,
allow_authors: bool = True,
):
@@ -795,9 +805,7 @@ class ExamOrCollectionGenericBase(models.Model, AuthorMixin):
"cid_user_groups",
"user_user_groups",
)
FK_fields = (
"examcollection",
)
FK_fields = ("examcollection",)
model_dict = model_to_dict(self)
@@ -824,8 +832,6 @@ class ExamOrCollectionGenericBase(models.Model, AuthorMixin):
for item in m2m_to_update[field]:
rel.add(item)
return cloned_model
@@ -835,9 +841,9 @@ class ExamBase(ExamOrCollectionGenericBase):
default=False,
)
#randomise_question_order = models.BooleanField(
# randomise_question_order = models.BooleanField(
# help_text="If an exam should randomise the order of questions in RTS.",
#)
# )
include_history = models.BooleanField(
help_text="If an exam should include history when taking",
@@ -883,20 +889,20 @@ class ExamBase(ExamOrCollectionGenericBase):
self.recreate_json = recreate_json
super().save(*args, **kwargs)
#def clean(self, *args, **kwargs):
# def clean(self, *args, **kwargs):
# if self.user_scores is None:
# self.user_scores = {}
# return super().clean(*args, **kwargs)
def get_questions(self):
"""Returns a list of questions in the exam in the correct order
This should be used in preference to exam_questions.all() as it
This should be used in preference to exam_questions.all() as it
will order the questions correctly"""
return self.exam_questions.all().order_by("examquestiondetail__sort_order")
def order_questions(self):
def order_questions(self):
"""Modifies the examquestiondetail sort_order to sequentially order the cases"""
for n, c in enumerate(self.examquestiondetail_set.all().order_by("sort_order")):
c.sort_order = n
@@ -1067,12 +1073,12 @@ class ExamBase(ExamOrCollectionGenericBase):
class ExamUserStatus(models.Model):
datetime = models.DateTimeField(auto_now_add=True)
#cid_user: "CidUser" = models.ForeignKey(
# cid_user: "CidUser" = models.ForeignKey(
# "CidUser", blank=True, null=True, on_delete=models.SET_NULL
#)
#user_user = models.ForeignKey(
# )
# user_user = models.ForeignKey(
# settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL
#)
# )
status = models.CharField(max_length=255)
extra = models.CharField(max_length=255)
@@ -1080,7 +1086,9 @@ class ExamUserStatus(models.Model):
object_id = models.PositiveIntegerField()
exam = GenericForeignKey("content_type", "object_id")
cid_user_exam = models.ForeignKey("CidUserExam", blank=True, null=True, on_delete=models.SET_NULL)
cid_user_exam = models.ForeignKey(
"CidUserExam", blank=True, null=True, on_delete=models.SET_NULL
)
def __str__(self):
self.cid_user_exam: "CidUserExam"
@@ -1393,10 +1401,11 @@ def get_next_cid():
class CidUserExam(models.Model):
"""This holds the current status of the exam for a candidate
Contrast with ExamUserStatus which holds the status changes for a candidate
Contrast with ExamUserStatus which holds the status changes for a candidate
(and should be merged into here)
"""
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
exam = GenericForeignKey("content_type", "object_id")
@@ -1460,9 +1469,22 @@ CID_GROUP_EXAMS = (
)
class CidUserGroup(models.Model):
name = models.CharField(blank=True, max_length=50)
archive = models.BooleanField(default=False)
class BaseUserGroup(models.Model):
name = models.CharField(blank=True, max_length=50, help_text="Name of the Group")
archive = models.BooleanField(
default=False,
help_text="Archived groups remain on the test system but are not displayed by default",
)
open_access = models.BooleanField(
default=False, help_text="If the group is freely accessible to use"
)
class Meta:
abstract = True
class CidUserGroup(BaseUserGroup):
def __str__(self) -> str:
return str(self.name)
@@ -1495,15 +1517,7 @@ USER_GROUP_EXAMS = (
)
class UserUserGroup(models.Model):
name = models.CharField(
blank=True, max_length=50, help_text="Name of the User Group"
)
archive = models.BooleanField(
default=False,
help_text="Archived groups remain on the test system but are not displayed by default",
)
class UserUserGroup(BaseUserGroup):
users = models.ManyToManyField(
settings.AUTH_USER_MODEL,
blank=True,