Skip to content

Commit

Permalink
Merge pull request #49 from crccheck/fix-unnamed-admin-urls
Browse files Browse the repository at this point in the history
Fix unnamed admin urls
  • Loading branch information
crccheck committed Jan 14, 2016
2 parents 22681a8 + 936fe08 commit 310d086
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 30 deletions.
12 changes: 12 additions & 0 deletions django_object_actions/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import mock
from django.test import TestCase

from example_project.polls.models import Poll
Expand All @@ -11,6 +12,16 @@
class BaseDjangoObjectActionsTest(TestCase):
def setUp(self):
self.instance = BaseDjangoObjectActions()
self.instance.model = mock.Mock(
**{'_meta.app_label': 'app', '_meta.model_name': 'model'})

@mock.patch('django_object_actions.utils.BaseDjangoObjectActions'
'.admin_site', create=True)
def test_get_tool_urls_trivial_case(self, mock_site):
urls = self.instance.get_tool_urls()

self.assertEqual(len(urls), 1)
self.assertEqual(urls[0].name, 'app_model_tools')

def test_get_object_actions_gets_attribute(self):
mock_objectactions = [] # set to something mutable
Expand All @@ -26,6 +37,7 @@ def test_get_object_actions_gets_attribute(self):
# WISHLIST assert get_object_actions was called with right args

def test_get_djoa_button_attrs_returns_defaults(self):
# TODO: use `mock`
mock_tool = type('mock_tool', (object, ), {})
attrs, __ = self.instance.get_djoa_button_attrs(mock_tool)
self.assertEqual(attrs['class'], '')
Expand Down
103 changes: 73 additions & 30 deletions django_object_actions/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,45 +12,64 @@


class BaseDjangoObjectActions(object):
"""ModelAdmin mixin to add object-tools just like adding admin actions."""
# list to hold each object action tool
"""
ModelAdmin mixin to add object-tools just like adding admin actions.
Attributes
----------
model : django.db.models.Model
The Django Model these tools work on. This is populated by Django.
objectactions : list
Write the names of the callable attributes (methods) of the model admin
that can be used as tools.
tools_view_name : str
The name of the Django Object Actions admin view, including the 'admin'
namespace. Populated by `get_tool_urls`.
"""
objectactions = []
tools_view_name = None

def get_tool_urls(self, urls):
"""Gets the url patterns that route each tool to a special view."""
def get_tool_urls(self):
"""Get the url patterns that route each tool to a special view."""
tools = {}

end = '_change'
for url_pattern in urls:
if url_pattern.name.endswith(end):
tools_view = url_pattern.name[:-len(end)] + '_tools'
change_view = 'admin:' + url_pattern.name
self.tools_view_name = 'admin:' + tools_view
break
# Look for the default change view url and use that as a template
try:
model_name = self.model._meta.model_name
except AttributeError:
# DJANGO15
model_name = self.model._meta.module_name
base_url_name = '%s_%s' % (self.model._meta.app_label, model_name)
model_tools_url_name = '%s_tools' % base_url_name
change_view = 'admin:%s_change' % base_url_name

self.tools_view_name = 'admin:' + model_tools_url_name

for tool in self.objectactions:
tools[tool] = getattr(self, tool)
my_urls = [
return [
# supports pks that are numbers or uuids
url(r'^(?P<pk>[0-9a-f\-]+)/tools/(?P<tool>\w+)/$',
self.admin_site.admin_view(
ModelToolsView.as_view(model=self.model, tools=tools, back=change_view)),
name=tools_view)
self.admin_site.admin_view( # checks permissions
ModelToolsView.as_view(
model=self.model,
tools=tools,
back=change_view,
)
),
name=model_tools_url_name)
]
return my_urls

###################################
# EXISTING ADMIN METHODS MODIFIED #
###################################
# EXISTING ADMIN METHODS MODIFIED
#################################

def get_urls(self):
"""Prepends `get_urls` with our own patterns."""
"""Prepend `get_urls` with our own patterns."""
urls = super(BaseDjangoObjectActions, self).get_urls()
return self.get_tool_urls(urls) + urls
return self.get_tool_urls() + urls

def render_change_form(self, request, context, **kwargs):
"""Puts `objectactions` into the context."""
"""Put `objectactions` into the context."""

def to_dict(tool_name):
"""To represents the tool func as a dict with extra meta."""
Expand All @@ -71,12 +90,23 @@ def to_dict(tool_name):
return super(BaseDjangoObjectActions, self).render_change_form(
request, context, **kwargs)

##################
# CUSTOM METHODS #
##################
# CUSTOM METHODS
################

def get_object_actions(self, request, context, **kwargs):
"""Override this to customize what actions get sent."""
"""
Override this method to customize what actions get sent.
For example, to restrict actions to superusers, you could do:
class ChoiceAdmin(DjangoObjectActions, admin.ModelAdmin):
def get_object_actions(self, request, context, **kwargs):
if request.user.is_superuser:
return super(ChoiceAdmin, self).get_object_actions(
request, context, **kwargs
)
return []
"""
return self.objectactions

def get_djoa_button_attrs(self, tool):
Expand Down Expand Up @@ -112,14 +142,25 @@ def get_djoa_button_attrs(self, tool):


class DjangoObjectActions(BaseDjangoObjectActions):
# override default change_form_template
change_form_template = "django_object_actions/change_form.html"


class ModelToolsView(SingleObjectMixin, View):
"""A special view that run the tool's callable."""
tools = {}
"""
The view that runs the tool's callable.
Attributes
----------
back : str
The urlpattern name to send users back to. Defaults to the change view.
model : django.db.model.Model
The model this tool operates on.
tools : dict
A mapping of tool names to tool callables.
"""
back = None
model = None
tools = None

def get(self, request, **kwargs):
# SingleOjectMixin's `get_object`. Works because the view
Expand All @@ -129,9 +170,11 @@ def get(self, request, **kwargs):
tool = self.tools[kwargs['tool']]
except KeyError:
raise Http404(u'Tool does not exist')

ret = tool(request, obj)
if isinstance(ret, HttpResponse):
return ret

back = reverse(self.back, args=(kwargs['pk'],))
return HttpResponseRedirect(back)

Expand Down Expand Up @@ -159,7 +202,7 @@ def decorated_function(self, request, queryset):
if not isinstance(queryset, QuerySet):
try:
# Django >=1.8
queryset = self.get_queryset(request).filter(pk=queryset.pk)
queryset = self.get_queryset(request).filter(pk=queryset.pk)
except AttributeError:
try:
# Django >=1.6,<1.8
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ dj-database-url==0.3.0
django-extensions==1.6.1
factory-boy==2.6.0
coverage==4.0.3
mock==1.3.0

0 comments on commit 310d086

Please sign in to comment.