.
This commit is contained in:
+12
-10
@@ -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",
|
|
||||||
]
|
|
||||||
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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
|
model = Exam
|
||||||
fields = [
|
|
||||||
"name",
|
class ExamAuthorForm(ExamAuthorFormMixin):
|
||||||
"time_limit",
|
class Meta(ExamAuthorFormMixin.Meta):
|
||||||
"open_access",
|
model = Exam
|
||||||
"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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
|
|||||||
+5
-27
@@ -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),
|
|
||||||
)
|
|
||||||
@@ -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
@@ -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
@@ -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
|
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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user