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))