Commit 149bae27 authored by Michael Bukachi's avatar Michael Bukachi

Merge remote-tracking branch 'upstream/master' into bootstrap4

parents 4eec03b2 35d21b68
...@@ -10,10 +10,6 @@ matrix: ...@@ -10,10 +10,6 @@ matrix:
env: TOX_ENV=flake8 env: TOX_ENV=flake8
- python: 2.7 - python: 2.7
env: TOX_ENV=docs-html env: TOX_ENV=docs-html
- python: 3.4
env: TOX_ENV=py34-WTForms1
- python: 3.4
env: TOX_ENV=py34-WTForms2
- python: 3.5 - python: 3.5
env: TOX_ENV=py35-WTForms1 env: TOX_ENV=py35-WTForms1
- python: 3.5 - python: 3.5
...@@ -22,6 +18,12 @@ matrix: ...@@ -22,6 +18,12 @@ matrix:
env: TOX_ENV=py36-WTForms1 env: TOX_ENV=py36-WTForms1
- python: 3.6 - python: 3.6
env: TOX_ENV=py36-WTForms2 env: TOX_ENV=py36-WTForms2
- python: 3.7
env: TOX_ENV=py37-WTForms1
- python: 3.7
env: TOX_ENV=py37-WTForms2
- python: 3.8
env: TOX_ENV=py38-WTForms2
addons: addons:
postgresql: "9.4" postgresql: "9.4"
......
Copyright (c) 2014, Serge S. Koval and contributors. See AUTHORS BSD 3-Clause License
for more details.
Some rights reserved. Copyright (c) 2014, Serge S. Koval and contributors
All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met: modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright 1. Redistributions of source code must retain the above copyright notice, this
notice, this list of conditions and the following disclaimer. list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Names of the contributors may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 2. Redistributions in binary form must reproduce the above copyright notice,
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED this list of conditions and the following disclaimer in the documentation
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE and/or other materials provided with the distribution.
DISCLAIMED. IN NO EVENT SHALL SERGE KOVAL BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 3. Neither the name of the copyright holder nor the names of its
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; contributors may be used to endorse or promote products derived from
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND this software without specific prior written permission.
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
\ No newline at end of file IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
...@@ -55,11 +55,11 @@ To run the examples in your local environment:: ...@@ -55,11 +55,11 @@ To run the examples in your local environment::
3. Install requirements:: 3. Install requirements::
pip install -r 'examples/sqla/requirements.txt' pip install -r examples/sqla/requirements.txt
4. Run the application:: 4. Run the application::
python examples/sqla/app.py python examples/sqla/run_server.py
Documentation Documentation
------------- -------------
......
Changelog Changelog
========= =========
Next release 1.5.6
-----
* SQLAlchemy 1.3.6 compatibility fix
* Python 3.8 support
1.5.5
-----
* Werkzeug 1.0 compatibility fix
* Use fa-circle-o icon for unchecked booleans
* A few SQLAlchemy-related bug fixes
1.5.4
----- -----
* Fix display of inline x-editable boolean fields on list view * Fix display of inline x-editable boolean fields on list view
...@@ -12,6 +25,7 @@ Next release ...@@ -12,6 +25,7 @@ Next release
* Update Mapbox API v1 URL format * Update Mapbox API v1 URL format
* Update jQuery and moment dependencies in templates * Update jQuery and moment dependencies in templates
* Fixed a datepicker issue, where only dates up to 2015 were showing up * Fixed a datepicker issue, where only dates up to 2015 were showing up
* Updated Pillow dependency version
1.5.3 1.5.3
----- -----
......
import os import os
import os.path as op import os.path as op
from werkzeug import secure_filename from werkzeug.utils import secure_filename
from sqlalchemy import event from sqlalchemy import event
from flask import Flask, request, render_template from flask import Flask, request, render_template
......
__version__ = '1.5.3' __version__ = '1.5.6'
__author__ = 'Flask-Admin team' __author__ = 'Flask-Admin team'
__email__ = 'serge.koval+github@gmail.com' __email__ = 'serge.koval+github@gmail.com'
......
...@@ -257,7 +257,7 @@ class BaseView(with_metaclass(AdminViewMeta, BaseViewClass)): ...@@ -257,7 +257,7 @@ class BaseView(with_metaclass(AdminViewMeta, BaseViewClass)):
self.static_folder = 'static' self.static_folder = 'static'
self.static_url_path = '/static/admin' self.static_url_path = '/static/admin'
# If name is not povided, use capitalized endpoint name # If name is not provided, use capitalized endpoint name
if self.name is None: if self.name is None:
self.name = self._prettify_class_name(self.__class__.__name__) self.name = self._prettify_class_name(self.__class__.__name__)
......
...@@ -8,7 +8,7 @@ import shutil ...@@ -8,7 +8,7 @@ import shutil
from operator import itemgetter from operator import itemgetter
from flask import flash, redirect, abort, request, send_file from flask import flash, redirect, abort, request, send_file
from werkzeug import secure_filename from werkzeug.utils import secure_filename
from wtforms import fields, validators from wtforms import fields, validators
from flask_admin import form, helpers from flask_admin import form, helpers
......
...@@ -42,10 +42,10 @@ class QueryAjaxModelLoader(AjaxModelLoader): ...@@ -42,10 +42,10 @@ class QueryAjaxModelLoader(AjaxModelLoader):
if not model: if not model:
return None return None
return (as_unicode(model.id), as_unicode(model)) return (as_unicode(model.pk), as_unicode(model))
def get_one(self, pk): def get_one(self, pk):
return self.model.objects.filter(id=pk).first() return self.model.objects.filter(pk=pk).first()
def get_list(self, term, offset=0, limit=DEFAULT_PAGE_SIZE): def get_list(self, term, offset=0, limit=DEFAULT_PAGE_SIZE):
query = self.model.objects query = self.model.objects
......
from sqlalchemy import or_, and_, cast from sqlalchemy import or_, and_, cast, text
from sqlalchemy.types import String from sqlalchemy.types import String
from flask_admin._compat import as_unicode, string_types from flask_admin._compat import as_unicode, string_types
...@@ -69,11 +69,13 @@ class QueryAjaxModelLoader(AjaxModelLoader): ...@@ -69,11 +69,13 @@ class QueryAjaxModelLoader(AjaxModelLoader):
def get_list(self, term, offset=0, limit=DEFAULT_PAGE_SIZE): def get_list(self, term, offset=0, limit=DEFAULT_PAGE_SIZE):
query = self.get_query() query = self.get_query()
filters = (cast(field, String).ilike(u'%%%s%%' % term) for field in self._cached_fields) # no type casting to string if a ColumnAssociationProxyInstance is given
filters = (field.ilike(u'%%%s%%' % term) if is_association_proxy(field)
else cast(field, String).ilike(u'%%%s%%' % term) for field in self._cached_fields)
query = query.filter(or_(*filters)) query = query.filter(or_(*filters))
if self.filters: if self.filters:
filters = ["%s.%s" % (self.model.__tablename__.lower(), value) for value in self.filters] filters = [text("%s.%s" % (self.model.__tablename__.lower(), value)) for value in self.filters]
query = query.filter(and_(*filters)) query = query.filter(and_(*filters))
if self.order_by: if self.order_by:
......
...@@ -216,4 +216,6 @@ def is_relationship(attr): ...@@ -216,4 +216,6 @@ def is_relationship(attr):
def is_association_proxy(attr): def is_association_proxy(attr):
if hasattr(attr, 'parent'):
attr = attr.parent
return hasattr(attr, 'extension_type') and attr.extension_type == ASSOCIATION_PROXY return hasattr(attr, 'extension_type') and attr.extension_type == ASSOCIATION_PROXY
...@@ -1111,6 +1111,20 @@ class ModelView(BaseModelView): ...@@ -1111,6 +1111,20 @@ class ModelView(BaseModelView):
return super(ModelView, self).handle_view_exception(exc) return super(ModelView, self).handle_view_exception(exc)
def build_new_instance(self):
"""
Build new instance of a model. Useful to override the Flask-Admin behavior
when the model has a custom __init__ method.
"""
model = self._manager.new_instance()
# TODO: We need a better way to create model instances and stay compatible with
# SQLAlchemy __init__() behavior
state = instance_state(model)
self._manager.dispatch.init(state, [], {})
return model
# Model handlers # Model handlers
def create_model(self, form): def create_model(self, form):
""" """
...@@ -1120,11 +1134,7 @@ class ModelView(BaseModelView): ...@@ -1120,11 +1134,7 @@ class ModelView(BaseModelView):
Form instance Form instance
""" """
try: try:
model = self._manager.new_instance() model = self.build_new_instance()
# TODO: We need a better way to create model instances and stay compatible with
# SQLAlchemy __init__() behavior
state = instance_state(model)
self._manager.dispatch.init(state, [], {})
form.populate_obj(model) form.populate_obj(model)
self.session.add(model) self.session.add(model)
......
import os import os
import os.path as op import os.path as op
from werkzeug import secure_filename from werkzeug.utils import secure_filename
from werkzeug.datastructures import FileStorage from werkzeug.datastructures import FileStorage
from wtforms import ValidationError, fields from wtforms import ValidationError, fields
...@@ -465,7 +465,10 @@ class ImageUploadField(FileUploadField): ...@@ -465,7 +465,10 @@ class ImageUploadField(FileUploadField):
return image return image
def _save_image(self, image, path, format='JPEG'): def _save_image(self, image, path, format='JPEG'):
if image.mode not in ('RGB', 'RGBA'): # New Pillow versions require RGB format for JPEGs
if format == 'JPEG' and image.mode != 'RGB':
image = image.convert('RGB')
elif image.mode not in ('RGB', 'RGBA'):
image = image.convert('RGBA') image = image.convert('RGBA')
with open(path, 'wb') as fp: with open(path, 'wb') as fp:
......
...@@ -5,7 +5,7 @@ import mimetypes ...@@ -5,7 +5,7 @@ import mimetypes
import time import time
from math import ceil from math import ceil
from werkzeug import secure_filename from werkzeug.utils import secure_filename
from flask import (current_app, request, redirect, flash, abort, json, from flask import (current_app, request, redirect, flash, abort, json,
Response, get_flashed_messages, stream_with_context) Response, get_flashed_messages, stream_with_context)
......
...@@ -36,7 +36,7 @@ def bool_formatter(view, value): ...@@ -36,7 +36,7 @@ def bool_formatter(view, value):
Value to check Value to check
""" """
glyph = 'ok-circle' if value else 'minus-sign' glyph = 'ok-circle' if value else 'minus-sign'
fa = 'check-circle' if value else 'minus-circle' fa = 'check-circle' if value else 'circle-o'
return Markup('<span class="fa fa-%s glyphicon glyphicon-%s icon-%s"></span>' % (fa, glyph, glyph)) return Markup('<span class="fa fa-%s glyphicon glyphicon-%s icon-%s"></span>' % (fa, glyph, glyph))
......
...@@ -138,6 +138,8 @@ ...@@ -138,6 +138,8 @@
{% set form = list_forms[get_pk_value(row)] %} {% set form = list_forms[get_pk_value(row)] %}
{% if form.csrf_token %} {% if form.csrf_token %}
{{ form[c](pk=get_pk_value(row), display_value=get_value(row, c), csrf=form.csrf_token._value()) }} {{ form[c](pk=get_pk_value(row), display_value=get_value(row, c), csrf=form.csrf_token._value()) }}
{% elif csrf_token %}
{{ form[c](pk=get_pk_value(row), display_value=get_value(row, c), csrf=csrf_token()) }}
{% else %} {% else %}
{{ form[c](pk=get_pk_value(row), display_value=get_value(row, c)) }} {{ form[c](pk=get_pk_value(row), display_value=get_value(row, c)) }}
{% endif %} {% endif %}
......
...@@ -137,6 +137,8 @@ ...@@ -137,6 +137,8 @@
{% set form = list_forms[get_pk_value(row)] %} {% set form = list_forms[get_pk_value(row)] %}
{% if form.csrf_token %} {% if form.csrf_token %}
{{ form[c](pk=get_pk_value(row), display_value=get_value(row, c), csrf=form.csrf_token._value()) }} {{ form[c](pk=get_pk_value(row), display_value=get_value(row, c), csrf=form.csrf_token._value()) }}
{% elif csrf_token %}
{{ form[c](pk=get_pk_value(row), display_value=get_value(row, c), csrf=csrf_token()) }}
{% else %} {% else %}
{{ form[c](pk=get_pk_value(row), display_value=get_value(row, c)) }} {{ form[c](pk=get_pk_value(row), display_value=get_value(row, c)) }}
{% endif %} {% endif %}
......
...@@ -4,7 +4,10 @@ from nose.tools import eq_, ok_ ...@@ -4,7 +4,10 @@ from nose.tools import eq_, ok_
from flask import Flask from flask import Flask
from werkzeug.wsgi import DispatcherMiddleware try:
from werkzeug.middleware.dispatcher import DispatcherMiddleware
except ImportError:
from werkzeug.wsgi import DispatcherMiddleware
from werkzeug.test import Client from werkzeug.test import Client
from wtforms import fields from wtforms import fields
......
...@@ -6,7 +6,7 @@ wtf-peewee ...@@ -6,7 +6,7 @@ wtf-peewee
mongoengine<0.11.0 mongoengine<0.11.0
pymongo==2.8 pymongo==2.8
flask-mongoengine==0.8.2 flask-mongoengine==0.8.2
pillow==2.9.0 pillow>=3.3.2
Babel<=1.3 Babel<=1.3
flask-babelex flask-babelex
shapely==1.5.9 shapely==1.5.9
......
...@@ -64,7 +64,7 @@ setup( ...@@ -64,7 +64,7 @@ setup(
install_requires=install_requires, install_requires=install_requires,
tests_require=[ tests_require=[
'nose>=1.0', 'nose>=1.0',
'pillow==2.9.0', 'pillow>=3.3.2',
'mongoengine', 'mongoengine',
'pymongo', 'pymongo',
'wtf-peewee', 'wtf-peewee',
......
[tox] [tox]
envlist = envlist =
py{27,34,35,36}-WTForms{1,2} py{27,35,36,37}-WTForms{1,2}
py38-WTForms2
flake8 flake8
docs-html docs-html
skipsdist = true skipsdist = true
......
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