From 781b6bc1fe7ab93f1cbd053bb9796a70903558e7 Mon Sep 17 00:00:00 2001 From: Ross Date: Sat, 21 May 2022 22:09:37 +0100 Subject: [PATCH] . --- anatomy/forms.py | 22 +++++++------ anatomy/urls.py | 1 + anatomy/views.py | 7 +++- generic/forms.py | 28 ++++++++++++++++ .../generic/exam_overview_headers.html | 1 + generic/views.py | 6 ++++ longs/forms.py | 19 ++++------- longs/migrations/0062_auto_20220521_2135.py | 32 +++++++++++++++++++ longs/models.py | 25 ++++++++++++++- longs/urls.py | 1 + longs/views.py | 7 +++- physics/forms.py | 11 +++++-- physics/migrations/0022_auto_20220521_2135.py | 32 +++++++++++++++++++ .../0023_alter_exam_cid_user_groups.py | 19 +++++++++++ .../0024_alter_exam_cid_user_groups.py | 19 +++++++++++ physics/models.py | 27 ++++++++++++++-- physics/urls.py | 1 + physics/views.py | 8 +++-- rapids/forms.py | 32 +++---------------- rapids/models.py | 1 - rapids/views.py | 5 ++- sbas/forms.py | 11 +++++-- sbas/migrations/0014_auto_20220521_2135.py | 32 +++++++++++++++++++ sbas/models.py | 25 ++++++++++++++- sbas/urls.py | 1 + sbas/views.py | 8 +++-- 26 files changed, 311 insertions(+), 70 deletions(-) create mode 100644 longs/migrations/0062_auto_20220521_2135.py create mode 100644 physics/migrations/0022_auto_20220521_2135.py create mode 100644 physics/migrations/0023_alter_exam_cid_user_groups.py create mode 100644 physics/migrations/0024_alter_exam_cid_user_groups.py create mode 100644 sbas/migrations/0014_auto_20220521_2135.py diff --git a/anatomy/forms.py b/anatomy/forms.py index 58c9f976..47b4fb9a 100644 --- a/anatomy/forms.py +++ b/anatomy/forms.py @@ -204,15 +204,17 @@ class BodyPartForm(ModelForm): class ExamForm(ExamFormMixin, ModelForm): + class Meta(ExamFormMixin.Meta): + model = Exam + +class ExamAuthorForm(ModelForm): class Meta: model = Exam - fields = [ - "name", - "time_limit", - "open_access", - "authors_only", - "exam_mode", - "active", - "publish_results", - "archive", - ] \ No newline at end of file + fields = ["author"] + + def __init__(self, *args, **kwargs): + ModelForm.__init__(self, *args, **kwargs) + self.fields["author"] = ModelMultipleChoiceField( + queryset=User.objects.all(), + widget=FilteredSelectMultiple(verbose_name="Authors", is_stacked=False), + ) \ No newline at end of file diff --git a/anatomy/urls.py b/anatomy/urls.py index 9c669476..fb2210d9 100644 --- a/anatomy/urls.py +++ b/anatomy/urls.py @@ -71,6 +71,7 @@ urlpatterns = [ name="exam_question_detail", ), path("exam//", views.GenericExamViews.exam_overview, name="exam_overview"), + path("exam//authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"), path( "exam//json_edit", views.GenericExamViews.exam_json_edit, diff --git a/anatomy/views.py b/anatomy/views.py index 250ec6f9..36f005f2 100644 --- a/anatomy/views.py +++ b/anatomy/views.py @@ -31,6 +31,7 @@ from .forms import ( AnswerFormSet, AnswerUpdateFormSet, BodyPartForm, + ExamAuthorForm, ExaminationForm, MarkAnatomyQuestionForm, AnatomyQuestionForm, @@ -49,7 +50,7 @@ from .models import ( # IncorrectAnswers, ) from generic.models import CidUser, Examination -from generic.views import ExamCloneMixin, ExamViews, GenericViewBase +from generic.views import AuthorRequiredMixin, ExamCloneMixin, ExamViews, GenericViewBase from reversion.views import RevisionMixin from .decorators import user_is_author_or_anatomy_checker @@ -999,6 +1000,10 @@ class ExamViewSet(RevisionMixin, viewsets.ModelViewSet): return Exam.objects.filter(author__id=user.id) +class ExamAuthorUpdate(RevisionMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView): + model = Exam + form_class = ExamAuthorForm + class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView): model = Exam template_name = "exam_confirm_delete.html" diff --git a/generic/forms.py b/generic/forms.py index d562ef67..10ac3756 100755 --- a/generic/forms.py +++ b/generic/forms.py @@ -48,6 +48,22 @@ from sbas.models import Exam as SbasExam from sbas.models import Question as SbasQuestion class ExamFormMixin: + class Meta: + fields = [ + "name", + "time_limit", + "open_access", + "authors_only", + "exam_mode", + "include_history", + "active", + "publish_results", + "archive", + "cid_user_groups", + "user_user_groups", + #"author", + ] + def save(self, commit=True): instance = ModelForm.save(self, False) instance.save() @@ -56,6 +72,18 @@ class ExamFormMixin: return instance +class ExamAuthorFormMixin(ModelForm): + class Meta: + fields = ["author"] + + def __init__(self, *args, **kwargs): + ModelForm.__init__(self, *args, **kwargs) + self.fields["author"] = ModelMultipleChoiceField( + queryset=User.objects.all(), + widget=FilteredSelectMultiple(verbose_name="Authors", is_stacked=False), + ) + + class ExaminationForm(ModelForm): class Meta: model = Examination diff --git a/generic/templates/generic/exam_overview_headers.html b/generic/templates/generic/exam_overview_headers.html index 69edd309..f8df7d58 100644 --- a/generic/templates/generic/exam_overview_headers.html +++ b/generic/templates/generic/exam_overview_headers.html @@ -2,6 +2,7 @@ Edit \ Delete \ Clone + \ Authors {% endif %} {% if request.user.is_superuser %} \ Admin Edit diff --git a/generic/views.py b/generic/views.py index 0988e9e9..2cb1d00c 100644 --- a/generic/views.py +++ b/generic/views.py @@ -71,6 +71,12 @@ import plotly.express as px # from rad.views import get_question_and_content_type from django.db.models import Prefetch +class AuthorRequiredMixin(object): + def get_object(self, *args, **kwargs): + obj = super().get_object(*args, **kwargs) + if self.request.user not in obj.author.all(): + raise PermissionDenied() # or Http404 + return obj class CidManagerRequiredMixin(UserPassesTestMixin): def test_func(self): diff --git a/longs/forms.py b/longs/forms.py index b9a0540b..94b20fb3 100755 --- a/longs/forms.py +++ b/longs/forms.py @@ -7,7 +7,7 @@ from django.forms import ( CharField, ) from django.forms import inlineformset_factory -from generic.forms import ExamFormMixin +from generic.forms import ExamAuthorFormMixin, ExamFormMixin from longs.models import ( # Examination, @@ -267,16 +267,9 @@ LongSeriesImageFormSet = inlineformset_factory( class ExamForm(ExamFormMixin, ModelForm): - class Meta: + class Meta(ExamFormMixin.Meta): model = Exam - fields = [ - "name", - "time_limit", - "open_access", - "authors_only", - "exam_mode", - "active", - "publish_results", - "double_mark", - "archive", - ] \ No newline at end of file + +class ExamAuthorForm(ExamAuthorFormMixin): + class Meta(ExamAuthorFormMixin.Meta): + model = Exam \ No newline at end of file diff --git a/longs/migrations/0062_auto_20220521_2135.py b/longs/migrations/0062_auto_20220521_2135.py new file mode 100644 index 00000000..3cdb0a77 --- /dev/null +++ b/longs/migrations/0062_auto_20220521_2135.py @@ -0,0 +1,32 @@ +# Generated by Django 3.2.13 on 2022-05-21 20:35 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('generic', '0032_userprofile_registration_number'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('longs', '0061_exam_valid_user_users'), + ] + + operations = [ + migrations.AddField( + model_name='ciduseranswer', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='user_longs_user_answers', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='exam', + name='cid_user_groups', + field=models.ManyToManyField(blank=True, help_text='These groups define which candidates are able to be added to the exams/collection.', related_name='longs_cid_user_groups', to='generic.CidUserGroup'), + ), + migrations.AddField( + model_name='exam', + name='user_user_groups', + field=models.ManyToManyField(blank=True, help_text='These groups define which candidates are able to be added to the exams/collection.', related_name='longs_user_user_groups', to='generic.UserUserGroup'), + ), + ] diff --git a/longs/models.py b/longs/models.py index bec129d3..8ba3760b 100644 --- a/longs/models.py +++ b/longs/models.py @@ -28,7 +28,7 @@ from helpers.images import image_as_base64, pretty_print_dicom from anatomy.models import Modality -from generic.models import CidUser, Examination, Condition, Sign, ExamBase, Plane, Contrast, QuestionNote +from generic.models import CidUser, CidUserGroup, Examination, Condition, Sign, ExamBase, Plane, Contrast, QuestionNote, UserUserGroup # from generic.models import Examination, Site, Condition, Sign @@ -601,6 +601,21 @@ class Exam(ExamBase): settings.AUTH_USER_MODEL, blank=True, related_name="user_longs_exams" ) + cid_user_groups = models.ManyToManyField( + CidUserGroup, + blank=True, + help_text="These groups define which candidates are able to be added to the exams/collection.", + related_name="longs_cid_user_groups" + + ) + + user_user_groups = models.ManyToManyField( + UserUserGroup, + blank=True, + help_text="These groups define which candidates are able to be added to the exams/collection.", + related_name="longs_user_user_groups" + ) + def get_exam_question_json(self, question_id): q = get_object_or_404(Long, pk=question_id) @@ -722,6 +737,14 @@ class CidUserAnswer(models.Model): help_text="Candidate ID (limitied by BigIntegerField size)", ) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + blank=True, + null=True, + related_name="user_longs_user_answers", + ) + # Each user answer is associated with a particular exam exam = models.ForeignKey( Exam, related_name="cid_user_answers", on_delete=models.CASCADE, null=True diff --git a/longs/urls.py b/longs/urls.py index 6f1eb644..de101d8d 100755 --- a/longs/urls.py +++ b/longs/urls.py @@ -91,6 +91,7 @@ urlpatterns = [ name="exam_question_detail", ), path("exam//", views.GenericExamViews.exam_overview, name="exam_overview"), + path("exam//authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"), path( "exam//json_edit", views.GenericExamViews.exam_json_edit, diff --git a/longs/views.py b/longs/views.py index 6e8918af..6b3035be 100755 --- a/longs/views.py +++ b/longs/views.py @@ -25,6 +25,7 @@ from django.http import Http404, JsonResponse from django.http import HttpResponseRedirect, HttpResponse from .forms import ( + ExamAuthorForm, LongForm, LongSeriesForm, LongSeriesImageFormSet, @@ -74,7 +75,7 @@ from django.forms.models import model_to_dict from longs.forms import LongCreationDefaultForm from longs.models import LongCreationDefault -from generic.views import ExamCloneMixin, ExamViews +from generic.views import AuthorRequiredMixin, ExamCloneMixin, ExamViews from reversion.views import RevisionMixin import reversion @@ -1141,6 +1142,10 @@ class ExamUpdate( form.instance.author.add(self.request.user.id) return super().form_valid(form) +class ExamAuthorUpdate(RevisionMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView): + model = Exam + form_class = ExamAuthorForm + class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView): model = Exam diff --git a/physics/forms.py b/physics/forms.py index e6688827..44096dab 100755 --- a/physics/forms.py +++ b/physics/forms.py @@ -8,6 +8,8 @@ from django.forms import ( ) from django.forms import inlineformset_factory +from generic.forms import ExamAuthorFormMixin, ExamFormMixin + from .models import ( Question, CidUserAnswer, @@ -30,7 +32,10 @@ class CidUserAnswerForm(ModelForm): # This should be made generic? -class ExamForm(ModelForm): - class Meta: +class ExamForm(ExamFormMixin, ModelForm): + class Meta(ExamFormMixin.Meta): model = Exam - fields = ["name", "time_limit", "open_access", "exam_mode", "active", "archive"] \ No newline at end of file + +class ExamAuthorForm(ExamAuthorFormMixin): + class Meta(ExamAuthorFormMixin.Meta): + model = Exam \ No newline at end of file diff --git a/physics/migrations/0022_auto_20220521_2135.py b/physics/migrations/0022_auto_20220521_2135.py new file mode 100644 index 00000000..db8c6075 --- /dev/null +++ b/physics/migrations/0022_auto_20220521_2135.py @@ -0,0 +1,32 @@ +# Generated by Django 3.2.13 on 2022-05-21 20:35 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('generic', '0032_userprofile_registration_number'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('physics', '0021_exam_valid_user_users'), + ] + + operations = [ + migrations.AddField( + model_name='ciduseranswer', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='user_physics_user_answers', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='exam', + name='cid_user_groups', + field=models.ManyToManyField(blank=True, help_text='These groups define which candidates are able to be added to the exams/collection.', related_name='physics_cid_user_groups', to='generic.CidUserGroup'), + ), + migrations.AddField( + model_name='exam', + name='user_user_groups', + field=models.ManyToManyField(blank=True, help_text='These groups define which candidates are able to be added to the exams/collection.', related_name='physics_user_user_groups', to='generic.UserUserGroup'), + ), + ] diff --git a/physics/migrations/0023_alter_exam_cid_user_groups.py b/physics/migrations/0023_alter_exam_cid_user_groups.py new file mode 100644 index 00000000..9f53210c --- /dev/null +++ b/physics/migrations/0023_alter_exam_cid_user_groups.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.13 on 2022-05-21 20:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('generic', '0032_userprofile_registration_number'), + ('physics', '0022_auto_20220521_2135'), + ] + + operations = [ + migrations.AlterField( + model_name='exam', + name='cid_user_groups', + field=models.ManyToManyField(blank=True, help_text='These groups define which candidates are able to be added to the exams/collection.', related_name='physics_exam_cid_user_groups', to='generic.CidUserGroup'), + ), + ] diff --git a/physics/migrations/0024_alter_exam_cid_user_groups.py b/physics/migrations/0024_alter_exam_cid_user_groups.py new file mode 100644 index 00000000..cb63f06c --- /dev/null +++ b/physics/migrations/0024_alter_exam_cid_user_groups.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.13 on 2022-05-21 20:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('generic', '0032_userprofile_registration_number'), + ('physics', '0023_alter_exam_cid_user_groups'), + ] + + operations = [ + migrations.AlterField( + model_name='exam', + name='cid_user_groups', + field=models.ManyToManyField(blank=True, help_text='These groups define which candidates are able to be added to the exams/collection.', related_name='physics_cid_user_groups', to='generic.CidUserGroup'), + ), + ] diff --git a/physics/models.py b/physics/models.py index 8a859306..23ffc7f5 100644 --- a/physics/models.py +++ b/physics/models.py @@ -12,7 +12,7 @@ from django.utils.translation import gettext_lazy as _ from sortedm2m.fields import SortedManyToManyField -from generic.models import CidUser, ExamBase, QuestionNote +from generic.models import CidUser, CidUserGroup, ExamBase, QuestionNote, UserUserGroup import reversion @@ -131,7 +131,7 @@ class Question(models.Model): return [self.a, self.b, self.c, self.d, self.e] -@reversion.register +#@reversion.register class Exam(ExamBase): app_name = "physics" @@ -160,6 +160,21 @@ class Exam(ExamBase): settings.AUTH_USER_MODEL, blank=True, related_name="user_physics_exams" ) + cid_user_groups = models.ManyToManyField( + CidUserGroup, + blank=True, + help_text="These groups define which candidates are able to be added to the exams/collection.", + related_name="physics_cid_user_groups" + + ) + + user_user_groups = models.ManyToManyField( + UserUserGroup, + blank=True, + help_text="These groups define which candidates are able to be added to the exams/collection.", + related_name="physics_user_user_groups" + ) + def get_take_url(self): return reverse("physics:exam_start", kwargs={"pk": self.pk}) @@ -179,6 +194,14 @@ class CidUserAnswer(models.Model): cid = models.IntegerField(blank=True, null=True) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + blank=True, + null=True, + related_name="user_physics_user_answers", + ) + # Each user answer is associated with a particular exam exam = models.ForeignKey( Exam, related_name="cid_user_answers", on_delete=models.CASCADE, null=True diff --git a/physics/urls.py b/physics/urls.py index 304d3609..3a8fd54f 100644 --- a/physics/urls.py +++ b/physics/urls.py @@ -26,6 +26,7 @@ urlpatterns = [ name="exam_take_overview", ), path("exam//", views.GenericExamViews.exam_overview, name="exam_overview"), + path("exam//authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"), path( "exam//scores", views.GenericExamViews.exam_scores_cid, diff --git a/physics/views.py b/physics/views.py index d7e57147..1c1cb21f 100644 --- a/physics/views.py +++ b/physics/views.py @@ -42,14 +42,14 @@ import json import statistics import plotly.express as px -from generic.views import ExamCloneMixin, ExamViews, GenericViewBase +from generic.views import AuthorRequiredMixin, ExamCloneMixin, ExamViews, GenericViewBase from rest_framework import viewsets, permissions from rest_framework.pagination import PageNumberPagination from django.core.exceptions import PermissionDenied -from .forms import CidUserAnswerForm, ExamForm +from .forms import CidUserAnswerForm, ExamAuthorForm, ExamForm from .decorators import ( user_is_author_or_physics_checker, user_is_exam_author_or_physics_checker, @@ -391,6 +391,10 @@ class ExamClone(AuthorOrCheckerRequiredMixin, ExamCloneMixin, ExamCreate): """Clone exam view""" +class ExamAuthorUpdate(RevisionMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView): + model = Exam + form_class = ExamAuthorForm + class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView): model = Exam template_name = "exam_confirm_delete.html" diff --git a/rapids/forms.py b/rapids/forms.py index 430af0f8..751441b1 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 ExamFormMixin +from generic.forms import ExamAuthorFormMixin, ExamFormMixin from rapids.models import ( Abnormality, @@ -230,31 +230,9 @@ AnswerUpdateFormSet = inlineformset_factory( # This should be made generic? class ExamForm(ExamFormMixin, ModelForm): - class Meta: + class Meta(ExamFormMixin.Meta): model = Exam - fields = [ - "name", - "time_limit", - "open_access", - "authors_only", - "exam_mode", - "include_history", - "active", - "publish_results", - "archive", - "cid_user_groups", - "user_user_groups", - #"author", - ] -class ExamAuthorForm(ModelForm): - class Meta: - model = Exam - fields = ["author"] - - def __init__(self, *args, **kwargs): - ModelForm.__init__(self, *args, **kwargs) - self.fields["author"] = ModelMultipleChoiceField( - queryset=User.objects.all(), - widget=FilteredSelectMultiple(verbose_name="Authors", is_stacked=False), - ) \ No newline at end of file +class ExamAuthorForm(ExamAuthorFormMixin): + class Meta(ExamAuthorFormMixin.Meta): + model = Exam \ No newline at end of file diff --git a/rapids/models.py b/rapids/models.py index d9b5105d..6904b712 100644 --- a/rapids/models.py +++ b/rapids/models.py @@ -624,7 +624,6 @@ class Exam(ExamBase): ) - user_user_groups = models.ManyToManyField( UserUserGroup, blank=True, diff --git a/rapids/views.py b/rapids/views.py index 8714118f..fb53bd20 100755 --- a/rapids/views.py +++ b/rapids/views.py @@ -42,7 +42,7 @@ from .models import ( CidUserAnswer, ) -from generic.views import ExamCloneMixin, ExamViews, GenericViewBase +from generic.views import AuthorRequiredMixin, ExamCloneMixin, ExamViews, GenericViewBase from reversion.views import RevisionMixin @@ -793,8 +793,7 @@ class ExamUpdate( return super().form_valid(form) -#TODO: AUTHOR REQURIED (NOT AUTHORORCHECKER) -class ExamAuthorUpdate(RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView): +class ExamAuthorUpdate(RevisionMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView): model = Exam form_class = ExamAuthorForm diff --git a/sbas/forms.py b/sbas/forms.py index f1def530..a6801c5c 100755 --- a/sbas/forms.py +++ b/sbas/forms.py @@ -8,6 +8,8 @@ from django.forms import ( ) from django.forms import inlineformset_factory +from generic.forms import ExamAuthorFormMixin, ExamFormMixin + from .models import ( Question, CidUserAnswer, @@ -28,7 +30,10 @@ class CidUserAnswerForm(ModelForm): self.fields['answer'].required = False # This should be made generic? -class ExamForm(ModelForm): - class Meta: +class ExamForm(ExamFormMixin, ModelForm): + class Meta(ExamFormMixin.Meta): model = Exam - fields = ["name", "time_limit", "exam_mode", "active", "archive"] \ No newline at end of file + +class ExamAuthorForm(ExamAuthorFormMixin): + class Meta(ExamAuthorFormMixin.Meta): + model = Exam \ No newline at end of file diff --git a/sbas/migrations/0014_auto_20220521_2135.py b/sbas/migrations/0014_auto_20220521_2135.py new file mode 100644 index 00000000..d61656b1 --- /dev/null +++ b/sbas/migrations/0014_auto_20220521_2135.py @@ -0,0 +1,32 @@ +# Generated by Django 3.2.13 on 2022-05-21 20:35 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('generic', '0032_userprofile_registration_number'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('sbas', '0013_exam_valid_user_users'), + ] + + operations = [ + migrations.AddField( + model_name='ciduseranswer', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='user_sbas_user_answers', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='exam', + name='cid_user_groups', + field=models.ManyToManyField(blank=True, help_text='These groups define which candidates are able to be added to the exams/collection.', related_name='sba_cid_user_groups', to='generic.CidUserGroup'), + ), + migrations.AddField( + model_name='exam', + name='user_user_groups', + field=models.ManyToManyField(blank=True, help_text='These groups define which candidates are able to be added to the exams/collection.', related_name='sba_user_user_groups', to='generic.UserUserGroup'), + ), + ] diff --git a/sbas/models.py b/sbas/models.py index 89dd796b..51dce7a2 100644 --- a/sbas/models.py +++ b/sbas/models.py @@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _ from sortedm2m.fields import SortedManyToManyField -from generic.models import CidUser, ExamBase, QuestionNote +from generic.models import CidUser, CidUserGroup, ExamBase, QuestionNote, UserUserGroup import reversion @@ -170,6 +170,21 @@ class Exam(ExamBase): settings.AUTH_USER_MODEL, blank=True, related_name="user_sba_exams" ) + cid_user_groups = models.ManyToManyField( + CidUserGroup, + blank=True, + help_text="These groups define which candidates are able to be added to the exams/collection.", + related_name="sba_cid_user_groups" + + ) + + user_user_groups = models.ManyToManyField( + UserUserGroup, + blank=True, + help_text="These groups define which candidates are able to be added to the exams/collection.", + related_name="sba_user_user_groups" + ) + def get_take_url(self): return reverse("sbas:exam_start", kwargs={"pk": self.pk}) @@ -189,6 +204,14 @@ class CidUserAnswer(models.Model): cid = models.IntegerField(blank=True, null=True) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + blank=True, + null=True, + related_name="user_sbas_user_answers", + ) + # Each user answer is associated with a particular exam exam = models.ForeignKey( Exam, related_name="cid_user_answers", on_delete=models.CASCADE, null=True diff --git a/sbas/urls.py b/sbas/urls.py index ce1458de..43b500e7 100644 --- a/sbas/urls.py +++ b/sbas/urls.py @@ -26,6 +26,7 @@ urlpatterns = [ name="exam_take_overview", ), path("exam//", views.GenericExamViews.exam_overview, name="exam_overview"), + path("exam//authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"), path( "exam//scores", views.GenericExamViews.exam_scores_cid, diff --git a/sbas/views.py b/sbas/views.py index cdfc73e8..f0db9768 100644 --- a/sbas/views.py +++ b/sbas/views.py @@ -2,7 +2,7 @@ from reversion.views import RevisionMixin from generic.mixins import SuperuserRequiredMixin from django.views.generic.detail import DetailView from generic.models import CidUser -from sbas.forms import CidUserAnswerForm, ExamForm +from sbas.forms import CidUserAnswerForm, ExamAuthorForm, ExamForm from django.shortcuts import render, get_object_or_404, redirect from django.views.decorators.csrf import csrf_exempt from django import forms @@ -40,7 +40,7 @@ import json import statistics import plotly.express as px -from generic.views import ExamCloneMixin, ExamViews, GenericViewBase +from generic.views import AuthorRequiredMixin, ExamCloneMixin, ExamViews, GenericViewBase from rest_framework import viewsets, permissions from rest_framework.pagination import PageNumberPagination @@ -378,6 +378,10 @@ class ExamUpdate( # # return Exam.objects.filter(author__id=user.id) +class ExamAuthorUpdate(RevisionMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView): + model = Exam + form_class = ExamAuthorForm + class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView): model = Exam