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 generic.forms import ExamAuthorFormMixin, ExamFormMixin, ExamMarkerFormMixin
|
||||
from generic.forms import ExamAuthorFormMixin, ExamFormMixin, ExamGroupsFormMixin, ExamMarkerFormMixin
|
||||
|
||||
from .models import (
|
||||
Answer,
|
||||
@@ -236,3 +236,9 @@ class ExamAuthorForm(ExamAuthorFormMixin):
|
||||
class ExamMarkerForm(ExamMarkerFormMixin):
|
||||
class Meta(ExamMarkerFormMixin.Meta):
|
||||
model = Exam
|
||||
|
||||
|
||||
|
||||
class ExamGroupsForm(ExamGroupsFormMixin):
|
||||
class Meta(ExamGroupsFormMixin.Meta):
|
||||
model = Exam
|
||||
+6
-1
@@ -33,6 +33,7 @@ from .forms import (
|
||||
AnswerUpdateFormSet,
|
||||
BodyPartForm,
|
||||
ExamAuthorForm,
|
||||
ExamGroupsForm,
|
||||
ExamMarkerForm,
|
||||
ExaminationForm,
|
||||
MarkAnatomyQuestionForm,
|
||||
@@ -51,7 +52,7 @@ from .models import (
|
||||
# IncorrectAnswers,
|
||||
)
|
||||
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 .decorators import user_is_author_or_anatomy_checker
|
||||
@@ -849,6 +850,10 @@ class ExamMarkersUpdate(
|
||||
template_name = "markers_form.html"
|
||||
|
||||
|
||||
class ExamGroupsUpdate(ExamGroupsUpdateBase):
|
||||
model = Exam
|
||||
form_class = ExamGroupsForm
|
||||
|
||||
|
||||
GenericViews = GenericViewBase("anatomy", AnatomyQuestion, UserAnswer, Exam)
|
||||
|
||||
|
||||
+43
-16
@@ -107,21 +107,21 @@ class ExamFormMixin:
|
||||
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()
|
||||
#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()
|
||||
|
||||
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,
|
||||
)
|
||||
#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,
|
||||
#)
|
||||
self.fields["start_date"] = SplitDateTimeFieldDefaultTime(
|
||||
widget=SplitDateTimeWidget(
|
||||
date_attrs={"type": "date", "class": "datepicker"},
|
||||
@@ -155,8 +155,8 @@ class ExamFormMixin:
|
||||
"active",
|
||||
"publish_results",
|
||||
"archive",
|
||||
"cid_user_groups",
|
||||
"user_user_groups",
|
||||
#"cid_user_groups",
|
||||
#"user_user_groups",
|
||||
# "author",
|
||||
]
|
||||
|
||||
@@ -198,6 +198,32 @@ class ExamMarkerFormMixin(ModelForm):
|
||||
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):
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -704,3 +730,4 @@ class ExamCollectionCloneForm(ModelForm):
|
||||
class Meta:
|
||||
model = ExamCollection
|
||||
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:
|
||||
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:
|
||||
@@ -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",
|
||||
@@ -884,7 +890,7 @@ class ExamBase(ExamOrCollectionGenericBase):
|
||||
|
||||
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)
|
||||
@@ -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"
|
||||
@@ -1397,6 +1405,7 @@ class CidUserExam(models.Model):
|
||||
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,
|
||||
|
||||
@@ -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,12 +1,11 @@
|
||||
|
||||
|
||||
<h3>CID Users</h3>
|
||||
{% if groups %}
|
||||
{% 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_update' exam.pk %}">Edit and add a group</a> to enable user management.
|
||||
{% endif %}
|
||||
<ol>
|
||||
{% 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}} /
|
||||
@@ -21,14 +20,14 @@
|
||||
</li>
|
||||
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</ol>
|
||||
|
||||
{% 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>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
<script>
|
||||
$(document).ready(() => {
|
||||
$(".toggle-all-btn").click((el) => {
|
||||
$(".toggle-btn").click();
|
||||
@@ -73,8 +72,8 @@
|
||||
});
|
||||
|
||||
})
|
||||
</script>
|
||||
<style>
|
||||
</script>
|
||||
<style>
|
||||
.current {
|
||||
color: lightgray;
|
||||
}
|
||||
@@ -85,4 +84,4 @@
|
||||
button {
|
||||
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,20 +47,23 @@ Markers:
|
||||
|
||||
|
||||
{% if can_edit %}
|
||||
<details>
|
||||
<details class="answer-management">
|
||||
<summary>Answer management</summary>
|
||||
|
||||
<div>
|
||||
<div class="alert alert-danger">
|
||||
<p>Manage answers for this collection. </p>
|
||||
|
||||
Please note these are permanant and cannot be undone.
|
||||
<i class="bi bi-exclamation-diamond"></i> Manage answers for this collection. Please note these are <b>permanant</b> and cannot be undone.
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
</details>
|
||||
{% endif %}
|
||||
@@ -104,4 +107,13 @@ Author(s): {% for author in exam.author.all %}
|
||||
{{ author }}{% if not forloop.last %}, {% endif %}
|
||||
{% 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>
|
||||
{% 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_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 %}
|
||||
<ol>
|
||||
{% 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/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>/groups", generic_exam_view.exam_groups_edit, name="exam_groups_edit"),
|
||||
path(
|
||||
"exam/<int:exam_id>/reset_answers",
|
||||
generic_exam_view.exam_reset_answers,
|
||||
|
||||
@@ -1042,6 +1042,23 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
|
||||
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):
|
||||
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-Length'] = file_size
|
||||
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,
|
||||
)
|
||||
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 (
|
||||
# Examination,
|
||||
@@ -318,3 +318,7 @@ class ExamAuthorForm(ExamAuthorFormMixin):
|
||||
class ExamMarkerForm(ExamMarkerFormMixin):
|
||||
class Meta(ExamMarkerFormMixin.Meta):
|
||||
model = Exam
|
||||
|
||||
class ExamGroupsForm(ExamGroupsFormMixin):
|
||||
class Meta(ExamGroupsFormMixin.Meta):
|
||||
model = Exam
|
||||
@@ -83,6 +83,7 @@ urlpatterns = [
|
||||
name="mark",
|
||||
),
|
||||
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>/question_json_refresh",
|
||||
|
||||
@@ -26,6 +26,7 @@ from django.http import HttpResponseRedirect, HttpResponse
|
||||
|
||||
from .forms import (
|
||||
ExamAuthorForm,
|
||||
ExamGroupsForm,
|
||||
ExamMarkerForm,
|
||||
LongForm,
|
||||
LongSeriesForm,
|
||||
@@ -81,6 +82,7 @@ from generic.views import (
|
||||
ExamCloneMixin,
|
||||
ExamCreateBase,
|
||||
ExamDeleteBase,
|
||||
ExamGroupsUpdateBase,
|
||||
ExamUpdateBase,
|
||||
ExamViews,
|
||||
SeriesImagesZipViewBase,
|
||||
@@ -991,6 +993,10 @@ class ExamMarkersUpdate(
|
||||
|
||||
template_name = "markers_form.html"
|
||||
|
||||
class ExamGroupsUpdate(ExamGroupsUpdateBase):
|
||||
model = Exam
|
||||
form_class = ExamGroupsForm
|
||||
|
||||
|
||||
# class ExamViewSet(RevisionMixin, viewsets.ModelViewSet):
|
||||
# queryset = Exam.objects.all().order_by("name")
|
||||
|
||||
+6
-1
@@ -8,7 +8,7 @@ from django.forms import (
|
||||
)
|
||||
from django.forms import inlineformset_factory
|
||||
|
||||
from generic.forms import ExamAuthorFormMixin, ExamFormMixin, ExamMarkerFormMixin
|
||||
from generic.forms import ExamAuthorFormMixin, ExamFormMixin, ExamGroupsFormMixin, ExamMarkerFormMixin
|
||||
|
||||
from .models import (
|
||||
Question,
|
||||
@@ -43,3 +43,8 @@ class ExamAuthorForm(ExamAuthorFormMixin):
|
||||
class ExamMarkerForm(ExamMarkerFormMixin):
|
||||
class Meta(ExamMarkerFormMixin.Meta):
|
||||
model = Exam
|
||||
|
||||
|
||||
class ExamGroupsForm(ExamGroupsFormMixin):
|
||||
class Meta(ExamGroupsFormMixin.Meta):
|
||||
model = Exam
|
||||
+7
-1
@@ -47,6 +47,7 @@ from generic.views import (
|
||||
ExamCloneMixin,
|
||||
ExamCreateBase,
|
||||
ExamDeleteBase,
|
||||
ExamGroupsUpdateBase,
|
||||
ExamUpdateBase,
|
||||
ExamViews,
|
||||
GenericViewBase,
|
||||
@@ -58,7 +59,7 @@ from rest_framework.pagination import PageNumberPagination
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
|
||||
from .forms import ExamMarkerForm, UserAnswerForm, ExamAuthorForm, ExamForm
|
||||
from .forms import ExamGroupsForm, ExamMarkerForm, UserAnswerForm, ExamAuthorForm, ExamForm
|
||||
from .decorators import (
|
||||
user_is_author_or_physics_checker,
|
||||
user_is_exam_author_or_physics_checker,
|
||||
@@ -367,6 +368,11 @@ class ExamMarkersUpdate(
|
||||
|
||||
template_name = "markers_form.html"
|
||||
|
||||
|
||||
class ExamGroupsUpdate(ExamGroupsUpdateBase):
|
||||
model = Exam
|
||||
form_class = ExamGroupsForm
|
||||
|
||||
class QuestionView(
|
||||
LoginRequiredMixin, SingleTableMixin, FilterView, AuthorOrCheckerRequiredMixin
|
||||
):
|
||||
|
||||
+5
-1
@@ -7,7 +7,7 @@ from django.forms import (
|
||||
CharField,
|
||||
)
|
||||
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 (
|
||||
Abnormality,
|
||||
@@ -245,3 +245,7 @@ class ExamAuthorForm(ExamAuthorFormMixin):
|
||||
class ExamMarkerForm(ExamMarkerFormMixin):
|
||||
class Meta(ExamMarkerFormMixin.Meta):
|
||||
model = Exam
|
||||
|
||||
class ExamGroupsForm(ExamGroupsFormMixin):
|
||||
class Meta(ExamGroupsFormMixin.Meta):
|
||||
model = Exam
|
||||
@@ -21,6 +21,7 @@ from django.http import HttpResponseRedirect, HttpResponse
|
||||
|
||||
from .forms import (
|
||||
ExamAuthorForm,
|
||||
ExamGroupsForm,
|
||||
ExamMarkerForm,
|
||||
RapidForm,
|
||||
MarkRapidQuestionForm,
|
||||
@@ -49,6 +50,7 @@ from generic.views import (
|
||||
ExamCloneMixin,
|
||||
ExamCreateBase,
|
||||
ExamDeleteBase,
|
||||
ExamGroupsUpdateBase,
|
||||
ExamUpdateBase,
|
||||
ExamViews,
|
||||
GenericViewBase,
|
||||
@@ -773,6 +775,10 @@ class ExamMarkersUpdate(
|
||||
|
||||
template_name = "markers_form.html"
|
||||
|
||||
class ExamGroupsUpdate(ExamGroupsUpdateBase):
|
||||
model = Exam
|
||||
form_class = ExamGroupsForm
|
||||
|
||||
class UserAnswerView(LoginRequiredMixin, DetailView):
|
||||
model = UserAnswer
|
||||
|
||||
|
||||
+6
-1
@@ -8,7 +8,7 @@ from django.forms import (
|
||||
)
|
||||
from django.forms import inlineformset_factory
|
||||
|
||||
from generic.forms import ExamAuthorFormMixin, ExamFormMixin, ExamMarkerFormMixin
|
||||
from generic.forms import ExamAuthorFormMixin, ExamFormMixin, ExamGroupsFormMixin, ExamMarkerFormMixin
|
||||
|
||||
from .models import (
|
||||
Question,
|
||||
@@ -45,6 +45,11 @@ class ExamMarkerForm(ExamMarkerFormMixin):
|
||||
class Meta(ExamMarkerFormMixin.Meta):
|
||||
model = Exam
|
||||
|
||||
|
||||
class ExamGroupsForm(ExamGroupsFormMixin):
|
||||
class Meta(ExamGroupsFormMixin.Meta):
|
||||
model = Exam
|
||||
|
||||
class QuestionForm(ModelForm):
|
||||
|
||||
# exams = ModelMultipleChoiceField(required=False, queryset=Exam.objects.all())
|
||||
|
||||
@@ -46,6 +46,7 @@ from generic.views import (
|
||||
ExamCloneMixin,
|
||||
ExamCreateBase,
|
||||
ExamDeleteBase,
|
||||
ExamGroupsUpdateBase,
|
||||
ExamUpdateBase,
|
||||
ExamViews,
|
||||
GenericViewBase,
|
||||
@@ -61,6 +62,7 @@ from .tables import QuestionTable, UserAnswerTable
|
||||
from .filters import QuestionFilter, UserAnswerFilter
|
||||
|
||||
from .forms import (
|
||||
ExamGroupsForm,
|
||||
ExamMarkerForm,
|
||||
QuestionForm,
|
||||
ExamForm,
|
||||
@@ -438,6 +440,10 @@ class ExamMarkersUpdate(
|
||||
|
||||
template_name = "markers_form.html"
|
||||
|
||||
class ExamGroupsUpdate(ExamGroupsUpdateBase):
|
||||
model = Exam
|
||||
form_class = ExamGroupsForm
|
||||
|
||||
class ExamClone(ExamCloneMixin, ExamCreate):
|
||||
"""Clone exam view"""
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
{% load thumbnail %}
|
||||
<div class="{{app_name}}">
|
||||
<h1>Exam: {{ exam }}</h1>
|
||||
{% include "generic/exam_candidate_headers.html" %}
|
||||
|
||||
{% include "generic/exam_cids.html" %}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user