Commit 7329f8aa authored by Serge S. Koval's avatar Serge S. Koval

Get rid of url_for in Flask-Admin templates and widgets and provide get_url...

Get rid of url_for in Flask-Admin templates and widgets and provide get_url helper that's overridable
parent 70a06219
from flask import request, url_for, redirect
from flask import request, redirect
from flask.ext.admin import tools
......@@ -114,8 +114,8 @@ class ActionsMixin(object):
return response
if not return_view:
url = url_for('.' + self._default_view)
url = self.get_url('.' + self._default_view)
else:
url = url_for('.' + return_view)
url = self.get_url('.' + return_view)
return redirect(url)
......@@ -2,7 +2,7 @@ import os.path as op
from functools import wraps
from flask import Blueprint, render_template, abort, g
from flask import Blueprint, render_template, abort, g, url_for
from flask.ext.admin import babel
from flask.ext.admin._compat import with_metaclass
from flask.ext.admin import helpers as h
......@@ -271,6 +271,9 @@ class BaseView(with_metaclass(AdminViewMeta, BaseViewClass)):
kwargs['_ngettext'] = babel.ngettext
kwargs['h'] = h
# Expose get_url helper
kwargs['get_url'] = self.get_url
# Contribute extra arguments
kwargs.update(self._template_args)
......@@ -331,6 +334,18 @@ class BaseView(with_metaclass(AdminViewMeta, BaseViewClass)):
"""
return abort(403)
def get_url(self, endpoint, **kwargs):
"""
Generate URL for the endpoint. If you want to customize URL generation
logic (persist some query string argument, for example), this is
right place to do it.
:param endpoint:
Flask endpoint name
:param kwargs:
Arguments for `url_for`
"""
return url_for(endpoint, **kwargs)
@property
def _debug(self):
......
......@@ -7,7 +7,7 @@ import shutil
from operator import itemgetter
from werkzeug import secure_filename
from flask import flash, url_for, redirect, abort, request, send_file
from flask import flash, redirect, abort, request, send_file
from wtforms import fields, validators
......@@ -302,14 +302,14 @@ class FileAdmin(BaseView, ActionsMixin):
Additional arguments
"""
if not path:
return url_for(endpoint)
return self.get_url(endpoint)
else:
if self._on_windows:
path = path.replace('\\', '/')
kwargs['path'] = path
return url_for(endpoint, **kwargs)
return self.get_url(endpoint, **kwargs)
def _get_file_url(self, path):
"""
......@@ -322,7 +322,8 @@ class FileAdmin(BaseView, ActionsMixin):
route = '.edit'
else:
route = '.download'
return url_for(route, path=path)
return self.get_url(route, path=path)
def _normalize_path(self, path):
"""
......@@ -531,7 +532,7 @@ class FileAdmin(BaseView, ActionsMixin):
# backward compatibility with base_url
base_url = self.get_base_url()
if base_url:
base_url = urljoin(url_for('.index'), base_url)
base_url = urljoin(self.get_url('.index'), base_url)
return redirect(urljoin(base_url, path))
return send_file(directory)
......@@ -580,7 +581,7 @@ class FileAdmin(BaseView, ActionsMixin):
path = request.form.get('path')
if not path:
return redirect(url_for('.index'))
return redirect(self.get_url('.index'))
# Get path and verify if it is valid
base_path, full_path, path = self._normalize_path(path)
......@@ -624,7 +625,7 @@ class FileAdmin(BaseView, ActionsMixin):
path = request.args.get('path')
if not path:
return redirect(url_for('.index'))
return redirect(self.get_url('.index'))
base_path, full_path, path = self._normalize_path(path)
......@@ -669,13 +670,15 @@ class FileAdmin(BaseView, ActionsMixin):
"""
Edit view method
"""
path = request.args.getlist('path')
next_url = None
path = request.args.getlist('path')
if not path:
return redirect(url_for('.index'))
return redirect(self.get_url('.index'))
if len(path) > 1:
next_url = url_for('.edit', path=path[1:])
next_url = self.get_url('.edit', path=path[1:])
path = path[0]
base_path, full_path, path = self._normalize_path(path)
......@@ -753,4 +756,4 @@ class FileAdmin(BaseView, ActionsMixin):
@action('edit', lazy_gettext('Edit'))
def action_edit(self, items):
return redirect(url_for('.edit', path=items))
return redirect(self.get_url('.edit', path=items))
from flask import url_for
from jinja2 import Markup, escape
from mongoengine.base import BaseList
......@@ -20,7 +19,7 @@ def grid_formatter(view, value):
'<i class="icon-file"></i>%(name)s' +
'</a> %(size)dk (%(content_type)s)') %
{
'url': url_for('.api_file_view', **args),
'url': view.get_url('.api_file_view', **args),
'name': escape(value.name),
'size': value.length // 1024,
'content_type': escape(value.content_type)
......@@ -36,8 +35,8 @@ def grid_image_formatter(view, value):
'<a href="%(url)s" target="_blank"><img src="%(thumb)s"/></a>' +
'</div>') %
{
'url': url_for('.api_file_view', **helpers.make_gridfs_args(value)),
'thumb': url_for('.api_file_view', **helpers.make_thumb_args(value)),
'url': view.get_url('.api_file_view', **helpers.make_gridfs_args(value)),
'thumb': view.get_url('.api_file_view', **helpers.make_thumb_args(value)),
})
......
from wtforms.widgets import HTMLString, html_params
from jinja2 import escape
from flask import url_for
from mongoengine.fields import GridFSProxy, ImageGridFsProxy
from flask.ext.admin.helpers import get_url
from . import helpers
......@@ -53,7 +53,7 @@ class MongoImageInput(object):
if field.data and isinstance(field.data, ImageGridFsProxy):
args = helpers.make_thumb_args(field.data)
placeholder = self.template % {
'thumb': url_for('.api_file_view', **args),
'thumb': get_url('.api_file_view', **args),
'marker': '_%s-delete' % field.name
}
......
import os
import os.path as op
from flask import url_for
from werkzeug import secure_filename
from werkzeug.datastructures import FileStorage
......@@ -15,6 +13,7 @@ except ImportError:
from wtforms.utils import unset_value
from flask.ext.admin.babel import gettext
from flask.ext.admin.helpers import get_url
from flask.ext.admin._compat import string_types, urljoin
......@@ -106,7 +105,7 @@ class ImageUploadInput(object):
if field.url_relative_path:
filename = urljoin(field.url_relative_path, filename)
return url_for(field.endpoint, filename=filename)
return get_url(field.endpoint, filename=filename)
# Fields
......
from re import sub
from jinja2 import contextfunction
from flask import g, request
from flask import g, request, url_for
from wtforms.validators import DataRequired, InputRequired
from flask.ext.admin._compat import urljoin, urlparse
......@@ -20,6 +20,25 @@ def get_current_view():
return getattr(g, '_admin_view', None)
def get_url(endpoint, **kwargs):
"""
Alternative to Flask `url_for`.
If there's current administrative view, will call its `get_url`. If there's none - will
use generic `url_for`.
:param endpoint:
Endpoint name
:param kwargs:
View arguments
"""
view = get_current_view()
if not view:
return url_for(endpoint, **kwargs)
return view.get_url(endpoint, **kwargs)
def is_required_form_field(field):
"""
Check if form field has `DataRequired` or `InputRequired` validators.
......
......@@ -98,7 +98,7 @@ class MenuView(BaseMenu):
if self._cached_url:
return self._cached_url
self._cached_url = url_for('%s.%s' % (self._view.endpoint, self._view._default_view))
self._cached_url = self._view.get_url('%s.%s' % (self._view.endpoint, self._view._default_view))
return self._cached_url
def is_active(self, view):
......
import warnings
import re
from flask import request, url_for, redirect, flash, abort, json, Response
from flask import request, redirect, flash, abort, json, Response
from jinja2 import contextfunction
......@@ -24,6 +24,38 @@ filter_char_re = re.compile('[^a-z0-9 ]')
filter_compact_re = re.compile(' +')
class ViewArgs(object):
"""
List view arguments.
"""
def __init__(self, page=None, sort=None, sort_desc=None, search=None, filters=None, extra_args=None):
self.page = page
self.sort = sort
self.sort_desc = bool(sort_desc)
self.search = search
self.filters = filters
if not self.search:
self.search = None
self.extra_args = extra_args or dict()
def clone(self, **kwargs):
if self.filters:
flt = list(self.filters)
else:
flt = None
kwargs.setdefault('page', self.page)
kwargs.setdefault('sort', self.sort)
kwargs.setdefault('sort_desc', self.sort_desc)
kwargs.setdefault('search', self.search)
kwargs.setdefault('filters', flt)
kwargs.setdefault('extra_args', dict(self.extra_args))
return ViewArgs(**kwargs)
class BaseModelView(BaseView, ActionsMixin):
"""
Base model view.
......@@ -1076,50 +1108,39 @@ class BaseModelView(BaseView, ActionsMixin):
"""
Return arguments from query string.
"""
page = request.args.get('page', 0, type=int)
sort = request.args.get('sort', None, type=int)
sort_desc = request.args.get('desc', None, type=int)
search = request.args.get('search', None)
filters = self._get_list_filter_args()
return ViewArgs(page=request.args.get('page', 0, type=int),
sort=request.args.get('sort', None, type=int),
sort_desc=request.args.get('desc', None, type=int),
search=request.args.get('search', None),
filters=self._get_list_filter_args())
return page, sort, sort_desc, search, filters
def _get_url(self, view=None, page=None, sort=None, sort_desc=None,
search=None, filters=None):
# URL generation helpers
def _get_list_url(self, view_args):
"""
Generate page URL with current page, sort column and
other parameters.
:param view:
View name
:param page:
Page number
:param sort:
Sort column index
:param sort_desc:
Use descending sorting order
:param search:
Search query
:param filters:
List of active filters
:param view_args:
ViewArgs object with page number, filters, etc.
"""
if not search:
search = None
if not page:
page = None
page = view_args.page or None
desc = 1 if view_args.sort_desc else None
kwargs = dict(page=page, sort=sort, desc=sort_desc, search=search)
kwargs = dict(page=page, sort=view_args.sort, desc=desc, search=view_args.search)
kwargs.update(view_args.extra_args)
if filters:
for i, pair in enumerate(filters):
if view_args.filters:
for i, pair in enumerate(view_args.filters):
idx, value = pair
key = 'flt%d_%s' % (i, self.get_filter_arg(idx, self._filters[idx]))
kwargs[key] = value
return url_for(view, **kwargs)
return self.get_url('.index_view', **kwargs)
# Actions
def is_action_allowed(self, name):
"""
Override this method to allow or disallow actions based
......@@ -1196,16 +1217,16 @@ class BaseModelView(BaseView, ActionsMixin):
List view
"""
# Grab parameters from URL
page, sort_idx, sort_desc, search, filters = self._get_list_extra_args()
view_args = self._get_list_extra_args()
# Map column index to column name
sort_column = self._get_column_by_idx(sort_idx)
sort_column = self._get_column_by_idx(view_args.sort)
if sort_column is not None:
sort_column = sort_column[0]
# Get count and data
count, data = self.get_list(page, sort_column, sort_desc,
search, filters)
count, data = self.get_list(view_args.page, sort_column, view_args.sort_desc,
view_args.search, view_args.filters)
# Calculate number of pages
num_pages = count // self.page_size
......@@ -1218,21 +1239,25 @@ class BaseModelView(BaseView, ActionsMixin):
if p == 0:
p = None
return self._get_url('.index_view', p, sort_idx, sort_desc,
search, filters)
return self._get_list_url(view_args.clone(page=p))
def sort_url(column, invert=False):
desc = None
if invert and not sort_desc:
if invert and not view_args.sort_desc:
desc = 1
return self._get_url('.index_view', page, column, desc,
search, filters)
return self._get_list_url(view_args.clone(sort=column, sort_desc=desc))
# Actions
actions, actions_confirmation = self.get_actions_list()
clear_search_url = self._get_list_url(view_args.clone(page=0,
sort=view_args.sort,
sort_desc=view_args.sort_desc,
search=None,
filters=None))
return self.render(self.list_template,
data=data,
# List
......@@ -1242,32 +1267,24 @@ class BaseModelView(BaseView, ActionsMixin):
enumerate=enumerate,
get_pk_value=self.get_pk_value,
get_value=self.get_list_value,
return_url=self._get_url('.index_view',
page,
sort_idx,
sort_desc,
search,
filters),
return_url=self._get_list_url(view_args),
# Pagination
count=count,
pager_url=pager_url,
num_pages=num_pages,
page=page,
page=view_args.page,
# Sorting
sort_column=sort_idx,
sort_desc=sort_desc,
sort_column=view_args.sort,
sort_desc=view_args.sort_desc,
sort_url=sort_url,
# Search
search_supported=self._search_supported,
clear_search_url=self._get_url('.index_view',
None,
sort_idx,
sort_desc),
search=search,
clear_search_url=clear_search_url,
search=view_args.search,
# Filters
filters=self._filters,
filter_groups=self._filter_groups,
active_filters=filters,
active_filters=view_args.filters,
# Actions
actions=actions,
......@@ -1278,7 +1295,7 @@ class BaseModelView(BaseView, ActionsMixin):
"""
Create model view
"""
return_url = get_redirect_target() or url_for('.index_view')
return_url = get_redirect_target() or self.get_url('.index_view')
if not self.can_create:
return redirect(return_url)
......@@ -1289,7 +1306,7 @@ class BaseModelView(BaseView, ActionsMixin):
if self.create_model(form):
if '_add_another' in request.form:
flash(gettext('Model was successfully created.'))
return redirect(url_for('.create_view', url=return_url))
return redirect(self._get_create_url(url=return_url))
else:
return redirect(return_url)
......@@ -1306,7 +1323,7 @@ class BaseModelView(BaseView, ActionsMixin):
"""
Edit model view
"""
return_url = get_redirect_target() or url_for('.index_view')
return_url = get_redirect_target() or self.get_url('.index_view')
if not self.can_edit:
return redirect(return_url)
......@@ -1347,7 +1364,7 @@ class BaseModelView(BaseView, ActionsMixin):
"""
Delete model view. Only POST method is allowed.
"""
return_url = get_redirect_target() or url_for('.index_view')
return_url = get_redirect_target() or self.get_url('.index_view')
# TODO: Use post
if not self.can_delete:
......
from flask import url_for, json
from flask import json
from wtforms.widgets import HTMLString, html_params
from flask.ext.admin._compat import as_unicode
from flask.ext.admin.babel import gettext
from flask.ext.admin.helpers import get_url
from flask.ext.admin.form import RenderTemplateWidget
......@@ -26,7 +27,7 @@ class AjaxSelect2Widget(object):
def __call__(self, field, **kwargs):
kwargs['data-role'] = u'select2-ajax'
kwargs['data-url'] = url_for('.ajax_lookup', name=field.loader.name)
kwargs['data-url'] = get_url('.ajax_lookup', name=field.loader.name)
allow_blank = getattr(field, 'allow_blank', False)
if allow_blank and not self.multiple:
......
......@@ -50,14 +50,14 @@
<td>
{% block list_row_actions scoped %}
{% if admin_view.can_rename and path and name != '..' %}
<a class="icon" href="{{ url_for('.rename', path=path) }}">
<a class="icon" href="{{ get_url('.rename', path=path) }}">
<i class="icon-pencil"></i>
</a>
{% endif %}
{%- if admin_view.can_delete and path -%}
{% if is_dir %}
{% if name != '..' and admin_view.can_delete_dirs %}
<form class="icon" method="POST" action="{{ url_for('.delete') }}">
<form class="icon" method="POST" action="{{ get_url('.delete') }}">
<input type="hidden" name="path" value="{{ path }}"></input>
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\' recursively?', name=name) }}')">
<i class="icon-remove"></i>
......@@ -65,7 +65,7 @@
</form>
{% endif %}
{% else %}
<form class="icon" method="POST" action="{{ url_for('.delete') }}">
<form class="icon" method="POST" action="{{ get_url('.delete') }}">
<input type="hidden" name="path" value="{{ path }}"></input>
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\'?', name=name) }}')">
<i class="icon-remove"></i>
......@@ -118,7 +118,7 @@
</div>
{% endblock %}
{% block actions %}
{{ actionslib.form(actions, url_for('.action_view')) }}
{{ actionslib.form(actions, get_url('.action_view')) }}
{% endblock %}
{% endblock %}
......
......@@ -17,7 +17,7 @@
</li>
{% if admin_view.can_create %}
<li>
<a href="{{ url_for('.create_view', url=return_url) }}" title="{{ _gettext('Create new record') }}">{{ _gettext('Create') }}</a>
<a href="{{ get_url('.create_view', url=return_url) }}" title="{{ _gettext('Create new record') }}">{{ _gettext('Create') }}</a>
</li>
{% endif %}
......@@ -102,12 +102,12 @@
<td>
{% block list_row_actions scoped %}
{%- if admin_view.can_edit -%}
<a class="icon" href="{{ url_for('.edit_view', id=get_pk_value(row), url=return_url) }}" title="{{ _gettext('Edit record') }}">
<a class="icon" href="{{ get_url('.edit_view', id=get_pk_value(row), url=return_url) }}" title="{{ _gettext('Edit record') }}">
<i class="icon-pencil"></i>
</a>
{%- endif -%}
{%- if admin_view.can_delete -%}
<form class="icon" method="POST" action="{{ url_for('.delete_view', id=get_pk_value(row), url=return_url) }}">
<form class="icon" method="POST" action="{{ get_url('.delete_view', id=get_pk_value(row), url=return_url) }}">
{% if csrf_token %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
{% endif %}
......@@ -139,7 +139,7 @@
{{ lib.pager(page, num_pages, pager_url) }}
{% endblock %}
{{ actionlib.form(actions, url_for('.action_view')) }}
{{ actionlib.form(actions, get_url('.action_view')) }}
{% endblock %}
{% block tail %}
......
......@@ -24,7 +24,7 @@
<script src="{{ admin_static.url(filename='admin/js/rediscli.js') }}"></script>
<script language="javascript">
$(function() {
var redisCli = new RedisCli({{ url_for('.execute_view')|tojson }});
var redisCli = new RedisCli({{ get_url('.execute_view')|tojson }});
});
</script>
{% endblock %}
......@@ -50,14 +50,14 @@
<td>
{% block list_row_actions scoped %}
{% if admin_view.can_rename and path and name != '..' %}
<a class="icon" href="{{ url_for('.rename', path=path) }}">
<a class="icon" href="{{ get_url('.rename', path=path) }}">
<i class="glyphicon glyphicon-pencil"></i>
</a>
{% endif %}
{%- if admin_view.can_delete and path -%}
{% if is_dir %}
{% if name != '..' and admin_view.can_delete_dirs %}
<form class="icon" method="POST" action="{{ url_for('.delete') }}">
<form class="icon" method="POST" action="{{ get_url('.delete') }}">
<input type="hidden" name="path" value="{{ path }}"></input>
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\' recursively?', name=name) }}')">
<i class="icon-remove"></i>
......@@ -65,7 +65,7 @@
</form>
{% endif %}
{% else %}
<form class="icon" method="POST" action="{{ url_for('.delete') }}">
<form class="icon" method="POST" action="{{ get_url('.delete') }}">
<input type="hidden" name="path" value="{{ path }}"></input>
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\'?', name=name) }}')">
<i class="glyphicon glyphicon-trash"></i>
......@@ -118,7 +118,7 @@
</div>
{% endblock %}
{% block actions %}
{{ actionslib.form(actions, url_for('.action_view')) }}
{{ actionslib.form(actions, get_url('.action_view')) }}
{% endblock %}
{% endblock %}
......
......@@ -17,7 +17,7 @@
</li>
{% if admin_view.can_create %}
<li>
<a href="{{ url_for('.create_view', url=return_url) }}" title="{{ _gettext('Create new record') }}">{{ _gettext('Create') }}</a>
<a href="{{ get_url('.create_view', url=return_url) }}" title="{{ _gettext('Create new record') }}">{{ _gettext('Create') }}</a>
</li>
{% endif %}
......@@ -102,12 +102,12 @@
<td>
{% block list_row_actions scoped %}
{%- if admin_view.can_edit -%}
<a class="icon" href="{{ url_for('.edit_view', id=get_pk_value(row), url=return_url) }}" title="{{ _gettext('Edit record') }}">
<a class="icon" href="{{ get_url('.edit_view', id=get_pk_value(row), url=return_url) }}" title="{{ _gettext('Edit record') }}">
<span class="glyphicon glyphicon-pencil"></span>
</a>
{%- endif -%}
{%- if admin_view.can_delete -%}
<form class="icon" method="POST" action="{{ url_for('.delete_view', id=get_pk_value(row), url=return_url) }}">
<form class="icon" method="POST" action="{{ get_url('.delete_view', id=get_pk_value(row), url=return_url) }}">
{% if csrf_token %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
{% endif %}
......@@ -139,7 +139,7 @@
{{ lib.pager(page, num_pages, pager_url) }}
{% endblock %}
{{ actionlib.form(actions, url_for('.action_view')) }}
{{ actionlib.form(actions, get_url('.action_view')) }}
{% endblock %}
{% block tail %}
......
......@@ -24,7 +24,7 @@
<script src="{{ admin_static.url(filename='admin/js/rediscli.js') }}"></script>
<script language="javascript">
$(function() {
var redisCli = new RedisCli({{ url_for('.execute_view')|tojson }});
var redisCli = new RedisCli({{ admin_view.get_url('.execute_view')|tojson }});
});
</script>
{% 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