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" %}
-
+ {% 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 %}
+
+
+
+
+
+
+ | {% trans "Answer" %} |
+ {% trans "Count" %} |
+
+
+
+ {% for stat in stats %}
+
+ |
+ {{ stat.answer }}
+ |
+ {{ stat.count }} |
+
+ {% endfor %}
+
+
+
+
+ {% 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 %}
+
+{% 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
+});