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