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

Support latest flask, sqlalchemy, flask-sqlalchemy and wtforms #2328

Closed
wants to merge 33 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
b5bc87c
Drop unused requirements
cjmayo Oct 19, 2023
53778e6
Make indirect requirements clearer
cjmayo Oct 19, 2023
fa08f57
Move test requirements into tox.ini
cjmayo Oct 19, 2023
734f660
Rename tox-version to tox-env
cjmayo Oct 19, 2023
44305b2
Use Python 3.x for checks
cjmayo Oct 19, 2023
323021d
Drop WTForms test environments
cjmayo Oct 19, 2023
c0f0c52
Don't need services for flake8 and sphinx
cjmayo Oct 19, 2023
6fac9ac
Set permissions on workflow
cjmayo Oct 19, 2023
a25bec9
Log information about tests that didn't pass
cjmayo Oct 19, 2023
8d60eee
Ensure Azure tests are not skipped
cjmayo Oct 19, 2023
f1945f3
Test the minimum requirements
cjmayo Oct 19, 2023
bdd45d1
flask-babelex requires pytz
cjmayo Oct 19, 2023
2c7cd11
Minimum version of sqlalchemy raised to 1.4
cjmayo Oct 19, 2023
bf22d18
Make minimum requirements Python 3.11 compatible
cjmayo Oct 19, 2023
4b71c70
Resolve test_multi_pk.py Flask-SQLAlchemy Model DeprecationWarning
dgilman Nov 1, 2023
f9330d8
Fix register_blueprint AssertionError in tests
Nov 1, 2023
fe037b6
Fix test_export_csv AssertionError
Nov 1, 2023
2c61488
Fix test_different_bind_joins UnboundExecutionError
cjmayo Nov 1, 2023
bfe9947
Fix test_models AssertionError: assert None == ''
cjmayo Nov 1, 2023
a608123
Fix test_citext 'Engine' object has no attribute 'execute'
cjmayo Nov 1, 2023
5d6da21
Resolve SQLAlchemy 2.0 "declarative_base() now in sqlalchemy.orm"
cjmayo Nov 1, 2023
b2cb244
Fix translating after WTForms 3 removed form._get_translations
cjmayo Nov 1, 2023
2027135
Fix Exception: Cannot find reverse relation for model
cjmayo Nov 1, 2023
f35f519
Resolve LegacyAPIWarning for Query.get()
dgilman Nov 1, 2023
69c9b28
Remove SQLAlchemy < 1.4 imports and comments
cjmayo Nov 1, 2023
b215278
Remove non-essential restrictions from requirements-dev.txt
cjmayo Nov 1, 2023
c6d5f3e
Exclude tests using flask_babelex
cjmayo Nov 1, 2023
291e75b
Exclude monoengine tests
cjmayo Nov 1, 2023
64fa2eb
sqlalchemy-utils CurrencyType needs Babel
cjmayo Nov 1, 2023
a3276d6
Drop support for Python 3.7, add 3.12
cjmayo Nov 1, 2023
cf6c71c
Fix test_file_admin: "a bytes-like object is required, not 'str'"
cjmayo Nov 1, 2023
eb0ce69
Fix test_safe_redirect after Werkzeug 2.3.0 updated safe chars
cjmayo Nov 1, 2023
def21a9
Exclude wtf-peewee 3.0.5
cjmayo Nov 1, 2023
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
33 changes: 25 additions & 8 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
name: Run tests
permissions:
contents: read
on:
push:
branches:
Expand All @@ -8,17 +10,15 @@ on:
- master

jobs:
test-job:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']
tox-version: [ 'WTForms2' ]
python-version: ['3.9', '3.10', '3.11', '3.12']
tox-env: [py]
include:
- python-version: 3.11
tox-version: flake8
- python-version: 3.11
tox-version: docs-html
- python-version: '3.8'
tox-env: minreqs
services:
# Label used to access the service container
postgres:
Expand Down Expand Up @@ -64,4 +64,21 @@ jobs:
- name: Install tox
run: pip install tox
- name: Run tests
run: tox -e ${{ matrix.tox-version }}
run: tox -e ${{ matrix.tox-env }}
checks:
name: ${{ matrix.tox-env }}
runs-on: ubuntu-latest
strategy:
matrix:
tox-env: [flake8, docs-html]
steps:
- name: Check out repository code
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Install tox
run: pip install tox
- name: Run ${{ matrix.tox-env }}
run: tox -e ${{ matrix.tox-env }}
2 changes: 1 addition & 1 deletion flask_admin/contrib/sqla/ajax.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def get_query(self):
def get_one(self, pk):
# prevent autoflush from occuring during populate_obj
with self.session.no_autoflush:
return self.get_query().get(pk)
return self.session.get(self.model, pk)

