Commit ee46f523 authored by Serge S. Koval's avatar Serge S. Koval

Added localization support through Flask-Babel.

parent cf4179ad
- Core
- View Site button?
- Localization
- Model Admin
- Ability to sort by fields that are not visible?
- List display callables
- Form Fields
- Override field class by field name
- Verify how boolean field is rendered
- Reduce number of parameters passed to list view
- Checkboxes and mass operations
- Filters
- Custom filters for date fields?
- Checkboxes and mass operations
- Ability to sort by fields that are not visible?
- List display callables?
- SQLA Model Admin
- Many2Many support
- Verify if it is working properly
......@@ -17,7 +16,6 @@
- Header title
- Mass-delete functionality
- File size restriction
- Localization
- Unit tests
- Form generation tests
- Documentation
......
from flask import _request_ctx_stack
def _gettext(string, **variables):
return string % variables
def _ngettext(singular, plural, num, **variables):
return (singular if num == 1 else plural) % variables
def _lazy_gettext(string, **variables):
return string % variables
# Wrap flask-babel API
try:
from flask.ext import babel
def _is_babel_on():
ctx = _request_ctx_stack.top
if ctx is None:
return False
return hasattr(ctx, 'babel_locale')
def gettext(string, **variables):
if not _is_babel_on():
return _gettext(string, **variables)
return babel.gettext(string, **variables)
def ngettext(singular, plural, num, **variables):
if not _is_babel_on():
return _ngettext(singular, plural, num, **variables)
return babel.ngettext(singular, plural, num, **variables)
def lazy_gettext(string, **variables):
from speaklater import make_lazy_string
return make_lazy_string(gettext, string, **variables)
except ImportError:
gettext = _gettext
ngettext = _ngettext
lazy_gettext = _lazy_gettext
......@@ -3,6 +3,8 @@ from re import sub
from flask import Blueprint, render_template, url_for, abort
from flask.ext.adminex import babel
def expose(url='/', methods=('GET',)):
"""
......@@ -159,6 +161,11 @@ class BaseView(object):
# Store self as admin_view
kwargs['admin_view'] = self
# Provide i18n support even if flask-babel is not installed
# or enabled.
kwargs['_gettext'] = babel.gettext
kwargs['_ngettext'] = babel.ngettext
return render_template(template, **kwargs)
def _prettify_name(self, name):
......@@ -260,9 +267,6 @@ class MenuItem(object):
def get_children(self):
return [c for c in self._children if c.is_accessible()]
def __repr__(self):
return 'MenuItem %s (%s)' % (self.name, repr(self._children))
class Admin(object):
"""
......
......@@ -7,11 +7,12 @@ import shutil
from operator import itemgetter
from flask import flash, url_for, redirect, abort, request
from werkzeug import secure_filename
from flask import flash, url_for, redirect, abort, request
from flask.ext.adminex.base import BaseView, expose
from flask.ext.adminex.babel import gettext, lazy_gettext
from flask.ext.adminex import form
from flask.ext import wtf
......@@ -28,7 +29,7 @@ class NameForm(form.BaseForm):
def validate_name(self, field):
if not self.regexp.match(field.data):
raise wtf.ValidationError('Invalid directory name')
raise wtf.ValidationError(gettext('Invalid directory name'))
class UploadForm(form.BaseForm):
......@@ -36,7 +37,7 @@ class UploadForm(form.BaseForm):
File upload form. Works with FileAdmin instance to check if it is allowed
to upload file with given extension.
"""
upload = wtf.FileField('File to upload')
upload = wtf.FileField(lazy_gettext('File to upload'))
def __init__(self, admin):
self.admin = admin
......@@ -45,12 +46,12 @@ class UploadForm(form.BaseForm):
def validate_upload(self, field):
if not self.upload.has_file():
raise wtf.ValidationError('File required.')
raise wtf.ValidationError(gettext('File required.'))
filename = self.upload.data.filename
if not self.admin.is_file_allowed(filename):
raise wtf.ValidationError('Invalid file type.')
raise wtf.ValidationError(gettext('Invalid file type.'))
class FileAdmin(BaseView):
......@@ -331,7 +332,7 @@ class FileAdmin(BaseView):
base_path, directory, path = self._normalize_path(path)
if not self.can_upload:
flash('File uploading is disabled.', 'error')
flash(gettext('File uploading is disabled.'), 'error')
return redirect(self._get_dir_url('.index', path))
form = UploadForm(self)
......@@ -340,14 +341,14 @@ class FileAdmin(BaseView):
secure_filename(form.upload.data.filename))
if op.exists(filename):
flash('File "%s" already exists.' % form.upload.data.filename,
flash(gettext('File "%(name)s" already exists.', name=form.upload.data.filename),
'error')
else:
try:
self.save_file(filename, form.upload.data)
return redirect(self._get_dir_url('.index', path))
except Exception, ex:
flash('Failed to save file: %s' % ex)
flash(gettext('Failed to save file: %(error)s', error=ex))
return self.render(self.upload_template, form=form)
......@@ -366,7 +367,7 @@ class FileAdmin(BaseView):
dir_url = self._get_dir_url('.index', path)
if not self.can_mkdir:
flash('Directory creation is disabled.', 'error')
flash(gettext('Directory creation is disabled.'), 'error')
return redirect(dir_url)
form = NameForm(request.form)
......@@ -376,7 +377,7 @@ class FileAdmin(BaseView):
os.mkdir(op.join(directory, form.name.data))
return redirect(dir_url)
except Exception, ex:
flash('Failed to create directory: %s' % ex, 'error')
flash(gettext('Failed to create directory: %(error)s', ex), 'error')
return self.render(self.mkdir_template,
form=form,
......@@ -398,25 +399,25 @@ class FileAdmin(BaseView):
return_url = self._get_dir_url('.index', op.dirname(path))
if not self.can_delete:
flash('Deletion is disabled.')
flash(gettext('Deletion is disabled.'))
return redirect(return_url)
if op.isdir(full_path):
if not self.can_delete_dirs:
flash('Directory deletion is disabled.')
flash(gettext('Directory deletion is disabled.'))
return redirect(return_url)
try:
shutil.rmtree(full_path)
flash('Directory "%s" was successfully deleted.' % path)
flash(gettext('Directory "%s" was successfully deleted.' % path))
except Exception, ex:
flash('Failed to delete directory: %s' % ex, 'error')
flash(gettext('Failed to delete directory: %(error)s', error=ex), 'error')
else:
try:
os.remove(full_path)
flash('File "%s" was successfully deleted.' % path)
flash(gettext('File "%(name)s" was successfully deleted.', name=path))
except Exception, ex:
flash('Failed to delete file: %s' % ex, 'error')
flash(gettext('Failed to delete file: %(name)s', name=ex), 'error')
return redirect(return_url)
......@@ -435,11 +436,11 @@ class FileAdmin(BaseView):
return_url = self._get_dir_url('.index', op.dirname(path))
if not self.can_rename:
flash('Renaming is disabled.')
flash(gettext('Renaming is disabled.'))
return redirect(return_url)
if not op.exists(full_path):
flash('Path does not exist.')
flash(gettext('Path does not exist.'))
return redirect(return_url)
form = NameForm(request.form, name=op.basename(path))
......@@ -449,11 +450,11 @@ class FileAdmin(BaseView):
filename = secure_filename(form.name.data)
os.rename(full_path, op.join(dir_base, filename))
flash('Successfully renamed "%s" to "%s"' % (
op.basename(path),
filename))
flash(gettext('Successfully renamed "%(src)s" to "%(dst)s"',
src=op.basename(path),
dst=filename))
except Exception, ex:
flash('Failed to rename: %s' % ex, 'error')
flash(gettext('Failed to rename: %(error)s', error=ex), 'error')
return redirect(return_url)
......
from flask.ext.babel import gettext
from flask.ext.adminex.model import filters
from flask.ext.adminex.ext.sqlamodel import tools
......@@ -30,7 +32,7 @@ class FilterEqual(BaseSQLAFilter):
return query.filter(self.column == value)
def operation(self):
return 'equals'
return gettext('equals')
class FilterNotEqual(BaseSQLAFilter):
......@@ -38,7 +40,7 @@ class FilterNotEqual(BaseSQLAFilter):
return query.filter(self.column != value)
def operation(self):
return 'not equal'
return gettext('not equal')
class FilterLike(BaseSQLAFilter):
......@@ -47,7 +49,7 @@ class FilterLike(BaseSQLAFilter):
return query.filter(self.column.ilike(stmt))
def operation(self):
return 'like'
return gettext('contains')
class FilterNotLike(BaseSQLAFilter):
......@@ -56,7 +58,7 @@ class FilterNotLike(BaseSQLAFilter):
return query.filter(~self.column.ilike(stmt))
def operation(self):
return 'not like'
return gettext('not contains')
class FilterGreater(BaseSQLAFilter):
......@@ -64,7 +66,7 @@ class FilterGreater(BaseSQLAFilter):
return query.filter(self.column > value)
def operation(self):
return 'greater than'
return gettext('greater than')
class FilterSmaller(BaseSQLAFilter):
......@@ -72,7 +74,7 @@ class FilterSmaller(BaseSQLAFilter):
return query.filter(self.column < value)
def operation(self):
return 'smaller than'
return gettext('smaller than')
# Customized type filters
......
......@@ -6,6 +6,7 @@ from sqlalchemy import or_
from wtforms.ext.sqlalchemy.orm import model_form
from flask import flash
from flask.ext.babel import gettext
from flask.ext.adminex.form import BaseForm
from flask.ext.adminex.model import BaseModelView
......@@ -500,7 +501,7 @@ class ModelView(BaseModelView):
self.session.commit()
return True
except Exception, ex:
flash('Failed to create model. ' + str(ex), 'error')
flash(gettext('Failed to create model. %(error)s', error=str(ex)), 'error')
return False
def update_model(self, form, model):
......@@ -515,7 +516,7 @@ class ModelView(BaseModelView):
self.session.commit()
return True
except Exception, ex:
flash('Failed to update model. ' + str(ex), 'error')
flash(gettext('Failed to update model. %(error)s', error=str(ex)), 'error')
return False
def delete_model(self, model):
......@@ -530,5 +531,5 @@ class ModelView(BaseModelView):
self.session.commit()
return True
except Exception, ex:
flash('Failed to delete model. ' + str(ex), 'error')
flash(gettext('Failed to delete model. %(error)s', error=str(ex)), 'error')
return False
......@@ -4,6 +4,8 @@ import datetime
from flask.ext import wtf
from wtforms import fields, widgets
from flask.ext.adminex.babel import gettext
class BaseForm(wtf.Form):
"""
......@@ -76,7 +78,7 @@ class TimeField(fields.Field):
except ValueError:
pass
raise ValueError('Invalid time format')
raise ValueError(gettext('Invalid time format'))
class ChosenSelectWidget(widgets.Select):
......
from flask import request, url_for, redirect, flash
from flask.ext.babel import gettext
from flask.ext.adminex.base import BaseView, expose
from flask.ext.adminex.model import filters
......@@ -737,7 +739,7 @@ class BaseModelView(BaseView):
if form.validate_on_submit():
if self.create_model(form):
if '_add_another' in request.form:
flash('Model was successfully created.')
flash(gettext('Model was successfully created.'))
return redirect(url_for('.create_view', url=return_url))
else:
return redirect(return_url)
......
from flask.ext.babel import lazy_gettext
class BaseFilter(object):
"""
Base filter class.
......@@ -76,7 +79,8 @@ class BaseBooleanFilter(BaseFilter):
"""
def __init__(self, name, data_type=None):
super(BaseBooleanFilter, self).__init__(name,
(('1', 'Yes'), ('0', 'No')),
(('1', lazy_gettext('Yes')),
('0', lazy_gettext('No'))),
data_type)
def validate(self, value):
......
{% extends 'admin/master.html' %}
{% import 'admin/lib.html' as lib %}
{% import 'admin/lib.html' as lib with context %}
{% block body %}
{{ lib.render_form(form, dir_url) }}
......
{% extends 'admin/master.html' %}
{% import 'admin/lib.html' as lib %}
{% import 'admin/lib.html' as lib with context %}
{% block body %}
<ul class="breadcrumb">
<li>
<a href="{{ get_dir_url('.index', path=None) }}">Root</a>
<a href="{{ get_dir_url('.index', path=None) }}">{{ _gettext('Root') }}</a>
</li>
{% for name, path in breadcrumbs[:-1] %}
<li>
......@@ -39,7 +39,7 @@
{% if name != '..' and admin_view.can_delete_dirs %}
<form class="icon" method="POST" action="{{ url_for('.delete') }}">
<input type="hidden" name="path" value="{{ path }}"></input>
<button onclick="return confirm('Are you sure you want to delete \'{{ name }}\' recursively?')">
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\' recursively?', name=name) }}')">
<i class="icon-remove"></i>
</button>
</form>
......@@ -47,7 +47,7 @@
{% else %}
<form class="icon" method="POST" action="{{ url_for('.delete') }}">
<input type="hidden" name="path" value="{{ path }}"></input>
<button onclick="return confirm('Are you sure you want to delete \'{{ name }}\'?')">
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\'?', name=name) }}')">
<i class="icon-remove"></i>
</button>
</form>
......@@ -72,9 +72,9 @@
{% endfor %}
</table>
{% if admin_view.can_upload %}
<a class="btn btn-primary btn-large" href="{{ get_dir_url('.upload', path=dir_path) }}">Upload File</a>
<a class="btn btn-primary btn-large" href="{{ get_dir_url('.upload', path=dir_path) }}">{{ _gettext('Upload File') }}</a>
{% endif %}
{% if admin_view.can_mkdir %}
<a class="btn btn-primary btn-large" href="{{ get_dir_url('.mkdir', path=dir_path) }}">Create Directory</a>
<a class="btn btn-primary btn-large" href="{{ get_dir_url('.mkdir', path=dir_path) }}">{{ _gettext('Create Directory') }}</a>
{% endif %}
{% endblock %}
{% extends 'admin/master.html' %}
{% import 'admin/lib.html' as lib %}
{% import 'admin/lib.html' as lib with context %}
{% block body %}
<h3>Please provide new name for <i>{{ name }}</i></h3>
<h3>{{ _gettext('Please provide new name for %(name)s', name=name) }}</h3>
{{ lib.render_form(form, dir_url) }}
{% endblock %}
\ No newline at end of file
......@@ -107,7 +107,7 @@
{{ extra }}
{% endif %}
{% if cancel_url %}
<a href="{{ cancel_url }}" class="btn btn-large">Cancel</a>
<a href="{{ cancel_url }}" class="btn btn-large">{{ _gettext('Cancel') }}</a>
{% endif %}
</div>
</div>
......
{% extends 'admin/master.html' %}
{% import 'admin/lib.html' as lib %}
{% import 'admin/lib.html' as lib with context %}
{% block head %}
<link href="{{ url_for('admin.static', filename='chosen/chosen.css') }}" rel="stylesheet">
......@@ -8,7 +8,7 @@
{% block body %}
{% macro extra() %}
<input name="_add_another" type="submit" class="btn btn-primary btn-large" value="Save and Add" />
<input name="_add_another" type="submit" class="btn btn-primary btn-large" value="{{ _gettext('Save and Add') }}" />
{% endmacro %}
<ul class="nav nav-tabs">
......@@ -16,7 +16,7 @@
<a href="{{ return_url }}">List</a>
</li>
<li class="active">
<a href="#">Create</a>
<a href="#">{{ _gettext('Create') }}</a>
</li>
</ul>
{{ lib.render_form(form, return_url, extra()) }}
......
{% extends 'admin/master.html' %}
{% import 'admin/lib.html' as lib %}
{% import 'admin/lib.html' as lib with context %}
{% block head %}
<link href="{{ url_for('admin.static', filename='chosen/chosen.css') }}" rel="stylesheet">
......
{% extends 'admin/master.html' %}
{% import 'admin/lib.html' as lib %}
{% import 'admin/lib.html' as lib with context %}
{% block head %}
<link href="{{ url_for('admin.static', filename='chosen/chosen.css') }}" rel="stylesheet">
......@@ -9,18 +9,18 @@
{% block body %}
<ul class="nav nav-tabs">
<li class="active">
<a href="#">List ({{ count }})</a>
<a href="#">{{ _gettext('List') }} ({{ count }})</a>
</li>
{% if admin_view.can_create %}
<li>
<a href="{{ url_for('.create_view', url=return_url) }}">Create</a>
<a href="{{ url_for('.create_view', url=return_url) }}">{{ _gettext('Create') }}</a>
</li>
{% endif %}
{% if filter_groups %}
<li class="dropdown">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
Add Filter<b class="caret"></b>
{{ _gettext('Add Filter') }}<b class="caret"></b>
</a>
<ul class="dropdown-menu field-filters">
{% for k in filter_groups %}
......@@ -41,7 +41,7 @@
{% if sort_desc %}
<input type="hidden" name="desc" value="{{ sort_desc }}"></input>
{% endif %}
<input type="text" name="search" value="{{ search or '' }}" class="search-query span2" placeholder="Search"></input>
<input type="text" name="search" value="{{ search or '' }}" class="search-query span2" placeholder="{{ _gettext('Search') }}"></input>
{% if search %}
<a href="{{ clear_search_url }}" class="clear">
<i class="icon-remove"></i>
......@@ -54,9 +54,9 @@
{% if filter_groups %}
<form id="filter_form" method="GET" action="{{ return_url }}">
<div class="pull-right">
<button type="submit" class="btn btn-primary" style="display: none">Apply</button>
<button type="submit" class="btn btn-primary" style="display: none">{{ _gettext('Apply') }}</button>
{% if active_filters %}
<a href="{{ clear_search_url }}" class="btn">Reset Filters</a>
<a href="{{ clear_search_url }}" class="btn">{{ _gettext('Reset Filters') }}</a>
{% endif %}
</div>
......@@ -64,7 +64,7 @@
{%- for i, flt in enumerate(active_filters) -%}
<div class="filter-row">
{% set filter = admin_view._filters[flt[0]] %}
<a href="#" class="btn remove-filter" title="Remove Filter">
<a href="#" class="btn remove-filter" title="{{ _gettext('Remove Filter') }}">
{{ filters[flt[0]] }}
</a><select class="filter-op" data-role="chosen">
{% for op in admin_view._filter_dict[filter.name] %}
......@@ -125,7 +125,7 @@
{%- endif -%}
{%- if admin_view.can_delete -%}
<form class="icon" method="POST" action="{{ url_for('.delete_view', id=get_pk_value(row), url=return_url) }}">
<button onclick="return confirm('You sure you want to delete this item?')">
<button onclick="return confirm('{{ _gettext('You sure you want to delete this item?') }}');">
<i class="icon-remove"></i>
</button>
</form>
......
......@@ -182,7 +182,6 @@ def test_submenu():
eq_(admin._menu[1].is_accessible(), False)
eq_(len(admin._menu[1].get_children()), 1)
ok_(repr(admin._menu[1]).startswith('MenuItem '))
def test_delayed_init():
......
......@@ -217,8 +217,8 @@ def test_column_filters():
eq_(view._filter_dict, {'Test1': [(0, 'equals'),
(1, 'not equal'),
(2, 'like'),
(3, 'not like')]})
(2, 'contains'),
(3, 'not contains')]})
db.session.add(Model1('model1'))
db.session.add(Model1('model2'))
......
......@@ -18,7 +18,7 @@ setup(
platforms='any',
install_requires=[
'Flask>=0.7',
'Flask-WTF>=0.6',
'Flask-WTF>=0.6'
],
tests_require=[
'nose>=1.0'
......
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