improve exam group management
This commit is contained in:
+7
-1
@@ -10,7 +10,7 @@ from django.forms import inlineformset_factory
|
|||||||
from django.contrib.postgres.forms import SimpleArrayField, SplitArrayField , SplitArrayWidget
|
from django.contrib.postgres.forms import SimpleArrayField, SplitArrayField , SplitArrayWidget
|
||||||
|
|
||||||
|
|
||||||
from generic.forms import ExamAuthorFormMixin, ExamFormMixin, ExamMarkerFormMixin
|
from generic.forms import ExamAuthorFormMixin, ExamFormMixin, ExamGroupsFormMixin, ExamMarkerFormMixin
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Answer,
|
Answer,
|
||||||
@@ -236,3 +236,9 @@ class ExamAuthorForm(ExamAuthorFormMixin):
|
|||||||
class ExamMarkerForm(ExamMarkerFormMixin):
|
class ExamMarkerForm(ExamMarkerFormMixin):
|
||||||
class Meta(ExamMarkerFormMixin.Meta):
|
class Meta(ExamMarkerFormMixin.Meta):
|
||||||
model = Exam
|
model = Exam
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ExamGroupsForm(ExamGroupsFormMixin):
|
||||||
|
class Meta(ExamGroupsFormMixin.Meta):
|
||||||
|
model = Exam
|
||||||
+6
-1
@@ -33,6 +33,7 @@ from .forms import (
|
|||||||
AnswerUpdateFormSet,
|
AnswerUpdateFormSet,
|
||||||
BodyPartForm,
|
BodyPartForm,
|
||||||
ExamAuthorForm,
|
ExamAuthorForm,
|
||||||
|
ExamGroupsForm,
|
||||||
ExamMarkerForm,
|
ExamMarkerForm,
|
||||||
ExaminationForm,
|
ExaminationForm,
|
||||||
MarkAnatomyQuestionForm,
|
MarkAnatomyQuestionForm,
|
||||||
@@ -51,7 +52,7 @@ from .models import (
|
|||||||
# IncorrectAnswers,
|
# IncorrectAnswers,
|
||||||
)
|
)
|
||||||
from generic.models import CidUser, Examination
|
from generic.models import CidUser, Examination
|
||||||
from generic.views import AuthorRequiredMixin, ExamCloneMixin, ExamCreateBase, ExamDeleteBase, ExamUpdateBase, ExamViews, GenericViewBase
|
from generic.views import AuthorRequiredMixin, ExamCloneMixin, ExamCreateBase, ExamDeleteBase, ExamGroupsUpdateBase, ExamUpdateBase, ExamViews, GenericViewBase
|
||||||
from reversion.views import RevisionMixin
|
from reversion.views import RevisionMixin
|
||||||
|
|
||||||
from .decorators import user_is_author_or_anatomy_checker
|
from .decorators import user_is_author_or_anatomy_checker
|
||||||
@@ -849,6 +850,10 @@ class ExamMarkersUpdate(
|
|||||||
template_name = "markers_form.html"
|
template_name = "markers_form.html"
|
||||||
|
|
||||||
|
|
||||||
|
class ExamGroupsUpdate(ExamGroupsUpdateBase):
|
||||||
|
model = Exam
|
||||||
|
form_class = ExamGroupsForm
|
||||||
|
|
||||||
|
|
||||||
GenericViews = GenericViewBase("anatomy", AnatomyQuestion, UserAnswer, Exam)
|
GenericViews = GenericViewBase("anatomy", AnatomyQuestion, UserAnswer, Exam)
|
||||||
|
|
||||||
|
|||||||
+43
-16
@@ -107,21 +107,21 @@ class ExamFormMixin:
|
|||||||
def __init__(self, *args, user, **kwargs) -> None:
|
def __init__(self, *args, user, **kwargs) -> None:
|
||||||
super(ModelForm, self).__init__(*args, **kwargs)
|
super(ModelForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
if user.is_superuser or user.groups.filter(name="cid_user_manager").exists():
|
#if user.is_superuser or user.groups.filter(name="cid_user_manager").exists():
|
||||||
cid_user_group_queryset = CidUserGroup.objects.filter(archive=False)
|
# cid_user_group_queryset = CidUserGroup.objects.filter(archive=False)
|
||||||
user_user_group_queryset = UserUserGroup.objects.filter(archive=False)
|
# user_user_group_queryset = UserUserGroup.objects.filter(archive=False)
|
||||||
else:
|
#else:
|
||||||
cid_user_group_queryset = CidUserGroup.objects.none()
|
# cid_user_group_queryset = CidUserGroup.objects.none()
|
||||||
user_user_group_queryset = UserUserGroup.objects.none()
|
# user_user_group_queryset = UserUserGroup.objects.none()
|
||||||
|
|
||||||
self.fields["cid_user_groups"] = ModelMultipleChoiceField(
|
#self.fields["cid_user_groups"] = ModelMultipleChoiceField(
|
||||||
required=False,
|
# required=False,
|
||||||
queryset=cid_user_group_queryset,
|
# queryset=cid_user_group_queryset,
|
||||||
)
|
#)
|
||||||
self.fields["user_user_groups"] = ModelMultipleChoiceField(
|
#self.fields["user_user_groups"] = ModelMultipleChoiceField(
|
||||||
required=False,
|
# required=False,
|
||||||
queryset=user_user_group_queryset,
|
# queryset=user_user_group_queryset,
|
||||||
)
|
#)
|
||||||
self.fields["start_date"] = SplitDateTimeFieldDefaultTime(
|
self.fields["start_date"] = SplitDateTimeFieldDefaultTime(
|
||||||
widget=SplitDateTimeWidget(
|
widget=SplitDateTimeWidget(
|
||||||
date_attrs={"type": "date", "class": "datepicker"},
|
date_attrs={"type": "date", "class": "datepicker"},
|
||||||
@@ -155,8 +155,8 @@ class ExamFormMixin:
|
|||||||
"active",
|
"active",
|
||||||
"publish_results",
|
"publish_results",
|
||||||
"archive",
|
"archive",
|
||||||
"cid_user_groups",
|
#"cid_user_groups",
|
||||||
"user_user_groups",
|
#"user_user_groups",
|
||||||
# "author",
|
# "author",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -198,6 +198,32 @@ class ExamMarkerFormMixin(ModelForm):
|
|||||||
self.fields["markers"].required = False
|
self.fields["markers"].required = False
|
||||||
|
|
||||||
|
|
||||||
|
class ExamGroupsFormMixin(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
#model = ExamCollection
|
||||||
|
fields = ("cid_user_groups", "user_user_groups")
|
||||||
|
|
||||||
|
def __init__(self, *args, user, **kwargs) -> None:
|
||||||
|
super(ModelForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
if user.is_superuser or user.groups.filter(name="cid_user_manager").exists():
|
||||||
|
cid_user_group_queryset = CidUserGroup.objects.filter(archive=False)
|
||||||
|
user_user_group_queryset = UserUserGroup.objects.filter(archive=False)
|
||||||
|
else:
|
||||||
|
cid_user_group_queryset = CidUserGroup.objects.none()
|
||||||
|
user_user_group_queryset = UserUserGroup.objects.none()
|
||||||
|
#cid_user_group_queryset = CidUserGroup.objects.none()
|
||||||
|
#user_user_group_queryset = UserUserGroup.objects.none()
|
||||||
|
|
||||||
|
self.fields["cid_user_groups"] = ModelMultipleChoiceField(
|
||||||
|
required=False,
|
||||||
|
queryset=cid_user_group_queryset,
|
||||||
|
)
|
||||||
|
self.fields["user_user_groups"] = ModelMultipleChoiceField(
|
||||||
|
required=False,
|
||||||
|
queryset=user_user_group_queryset,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ExaminationForm(ModelForm):
|
class ExaminationForm(ModelForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -704,3 +730,4 @@ class ExamCollectionCloneForm(ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = ExamCollection
|
model = ExamCollection
|
||||||
fields = ("name", "date")
|
fields = ("name", "date")
|
||||||
|
|
||||||
|
|||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
# Generated by Django 5.0.2 on 2024-08-12 09:32
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('generic', '0016_remove_examuserstatus_cid_user_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cidusergroup',
|
||||||
|
name='open_access',
|
||||||
|
field=models.BooleanField(default=False, help_text='If the group is freely accessible to use'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='userusergroup',
|
||||||
|
name='open_access',
|
||||||
|
field=models.BooleanField(default=False, help_text='If the group is freely accessible to use'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cidusergroup',
|
||||||
|
name='archive',
|
||||||
|
field=models.BooleanField(default=False, help_text='Archived groups remain on the test system but are not displayed by default'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cidusergroup',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(blank=True, help_text='Name of the Group', max_length=50),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='userusergroup',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(blank=True, help_text='Name of the Group', max_length=50),
|
||||||
|
),
|
||||||
|
]
|
||||||
+52
-38
@@ -437,13 +437,12 @@ class SeriesBase(models.Model):
|
|||||||
if series_number is not None:
|
if series_number is not None:
|
||||||
series_html = format_html(
|
series_html = format_html(
|
||||||
"<span class='series-block-series-number'>Series {}</span>{}<br>",
|
"<span class='series-block-series-number'>Series {}</span>{}<br>",
|
||||||
series_number
|
series_number,
|
||||||
)
|
)
|
||||||
description = ""
|
description = ""
|
||||||
if self.description:
|
if self.description:
|
||||||
description = format_html("{}<br/>", self.description)
|
description = format_html("{}<br/>", self.description)
|
||||||
|
|
||||||
|
|
||||||
return format_html(
|
return format_html(
|
||||||
"""{}{}
|
"""{}{}
|
||||||
<span>
|
<span>
|
||||||
@@ -554,9 +553,16 @@ class ExamOrCollectionGenericBase(models.Model, AuthorMixin):
|
|||||||
|
|
||||||
name = models.CharField(max_length=200, help_text="Name of the exam/collection")
|
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)
|
start_date = models.DateTimeField(
|
||||||
end_date = models.DateTimeField(help_text="Date (and time) the exam/collection ends", null=True, blank=True)
|
help_text="Date (and time) the exam/collection starts", 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")
|
)
|
||||||
|
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(
|
active = models.BooleanField(
|
||||||
help_text="If an exam/collection should be available to take", default=False
|
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):
|
def clean(self, *args, **kwargs):
|
||||||
if self.restrict_to_dates:
|
if self.restrict_to_dates:
|
||||||
if self.start_date is None:
|
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:
|
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)
|
return super().clean(*args, **kwargs)
|
||||||
|
|
||||||
def get_app_name(self):
|
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):
|
def get_base_template(self):
|
||||||
return f"{self.get_app_name()}/exams.html"
|
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)
|
return self.check_user_can_take(cid, passcode, request.user, active_only)
|
||||||
|
|
||||||
def check_user_can_take(
|
def check_user_can_take(self, cid, passcode, user=None, active_only=True):
|
||||||
self, cid, passcode, user = None, active_only=True
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
Helper to check if a user is allowed to access an exam/collection
|
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 we are limiting by dates check that the current date is within the range
|
||||||
if self.restrict_to_dates:
|
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")
|
raise Http404("Exam not found")
|
||||||
# If it is not active no one can take
|
# If it is not active no one can take
|
||||||
elif active_only and not self.active:
|
elif active_only and not self.active:
|
||||||
@@ -671,7 +681,7 @@ class ExamOrCollectionGenericBase(models.Model, AuthorMixin):
|
|||||||
cid: int | None = None,
|
cid: int | None = None,
|
||||||
passcode: str | None = None,
|
passcode: str | None = None,
|
||||||
user: User | None = None,
|
user: User | None = None,
|
||||||
#request: HttpRequest | None = None,
|
# request: HttpRequest | None = None,
|
||||||
user_id: int | None = None,
|
user_id: int | None = None,
|
||||||
allow_authors: bool = True,
|
allow_authors: bool = True,
|
||||||
):
|
):
|
||||||
@@ -795,9 +805,7 @@ class ExamOrCollectionGenericBase(models.Model, AuthorMixin):
|
|||||||
"cid_user_groups",
|
"cid_user_groups",
|
||||||
"user_user_groups",
|
"user_user_groups",
|
||||||
)
|
)
|
||||||
FK_fields = (
|
FK_fields = ("examcollection",)
|
||||||
"examcollection",
|
|
||||||
)
|
|
||||||
|
|
||||||
model_dict = model_to_dict(self)
|
model_dict = model_to_dict(self)
|
||||||
|
|
||||||
@@ -824,8 +832,6 @@ class ExamOrCollectionGenericBase(models.Model, AuthorMixin):
|
|||||||
for item in m2m_to_update[field]:
|
for item in m2m_to_update[field]:
|
||||||
rel.add(item)
|
rel.add(item)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return cloned_model
|
return cloned_model
|
||||||
|
|
||||||
|
|
||||||
@@ -835,9 +841,9 @@ class ExamBase(ExamOrCollectionGenericBase):
|
|||||||
default=False,
|
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.",
|
# help_text="If an exam should randomise the order of questions in RTS.",
|
||||||
#)
|
# )
|
||||||
|
|
||||||
include_history = models.BooleanField(
|
include_history = models.BooleanField(
|
||||||
help_text="If an exam should include history when taking",
|
help_text="If an exam should include history when taking",
|
||||||
@@ -884,7 +890,7 @@ class ExamBase(ExamOrCollectionGenericBase):
|
|||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
#def clean(self, *args, **kwargs):
|
# def clean(self, *args, **kwargs):
|
||||||
# if self.user_scores is None:
|
# if self.user_scores is None:
|
||||||
# self.user_scores = {}
|
# self.user_scores = {}
|
||||||
# return super().clean(*args, **kwargs)
|
# return super().clean(*args, **kwargs)
|
||||||
@@ -1067,12 +1073,12 @@ class ExamBase(ExamOrCollectionGenericBase):
|
|||||||
|
|
||||||
class ExamUserStatus(models.Model):
|
class ExamUserStatus(models.Model):
|
||||||
datetime = models.DateTimeField(auto_now_add=True)
|
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
|
# "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
|
# settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL
|
||||||
#)
|
# )
|
||||||
status = models.CharField(max_length=255)
|
status = models.CharField(max_length=255)
|
||||||
extra = models.CharField(max_length=255)
|
extra = models.CharField(max_length=255)
|
||||||
|
|
||||||
@@ -1080,7 +1086,9 @@ class ExamUserStatus(models.Model):
|
|||||||
object_id = models.PositiveIntegerField()
|
object_id = models.PositiveIntegerField()
|
||||||
exam = GenericForeignKey("content_type", "object_id")
|
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):
|
def __str__(self):
|
||||||
self.cid_user_exam: "CidUserExam"
|
self.cid_user_exam: "CidUserExam"
|
||||||
@@ -1397,6 +1405,7 @@ class CidUserExam(models.Model):
|
|||||||
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)
|
(and should be merged into here)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||||
object_id = models.PositiveIntegerField()
|
object_id = models.PositiveIntegerField()
|
||||||
exam = GenericForeignKey("content_type", "object_id")
|
exam = GenericForeignKey("content_type", "object_id")
|
||||||
@@ -1460,9 +1469,22 @@ CID_GROUP_EXAMS = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CidUserGroup(models.Model):
|
class BaseUserGroup(models.Model):
|
||||||
name = models.CharField(blank=True, max_length=50)
|
name = models.CharField(blank=True, max_length=50, help_text="Name of the Group")
|
||||||
archive = models.BooleanField(default=False)
|
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:
|
def __str__(self) -> str:
|
||||||
return str(self.name)
|
return str(self.name)
|
||||||
@@ -1495,15 +1517,7 @@ USER_GROUP_EXAMS = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class UserUserGroup(models.Model):
|
class UserUserGroup(BaseUserGroup):
|
||||||
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",
|
|
||||||
)
|
|
||||||
|
|
||||||
users = models.ManyToManyField(
|
users = models.ManyToManyField(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<a href="{% url exam.get_app_name|add:':exam_cids' exam.id %}" title="Candidate overview">Overview</a>
|
||||||
|
\ <a href="{% url exam.get_app_name|add:':exam_cids_edit' exam.id %}" title="Edit the Exam CID candidates">Edit CIDs</a>
|
||||||
|
\ <a href="{% url exam.get_app_name|add:':exam_users_edit' exam.id %}" title="Edit the Exam User candidates">Edit User</a>
|
||||||
|
\ <a href="{% url exam.get_app_name|add:':exam_groups_edit' exam.id %}" title="Edit the Exam Groups">Edit Groups</a>
|
||||||
@@ -1,88 +1,87 @@
|
|||||||
|
{% include "generic/exam_candidate_headers.html" %}
|
||||||
|
<h3>CID Users</h3>
|
||||||
|
{% if groups %}
|
||||||
|
Users from the following groups are available: {% for group in groups %}<a href='{{group.get_absolute_url}}'>{{group}}</a>, {% endfor %} .
|
||||||
|
{% else %}
|
||||||
|
This exam is not associated with any user groups. <a href="{% url exam.app_name|add:':exam_groups_edit' exam.pk %}">Edit and add a group</a> to enable user management.
|
||||||
|
{% endif %}
|
||||||
|
<ol>
|
||||||
|
{% for cid in current_cid_users %}
|
||||||
|
|
||||||
|
<li class="current"><a href="{% url 'generic:update_cid' cid.pk %}">{{cid.cid}}</a> [{{cid.passcode}}] {{cid.name}} /
|
||||||
|
Email: {{cid.email}} <button class="toggle-btn" data-pk="{{cid.pk}}" data-cid="{{cid.cid}}">Toggle</button>
|
||||||
|
</li>
|
||||||
|
|
||||||
<h3>CID Users</h3>
|
{% endfor %}
|
||||||
{% if groups %}
|
{% for cid in available_cid_users %}
|
||||||
Users from the following groups are available: {% for group in groups %}<a href='{{group.get_absolute_url}}'>{{group}}</a>, {% endfor %} .
|
|
||||||
{% else %}
|
|
||||||
This exam is not associated with any user groups. <a href="{% url exam.app_name|add:':exam_update' exam.pk %}">Edit and add a group</a> to enable user management.
|
|
||||||
{% endif %}
|
|
||||||
<ol>
|
|
||||||
{% for cid in current_cid_users %}
|
|
||||||
|
|
||||||
<li class="current"><a href="{% url 'generic:update_cid' cid.pk %}">{{cid.cid}}</a> [{{cid.passcode}}] {{cid.name}} /
|
<li><a href="{% url 'generic:update_cid' cid.pk %}">{{cid.cid}}</a> [{{cid.passcode}}] {{cid.name}} /
|
||||||
Email: {{cid.email}} <button class="toggle-btn" data-pk="{{cid.pk}}" data-cid="{{cid.cid}}">Toggle</button>
|
Email: {{cid.email}} <button class="toggle-btn" data-pk="{{cid.pk}}" data-cid="{{cid.cid}}">Toggle</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for cid in available_cid_users %}
|
</ol>
|
||||||
|
|
||||||
<li><a href="{% url 'generic:update_cid' cid.pk %}">{{cid.cid}}</a> [{{cid.passcode}}] {{cid.name}} /
|
|
||||||
Email: {{cid.email}} <button class="toggle-btn" data-pk="{{cid.pk}}" data-cid="{{cid.cid}}">Toggle</button>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
{% comment %} Hide the toggle button if there are no users (to add or remove) {% endcomment %}
|
{% comment %} Hide the toggle button if there are no users (to add or remove) {% endcomment %}
|
||||||
{% if current_cid_users or available_cid_users %}
|
{% if current_cid_users or available_cid_users %}
|
||||||
<p><button class="toggle-all-btn">Toggle all</button></p>
|
<p><button class="toggle-all-btn">Toggle all</button></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(() => {
|
$(document).ready(() => {
|
||||||
$(".toggle-all-btn").click((el) => {
|
$(".toggle-all-btn").click((el) => {
|
||||||
$(".toggle-btn").click();
|
$(".toggle-btn").click();
|
||||||
});
|
});
|
||||||
$(".toggle-btn").click((el) => {
|
$(".toggle-btn").click((el) => {
|
||||||
let parent = $(el.target).parent()
|
let parent = $(el.target).parent()
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "{% url exam.app_name|add:':exam_json_edit' exam.pk %}",
|
url: "{% url exam.app_name|add:':exam_json_edit' exam.pk %}",
|
||||||
data: {
|
data: {
|
||||||
csrfmiddlewaretoken: "{{csrf_token}}",
|
csrfmiddlewaretoken: "{{csrf_token}}",
|
||||||
edit_cid_user: el.target.dataset.pk,
|
edit_cid_user: el.target.dataset.pk,
|
||||||
add: !parent.hasClass("current"),
|
add: !parent.hasClass("current"),
|
||||||
},
|
},
|
||||||
type: "POST",
|
type: "POST",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
})
|
})
|
||||||
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
|
||||||
.done(function (data) {
|
.done(function (data) {
|
||||||
console.log("done", data);
|
console.log("done", data);
|
||||||
|
|
||||||
if (data.status == "success") {
|
if (data.status == "success") {
|
||||||
if (data.added) {
|
if (data.added) {
|
||||||
parent.addClass("current");
|
parent.addClass("current");
|
||||||
toastr.info(`User ${el.target.dataset.cid} added to exam`)
|
toastr.info(`User ${el.target.dataset.cid} added to exam`)
|
||||||
} else {
|
} else {
|
||||||
parent.removeClass("current");
|
parent.removeClass("current");
|
||||||
toastr.info(`User ${el.target.dataset.cid} removed from exam`)
|
toastr.info(`User ${el.target.dataset.cid} removed from exam`)
|
||||||
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
toastr.error(data.status)
|
|
||||||
}
|
}
|
||||||
})
|
} else {
|
||||||
.fail(function (data) {
|
toastr.error(data.status)
|
||||||
console.log("fail", data);
|
}
|
||||||
toastr.error(data.responseJSON.status)
|
})
|
||||||
|
.fail(function (data) {
|
||||||
|
console.log("fail", data);
|
||||||
|
toastr.error(data.responseJSON.status)
|
||||||
|
|
||||||
})
|
})
|
||||||
.always(function () {
|
.always(function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
.current {
|
.current {
|
||||||
color: lightgray;
|
color: lightgray;
|
||||||
}
|
}
|
||||||
.current::before {
|
.current::before {
|
||||||
content: "[ADDED TO EXAM] - "
|
content: "[ADDED TO EXAM] - "
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{% extends exam.get_app_name|add:'/exams.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Exam: {{exam}}</h1>
|
||||||
|
{% include "generic/exam_candidate_headers.html" %}
|
||||||
|
|
||||||
|
<h3>Groups</h3>
|
||||||
|
|
||||||
|
<form action="" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<table>
|
||||||
|
{{ form }}
|
||||||
|
</table>
|
||||||
|
<input type="submit" value="Submit">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock content %}
|
||||||
@@ -47,21 +47,24 @@ Markers:
|
|||||||
|
|
||||||
|
|
||||||
{% if can_edit %}
|
{% if can_edit %}
|
||||||
<details>
|
<details class="answer-management">
|
||||||
<summary>Answer management</summary>
|
<summary>Answer management</summary>
|
||||||
|
|
||||||
<div class="alert alert-danger">
|
<div>
|
||||||
<p>Manage answers for this collection. </p>
|
<div class="alert alert-danger">
|
||||||
|
<i class="bi bi-exclamation-diamond"></i> Manage answers for this collection. Please note these are <b>permanant</b> and cannot be undone.
|
||||||
|
</div>
|
||||||
|
|
||||||
Please note these are permanant and cannot be undone.
|
<details>
|
||||||
|
<summary>Click here to show management options</summary>
|
||||||
|
<button title="This will clear all user answers and attempts"
|
||||||
|
hx-post="{% url exam.get_app_name|add:':exam_reset_answers' exam.pk %}"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-confirm="Are you sure you want to reset all answers? This action cannot be undone."
|
||||||
|
>Reset all answers</button>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button title="This will clear all user answers and attempts"
|
|
||||||
hx-post="{% url exam.get_app_name|add:':exam_reset_answers' exam.pk %}"
|
|
||||||
hx-swap="outerHTML"
|
|
||||||
hx-confirm="Are you sure you want to reset all answers? This action cannot be undone."
|
|
||||||
>Reset all answers</button>
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -104,4 +107,13 @@ Author(s): {% for author in exam.author.all %}
|
|||||||
{{ author }}{% if not forloop.last %}, {% endif %}
|
{{ author }}{% if not forloop.last %}, {% endif %}
|
||||||
{% endfor %}<br/>
|
{% endfor %}<br/>
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
<style>
|
||||||
|
.answer-management[open] {
|
||||||
|
border: 1px dashed lightgrey;
|
||||||
|
border-top: 1px dotted lightgrey;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock css %}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
{% include "generic/exam_candidate_headers.html" %}
|
||||||
<h3>Users</h3>
|
<h3>Users</h3>
|
||||||
{% if groups %}
|
{% if groups %}
|
||||||
Users from the following groups are available: {% for group in groups %}<a href='{{group.get_absolute_url}}'>{{group}}</a>, {% endfor %} .
|
Users from the following groups are available: {% for group in groups %}<a href='{{group.get_absolute_url}}'>{{group}}</a>, {% endfor %} .
|
||||||
{% else %}
|
{% else %}
|
||||||
This exam is not associated with any user groups. <a href="{% url exam.app_name|add:':exam_update' exam.pk %}">Edit and add a group</a> to enable user management.
|
This exam is not associated with any user groups. <a href="{% url exam.app_name|add:':exam_groups_edit' exam.pk %}">Edit and add a group</a> to enable user management.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<ol>
|
<ol>
|
||||||
{% for user in current_user_users %}
|
{% for user in current_user_users %}
|
||||||
|
|||||||
@@ -343,6 +343,7 @@ def generic_exam_urls(generic_exam_view: GenericExamViews):
|
|||||||
path("exam/", generic_exam_view.exam_list, name="exam_list"),
|
path("exam/", generic_exam_view.exam_list, name="exam_list"),
|
||||||
path("exam/all", generic_exam_view.exam_list_all, name="exam_list_all"),
|
path("exam/all", generic_exam_view.exam_list_all, name="exam_list_all"),
|
||||||
path("exam/<int:exam_id>/cids", generic_exam_view.exam_cids, name="exam_cids"),
|
path("exam/<int:exam_id>/cids", generic_exam_view.exam_cids, name="exam_cids"),
|
||||||
|
#path("exam/<int:exam_id>/groups", generic_exam_view.exam_groups_edit, name="exam_groups_edit"),
|
||||||
path(
|
path(
|
||||||
"exam/<int:exam_id>/reset_answers",
|
"exam/<int:exam_id>/reset_answers",
|
||||||
generic_exam_view.exam_reset_answers,
|
generic_exam_view.exam_reset_answers,
|
||||||
|
|||||||
@@ -1042,6 +1042,23 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
|
|
||||||
return render(request, "exam_cids.html", context)
|
return render(request, "exam_cids.html", context)
|
||||||
|
|
||||||
|
#def exam_groups_edit(self, request, exam_id):
|
||||||
|
# exam = get_object_or_404(self.Exam, pk=exam_id)
|
||||||
|
|
||||||
|
# if not request.user.groups.filter(name="cid_user_manager").exists():
|
||||||
|
# # raise PermissionDenied
|
||||||
|
# if request.user not in exam.author.all():
|
||||||
|
# raise PermissionDenied
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
|
||||||
|
# context = {
|
||||||
|
# "exam": exam,
|
||||||
|
# #"groups": exam_groups,
|
||||||
|
# }
|
||||||
|
# return render(request, "exam_groups_edit.html", context)
|
||||||
|
|
||||||
def exam_users_edit(self, request, exam_id):
|
def exam_users_edit(self, request, exam_id):
|
||||||
exam = get_object_or_404(self.Exam, pk=exam_id)
|
exam = get_object_or_404(self.Exam, pk=exam_id)
|
||||||
|
|
||||||
@@ -3483,3 +3500,16 @@ class SeriesImagesZipViewBase(SuperuserRequiredMixin, View):
|
|||||||
response['Content-Disposition'] = 'attachment; filename=%s' % self.get_archive_name(request)
|
response['Content-Disposition'] = 'attachment; filename=%s' % self.get_archive_name(request)
|
||||||
response['Content-Length'] = file_size
|
response['Content-Length'] = file_size
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
class ExamGroupsUpdateBase(
|
||||||
|
RevisionMixin, CheckCanEditMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView
|
||||||
|
):
|
||||||
|
template_name = "generic/exam_groups_edit.html"
|
||||||
|
|
||||||
|
def get_success_url(self) -> str:
|
||||||
|
return reverse_lazy(f"{self.object.get_app_name()}:exam_cids", kwargs={"exam_id": self.object.pk})
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
kwargs = super(ExamGroupsUpdateBase, self).get_form_kwargs()
|
||||||
|
kwargs.update({"user": self.request.user})
|
||||||
|
return kwargs
|
||||||
+5
-1
@@ -8,7 +8,7 @@ from django.forms import (
|
|||||||
CharField,
|
CharField,
|
||||||
)
|
)
|
||||||
from django.forms import inlineformset_factory
|
from django.forms import inlineformset_factory
|
||||||
from generic.forms import ExamAuthorFormMixin, ExamFormMixin, ExamMarkerFormMixin
|
from generic.forms import ExamAuthorFormMixin, ExamFormMixin, ExamGroupsFormMixin, ExamMarkerFormMixin
|
||||||
|
|
||||||
from longs.models import (
|
from longs.models import (
|
||||||
# Examination,
|
# Examination,
|
||||||
@@ -318,3 +318,7 @@ class ExamAuthorForm(ExamAuthorFormMixin):
|
|||||||
class ExamMarkerForm(ExamMarkerFormMixin):
|
class ExamMarkerForm(ExamMarkerFormMixin):
|
||||||
class Meta(ExamMarkerFormMixin.Meta):
|
class Meta(ExamMarkerFormMixin.Meta):
|
||||||
model = Exam
|
model = Exam
|
||||||
|
|
||||||
|
class ExamGroupsForm(ExamGroupsFormMixin):
|
||||||
|
class Meta(ExamGroupsFormMixin.Meta):
|
||||||
|
model = Exam
|
||||||
@@ -83,6 +83,7 @@ urlpatterns = [
|
|||||||
name="mark",
|
name="mark",
|
||||||
),
|
),
|
||||||
path("exam/<int:pk>/markers", views.ExamMarkersUpdate.as_view(), name="exam_markers"),
|
path("exam/<int:pk>/markers", views.ExamMarkersUpdate.as_view(), name="exam_markers"),
|
||||||
|
path("exam/<int:pk>/groups", views.ExamGroupsUpdate.as_view(), name="exam_groups_edit"),
|
||||||
path("exam/<int:pk>/authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"),
|
path("exam/<int:pk>/authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"),
|
||||||
path(
|
path(
|
||||||
"exam/<int:pk>/question_json_refresh",
|
"exam/<int:pk>/question_json_refresh",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from django.http import HttpResponseRedirect, HttpResponse
|
|||||||
|
|
||||||
from .forms import (
|
from .forms import (
|
||||||
ExamAuthorForm,
|
ExamAuthorForm,
|
||||||
|
ExamGroupsForm,
|
||||||
ExamMarkerForm,
|
ExamMarkerForm,
|
||||||
LongForm,
|
LongForm,
|
||||||
LongSeriesForm,
|
LongSeriesForm,
|
||||||
@@ -81,6 +82,7 @@ from generic.views import (
|
|||||||
ExamCloneMixin,
|
ExamCloneMixin,
|
||||||
ExamCreateBase,
|
ExamCreateBase,
|
||||||
ExamDeleteBase,
|
ExamDeleteBase,
|
||||||
|
ExamGroupsUpdateBase,
|
||||||
ExamUpdateBase,
|
ExamUpdateBase,
|
||||||
ExamViews,
|
ExamViews,
|
||||||
SeriesImagesZipViewBase,
|
SeriesImagesZipViewBase,
|
||||||
@@ -991,6 +993,10 @@ class ExamMarkersUpdate(
|
|||||||
|
|
||||||
template_name = "markers_form.html"
|
template_name = "markers_form.html"
|
||||||
|
|
||||||
|
class ExamGroupsUpdate(ExamGroupsUpdateBase):
|
||||||
|
model = Exam
|
||||||
|
form_class = ExamGroupsForm
|
||||||
|
|
||||||
|
|
||||||
# class ExamViewSet(RevisionMixin, viewsets.ModelViewSet):
|
# class ExamViewSet(RevisionMixin, viewsets.ModelViewSet):
|
||||||
# queryset = Exam.objects.all().order_by("name")
|
# queryset = Exam.objects.all().order_by("name")
|
||||||
|
|||||||
+6
-1
@@ -8,7 +8,7 @@ from django.forms import (
|
|||||||
)
|
)
|
||||||
from django.forms import inlineformset_factory
|
from django.forms import inlineformset_factory
|
||||||
|
|
||||||
from generic.forms import ExamAuthorFormMixin, ExamFormMixin, ExamMarkerFormMixin
|
from generic.forms import ExamAuthorFormMixin, ExamFormMixin, ExamGroupsFormMixin, ExamMarkerFormMixin
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Question,
|
Question,
|
||||||
@@ -43,3 +43,8 @@ class ExamAuthorForm(ExamAuthorFormMixin):
|
|||||||
class ExamMarkerForm(ExamMarkerFormMixin):
|
class ExamMarkerForm(ExamMarkerFormMixin):
|
||||||
class Meta(ExamMarkerFormMixin.Meta):
|
class Meta(ExamMarkerFormMixin.Meta):
|
||||||
model = Exam
|
model = Exam
|
||||||
|
|
||||||
|
|
||||||
|
class ExamGroupsForm(ExamGroupsFormMixin):
|
||||||
|
class Meta(ExamGroupsFormMixin.Meta):
|
||||||
|
model = Exam
|
||||||
+7
-1
@@ -47,6 +47,7 @@ from generic.views import (
|
|||||||
ExamCloneMixin,
|
ExamCloneMixin,
|
||||||
ExamCreateBase,
|
ExamCreateBase,
|
||||||
ExamDeleteBase,
|
ExamDeleteBase,
|
||||||
|
ExamGroupsUpdateBase,
|
||||||
ExamUpdateBase,
|
ExamUpdateBase,
|
||||||
ExamViews,
|
ExamViews,
|
||||||
GenericViewBase,
|
GenericViewBase,
|
||||||
@@ -58,7 +59,7 @@ from rest_framework.pagination import PageNumberPagination
|
|||||||
|
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
|
|
||||||
from .forms import ExamMarkerForm, UserAnswerForm, ExamAuthorForm, ExamForm
|
from .forms import ExamGroupsForm, ExamMarkerForm, UserAnswerForm, ExamAuthorForm, ExamForm
|
||||||
from .decorators import (
|
from .decorators import (
|
||||||
user_is_author_or_physics_checker,
|
user_is_author_or_physics_checker,
|
||||||
user_is_exam_author_or_physics_checker,
|
user_is_exam_author_or_physics_checker,
|
||||||
@@ -367,6 +368,11 @@ class ExamMarkersUpdate(
|
|||||||
|
|
||||||
template_name = "markers_form.html"
|
template_name = "markers_form.html"
|
||||||
|
|
||||||
|
|
||||||
|
class ExamGroupsUpdate(ExamGroupsUpdateBase):
|
||||||
|
model = Exam
|
||||||
|
form_class = ExamGroupsForm
|
||||||
|
|
||||||
class QuestionView(
|
class QuestionView(
|
||||||
LoginRequiredMixin, SingleTableMixin, FilterView, AuthorOrCheckerRequiredMixin
|
LoginRequiredMixin, SingleTableMixin, FilterView, AuthorOrCheckerRequiredMixin
|
||||||
):
|
):
|
||||||
|
|||||||
+5
-1
@@ -7,7 +7,7 @@ from django.forms import (
|
|||||||
CharField,
|
CharField,
|
||||||
)
|
)
|
||||||
from django.forms import inlineformset_factory
|
from django.forms import inlineformset_factory
|
||||||
from generic.forms import ExamAuthorFormMixin, ExamFormMixin, ExamMarkerFormMixin
|
from generic.forms import ExamAuthorFormMixin, ExamFormMixin, ExamGroupsFormMixin, ExamMarkerFormMixin
|
||||||
|
|
||||||
from rapids.models import (
|
from rapids.models import (
|
||||||
Abnormality,
|
Abnormality,
|
||||||
@@ -245,3 +245,7 @@ class ExamAuthorForm(ExamAuthorFormMixin):
|
|||||||
class ExamMarkerForm(ExamMarkerFormMixin):
|
class ExamMarkerForm(ExamMarkerFormMixin):
|
||||||
class Meta(ExamMarkerFormMixin.Meta):
|
class Meta(ExamMarkerFormMixin.Meta):
|
||||||
model = Exam
|
model = Exam
|
||||||
|
|
||||||
|
class ExamGroupsForm(ExamGroupsFormMixin):
|
||||||
|
class Meta(ExamGroupsFormMixin.Meta):
|
||||||
|
model = Exam
|
||||||
@@ -21,6 +21,7 @@ from django.http import HttpResponseRedirect, HttpResponse
|
|||||||
|
|
||||||
from .forms import (
|
from .forms import (
|
||||||
ExamAuthorForm,
|
ExamAuthorForm,
|
||||||
|
ExamGroupsForm,
|
||||||
ExamMarkerForm,
|
ExamMarkerForm,
|
||||||
RapidForm,
|
RapidForm,
|
||||||
MarkRapidQuestionForm,
|
MarkRapidQuestionForm,
|
||||||
@@ -49,6 +50,7 @@ from generic.views import (
|
|||||||
ExamCloneMixin,
|
ExamCloneMixin,
|
||||||
ExamCreateBase,
|
ExamCreateBase,
|
||||||
ExamDeleteBase,
|
ExamDeleteBase,
|
||||||
|
ExamGroupsUpdateBase,
|
||||||
ExamUpdateBase,
|
ExamUpdateBase,
|
||||||
ExamViews,
|
ExamViews,
|
||||||
GenericViewBase,
|
GenericViewBase,
|
||||||
@@ -773,6 +775,10 @@ class ExamMarkersUpdate(
|
|||||||
|
|
||||||
template_name = "markers_form.html"
|
template_name = "markers_form.html"
|
||||||
|
|
||||||
|
class ExamGroupsUpdate(ExamGroupsUpdateBase):
|
||||||
|
model = Exam
|
||||||
|
form_class = ExamGroupsForm
|
||||||
|
|
||||||
class UserAnswerView(LoginRequiredMixin, DetailView):
|
class UserAnswerView(LoginRequiredMixin, DetailView):
|
||||||
model = UserAnswer
|
model = UserAnswer
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -8,7 +8,7 @@ from django.forms import (
|
|||||||
)
|
)
|
||||||
from django.forms import inlineformset_factory
|
from django.forms import inlineformset_factory
|
||||||
|
|
||||||
from generic.forms import ExamAuthorFormMixin, ExamFormMixin, ExamMarkerFormMixin
|
from generic.forms import ExamAuthorFormMixin, ExamFormMixin, ExamGroupsFormMixin, ExamMarkerFormMixin
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Question,
|
Question,
|
||||||
@@ -45,6 +45,11 @@ class ExamMarkerForm(ExamMarkerFormMixin):
|
|||||||
class Meta(ExamMarkerFormMixin.Meta):
|
class Meta(ExamMarkerFormMixin.Meta):
|
||||||
model = Exam
|
model = Exam
|
||||||
|
|
||||||
|
|
||||||
|
class ExamGroupsForm(ExamGroupsFormMixin):
|
||||||
|
class Meta(ExamGroupsFormMixin.Meta):
|
||||||
|
model = Exam
|
||||||
|
|
||||||
class QuestionForm(ModelForm):
|
class QuestionForm(ModelForm):
|
||||||
|
|
||||||
# exams = ModelMultipleChoiceField(required=False, queryset=Exam.objects.all())
|
# exams = ModelMultipleChoiceField(required=False, queryset=Exam.objects.all())
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ from generic.views import (
|
|||||||
ExamCloneMixin,
|
ExamCloneMixin,
|
||||||
ExamCreateBase,
|
ExamCreateBase,
|
||||||
ExamDeleteBase,
|
ExamDeleteBase,
|
||||||
|
ExamGroupsUpdateBase,
|
||||||
ExamUpdateBase,
|
ExamUpdateBase,
|
||||||
ExamViews,
|
ExamViews,
|
||||||
GenericViewBase,
|
GenericViewBase,
|
||||||
@@ -61,6 +62,7 @@ from .tables import QuestionTable, UserAnswerTable
|
|||||||
from .filters import QuestionFilter, UserAnswerFilter
|
from .filters import QuestionFilter, UserAnswerFilter
|
||||||
|
|
||||||
from .forms import (
|
from .forms import (
|
||||||
|
ExamGroupsForm,
|
||||||
ExamMarkerForm,
|
ExamMarkerForm,
|
||||||
QuestionForm,
|
QuestionForm,
|
||||||
ExamForm,
|
ExamForm,
|
||||||
@@ -438,6 +440,10 @@ class ExamMarkersUpdate(
|
|||||||
|
|
||||||
template_name = "markers_form.html"
|
template_name = "markers_form.html"
|
||||||
|
|
||||||
|
class ExamGroupsUpdate(ExamGroupsUpdateBase):
|
||||||
|
model = Exam
|
||||||
|
form_class = ExamGroupsForm
|
||||||
|
|
||||||
class ExamClone(ExamCloneMixin, ExamCreate):
|
class ExamClone(ExamCloneMixin, ExamCreate):
|
||||||
"""Clone exam view"""
|
"""Clone exam view"""
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
{% load thumbnail %}
|
{% load thumbnail %}
|
||||||
<div class="{{app_name}}">
|
<div class="{{app_name}}">
|
||||||
<h1>Exam: {{ exam }}</h1>
|
<h1>Exam: {{ exam }}</h1>
|
||||||
|
{% include "generic/exam_candidate_headers.html" %}
|
||||||
|
|
||||||
{% include "generic/exam_cids.html" %}
|
{% include "generic/exam_cids.html" %}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user