Add QuestionForm and related views: implement form for creating questions with user context and exam association

This commit is contained in:
Ross
2025-11-17 12:12:06 +00:00
parent f55bb1b18e
commit aa90ae5e09
4 changed files with 129 additions and 2 deletions
+30 -1
View File
@@ -47,4 +47,33 @@ class ExamMarkerForm(ExamMarkerFormMixin):
class ExamGroupsForm(ExamGroupsFormMixin):
class Meta(ExamGroupsFormMixin.Meta):
model = Exam
model = Exam
class QuestionForm(ModelForm):
class Meta:
model = Question
fields = [
"stem",
"a",
"a_answer",
"a_feedback",
"b",
"b_answer",
"b_feedback",
"c",
"c_answer",
"c_feedback",
"d",
"d_answer",
"d_feedback",
"e",
"e_answer",
"e_feedback",
"category",
]
def __init__(self, *args, **kwargs):
# accept an optional user kwarg for parity with other apps
self.user = kwargs.pop("user", None)
super().__init__(*args, **kwargs)
@@ -0,0 +1,40 @@
{% extends "physics/base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<div class="container py-3">
<div class="row">
<div class="col-12">
<h1>Add question</h1>
<form method="post" novalidate>
{% csrf_token %}
{{ form.media }}
{% if form.non_field_errors %}
<div class="alert alert-danger">{{ form.non_field_errors }}</div>
{% endif %}
{% if form.helper %}
{% crispy form form.helper %}
{% else %}
{% for field in form %}
<div class="mb-3">
{{ field.label_tag }}
{{ field }}
{% if field.help_text %}
<div class="form-text">{{ field.help_text }}</div>
{% endif %}
{% for error in field.errors %}
<div class="text-danger small">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
{% endif %}
<button type="submit" class="btn btn-primary">Save</button>
<a href="javascript:history.back()" class="btn btn-secondary">Cancel</a>
</form>
</div>
</div>
</div>
{% endblock %}
+2
View File
@@ -10,6 +10,8 @@ urlpatterns = []
urlpatterns.extend(
[
path("question/", views.QuestionView.as_view(), name="question_view"),
path("question/create/", views.QuestionCreate.as_view(), name="question_create"),
path("question/create/exam/<int:pk>", views.QuestionCreate.as_view(), name="question_create_exam"),
path("question/<int:pk>/", views.question_detail, name="question_detail"),
path("exam/<int:pk>/take_old", views.exam_take_old, name="exam_take_old"),
path(
+57 -1
View File
@@ -56,7 +56,14 @@ from generic.views import (
from django.core.exceptions import PermissionDenied
from .forms import ExamGroupsForm, ExamMarkerForm, UserAnswerForm, ExamAuthorForm, ExamForm
from .forms import (
ExamGroupsForm,
ExamMarkerForm,
UserAnswerForm,
ExamAuthorForm,
ExamForm,
QuestionForm,
)
from .decorators import (
user_is_author_or_physics_checker,
user_is_exam_author_or_physics_checker,
@@ -534,6 +541,55 @@ class QuestionView(
return context
class QuestionCreateBase(RevisionMixin, LoginRequiredMixin, CreateView):
model = Question
form_class = QuestionForm
def get_form_kwargs(self):
kwargs = super(QuestionCreateBase, self).get_form_kwargs()
kwargs.update({"user": self.request.user})
return kwargs
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.save()
# add the current user as an author
form.instance.author.add(self.request.user.id)
response = super().form_valid(form)
return response
class QuestionCreate(QuestionCreateBase):
def get_initial(self):
if "pk" in self.kwargs:
initial = super().get_initial()
exam = get_object_or_404(Exam, pk=self.kwargs["pk"])
# When creating a question from an exam context we don't have an
# 'exams' field on the ModelForm (exams is a reverse relation).
# Store nothing in the initial data here; attachment is handled
# in form_valid below.
return initial
def form_valid(self, form):
# Let the base class save the object and set the author
response = super().form_valid(form)
# If the create URL included an exam pk, add the new question to that exam
if "pk" in self.kwargs:
try:
exam = get_object_or_404(Exam, pk=self.kwargs["pk"])
# exam.exam_questions is a ManyToMany through relation; add is fine
exam.exam_questions.add(self.object)
except Exception:
# Don't fail the whole request if attaching to the exam fails;
# the question has already been created and author set.
pass
return response
class UserAnswerView(AuthorOrCheckerRequiredMixin, LoginRequiredMixin, DetailView):
model = UserAnswer