Compare commits

...

10 Commits

Author SHA1 Message Date
Mira Weller
5c599ab056 add back sort button disabled state management 2024-06-25 12:29:09 +02:00
Mira Weller
b2a0893e63 Change description text 2024-06-24 18:37:01 +02:00
Mira Weller
4945003f32 address review comments (improve reorder arrow logic, dimming) 2024-06-24 17:34:42 +02:00
Mira
b7cd10308f Apply suggestions from code review
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2024-06-24 17:34:42 +02:00
Mira
58a9a25515 Fix url in test 2024-06-24 17:34:42 +02:00
Mira Weller
9e23db1660 tests 2024-06-24 17:34:41 +02:00
Mira Weller
2d044b0b02 additional ui changes 2024-06-24 17:34:41 +02:00
Mira Weller
93a9528d66 show sort buttons if user fails at drag and drop, add labels for accessibility 2024-06-24 17:34:41 +02:00
Mira Weller
578666f8e8 sort buttons .sr-only instead of hide 2024-06-24 17:34:41 +02:00
Mira Weller
46ec507442 improve product list sorting ui (allow move between categories, hide up/down arrows if drag-drop is available) 2024-06-24 17:34:41 +02:00
11 changed files with 176 additions and 75 deletions

View File

@@ -122,6 +122,16 @@ class ItemCategory(LoggedModel):
return _('{category} (Add-On products)').format(category=str(name))
return str(name)
def get_category_type_display(self):
if self.is_addon:
return _('Add-On products')
else:
return None
@property
def category_type(self):
return 'addon' if self.is_addon else 'normal'
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
if self.event:

View File

@@ -32,7 +32,6 @@
<tr>
<th>{% trans "Product categories" %}</th>
<th class="action-col-2"></th>
<th class="action-col-2"></th>
</tr>
</thead>
<tbody data-dnd-url="{% url "control:event.items.categories.reorder" organizer=request.event.organizer.slug event=request.event.slug %}">
@@ -41,18 +40,16 @@
<td>
<strong><a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}">{{ c.internal_name|default:c.name }}</a></strong>
</td>
<td>
<button formaction="{% url "control:event.items.categories.up" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 and not page_obj.has_previous %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button formaction="{% url "control:event.items.categories.down" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
<span class="dnd-container"></span>
</td>
<td class="text-right flip">
<a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<button title="{% trans "Move up" %}" formaction="{% url "control:event.items.categories.up" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 and not page_obj.has_previous %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button title="{% trans "Move down" %}" formaction="{% url "control:event.items.categories.down" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
<span class="dnd-container" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
<a title="{% trans "Edit" %}" href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.items.categories.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ c.id }}"
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
<span class="fa fa-copy"></span>
</a>
<a href="{% url "control:event.items.categories.delete" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
<a title="{% trans "Delete" %}" href="{% url "control:event.items.categories.delete" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
</td>
</tr>
{% endfor %}

View File

@@ -56,8 +56,7 @@
<th>{% trans "Internal name" %}</th>
<th></th>
<th></th>
<th>{% trans "Products" %}</th>
<th class="action-col-2"></th>
<th colspan="2">{% trans "Products" %}</th>
<th class="action-col-2"></th>
</tr>
</thead>
@@ -102,9 +101,10 @@
{% endif %}
{% endif %}
</td>
<td>
<td {% if d.benefit_same_products %}colspan="2"{% endif %}>
{% if not d.benefit_same_products %}{% trans "Condition:" %}{% endif %}
{% if d.condition_all_products %}
<em>{% trans "All" %}</em>
<ul><li><em>{% trans "All" %}</em></li></ul>
{% else %}
<ul>
{% for item in d.condition_limit_products.all %}
@@ -115,18 +115,28 @@
</ul>
{% endif %}
</td>
<td>
{% if not d.benefit_same_products %}
<td>
{% trans "Applies to:" %}
<ul>
{% for item in d.benefit_limit_products.all %}
<li>
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item }}</a>
</li>
{% endfor %}
</ul>
</td>
{% endif %}
<td class="text-right flip">
<button formaction="{% url "control:event.items.discounts.up" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
class="btn btn-default btn-sm sortable-up"
class="btn btn-default btn-sm sortable-up" title="{% trans "Move up" %}"
{% if forloop.counter0 == 0 and not page_obj.has_previous %}
disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button formaction="{% url "control:event.items.discounts.down" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
class="btn btn-default btn-sm sortable-down"
class="btn btn-default btn-sm sortable-down" title="{% trans "Move down" %}"
{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}>
<i class="fa fa-arrow-down"></i></button>
<span class="dnd-container"></span>
</td>
<td class="text-right flip">
<span class="dnd-container" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
<a href="{% url "control:event.items.discounts.edit" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.items.discounts.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ d.id }}"

