diff --git a/src/pretix/base/migrations/0032_question_position.py b/src/pretix/base/migrations/0032_question_position.py
new file mode 100644
index 000000000..dc9e097ee
--- /dev/null
+++ b/src/pretix/base/migrations/0032_question_position.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.9 on 2016-08-21 19:27
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('pretixbase', '0031_auto_20160816_0648'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='question',
+ name='position',
+ field=models.IntegerField(default=0),
+ ),
+ ]
diff --git a/src/pretix/base/migrations/0033_auto_20160821_2222.py b/src/pretix/base/migrations/0033_auto_20160821_2222.py
new file mode 100644
index 000000000..88b1e1b29
--- /dev/null
+++ b/src/pretix/base/migrations/0033_auto_20160821_2222.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.9 on 2016-08-21 22:22
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('pretixbase', '0032_question_position'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='question',
+ options={'ordering': ('position', 'id'), 'verbose_name': 'Question', 'verbose_name_plural': 'Questions'},
+ ),
+ ]
diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py
index 0fed42568..8885315d0 100644
--- a/src/pretix/base/models/items.py
+++ b/src/pretix/base/models/items.py
@@ -386,10 +386,14 @@ class Question(LoggedModel):
blank=True,
help_text=_('This question will be asked to buyers of the selected products')
)
+ position = models.IntegerField(
+ default=0
+ )
class Meta:
verbose_name = _("Question")
verbose_name_plural = _("Questions")
+ ordering = ('position', 'id')
def __str__(self):
return str(self.question)
@@ -404,6 +408,13 @@ class Question(LoggedModel):
if self.event:
self.event.get_cache().clear()
+ @property
+ def sortkey(self):
+ return self.position, self.id
+
+ def __lt__(self, other) -> bool:
+ return self.sortkey < other.sortkey
+
class QuestionOption(models.Model):
question = models.ForeignKey('Question', related_name='options')
diff --git a/src/pretix/control/templates/pretixcontrol/items/questions.html b/src/pretix/control/templates/pretixcontrol/items/questions.html
index 73fef2863..4bcc86272 100644
--- a/src/pretix/control/templates/pretixcontrol/items/questions.html
+++ b/src/pretix/control/templates/pretixcontrol/items/questions.html
@@ -41,6 +41,10 @@
{% url "control:event.items.questions.edit" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}">{{ q.question }}
{{ q.get_type_display }} |
+
+
+
+ |
|
diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py
index 410cc3647..b47ccdf43 100644
--- a/src/pretix/control/urls.py
+++ b/src/pretix/control/urls.py
@@ -50,6 +50,9 @@ urlpatterns = [
url(r'^questions/$', item.QuestionList.as_view(), name='event.items.questions'),
url(r'^questions/(?P\d+)/delete$', item.QuestionDelete.as_view(),
name='event.items.questions.delete'),
+ 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(),
name='event.items.questions.edit'),
url(r'^questions/add$', item.QuestionCreate.as_view(), name='event.items.questions.add'),
diff --git a/src/pretix/control/views/item.py b/src/pretix/control/views/item.py
index 007554c6c..3a5d278da 100644
--- a/src/pretix/control/views/item.py
+++ b/src/pretix/control/views/item.py
@@ -208,7 +208,7 @@ def category_move(request, category, up=True):
if cat.position != i:
cat.position = i
cat.save()
- messages.success(request, _('The order of categories as been updated.'))
+ messages.success(request, _('The order of categories has been updated.'))
@event_permission_required("can_change_items")
@@ -237,6 +237,49 @@ class QuestionList(ListView):
return self.request.event.questions.all()
+def question_move(request, question, up=True):
+ """
+ This is a helper function to avoid duplicating code in question_move_up and
+ question_move_down. It takes a question and a direction and then tries to bring
+ all items for this question in a new order.
+ """
+ try:
+ question = request.event.questions.get(
+ id=question
+ )
+ except Question.DoesNotExist:
+ raise Http404(_("The selected question does not exist."))
+ questions = list(request.event.questions.order_by("position"))
+
+ index = questions.index(question)
+ if index != 0 and up:
+ questions[index - 1], questions[index] = questions[index], questions[index - 1]
+ elif index != len(questions) - 1 and not up:
+ questions[index + 1], questions[index] = questions[index], questions[index + 1]
+
+ for i, qt in enumerate(questions):
+ if qt.position != i:
+ qt.position = i
+ qt.save()
+ messages.success(request, _('The order of questions has been updated.'))
+
+
+@event_permission_required("can_change_items")
+def question_move_up(request, organizer, event, question):
+ question_move(request, question, up=True)
+ return redirect('control:event.items.questions',
+ organizer=request.event.organizer.slug,
+ event=request.event.slug)
+
+
+@event_permission_required("can_change_items")
+def question_move_down(request, organizer, event, question):
+ question_move(request, question, up=False)
+ return redirect('control:event.items.questions',
+ organizer=request.event.organizer.slug,
+ event=request.event.slug)
+
+
class QuestionDelete(EventPermissionRequiredMixin, DeleteView):
model = Question
template_name = 'pretixcontrol/items/question_delete.html'
diff --git a/src/pretix/presale/views/questions.py b/src/pretix/presale/views/questions.py
index 6ad413968..6e77c805e 100644
--- a/src/pretix/presale/views/questions.py
+++ b/src/pretix/presale/views/questions.py
@@ -9,7 +9,7 @@ class QuestionsViewMixin:
@cached_property
def forms(self):
"""
- A list of forms with one form for each cart cart position that has questions
+ A list of forms with one form for each cart position that has questions
the user can answer. All forms have a custom prefix, so that they can all be
submitted at once.
"""
diff --git a/src/tests/control/test_items.py b/src/tests/control/test_items.py
index 9e4d56748..d48c97640 100644
--- a/src/tests/control/test_items.py
+++ b/src/tests/control/test_items.py
@@ -97,6 +97,23 @@ class QuestionsTest(ItemFormTest):
self.assertTrue(c.required)
assert str(Question.objects.get(id=c.id).question) == 'How old are you?'
+ def test_sort(self):
+ q1 = Question.objects.create(event=self.event1, question="Vegetarian?", type="N", required=True, position=0)
+ Question.objects.create(event=self.event1, question="Food allergies?", position=1)
+ doc = self.get_doc('/control/event/%s/%s/questions/' % (self.orga1.slug, self.event1.slug))
+ self.assertIn("Vegetarian?", doc.select("table > tbody > tr")[0].text)
+ self.assertIn("Food allergies?", doc.select("table > tbody > tr")[1].text)
+
+ self.client.get('/control/event/%s/%s/questions/%s/down' % (self.orga1.slug, self.event1.slug, q1.id))
+ doc = self.get_doc('/control/event/%s/%s/questions/' % (self.orga1.slug, self.event1.slug))
+ self.assertIn("Vegetarian?", doc.select("table > tbody > tr")[1].text)
+ self.assertIn("Food allergies?", doc.select("table > tbody > tr")[0].text)
+
+ self.client.get('/control/event/%s/%s/questions/%s/up' % (self.orga1.slug, self.event1.slug, q1.id))
+ doc = self.get_doc('/control/event/%s/%s/questions/' % (self.orga1.slug, self.event1.slug))
+ self.assertIn("Vegetarian?", doc.select("table > tbody > tr")[0].text)
+ self.assertIn("Food allergies?", doc.select("table > tbody > tr")[1].text)
+
def test_delete(self):
c = Question.objects.create(event=self.event1, question="What is your shoe size?", type="N", required=True)
doc = self.get_doc('/control/event/%s/%s/questions/%s/delete' % (self.orga1.slug, self.event1.slug, c.id))