From 6fe2bae8a876ea95b5a01831332ca67479f4e620 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 12 Aug 2024 10:33:15 +0100 Subject: [PATCH] improve exam group management --- anatomy/forms.py | 8 +- anatomy/views.py | 7 +- generic/forms.py | 59 +++++-- ...cess_userusergroup_open_access_and_more.py | 38 +++++ generic/models.py | 104 +++++++------ .../generic/exam_candidate_headers.html | 4 + generic/templates/generic/exam_cids_edit.html | 147 +++++++++--------- .../templates/generic/exam_groups_edit.html | 17 ++ .../generic/exam_overview_headers.html | 32 ++-- .../templates/generic/exam_users_edit.html | 4 +- generic/urls.py | 1 + generic/views.py | 32 +++- longs/forms.py | 6 +- longs/urls.py | 1 + longs/views.py | 6 + physics/forms.py | 7 +- physics/views.py | 8 +- rapids/forms.py | 6 +- rapids/views.py | 6 + sbas/forms.py | 7 +- sbas/views.py | 6 + templates/exam_cids.html | 1 + 22 files changed, 352 insertions(+), 155 deletions(-) create mode 100644 generic/migrations/0017_cidusergroup_open_access_userusergroup_open_access_and_more.py create mode 100644 generic/templates/generic/exam_candidate_headers.html create mode 100644 generic/templates/generic/exam_groups_edit.html diff --git a/anatomy/forms.py b/anatomy/forms.py index 2f5d64cf..6a44b3cc 100644 --- a/anatomy/forms.py +++ b/anatomy/forms.py @@ -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 \ No newline at end of file diff --git a/anatomy/views.py b/anatomy/views.py index a4171f2d..61903d88 100644 --- a/anatomy/views.py +++ b/anatomy/views.py @@ -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) diff --git a/generic/forms.py b/generic/forms.py index 33266a98..f09f278b 100755 --- a/generic/forms.py +++ b/generic/forms.py @@ -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") + diff --git a/generic/migrations/0017_cidusergroup_open_access_userusergroup_open_access_and_more.py b/generic/migrations/0017_cidusergroup_open_access_userusergroup_open_access_and_more.py new file mode 100644 index 00000000..dc4f963f --- /dev/null +++ b/generic/migrations/0017_cidusergroup_open_access_userusergroup_open_access_and_more.py @@ -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), + ), + ] diff --git a/generic/models.py b/generic/models.py index e55880f7..85a6d59e 100644 --- a/generic/models.py +++ b/generic/models.py @@ -437,13 +437,12 @@ class SeriesBase(models.Model): if series_number is not None: series_html = format_html( "Series {}{}
", - series_number + series_number, ) description = "" if self.description: description = format_html("{}
", self.description) - return format_html( """{}{} @@ -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, diff --git a/generic/templates/generic/exam_candidate_headers.html b/generic/templates/generic/exam_candidate_headers.html new file mode 100644 index 00000000..140534bc --- /dev/null +++ b/generic/templates/generic/exam_candidate_headers.html @@ -0,0 +1,4 @@ +Overview +\ Edit CIDs +\ Edit User +\ Edit Groups \ No newline at end of file diff --git a/generic/templates/generic/exam_cids_edit.html b/generic/templates/generic/exam_cids_edit.html index 2606d148..f37921f3 100644 --- a/generic/templates/generic/exam_cids_edit.html +++ b/generic/templates/generic/exam_cids_edit.html @@ -1,88 +1,87 @@ +{% include "generic/exam_candidate_headers.html" %} +

CID Users

+{% if groups %} + Users from the following groups are available: {% for group in groups %}{{group}}, {% endfor %} . +{% else %} + This exam is not associated with any user groups. Edit and add a group to enable user management. +{% endif %} +
    + {% for cid in current_cid_users %} - -

    CID Users

    - {% if groups %} - Users from the following groups are available: {% for group in groups %}{{group}}, {% endfor %} . - {% else %} - This exam is not associated with any user groups. Edit and add a group to enable user management. - {% endif %} -
      - {% for cid in current_cid_users %} - -
    1. {{cid.cid}} [{{cid.passcode}}] {{cid.name}} / - Email: {{cid.email}} -
    2. - - {% endfor %} - {% for cid in available_cid_users %} - -
    3. {{cid.cid}} [{{cid.passcode}}] {{cid.name}} / - Email: {{cid.email}} -
    4. - - {% endfor %} -
    +
  1. {{cid.cid}} [{{cid.passcode}}] {{cid.name}} / + Email: {{cid.email}} +
  2. + + {% endfor %} + {% for cid in available_cid_users %} + +
  3. {{cid.cid}} [{{cid.passcode}}] {{cid.name}} / + Email: {{cid.email}} +
  4. + + {% endfor %} +
{% comment %} Hide the toggle button if there are no users (to add or remove) {% endcomment %} - {% if current_cid_users or available_cid_users %} -

