This commit is contained in:
Ross
2022-05-21 22:09:37 +01:00
parent 851e681400
commit 781b6bc1fe
26 changed files with 311 additions and 70 deletions
+12 -10
View File
@@ -204,15 +204,17 @@ class BodyPartForm(ModelForm):
class ExamForm(ExamFormMixin, ModelForm): class ExamForm(ExamFormMixin, ModelForm):
class Meta(ExamFormMixin.Meta):
model = Exam
class ExamAuthorForm(ModelForm):
class Meta: class Meta:
model = Exam model = Exam
fields = [ fields = ["author"]
"name",
"time_limit", def __init__(self, *args, **kwargs):
"open_access", ModelForm.__init__(self, *args, **kwargs)
"authors_only", self.fields["author"] = ModelMultipleChoiceField(
"exam_mode", queryset=User.objects.all(),
"active", widget=FilteredSelectMultiple(verbose_name="Authors", is_stacked=False),
"publish_results", )
"archive",
]
+1
View File
@@ -71,6 +71,7 @@ urlpatterns = [
name="exam_question_detail", name="exam_question_detail",
), ),
path("exam/<int:pk>/", views.GenericExamViews.exam_overview, name="exam_overview"), path("exam/<int:pk>/", views.GenericExamViews.exam_overview, name="exam_overview"),
path("exam/<int:pk>/authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"),
path( path(
"exam/<int:pk>/json_edit", "exam/<int:pk>/json_edit",
views.GenericExamViews.exam_json_edit, views.GenericExamViews.exam_json_edit,
+6 -1
View File
@@ -31,6 +31,7 @@ from .forms import (
AnswerFormSet, AnswerFormSet,
AnswerUpdateFormSet, AnswerUpdateFormSet,
BodyPartForm, BodyPartForm,
ExamAuthorForm,
ExaminationForm, ExaminationForm,
MarkAnatomyQuestionForm, MarkAnatomyQuestionForm,
AnatomyQuestionForm, AnatomyQuestionForm,
@@ -49,7 +50,7 @@ from .models import (
# IncorrectAnswers, # IncorrectAnswers,
) )
from generic.models import CidUser, Examination 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 reversion.views import RevisionMixin
from .decorators import user_is_author_or_anatomy_checker 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) return Exam.objects.filter(author__id=user.id)
class ExamAuthorUpdate(RevisionMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView):
model = Exam
form_class = ExamAuthorForm
class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView): class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView):
model = Exam model = Exam
template_name = "exam_confirm_delete.html" template_name = "exam_confirm_delete.html"
+28
View File
@@ -48,6 +48,22 @@ from sbas.models import Exam as SbasExam
from sbas.models import Question as SbasQuestion from sbas.models import Question as SbasQuestion
class ExamFormMixin: 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): def save(self, commit=True):
instance = ModelForm.save(self, False) instance = ModelForm.save(self, False)
instance.save() instance.save()
@@ -56,6 +72,18 @@ class ExamFormMixin:
return instance 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 ExaminationForm(ModelForm):
class Meta: class Meta:
model = Examination model = Examination
@@ -2,6 +2,7 @@
<a href="{% url app_name|add:':exam_update' exam.id %}" title="Edit the Exam">Edit</a> <a href="{% url app_name|add:':exam_update' exam.id %}" title="Edit the Exam">Edit</a>
\ <a href="{% url app_name|add:':exam_delete' exam.id %}" title="Delete the Exam">Delete</a> \ <a href="{% url app_name|add:':exam_delete' exam.id %}" title="Delete the Exam">Delete</a>
\ <a href="{% url app_name|add:':exam_clone' exam.id %}" title="Clone the Exam">Clone</a> \ <a href="{% url app_name|add:':exam_clone' exam.id %}" title="Clone the Exam">Clone</a>
\ <a href="{% url app_name|add:':exam_authors' exam.id %}" title="Edit Exam Authors">Authors</a>
{% endif %} {% endif %}
{% if request.user.is_superuser %} {% if request.user.is_superuser %}
\ <a href="{% url 'admin:'|add:app_name|add:'_exam_change' exam.id %}" title="Edit the Exam using the admin interface">Admin Edit</a> \ <a href="{% url 'admin:'|add:app_name|add:'_exam_change' exam.id %}" title="Edit the Exam using the admin interface">Admin Edit</a>
+6
View File
@@ -71,6 +71,12 @@ import plotly.express as px
# from rad.views import get_question_and_content_type # from rad.views import get_question_and_content_type
from django.db.models import Prefetch 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): class CidManagerRequiredMixin(UserPassesTestMixin):
def test_func(self): def test_func(self):
+6 -13
View File
@@ -7,7 +7,7 @@ from django.forms import (
CharField, CharField,
) )
from django.forms import inlineformset_factory from django.forms import inlineformset_factory
from generic.forms import ExamFormMixin from generic.forms import ExamAuthorFormMixin, ExamFormMixin
from longs.models import ( from longs.models import (
# Examination, # Examination,
@@ -267,16 +267,9 @@ LongSeriesImageFormSet = inlineformset_factory(
class ExamForm(ExamFormMixin, ModelForm): class ExamForm(ExamFormMixin, ModelForm):
class Meta: class Meta(ExamFormMixin.Meta):
model = Exam
class ExamAuthorForm(ExamAuthorFormMixin):
class Meta(ExamAuthorFormMixin.Meta):
model = Exam model = Exam
fields = [
"name",
"time_limit",
"open_access",
"authors_only",
"exam_mode",
"active",
"publish_results",
"double_mark",
"archive",
]
@@ -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'),
),
]
+24 -1
View File
@@ -28,7 +28,7 @@ from helpers.images import image_as_base64, pretty_print_dicom
from anatomy.models import Modality 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 # 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" 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): def get_exam_question_json(self, question_id):
q = get_object_or_404(Long, pk=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)", 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 # Each user answer is associated with a particular exam
exam = models.ForeignKey( exam = models.ForeignKey(
Exam, related_name="cid_user_answers", on_delete=models.CASCADE, null=True Exam, related_name="cid_user_answers", on_delete=models.CASCADE, null=True
+1
View File
@@ -91,6 +91,7 @@ urlpatterns = [
name="exam_question_detail", name="exam_question_detail",
), ),
path("exam/<int:pk>/", views.GenericExamViews.exam_overview, name="exam_overview"), path("exam/<int:pk>/", views.GenericExamViews.exam_overview, name="exam_overview"),
path("exam/<int:pk>/authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"),
path( path(
"exam/<int:pk>/json_edit", "exam/<int:pk>/json_edit",
views.GenericExamViews.exam_json_edit, views.GenericExamViews.exam_json_edit,
+6 -1
View File
@@ -25,6 +25,7 @@ from django.http import Http404, JsonResponse
from django.http import HttpResponseRedirect, HttpResponse from django.http import HttpResponseRedirect, HttpResponse
from .forms import ( from .forms import (
ExamAuthorForm,
LongForm, LongForm,
LongSeriesForm, LongSeriesForm,
LongSeriesImageFormSet, LongSeriesImageFormSet,
@@ -74,7 +75,7 @@ from django.forms.models import model_to_dict
from longs.forms import LongCreationDefaultForm from longs.forms import LongCreationDefaultForm
from longs.models import LongCreationDefault from longs.models import LongCreationDefault
from generic.views import ExamCloneMixin, ExamViews from generic.views import AuthorRequiredMixin, ExamCloneMixin, ExamViews
from reversion.views import RevisionMixin from reversion.views import RevisionMixin
import reversion import reversion
@@ -1141,6 +1142,10 @@ class ExamUpdate(
form.instance.author.add(self.request.user.id) form.instance.author.add(self.request.user.id)
return super().form_valid(form) return super().form_valid(form)
class ExamAuthorUpdate(RevisionMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView):
model = Exam
form_class = ExamAuthorForm
class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView): class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView):
model = Exam model = Exam
+8 -3
View File
@@ -8,6 +8,8 @@ from django.forms import (
) )
from django.forms import inlineformset_factory from django.forms import inlineformset_factory
from generic.forms import ExamAuthorFormMixin, ExamFormMixin
from .models import ( from .models import (
Question, Question,
CidUserAnswer, CidUserAnswer,
@@ -30,7 +32,10 @@ class CidUserAnswerForm(ModelForm):
# This should be made generic? # This should be made generic?
class ExamForm(ModelForm): class ExamForm(ExamFormMixin, ModelForm):
class Meta: class Meta(ExamFormMixin.Meta):
model = Exam
class ExamAuthorForm(ExamAuthorFormMixin):
class Meta(ExamAuthorFormMixin.Meta):
model = Exam model = Exam
fields = ["name", "time_limit", "open_access", "exam_mode", "active", "archive"]
@@ -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'),
),
]
@@ -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'),
),
]
@@ -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'),
),
]
+25 -2
View File
@@ -12,7 +12,7 @@ from django.utils.translation import gettext_lazy as _
from sortedm2m.fields import SortedManyToManyField from sortedm2m.fields import SortedManyToManyField
from generic.models import CidUser, ExamBase, QuestionNote from generic.models import CidUser, CidUserGroup, ExamBase, QuestionNote, UserUserGroup
import reversion import reversion
@@ -131,7 +131,7 @@ class Question(models.Model):
return [self.a, self.b, self.c, self.d, self.e] return [self.a, self.b, self.c, self.d, self.e]
@reversion.register #@reversion.register
class Exam(ExamBase): class Exam(ExamBase):
app_name = "physics" app_name = "physics"
@@ -160,6 +160,21 @@ class Exam(ExamBase):
settings.AUTH_USER_MODEL, blank=True, related_name="user_physics_exams" 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): def get_take_url(self):
return reverse("physics:exam_start", kwargs={"pk": self.pk}) return reverse("physics:exam_start", kwargs={"pk": self.pk})
@@ -179,6 +194,14 @@ class CidUserAnswer(models.Model):
cid = models.IntegerField(blank=True, null=True) 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 # Each user answer is associated with a particular exam
exam = models.ForeignKey( exam = models.ForeignKey(
Exam, related_name="cid_user_answers", on_delete=models.CASCADE, null=True Exam, related_name="cid_user_answers", on_delete=models.CASCADE, null=True
+1
View File
@@ -26,6 +26,7 @@ urlpatterns = [
name="exam_take_overview", name="exam_take_overview",
), ),
path("exam/<int:pk>/", views.GenericExamViews.exam_overview, name="exam_overview"), path("exam/<int:pk>/", views.GenericExamViews.exam_overview, name="exam_overview"),
path("exam/<int:pk>/authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"),
path( path(
"exam/<int:pk>/scores", "exam/<int:pk>/scores",
views.GenericExamViews.exam_scores_cid, views.GenericExamViews.exam_scores_cid,
+6 -2
View File
@@ -42,14 +42,14 @@ import json
import statistics import statistics
import plotly.express as px 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 import viewsets, permissions
from rest_framework.pagination import PageNumberPagination from rest_framework.pagination import PageNumberPagination
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from .forms import CidUserAnswerForm, ExamForm from .forms import CidUserAnswerForm, ExamAuthorForm, ExamForm
from .decorators import ( from .decorators import (
user_is_author_or_physics_checker, user_is_author_or_physics_checker,
user_is_exam_author_or_physics_checker, user_is_exam_author_or_physics_checker,
@@ -391,6 +391,10 @@ class ExamClone(AuthorOrCheckerRequiredMixin, ExamCloneMixin, ExamCreate):
"""Clone exam view""" """Clone exam view"""
class ExamAuthorUpdate(RevisionMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView):
model = Exam
form_class = ExamAuthorForm
class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView): class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView):
model = Exam model = Exam
template_name = "exam_confirm_delete.html" template_name = "exam_confirm_delete.html"
+4 -26
View File
@@ -7,7 +7,7 @@ from django.forms import (
CharField, CharField,
) )
from django.forms import inlineformset_factory from django.forms import inlineformset_factory
from generic.forms import ExamFormMixin from generic.forms import ExamAuthorFormMixin, ExamFormMixin
from rapids.models import ( from rapids.models import (
Abnormality, Abnormality,
@@ -230,31 +230,9 @@ AnswerUpdateFormSet = inlineformset_factory(
# This should be made generic? # This should be made generic?
class ExamForm(ExamFormMixin, ModelForm): class ExamForm(ExamFormMixin, ModelForm):
class Meta: class Meta(ExamFormMixin.Meta):
model = Exam 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 ExamAuthorForm(ExamAuthorFormMixin):
class Meta: class Meta(ExamAuthorFormMixin.Meta):
model = Exam 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),
)
-1
View File
@@ -624,7 +624,6 @@ class Exam(ExamBase):
) )
user_user_groups = models.ManyToManyField( user_user_groups = models.ManyToManyField(
UserUserGroup, UserUserGroup,
blank=True, blank=True,
+2 -3
View File
@@ -42,7 +42,7 @@ from .models import (
CidUserAnswer, CidUserAnswer,
) )
from generic.views import ExamCloneMixin, ExamViews, GenericViewBase from generic.views import AuthorRequiredMixin, ExamCloneMixin, ExamViews, GenericViewBase
from reversion.views import RevisionMixin from reversion.views import RevisionMixin
@@ -793,8 +793,7 @@ class ExamUpdate(
return super().form_valid(form) return super().form_valid(form)
#TODO: AUTHOR REQURIED (NOT AUTHORORCHECKER) class ExamAuthorUpdate(RevisionMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView):
class ExamAuthorUpdate(RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView):
model = Exam model = Exam
form_class = ExamAuthorForm form_class = ExamAuthorForm
+8 -3
View File
@@ -8,6 +8,8 @@ from django.forms import (
) )
from django.forms import inlineformset_factory from django.forms import inlineformset_factory
from generic.forms import ExamAuthorFormMixin, ExamFormMixin
from .models import ( from .models import (
Question, Question,
CidUserAnswer, CidUserAnswer,
@@ -28,7 +30,10 @@ class CidUserAnswerForm(ModelForm):
self.fields['answer'].required = False self.fields['answer'].required = False
# This should be made generic? # This should be made generic?
class ExamForm(ModelForm): class ExamForm(ExamFormMixin, ModelForm):
class Meta: class Meta(ExamFormMixin.Meta):
model = Exam
class ExamAuthorForm(ExamAuthorFormMixin):
class Meta(ExamAuthorFormMixin.Meta):
model = Exam model = Exam
fields = ["name", "time_limit", "exam_mode", "active", "archive"]
@@ -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'),
),
]
+24 -1
View File
@@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
from sortedm2m.fields import SortedManyToManyField from sortedm2m.fields import SortedManyToManyField
from generic.models import CidUser, ExamBase, QuestionNote from generic.models import CidUser, CidUserGroup, ExamBase, QuestionNote, UserUserGroup
import reversion import reversion
@@ -170,6 +170,21 @@ class Exam(ExamBase):
settings.AUTH_USER_MODEL, blank=True, related_name="user_sba_exams" 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): def get_take_url(self):
return reverse("sbas:exam_start", kwargs={"pk": self.pk}) return reverse("sbas:exam_start", kwargs={"pk": self.pk})
@@ -189,6 +204,14 @@ class CidUserAnswer(models.Model):
cid = models.IntegerField(blank=True, null=True) 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 # Each user answer is associated with a particular exam
exam = models.ForeignKey( exam = models.ForeignKey(
Exam, related_name="cid_user_answers", on_delete=models.CASCADE, null=True Exam, related_name="cid_user_answers", on_delete=models.CASCADE, null=True
+1
View File
@@ -26,6 +26,7 @@ urlpatterns = [
name="exam_take_overview", name="exam_take_overview",
), ),
path("exam/<int:pk>/", views.GenericExamViews.exam_overview, name="exam_overview"), path("exam/<int:pk>/", views.GenericExamViews.exam_overview, name="exam_overview"),
path("exam/<int:pk>/authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"),
path( path(
"exam/<int:pk>/scores", "exam/<int:pk>/scores",
views.GenericExamViews.exam_scores_cid, views.GenericExamViews.exam_scores_cid,
+6 -2
View File
@@ -2,7 +2,7 @@ from reversion.views import RevisionMixin
from generic.mixins import SuperuserRequiredMixin from generic.mixins import SuperuserRequiredMixin
from django.views.generic.detail import DetailView from django.views.generic.detail import DetailView
from generic.models import CidUser 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.shortcuts import render, get_object_or_404, redirect
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django import forms from django import forms
@@ -40,7 +40,7 @@ import json
import statistics import statistics
import plotly.express as px 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 import viewsets, permissions
from rest_framework.pagination import PageNumberPagination from rest_framework.pagination import PageNumberPagination
@@ -378,6 +378,10 @@ class ExamUpdate(
# #
# return Exam.objects.filter(author__id=user.id) # return Exam.objects.filter(author__id=user.id)
class ExamAuthorUpdate(RevisionMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView):
model = Exam
form_class = ExamAuthorForm
class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView): class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView):
model = Exam model = Exam