improve exam group management
This commit is contained in:
+59
-45
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user