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 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",
]
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
@@ -71,6 +71,7 @@ urlpatterns = [
name="exam_question_detail",
),
path("exam/<int:pk>/", views.GenericExamViews.exam_overview, name="exam_overview"),
path("exam/<int:pk>/authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"),
path(
"exam/<int:pk>/json_edit",
views.GenericExamViews.exam_json_edit,
+6 -1
View File
@@ -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"
+28
View File
@@ -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
@@ -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_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_authors' exam.id %}" title="Edit Exam Authors">Authors</a>
{% endif %}
{% 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>
+6
View File
@@ -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):
+6 -13
View File
@@ -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",
]
class ExamAuthorForm(ExamAuthorFormMixin):
class Meta(ExamAuthorFormMixin.Meta):
model = Exam
@@ -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 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
+1
View File
@@ -91,6 +91,7 @@ urlpatterns = [
name="exam_question_detail",
),
path("exam/<int:pk>/", views.GenericExamViews.exam_overview, name="exam_overview"),
path("exam/<int:pk>/authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"),
path(
"exam/<int:pk>/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 .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
+8 -3
View File
@@ -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"]
class ExamAuthorForm(ExamAuthorFormMixin):
class Meta(ExamAuthorFormMixin.Meta):
model = Exam
@@ -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 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
+1
View File
@@ -26,6 +26,7 @@ urlpatterns = [
name="exam_take_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(
"exam/<int:pk>/scores",
views.GenericExamViews.exam_scores_cid,
+6 -2
View File
@@ -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"
+5 -27
View File
@@ -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),
)
class ExamAuthorForm(ExamAuthorFormMixin):
class Meta(ExamAuthorFormMixin.Meta):
model = Exam
-1
View File
@@ -624,7 +624,6 @@ class Exam(ExamBase):
)
user_user_groups = models.ManyToManyField(
UserUserGroup,
blank=True,
+2 -3
View File
@@ -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
+8 -3
View File
@@ -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"]
class ExamAuthorForm(ExamAuthorFormMixin):
class Meta(ExamAuthorFormMixin.Meta):
model = Exam
@@ -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 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
+1
View File
@@ -26,6 +26,7 @@ urlpatterns = [
name="exam_take_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(
"exam/<int:pk>/scores",
views.GenericExamViews.exam_scores_cid,
+6 -2
View File
@@ -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