Commit 00e1c558 authored by Serge S. Koval's avatar Serge S. Koval

Merge pull request #1242 from flask-admin/list_row_actions

List row actions
parents bc0f282f 9fd7637d
...@@ -21,7 +21,7 @@ from flask_admin.babel import gettext ...@@ -21,7 +21,7 @@ from flask_admin.babel import gettext
from flask_admin.base import BaseView, expose from flask_admin.base import BaseView, expose
from flask_admin.form import BaseForm, FormOpts, rules from flask_admin.form import BaseForm, FormOpts, rules
from flask_admin.model import filters, typefmt from flask_admin.model import filters, typefmt, template
from flask_admin.actions import ActionsMixin from flask_admin.actions import ActionsMixin
from flask_admin.helpers import (get_form_data, validate_form_on_submit, from flask_admin.helpers import (get_form_data, validate_form_on_submit,
get_redirect_target, flash_errors) get_redirect_target, flash_errors)
...@@ -459,6 +459,24 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -459,6 +459,24 @@ class BaseModelView(BaseView, ActionsMixin):
actions endpoints are accessible. actions endpoints are accessible.
""" """
column_extra_row_actions = None
"""
List of row actions (instances of :class:`~flask_admin.model.template.BaseListRowAction`).
Flask-Admin will generate standard per-row actions (edit, delete, etc)
and will append custom actions from this list right after them.
For example::
from flask_admin.model.template import EndpointLinkRowAction, LinkRowAction
class MyModelView(BaseModelView):
column_extra_row_actions = [
LinkRowAction('glyphicon glyphicon-off', 'http://direct.link/?id={row_id}'),
EndpointLinkRowAction('glyphicon glyphicon-test', 'my_view.index_view')
]
"""
simple_list_pager = False simple_list_pager = False
""" """
Enable or disable simple list pager. Enable or disable simple list pager.
...@@ -925,6 +943,29 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -925,6 +943,29 @@ class BaseModelView(BaseView, ActionsMixin):
return [(c, self.get_column_name(c)) for c in columns] return [(c, self.get_column_name(c)) for c in columns]
def get_list_row_actions(self):
"""
Return list of row action objects, each is instance of :class:`~flask_admin.model.template.BaseListRowAction`
"""
actions = []
if self.can_view_details:
if self.details_modal:
actions.append(template.ViewPopupRowAction())
else:
actions.append(template.ViewRowAction())
if self.can_edit:
if self.edit_modal:
actions.append(template.EditPopupRowAction())
else:
actions.append(template.EditRowAction())
if self.can_delete:
actions.append(template.DeleteRowAction())
return actions + (self.column_extra_row_actions or [])
def get_details_columns(self): def get_details_columns(self):
""" """
Returns a list of the model field names in the details view. If Returns a list of the model field names in the details view. If
...@@ -1815,6 +1856,7 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1815,6 +1856,7 @@ class BaseModelView(BaseView, ActionsMixin):
list_columns=self._list_columns, list_columns=self._list_columns,
sortable_columns=self._sortable_columns, sortable_columns=self._sortable_columns,
editable_columns=self.column_editable_list, editable_columns=self.column_editable_list,
list_row_actions=self.get_list_row_actions(),
# Pagination # Pagination
count=count, count=count,
......
from jinja2 import contextfunction
from flask_admin._compat import string_types, reduce
from flask_admin.babel import gettext
class BaseListRowAction(object):
def __init__(self, title=None):
self.title = title
def render(self, context, row_id, row):
raise NotImplementedError()
@contextfunction
def render_ctx(self, context, row_id, row):
return self.render(context, row_id, row)
def _resolve_symbol(self, context, symbol):
if '.' in symbol:
parts = symbol.split('.')
m = context.resolve(parts[0])
return reduce(getattr, parts[1:], m)
else:
return context.resolve(symbol)
class LinkRowAction(BaseListRowAction):
def __init__(self, icon_class, url, title=None):
super(LinkRowAction, self).__init__(title=title)
self.url = url
self.icon_class = icon_class
def render(self, context, row_id, row):
m = self._resolve_symbol(context, 'row_actions.link')
if isinstance(self.url, string_types):
url = self.url.format(row_id=row_id)
else:
url = self.url(self, row_id, row)
return m(self, url)
class EndpointLinkRowAction(BaseListRowAction):
def __init__(self, icon_class, endpoint, title=None, id_arg='id', url_args=None):
super(EndpointLinkRowAction, self).__init__(title=title)
self.icon_class = icon_class
self.endpoint = endpoint
self.id_arg = id_arg
self.url_args = url_args
def render(self, context, row_id, row):
m = self._resolve_symbol(context, 'row_actions.link')
get_url = self._resolve_symbol(context, 'get_url')
kwargs = dict(self.url_args) if self.url_args else {}
kwargs[self.id_arg] = row_id
url = get_url(self.endpoint, **kwargs)
return m(self, url)
class TemplateLinkRowAction(BaseListRowAction):
def __init__(self, template_name, title=None):
super(TemplateLinkRowAction, self).__init__(title=title)
self.template_name = template_name
def render(self, context, row_id, row):
m = self._resolve_symbol(context, self.template_name)
return m(self, row_id, row)
class ViewRowAction(TemplateLinkRowAction):
def __init__(self):
super(ViewRowAction, self).__init__(
'row_actions.view_row',
gettext('View row'))
class ViewPopupRowAction(TemplateLinkRowAction):
def __init__(self):
super(ViewPopupRowAction, self).__init__(
'row_actions.view_row_popup',
gettext('View row'))
class EditRowAction(TemplateLinkRowAction):
def __init__(self):
super(EditRowAction, self).__init__(
'row_actions.edit_row',
gettext('Edit row'))
class EditPopupRowAction(TemplateLinkRowAction):
def __init__(self):
super(EditPopupRowAction, self).__init__(
'row_actions.edit_row_popup',
gettext('Edit row'))
class DeleteRowAction(TemplateLinkRowAction):
def __init__(self):
super(DeleteRowAction, self).__init__(
'row_actions.delete_row',
gettext('Edit row'))
# Macro helper
def macro(name): def macro(name):
''' '''
Jinja2 macro list column formatter. Jinja2 macro list column formatter.
...@@ -14,3 +126,4 @@ def macro(name): ...@@ -14,3 +126,4 @@ def macro(name):
return m(model=model, column=column) return m(model=model, column=column)
return inner return inner
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
{% import 'admin/static.html' as admin_static with context%} {% import 'admin/static.html' as admin_static with context%}
{% import 'admin/model/layout.html' as model_layout with context %} {% import 'admin/model/layout.html' as model_layout with context %}
{% import 'admin/actions.html' as actionlib with context %} {% import 'admin/actions.html' as actionlib with context %}
{% import 'admin/model/row_actions.html' as row_actions with context %}
{% block head %} {% block head %}
{{ super() }} {{ super() }}
...@@ -116,40 +117,11 @@ ...@@ -116,40 +117,11 @@
{% block list_row_actions_column scoped %} {% block list_row_actions_column scoped %}
{% if admin_view.column_display_actions %} {% if admin_view.column_display_actions %}
<td class="list-buttons-column"> <td class="list-buttons-column">
{% block list_row_actions scoped %} {% block list_row_actions scoped %}
{%- if admin_view.can_view_details -%} {% for action in list_row_actions %}
{%- if admin_view.details_modal -%} {{ action.render_ctx(get_pk_value(row), row) }}
{{ lib.add_modal_button(url=get_url('.details_view', id=get_pk_value(row), url=return_url, modal=True), title=_gettext('View Record'), content='<span class="fa fa-eye glyphicon icon-eye-open"></span>') }} {% endfor %}
{% else %} {% endblock %}
<a class="icon" href="{{ get_url('.details_view', id=get_pk_value(row), url=return_url) }}" title="{{ _gettext('View Record') }}">
<span class="fa fa-eye icon-eye-open"></span>
</a>
{%- endif -%}
{%- endif -%}
{%- if admin_view.can_edit -%}
{%- if admin_view.edit_modal -%}
{{ lib.add_modal_button(url=get_url('.edit_view', id=get_pk_value(row), url=return_url, modal=True), title=_gettext('Edit Record'), content='<i class="fa fa-pencil icon-pencil"></i>') }}
{% else %}
<a class="icon" href="{{ get_url('.edit_view', id=get_pk_value(row), url=return_url) }}" title="{{ _gettext('Edit Record') }}">
<i class="fa fa-pencil icon-pencil"></i>
</a>
{%- endif -%}
{%- endif -%}
{%- if admin_view.can_delete -%}
<form class="icon" method="POST" action="{{ get_url('.delete_view') }}">
{{ delete_form.id(value=get_pk_value(row)) }}
{{ delete_form.url(value=return_url) }}
{% if delete_form.csrf_token %}
{{ delete_form.csrf_token }}
{% elif csrf_token %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
{% endif %}
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete this record?') }}');" title="{{ _gettext('Delete record') }}">
<i class="fa fa-trash icon-trash"></i>
</button>
</form>
{%- endif -%}
{% endblock %}
</td> </td>
{%- endif -%} {%- endif -%}
{% endblock %} {% endblock %}
......
{% import 'admin/lib.html' as lib with context %}
{% macro link(action, url, icon_class=None) %}
<a class="icon" href="{{ url }}" title="{{ action.title or '' }}">
<span class="{{ icon_class or action.icon_class }}"></span>
</a>
{% endmacro %}
{% macro view_row(action, row_id, row) %}
{{ link(action, get_url('.details_view', id=row_id, url=return_url), 'fa fa-eye glyphicon icon-eye-open') }}
{% endmacro %}
{% macro view_row_popup(action, row_id, row) %}
{{ lib.add_modal_button(url=get_url('.details_view', id=row_id, url=return_url, modal=True), title=action.title, content='<span class="fa fa-eye glyphicon icon-eye-open"></span>') }}
{% endmacro %}
{% macro edit_row(action, row_id, row) %}
{{ link(action, get_url('.edit_view', id=row_id, url=return_url), 'fa fa-pencil glyphicon icon-pencil') }}
{% endmacro %}
{% macro edit_row_popup(action, row_id, row) %}
{{ lib.add_modal_button(url=get_url('.edit_view', id=row_id, url=return_url, modal=True), title=action.title, content='<span class="fa fa-pencil glyphicon icon-pencil"></span>') }}
{% endmacro %}
{% macro delete_row(action, row_id, row) %}
<form class="icon" method="POST" action="{{ get_url('.delete_view') }}">
{{ delete_form.id(value=get_pk_value(row)) }}
{{ delete_form.url(value=return_url) }}
{% if delete_form.csrf_token %}
{{ delete_form.csrf_token }}
{% elif csrf_token %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
{% endif %}
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete this record?') }}');" title="Delete record">
<span class="fa fa-trash glyphicon icon-trash"></span>
</button>
</form>
{% endmacro %}
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
{% import 'admin/static.html' as admin_static with context%} {% import 'admin/static.html' as admin_static with context%}
{% import 'admin/model/layout.html' as model_layout with context %} {% import 'admin/model/layout.html' as model_layout with context %}
{% import 'admin/actions.html' as actionlib with context %} {% import 'admin/actions.html' as actionlib with context %}
{% import 'admin/model/row_actions.html' as row_actions with context %}
{% block head %} {% block head %}
{{ super() }} {{ super() }}
...@@ -116,38 +117,9 @@ ...@@ -116,38 +117,9 @@
{% if admin_view.column_display_actions %} {% if admin_view.column_display_actions %}
<td class="list-buttons-column"> <td class="list-buttons-column">
{% block list_row_actions scoped %} {% block list_row_actions scoped %}
{%- if admin_view.can_view_details -%} {% for action in list_row_actions %}
{%- if admin_view.details_modal -%} {{ action.render_ctx(get_pk_value(row), row) }}
{{ lib.add_modal_button(url=get_url('.details_view', id=get_pk_value(row), url=return_url, modal=True), title=_gettext('View Record'), content='<span class="fa fa-eye glyphicon glyphicon-eye-open"></span>') }} {% endfor %}
{% else %}
<a class="icon" href="{{ get_url('.details_view', id=get_pk_value(row), url=return_url) }}" title="{{ _gettext('View Record') }}">
<span class="fa fa-eye glyphicon glyphicon-eye-open"></span>
</a>
{%- endif -%}
{%- endif -%}
{%- if admin_view.can_edit -%}
{%- if admin_view.edit_modal -%}
{{ lib.add_modal_button(url=get_url('.edit_view', id=get_pk_value(row), url=return_url, modal=True), title=_gettext('Edit Record'), content='<span class="fa fa-pencil glyphicon glyphicon-pencil"></span>') }}
{% else %}
<a class="icon" href="{{ get_url('.edit_view', id=get_pk_value(row), url=return_url) }}" title="{{ _gettext('Edit Record') }}">
<span class="fa fa-pencil glyphicon glyphicon-pencil"></span>
</a>
{%- endif -%}
{%- endif -%}
{%- if admin_view.can_delete -%}
<form class="icon" method="POST" action="{{ get_url('.delete_view') }}">
{{ delete_form.id(value=get_pk_value(row)) }}
{{ delete_form.url(value=return_url) }}
{% if delete_form.csrf_token %}
{{ delete_form.csrf_token }}
{% elif csrf_token %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
{% endif %}
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete this record?') }}');" title="Delete record">
<span class="fa fa-trash glyphicon glyphicon-trash"></span>
</button>
</form>
{%- endif -%}
{% endblock %} {% endblock %}
</td> </td>
{%- endif -%} {%- endif -%}
......
{% import 'admin/lib.html' as lib with context %}
{% macro link(action, url, icon_class=None) %}
<a class="icon" href="{{ url }}" title="{{ action.title or '' }}">
<span class="{{ icon_class or action.icon_class }}"></span>
</a>
{% endmacro %}
{% macro view_row(action, row_id, row) %}
{{ link(action, get_url('.details_view', id=row_id, url=return_url), 'fa fa-eye glyphicon glyphicon-eye-open') }}
{% endmacro %}
{% macro view_row_popup(action, row_id, row) %}
{{ lib.add_modal_button(url=get_url('.details_view', id=row_id, url=return_url, modal=True), title=action.title, content='<span class="fa fa-eye glyphicon glyphicon-eye-open"></span>') }}
{% endmacro %}
{% macro edit_row(action, row_id, row) %}
{{ link(action, get_url('.edit_view', id=row_id, url=return_url), 'fa fa-pencil glyphicon glyphicon-pencil') }}
{% endmacro %}
{% macro edit_row_popup(action, row_id, row) %}
{{ lib.add_modal_button(url=get_url('.edit_view', id=row_id, url=return_url, modal=True), title=action.title, content='<span class="fa fa-pencil glyphicon glyphicon-pencil"></span>') }}
{% endmacro %}
{% macro delete_row(action, row_id, row) %}
<form class="icon" method="POST" action="{{ get_url('.delete_view') }}">
{{ delete_form.id(value=get_pk_value(row)) }}
{{ delete_form.url(value=return_url) }}
{% if delete_form.csrf_token %}
{{ delete_form.csrf_token }}
{% elif csrf_token %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
{% endif %}
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete this record?') }}');" title="Delete record">
<span class="fa fa-trash glyphicon glyphicon-trash"></span>
</button>
</form>
{% endmacro %}
...@@ -726,3 +726,70 @@ def test_export_csv(): ...@@ -726,3 +726,70 @@ def test_export_csv():
rv = client.get('/admin/macro_exception_macro_override/export/csv/') rv = client.get('/admin/macro_exception_macro_override/export/csv/')
data = rv.data.decode('utf-8') data = rv.data.decode('utf-8')
eq_(rv.status_code, 500) eq_(rv.status_code, 500)
def test_list_row_actions():
app, admin = setup()
client = app.test_client()
from flask_admin.model import template
# Test default actions
view = MockModelView(Model, endpoint='test')
admin.add_view(view)
actions = view.get_list_row_actions()
ok_(isinstance(actions[0], template.EditRowAction))
ok_(isinstance(actions[1], template.DeleteRowAction))
rv = client.get('/admin/test/')
eq_(rv.status_code, 200)
# Test default actions
view = MockModelView(Model, endpoint='test1', can_edit=False, can_delete=False, can_view_details=True)
admin.add_view(view)
actions = view.get_list_row_actions()
eq_(len(actions), 1)
ok_(isinstance(actions[0], template.ViewRowAction))
rv = client.get('/admin/test1/')
eq_(rv.status_code, 200)
# Test popups
view = MockModelView(Model, endpoint='test2',
can_view_details=True,
details_modal=True,
edit_modal=True)
admin.add_view(view)
actions = view.get_list_row_actions()
ok_(isinstance(actions[0], template.ViewPopupRowAction))
ok_(isinstance(actions[1], template.EditPopupRowAction))
ok_(isinstance(actions[2], template.DeleteRowAction))
rv = client.get('/admin/test2/')
eq_(rv.status_code, 200)
# Test custom views
view = MockModelView(Model, endpoint='test3',
column_extra_row_actions=[
template.LinkRowAction('glyphicon glyphicon-off', 'http://localhost/?id={row_id}'),
template.EndpointLinkRowAction('glyphicon glyphicon-test', 'test1.index_view')
])
admin.add_view(view)
actions = view.get_list_row_actions()
ok_(isinstance(actions[0], template.EditRowAction))
ok_(isinstance(actions[1], template.DeleteRowAction))
ok_(isinstance(actions[2], template.LinkRowAction))
ok_(isinstance(actions[3], template.EndpointLinkRowAction))
rv = client.get('/admin/test3/')
eq_(rv.status_code, 200)
data = rv.data.decode('utf-8')
ok_('glyphicon-off' in data)
ok_('http://localhost/?id=' in data)
ok_('glyphicon-test' in data)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment