Commit 12e7b17a authored by P.J. Janse van Rensburg's avatar P.J. Janse van Rensburg

Merge branch 'master' into sqlalchemy-utils-types

parents 6cc02583 d29796b6
Changelog Changelog
========= =========
-----
Next release Next release
-----
* Fix display of inline x-editable boolean fields on list view
* Add support for several SQLAlchemy-Utils data types * Add support for several SQLAlchemy-Utils data types
1.5.3 1.5.3
----- -----
......
...@@ -43,7 +43,7 @@ master_doc = 'index' ...@@ -43,7 +43,7 @@ master_doc = 'index'
# General information about the project. # General information about the project.
project = u'flask-admin' project = u'flask-admin'
copyright = u'2012-2015, Serge S. Koval' copyright = u'2012-2019, Flask-Admin Team'
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
...@@ -256,13 +256,13 @@ intersphinx_mapping = {'http://docs.python.org/': None} ...@@ -256,13 +256,13 @@ intersphinx_mapping = {'http://docs.python.org/': None}
# fall back if theme is not there # fall back if theme is not there
try: try:
__import__('flask_theme_support') __import__('flask_theme_support')
except ImportError, e: except ImportError as e:
print '-' * 74 print('-' * 74)
print 'Warning: Flask themes unavailable. Building with default theme' print('Warning: Flask themes unavailable. Building with default theme')
print 'If you want the Flask themes, run this command and build again:' print('If you want the Flask themes, run this command and build again:')
print print()
print ' git submodule update --init' print(' git submodule update --init')
print '-' * 74 print('-' * 74)
pygments_style = 'tango' pygments_style = 'tango'
html_theme = 'default' html_theme = 'default'
......
...@@ -54,15 +54,11 @@ security = Security(app, user_datastore) ...@@ -54,15 +54,11 @@ security = Security(app, user_datastore)
# Create customized model view class # Create customized model view class
class MyModelView(sqla.ModelView): class MyModelView(sqla.ModelView):
def is_accessible(self): def is_accessible(self):
if not current_user.is_active or not current_user.is_authenticated: return (current_user.is_active and
return False current_user.is_authenticated and
current_user.has_role('superuser')
if current_user.has_role('superuser'): )
return True
return False
def _handle_view(self, name, **kwargs): def _handle_view(self, name, **kwargs):
""" """
......
...@@ -2,3 +2,4 @@ Flask ...@@ -2,3 +2,4 @@ Flask
Flask-Admin Flask-Admin
Flask-MongoEngine Flask-MongoEngine
Flask-Login>=0.3.0 Flask-Login>=0.3.0
Pillow
\ No newline at end of file
...@@ -19,7 +19,7 @@ class GeoJSONField(JSONField): ...@@ -19,7 +19,7 @@ class GeoJSONField(JSONField):
super(GeoJSONField, self).__init__(label, validators, **kwargs) super(GeoJSONField, self).__init__(label, validators, **kwargs)
self.web_srid = 4326 self.web_srid = 4326
self.srid = srid self.srid = srid
if self.srid is -1: if self.srid == -1:
self.transform_srid = self.web_srid self.transform_srid = self.web_srid
else: else:
self.transform_srid = self.srid self.transform_srid = self.srid
...@@ -30,11 +30,11 @@ class GeoJSONField(JSONField): ...@@ -30,11 +30,11 @@ class GeoJSONField(JSONField):
if self.raw_data: if self.raw_data:
return self.raw_data[0] return self.raw_data[0]
if type(self.data) is geoalchemy2.elements.WKBElement: if type(self.data) is geoalchemy2.elements.WKBElement:
if self.srid is -1: if self.srid == -1:
return self.session.scalar(func.ST_AsGeoJson(self.data)) return self.session.scalar(func.ST_AsGeoJSON(self.data))
else: else:
return self.session.scalar( return self.session.scalar(
func.ST_AsGeoJson( func.ST_AsGeoJSON(
func.ST_Transform(self.data, self.web_srid) func.ST_Transform(self.data, self.web_srid)
) )
) )
...@@ -43,7 +43,7 @@ class GeoJSONField(JSONField): ...@@ -43,7 +43,7 @@ class GeoJSONField(JSONField):
def process_formdata(self, valuelist): def process_formdata(self, valuelist):
super(GeoJSONField, self).process_formdata(valuelist) super(GeoJSONField, self).process_formdata(valuelist)
if str(self.data) is '': if str(self.data) == '':
self.data = None self.data = None
if self.data is not None: if self.data is not None:
web_shape = self.session.scalar( web_shape = self.session.scalar(
......
...@@ -17,8 +17,10 @@ def geom_formatter(view, value): ...@@ -17,8 +17,10 @@ def geom_formatter(view, value):
"data-tile-layer-url": view.tile_layer_url, "data-tile-layer-url": view.tile_layer_url,
"data-tile-layer-attribution": view.tile_layer_attribution "data-tile-layer-attribution": view.tile_layer_attribution
}) })
if value.srid is -1:
if value.srid == -1:
value.srid = 4326 value.srid = 4326
geojson = view.session.query(view.model).with_entities(func.ST_AsGeoJSON(value)).scalar() geojson = view.session.query(view.model).with_entities(func.ST_AsGeoJSON(value)).scalar()
return Markup('<textarea %s>%s</textarea>' % (params, geojson)) return Markup('<textarea %s>%s</textarea>' % (params, geojson))
......
...@@ -7,6 +7,7 @@ from flask_mongoengine.wtf import orm, fields as mongo_fields ...@@ -7,6 +7,7 @@ from flask_mongoengine.wtf import orm, fields as mongo_fields
from flask_admin import form from flask_admin import form
from flask_admin.model.form import FieldPlaceholder from flask_admin.model.form import FieldPlaceholder
from flask_admin.model.fields import InlineFieldList, AjaxSelectField, AjaxSelectMultipleField from flask_admin.model.fields import InlineFieldList, AjaxSelectField, AjaxSelectMultipleField
from flask_admin.form.validators import FieldListInputRequired
from flask_admin._compat import iteritems from flask_admin._compat import iteritems
from .fields import ModelFormField, MongoFileField, MongoImageField from .fields import ModelFormField, MongoFileField, MongoImageField
...@@ -74,7 +75,10 @@ class CustomModelConverter(orm.ModelConverter): ...@@ -74,7 +75,10 @@ class CustomModelConverter(orm.ModelConverter):
kwargs['validators'] = list(kwargs['validators']) kwargs['validators'] = list(kwargs['validators'])
if field.required: if field.required:
kwargs['validators'].append(validators.InputRequired()) if isinstance(field, ListField):
kwargs['validators'].append(FieldListInputRequired())
else:
kwargs['validators'].append(validators.InputRequired())
elif not isinstance(field, ListField): elif not isinstance(field, ListField):
kwargs['validators'].append(validators.Optional()) kwargs['validators'].append(validators.Optional())
......
...@@ -364,8 +364,8 @@ class ModelView(BaseModelView): ...@@ -364,8 +364,8 @@ class ModelView(BaseModelView):
# Check type # Check type
if (field_type not in self.allowed_search_types): if (field_type not in self.allowed_search_types):
raise Exception('Can only search on text columns. ' + raise Exception('Can only search on text columns. ' +
'Failed to setup search for "%s"' % p) 'Failed to setup search for "%s"' % p)
self._search_fields.append(p) self._search_fields.append(p)
......
...@@ -221,8 +221,8 @@ class ModelView(BaseModelView): ...@@ -221,8 +221,8 @@ class ModelView(BaseModelView):
# Check type # Check type
if not isinstance(p, (CharField, TextField)): if not isinstance(p, (CharField, TextField)):
raise Exception('Can only search on text columns. ' + raise Exception('Can only search on text columns. ' +
'Failed to setup search for "%s"' % p) 'Failed to setup search for "%s"' % p)
self._search_fields.append(p) self._search_fields.append(p)
......
...@@ -436,6 +436,22 @@ class ChoiceTypeNotLikeFilter(FilterNotLike): ...@@ -436,6 +436,22 @@ class ChoiceTypeNotLikeFilter(FilterNotLike):
return query return query
class UuidFilterEqual(FilterEqual, filters.BaseUuidFilter):
pass
class UuidFilterNotEqual(FilterNotEqual, filters.BaseUuidFilter):
pass
class UuidFilterInList(filters.BaseUuidListFilter, FilterInList):
pass
class UuidFilterNotInList(filters.BaseUuidListFilter, FilterNotInList):
pass
# Base SQLA filter field converter # Base SQLA filter field converter
class FilterConverter(filters.BaseFilterConverter): class FilterConverter(filters.BaseFilterConverter):
strings = (FilterLike, FilterNotLike, FilterEqual, FilterNotEqual, strings = (FilterLike, FilterNotLike, FilterEqual, FilterNotEqual,
...@@ -457,12 +473,16 @@ class FilterConverter(filters.BaseFilterConverter): ...@@ -457,12 +473,16 @@ class FilterConverter(filters.BaseFilterConverter):
DateTimeGreaterFilter, DateTimeSmallerFilter, DateTimeGreaterFilter, DateTimeSmallerFilter,
DateTimeBetweenFilter, DateTimeNotBetweenFilter, DateTimeBetweenFilter, DateTimeNotBetweenFilter,
FilterEmpty) FilterEmpty)
time_filters = (TimeEqualFilter, TimeNotEqualFilter, TimeGreaterFilter, TimeSmallerFilter, time_filters = (TimeEqualFilter, TimeNotEqualFilter, TimeGreaterFilter,
TimeBetweenFilter, TimeNotBetweenFilter, FilterEmpty) TimeSmallerFilter, TimeBetweenFilter, TimeNotBetweenFilter,
FilterEmpty)
choice_type_filters = (ChoiceTypeEqualFilter, ChoiceTypeNotEqualFilter, choice_type_filters = (ChoiceTypeEqualFilter, ChoiceTypeNotEqualFilter,
ChoiceTypeLikeFilter, ChoiceTypeNotLikeFilter, FilterEmpty) ChoiceTypeLikeFilter, ChoiceTypeNotLikeFilter, FilterEmpty)
uuid_filters = (UuidFilterEqual, UuidFilterNotEqual, FilterEmpty,
UuidFilterInList, UuidFilterNotInList)
arrow_type_filters = (DateTimeGreaterFilter, DateTimeSmallerFilter, FilterEmpty) arrow_type_filters = (DateTimeGreaterFilter, DateTimeSmallerFilter, FilterEmpty)
def convert(self, type_name, column, name, **kwargs): def convert(self, type_name, column, name, **kwargs):
filter_name = type_name.lower() filter_name = type_name.lower()
...@@ -531,3 +551,7 @@ class FilterConverter(filters.BaseFilterConverter): ...@@ -531,3 +551,7 @@ class FilterConverter(filters.BaseFilterConverter):
kwargs['enum_class'] = column.type._enum_class kwargs['enum_class'] = column.type._enum_class
return [f(column, name, options, **kwargs) for f in self.enum] return [f(column, name, options, **kwargs) for f in self.enum]
@filters.convert('uuid')
def conv_uuid(self, column, name, **kwargs):
return [f(column, name, **kwargs) for f in self.uuid_filters]
...@@ -3,6 +3,7 @@ import warnings ...@@ -3,6 +3,7 @@ import warnings
import inspect import inspect
from sqlalchemy.orm.attributes import InstrumentedAttribute from sqlalchemy.orm.attributes import InstrumentedAttribute
from sqlalchemy.orm.base import manager_of_class, instance_state
from sqlalchemy.orm import joinedload, aliased from sqlalchemy.orm import joinedload, aliased
from sqlalchemy.sql.expression import desc from sqlalchemy.sql.expression import desc
from sqlalchemy import Boolean, Table, func, or_ from sqlalchemy import Boolean, Table, func, or_
...@@ -328,6 +329,8 @@ class ModelView(BaseModelView): ...@@ -328,6 +329,8 @@ class ModelView(BaseModelView):
menu_icon_type=menu_icon_type, menu_icon_type=menu_icon_type,
menu_icon_value=menu_icon_value) menu_icon_value=menu_icon_value)
self._manager = manager_of_class(self.model)
# Primary key # Primary key
self._primary_key = self.scaffold_pk() self._primary_key = self.scaffold_pk()
...@@ -1111,7 +1114,12 @@ class ModelView(BaseModelView): ...@@ -1111,7 +1114,12 @@ class ModelView(BaseModelView):
Form instance Form instance
""" """
try: try:
model = self.model() 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, [], {})
form.populate_obj(model) form.populate_obj(model)
self.session.add(model) self.session.add(model)
self._on_model_change(form, model, True) self._on_model_change(form, model, True)
......
from flask_admin.babel import gettext
from wtforms.validators import StopValidation
class FieldListInputRequired(object):
"""
Validates that at least one item was provided for a FieldList
"""
field_flags = ('required',)
def __call__(self, form, field):
if len(field.entries) == 0:
field.errors[:] = []
raise StopValidation(gettext('This field requires at least one item.'))
...@@ -45,13 +45,15 @@ def get_url(endpoint, **kwargs): ...@@ -45,13 +45,15 @@ def get_url(endpoint, **kwargs):
def is_required_form_field(field): def is_required_form_field(field):
""" """
Check if form field has `DataRequired` or `InputRequired` validators. Check if form field has `DataRequired`, `InputRequired`, or
`FieldListInputRequired` validators.
:param field: :param field:
WTForms field to check WTForms field to check
""" """
from flask_admin.form.validators import FieldListInputRequired
for validator in field.validators: for validator in field.validators:
if isinstance(validator, (DataRequired, InputRequired)): if isinstance(validator, (DataRequired, InputRequired, FieldListInputRequired)):
return True return True
return False return False
......
import time import time
import datetime import datetime
import uuid
from flask_admin.babel import lazy_gettext from flask_admin.babel import lazy_gettext
...@@ -269,6 +270,29 @@ class BaseTimeBetweenFilter(BaseFilter): ...@@ -269,6 +270,29 @@ class BaseTimeBetweenFilter(BaseFilter):
return False return False
class BaseUuidFilter(BaseFilter):
"""
Base uuid filter
"""
def __init__(self, name, options=None, data_type=None):
super(BaseUuidFilter, self).__init__(name,
options,
data_type='uuid')
def clean(self, value):
value = uuid.UUID(value)
return str(value)
class BaseUuidListFilter(BaseFilter):
"""
Base uuid list filter
"""
def clean(self, value):
return [str(uuid.UUID(v.strip())) for v in value.split(',') if v.strip()]
def convert(*args): def convert(*args):
""" """
Decorator for field to filter conversion routine. Decorator for field to filter conversion routine.
......
...@@ -110,6 +110,7 @@ class XEditableWidget(object): ...@@ -110,6 +110,7 @@ class XEditableWidget(object):
kwargs['data-rows'] = '5' kwargs['data-rows'] = '5'
elif field.type == 'BooleanField': elif field.type == 'BooleanField':
kwargs['data-type'] = 'select2' kwargs['data-type'] = 'select2'
kwargs['data-value'] = '1' if field.data else ''
# data-source = dropdown options # data-source = dropdown options
kwargs['data-source'] = json.dumps([ kwargs['data-source'] = json.dumps([
{'value': '', 'text': gettext('No')}, {'value': '', 'text': gettext('No')},
......
...@@ -494,15 +494,21 @@ ...@@ -494,15 +494,21 @@
case 'x-editable-boolean': case 'x-editable-boolean':
$el.editable({ $el.editable({
params: overrideXeditableParams, params: overrideXeditableParams,
display: function(value, sourceData, response) { display: function(value, response) {
// display new boolean value as an icon // display boolean value as an icon
if(response) { if(value == '1') {
if(value == '1') { $(this).html('<span class="fa fa-check-circle glyphicon glyphicon-ok-circle icon-ok-circle"></span>');
$(this).html('<span class="fa fa-check-circle glyphicon glyphicon-ok-circle icon-ok-circle"></span>'); } else {
} else { $(this).html('<span class="fa fa-minus-circle glyphicon glyphicon-minus-sign icon-minus-sign"></span>');
$(this).html('<span class="fa fa-minus-circle glyphicon glyphicon-minus-sign icon-minus-sign"></span>');
}
} }
},
success: function(response, newValue) {
// update display
if(newValue == '1') {
$(this).html('<span class="fa fa-check-circle glyphicon glyphicon-ok-circle icon-ok-circle"></span>');
} else {
$(this).html('<span class="fa fa-minus-circle glyphicon glyphicon-minus-sign icon-minus-sign"></span>');
}
} }
}); });
} }
......
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