Skip to content

Commit

Permalink
Add 'extra actions' to ViewSet & browsable API
Browse files Browse the repository at this point in the history
  • Loading branch information
Ryan P Kilby committed Nov 18, 2017
1 parent ba7e3fc commit a9447da
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 1 deletion.
3 changes: 2 additions & 1 deletion rest_framework/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,12 @@ def action(methods=None, detail=True, name=None, url_name=None, url_path=None, *
def decorator(func):
func.bind_to_methods = methods
func.detail = detail
func.name = name or pretty_name(func.__name__)
func.url_name = url_name or func.__name__
func.url_path = url_path or func.__name__
func.kwargs = kwargs
func.kwargs.update({
'name': name or pretty_name(func.__name__),
'name': func.name,
'description': func.__doc__ or None
})

Expand Down
7 changes: 7 additions & 0 deletions rest_framework/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,11 @@ def get_description(self, view, status_code):
def get_breadcrumbs(self, request):
return get_breadcrumbs(request.path, request)

def get_extra_actions(self, view):
if hasattr(view, 'get_extra_action_url_map'):
return view.get_extra_action_url_map()
return None

def get_filter_form(self, data, view, request):
if not hasattr(view, 'get_queryset') or not hasattr(view, 'filter_backends'):
return
Expand Down Expand Up @@ -692,6 +697,8 @@ def get_context(self, data, accepted_media_type, renderer_context):
'delete_form': self.get_rendered_html_form(data, view, 'DELETE', request),
'options_form': self.get_rendered_html_form(data, view, 'OPTIONS', request),

'extra_actions': self.get_extra_actions(view),

'filter_form': self.get_filter_form(data, view, request),

'raw_data_put_form': raw_data_put_form,
Expand Down
14 changes: 14 additions & 0 deletions rest_framework/templates/rest_framework/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,20 @@ <h4 class="text-center">Are you sure you want to delete this {{ name }}?</h4>
</div>
{% endif %}

{% if extra_actions %}
<div class="dropdown" style="float: right; margin-right: 10px">
<button class="btn btn-default" id="extra-actions-menu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
{% trans "Extra Actions" %}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="extra-actions-menu">
{% for action_name, url in extra_actions|items %}
<li><a href="{{ url }}">{{ action_name }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}

{% if filter_form %}
<button style="float: right; margin-right: 10px" data-toggle="modal" data-target="#filtersModal" class="btn btn-default">
<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span>
Expand Down
30 changes: 30 additions & 0 deletions rest_framework/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@
"""
from __future__ import unicode_literals

from collections import OrderedDict
from functools import update_wrapper
from inspect import getmembers

from django.urls import NoReverseMatch
from django.utils.decorators import classonlymethod
from django.views.decorators.csrf import csrf_exempt

from rest_framework import generics, mixins, views
from rest_framework.reverse import reverse


class ViewSetMixin(object):
Expand Down Expand Up @@ -134,6 +137,33 @@ def get_extra_actions(cls):
"""
return [method for _, method in getmembers(cls, _is_extra_action)]

def get_extra_action_url_map(self):
"""
Build a map of actions: urls for the extra actions. This requires the
`detail` attribute to have been provided.
"""
action_map = OrderedDict()

# exit early if `detail` has not been provided
if self.detail is None:
return action_map

# filter for the relevant extra actions
actions = [
action for action in self.get_extra_actions()
if action.detail == self.detail
]

for action in actions:
try:
url_name = '%s-%s' % (self.basename, action.url_name)
url = reverse(url_name, self.args, self.kwargs, request=self.request)
action_map[action.name] = url
except NoReverseMatch:
pass # do nothing, URL requires additionalargs to reverse

return action_map


def _is_extra_action(attr):
return hasattr(attr, 'bind_to_methods')
Expand Down

0 comments on commit a9447da

Please sign in to comment.