View File

@@ -1,13 +1,15 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load money %}
{% block title %}{% trans "Products" %}{% endblock %}
{% block inside %}
{% blocktrans asvar s_taxes %}taxes{% endblocktrans %}
<h1>{% trans "Products" %}</h1>
<p>
{% blocktrans trimmed %}
Below, you find a list of all available products. You can click on a product name to inspect and change
product details. You can also use the buttons on the right to change the order of products within a
give category.
product details. You can also use the buttons on the right to change the order of products or move
products to a different category.
{% endblocktrans %}
</p>
{% if items|length == 0 %}
@@ -29,7 +31,7 @@
<form method="post">
{% csrf_token %}
<div class="table-responsive">
<table class="table table-condensed table-hover">
<table class="table table-condensed table-hover table-items">
<thead>
<tr>
<th>{% trans "Product name" %}</th>
@@ -37,16 +39,24 @@
<th class="iconcol"></th>
<th class="iconcol"></th>
<th class="iconcol"></th>
<th>{% trans "Category" %}</th>
<th class="action-col-2"><span class="sr-only">Move</span></th>
<th class="text-right flip">{% trans "Default price" %}</th>
<th class="action-col-2"><span class="sr-only">Edit</span></th>
</tr>
</thead>
{% regroup items by category as cat_list %}
{% for c in cat_list %}
<tbody data-dnd-url="{% url "control:event.items.reorder" organizer=request.event.organizer.slug event=request.event.slug %}">
{% for i in c.list %}
{% if forloop.counter0 == 0 and i.category %}<tr class="sortable-disabled"><th colspan="8" scope="colgroup" class="text-muted">{{ i.category }}</th></tr>{% endif %}
{% for c, items in cat_list %}
{% if c %}
<tbody>
<tr class="sortable-disabled"><th colspan="9" scope="colgroup" class="text-muted">
{{ c.name }}{% if c.category_type != "normal" %} <span class="font-normal">({{ c.get_category_type_display }})</span>{% endif %}
<a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" title="{% trans "Edit" %}"><span class="fa fa-edit fa-fw"></span></a>
</th></tr>
</tbody>
{% endif %}
<tbody data-dnd-url="{% url "control:event.items.reorder" organizer=request.event.organizer.slug event=request.event.slug category=c.id|default:0 %}"
data-dnd-group="items">
{% for i in items %}
{% if forloop.counter0 == 0 and i.category %}{% endif %}
<tr data-dnd-id="{{ i.id }}" {% if not i.active %}class="row-muted"{% endif %}>
<td><strong>
{% if not i.active %}<strike>{% endif %}
@@ -92,15 +102,15 @@
</td>
<td>
{% if i.var_count %}
<span class="fa fa-th-large fa-fw text-muted" data-toggle="tooltip" title="{% trans "Product with variations" %}"></span>
<span class="fa fa-bars fa-fw text-muted" data-toggle="tooltip" title="{% trans "Product with variations" %}"></span>
{% endif %}
</td>
<td>
{% if i.category.is_addon %}
<span class="fa fa-puzzle-piece fa-fw text-muted" data-toggle="tooltip"
<span class="fa fa-plus-square fa-fw text-muted" data-toggle="tooltip"
title="{% trans "Only available as an add-on product" %}"></span>
{% elif i.require_bundling %}
<span class="fa fa-puzzle-piece fa-fw text-muted" data-toggle="tooltip"
<span class="fa fa-plus-square fa-fw text-muted" data-toggle="tooltip"
title="{% trans "Only available as part of a bundle" %}"></span>
{% elif i.hide_without_voucher %}
<span class="fa fa-tags fa-fw text-muted" data-toggle="tooltip"
@@ -110,16 +120,29 @@
title="{% trans "Can only be bought using a voucher" %}"></span>
{% endif %}
</td>
<td>{% if i.category %}{{ i.category.name }}{% endif %}</td>
<td>
<button formaction="{% url "control:event.items.up" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button formaction="{% url "control:event.items.down" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
<span class="dnd-container"></span>
<td class="text-right flip">
{% if i.free_price %}
<span class="fa fa-edit fa-fw text-muted" data-toggle="tooltip" title="{% trans "Free price input" %}">
</span>
{% endif %}
{{ i.default_price|money:request.event.currency }}
{% if i.original_price %}<strike class="text-muted">{{ i.original_price|money:request.event.currency }}</strike>{% endif %}
{% if i.tax_rule and i.default_price %}
<br/>
<small class="text-muted">
{% blocktrans trimmed with rate=i.tax_rule.rate|floatformat:-2 taxname=i.tax_rule.name|default:s_taxes %}
incl. {{ rate }}% {{ taxname }}
{% endblocktrans %}
</small>
{% endif %}
</td>
<td class="text-right flip col-actions">
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<button title="{% trans "Move up" %}" formaction="{% url "control:event.items.up" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button title="{% trans "Move down" %}" formaction="{% url "control:event.items.down" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
<span class="dnd-container" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm" title="{% trans "Edit" %}"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.items.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ i.id }}" class="btn btn-default btn-sm" title="{% trans "Clone" %}" data-toggle="tooltip"><i class="fa fa-copy"></i></a>
<a href="{% url "control:event.items.delete" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
<a href="{% url "control:event.items.delete" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-danger btn-sm" title="{% trans "Delete" %}"><i class="fa fa-trash"></i></a>
</td>
</tr>
{% endfor %}

View File

@@ -51,9 +51,9 @@
{% endif %}
</td>
<td>
<button formaction="{% url "control:organizer.property.up" organizer=request.organizer.slug property=p.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 and not page_obj.has_previous %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button formaction="{% url "control:organizer.property.down" organizer=request.organizer.slug property=p.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
<span class="dnd-container"></span>
<button title="{% trans "Move up" %}" formaction="{% url "control:organizer.property.up" organizer=request.organizer.slug property=p.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 and not page_obj.has_previous %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button title="{% trans "Move down" %}" formaction="{% url "control:organizer.property.down" organizer=request.organizer.slug property=p.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
<span class="dnd-container" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
</td>
<td class="text-right flip">
<a href="{% url "control:organizer.property.edit" organizer=request.organizer.slug property=p.id %}"

View File

@@ -293,7 +293,7 @@ urlpatterns = [
re_path(r'^items/(?P<item>\d+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'),
re_path(r'^items/(?P<item>\d+)/up$', item.item_move_up, name='event.items.up'),
re_path(r'^items/(?P<item>\d+)/down$', item.item_move_down, name='event.items.down'),
re_path(r'^items/reorder$', item.reorder_items, name='event.items.reorder'),
re_path(r'^items/reorder/(?P<category>\d+)/$', item.reorder_items, name='event.items.reorder'),
re_path(r'^items/(?P<item>\d+)/delete$', item.ItemDelete.as_view(), name='event.items.delete'),
re_path(r'^items/typeahead/meta/$', typeahead.item_meta_values, name='event.items.meta.typeahead'),
re_path(r'^items/select2$', typeahead.items_select2, name='event.items.select2'),

View File

@@ -35,6 +35,7 @@
import json
from collections import OrderedDict, namedtuple
from itertools import groupby
from json.decoder import JSONDecodeError
from django.contrib import messages
@@ -113,6 +114,8 @@ class ItemList(ListView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['sales_channels'] = get_all_sales_channels()
items_by_category = {cat: list(items) for cat, items in groupby(ctx['items'], lambda item: item.category)}
ctx['cat_list'] = [(cat, items_by_category.get(cat, [])) for cat in [None, *self.request.event.categories.all()]]
return ctx
@@ -169,7 +172,7 @@ def item_move_down(request, organizer, event, item):
@transaction.atomic
@event_permission_required("can_change_items")
@require_http_methods(["POST"])
def reorder_items(request, organizer, event):
def reorder_items(request, organizer, event, category):
try:
ids = json.loads(request.body.decode('utf-8'))['ids']
except (JSONDecodeError, KeyError, ValueError):
@@ -180,23 +183,21 @@ def reorder_items(request, organizer, event):
if len(input_items) != len(ids):
raise Http404(_("Some of the provided object ids are invalid."))
item_categories = {i.category_id for i in input_items}
if len(item_categories) > 1:
raise Http404(_("You cannot reorder items spanning different categories."))
# get first and only category
item_category = next(iter(item_categories))
if len(input_items) != request.event.items.filter(category=item_category).count():
raise Http404(_("Not all objects have been selected."))
if int(category):
target_category = request.event.categories.get(id=category)
else:
target_category = None
for i in input_items:
pos = ids.index(str(i.pk))
if pos != i.position: # Save unneccessary UPDATE queries
if pos != i.position or target_category != i.category: # Save unneccessary UPDATE queries
i.position = pos
i.save(update_fields=['position'])
i.category = target_category
i.save(update_fields=['position', 'category_id'])
i.log_action(
'pretix.event.item.reordered', user=request.user, data={
'position': i,
'category': target_category and target_category.pk,
}
)

View File

@@ -1,48 +1,78 @@
/*global $, Sortable*/
$(function () {
$("[data-dnd-url]").each(function(){
var container = $(this),
const allContainers = $("[data-dnd-url]");
function updateAllSortButtonStates() {
allContainers.each(function() { updateSortButtonState($(this)); });
}
function updateSortButtonState(container) {
var disabledUp = container.find(".sortable-up:disabled"),
firstUp = container.find(">tr[data-dnd-id] .sortable-up").first();
if (disabledUp.length && disabledUp.get(0) !== firstUp.get(0)) {
disabledUp.prop("disabled", false);
firstUp.prop("disabled", true);
}
var disabledDown = container.find(".sortable-down:disabled"),
lastDown = container.find(">tr[data-dnd-id] .sortable-down").last();
if (disabledDown.length && disabledDown.get(0) !== lastDown.get(0)) {
disabledDown.prop("disabled", false);
lastDown.prop("disabled", true);
}
}
let didSort = false, lastClick = 0;
allContainers.each(function(){
const container = $(this),
url = container.data("dnd-url"),
handle = $('<span class="btn btn-default btn-sm dnd-sort-handle"><i class="fa fa-arrows"></i></span>');
container.find(".dnd-container").append(handle);
if (container.find("[data-dnd-id]").length < 2) {
if (!sessionStorage.dndShowMoveButtons) {
container.find(".sortable-up, .sortable-down").addClass("sr-only").on("click", function () {
sessionStorage.dndShowMoveButtons = 'true';
});
}
if (container.find("[data-dnd-id]").length < 2 && !container.data("dnd-group")) {
handle.addClass("disabled");
return;
}
function maybeShowSortButtons() {
if (Date.now() - lastClick < 3000) {
$("[data-dnd-url] .sortable-up, [data-dnd-url] .sortable-down").removeClass("sr-only");
updateAllSortButtonStates();
}
lastClick = Date.now();
}
container.find(".dnd-sort-handle").on("mouseup", maybeShowSortButtons);
const group = container.data("dnd-group");
const containers = group ? container.parent().find('[data-dnd-group="' + group + '"]') : container;
Sortable.create(container.get(0), {
filter: ".sortable-disabled",
handle: ".dnd-sort-handle",
group: group,
onMove: function (evt) {
return evt.related.className.indexOf('sortable-disabled') === -1;
},
onStart: function (evt) {
container.addClass("sortable-dragarea");
containers.addClass("sortable-dragarea");
container.parent().addClass("sortable-sorting");
didSort = false;
},
onEnd: function (evt) {
container.removeClass("sortable-dragarea");
containers.removeClass("sortable-dragarea");
container.parent().removeClass("sortable-sorting");
var disabledUp = container.find(".sortable-up:disabled"),
firstUp = container.find(">tr[data-dnd-id] .sortable-up").first();
if (disabledUp.length && disabledUp.get(0) !== firstUp.get(0)) {
disabledUp.prop("disabled", false);
firstUp.prop("disabled", true);
}
var disabledDown = container.find(".sortable-down:disabled"),
lastDown = container.find(">tr[data-dnd-id] .sortable-down").last();
if (disabledDown.length && disabledDown.get(0) !== lastDown.get(0)) {
disabledDown.prop("disabled", false);
lastDown.prop("disabled", true);
if (!didSort) {
maybeShowSortButtons();
} else {
$("[data-dnd-url] .sortable-up, [data-dnd-url] .sortable-down").addClass("sr-only");
delete sessionStorage.dndShowMoveButtons;
}
},
onSort: function (evt){
var container = $(evt.to),
ids = container.find("[data-dnd-id]").toArray().map(function (e) { return e.dataset.dndId; });
if (evt.target !== evt.to) return;
didSort = true;
const ids = container.find("[data-dnd-id]").toArray().map(function (e) { return e.dataset.dndId; });
$.ajax(
{
'type': 'POST',

View File

@@ -841,9 +841,21 @@ h1 .label {
tbody[data-dnd-url] {
transition: opacity 1s;
}
.table-items tbody + tbody {
border-top-width: 1px;
}
.table-items tbody[data-dnd-url]:after {
content:'';
display: table-row;
height: 0.5em;
}
.sortable-sorting tbody:not(.sortable-dragarea) {
opacity: .4;
}
.font-normal {
font-style: normal;
font-weight: normal;
}
tbody th {
background: $table-bg-hover;
}

View File

@@ -402,6 +402,24 @@ class ItemsTest(ItemFormTest):
self.item2.refresh_from_db()
assert self.item1.position < self.item2.position
def test_reorder(self):
self.client.post('/control/event/%s/%s/items/reorder/0/' % (self.orga1.slug, self.event1.slug), {
'ids': [str(self.item2.id), str(self.item1.id)],
}, content_type='application/json')
self.item1.refresh_from_db()
self.item2.refresh_from_db()
assert self.item1.position > self.item2.position
assert self.item1.category is None
assert self.item2.category is None
self.client.post('/control/event/%s/%s/items/reorder/%s/' % (self.orga1.slug, self.event1.slug, self.addoncat.id), {
'ids': [str(self.item1.id), str(self.item2.id)],
}, content_type='application/json')
self.item1.refresh_from_db()
self.item2.refresh_from_db()
assert self.item1.position < self.item2.position
assert self.item1.category.id == self.addoncat.id
assert self.item2.category.id == self.addoncat.id
def test_create(self):
self.client.post('/control/event/%s/%s/items/add' % (self.orga1.slug, self.event1.slug), {
'name_0': 'T-Shirt',

View File

@@ -323,7 +323,7 @@ event_permission_urls = [
("can_change_items", "items/add", 200, HTTP_GET),
("can_change_items", "items/1/up", 404, HTTP_POST),
("can_change_items", "items/1/down", 404, HTTP_POST),
("can_change_items", "items/reorder", 400, HTTP_POST),
("can_change_items", "items/reorder/2/", 400, HTTP_POST),
("can_change_items", "items/1/delete", 404, HTTP_GET),
# ("can_change_items", "categories/", 200),
# We don't have to create categories and similar objects