add markers to exams (move away from group based to content based permissions)
This commit is contained in:
+6
-2
@@ -10,7 +10,7 @@ from django.forms import inlineformset_factory
|
|||||||
from django.contrib.postgres.forms import SimpleArrayField, SplitArrayField , SplitArrayWidget
|
from django.contrib.postgres.forms import SimpleArrayField, SplitArrayField , SplitArrayWidget
|
||||||
|
|
||||||
|
|
||||||
from generic.forms import ExamAuthorFormMixin, ExamFormMixin
|
from generic.forms import ExamAuthorFormMixin, ExamFormMixin, ExamMarkerFormMixin
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Answer,
|
Answer,
|
||||||
@@ -231,4 +231,8 @@ class ExamForm(ExamFormMixin, ModelForm):
|
|||||||
|
|
||||||
class ExamAuthorForm(ExamAuthorFormMixin):
|
class ExamAuthorForm(ExamAuthorFormMixin):
|
||||||
class Meta(ExamAuthorFormMixin.Meta):
|
class Meta(ExamAuthorFormMixin.Meta):
|
||||||
model = Exam
|
model = Exam
|
||||||
|
|
||||||
|
class ExamMarkerForm(ExamMarkerFormMixin):
|
||||||
|
class Meta(ExamMarkerFormMixin.Meta):
|
||||||
|
model = Exam
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 5.0.2 on 2024-08-05 09:43
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('anatomy', '0018_alter_exam_end_date_alter_exam_exam_results_emailed_and_more'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='exam',
|
||||||
|
name='markers',
|
||||||
|
field=models.ManyToManyField(blank=True, help_text='Authorised markers for the exam', related_name='anatomy_exam_markers', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -387,6 +387,13 @@ class Exam(ExamBase):
|
|||||||
related_name="anatomy_exam_author",
|
related_name="anatomy_exam_author",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
markers = models.ManyToManyField(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
blank=True,
|
||||||
|
help_text="Authorised markers for the exam",
|
||||||
|
related_name="anatomy_exam_markers",
|
||||||
|
)
|
||||||
|
|
||||||
valid_cid_users = models.ManyToManyField(
|
valid_cid_users = models.ManyToManyField(
|
||||||
CidUser, blank=True, related_name="anatomy_exams"
|
CidUser, blank=True, related_name="anatomy_exams"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ urlpatterns = [
|
|||||||
path(
|
path(
|
||||||
"exam/<int:exam_pk>/<int:sk>/mark/review", views.mark_review, name="mark_review"
|
"exam/<int:exam_pk>/<int:sk>/mark/review", views.mark_review, name="mark_review"
|
||||||
),
|
),
|
||||||
|
path("exam/<int:pk>/markers", views.ExamMarkersUpdate.as_view(), name="exam_markers"),
|
||||||
path("exam/<int:pk>/authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"),
|
path("exam/<int:pk>/authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"),
|
||||||
path("exam/create", views.ExamCreate.as_view(), name="exam_create"),
|
path("exam/create", views.ExamCreate.as_view(), name="exam_create"),
|
||||||
path("exam/<int:exam_id>/clone", views.ExamClone.as_view(), name="exam_clone"),
|
path("exam/<int:exam_id>/clone", views.ExamClone.as_view(), name="exam_clone"),
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ from .forms import (
|
|||||||
AnswerUpdateFormSet,
|
AnswerUpdateFormSet,
|
||||||
BodyPartForm,
|
BodyPartForm,
|
||||||
ExamAuthorForm,
|
ExamAuthorForm,
|
||||||
|
ExamMarkerForm,
|
||||||
ExaminationForm,
|
ExaminationForm,
|
||||||
MarkAnatomyQuestionForm,
|
MarkAnatomyQuestionForm,
|
||||||
AnatomyQuestionForm,
|
AnatomyQuestionForm,
|
||||||
@@ -836,6 +837,14 @@ class ExamAuthorUpdate(RevisionMixin, CheckCanEditMixin, LoginRequiredMixin, Aut
|
|||||||
|
|
||||||
template_name = "author_form.html"
|
template_name = "author_form.html"
|
||||||
|
|
||||||
|
class ExamMarkersUpdate(
|
||||||
|
RevisionMixin, CheckCanEditMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView
|
||||||
|
):
|
||||||
|
model = Exam
|
||||||
|
form_class = ExamMarkerForm
|
||||||
|
|
||||||
|
template_name = "markers_form.html"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
GenericViews = GenericViewBase("anatomy", AnatomyQuestion, UserAnswer, Exam)
|
GenericViews = GenericViewBase("anatomy", AnatomyQuestion, UserAnswer, Exam)
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 5.0.2 on 2024-08-05 09:43
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('atlas', '0057_alter_caseprior_prior_case'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='casecollection',
|
||||||
|
name='markers',
|
||||||
|
field=models.ManyToManyField(blank=True, help_text='Authorised markers for the exam', related_name='casecollection_markers', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -780,6 +780,13 @@ class CaseCollection(ExamOrCollectionGenericBase):
|
|||||||
related_name="casecollection_authored_cases",
|
related_name="casecollection_authored_cases",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
markers = models.ManyToManyField(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
blank=True,
|
||||||
|
help_text="Authorised markers for the exam",
|
||||||
|
related_name="casecollection_markers",
|
||||||
|
)
|
||||||
|
|
||||||
valid_cid_users = models.ManyToManyField(
|
valid_cid_users = models.ManyToManyField(
|
||||||
CidUser, blank=True, related_name="casecollection_exams"
|
CidUser, blank=True, related_name="casecollection_exams"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -185,6 +185,17 @@ class ExamAuthorFormMixin(ModelForm):
|
|||||||
widget=FilteredSelectMultiple(verbose_name="Authors", is_stacked=False),
|
widget=FilteredSelectMultiple(verbose_name="Authors", is_stacked=False),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class ExamMarkerFormMixin(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
fields = ["markers"]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
ModelForm.__init__(self, *args, **kwargs)
|
||||||
|
self.fields["markers"] = ModelMultipleChoiceField(
|
||||||
|
queryset=User.objects.all(),
|
||||||
|
widget=FilteredSelectMultiple(verbose_name="Markers", is_stacked=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ExaminationForm(ModelForm):
|
class ExaminationForm(ModelForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
\ <a href="{% url exam.get_app_name|add:':exam_delete' exam.id %}" title="Delete the Exam">Delete</a>
|
\ <a href="{% url exam.get_app_name|add:':exam_delete' exam.id %}" title="Delete the Exam">Delete</a>
|
||||||
\ <a href="{% url exam.get_app_name|add:':exam_clone' exam.id %}" title="Clone the Exam">Clone</a>
|
\ <a href="{% url exam.get_app_name|add:':exam_clone' exam.id %}" title="Clone the Exam">Clone</a>
|
||||||
\ <a href="{% url exam.get_app_name|add:':exam_authors' exam.id %}" title="Edit Exam Authors">Authors</a>
|
\ <a href="{% url exam.get_app_name|add:':exam_authors' exam.id %}" title="Edit Exam Authors">Authors</a>
|
||||||
|
\ <a href="{% url exam.get_app_name|add:':exam_markers' exam.id %}" title="Edit Exam Markers">Markers</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if can_edit and request.user.is_superuser %}
|
{% if can_edit and request.user.is_superuser %}
|
||||||
\
|
\
|
||||||
|
|||||||
+6
-2
@@ -8,7 +8,7 @@ from django.forms import (
|
|||||||
CharField,
|
CharField,
|
||||||
)
|
)
|
||||||
from django.forms import inlineformset_factory
|
from django.forms import inlineformset_factory
|
||||||
from generic.forms import ExamAuthorFormMixin, ExamFormMixin
|
from generic.forms import ExamAuthorFormMixin, ExamFormMixin, ExamMarkerFormMixin
|
||||||
|
|
||||||
from longs.models import (
|
from longs.models import (
|
||||||
# Examination,
|
# Examination,
|
||||||
@@ -313,4 +313,8 @@ class ExamForm(ExamFormMixin, ModelForm):
|
|||||||
class ExamAuthorForm(ExamAuthorFormMixin):
|
class ExamAuthorForm(ExamAuthorFormMixin):
|
||||||
class Meta(ExamAuthorFormMixin.Meta):
|
class Meta(ExamAuthorFormMixin.Meta):
|
||||||
model = Exam
|
model = Exam
|
||||||
#fields = ExamAuthorFormMixin.Meta.fields.append("double_mark")
|
#fields = ExamAuthorFormMixin.Meta.fields.append("double_mark")
|
||||||
|
|
||||||
|
class ExamMarkerForm(ExamMarkerFormMixin):
|
||||||
|
class Meta(ExamMarkerFormMixin.Meta):
|
||||||
|
model = Exam
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 5.0.2 on 2024-08-05 09:43
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('longs', '0025_remove_answermarks_candidate_feedback_and_more'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='exam',
|
||||||
|
name='markers',
|
||||||
|
field=models.ManyToManyField(blank=True, help_text='Authorised markers for the exam', related_name='long_exam_markers', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -445,6 +445,13 @@ class Exam(ExamBase):
|
|||||||
help_text="Author of exam",
|
help_text="Author of exam",
|
||||||
related_name="long_exam_author",
|
related_name="long_exam_author",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
markers = models.ManyToManyField(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
blank=True,
|
||||||
|
help_text="Authorised markers for the exam",
|
||||||
|
related_name="long_exam_markers",
|
||||||
|
)
|
||||||
|
|
||||||
double_mark = models.BooleanField(
|
double_mark = models.BooleanField(
|
||||||
default=True, help_text="Defines if an exam is expected to be double marked"
|
default=True, help_text="Defines if an exam is expected to be double marked"
|
||||||
|
|||||||
@@ -176,7 +176,6 @@ def test_exams(db, client):
|
|||||||
|
|
||||||
active_exams_cid = client.get(active_exams_url)
|
active_exams_cid = client.get(active_exams_url)
|
||||||
|
|
||||||
print(cid_user)
|
|
||||||
assert active_exams_cid.status_code == 200
|
assert active_exams_cid.status_code == 200
|
||||||
assert len(json.loads(active_exams_cid.content)["exams"]) == 1
|
assert len(json.loads(active_exams_cid.content)["exams"]) == 1
|
||||||
|
|
||||||
@@ -202,7 +201,6 @@ def test_exams(db, client):
|
|||||||
assert exam_metadata["name"] == "Cid Exam"
|
assert exam_metadata["name"] == "Cid Exam"
|
||||||
assert exam_metadata["exam_active"] == True
|
assert exam_metadata["exam_active"] == True
|
||||||
assert exam_metadata["exam_mode"] == True
|
assert exam_metadata["exam_mode"] == True
|
||||||
print(f"{exam_metadata=}")
|
|
||||||
|
|
||||||
res = client.get(exam_metadata["url"])
|
res = client.get(exam_metadata["url"])
|
||||||
assert res.status_code == 200
|
assert res.status_code == 200
|
||||||
@@ -224,7 +222,6 @@ def test_exams(db, client):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for q in exam_json["question_requests"]:
|
for q in exam_json["question_requests"]:
|
||||||
print("check question:", q)
|
|
||||||
# check redirects to the json file work
|
# check redirects to the json file work
|
||||||
# TODO: sort out testing of the saved JSON files
|
# TODO: sort out testing of the saved JSON files
|
||||||
try:
|
try:
|
||||||
@@ -258,8 +255,6 @@ def test_exams(db, client):
|
|||||||
|
|
||||||
assert question_json_res.redirect_chain[1][0].endswith(f"{q}.json")
|
assert question_json_res.redirect_chain[1][0].endswith(f"{q}.json")
|
||||||
|
|
||||||
print(cid_user, testing_cid_user)
|
|
||||||
|
|
||||||
if testing_cid_user:
|
if testing_cid_user:
|
||||||
question_json_unbased_res = client.get(
|
question_json_unbased_res = client.get(
|
||||||
reverse(
|
reverse(
|
||||||
@@ -281,7 +276,6 @@ def test_exams(db, client):
|
|||||||
"sk": q,
|
"sk": q,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
print(test)
|
|
||||||
question_json_unbased_res = client.get(
|
question_json_unbased_res = client.get(
|
||||||
|
|
||||||
reverse(
|
reverse(
|
||||||
@@ -516,8 +510,6 @@ def test_exams(db, client):
|
|||||||
|
|
||||||
assert len(answer_lis) == len(valid_answers_by_question)
|
assert len(answer_lis) == len(valid_answers_by_question)
|
||||||
|
|
||||||
pprint(valid_answers_by_question)
|
|
||||||
print(cid_user)
|
|
||||||
|
|
||||||
for n in range(
|
for n in range(
|
||||||
len(valid_answers_by_question)
|
len(valid_answers_by_question)
|
||||||
@@ -526,7 +518,6 @@ def test_exams(db, client):
|
|||||||
#n = str(n + 1) # questions are not zero indexed (lists are)
|
#n = str(n + 1) # questions are not zero indexed (lists are)
|
||||||
|
|
||||||
for qidn, section_title in sections:
|
for qidn, section_title in sections:
|
||||||
print(n, qidn)
|
|
||||||
answer = valid_answers_by_question[n][qidn]
|
answer = valid_answers_by_question[n][qidn]
|
||||||
|
|
||||||
li = lis.find("li", attrs={"data-qidn": qidn})
|
li = lis.find("li", attrs={"data-qidn": qidn})
|
||||||
@@ -558,6 +549,19 @@ def test_exams(db, client):
|
|||||||
assert "Not answered" in str(answer_lis[-1])
|
assert "Not answered" in str(answer_lis[-1])
|
||||||
assert str(answer_lis[-1]).count("Not answered") == 5
|
assert str(answer_lis[-1]).count("Not answered") == 5
|
||||||
|
|
||||||
|
|
||||||
|
## Load the mark overview page
|
||||||
|
#mark_overview_res = client.get(reverse(
|
||||||
|
# f"{exam.app_name}:mark_overview",
|
||||||
|
# kwargs={"pk": exam.pk},
|
||||||
|
# ))
|
||||||
|
#print(mark_overview_res.content)
|
||||||
|
#mark_overview_soup = BeautifulSoup(mark_overview_res.content, "html.parser")
|
||||||
|
|
||||||
|
#to_mark = mark_overview_soup.find("ul", {"id": "question-mark-list"}).find_all("li")
|
||||||
|
#assert len(to_mark) == 5
|
||||||
|
|
||||||
|
|
||||||
# Publish the results!
|
# Publish the results!
|
||||||
exam.publish_results = True
|
exam.publish_results = True
|
||||||
exam.save()
|
exam.save()
|
||||||
@@ -594,8 +598,6 @@ def test_exams(db, client):
|
|||||||
|
|
||||||
assert answer["ans"] in str(li) # Check that the saved answer is shown
|
assert answer["ans"] in str(li) # Check that the saved answer is shown
|
||||||
|
|
||||||
print(cid_exam_scores_soup.find("div", {"class": "score-overview"}))
|
|
||||||
|
|
||||||
# For long cases an unmarked answers is given a score of 4
|
# For long cases an unmarked answers is given a score of 4
|
||||||
# Each user needs marking individually
|
# Each user needs marking individually
|
||||||
assert (
|
assert (
|
||||||
@@ -609,10 +611,8 @@ def test_exams(db, client):
|
|||||||
# For longs this needs to be done for each user
|
# For longs this needs to be done for each user
|
||||||
question: Question
|
question: Question
|
||||||
for question in exam.exam_questions.all():
|
for question in exam.exam_questions.all():
|
||||||
print(question)
|
|
||||||
|
|
||||||
answers = question.get_unmarked_user_answers()
|
answers = question.get_unmarked_user_answers()
|
||||||
print(answers)
|
|
||||||
for answer in answers:
|
for answer in answers:
|
||||||
answer.score = UserAnswer.ScoreOptions.EIGHT
|
answer.score = UserAnswer.ScoreOptions.EIGHT
|
||||||
answer.save()
|
answer.save()
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ urlpatterns = [
|
|||||||
views.mark,
|
views.mark,
|
||||||
name="mark",
|
name="mark",
|
||||||
),
|
),
|
||||||
|
path("exam/<int:pk>/markers", views.ExamMarkersUpdate.as_view(), name="exam_markers"),
|
||||||
path("exam/<int:pk>/authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"),
|
path("exam/<int:pk>/authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"),
|
||||||
path(
|
path(
|
||||||
"exam/<int:pk>/question_json_refresh",
|
"exam/<int:pk>/question_json_refresh",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from django.http import HttpResponseRedirect, HttpResponse
|
|||||||
|
|
||||||
from .forms import (
|
from .forms import (
|
||||||
ExamAuthorForm,
|
ExamAuthorForm,
|
||||||
|
ExamMarkerForm,
|
||||||
LongForm,
|
LongForm,
|
||||||
LongSeriesForm,
|
LongSeriesForm,
|
||||||
LongSeriesImageFormSet,
|
LongSeriesImageFormSet,
|
||||||
@@ -977,6 +978,14 @@ class ExamAuthorUpdate(
|
|||||||
form_class = ExamAuthorForm
|
form_class = ExamAuthorForm
|
||||||
template_name = "author_form.html"
|
template_name = "author_form.html"
|
||||||
|
|
||||||
|
class ExamMarkersUpdate(
|
||||||
|
RevisionMixin, CheckCanEditMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView
|
||||||
|
):
|
||||||
|
model = Exam
|
||||||
|
form_class = ExamMarkerForm
|
||||||
|
|
||||||
|
template_name = "markers_form.html"
|
||||||
|
|
||||||
|
|
||||||
# class ExamViewSet(RevisionMixin, viewsets.ModelViewSet):
|
# class ExamViewSet(RevisionMixin, viewsets.ModelViewSet):
|
||||||
# queryset = Exam.objects.all().order_by("name")
|
# queryset = Exam.objects.all().order_by("name")
|
||||||
|
|||||||
+5
-1
@@ -8,7 +8,7 @@ from django.forms import (
|
|||||||
)
|
)
|
||||||
from django.forms import inlineformset_factory
|
from django.forms import inlineformset_factory
|
||||||
|
|
||||||
from generic.forms import ExamAuthorFormMixin, ExamFormMixin
|
from generic.forms import ExamAuthorFormMixin, ExamFormMixin, ExamMarkerFormMixin
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Question,
|
Question,
|
||||||
@@ -38,4 +38,8 @@ class ExamForm(ExamFormMixin, ModelForm):
|
|||||||
|
|
||||||
class ExamAuthorForm(ExamAuthorFormMixin):
|
class ExamAuthorForm(ExamAuthorFormMixin):
|
||||||
class Meta(ExamAuthorFormMixin.Meta):
|
class Meta(ExamAuthorFormMixin.Meta):
|
||||||
|
model = Exam
|
||||||
|
|
||||||
|
class ExamMarkerForm(ExamMarkerFormMixin):
|
||||||
|
class Meta(ExamMarkerFormMixin.Meta):
|
||||||
model = Exam
|
model = Exam
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 5.0.2 on 2024-08-05 09:43
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('physics', '0011_alter_exam_end_date_alter_exam_exam_results_emailed_and_more'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='exam',
|
||||||
|
name='markers',
|
||||||
|
field=models.ManyToManyField(blank=True, help_text='Authorised markers for the exam', related_name='physics_exam_markers', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -167,6 +167,13 @@ class Exam(ExamBase):
|
|||||||
related_name="physics_exam_author",
|
related_name="physics_exam_author",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
markers = models.ManyToManyField(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
blank=True,
|
||||||
|
help_text="Authorised markers for the exam",
|
||||||
|
related_name="physics_exam_markers",
|
||||||
|
)
|
||||||
|
|
||||||
valid_cid_users = models.ManyToManyField(
|
valid_cid_users = models.ManyToManyField(
|
||||||
CidUser, blank=True, related_name="physics_exams"
|
CidUser, blank=True, related_name="physics_exams"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ urlpatterns.extend(
|
|||||||
views.exam_take_overview,
|
views.exam_take_overview,
|
||||||
name="exam_take_overview_user",
|
name="exam_take_overview_user",
|
||||||
),
|
),
|
||||||
|
path("exam/<int:pk>/markers", views.ExamMarkersUpdate.as_view(), name="exam_markers"),
|
||||||
path(
|
path(
|
||||||
"exam/<int:pk>/authors",
|
"exam/<int:pk>/authors",
|
||||||
views.ExamAuthorUpdate.as_view(),
|
views.ExamAuthorUpdate.as_view(),
|
||||||
|
|||||||
+8
-1
@@ -58,7 +58,7 @@ from rest_framework.pagination import PageNumberPagination
|
|||||||
|
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
|
|
||||||
from .forms import UserAnswerForm, ExamAuthorForm, ExamForm
|
from .forms import ExamMarkerForm, UserAnswerForm, 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,
|
||||||
@@ -359,6 +359,13 @@ class ExamAuthorUpdate(
|
|||||||
form_class = ExamAuthorForm
|
form_class = ExamAuthorForm
|
||||||
template_name = "author_form.html"
|
template_name = "author_form.html"
|
||||||
|
|
||||||
|
class ExamMarkersUpdate(
|
||||||
|
RevisionMixin, CheckCanEditMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView
|
||||||
|
):
|
||||||
|
model = Exam
|
||||||
|
form_class = ExamMarkerForm
|
||||||
|
|
||||||
|
template_name = "markers_form.html"
|
||||||
|
|
||||||
class QuestionView(
|
class QuestionView(
|
||||||
LoginRequiredMixin, SingleTableMixin, FilterView, AuthorOrCheckerRequiredMixin
|
LoginRequiredMixin, SingleTableMixin, FilterView, AuthorOrCheckerRequiredMixin
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import pytest
|
|||||||
|
|
||||||
from generic.models import CidUser, CidUserGroup, UserUserGroup, ExamBase
|
from generic.models import CidUser, CidUserGroup, UserUserGroup, ExamBase
|
||||||
from generic.views import ExamViews
|
from generic.views import ExamViews
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User, Group
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rich.pretty import pprint
|
from rich.pretty import pprint
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ def AssertNotFound(client, url):
|
|||||||
), f"Response content: {response.content}"
|
), f"Response content: {response.content}"
|
||||||
|
|
||||||
|
|
||||||
def create_cid_user_and_groups(db):
|
def create_cid_user_and_groups(db, marker=None):
|
||||||
group1 = CidUserGroup.objects.create(name="Group1")
|
group1 = CidUserGroup.objects.create(name="Group1")
|
||||||
group2 = CidUserGroup.objects.create(name="Group2")
|
group2 = CidUserGroup.objects.create(name="Group2")
|
||||||
|
|
||||||
@@ -43,6 +43,10 @@ def create_cid_user_and_groups(db):
|
|||||||
usergroup2 = UserUserGroup.objects.create(name="Group2")
|
usergroup2 = UserUserGroup.objects.create(name="Group2")
|
||||||
usergroup2.users.add(user2)
|
usergroup2.users.add(user2)
|
||||||
|
|
||||||
|
#if marker is not None:
|
||||||
|
# marker_group = Group.objects.create(name=f"{marker}_marker")
|
||||||
|
# #marker_group = Group.objects.create(name=f"{marker}_marker")
|
||||||
|
|
||||||
|
|
||||||
def create_exam(db, ExamViews: ExamViews):
|
def create_exam(db, ExamViews: ExamViews):
|
||||||
exam: ExamBase = ExamViews.Exam.objects.create(
|
exam: ExamBase = ExamViews.Exam.objects.create(
|
||||||
|
|||||||
+5
-1
@@ -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 ExamAuthorFormMixin, ExamFormMixin
|
from generic.forms import ExamAuthorFormMixin, ExamFormMixin, ExamMarkerFormMixin
|
||||||
|
|
||||||
from rapids.models import (
|
from rapids.models import (
|
||||||
Abnormality,
|
Abnormality,
|
||||||
@@ -240,4 +240,8 @@ class ExamForm(ExamFormMixin, ModelForm):
|
|||||||
|
|
||||||
class ExamAuthorForm(ExamAuthorFormMixin):
|
class ExamAuthorForm(ExamAuthorFormMixin):
|
||||||
class Meta(ExamAuthorFormMixin.Meta):
|
class Meta(ExamAuthorFormMixin.Meta):
|
||||||
|
model = Exam
|
||||||
|
|
||||||
|
class ExamMarkerForm(ExamMarkerFormMixin):
|
||||||
|
class Meta(ExamMarkerFormMixin.Meta):
|
||||||
model = Exam
|
model = Exam
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 5.0.2 on 2024-08-05 09:43
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('rapids', '0010_alter_exam_end_date_alter_exam_exam_results_emailed_and_more'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='exam',
|
||||||
|
name='markers',
|
||||||
|
field=models.ManyToManyField(blank=True, help_text='Authorised markers for the exam', related_name='rapid_exam_markers', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -629,6 +629,13 @@ class Exam(ExamBase):
|
|||||||
related_name="rapid_exam_author",
|
related_name="rapid_exam_author",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
markers = models.ManyToManyField(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
blank=True,
|
||||||
|
help_text="Authorised markers for the exam",
|
||||||
|
related_name="rapid_exam_markers",
|
||||||
|
)
|
||||||
|
|
||||||
valid_cid_users = models.ManyToManyField(
|
valid_cid_users = models.ManyToManyField(
|
||||||
CidUser, blank=True, related_name="rapid_exams"
|
CidUser, blank=True, related_name="rapid_exams"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ urlpatterns = [
|
|||||||
path(
|
path(
|
||||||
"exam/<int:exam_pk>/<int:sk>/mark/review", views.mark_review, name="mark_review"
|
"exam/<int:exam_pk>/<int:sk>/mark/review", views.mark_review, name="mark_review"
|
||||||
),
|
),
|
||||||
|
path("exam/<int:pk>/markers", views.ExamMarkersUpdate.as_view(), name="exam_markers"),
|
||||||
path("exam/<int:pk>/authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"),
|
path("exam/<int:pk>/authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"),
|
||||||
path("exam/create", views.ExamCreate.as_view(), name="exam_create"),
|
path("exam/create", views.ExamCreate.as_view(), name="exam_create"),
|
||||||
path("exam/<int:exam_id>/clone", views.ExamClone.as_view(), name="exam_clone"),
|
path("exam/<int:exam_id>/clone", views.ExamClone.as_view(), name="exam_clone"),
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from django.http import HttpResponseRedirect, HttpResponse
|
|||||||
|
|
||||||
from .forms import (
|
from .forms import (
|
||||||
ExamAuthorForm,
|
ExamAuthorForm,
|
||||||
|
ExamMarkerForm,
|
||||||
RapidForm,
|
RapidForm,
|
||||||
MarkRapidQuestionForm,
|
MarkRapidQuestionForm,
|
||||||
ImageFormSet,
|
ImageFormSet,
|
||||||
@@ -761,6 +762,14 @@ class ExamAuthorUpdate(
|
|||||||
template_name = "author_form.html"
|
template_name = "author_form.html"
|
||||||
|
|
||||||
|
|
||||||
|
class ExamMarkersUpdate(
|
||||||
|
RevisionMixin, CheckCanEditMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView
|
||||||
|
):
|
||||||
|
model = Exam
|
||||||
|
form_class = ExamMarkerForm
|
||||||
|
|
||||||
|
template_name = "markers_form.html"
|
||||||
|
|
||||||
class UserAnswerView(LoginRequiredMixin, DetailView):
|
class UserAnswerView(LoginRequiredMixin, DetailView):
|
||||||
model = UserAnswer
|
model = UserAnswer
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -8,7 +8,7 @@ from django.forms import (
|
|||||||
)
|
)
|
||||||
from django.forms import inlineformset_factory
|
from django.forms import inlineformset_factory
|
||||||
|
|
||||||
from generic.forms import ExamAuthorFormMixin, ExamFormMixin
|
from generic.forms import ExamAuthorFormMixin, ExamFormMixin, ExamMarkerFormMixin
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Question,
|
Question,
|
||||||
@@ -41,6 +41,9 @@ class ExamAuthorForm(ExamAuthorFormMixin):
|
|||||||
class Meta(ExamAuthorFormMixin.Meta):
|
class Meta(ExamAuthorFormMixin.Meta):
|
||||||
model = Exam
|
model = Exam
|
||||||
|
|
||||||
|
class ExamMarkerForm(ExamMarkerFormMixin):
|
||||||
|
class Meta(ExamMarkerFormMixin.Meta):
|
||||||
|
model = Exam
|
||||||
|
|
||||||
class QuestionForm(ModelForm):
|
class QuestionForm(ModelForm):
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 5.0.2 on 2024-08-05 09:43
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('sbas', '0011_alter_exam_end_date_alter_exam_exam_results_emailed_and_more'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='exam',
|
||||||
|
name='markers',
|
||||||
|
field=models.ManyToManyField(blank=True, help_text='Authorised markers for the exam', related_name='sba_exam_markers', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -185,6 +185,13 @@ class Exam(ExamBase):
|
|||||||
related_name="sba_exam_author",
|
related_name="sba_exam_author",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
markers = models.ManyToManyField(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
blank=True,
|
||||||
|
help_text="Authorised markers for the exam",
|
||||||
|
related_name="sba_exam_markers",
|
||||||
|
)
|
||||||
|
|
||||||
valid_cid_users = models.ManyToManyField(CidUser, blank=True, related_name="sba_exams")
|
valid_cid_users = models.ManyToManyField(CidUser, blank=True, related_name="sba_exams")
|
||||||
|
|
||||||
valid_user_users = models.ManyToManyField(
|
valid_user_users = models.ManyToManyField(
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ urlpatterns = [
|
|||||||
name="exam_take_overview_user",
|
name="exam_take_overview_user",
|
||||||
),
|
),
|
||||||
|
|
||||||
|
path("exam/<int:pk>/markers", views.ExamMarkersUpdate.as_view(), name="exam_markers"),
|
||||||
path(
|
path(
|
||||||
"exam/<int:pk>/authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"
|
"exam/<int:pk>/authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ from .tables import QuestionTable, UserAnswerTable
|
|||||||
from .filters import QuestionFilter, UserAnswerFilter
|
from .filters import QuestionFilter, UserAnswerFilter
|
||||||
|
|
||||||
from .forms import (
|
from .forms import (
|
||||||
|
ExamMarkerForm,
|
||||||
QuestionForm,
|
QuestionForm,
|
||||||
ExamForm,
|
ExamForm,
|
||||||
)
|
)
|
||||||
@@ -429,6 +430,14 @@ class ExamAuthorUpdate(
|
|||||||
|
|
||||||
template_name = "author_form.html"
|
template_name = "author_form.html"
|
||||||
|
|
||||||
|
class ExamMarkersUpdate(
|
||||||
|
RevisionMixin, CheckCanEditMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView
|
||||||
|
):
|
||||||
|
model = Exam
|
||||||
|
form_class = ExamMarkerForm
|
||||||
|
|
||||||
|
template_name = "markers_form.html"
|
||||||
|
|
||||||
class ExamClone(ExamCloneMixin, ExamCreate):
|
class ExamClone(ExamCloneMixin, ExamCreate):
|
||||||
"""Clone exam view"""
|
"""Clone exam view"""
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,10 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<h2>Edit Authors</h2>
|
<h2>Edit Authors</h2>
|
||||||
|
<p>Authors have full access to access and edit the exam/collection (including the ability to add/remove other authors).</p>
|
||||||
|
|
||||||
|
<p>If you just want a user to be able to mark questions add them as a marker instead.</p>
|
||||||
|
|
||||||
<form action="" method="post">
|
<form action="" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<table>
|
<table>
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
|
||||||
|
{% extends object.get_base_template %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
{{ form.media }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block navigation %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% include object.get_link_headers %}
|
||||||
|
{% endblock navigation %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h2>Edit Markers</h2>
|
||||||
|
Markers have access to mark the exam, but they cannot otherwise edit it.
|
||||||
|
<form action="" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<table>
|
||||||
|
{{ form.as_table }}
|
||||||
|
</table>
|
||||||
|
<input type="submit" value="Submit">
|
||||||
|
<a href="{{ object.get_absolute_url }}" class="btn btn-default">Cancel</a>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user