diff --git a/generic/views.py b/generic/views.py index 460a47cf..96768612 100644 --- a/generic/views.py +++ b/generic/views.py @@ -1891,7 +1891,7 @@ class GenericViewBase: "rapids": "rapid_checker", "anatomy": "anatomy_checker", "longs": "longs_checker", - "sbas": "sbas_checker", + "sbas": "sba_checker", "physics": "physics_checker", } diff --git a/sbas/forms.py b/sbas/forms.py index a6801c5c..58a347d3 100755 --- a/sbas/forms.py +++ b/sbas/forms.py @@ -14,11 +14,14 @@ from .models import ( Question, CidUserAnswer, Exam, + Category ) from django.contrib.admin.widgets import FilteredSelectMultiple from django.forms.widgets import RadioSelect, TextInput, Textarea +from tinymce.widgets import TinyMCE + class CidUserAnswerForm(ModelForm): class Meta: @@ -36,4 +39,118 @@ class ExamForm(ExamFormMixin, ModelForm): class ExamAuthorForm(ExamAuthorFormMixin): class Meta(ExamAuthorFormMixin.Meta): - model = Exam \ No newline at end of file + model = Exam + + +class QuestionForm(ModelForm): + + # exams = ModelMultipleChoiceField(required=False, queryset=Exam.objects.all()) + + class Media: + # Django also includes a few javascript files necessary + # for the operation of this form element. You need to + # include + # in the template. + css = { + "all": ["css/widgets.css"], + } + # Adding this javascript is crucial + js = ["jsi18n.js", "tesseract.min.js"] + + def __init__(self, *args, **kwargs): + self.user = kwargs.pop( + "user" + ) # To get request.user. Do not use kwargs.pop('user', None) due to potential security hole + if kwargs.get("instance"): + # We get the 'initial' keyword argument or initialize it + # as a dict if it didn't exist. + initial = kwargs.setdefault("initial", {}) + # The widget for a ModelMultipleChoiceField expects + # a list of primary key for the selected data. + initial["exams"] = [t.pk for t in kwargs["instance"].exams.all()] + + ModelForm.__init__(self, *args, **kwargs) + + super(QuestionForm, self).__init__(*args, **kwargs) + # self.fields['question'].widget.attrs = {'class': 'question-form', 'rows': 10, 'cols': 100} + # self.fields['feedback'].widget.attrs = {'class': 'feedback-form', 'rows': 10, 'cols': 100} + self.fields["category"] = ModelChoiceField( + required=True, + queryset=Category.objects.all(), + initial=1, + ) + + + if self.user.groups.filter(name="sba_checker").exists(): + exam_queryset = Exam.objects.all() + else: + exam_queryset = Exam.objects.filter( + author__id=self.user.id + ) | Exam.objects.filter(open_access=True) + + self.fields["exams"] = ModelMultipleChoiceField( + required=False, + queryset=exam_queryset, + widget=FilteredSelectMultiple(verbose_name="Exams", is_stacked=False), + ) + + def save(self, commit=True): + # Get the unsaved Pizza instance + instance = ModelForm.save(self, False) + instance.save() + + old_exams = instance.exams.all() + + new_exams = self.cleaned_data["exams"] + + for exam in old_exams: + if exam not in new_exams: + exam.exam_questions.remove(instance) + + for exam in new_exams: + exam.exam_questions.add(instance) + + self.save_m2m() + + return instance + + class Meta: + model = Question + + fields = [ + "stem", + "feedback", + "category", + "a_answer", + "a_feedback", + "b_answer", + "b_feedback", + "c_answer", + "c_feedback", + "d_answer", + "d_feedback", + "e_answer", + "e_feedback", + ] + + widgets = { + # "normal": RadioSelect( + # choices=[(True, 'Yes'), + # (False, 'No')]) + "stem" : TinyMCE(attrs={'cols': 80, 'rows': 30}), + "feedback" : TinyMCE(attrs={'cols': 80, 'rows': 30}), + "a_answer" : TinyMCE(attrs={'cols': 80, 'rows': 5}, mce_attrs={'height': 140}), + "a_feedback" : TinyMCE(attrs={'cols': 80, 'rows': 5}, mce_attrs={'height': 140}), + "b_answer" : TinyMCE(attrs={'cols': 80, 'rows': 4}, mce_attrs={'height': 140}), + "c_answer" : TinyMCE(attrs={'cols': 80, 'rows': 4}, mce_attrs={'height': 140}), + "d_answer" : TinyMCE(attrs={'cols': 80, 'rows': 4}, mce_attrs={'height': 140}), + "e_answer" : TinyMCE(attrs={'cols': 80, 'rows': 4}, mce_attrs={'height': 140}), + "b_feedback" : TinyMCE(attrs={'cols': 80, 'rows': 4}, mce_attrs={'height': 140}), + "c_feedback" : TinyMCE(attrs={'cols': 80, 'rows': 4}, mce_attrs={'height': 140}), + "d_feedback" : TinyMCE(attrs={'cols': 80, 'rows': 4}, mce_attrs={'height': 140}), + "e_feedback" : TinyMCE(attrs={'cols': 80, 'rows': 4}, mce_attrs={'height': 140}), + } + + #widgets = { + # "structure": autocomplete.ModelSelect2(url="anatomy:structure-autocomplete") + #} diff --git a/sbas/templates/sbas/base.html b/sbas/templates/sbas/base.html index 1b4c0ea0..44904176 100644 --- a/sbas/templates/sbas/base.html +++ b/sbas/templates/sbas/base.html @@ -8,7 +8,9 @@ Sbas: {% if request.user.is_authenticated %} Exams / - Questions + Create Exam / + Questions / + Create Question {% if request.user.is_superuser %} / Answers {% endif %} diff --git a/sbas/templates/sbas/question_detail.html b/sbas/templates/sbas/question_detail.html index 95193765..f22fc347 100644 --- a/sbas/templates/sbas/question_detail.html +++ b/sbas/templates/sbas/question_detail.html @@ -2,6 +2,9 @@ {% block content %}
+ Edit + Clone + Delete Admin Edit Add Note diff --git a/sbas/templates/sbas/question_form.html b/sbas/templates/sbas/question_form.html new file mode 100644 index 00000000..20b6a262 --- /dev/null +++ b/sbas/templates/sbas/question_form.html @@ -0,0 +1,24 @@ +{% extends "sbas/base.html" %} + +{% load static %} + +{% block js %} + + + + +{{ form.media }} +{% endblock %} +{% block content %} +

