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
+7 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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")
@@ -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),
),
]
+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,
@@ -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>
+73 -74
View File
@@ -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 %}
<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>
{% 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>
{% endfor %}
{% for cid in available_cid_users %}
<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>
<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>
{% endfor %}
{% for cid in available_cid_users %}
<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 %}
{% if current_cid_users or available_cid_users %}
<p><button class="toggle-all-btn">Toggle all</button></p>
{% endif %}
<script>
$(document).ready(() => {
$(".toggle-all-btn").click((el) => {
$(".toggle-btn").click();
});
$(".toggle-btn").click((el) => {
let parent = $(el.target).parent()
$.ajax({
url: "{% url exam.app_name|add:':exam_json_edit' exam.pk %}",
data: {
csrfmiddlewaretoken: "{{csrf_token}}",
edit_cid_user: el.target.dataset.pk,
add: !parent.hasClass("current"),
},
type: "POST",
dataType: "json",
})
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
.done(function (data) {
console.log("done", data);
{% if current_cid_users or available_cid_users %}
<p><button class="toggle-all-btn">Toggle all</button></p>
{% endif %}
if (data.status == "success") {
if (data.added) {
<script>
$(document).ready(() => {
$(".toggle-all-btn").click((el) => {
$(".toggle-btn").click();
});
$(".toggle-btn").click((el) => {
let parent = $(el.target).parent()
$.ajax({
url: "{% url exam.app_name|add:':exam_json_edit' exam.pk %}",
data: {
csrfmiddlewaretoken: "{{csrf_token}}",
edit_cid_user: el.target.dataset.pk,
add: !parent.hasClass("current"),
},
type: "POST",
dataType: "json",
})
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
.done(function (data) {
console.log("done", data);
if (data.status == "success") {
if (data.added) {
parent.addClass("current");
toastr.info(`User ${el.target.dataset.cid} added to exam`)
} else {
} else {
parent.removeClass("current");
toastr.info(`User ${el.target.dataset.cid} removed from exam`)
}
} else {
toastr.error(data.status)
}
})
.fail(function (data) {
console.log("fail", data);
toastr.error(data.responseJSON.status)
} else {
toastr.error(data.status)
}
})
.fail(function (data) {
console.log("fail", data);
toastr.error(data.responseJSON.status)
})
.always(function () {
})
})
.always(function () {
})
});
});
})
</script>
<style>
.current {
color: lightgray;
}
.current::before {
content: "[ADDED TO EXAM] - "
}
})
</script>
<style>
.current {
color: lightgray;
}
.current::before {
content: "[ADDED TO EXAM] - "
}
button {
padding: 0;
}
</style>
button {
padding: 0;
}
</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 %}
<details>
<details class="answer-management">
<summary>Answer management</summary>
<div class="alert alert-danger">
<p>Manage answers for this collection. </p>
<div>
<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>
<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>
{% 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 %}
+1
View File
@@ -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,
+31 -1
View File
@@ -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)
@@ -3482,4 +3499,17 @@ class SeriesImagesZipViewBase(SuperuserRequiredMixin, View):
response = HttpResponse(temp_file, content_type='application/zip')
response['Content-Disposition'] = 'attachment; filename=%s' % self.get_archive_name(request)
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
View File
@@ -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
+1
View File
@@ -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",
+6
View File
@@ -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
View File
@@ -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,
@@ -42,4 +42,9 @@ class ExamAuthorForm(ExamAuthorFormMixin):
class ExamMarkerForm(ExamMarkerFormMixin):
class Meta(ExamMarkerFormMixin.Meta):
model = Exam
class ExamGroupsForm(ExamGroupsFormMixin):
class Meta(ExamGroupsFormMixin.Meta):
model = Exam
+7 -1
View File
@@ -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
View File
@@ -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,
@@ -244,4 +244,8 @@ class ExamAuthorForm(ExamAuthorFormMixin):
class ExamMarkerForm(ExamMarkerFormMixin):
class Meta(ExamMarkerFormMixin.Meta):
model = Exam
class ExamGroupsForm(ExamGroupsFormMixin):
class Meta(ExamGroupsFormMixin.Meta):
model = Exam
+6
View File
@@ -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
View File
@@ -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())
+6
View File
@@ -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"""
+1
View File
@@ -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" %}