diff --git a/src/pretix/control/templates/pretixcontrol/base.html b/src/pretix/control/templates/pretixcontrol/base.html index 58368abbc..72866851e 100644 --- a/src/pretix/control/templates/pretixcontrol/base.html +++ b/src/pretix/control/templates/pretixcontrol/base.html @@ -23,6 +23,7 @@ + {% endcompress %} {{ html_head|safe }} diff --git a/src/pretix/control/templates/pretixcontrol/items/question.html b/src/pretix/control/templates/pretixcontrol/items/question.html index f26719bc2..0f6b0464e 100644 --- a/src/pretix/control/templates/pretixcontrol/items/question.html +++ b/src/pretix/control/templates/pretixcontrol/items/question.html @@ -2,85 +2,84 @@ {% load i18n %} {% load bootstrap3 %} {% load formset_tags %} -{% block title %}{% trans "Question" %}{% endblock %} +{% block title %}{% blocktrans with name=question.question %}Question: {{ name }}{% endblocktrans %}{% endblock %} {% block inside %} -

{% trans "Question" %}

-
- {% csrf_token %} - {% bootstrap_form_errors form %} -
- {% trans "General information" %} - {% bootstrap_field form.question layout="horizontal" %} - {% bootstrap_field form.type layout="horizontal" %} - {% bootstrap_field form.required layout="horizontal" %} -
-
- {% trans "Apply to products" %} - {% bootstrap_field form.items layout="horizontal" %} -
-
- {% blocktrans trimmed %} - If you mark a Yes/No question as required, it means that the user has to select Yes and No is not - accepted. If you want to allow both options, do not make this field required. - {% endblocktrans %} -
-
- {% trans "Answer options" %} - -
- {{ formset.management_form }} - {% bootstrap_formset_errors formset %} -
- {% for form in formset %} -
-
- {{ form.id }} - {% bootstrap_field form.DELETE form_group_class="" layout="inline" %} -
-
-
- {% bootstrap_form_errors form %} - {% bootstrap_field form.answer layout='inline' form_group_class="" %} -
-
- -
-
-
- {% endfor %} -
- -

- -

-
-
-
- -
+

+ {% blocktrans with name=question.question %}Question: {{ name }}{% endblocktrans %} + + + {% trans "Edit question" %} + +

+ +

+ + + +

+ {% if not stats %} +
+

+ {% blocktrans trimmed %} + No matching answers found. + {% endblocktrans %} +

+ {% if not items %} +

+ {% trans "You need to assign the question to a product to collect answers." %} +