def get_list(self, term, offset=0, limit=DEFAULT_PAGE_SIZE):
query = self.get_query()
Expand Down
1 change: 0 additions & 1 deletion flask_admin/contrib/sqla/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,6 @@ def apply(self, query, value, alias=None):
class DateNotBetweenFilter(DateBetweenFilter):
def apply(self, query, value, alias=None):
start, end = value
# ~between() isn't possible until sqlalchemy 1.0.0
return query.filter(not_(self.get_column(alias).between(start, end)))

def operation(self):
Expand Down
1 change: 1 addition & 0 deletions flask_admin/contrib/sqla/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,7 @@ def _calculate_mapping_key_pair(self, model, info):

reverse_props = []
forward_reverse_props_keys = dict()
model.registry.configure()
for prop in target_mapper.iterate_properties:
if hasattr(prop, 'direction') and prop.direction.name in ('MANYTOONE', 'MANYTOMANY'):
if issubclass(model, prop.mapper.class_):
Expand Down
9 changes: 2 additions & 7 deletions flask_admin/contrib/sqla/tools.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
import types

from sqlalchemy import tuple_, or_, and_, inspect
try:
# Attempt _class_resolver import from SQLALchemy 1.4/2.0 module architecture.
from sqlalchemy.orm.clsregistry import _class_resolver
except ImportError:
# If 1.4/2.0 module import fails, fall back to <1.3.x architecture.
from sqlalchemy.ext.declarative.clsregistry import _class_resolver
from sqlalchemy.orm.clsregistry import _class_resolver
from sqlalchemy.ext.hybrid import hybrid_property
try:
# Attempt ASSOCATION_PROXY import from pre-2.0 release
from sqlalchemy.ext.associationproxy import ASSOCIATION_PROXY
except ImportError:
# SQLAlchemy 2.0
from sqlalchemy.ext.associationproxy import AssociationProxyExtensionType
ASSOCIATION_PROXY = AssociationProxyExtensionType.ASSOCIATION_PROXY
from sqlalchemy.sql.operators import eq
Expand Down
1 change: 1 addition & 0 deletions flask_admin/contrib/sqla/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ def _get_model_iterator(self, model=None):
if model is None:
model = self.model

model.registry.configure()
return model._sa_class_manager.mapper.iterate_properties

def _apply_path_joins(self, query, joins, path, inner_join=True):
Expand Down
9 changes: 5 additions & 4 deletions flask_admin/form/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,17 @@


class BaseForm(form.Form):
_translations = Translations()
class Meta:
_translations = Translations()

def get_translations(self, form):
return self._translations

def __init__(self, formdata=None, obj=None, prefix=u'', **kwargs):
self._obj = obj

super(BaseForm, self).__init__(formdata=formdata, obj=obj, prefix=prefix, **kwargs)

def _get_translations(self):
return self._translations


class FormOpts(object):
__slots__ = ['widget_args', 'form_rules']
Expand Down
4 changes: 2 additions & 2 deletions flask_admin/tests/fileadmin/test_fileadmin.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from io import StringIO
from io import BytesIO
import os
import os.path as op
import unittest
Expand Down Expand Up @@ -76,7 +76,7 @@ class MyFileAdmin(fileadmin_class):
assert rv.status_code == 200

rv = client.post('/admin/myfileadmin/upload/',
data=dict(upload=(StringIO(""), 'dummy.txt')))
data=dict(upload=(BytesIO("".encode("utf8")), 'dummy.txt')))
assert rv.status_code == 302

rv = client.get('/admin/myfileadmin/')
Expand Down
74 changes: 37 additions & 37 deletions flask_admin/tests/mongoengine/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ def test_model():
model = Model1.objects.first()
assert model.test1 == 'test1large'
assert model.test2 == 'test2'
assert model.test3 == ''
assert model.test4 == ''
assert model.test3 in ('', None) # WTForms 2, WTForms 3
assert model.test4 in ('', None) # WTForms 2, WTForms 3

rv = client.get('/admin/model1/')
assert rv.status_code == 200
Expand All @@ -134,8 +134,8 @@ def test_model():
model = Model1.objects.first()
assert model.test1 == 'test1small'
assert model.test2 == 'test2large'
assert model.test3 == ''
assert model.test4 == ''
assert model.test3 in ('', None) # WTForms 2, WTForms 3
assert model.test4 in ('', None) # WTForms 2, WTForms 3

