From aa90ae5e0940e74b0d1d0ef4e14a6578388411bb Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 17 Nov 2025 12:12:06 +0000 Subject: [PATCH] Add QuestionForm and related views: implement form for creating questions with user context and exam association --- physics/forms.py | 31 ++++++++++- physics/templates/physics/question_form.html | 40 ++++++++++++++ physics/urls.py | 2 + physics/views.py | 58 +++++++++++++++++++- 4 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 physics/templates/physics/question_form.html diff --git a/physics/forms.py b/physics/forms.py index dd1bd674..b4c28c58 100755 --- a/physics/forms.py +++ b/physics/forms.py @@ -47,4 +47,33 @@ class ExamMarkerForm(ExamMarkerFormMixin): class ExamGroupsForm(ExamGroupsFormMixin): class Meta(ExamGroupsFormMixin.Meta): - model = Exam \ No newline at end of file + 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) \ No newline at end of file diff --git a/physics/templates/physics/question_form.html b/physics/templates/physics/question_form.html new file mode 100644 index 00000000..e20843da --- /dev/null +++ b/physics/templates/physics/question_form.html @@ -0,0 +1,40 @@ +{% extends "physics/base.html" %} +{% load crispy_forms_tags %} + +{% block content %} +
+
+
+

Add question

+
+ {% csrf_token %} + {{ form.media }} + + {% if form.non_field_errors %} +
{{ form.non_field_errors }}
+ {% endif %} + + {% if form.helper %} + {% crispy form form.helper %} + {% else %} + {% for field in form %} +
+ {{ field.label_tag }} + {{ field }} + {% if field.help_text %} +
{{ field.help_text }}
+ {% endif %} + {% for error in field.errors %} +
{{ error }}
+ {% endfor %} +
+ {% endfor %} + {% endif %} + + + Cancel +
+
+
+
+{% endblock %} diff --git a/physics/urls.py b/physics/urls.py index 383c1600..d3355101 100644 --- a/physics/urls.py +++ b/physics/urls.py @@ -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/", views.QuestionCreate.as_view(), name="question_create_exam"), path("question//", views.question_detail, name="question_detail"), path("exam//take_old", views.exam_take_old, name="exam_take_old"), path( diff --git a/physics/views.py b/physics/views.py index e705a6fd..64f03bf4 100644 --- a/physics/views.py +++ b/physics/views.py @@ -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