forked from CGM_Public/pretix_original
Reordering questions with drag'n'drop (#1433)
* Reordering questions with drag'n'drop * Add permission check * Test permissions for question reordering * Handle malformed requests for question reordering * Show first up arrow and last down arrow * Provide page offset * Revert "Provide page offset" This reverts commit 8090bd573f851a74cca442f4651c111b8750948d. * Reorder questions endpoint with pagination support * Rudimentary test for reordering endpoint * Make reordering questions atomic * cache questions * Properly support pagination for reorder_questions * appease linter * Fix test
This commit is contained in:
@@ -43,6 +43,7 @@
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/quota.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/subevent.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/question.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/dragndroplist.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/mail.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/orderchange.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/typeahead.js" %}"></script>
|
||||
@@ -51,6 +52,7 @@
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/tabs.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixbase/js/details.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixbase/js/asynctask.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "sortable/Sortable.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "colorpicker/bootstrap-colorpicker.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "fileupload/jquery.ui.widget.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "fileupload/jquery.fileupload.js" %}"></script>
|
||||
|
||||
@@ -39,9 +39,9 @@
|
||||
<th class="action-col-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody data-dnd-url="{% url "control:event.items.questions.reorder" organizer=request.event.organizer.slug event=request.event.slug %}">
|
||||
{% for q in questions %}
|
||||
<tr>
|
||||
<tr data-dnd-id="{{q.id}}">
|
||||
<td><strong><a href="
|
||||
{% url "control:event.items.questions.show" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}">{{ q.question }}</a></strong>
|
||||
</td>
|
||||
|
||||
@@ -165,6 +165,7 @@ urlpatterns = [
|
||||
name='event.items.categories.edit'),
|
||||
url(r'^categories/add$', item.CategoryCreate.as_view(), name='event.items.categories.add'),
|
||||
url(r'^questions/$', item.QuestionList.as_view(), name='event.items.questions'),
|
||||
url(r'^questions/reorder$', item.reorder_questions, name='event.items.questions.reorder'),
|
||||
url(r'^questions/(?P<question>\d+)/delete$', item.QuestionDelete.as_view(),
|
||||
name='event.items.questions.delete'),
|
||||
url(r'^questions/(?P<question>\d+)/up$', item.question_move_up, name='event.items.questions.up'),
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import json
|
||||
from collections import OrderedDict
|
||||
from json.decoder import JSONDecodeError
|
||||
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.files import File
|
||||
from django.db import transaction
|
||||
from django.db.models import Count, F, Prefetch, Q
|
||||
from django.db.models import Count, F, Max, Prefetch, Q
|
||||
from django.forms.models import inlineformset_factory
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.http import (
|
||||
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect,
|
||||
)
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import resolve, reverse
|
||||
from django.utils.functional import cached_property
|
||||
@@ -283,6 +286,54 @@ class QuestionList(PaginationMixin, ListView):
|
||||
return self.request.event.questions.prefetch_related('items')
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
@event_permission_required("can_change_items")
|
||||
def reorder_questions(request, organizer, event):
|
||||
try:
|
||||
ids = [int(id) for id in json.loads(request.body.decode('utf-8'))['ids']]
|
||||
except (JSONDecodeError, KeyError, ValueError):
|
||||
return HttpResponseBadRequest("expected JSON: {ids:[]}")
|
||||
|
||||
input_questions = request.event.questions.filter(id__in=ids)
|
||||
|
||||
if input_questions.count() != len(ids):
|
||||
raise Http404(_("Some of the provided question ids are invalid."))
|
||||
|
||||
first = input_questions.first()
|
||||
last = input_questions.last()
|
||||
original_lowest_score = (first.position, first.id)
|
||||
original_highest_score = (last.position, last.id)
|
||||
|
||||
if request.event.questions.filter(
|
||||
Q(Q(position__gt=original_lowest_score[0])
|
||||
| Q(Q(position=original_lowest_score[0]) & Q(pk__gt=original_lowest_score[1])))
|
||||
&
|
||||
Q(Q(position__lt=original_highest_score[0])
|
||||
| Q(Q(position=original_highest_score[0]) & Q(pk__lt=original_highest_score[1])))
|
||||
).exclude(id__in=ids).exists():
|
||||
return HttpResponseBadRequest("ids need to be from a consecutive range of questions")
|
||||
|
||||
highest_position_on_previous_page = request.event.questions.filter(
|
||||
Q(position__lt=original_lowest_score[0])
|
||||
| Q(Q(position=original_lowest_score[0]) & Q(pk__lt=original_lowest_score[1]))
|
||||
).aggregate(m=Max('position'))['m'] or 0
|
||||
|
||||
questions_on_later_pages = request.event.questions.filter(
|
||||
Q(position__gt=original_highest_score[0])
|
||||
| Q(Q(position=original_highest_score[0]) & Q(pk__gt=original_highest_score[1]))
|
||||
)
|
||||
|
||||
ordered_questions = sorted(input_questions, key=lambda k: ids.index(k.pk))
|
||||
|
||||
for i, q in enumerate(ordered_questions + list(questions_on_later_pages)):
|
||||
pos = highest_position_on_previous_page + 1 + i
|
||||
if pos != q.position: # Save unneccessary UPDATE queries
|
||||
q.position = pos
|
||||
q.save(update_fields=['position'])
|
||||
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
def question_move(request, question, up=True):
|
||||
"""
|
||||
This is a helper function to avoid duplicating code in question_move_up and
|
||||
|
||||
Reference in New Issue
Block a user