url = '/admin/model1/delete/?id=%s' % model.id
rv = client.post(url)
Expand All @@ -152,6 +152,10 @@ def test_column_editable_list():
column_editable_list=['test1', 'datetime_field'])
admin.add_view(view)

# Test in-line editing for relations
view = CustomModelView(Model2, column_editable_list=['model1'])
admin.add_view(view)

fill_db(Model1, Model2)

client = app.test_client()
Expand Down Expand Up @@ -199,10 +203,6 @@ def test_column_editable_list():
data = rv.data.decode('utf-8')
assert 'problematic-input' not in data

# Test in-line editing for relations
view = CustomModelView(Model2, column_editable_list=['model1'])
admin.add_view(view)

obj3 = Model2.objects.get(string_field='string_field_val_1')
rv = client.post('/admin/model2/ajax/update/', data={
'list_form_pk': str(obj3.id),
Expand Down Expand Up @@ -300,6 +300,26 @@ def test_column_filters():
(6, 'not in list'),
]

# Test numeric filter
view2 = CustomModelView(Model2, column_filters=['int_field'])
admin.add_view(view2)

# Test boolean filter
view3 = CustomModelView(Model2, column_filters=['bool_field'],
endpoint="_bools")
admin.add_view(view3)

# Test float filter
view4 = CustomModelView(Model2, column_filters=['float_field'],
endpoint="_float")
admin.add_view(view4)

# Test datetime filter
view5 = CustomModelView(Model1,
column_filters=['datetime_field'],
endpoint="_datetime")
admin.add_view(view5)

# Make some test clients
client = app.test_client()

Expand Down Expand Up @@ -365,12 +385,8 @@ def test_column_filters():
assert 'test1_val_3' in data
assert 'test1_val_4' in data

# Test numeric filter
view = CustomModelView(Model2, column_filters=['int_field'])
admin.add_view(view)

assert \
[(f['index'], f['operation']) for f in view._filter_groups[u'Int Field']] == \
[(f['index'], f['operation']) for f in view2._filter_groups[u'Int Field']] == \
[
(0, 'equals'),
(1, 'not equal'),
Expand Down Expand Up @@ -471,13 +487,8 @@ def test_column_filters():
assert 'string_field_val_3' not in data
assert 'string_field_val_4' not in data

# Test boolean filter
view = CustomModelView(Model2, column_filters=['bool_field'],
endpoint="_bools")
admin.add_view(view)

assert \
[(f['index'], f['operation']) for f in view._filter_groups[u'Bool Field']] == \
[(f['index'], f['operation']) for f in view3._filter_groups[u'Bool Field']] == \
[
(0, 'equals'),
(1, 'not equal'),
Expand Down Expand Up @@ -511,13 +522,8 @@ def test_column_filters():
assert 'string_field_val_1' in data
assert 'string_field_val_2' not in data

# Test float filter
view = CustomModelView(Model2, column_filters=['float_field'],
endpoint="_float")
admin.add_view(view)

assert \
[(f['index'], f['operation']) for f in view._filter_groups[u'Float Field']] == \
[(f['index'], f['operation']) for f in view4._filter_groups[u'Float Field']] == \
[
(0, 'equals'),
(1, 'not equal'),
Expand Down Expand Up @@ -604,14 +610,8 @@ def test_column_filters():
assert 'string_field_val_3' not in data
assert 'string_field_val_4' not in data

# Test datetime filter
view = CustomModelView(Model1,
column_filters=['datetime_field'],
endpoint="_datetime")
admin.add_view(view)

assert \
[(f['index'], f['operation']) for f in view._filter_groups[u'Datetime Field']] == \
[(f['index'], f['operation']) for f in view5._filter_groups[u'Datetime Field']] == \
[
(0, 'equals'),
(1, 'not equal'),
Expand Down Expand Up @@ -1185,6 +1185,11 @@ def test_export_csv():
endpoint='row_limit_2')
admin.add_view(view)

view = CustomModelView(Model1, can_export=True,
column_list=['test1', 'test2'],
endpoint='no_row_limit')
admin.add_view(view)

for x in range(5):
fill_db(Model1, Model2)

Expand All @@ -1198,11 +1203,6 @@ def test_export_csv():
"test1_val_1,test2_val_1\r\n" + \
"test1_val_2,test2_val_2\r\n" == data

view = CustomModelView(Model1, can_export=True,
column_list=['test1', 'test2'],
endpoint='no_row_limit')
admin.add_view(view)

# test row limit without export_max_rows
rv = client.get('/admin/no_row_limit/export/csv/')
data = rv.data.decode('utf-8')
Expand Down
Loading
Loading