Added statistic view on questions

This commit is contained in:
Raphael Michel
2016-09-18 20:45:05 +02:00
parent bcfd1dcf3b
commit d2f50cff16
7 changed files with 325 additions and 87 deletions
@@ -23,6 +23,7 @@
<script type="text/javascript" src="{% static "pretixcontrol/js/sb-admin-2.js" %}"></script> <script type="text/javascript" src="{% static "pretixcontrol/js/sb-admin-2.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/main.js" %}"></script> <script type="text/javascript" src="{% static "pretixcontrol/js/ui/main.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/quota.js" %}"></script> <script type="text/javascript" src="{% static "pretixcontrol/js/ui/quota.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/question.js" %}"></script>
{% endcompress %} {% endcompress %}
{{ html_head|safe }} {{ html_head|safe }}
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -2,85 +2,84 @@
{% load i18n %} {% load i18n %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load formset_tags %} {% load formset_tags %}
{% block title %}{% trans "Question" %}{% endblock %} {% block title %}{% blocktrans with name=question.question %}Question: {{ name }}{% endblocktrans %}{% endblock %}
{% block inside %} {% block inside %}
<h1>{% trans "Question" %}</h1> <h1>
<form action="" method="post" class="form-horizontal"> {% blocktrans with name=question.question %}Question: {{ name }}{% endblocktrans %}
{% csrf_token %} <a href="{% url "control:event.items.questions.edit" event=request.event.slug organizer=request.event.organizer.slug question=question.pk %}"
{% bootstrap_form_errors form %} class="btn btn-default">
<fieldset> <span class="fa fa-edit"></span>
<legend>{% trans "General information" %}</legend> {% trans "Edit question" %}
{% bootstrap_field form.question layout="horizontal" %} </a>
{% bootstrap_field form.type layout="horizontal" %} </h1>
{% bootstrap_field form.required layout="horizontal" %} <form class="form-inline helper-display-inline" action="" method="get">
</fieldset> <p>
<fieldset> <select name="status" class="form-control">
<legend>{% trans "Apply to products" %}</legend> <option value="">{% trans "All orders" %}</option>
{% bootstrap_field form.items layout="horizontal" %} <option value="p" {% if request.GET.status == "p" %}selected="selected"{% endif %}>{% trans "Paid" %}</option>
</fieldset> <option value="n" {% if request.GET.status == "n" %}selected="selected"{% endif %}>{% trans "Pending" %}</option>
<div class="alert alert-info alert-required-boolean"> <option value="o" {% if request.GET.status == "o" %}selected="selected"{% endif %}>{% trans "Pending (overdue)" %}</option>
{% blocktrans trimmed %} <option value="e" {% if request.GET.status == "e" %}selected="selected"{% endif %}>{% trans "Expired" %}</option>
If you mark a Yes/No question as required, it means that the user has to select Yes and No is not <option value="ne" {% if request.GET.status == "ne" %}selected="selected"{% endif %}>{% trans "Pending or expired" %}</option>
accepted. If you want to allow both options, do not make this field required. <option value="c" {% if request.GET.status == "c" %}selected="selected"{% endif %}>{% trans "Cancelled" %}</option>
{% endblocktrans %} <option value="r" {% if request.GET.status == "r" %}selected="selected"{% endif %}>{% trans "Refunded" %}</option>
</div> </select>
<fieldset id="answer-options"> <select name="item" class="form-control">
<legend>{% trans "Answer options" %}</legend> <option value="">{% trans "All products" %}</option>
<noscript> {% for item in items %}
<p>{% trans "Only applicable if you choose 'Choose one/multiple from a list' above." %}</p> <option value="{{ item.id }}"
</noscript> {% if request.GET.item|add:0 == item.id %}selected="selected"{% endif %}>
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}"> {{ item.name }}
{{ formset.management_form }} </option>
{% bootstrap_formset_errors formset %} {% endfor %}
<div data-formset-body> </select>
{% for form in formset %} <button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
<div data-formset-form> </p>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="row question-option-row">
<div class="col-xs-10">
{% bootstrap_form_errors form %}
{% bootstrap_field form.answer layout='inline' form_group_class="" %}
</div>
<div class="col-xs-2 text-right">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div data-formset-form>
<div class="sr-only">
{{ formset.empty_form.id }}
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="row question-option-row">
<div class="col-xs-10">
{% bootstrap_field formset.empty_form.answer layout='inline' form_group_class="" %}
</div>
<div class="col-xs-2 text-right">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add a new option" %}</button>
</p>
</div>
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form> </form>
{% if not stats %}
<div class="empty-collection">
<p>
{% blocktrans trimmed %}
No matching answers found.
{% endblocktrans %}
</p>
{% if not items %}
<p>
{% trans "You need to assign the question to a product to collect answers." %}
</p>
<a href="{% url "control:event.items.questions.edit" event=request.event.slug organizer=request.event.organizer.slug question=question.pk %}"
class="btn btn-primary btn-lg"><i class="fa fa-edit"></i> {% trans "Edit question" %}</a>
{% endif %}
</div>
{% else %}
<div class="row" id="question-stats">
<div class="col-md-6 col-xs-12">
<div class="chart" id="question_chart" data-type="{{ question.type }}">
</div>
<script type="application/json" id="question-chart-data">{{ stats_json|safe }}</script>
</div>
<div class="col-md-6 col-xs-12">
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>{% trans "Answer" %}</th>
<th>{% trans "Count" %}</th>
</tr>
</thead>
<tbody>
{% for stat in stats %}
<tr>
<td>
{{ stat.answer }}
</td>
<td>{{ stat.count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
{% endblock %} {% endblock %}
@@ -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 %}
<h1>{% blocktrans with name=question.question %}Question: {{ name }}{% endblocktrans %}</h1>
{% else %}
<h1>{% trans "Question" %}</h1>
{% endif %}
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% bootstrap_form_errors form %}
<fieldset>
<legend>{% trans "General information" %}</legend>
{% bootstrap_field form.question layout="horizontal" %}
{% bootstrap_field form.type layout="horizontal" %}
{% bootstrap_field form.required layout="horizontal" %}
</fieldset>
<fieldset>
<legend>{% trans "Apply to products" %}</legend>
{% bootstrap_field form.items layout="horizontal" %}
</fieldset>
<div class="alert alert-info alert-required-boolean">
{% 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 %}
</div>
<fieldset id="answer-options">
<legend>{% trans "Answer options" %}</legend>
<noscript>
<p>{% trans "Only applicable if you choose 'Choose one/multiple from a list' above." %}</p>
</noscript>
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
{{ formset.management_form }}
{% bootstrap_formset_errors formset %}
<div data-formset-body>
{% for form in formset %}
<div data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="row question-option-row">
<div class="col-xs-10">
{% bootstrap_form_errors form %}
{% bootstrap_field form.answer layout='inline' form_group_class="" %}
</div>
<div class="col-xs-2 text-right">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div data-formset-form>
<div class="sr-only">
{{ formset.empty_form.id }}
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="row question-option-row">
<div class="col-xs-10">
{% bootstrap_field formset.empty_form.answer layout='inline' form_group_class="" %}
</div>
<div class="col-xs-2 text-right">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add a new option" %}</button>
</p>
</div>
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}
@@ -38,15 +38,16 @@
{% for q in questions %} {% for q in questions %}
<tr> <tr>
<td><strong><a href=" <td><strong><a href="
{% url "control:event.items.questions.edit" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}">{{ q.question }}</a></strong> {% url "control:event.items.questions.show" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}">{{ q.question }}</a></strong>
</td> </td>
<td>{{ q.get_type_display }}</td> <td>{{ q.get_type_display }}</td>
<td> <td>
<a href="{% url "control:event.items.questions.up" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-default btn-sm {% if forloop.counter0 == 0 %}disabled{% endif %}"><i class="fa fa-arrow-up"></i></a> <a href="{% url "control:event.items.questions.up" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-default btn-sm {% if forloop.counter0 == 0 %}disabled{% endif %}"><i class="fa fa-arrow-up"></i></a>
<a href="{% url "control:event.items.questions.down" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-default btn-sm {% if forloop.revcounter0 == 0 %}disabled{% endif %}"><i class="fa fa-arrow-down"></i></a> <a href="{% url "control:event.items.questions.down" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-default btn-sm {% if forloop.revcounter0 == 0 %}disabled{% endif %}"><i class="fa fa-arrow-down"></i></a>
</td> </td>
<td class="text-right"><a href=" <td class="text-right">
{% url "control:event.items.questions.delete" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a> <a href="{% url "control:event.items.questions.edit" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.items.questions.delete" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
+3 -1
View File
@@ -53,7 +53,9 @@ urlpatterns = [
url(r'^questions/(?P<question>\d+)/up$', item.question_move_up, name='event.items.questions.up'), url(r'^questions/(?P<question>\d+)/up$', item.question_move_up, name='event.items.questions.up'),
url(r'^questions/(?P<question>\d+)/down$', item.question_move_down, url(r'^questions/(?P<question>\d+)/down$', item.question_move_down,
name='event.items.questions.down'), name='event.items.questions.down'),
url(r'^questions/(?P<question>\d+)/$', item.QuestionUpdate.as_view(), url(r'^questions/(?P<question>\d+)/$', item.QuestionView.as_view(),
name='event.items.questions.show'),
url(r'^questions/(?P<question>\d+)/change$', item.QuestionUpdate.as_view(),
name='event.items.questions.edit'), name='event.items.questions.edit'),
url(r'^questions/add$', item.QuestionCreate.as_view(), name='event.items.questions.add'), url(r'^questions/add$', item.QuestionCreate.as_view(), name='event.items.questions.add'),
url(r'^quotas/$', item.QuotaList.as_view(), name='event.items.quotas'), url(r'^quotas/$', item.QuotaList.as_view(), name='event.items.quotas'),
+68 -4
View File
@@ -4,19 +4,22 @@ from django.contrib import messages
from django.core.files import File from django.core.files import File
from django.core.urlresolvers import resolve, reverse from django.core.urlresolvers import resolve, reverse
from django.db import transaction from django.db import transaction
from django.db.models import Count
from django.forms.models import ModelMultipleChoiceField, inlineformset_factory from django.forms.models import ModelMultipleChoiceField, inlineformset_factory
from django.http import Http404, HttpResponseRedirect from django.http import Http404, HttpResponseRedirect
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import ugettext, ugettext_lazy as _ from django.utils.translation import ugettext, ugettext_lazy as _
from django.views.generic import ListView from django.views.generic import ListView
from django.views.generic.base import TemplateView 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 django.views.generic.edit import DeleteView
from pretix.base.forms import I18nFormSet from pretix.base.forms import I18nFormSet
from pretix.base.models import ( 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 ( from pretix.control.forms.item import (
CategoryForm, ItemCreateForm, ItemUpdateForm, ItemVariationForm, CategoryForm, ItemCreateForm, ItemUpdateForm, ItemVariationForm,
@@ -377,10 +380,71 @@ class QuestionMixin:
return ctx 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): class QuestionUpdate(EventPermissionRequiredMixin, QuestionMixin, UpdateView):
model = Question model = Question
form_class = QuestionForm form_class = QuestionForm
template_name = 'pretixcontrol/items/question.html' template_name = 'pretixcontrol/items/question_edit.html'
permission = 'can_change_items' permission = 'can_change_items'
context_object_name = 'question' context_object_name = 'question'
@@ -417,7 +481,7 @@ class QuestionUpdate(EventPermissionRequiredMixin, QuestionMixin, UpdateView):
class QuestionCreate(EventPermissionRequiredMixin, QuestionMixin, CreateView): class QuestionCreate(EventPermissionRequiredMixin, QuestionMixin, CreateView):
model = Question model = Question
form_class = QuestionForm form_class = QuestionForm
template_name = 'pretixcontrol/items/question.html' template_name = 'pretixcontrol/items/question_edit.html'
permission = 'can_change_items' permission = 'can_change_items'
context_object_name = 'question' context_object_name = 'question'
@@ -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
});