Merge pull request #143 from MagicSolutions/fix/admin-urs

Use separate URLs per model in administration to do the sorting
master
Brandon Taylor 2016-08-03 10:06:29 -04:00 committed by GitHub
commit e9b03a3a4f
7 changed files with 181 additions and 34 deletions

View File

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

View File

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

View File

@ -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 }}

View File

@ -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 %}

View File

@ -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 %}

View File

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

View File

@ -32,10 +32,16 @@ class SortableTestCase(TestCase):
self.client = Client() self.client = Client()
self.user_raw_password = 'admin' self.user_raw_password = 'admin'
self.user = User.objects.create_user('admin', 'admin@admin.com', self.user = User.objects.create_user('admin', 'admin@admin.com',
self.user_raw_password) self.user_raw_password)
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)