Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace Flask-BabelEx with Flask-Babel #2460

Merged
merged 5 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,20 @@ on:
- '*.rst'
jobs:
tests:
name: ${{ format('py{0}-wtforms{1}', matrix.python, matrix.wtforms) }}
name: ${{ matrix.tox == 'normal' && format('py{0}-flask{1}-wtforms{2}', matrix.python, matrix.flask, matrix.wtforms) || matrix.tox }}
runs-on: ${{ matrix.os || 'ubuntu-latest' }}
strategy:
fail-fast: false
matrix:
python: ['3.8', '3.9', '3.10', '3.11', '3.12']
flask: ['2']
wtforms: ['2']
tox: ['normal']
include:
- python: '3.12'
flask: '2'
wtforms: '2'
tox: 'py312-flask2-wtforms2-no-flask-babel'
services:
# Label used to access the service container
postgres:
Expand Down Expand Up @@ -68,7 +75,7 @@ jobs:
PGPASSWORD: postgres
run: psql -U postgres -h localhost -c 'CREATE EXTENSION hstore;' flask_admin_test
- run: pip install tox
- run: tox run -e ${{ matrix.tox || format('py{0}-wtforms{1}', matrix.python, matrix.wtforms) }}
- run: tox run -e ${{ matrix.tox == 'normal' && format('py{0}-flask{1}-wtforms{2}', matrix.python, matrix.flask, matrix.wtforms) || matrix.tox }}
not_tests:
name: ${{ matrix.tox }}
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ Flask-Admin is built with the help of
[Bootswatch](http://bootswatch.com/).

If you want to localize your application, install the
[Flask-BabelEx](https://pypi.python.org/pypi/Flask-BabelEx) package.
[Flask-Babel](https://pypi.python.org/pypi/Flask-Babel) package.

You can help improve Flask-Admin\'s translations through Crowdin:
<https://crowdin.com/project/flask-admin>
28 changes: 13 additions & 15 deletions doc/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,43 +31,41 @@ To add custom JavaScript or CSS in your *ModelView* use *extra_js* or *extra_css
extra_js = ['https://example.com/custom.js']
extra_css = ['https://example.com/custom.css']

Localization With Flask-Babelex
Localization With Flask-Babel
-------------------------------

****

Flask-Admin comes with translations for several languages.
Enabling localization is simple:

#. Install `Flask-BabelEx <http://github.com/mrjoes/flask-babelex/>`_ to do the heavy
lifting. It's a fork of the
`Flask-Babel <http://github.com/mitshuhiko/flask-babel/>`_ package::
#. Install `Flask-Babel <https://github.com/python-babel/flask-babel/>`_ to do the heavy
lifting.

pip install flask-babelex

#. Initialize Flask-BabelEx by creating instance of `Babel` class::

from flask import Flask
from flask_babelex import Babel

app = Flask(__name__)
babel = Babel(app)
pip install flask-babel

#. Create a locale selector function::

@babel.localeselector
def get_locale():
if request.args.get('lang'):
session['lang'] = request.args.get('lang')
return session.get('lang', 'en')

#. Initialize Flask-Babel by creating instance of `Babel` class::

from flask import Flask
from flask_babel import Babel

app = Flask(__name__)
babel = Babel(app, locale_selector=get_locale)

Now, you could try a French version of the application at: `http://localhost:5000/admin/?lang=fr <http://localhost:5000/admin/?lang=fr>`_.

Go ahead and add your own logic to the locale selector function. The application can store locale in
a user profile, cookie, session, etc. It can also use the `Accept-Language`
header to make the selection automatically.

If the built-in translations are not enough, look at the `Flask-BabelEx documentation <https://pythonhosted.org/Flask-BabelEx/>`_
If the built-in translations are not enough, look at the `Flask-Babel documentation <https://pythonhosted.org/Flask-Babel/>`_
to see how you can add your own.

.. _file-admin:
Expand Down
4 changes: 1 addition & 3 deletions examples/babel/README.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
This example show how to translate Flask-Admin into different language using customized version of the `Flask-Babel <https://github.com/mrjoes/flask-babelex>`
This example show how to translate Flask-Admin into different language using customized version of the `Flask-Babel <https://github.com/python-babel/flask-babel>`

To run this example:

Expand All @@ -19,5 +19,3 @@ To run this example:
4. Run the application::

python examples/babel/app.py


9 changes: 4 additions & 5 deletions examples/babel/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from flask_sqlalchemy import SQLAlchemy

import flask_admin as admin
from flask_babelex import Babel
from flask_babel import Babel

from flask_admin.contrib import sqla

Expand All @@ -17,11 +17,7 @@
app.config['SQLALCHEMY_ECHO'] = True
db = SQLAlchemy(app)

# Initialize babel
babel = Babel(app)


@babel.localeselector
def get_locale():
override = request.args.get('lang')

Expand All @@ -30,6 +26,9 @@ def get_locale():

return session.get('lang', 'en')

# Initialize babel
babel = Babel(app, locale_selector=get_locale)


# Create models
class User(db.Model):
Expand Down
2 changes: 1 addition & 1 deletion examples/babel/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Flask
Flask-Admin
Flask-SQLAlchemy
Flask-BabelEx
Flask-Babel
2 changes: 1 addition & 1 deletion examples/sqla/admin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from flask import Flask, request, session
from flask_sqlalchemy import SQLAlchemy
from flask_babelex import Babel
from flask_babel import Babel


app = Flask(__name__)
Expand Down
2 changes: 1 addition & 1 deletion examples/sqla/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Flask
Flask-Admin
Flask-BabelEx
Flask-Babel
Flask-SQLAlchemy
tablib
enum34; python_version < '3.0'
Expand Down
5 changes: 1 addition & 4 deletions flask_admin/babel.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
try:
try:
from flask_babelex import Domain
except ImportError:
from flask_babel import Domain
from flask_babel import Domain

except ImportError:
def gettext(string, **variables):
Expand Down
20 changes: 20 additions & 0 deletions flask_admin/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import pytest


def flask_babel_test_decorator(fn):
"""Decorator to annotate any tests that *require* Flask-Babel to be available,
ie they check translations directly, with the `flask_babel` mark.

If Flask-Babel is not installed, we add the `xfail` mark to the test so that pytest
will expect, and therefore ignore, its failure.
"""
fn = pytest.mark.flask_babel(fn)

try:
import flask_babel
except ImportError:
return pytest.mark.xfail(
reason="flask-babel is not installed; translations unavailable"
)(fn)

return fn
30 changes: 30 additions & 0 deletions flask_admin/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import pytest
from flask import Flask

from flask_admin import Admin


@pytest.fixture(scope='function')
def app():
app = Flask(__name__)
app.config['SECRET_KEY'] = '1'
app.config['WTF_CSRF_ENABLED'] = False

yield app


@pytest.fixture
def babel(app):
try:
from flask_babel import Babel
_ = Babel(app)
except ImportError:
pass

yield babel


@pytest.fixture
def admin(app, babel):
admin = Admin(app)
yield admin
11 changes: 0 additions & 11 deletions flask_admin/tests/fileadmin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +0,0 @@
from flask import Flask
from flask_admin import Admin


def setup():
app = Flask(__name__)
app.config['SECRET_KEY'] = '1'
app.config['CSRF_ENABLED'] = False

admin = Admin(app)
return app, admin
48 changes: 29 additions & 19 deletions flask_admin/tests/fileadmin/test_fileadmin.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
from io import StringIO
import os
import os.path as op
import unittest

from flask_admin.contrib import fileadmin
from flask_admin import Admin
from flask import Flask

from . import setup


class Base:
class FileAdminTests(unittest.TestCase):
class FileAdminTests:
_test_files_root = op.join(op.dirname(__file__), 'files')

def fileadmin_class(self):
Expand All @@ -20,12 +17,10 @@ def fileadmin_class(self):
def fileadmin_args(self):
raise NotImplementedError

def test_file_admin(self):
def test_file_admin(self, app, admin):
fileadmin_class = self.fileadmin_class()
fileadmin_args, fileadmin_kwargs = self.fileadmin_args()

app, admin = setup()

class MyFileAdmin(fileadmin_class):
editable_extensions = ('txt',)

Expand Down Expand Up @@ -132,10 +127,8 @@ class MyFileAdmin(fileadmin_class):
assert 'path=dummy_renamed_dir' not in rv.data.decode('utf-8')
assert 'path=dummy.txt' in rv.data.decode('utf-8')

def test_modal_edit(self):
# bootstrap 2 - test edit_modal
app_bs2 = Flask(__name__)
admin_bs2 = Admin(app_bs2, template_mode="bootstrap2")
def test_modal_edit_bs2(self, app, babel):
admin_bs2 = Admin(app, template_mode="bootstrap2")

fileadmin_class = self.fileadmin_class()
fileadmin_args, fileadmin_kwargs = self.fileadmin_args()
Expand All @@ -159,7 +152,7 @@ class EditModalOff(fileadmin_class):
admin_bs2.add_view(edit_modal_on)
admin_bs2.add_view(edit_modal_off)

client_bs2 = app_bs2.test_client()
client_bs2 = app.test_client()

# bootstrap 2 - ensure modal window is added when edit_modal is
# enabled
Expand All @@ -174,14 +167,32 @@ class EditModalOff(fileadmin_class):
data = rv.data.decode('utf-8')
assert 'fa_modal_window' not in data

# bootstrap 3
app_bs3 = Flask(__name__)
admin_bs3 = Admin(app_bs3, template_mode="bootstrap3")
def test_modal_edit_bs3(self, app, babel):
admin_bs3 = Admin(app, template_mode="bootstrap3")

fileadmin_class = self.fileadmin_class()
fileadmin_args, fileadmin_kwargs = self.fileadmin_args()

class EditModalOn(fileadmin_class):
edit_modal = True
editable_extensions = ('txt',)

class EditModalOff(fileadmin_class):
edit_modal = False
editable_extensions = ('txt',)

on_view_kwargs = dict(fileadmin_kwargs)
on_view_kwargs.setdefault('endpoint', 'edit_modal_on')
edit_modal_on = EditModalOn(*fileadmin_args, **on_view_kwargs)

off_view_kwargs = dict(fileadmin_kwargs)
off_view_kwargs.setdefault('endpoint', 'edit_modal_off')
edit_modal_off = EditModalOff(*fileadmin_args, **off_view_kwargs)

admin_bs3.add_view(edit_modal_on)
admin_bs3.add_view(edit_modal_off)

client_bs3 = app_bs3.test_client()
client_bs3 = app.test_client()

# bootstrap 3 - ensure modal window is added when edit_modal is
# enabled
Expand All @@ -197,17 +208,16 @@ class EditModalOff(fileadmin_class):
assert 'fa_modal_window' not in data


class LocalFileAdminTests(Base.FileAdminTests):
class TestLocalFileAdmin(Base.FileAdminTests):
def fileadmin_class(self):
return fileadmin.FileAdmin

def fileadmin_args(self):
return (self._test_files_root, '/files'), {}

def test_fileadmin_sort_bogus_url_param(self):
def test_fileadmin_sort_bogus_url_param(self, app, admin):
fileadmin_class = self.fileadmin_class()
fileadmin_args, fileadmin_kwargs = self.fileadmin_args()
app, admin = setup()

class MyFileAdmin(fileadmin_class):
editable_extensions = ('txt',)
Expand Down
10 changes: 7 additions & 3 deletions flask_admin/tests/fileadmin/test_fileadmin_azure.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@

from unittest import SkipTest

import pytest

from flask_admin.contrib.fileadmin import azure

from .test_fileadmin import Base


class AzureFileAdminTests(Base.FileAdminTests):
class TestAzureFileAdmin(Base.FileAdminTests):
_test_storage = getenv('AZURE_STORAGE_CONNECTION_STRING')

def setUp(self):
@pytest.fixture(autouse=True)
def setup_and_teardown(self):
if not azure.BlockBlobService:
raise SkipTest('AzureFileAdmin dependencies not installed')

Expand All @@ -26,7 +29,8 @@ def setUp(self):
dummy = op.join(self._test_files_root, 'dummy.txt')
client.create_blob_from_path(self._container_name, 'dummy.txt', dummy)

def tearDown(self):
yield

client = azure.BlockBlobService(connection_string=self._test_storage)
client.delete_container(self._container_name)

Expand Down
20 changes: 0 additions & 20 deletions flask_admin/tests/geoa/__init__.py
Original file line number Diff line number Diff line change
@@ -1,20 +0,0 @@
from flask import Flask
from flask_admin import Admin
from flask_sqlalchemy import SQLAlchemy


def setup():
app = Flask(__name__)
app.config['SECRET_KEY'] = '1'
app.config['CSRF_ENABLED'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://postgres:postgres@localhost/flask_admin_test'
app.config['SQLALCHEMY_ECHO'] = True
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy()
db.init_app(app)
admin = Admin(app)

app.app_context().push()

return app, db, admin
Loading
Loading