Commit b6541d53 authored by mrjoes's avatar mrjoes

Added ability to perform actions on more than one model.

parent aaa36abf
...@@ -7,5 +7,6 @@ pyenv ...@@ -7,5 +7,6 @@ pyenv
build build
source/_static* source/_static*
source/_templates* source/_templates*
dist/*
make.bat make.bat
venv venv
...@@ -6,9 +6,9 @@ from wtforms.ext.sqlalchemy.orm import model_form ...@@ -6,9 +6,9 @@ from wtforms.ext.sqlalchemy.orm import model_form
from flask import flash from flask import flash
from flask.ext.admin.babel import gettext from flask.ext.admin.babel import gettext, ngettext
from flask.ext.admin.form import BaseForm from flask.ext.admin.form import BaseForm
from flask.ext.admin.model import BaseModelView from flask.ext.admin.model import BaseModelView, action
from flask.ext.admin.contrib.sqlamodel import form, filters, tools from flask.ext.admin.contrib.sqlamodel import form, filters, tools
...@@ -549,3 +549,30 @@ class ModelView(BaseModelView): ...@@ -549,3 +549,30 @@ class ModelView(BaseModelView):
except Exception, ex: except Exception, ex:
flash(gettext('Failed to delete model. %(error)s', error=str(ex)), 'error') flash(gettext('Failed to delete model. %(error)s', error=str(ex)), 'error')
return False return False
# Default model actions
def is_action_allowed(self, name):
# Check delete action permission
if name == 'delete' and not self.can_delete:
return False
return super(ModelView, self).is_action_allowed(name)
@action('delete', 'Delete', 'Are you sure you want to delete selected models?')
def action_delete(self, ids):
try:
model_pk = getattr(self.model, self._primary_key)
query = self.session.query(self.model).filter(model_pk.in_(ids))
# TODO: Load up ORM and delete models one by one?
count = query.delete(synchronize_session=False)
self.session.commit()
flash(ngettext('Model was successfully deleted.',
'%(count)s models were sucessfully deleted.',
count,
count=count))
except Exception, ex:
flash(gettext('Failed to delete models. %(error)s', error=str(ex)), 'error')
from .base import BaseModelView from .base import BaseModelView
from .action import action
def action(name, text, confirmation=None):
"""
Use this decorator to expose mass-model actions
`name`
Action name
`text`
Action text.
Will be passed through gettext() before rendering.
`confirmation`
Confirmation text. If not provided, action will be executed
unconditionally.
Will be passed through gettext() before rendering.
"""
def wrap(f):
f._action = (name, text, confirmation)
return f
return wrap
...@@ -151,7 +151,6 @@ class BaseModelView(BaseView): ...@@ -151,7 +151,6 @@ class BaseModelView(BaseView):
) )
""" """
form_columns = None form_columns = None
""" """
Collection of the model field names for the form. If set to `None` will Collection of the model field names for the form. If set to `None` will
...@@ -183,6 +182,16 @@ class BaseModelView(BaseView): ...@@ -183,6 +182,16 @@ class BaseModelView(BaseView):
form_overrides = dict(name=wtf.FileField) form_overrides = dict(name=wtf.FileField)
""" """
# Actions
disallowed_actions = []
"""
Set of disallowed action names. For example, if you want to disable
mass model deletion, do something like this:
class MyModelView(BaseModelView):
disallowed_actions = ['delete']
"""
# Various settings # Various settings
page_size = 20 page_size = 20
""" """
...@@ -220,6 +229,9 @@ class BaseModelView(BaseView): ...@@ -220,6 +229,9 @@ class BaseModelView(BaseView):
self.model = model self.model = model
# Actions
self._init_actions()
# Scaffolding # Scaffolding
self._refresh_cache() self._refresh_cache()
...@@ -263,6 +275,22 @@ class BaseModelView(BaseView): ...@@ -263,6 +275,22 @@ class BaseModelView(BaseView):
self._filter_groups = None self._filter_groups = None
self._filter_types = None self._filter_types = None
# Actions
def _init_actions(self):
self._actions = []
self._action_data = dict()
for p in dir(self):
attr = getattr(self, p)
if hasattr(attr, '_action'):
name, text, desc = attr._action
self._actions.append((name, text))
# TODO: Use namedtuple
self._action_data[name] = (attr, text, desc)
# Primary key # Primary key
def get_pk_value(self, model): def get_pk_value(self, model):
""" """
...@@ -624,6 +652,16 @@ class BaseModelView(BaseView): ...@@ -624,6 +652,16 @@ class BaseModelView(BaseView):
return url_for(view, **kwargs) return url_for(view, **kwargs)
def is_action_allowed(self, name):
"""
Override this method to allow or disallow actions based
on some condition.
Default implementation only checks if particular action
is not in `disallowed_actions`.
"""
return name not in self.disallowed_actions
# Views # Views
@expose('/') @expose('/')
def index_view(self): def index_view(self):
...@@ -677,6 +715,14 @@ class BaseModelView(BaseView): ...@@ -677,6 +715,14 @@ class BaseModelView(BaseView):
return self._get_url('.index_view', page, column, desc, return self._get_url('.index_view', page, column, desc,
search, filters) search, filters)
# Actions
actions = filter(lambda x: self.is_action_allowed(x[0]), self._actions)
actions_confirmation = dict()
for act in actions:
name, _ = act
actions_confirmation[name] = gettext(self._action_data[name][2])
return self.render(self.list_template, return self.render(self.list_template,
data=data, data=data,
# List # List
...@@ -713,7 +759,11 @@ class BaseModelView(BaseView): ...@@ -713,7 +759,11 @@ class BaseModelView(BaseView):
filter_groups=self._filter_groups, filter_groups=self._filter_groups,
filter_types=self._filter_types, filter_types=self._filter_types,
filter_data=filters_data, filter_data=filters_data,
active_filters=filters active_filters=filters,
# Actions
actions=actions,
actions_confirmation=actions_confirmation
) )
@expose('/new/', methods=('GET', 'POST')) @expose('/new/', methods=('GET', 'POST'))
...@@ -790,3 +840,21 @@ class BaseModelView(BaseView): ...@@ -790,3 +840,21 @@ class BaseModelView(BaseView):
self.delete_model(model) self.delete_model(model)
return redirect(return_url) return redirect(return_url)
@expose('/action/', methods=('POST',))
def action_view(self):
"""
Mass-model action view.
"""
action = request.form.get('action')
ids = request.form.getlist('rowid')
handler = self._action_data.get(action)
if handler and self.is_action_allowed(action):
response = handler[0](ids)
if response is not None:
return response
return redirect(url_for('.index_view'))
var AdminFilters = function(element, filters_element, adminForm, operations, options, types) { var AdminFilters = function(element, filters_element, adminForm, operations, options, types) {
var $root = $(element) var $root = $(element);
var $container = $('.filters', $root); var $container = $('.filters', $root);
var lastCount = 0; var lastCount = 0;
......
...@@ -32,6 +32,21 @@ ...@@ -32,6 +32,21 @@
</li> </li>
{% endif %} {% endif %}
{% if actions %}
<li class="dropdown">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
{{ _gettext('With selected')}}<b class="caret"></b>
</a>
<ul id="action_list" class="dropdown-menu">
{% for p in actions %}
<li>
<a href="#" onclick="return actionExecute('{{ p[0] }}');">{{ _gettext(p[1]) }}</a>
</li>
{% endfor %}
</ul>
</li>
{% endif %}
{% if search_supported %} {% if search_supported %}
<li> <li>
<form method="GET" action="{{ return_url }}" class="search-form"> <form method="GET" action="{{ return_url }}" class="search-form">
...@@ -90,6 +105,11 @@ ...@@ -90,6 +105,11 @@
<table class="table table-striped table-bordered model-list"> <table class="table table-striped table-bordered model-list">
<thead> <thead>
<tr> <tr>
{% if actions %}
<th class="span1">
<input type="checkbox" name="rowtoggle" id="rowtoggle" />
</th>
{% endif %}
<th class="span1">&nbsp;</th> <th class="span1">&nbsp;</th>
{% set column = 0 %} {% set column = 0 %}
{% for c, name in list_columns %} {% for c, name in list_columns %}
...@@ -117,6 +137,11 @@ ...@@ -117,6 +137,11 @@
</thead> </thead>
{% for row in data %} {% for row in data %}
<tr> <tr>
{% if actions %}
<td>
<input type="checkbox" name="rowid" value="{{ get_pk_value(row) }}" />
</td>
{% endif %}
<td> <td>
{%- if admin_view.can_edit -%} {%- if admin_view.can_edit -%}
<a class="icon" href="{{ url_for('.edit_view', id=get_pk_value(row), url=return_url) }}"> <a class="icon" href="{{ url_for('.edit_view', id=get_pk_value(row), url=return_url) }}">
...@@ -131,13 +156,21 @@ ...@@ -131,13 +156,21 @@
</form> </form>
{%- endif -%} {%- endif -%}
</td> </td>
{% block list_columns scoped %}
{% for c, name in list_columns %} {% for c, name in list_columns %}
<td>{{ get_value(row, c) }}</td> <td>{{ get_value(row, c) }}</td>
{% endfor %} {% endfor %}
{% endblock %}
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
{{ lib.pager(page, num_pages, pager_url) }} {{ lib.pager(page, num_pages, pager_url) }}
{% if actions %}
<form id="action_form" action="{{ url_for('.action_view') }}" method="POST" style="display: none">
<input type="hidden" id="action" name="action" />
</form>
{% endif %}
{% endblock %} {% endblock %}
{% block tail %} {% block tail %}
...@@ -151,6 +184,45 @@ ...@@ -151,6 +184,45 @@
{{ admin_view._filter_dict|tojson|safe }}, {{ admin_view._filter_dict|tojson|safe }},
{{ filter_data|tojson|safe }}, {{ filter_data|tojson|safe }},
{{ filter_types|tojson|safe }}); {{ filter_types|tojson|safe }});
// Actions helpers. TODO: Move to separate file
$(function() {
$('#rowtoggle').change(function() {
$('td input[type=checkbox]').attr('checked', this.checked);
});
var actionForm = $('#action_form');
actionForm.submit(function() {
$('td input[type=checkbox]:checked').each(function() {
actionForm.append($(this).clone());
});
});
});
// TODO: Cleanup this mess
var confirmation = {{ actions_confirmation|tojson|safe }};
function actionExecute(name) {
var selected = $('td input[type=checkbox]:checked').size();
if (selected == 0)
{
// TODO: Translation
alert('{{ _gettext("Please select at least one model.") }}');
return false;
}
var msg = confirmation[$('#action_form select').val()];
if (!!msg)
if (!confirm(msg))
return false;
$('#action_form #action').val(name);
$('#action_form').submit();
return false;
}
</script> </script>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
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