+ + {% trans "Edit question" %} + {% endif %} +
+ {% else %} +
+
+
+ +
+ +
+
+ + + + + + + + + {% for stat in stats %} + + + + + {% endfor %} + +
{% trans "Answer" %}{% trans "Count" %}
+ {{ stat.answer }} + {{ stat.count }}
+
+
+ {% endif %} {% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/items/question_edit.html b/src/pretix/control/templates/pretixcontrol/items/question_edit.html new file mode 100644 index 000000000..409e37f1e --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/items/question_edit.html @@ -0,0 +1,96 @@ +{% extends "pretixcontrol/items/base.html" %} +{% load i18n %} +{% load bootstrap3 %} +{% load formset_tags %} +{% block title %} + {% if question %} + {% blocktrans with name=question.question %}Question: {{ name }}{% endblocktrans %} + {% else %} + {% trans "Question" %} + {% endif %} +{% endblock %} +{% block inside %} + {% if question %} +

{% blocktrans with name=question.question %}Question: {{ name }}{% endblocktrans %}

+ {% else %} +

{% trans "Question" %}

+ {% endif %} +
+ {% csrf_token %} + {% bootstrap_form_errors form %} +
+ {% trans "General information" %} + {% bootstrap_field form.question layout="horizontal" %} + {% bootstrap_field form.type layout="horizontal" %} + {% bootstrap_field form.required layout="horizontal" %} +
+
+ {% trans "Apply to products" %} + {% bootstrap_field form.items layout="horizontal" %} +
+
+ {% blocktrans trimmed %} + If you mark a Yes/No question as required, it means that the user has to select Yes and No is not + accepted. If you want to allow both options, do not make this field required. + {% endblocktrans %} +
+
+ {% trans "Answer options" %} + +
+ {{ formset.management_form }} + {% bootstrap_formset_errors formset %} +
+ {% for form in formset %} +
+
+ {{ form.id }} + {% bootstrap_field form.DELETE form_group_class="" layout="inline" %} +
+
+
+ {% bootstrap_form_errors form %} + {% bootstrap_field form.answer layout='inline' form_group_class="" %} +
+
+ +
+
+
+ {% endfor %} +
+ +

+ +

+
+
+
+ +
+
+{% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/items/questions.html b/src/pretix/control/templates/pretixcontrol/items/questions.html index 4bcc86272..3258a7e04 100644 --- a/src/pretix/control/templates/pretixcontrol/items/questions.html +++ b/src/pretix/control/templates/pretixcontrol/items/questions.html @@ -38,15 +38,16 @@ {% for q in questions %} {{ q.question }} + {% url "control:event.items.questions.show" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}">{{ q.question }} {{ q.get_type_display }} - + + + {% endfor %} diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index abac22783..774969507 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -53,7 +53,9 @@ urlpatterns = [ url(r'^questions/(?P\d+)/up$', item.question_move_up, name='event.items.questions.up'), url(r'^questions/(?P\d+)/down$', item.question_move_down, name='event.items.questions.down'), - url(r'^questions/(?P\d+)/$', item.QuestionUpdate.as_view(), + url(r'^questions/(?P\d+)/$', item.QuestionView.as_view(), + name='event.items.questions.show'), + url(r'^questions/(?P\d+)/change$', item.QuestionUpdate.as_view(), name='event.items.questions.edit'), url(r'^questions/add$', item.QuestionCreate.as_view(), name='event.items.questions.add'), url(r'^quotas/$', item.QuotaList.as_view(), name='event.items.quotas'), diff --git a/src/pretix/control/views/item.py b/src/pretix/control/views/item.py index 380d81c39..6734a7300 100644 --- a/src/pretix/control/views/item.py +++ b/src/pretix/control/views/item.py @@ -4,19 +4,22 @@ from django.contrib import messages from django.core.files import File from django.core.urlresolvers import resolve, reverse from django.db import transaction +from django.db.models import Count from django.forms.models import ModelMultipleChoiceField, inlineformset_factory from django.http import Http404, HttpResponseRedirect from django.shortcuts import redirect from django.utils.functional import cached_property +from django.utils.timezone import now from django.utils.translation import ugettext, ugettext_lazy as _ from django.views.generic import ListView from django.views.generic.base import TemplateView -from django.views.generic.detail import SingleObjectMixin +from django.views.generic.detail import DetailView, SingleObjectMixin from django.views.generic.edit import DeleteView from pretix.base.forms import I18nFormSet from pretix.base.models import ( - Item, ItemCategory, ItemVariation, Question, QuestionOption, Quota, + Item, ItemCategory, ItemVariation, Order, Question, QuestionAnswer, + QuestionOption, Quota, ) from pretix.control.forms.item import ( CategoryForm, ItemCreateForm, ItemUpdateForm, ItemVariationForm, @@ -377,10 +380,71 @@ class QuestionMixin: return ctx +class QuestionView(EventPermissionRequiredMixin, QuestionMixin, ChartContainingView, DetailView): + model = Question + template_name = 'pretixcontrol/items/question.html' + permission = 'can_change_items' + template_name_field = 'question' + + def get_answer_statistics(self): + qs = QuestionAnswer.objects.filter( + question=self.object, orderposition__isnull=False, + orderposition__order__event=self.request.event + ) + if self.request.GET.get("status", "") != "": + s = self.request.GET.get("status", "") + if s == 'o': + qs = qs.filter(orderposition__order__status=Order.STATUS_PENDING, expires__lt=now().date()) + elif s == 'ne': + qs = qs.filter(orderposition__order__status__in=[Order.STATUS_PENDING, Order.STATUS_EXPIRED]) + else: + qs = qs.filter(orderposition__order__status=s) + if self.request.GET.get("item", "") != "": + i = self.request.GET.get("item", "") + qs = qs.filter(orderposition__item_id__in=(i,)) + + if self.object.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE): + qs = qs.order_by('options').values('options', 'options__answer')\ + .annotate(count=Count('id')).order_by('-count') + for a in qs: + a['answer'] = str(a['options__answer']) + del a['options__answer'] + else: + qs = qs.order_by('answer').values('answer').annotate(count=Count('id')).order_by('-count') + + if self.object.type == Question.TYPE_BOOLEAN: + for a in qs: + a['answer'] = ugettext('Yes') if a['answer'] == 'True' else ugettext('No') + a['answer_bool'] = a['answer'] == 'True' + + return list(qs) + + def get_context_data(self, **kwargs): + ctx = super().get_context_data() + ctx['items'] = self.object.items.all() + ctx['stats'] = self.get_answer_statistics() + ctx['stats_json'] = json.dumps(self.get_answer_statistics()) + return ctx + + def get_object(self, queryset=None) -> Question: + try: + return self.request.event.questions.get( + id=self.kwargs['question'] + ) + except Question.DoesNotExist: + raise Http404(_("The requested question does not exist.")) + + def get_success_url(self) -> str: + return reverse('control:event.items.questions', kwargs={ + 'organizer': self.request.event.organizer.slug, + 'event': self.request.event.slug, + }) + + class QuestionUpdate(EventPermissionRequiredMixin, QuestionMixin, UpdateView): model = Question form_class = QuestionForm - template_name = 'pretixcontrol/items/question.html' + template_name = 'pretixcontrol/items/question_edit.html' permission = 'can_change_items' context_object_name = 'question' @@ -417,7 +481,7 @@ class QuestionUpdate(EventPermissionRequiredMixin, QuestionMixin, UpdateView): class QuestionCreate(EventPermissionRequiredMixin, QuestionMixin, CreateView): model = Question form_class = QuestionForm - template_name = 'pretixcontrol/items/question.html' + template_name = 'pretixcontrol/items/question_edit.html' permission = 'can_change_items' context_object_name = 'question' diff --git a/src/static/pretixcontrol/js/ui/question.js b/src/static/pretixcontrol/js/ui/question.js new file mode 100644 index 000000000..7457d8555 --- /dev/null +++ b/src/static/pretixcontrol/js/ui/question.js @@ -0,0 +1,75 @@ +/*global $, Morris, gettext*/ +$(function () { + if (!$("#question-stats").length) { + return; + } + + $(".chart").css("height", "250px"); + var data_type = $("#question_chart").attr("data-type"), + data = JSON.parse($("#question-chart-data").html()), + others_sum = 0, + max_num = 8; + + for (var i in data) { + data[i].value = data[i].count; + data[i].label = data[i].answer; + if (data[i].label.length > 20) { + data[i].label = data[i].label.substring(0, 20) + '…'; + } + } + + if (data_type == 'N') { + // Sort + data.sort(function (a, b) { + if (parseFloat(a.label) > parseFloat(b.label)) { + return 1; + } else if (parseFloat(a.label) < parseFloat(b.label)) { + return -1; + } else { + return 0; + } + }); + max_num = 20; + } + + // Limit shown options + if (data.length > max_num) { + for (var i = max_num; i < data.length; i++) { + others_sum += data[i].count; + } + data = data.slice(0, max_num); + data.push({'value': others_sum, 'label': gettext('Others')}); + } + + if (data_type === 'B') { + var colors; + if (data[0].answer_bool) { + colors = ['#41A351', '#BD362F']; + } else { + colors = ['#BD362F', '#41A351']; + } + new Morris.Donut({ + element: 'question_chart', + data: data, + resize: true, + colors: colors + }); + } else if (data_type === 'C') { + new Morris.Donut({ + element: 'question_chart', + data: data, + resize: true + }); + } else { // M, N, S, T + new Morris.Bar({ + element: 'question_chart', + data: data, + resize: true, + xkey: 'label', + ykeys: ['count'], + labels: [gettext('Count')] + }); + } + + // N, S, T +});