Merge pull request #143 from MagicSolutions/fix/admin-urs
Use separate URLs per model in administration to do the sortingmaster
commit
e9b03a3a4f
|
|
@ -13,18 +13,21 @@ except ImportError:
|
||||||
from django.contrib.admin import ModelAdmin, TabularInline, StackedInline
|
from django.contrib.admin import ModelAdmin, TabularInline, StackedInline
|
||||||
from django.contrib.admin.options import InlineModelAdmin
|
from django.contrib.admin.options import InlineModelAdmin
|
||||||
|
|
||||||
if VERSION >= (1, 8):
|
try:
|
||||||
from django.contrib.auth import get_permission_codename
|
|
||||||
from django.contrib.contenttypes.admin import (GenericStackedInline,
|
from django.contrib.contenttypes.admin import (GenericStackedInline,
|
||||||
GenericTabularInline)
|
GenericTabularInline)
|
||||||
else:
|
except:
|
||||||
|
# Django < 1.7
|
||||||
from django.contrib.contenttypes.generic import (GenericStackedInline,
|
from django.contrib.contenttypes.generic import (GenericStackedInline,
|
||||||
GenericTabularInline)
|
GenericTabularInline)
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.http import HttpResponse
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.shortcuts import render
|
from django.http import HttpResponse, Http404
|
||||||
|
from django.shortcuts import render, get_object_or_404
|
||||||
from django.template.defaultfilters import capfirst
|
from django.template.defaultfilters import capfirst
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.decorators.http import require_POST
|
||||||
|
|
||||||
from adminsortable.fields import SortableForeignKey
|
from adminsortable.fields import SortableForeignKey
|
||||||
from adminsortable.models import SortableMixin
|
from adminsortable.models import SortableMixin
|
||||||
|
|
@ -98,18 +101,33 @@ class SortableAdmin(SortableAdminBase, ModelAdmin):
|
||||||
|
|
||||||
def get_urls(self):
|
def get_urls(self):
|
||||||
urls = super(SortableAdmin, self).get_urls()
|
urls = super(SortableAdmin, self).get_urls()
|
||||||
|
opts = self.model._meta
|
||||||
|
try:
|
||||||
|
info = opts.app_label, opts.model_name
|
||||||
|
except AttributeError:
|
||||||
|
# Django < 1.7
|
||||||
|
info = opts.app_label, opts.modul_name
|
||||||
|
|
||||||
# this ajax view changes the order
|
# this ajax view changes the order of instances of self.model
|
||||||
admin_do_sorting_url = url(r'^sorting/do-sorting/(?P<model_type_id>\d+)/$',
|
admin_do_sorting_url = url(
|
||||||
|
r'^sort/do-sorting/$',
|
||||||
self.admin_site.admin_view(self.do_sorting_view),
|
self.admin_site.admin_view(self.do_sorting_view),
|
||||||
name='admin_do_sorting')
|
name='%s_%s_do_sorting' % info)
|
||||||
|
|
||||||
|
# this ajax view changes the order of instances of inline models
|
||||||
|
admin_do_inline_sorting_url = url(
|
||||||
|
r'^sort/do-sorting/(?P<model_type_id>\d+)/$',
|
||||||
|
self.admin_site.admin_view(self.do_sorting_view),
|
||||||
|
name='%s_%s_do_sorting' % info)
|
||||||
|
|
||||||
# this view displays the sortable objects
|
# this view displays the sortable objects
|
||||||
admin_sort_url = url(r'^sort/$',
|
admin_sort_url = url(
|
||||||
|
r'^sort/$',
|
||||||
self.admin_site.admin_view(self.sort_view),
|
self.admin_site.admin_view(self.sort_view),
|
||||||
name='admin_sort')
|
name='%s_%s_sort' % info)
|
||||||
|
|
||||||
urls = [
|
urls = [
|
||||||
|
admin_do_inline_sorting_url,
|
||||||
admin_do_sorting_url,
|
admin_do_sorting_url,
|
||||||
admin_sort_url
|
admin_sort_url
|
||||||
] + urls
|
] + urls
|
||||||
|
|
@ -120,15 +138,10 @@ class SortableAdmin(SortableAdminBase, ModelAdmin):
|
||||||
Custom admin view that displays the objects as a list whose sort
|
Custom admin view that displays the objects as a list whose sort
|
||||||
order can be changed via drag-and-drop.
|
order can be changed via drag-and-drop.
|
||||||
"""
|
"""
|
||||||
|
if not self.has_change_permission(request):
|
||||||
|
raise PermissionDenied
|
||||||
|
|
||||||
opts = self.model._meta
|
opts = self.model._meta
|
||||||
if VERSION >= (1, 8):
|
|
||||||
codename = get_permission_codename('change', opts)
|
|
||||||
has_perm = request.user.has_perm('{0}.{1}'.format(opts.app_label,
|
|
||||||
codename))
|
|
||||||
else:
|
|
||||||
has_perm = request.user.has_perm('{0}.{1}'.format(opts.app_label,
|
|
||||||
opts.get_change_permission()))
|
|
||||||
|
|
||||||
jquery_lib_path = 'admin/js/jquery.js' if VERSION < (1, 9) \
|
jquery_lib_path = 'admin/js/jquery.js' if VERSION < (1, 9) \
|
||||||
else 'admin/js/vendor/jquery/jquery.js'
|
else 'admin/js/vendor/jquery/jquery.js'
|
||||||
|
|
@ -218,7 +231,7 @@ class SortableAdmin(SortableAdminBase, ModelAdmin):
|
||||||
capfirst(verbose_name_plural)),
|
capfirst(verbose_name_plural)),
|
||||||
'opts': opts,
|
'opts': opts,
|
||||||
'app_label': opts.app_label,
|
'app_label': opts.app_label,
|
||||||
'has_perm': has_perm,
|
'has_perm': True,
|
||||||
'objects': objects,
|
'objects': objects,
|
||||||
'group_expression': sortable_by_expression,
|
'group_expression': sortable_by_expression,
|
||||||
'sortable_by_class': sortable_by_class,
|
'sortable_by_class': sortable_by_class,
|
||||||
|
|
@ -254,19 +267,31 @@ class SortableAdmin(SortableAdminBase, ModelAdmin):
|
||||||
return super(SortableAdmin, self).change_view(request, object_id,
|
return super(SortableAdmin, self).change_view(request, object_id,
|
||||||
form_url='', extra_context=extra_context)
|
form_url='', extra_context=extra_context)
|
||||||
|
|
||||||
|
@method_decorator(require_POST)
|
||||||
def do_sorting_view(self, request, model_type_id=None):
|
def do_sorting_view(self, request, model_type_id=None):
|
||||||
"""
|
"""
|
||||||
This view sets the ordering of the objects for the model type
|
This view sets the ordering of the objects for the model type
|
||||||
and primary keys passed in. It must be an Ajax POST.
|
and primary keys passed in. It must be an Ajax POST.
|
||||||
"""
|
"""
|
||||||
|
if not self.has_change_permission(request):
|
||||||
|
raise PermissionDenied
|
||||||
|
|
||||||
response = {'objects_sorted': False}
|
response = {'objects_sorted': False}
|
||||||
|
|
||||||
if request.is_ajax() and request.method == 'POST':
|
if request.is_ajax():
|
||||||
try:
|
try:
|
||||||
|
if model_type_id is None:
|
||||||
|
klass = self.model
|
||||||
|
else:
|
||||||
|
klass = get_object_or_404(ContentType,
|
||||||
|
id=model_type_id).model_class()
|
||||||
|
if klass not in (inline.model for inline in self.inlines):
|
||||||
|
raise Http404(
|
||||||
|
'There is no inline model with type id: {0}'.format(
|
||||||
|
model_type_id))
|
||||||
|
|
||||||
indexes = list(map(str,
|
indexes = list(map(str,
|
||||||
request.POST.get('indexes', []).split(',')))
|
request.POST.get('indexes', []).split(',')))
|
||||||
klass = ContentType.objects.get(
|
|
||||||
id=model_type_id).model_class()
|
|
||||||
objects_dict = dict([(str(obj.pk), obj) for obj in
|
objects_dict = dict([(str(obj.pk), obj) for obj in
|
||||||
klass.objects.filter(pk__in=indexes)])
|
klass.objects.filter(pk__in=indexes)])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
{% load i18n admin_modify adminsortable_tags %}
|
{% load i18n admin_modify adminsortable_tags admin_urls %}
|
||||||
{% load static from staticfiles %}
|
{% load static from staticfiles %}
|
||||||
<div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group">
|
<div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group">
|
||||||
<h2>{{ inline_admin_formset.opts.verbose_name_plural|title }} {% if inline_admin_formset.formset.initial_form_count > 1 %} - {% trans "drag and drop to change order" %}{% endif %}</h2>
|
<h2>{{ inline_admin_formset.opts.verbose_name_plural|title }} {% if inline_admin_formset.formset.initial_form_count > 1 %} - {% trans "drag and drop to change order" %}{% endif %}</h2>
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
{% if inline_admin_form.has_auto_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
|
{% if inline_admin_form.has_auto_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
|
||||||
{{ inline_admin_form.fk_field.field }}
|
{{ inline_admin_form.fk_field.field }}
|
||||||
{% if inline_admin_form.original %}
|
{% if inline_admin_form.original %}
|
||||||
<input type="hidden" name="admin_sorting_url" value="{% url 'admin:admin_do_sorting' inline_admin_form.original.model_type_id %}" />
|
<input type="hidden" name="admin_sorting_url" value="{% url opts|admin_urlname:'do_sorting' inline_admin_form.original.model_type_id %}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>{% endfor %}
|
</div>{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
{% include "admin/includes/fieldset.html" %}
|
{% include "admin/includes/fieldset.html" %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if inline_admin_form.original %}
|
{% if inline_admin_form.original %}
|
||||||
<input type="hidden" name="admin_sorting_url" value="{% url 'admin:admin_do_sorting' inline_admin_form.original.model_type_id %}" />
|
<input type="hidden" name="admin_sorting_url" value="{% url opts|admin_urlname:'do_sorting' inline_admin_form.original.model_type_id %}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if inline_admin_form.needs_explicit_pk_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
|
{% if inline_admin_form.needs_explicit_pk_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
|
||||||
{{ inline_admin_form.fk_field.field }}
|
{{ inline_admin_form.fk_field.field }}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
{% load i18n admin_modify adminsortable_tags %}
|
{% load i18n admin_modify adminsortable_tags admin_urls %}
|
||||||
{% load static from staticfiles %}
|
{% load static from staticfiles %}
|
||||||
<div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group">
|
<div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group">
|
||||||
<div class="tabular inline-related {% if forloop.last %}last-related{% endif %}">
|
<div class="tabular inline-related {% if forloop.last %}last-related{% endif %}">
|
||||||
|
|
@ -44,7 +44,7 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endspaceless %}
|
{% endspaceless %}
|
||||||
{% if inline_admin_form.original %}
|
{% if inline_admin_form.original %}
|
||||||
<input type="hidden" name="admin_sorting_url" value="{% url 'admin:admin_do_sorting' inline_admin_form.original.model_type_id %}" />
|
<input type="hidden" name="admin_sorting_url" value="{% url opts|admin_urlname:'do_sorting' inline_admin_form.original.model_type_id %}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
{% for fieldset in inline_admin_form %}
|
{% for fieldset in inline_admin_form %}
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endspaceless %}
|
{% endspaceless %}
|
||||||
{% if inline_admin_form.original %}
|
{% if inline_admin_form.original %}
|
||||||
<input type="hidden" name="admin_sorting_url" value="{% url 'admin:admin_do_sorting' inline_admin_form.original.model_type_id %}" />
|
<input type="hidden" name="admin_sorting_url" value="{% url opts|admin_urlname:'do_sorting' inline_admin_form.original.model_type_id %}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
{% for fieldset in inline_admin_form %}
|
{% for fieldset in inline_admin_form %}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{% load adminsortable_tags %}
|
{% load adminsortable_tags admin_urls %}
|
||||||
|
|
||||||
<form>
|
<form>
|
||||||
<input name="pk" type="hidden" value="{{ object.pk }}" />
|
<input name="pk" type="hidden" value="{{ object.pk }}" />
|
||||||
<a href="{% url 'admin:admin_do_sorting' object.model_type_id %}" class="admin_sorting_url"><i class="fa fa-{% if forloop.first %}sort-desc{% elif forloop.last %}sort-asc{% else %}sort{% endif %}"></i> {{ object }}</a>
|
<a href="{% url opts|admin_urlname:'do_sorting' %}" class="admin_sorting_url"><i class="fa fa-{% if forloop.first %}sort-desc{% elif forloop.last %}sort-asc{% else %}sort{% endif %}"></i> {{ object }}</a>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,12 @@ class SortableTestCase(TestCase):
|
||||||
self.user.is_staff = True
|
self.user.is_staff = True
|
||||||
self.user.is_superuser = True
|
self.user.is_superuser = True
|
||||||
self.user.save()
|
self.user.save()
|
||||||
|
self.staff_raw_password = 'staff'
|
||||||
|
self.staff = User.objects.create_user('staff', 'staff@staff.com',
|
||||||
|
self.staff_raw_password)
|
||||||
|
self.staff.is_staff = True
|
||||||
|
self.user.is_superuser = False
|
||||||
|
self.staff.save()
|
||||||
|
|
||||||
# create people
|
# create people
|
||||||
Person.objects.create(first_name='Bob', last_name='Smith',
|
Person.objects.create(first_name='Bob', last_name='Smith',
|
||||||
|
|
@ -64,6 +70,9 @@ class SortableTestCase(TestCase):
|
||||||
def test_new_user_is_staff(self):
|
def test_new_user_is_staff(self):
|
||||||
self.assertEqual(self.user.is_staff, True, 'User is not staff')
|
self.assertEqual(self.user.is_staff, True, 'User is not staff')
|
||||||
|
|
||||||
|
def test_new_staff_is_staff(self):
|
||||||
|
self.assertEqual(self.staff.is_staff, True, 'Staff User is not staff')
|
||||||
|
|
||||||
def test_is_not_sortable(self):
|
def test_is_not_sortable(self):
|
||||||
"""
|
"""
|
||||||
A model should only become sortable if it has more than
|
A model should only become sortable if it has more than
|
||||||
|
|
@ -100,8 +109,11 @@ class SortableTestCase(TestCase):
|
||||||
return category1, category2, category3
|
return category1, category2, category3
|
||||||
|
|
||||||
def get_sorting_url(self):
|
def get_sorting_url(self):
|
||||||
return '/admin/app/category/sorting/do-sorting/{0}/'.format(
|
return '/admin/app/category/sort/do-sorting/'
|
||||||
Category.model_type_id())
|
|
||||||
|
def get_inline_sorting_url(self, model):
|
||||||
|
return '/admin/app/project/sort/do-sorting/{0}/'.format(
|
||||||
|
model.model_type_id())
|
||||||
|
|
||||||
def get_category_indexes(self, *categories):
|
def get_category_indexes(self, *categories):
|
||||||
return {'indexes': ','.join([str(c.id) for c in categories])}
|
return {'indexes': ','.join([str(c.id) for c in categories])}
|
||||||
|
|
@ -120,6 +132,38 @@ class SortableTestCase(TestCase):
|
||||||
self.assertTrue('adminsortable/change_list.html' in template_names,
|
self.assertTrue('adminsortable/change_list.html' in template_names,
|
||||||
'adminsortable/change_list.html was not rendered')
|
'adminsortable/change_list.html was not rendered')
|
||||||
|
|
||||||
|
def test_adminsortable_change_list_sorting_fails_if_not_post(self):
|
||||||
|
logged_in = self.client.login(username=self.user.username,
|
||||||
|
password=self.user_raw_password)
|
||||||
|
self.assertTrue(logged_in, 'User is not logged in')
|
||||||
|
|
||||||
|
category1, category2, category3 = self.make_test_categories()
|
||||||
|
# make a normal GET
|
||||||
|
response = self.client.get(
|
||||||
|
self.get_sorting_url(),
|
||||||
|
data=self.get_category_indexes(category1, category2, category3))
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
response.status_code,
|
||||||
|
httplib.METHOD_NOT_ALLOWED,
|
||||||
|
'Objects should not have been sorted. A POST method is required.')
|
||||||
|
|
||||||
|
def test_adminsortable_change_list_sorting_fails_permission_denied(self):
|
||||||
|
logged_in = self.client.login(username=self.staff.username,
|
||||||
|
password=self.staff_raw_password)
|
||||||
|
self.assertTrue(logged_in, 'User is not logged in')
|
||||||
|
|
||||||
|
category1, category2, category3 = self.make_test_categories()
|
||||||
|
# make a normal POST
|
||||||
|
response = self.client.post(
|
||||||
|
self.get_sorting_url(),
|
||||||
|
data=self.get_category_indexes(category1, category2, category3))
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
response.status_code,
|
||||||
|
httplib.FORBIDDEN,
|
||||||
|
'Objects should not have been sorted. User is not allowed.')
|
||||||
|
|
||||||
def test_adminsortable_change_list_sorting_fails_if_not_ajax(self):
|
def test_adminsortable_change_list_sorting_fails_if_not_ajax(self):
|
||||||
logged_in = self.client.login(username=self.user.username,
|
logged_in = self.client.login(username=self.user.username,
|
||||||
password=self.user_raw_password)
|
password=self.user_raw_password)
|
||||||
|
|
@ -203,3 +247,81 @@ class SortableTestCase(TestCase):
|
||||||
response = self.client.get('/admin/app/project/sort/')
|
response = self.client.get('/admin/app/project/sort/')
|
||||||
self.assertEquals(response.status_code, httplib.OK,
|
self.assertEquals(response.status_code, httplib.OK,
|
||||||
'Unable to reach sort view.')
|
'Unable to reach sort view.')
|
||||||
|
|
||||||
|
def test_adminsortable_change_list_view_permission_denied(self):
|
||||||
|
category1 = self.create_category(title='Category 3')
|
||||||
|
Project.objects.create(category=category1, description="foo")
|
||||||
|
|
||||||
|
self.client.login(username=self.staff.username,
|
||||||
|
password=self.staff_raw_password)
|
||||||
|
response = self.client.get('/admin/app/project/sort/')
|
||||||
|
self.assertEquals(response.status_code, httplib.FORBIDDEN,
|
||||||
|
'Sort view must be forbidden.')
|
||||||
|
|
||||||
|
def test_adminsortable_inline_changelist_not_found(self):
|
||||||
|
self.client.login(username=self.user.username,
|
||||||
|
password=self.user_raw_password)
|
||||||
|
|
||||||
|
project = Project.objects.create(
|
||||||
|
category=self.create_category(),
|
||||||
|
description='Test project'
|
||||||
|
)
|
||||||
|
note1 = project.note_set.create(text='note 1')
|
||||||
|
note2 = project.note_set.create(text='note 2')
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
self.get_inline_sorting_url(Category),
|
||||||
|
data=self.get_category_indexes(note2, note1),
|
||||||
|
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
response.status_code,
|
||||||
|
httplib.NOT_FOUND,
|
||||||
|
'Categories must not be sortable trough ProjectAdmin')
|
||||||
|
|
||||||
|
def test_adminsortable_inline_changelist_success(self):
|
||||||
|
self.client.login(username=self.user.username,
|
||||||
|
password=self.user_raw_password)
|
||||||
|
|
||||||
|
project = Project.objects.create(
|
||||||
|
category=self.create_category(),
|
||||||
|
description='Test project'
|
||||||
|
)
|
||||||
|
note1 = project.note_set.create(text='note 1')
|
||||||
|
note2 = project.note_set.create(text='note 2')
|
||||||
|
note3 = project.note_set.create(text='note 3')
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
self.get_inline_sorting_url(project.note_set.model),
|
||||||
|
data=self.get_category_indexes(note3, note2, note1),
|
||||||
|
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
response.status_code,
|
||||||
|
httplib.OK,
|
||||||
|
'Note inline must be sortable in ProjectAdmin')
|
||||||
|
content = json.loads(response.content.decode(encoding='UTF-8'),
|
||||||
|
'latin-1')
|
||||||
|
self.assertTrue(content.get('objects_sorted'),
|
||||||
|
'Objects should have been sorted.')
|
||||||
|
|
||||||
|
notes = list(project.note_set.all().values('id', 'order', 'text'))
|
||||||
|
expected_notes = [
|
||||||
|
{
|
||||||
|
'id': note3.pk,
|
||||||
|
'order': 1,
|
||||||
|
'text': note3.text,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': note2.pk,
|
||||||
|
'order': 2,
|
||||||
|
'text': note2.text,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': note1.pk,
|
||||||
|
'order': 3,
|
||||||
|
'text': note1.text,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
self.assertEqual(notes, expected_notes)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue