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
+
+{% 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)