Submit Question

+
+ {% csrf_token %} + + + {{ form.as_table }} +
+ + +
+{% endblock %} \ No newline at end of file diff --git a/sbas/urls.py b/sbas/urls.py index 47a751aa..ea14b4b7 100644 --- a/sbas/urls.py +++ b/sbas/urls.py @@ -10,7 +10,20 @@ app_name = "sbas" urlpatterns = [ path("question/", views.QuestionView.as_view(), name="question_view"), path("question//", views.question_detail, name="question_detail"), - # path("question/create/", views.QuestionCreate.as_view(), name="anatomy_question_create"), + path("question/create/", views.QuestionCreate.as_view(), name="question_create"), + path( + "question//update", + views.QuestionUpdate.as_view(), + name="question_update", + ), + path( + "question//clone", views.QuestionClone.as_view(), name="question_clone" + ), + path( + "question//delete", + views.QuestionDelete.as_view(), + name="question_delete", + ), # path("question//update", views.QuestionUpdate.as_view(), name="anatomy_question_update"), # path("exam///mark", views.mark, name="mark"), # path("exam//mark", views.mark_overview, name="mark_overview"), @@ -28,6 +41,7 @@ urlpatterns = [ path("exam//authors", views.ExamAuthorUpdate.as_view(), name="exam_authors"), path("exam//clone", views.ExamClone.as_view(), name="exam_clone"), path("exam/available", views.active_exams, name="active_exams"), + path("exam/create", views.ExamUpdate.as_view(), name="exam_create"), path("exam//update", views.ExamUpdate.as_view(), name="exam_update"), path("exam//delete", views.ExamDelete.as_view(), name="exam_delete"), # path("exam/json/", views.exam_json, name="exam_json"), diff --git a/sbas/views.py b/sbas/views.py index ea2f22ea..82ca6480 100644 --- a/sbas/views.py +++ b/sbas/views.py @@ -7,6 +7,7 @@ from django.shortcuts import render, get_object_or_404, redirect from django.views.decorators.csrf import csrf_exempt from django import forms from django.utils import timezone +from django.forms.models import model_to_dict # from django.contrib.auth.models import User from django.contrib.auth.decorators import login_required, user_passes_test @@ -50,11 +51,16 @@ from django.core.exceptions import PermissionDenied from .tables import QuestionTable, UserAnswerTable from .filters import QuestionFilter, UserAnswerFilter +from .forms import ( + QuestionForm, + ExamForm, +) + class AuthorOrCheckerRequiredMixin(object): def get_object(self, *args, **kwargs): obj = super().get_object(*args, **kwargs) - if self.request.user.groups.filter(name="sbas_checker").exists(): + if self.request.user.groups.filter(name="sba_checker").exists(): return obj if self.request.user not in obj.author.all(): raise PermissionDenied() # or Http404 @@ -299,9 +305,109 @@ class QuestionView(LoginRequiredMixin, SingleTableMixin, FilterView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["app_name"] = "anatomy" + context["app_name"] = "sbas" 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 get_context_data(self, **kwargs): + context = super(QuestionCreateBase, self).get_context_data(**kwargs) + #if self.request.POST: + # context["answer_formset"] = AnswerFormSet( + # self.request.POST, self.request.FILES + # ) + #else: + # context["answer_formset"] = AnswerFormSet() + return context + + def form_valid(self, form): + + self.object = form.save(commit=False) + self.object.save() + + form.instance.author.add(self.request.user.id) + + context = self.get_context_data(form=form) + formset = context["answer_formset"] + + if formset.is_valid(): + response = super().form_valid(form) + formset.instance = self.object + formset.save() + # If the normal submit button is pressed we save as normal + if "submit" in self.request.POST: + return response + # else we redirect to the clone url + else: + return redirect("sbas:question_clone", pk=self.object.pk) + + else: + return super().form_invalid(form) + +class QuestionCreate(QuestionCreateBase): + + # initial = {'laterality': AnatomyQuestion.NONE} + + def get_initial(self): + if "pk" in self.kwargs: + initial = super().get_initial() + exam = get_object_or_404(Exam, pk=self.kwargs["pk"]) + + initial["exams"] = [exam.id] + + return initial + +class QuestionUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateView): + model = Question + form_class = QuestionForm + + def get_form_kwargs(self): + kwargs = super(QuestionUpdate, self).get_form_kwargs() + kwargs.update({"user": self.request.user}) + return kwargs + + def get_context_data(self, **kwargs): + context = super(QuestionUpdate, self).get_context_data(**kwargs) + return context + + def form_valid(self, form): + + # save exam orders (there must be a better way to do this) + #exam_orders = {} + #for exam in self.object.exams.all(): + # exam_orders[exam] = list(exam.exam_questions.all()) + # print(exam_orders[exam]) + + self.object = form.save(commit=False) + self.object.save() + + form.instance.author.add(self.request.user.id) + + context = self.get_context_data(form=form) + response = super().form_valid(form) + return response + +class QuestionClone(QuestionCreateBase): + """Clones a existing question""" + + def get_initial(self): + # print(self.request) + old_object = get_object_or_404(Question, pk=self.kwargs["pk"]) + initial_data = model_to_dict(old_object, exclude=["id"]) + + # We don't want to clone the exam details + # exams = old_object.exams.all().values_list("id", flat=True) + + # initial_data["exams"] = list(exams) + + return initial_data class UserAnswerView(LoginRequiredMixin, DetailView): model = CidUserAnswer @@ -364,7 +470,7 @@ class ExamDelete(AuthorOrCheckerRequiredMixin, ExamDeleteBase): # This view should return a list exams available to a user. # """ # user = self.request.user -# if user.groups.filter(name="sbas_checker").exists(): +# if user.groups.filter(name="sba_checker").exists(): # return Exam.objects.all() # # return Exam.objects.filter(author__id=user.id)