- {% endif %} - - - \ No newline at end of file + button { + padding: 0; + } + \ No newline at end of file diff --git a/generic/templates/generic/exam_groups_edit.html b/generic/templates/generic/exam_groups_edit.html new file mode 100644 index 00000000..97dcd919 --- /dev/null +++ b/generic/templates/generic/exam_groups_edit.html @@ -0,0 +1,17 @@ +{% extends exam.get_app_name|add:'/exams.html' %} + +{% block content %} +

Exam: {{exam}}

+ {% include "generic/exam_candidate_headers.html" %} + +

Groups

+ +
+ {% csrf_token %} + + {{ form }} +
+ +
+ +{% endblock content %} diff --git a/generic/templates/generic/exam_overview_headers.html b/generic/templates/generic/exam_overview_headers.html index c3f864a4..ac157f93 100644 --- a/generic/templates/generic/exam_overview_headers.html +++ b/generic/templates/generic/exam_overview_headers.html @@ -47,21 +47,24 @@ Markers: {% if can_edit %} -
+
Answer management -
-

Manage answers for this collection.

+
+
+ Manage answers for this collection. Please note these are permanant and cannot be undone. +
- Please note these are permanant and cannot be undone. +
+ Click here to show management options + +
- -
{% endif %} @@ -104,4 +107,13 @@ Author(s): {% for author in exam.author.all %} {{ author }}{% if not forloop.last %}, {% endif %} {% endfor %}
+{% block css %} + +{% endblock css %} + diff --git a/generic/templates/generic/exam_users_edit.html b/generic/templates/generic/exam_users_edit.html index 871f771e..a0c13c96 100644 --- a/generic/templates/generic/exam_users_edit.html +++ b/generic/templates/generic/exam_users_edit.html @@ -1,9 +1,9 @@ - +{% include "generic/exam_candidate_headers.html" %}

Users

{% if groups %} Users from the following groups are available: {% for group in groups %}{{group}}, {% endfor %} . {% else %} - This exam is not associated with any user groups. Edit and add a group to enable user management. + This exam is not associated with any user groups. Edit and add a group to enable user management. {% endif %}
    {% for user in current_user_users %} diff --git a/generic/urls.py b/generic/urls.py index 2978bd18..66c62f9d 100755 --- a/generic/urls.py +++ b/generic/urls.py @@ -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//cids", generic_exam_view.exam_cids, name="exam_cids"), + #path("exam//groups", generic_exam_view.exam_groups_edit, name="exam_groups_edit"), path( "exam//reset_answers", generic_exam_view.exam_reset_answers, diff --git a/generic/views.py b/generic/views.py index 4af25ba0..32d44bea 100644 --- a/generic/views.py +++ b/generic/views.py @@ -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 \ No newline at end of file + 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 \ No newline at end of file diff --git a/longs/forms.py b/longs/forms.py index 46c69ec2..f885538c 100755 --- a/longs/forms.py +++ b/longs/forms.py @@ -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 \ No newline at end of file diff --git a/longs/urls.py b/longs/urls.py index f24373b2..cb61c628 100755 --- a/longs/urls.py +++ b/longs/urls.py @@ -83,6 +83,7 @@ urlpatterns = [ name="mark", ), path("exam//markers", views.ExamMarkersUpdate.as_view(), name="exam_markers"), + path("exam//groups", views.ExamGroupsUpdate.as_view(), name="exam_groups_edit"), path("exam//authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"), path( "exam//question_json_refresh", diff --git a/longs/views.py b/longs/views.py index 6b602ea8..77044d1f 100755 --- a/longs/views.py +++ b/longs/views.py @@ -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") diff --git a/physics/forms.py b/physics/forms.py index 3048725d..dd1bd674 100755 --- a/physics/forms.py +++ b/physics/forms.py @@ -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 \ No newline at end of file diff --git a/physics/views.py b/physics/views.py index e9e42d6a..41741966 100644 --- a/physics/views.py +++ b/physics/views.py @@ -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 ): diff --git a/rapids/forms.py b/rapids/forms.py index 37b35b40..3917a639 100755 --- a/rapids/forms.py +++ b/rapids/forms.py @@ -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 \ No newline at end of file diff --git a/rapids/views.py b/rapids/views.py index 59440191..b8a5b8d3 100755 --- a/rapids/views.py +++ b/rapids/views.py @@ -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 diff --git a/sbas/forms.py b/sbas/forms.py index ed69fbf9..fd4e5752 100755 --- a/sbas/forms.py +++ b/sbas/forms.py @@ -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()) diff --git a/sbas/views.py b/sbas/views.py index 649fdb69..5c72cd99 100644 --- a/sbas/views.py +++ b/sbas/views.py @@ -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""" diff --git a/templates/exam_cids.html b/templates/exam_cids.html index a979f11e..18902e26 100644 --- a/templates/exam_cids.html +++ b/templates/exam_cids.html @@ -5,6 +5,7 @@ {% load thumbnail %}

    Exam: {{ exam }}

    + {% include "generic/exam_candidate_headers.html" %} {% include "generic/exam_cids.html" %}