Commit 44f78e43 authored by Sergey Markelov's avatar Sergey Markelov

Merge remote-tracking branch 'upstream/master'

parents dc29c583 3f0bea2c
...@@ -56,6 +56,47 @@ Form Rendering Rule Description ...@@ -56,6 +56,47 @@ Form Rendering Rule Description
:class:`flask.ext.admin.form.rules.FieldSet` Renders form header and child rules :class:`flask.ext.admin.form.rules.FieldSet` Renders form header and child rules
======================================================= ======================================================== ======================================================= ========================================================
Enabling CSRF Validation
---------------
Flask-Admin does not use Flask-WTF Form class - it uses the wtforms Form class, which does not have CSRF validation.
Adding CSRF validation will require importing flask_wtf and overriding the :class:`flask.ext.admin.form.BaseForm` by using :attr:`flask.ext.admin.model.BaseModelView.form_base_class`::
import os
import flask
**import flask_wtf**
import flask_admin
import flask_sqlalchemy
from flask_admin.contrib.sqla import ModelView
DBFILE = 'app.db'
app = flask.Flask(__name__)
app.config['SECRET_KEY'] = 'Dnit7qz7mfcP0YuelDrF8vLFvk0snhwP'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + DBFILE
**app.config['CSRF_ENABLED'] = True**
**flask_wtf.CsrfProtect(app)**
db = flask_sqlalchemy.SQLAlchemy(app)
admin = flask_admin.Admin(app, name='Admin')
## Here is the fix:
class MyModelView(ModelView):
**form_base_class = flask_wtf.Form**
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String)
password = db.Column(db.String)
if not os.path.exists(DBFILE):
db.create_all()
## The subclass is used here:
admin.add_view( MyModelView(User, db.session, name='User') )
app.run(debug=True)
Further reading Further reading
--------------- ---------------
......
...@@ -23,6 +23,7 @@ if not PY2: ...@@ -23,6 +23,7 @@ if not PY2:
iterkeys = lambda d: iter(d.keys()) iterkeys = lambda d: iter(d.keys())
itervalues = lambda d: iter(d.values()) itervalues = lambda d: iter(d.values())
iteritems = lambda d: iter(d.items()) iteritems = lambda d: iter(d.items())
filter_list = lambda f, l: list(filter(f, l))
def as_unicode(s): def as_unicode(s):
if isinstance(s, bytes): if isinstance(s, bytes):
...@@ -41,6 +42,7 @@ else: ...@@ -41,6 +42,7 @@ else:
iterkeys = lambda d: d.iterkeys() iterkeys = lambda d: d.iterkeys()
itervalues = lambda d: d.itervalues() itervalues = lambda d: d.itervalues()
iteritems = lambda d: d.iteritems() iteritems = lambda d: d.iteritems()
filter_list = filter
def as_unicode(s): def as_unicode(s):
if isinstance(s, str): if isinstance(s, str):
......
...@@ -603,7 +603,7 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -603,7 +603,7 @@ class FileAdmin(BaseView, ActionsMixin):
try: try:
shutil.rmtree(full_path) shutil.rmtree(full_path)
self.on_directory_delete(full_path, path) self.on_directory_delete(full_path, path)
flash(gettext('Directory "%s" was successfully deleted.' % path)) flash(gettext('Directory "%(path)s" was successfully deleted.', path=path))
except Exception as ex: except Exception as ex:
flash(gettext('Failed to delete directory: %(error)s', error=ex), 'error') flash(gettext('Failed to delete directory: %(error)s', error=ex), 'error')
else: else:
......
from werkzeug.datastructures import FileStorage from werkzeug.datastructures import FileStorage
from wtforms import fields from wtforms import fields
from wtforms.fields.core import _unset_value
try:
from wtforms.fields.core import _unset_value as unset_value
except ImportError:
from wtforms.utils import unset_value
from . import widgets from . import widgets
from flask.ext.admin.model.fields import InlineFormField from flask.ext.admin.model.fields import InlineFormField
...@@ -47,7 +51,7 @@ class MongoFileField(fields.FileField): ...@@ -47,7 +51,7 @@ class MongoFileField(fields.FileField):
self._should_delete = False self._should_delete = False
def process(self, formdata, data=_unset_value): def process(self, formdata, data=unset_value):
if formdata: if formdata:
marker = '_%s-delete' % self.name marker = '_%s-delete' % self.name
if marker in formdata: if marker in formdata:
......
...@@ -95,32 +95,32 @@ class FilterConverter(filters.BaseFilterConverter): ...@@ -95,32 +95,32 @@ class FilterConverter(filters.BaseFilterConverter):
enum = (FilterEqual, FilterNotEqual) enum = (FilterEqual, FilterNotEqual)
def convert(self, type_name, column, name, **kwargs): def convert(self, type_name, column, name, **kwargs):
if type_name in self.converters: if type_name.lower() in self.converters:
return self.converters[type_name](column, name, **kwargs) return self.converters[type_name.lower()](column, name, **kwargs)
return None return None
@filters.convert('String', 'Unicode', 'Text', 'UnicodeText') @filters.convert('string', 'unicode', 'text', 'unicodetext')
def conv_string(self, column, name, **kwargs): def conv_string(self, column, name, **kwargs):
return [f(column, name, **kwargs) for f in self.strings] return [f(column, name, **kwargs) for f in self.strings]
@filters.convert('Boolean') @filters.convert('boolean')
def conv_bool(self, column, name, **kwargs): def conv_bool(self, column, name, **kwargs):
return [f(column, name, **kwargs) for f in self.bool] return [f(column, name, **kwargs) for f in self.bool]
@filters.convert('Integer', 'SmallInteger', 'Numeric', 'Float', 'BigInteger') @filters.convert('integer', 'smallinteger', 'numeric', 'float', 'biginteger')
def conv_int(self, column, name, **kwargs): def conv_int(self, column, name, **kwargs):
return [f(column, name, **kwargs) for f in self.numeric] return [f(column, name, **kwargs) for f in self.numeric]
@filters.convert('Date') @filters.convert('date')
def conv_date(self, column, name, **kwargs): def conv_date(self, column, name, **kwargs):
return [f(column, name, data_type='datepicker', **kwargs) for f in self.numeric] return [f(column, name, data_type='datepicker', **kwargs) for f in self.numeric]
@filters.convert('DateTime') @filters.convert('datetime')
def conv_datetime(self, column, name, **kwargs): def conv_datetime(self, column, name, **kwargs):
return [f(column, name, data_type='datetimepicker', **kwargs) for f in self.numeric] return [f(column, name, data_type='datetimepicker', **kwargs) for f in self.numeric]
@filters.convert('Enum', 'ENUM') @filters.convert('enum')
def conv_enum(self, column, name, options=None, **kwargs): def conv_enum(self, column, name, options=None, **kwargs):
if not options: if not options:
options = [ options = [
......
...@@ -2,7 +2,6 @@ from wtforms import fields, validators ...@@ -2,7 +2,6 @@ from wtforms import fields, validators
from sqlalchemy import Boolean, Column from sqlalchemy import Boolean, Column
from flask.ext.admin import form from flask.ext.admin import form
from flask.ext.admin.form import Select2Field
from flask.ext.admin.model.form import (converts, ModelConverterBase, from flask.ext.admin.model.form import (converts, ModelConverterBase,
InlineModelConverterBase, FieldPlaceholder) InlineModelConverterBase, FieldPlaceholder)
from flask.ext.admin.model.fields import AjaxSelectField, AjaxSelectMultipleField from flask.ext.admin.model.fields import AjaxSelectField, AjaxSelectMultipleField
...@@ -12,7 +11,7 @@ from flask.ext.admin._compat import iteritems ...@@ -12,7 +11,7 @@ from flask.ext.admin._compat import iteritems
from .validators import Unique from .validators import Unique
from .fields import QuerySelectField, QuerySelectMultipleField, InlineModelFormList from .fields import QuerySelectField, QuerySelectMultipleField, InlineModelFormList
from .tools import is_inherited_primary_key, get_column_for_current_model, has_multiple_pks from .tools import has_multiple_pks, filter_foreign_columns
from .ajax import create_ajax_loader from .ajax import create_ajax_loader
try: try:
...@@ -152,11 +151,13 @@ class AdminModelConverter(ModelConverterBase): ...@@ -152,11 +151,13 @@ class AdminModelConverter(ModelConverterBase):
# Ignore pk/fk # Ignore pk/fk
if hasattr(prop, 'columns'): if hasattr(prop, 'columns'):
# Check if more than one column mapped to the property # Check if more than one column mapped to the property
if len(prop.columns) != 1: if len(prop.columns) > 1:
if is_inherited_primary_key(prop): columns = filter_foreign_columns(model.__table__, prop.columns)
column = get_column_for_current_model(prop)
else: if len(columns) > 1:
raise TypeError('Can not convert multiple-column properties (%s.%s)' % (model, prop.key)) raise TypeError('Can not convert multiple-column properties (%s.%s)' % (model, prop.key))
column = columns[0]
else: else:
# Grab column # Grab column
column = prop.columns[0] column = prop.columns[0]
...@@ -238,7 +239,7 @@ class AdminModelConverter(ModelConverterBase): ...@@ -238,7 +239,7 @@ class AdminModelConverter(ModelConverterBase):
if mapper.class_ == self.view.model and form_choices: if mapper.class_ == self.view.model and form_choices:
choices = form_choices.get(column.key) choices = form_choices.get(column.key)
if choices: if choices:
return Select2Field( return form.Select2Field(
choices=choices, choices=choices,
allow_blank=column.nullable, allow_blank=column.nullable,
**kwargs **kwargs
...@@ -292,8 +293,7 @@ class AdminModelConverter(ModelConverterBase): ...@@ -292,8 +293,7 @@ class AdminModelConverter(ModelConverterBase):
@converts('DateTime') @converts('DateTime')
def convert_datetime(self, field_args, **extra): def convert_datetime(self, field_args, **extra):
field_args['widget'] = form.DateTimePickerWidget() return form.DateTimeField(**field_args)
return DateTimeField(**field_args)
@converts('Time') @converts('Time')
def convert_time(self, field_args, **extra): def convert_time(self, field_args, **extra):
......
...@@ -3,6 +3,10 @@ from sqlalchemy.sql.operators import eq ...@@ -3,6 +3,10 @@ from sqlalchemy.sql.operators import eq
from sqlalchemy.exc import DBAPIError from sqlalchemy.exc import DBAPIError
from ast import literal_eval from ast import literal_eval
from flask.ext.admin._compat import filter_list
from flask.ext.admin.tools import iterencode, iterdecode
def parse_like_term(term): def parse_like_term(term):
if term.startswith('^'): if term.startswith('^'):
stmt = '%s%%' % term[1:] stmt = '%s%%' % term[1:]
...@@ -14,6 +18,16 @@ def parse_like_term(term): ...@@ -14,6 +18,16 @@ def parse_like_term(term):
return stmt return stmt
def filter_foreign_columns(base_table, columns):
"""
Return list of columns that belong to passed table.
:param base_table: Table to check against
:param columns: List of columns to filter
"""
return filter_list(lambda c: c.table == base_table, columns)
def get_primary_key(model): def get_primary_key(model):
""" """
Return primary key name from a model. If the primary key consists of multiple columns, Return primary key name from a model. If the primary key consists of multiple columns,
...@@ -26,18 +40,11 @@ def get_primary_key(model): ...@@ -26,18 +40,11 @@ def get_primary_key(model):
pks = [] pks = []
for p in props: for p in props:
if hasattr(p, 'expression'): # expression = primary column or expression for this ColumnProperty if hasattr(p, 'columns'):
if p.expression.primary_key: for c in filter_foreign_columns(model.__table__, p.columns):
if is_inherited_primary_key(p): if c.primary_key:
pks.append(get_column_for_current_model(p).key)
else:
pks.append(p.key) pks.append(p.key)
else: break
if hasattr(p, 'columns'):
for c in p.columns:
if c.primary_key:
pks.append(p.key)
break
if len(pks) == 1: if len(pks) == 1:
return pks[0] return pks[0]
...@@ -46,54 +53,16 @@ def get_primary_key(model): ...@@ -46,54 +53,16 @@ def get_primary_key(model):
else: else:
return None return None
def is_inherited_primary_key(prop):
"""
Return True, if the ColumnProperty is an inherited primary key
Check if all columns are primary keys and _one_ does not have a foreign key -> looks like joined
table inheritance: http://docs.sqlalchemy.org/en/latest/orm/inheritance.html with "standard
practice" of same column name.
:param prop: The ColumnProperty to check
:return: Boolean
:raises: Exceptions as they occur - no ExceptionHandling here
"""
if not hasattr(prop, 'expression'):
return False
if prop.expression.primary_key:
return len(prop._orig_columns) == len(prop.columns)-1
return False
def get_column_for_current_model(prop):
"""
Return the Column() of the ColumnProperty "prop", that refers to the current model
When using inheritance, a ColumnProperty may contain multiple columns. This function
returns the Column(), the belongs to the Model of the ColumnProperty - the "current"
model
:param prop: The ColumnProperty
:return: The column for the current model
:raises: TypeError if not exactely one Column() for the current model could be found.
All other Exceptions not handled here but raised
"""
candidates = [column for column in prop.columns if column.expression == prop.expression]
if len(candidates) != 1:
raise TypeError('Not exactly one column for the current model found. ' +
'Found %d columns for property %s' % (len(candidates), prop))
else:
return candidates[0]
def has_multiple_pks(model): def has_multiple_pks(model):
"""Return True, if the model has more than one primary key """
Return True, if the model has more than one primary key
""" """
if not hasattr(model, '_sa_class_manager'): if not hasattr(model, '_sa_class_manager'):
raise TypeError('model must be a sqlalchemy mapped model') raise TypeError('model must be a sqlalchemy mapped model')
pks = model._sa_class_manager.mapper.primary_key
return len(pks) > 1 return len(model._sa_class_manager.mapper.primary_key) > 1
def tuple_operator_in(model_pk, ids): def tuple_operator_in(model_pk, ids):
"""The tuple_ Operator only works on certain engines like MySQL or Postgresql. It does not work with sqlite. """The tuple_ Operator only works on certain engines like MySQL or Postgresql. It does not work with sqlite.
...@@ -123,36 +92,27 @@ def tuple_operator_in(model_pk, ids): ...@@ -123,36 +92,27 @@ def tuple_operator_in(model_pk, ids):
def get_query_for_ids(modelquery, model, ids): def get_query_for_ids(modelquery, model, ids):
""" """
Return a query object, that contains all entities of the given model for Return a query object filtered by primary key values passed in `ids` argument.
the primary keys provided in the ids-parameter.
Unfortunately, it is not possible to use `in_` filter if model has more than one
The ``pks`` parameter is a tuple, that contains the different primary key values, primary key.
that should be returned. If the primary key of the model consists of multiple columns
every entry of the ``pks`` parameter must be a tuple containing the columns-values in the
correct order, that make up the primary key of the model
If the model has multiple primary keys, the
`tuple_ <http://docs.sqlalchemy.org/en/latest/core/expression_api.html#sqlalchemy.sql.expression.tuple_>`_
operator will be used. As this operator does not work on certain databases,
notably on sqlite, a workaround function :func:`tuple_operator_in` is provided
that implements the same logic using OR and AND operations.
When having multiple primary keys, the pks are provided as a list of tuple-look-alike-strings,
``[u'(1, 2)', u'(1, 1)']``. These needs to be evaluated into real tuples, where
`Stackoverflow Question 3945856 <http://stackoverflow.com/questions/3945856/converting-string-to-tuple-and-adding-to-tuple>`_
pointed to `Literal Eval <http://docs.python.org/2/library/ast.html#ast.literal_eval>`_, which is now used.
""" """
if has_multiple_pks(model): if has_multiple_pks(model):
model_pk = [getattr(model, pk_name).expression for pk_name in get_primary_key(model)] # Decode keys to tuples
ids = [literal_eval(id) for id in ids] decoded_ids = [iterdecode(v) for v in ids]
# Get model primary key property references
model_pk = [getattr(model, name) for name in get_primary_key(model)]
try: try:
query = modelquery.filter(tuple_(*model_pk).in_(ids)) query = modelquery.filter(tuple_(*model_pk).in_(decoded_ids))
# Only the execution of the query will tell us, if the tuple_ # Only the execution of the query will tell us, if the tuple_
# operator really works # operator really works
query.all() query.all()
except DBAPIError: except DBAPIError:
query = modelquery.filter(tuple_operator_in(model_pk, ids)) query = modelquery.filter(tuple_operator_in(model_pk, decoded_ids))
else: else:
model_pk = getattr(model, get_primary_key(model)) model_pk = getattr(model, get_primary_key(model))
query = modelquery.filter(model_pk.in_(ids)) query = modelquery.filter(model_pk.in_(ids))
return query return query
...@@ -16,7 +16,7 @@ from flask.ext.admin._backwards import ObsoleteAttr ...@@ -16,7 +16,7 @@ from flask.ext.admin._backwards import ObsoleteAttr
from flask.ext.admin.contrib.sqla import form, filters, tools from flask.ext.admin.contrib.sqla import form, filters, tools
from .typefmt import DEFAULT_FORMATTERS from .typefmt import DEFAULT_FORMATTERS
from .tools import is_inherited_primary_key, get_column_for_current_model, get_query_for_ids from .tools import get_query_for_ids
from .ajax import create_ajax_loader from .ajax import create_ajax_loader
...@@ -296,23 +296,20 @@ class ModelView(BaseModelView): ...@@ -296,23 +296,20 @@ class ModelView(BaseModelView):
# Scaffolding # Scaffolding
def scaffold_pk(self): def scaffold_pk(self):
""" """
Return the primary key name from a model Return the primary key name(s) from a model
PK can be a single value or a tuple if multiple PKs exist If model has single primary key, will return a string and tuple otherwise
""" """
return tools.get_primary_key(self.model) return tools.get_primary_key(self.model)
def get_pk_value(self, model): def get_pk_value(self, model):
""" """
Return the PK value from a model object. Return the primary key value from a model object.
PK can be a single value or a tuple if multiple PKs exist If there are multiple primary keys, they're encoded into string representation.
""" """
try: if isinstance(self._primary_key, tuple):
return tools.iterencode(getattr(model, attr) for attr in self._primary_key)
else:
return getattr(model, self._primary_key) return getattr(model, self._primary_key)
except TypeError:
v = []
for attr in self._primary_key:
v.append(getattr(model, attr))
return tuple(v)
def scaffold_list_columns(self): def scaffold_list_columns(self):
""" """
...@@ -321,26 +318,21 @@ class ModelView(BaseModelView): ...@@ -321,26 +318,21 @@ class ModelView(BaseModelView):
columns = [] columns = []
for p in self._get_model_iterator(): for p in self._get_model_iterator():
# Verify type
if hasattr(p, 'direction'): if hasattr(p, 'direction'):
if self.column_display_all_relations or p.direction.name == 'MANYTOONE': if self.column_display_all_relations or p.direction.name == 'MANYTOONE':
columns.append(p.key) columns.append(p.key)
elif hasattr(p, 'columns'): elif hasattr(p, 'columns'):
column_inherited_primary_key = False if len(p.columns) > 1:
filtered = tools.filter_foreign_columns(self.model.__table__, p.columns)
if len(p.columns) != 1: if len(filtered) > 1:
if is_inherited_primary_key(p): # TODO: Skip column and issue a warning
column = get_column_for_current_model(p)
else:
raise TypeError('Can not convert multiple-column properties (%s.%s)' % (self.model, p.key)) raise TypeError('Can not convert multiple-column properties (%s.%s)' % (self.model, p.key))
column = filtered[0]
else: else:
# Grab column
column = p.columns[0] column = p.columns[0]
# An inherited primary key has a foreign key as well
if column.foreign_keys and not is_inherited_primary_key(p):
continue
if not self.column_display_pk and column.primary_key: if not self.column_display_pk and column.primary_key:
continue continue
...@@ -783,7 +775,7 @@ class ModelView(BaseModelView): ...@@ -783,7 +775,7 @@ class ModelView(BaseModelView):
:param id: :param id:
Model id Model id
""" """
return self.session.query(self.model).get(id) return self.session.query(self.model).get(tools.iterdecode(id))
# Error handler # Error handler
def handle_view_exception(self, exc): def handle_view_exception(self, exc):
...@@ -882,7 +874,6 @@ class ModelView(BaseModelView): ...@@ -882,7 +874,6 @@ class ModelView(BaseModelView):
lazy_gettext('Are you sure you want to delete selected models?')) lazy_gettext('Are you sure you want to delete selected models?'))
def action_delete(self, ids): def action_delete(self, ids):
try: try:
query = get_query_for_ids(self.get_query(), self.model, ids) query = get_query_for_ids(self.get_query(), self.model, ids)
if self.fast_mass_delete: if self.fast_mass_delete:
......
...@@ -7,9 +7,34 @@ from flask.ext.admin._compat import text_type, as_unicode ...@@ -7,9 +7,34 @@ from flask.ext.admin._compat import text_type, as_unicode
from . import widgets as admin_widgets from . import widgets as admin_widgets
__all__ = ['TimeField', 'Select2Field', 'Select2TagsField'] """
An understanding of WTForms's Custom Widgets is helpful for understanding this code: http://wtforms.simplecodes.com/docs/0.6.2/widgets.html#custom-widgets
"""
__all__ = ['DateTimeField', 'TimeField', 'Select2Field', 'Select2TagsField']
class DateTimeField(fields.DateTimeField):
"""
Allows modifying the datetime format of a DateTimeField using form_args.
"""
widget = admin_widgets.DateTimePickerWidget()
def __init__(self, label=None, validators=None, format=None, **kwargs):
"""
Constructor
:param label:
Label
:param validators:
Field validators
:param format:
Format for text to date conversion. Defaults to '%Y-%m-%d %H:%M:%S'
:param kwargs:
Any additional parameters
"""
super(DateTimeField, self).__init__(label, validators, **kwargs)
self.format = format or '%Y-%m-%d %H:%M:%S'
class TimeField(fields.Field): class TimeField(fields.Field):
""" """
A text field which stores a `datetime.time` object. A text field which stores a `datetime.time` object.
...@@ -30,8 +55,6 @@ class TimeField(fields.Field): ...@@ -30,8 +55,6 @@ class TimeField(fields.Field):
Supported time formats, as a enumerable. Supported time formats, as a enumerable.
:param default_format: :param default_format:
Default time format. Defaults to '%H:%M:%S' Default time format. Defaults to '%H:%M:%S'
:param widget_format:
Widget date format. Defaults to 'hh:ii:ss'
:param kwargs: :param kwargs:
Any additional parameters Any additional parameters
""" """
...@@ -42,7 +65,6 @@ class TimeField(fields.Field): ...@@ -42,7 +65,6 @@ class TimeField(fields.Field):
'%I:%M:%S %p', '%I:%M %p') '%I:%M:%S %p', '%I:%M %p')
self.default_format = default_format or '%H:%M:%S' self.default_format = default_format or '%H:%M:%S'
self.widget_format = widget_format or 'hh:ii:ss'
def _value(self): def _value(self):
if self.raw_data: if self.raw_data:
......
...@@ -3,9 +3,7 @@ from flask.globals import _request_ctx_stack ...@@ -3,9 +3,7 @@ from flask.globals import _request_ctx_stack
from flask.ext.admin.babel import gettext, ngettext from flask.ext.admin.babel import gettext, ngettext
from flask.ext.admin import helpers as h from flask.ext.admin import helpers as h
__all__ = ['Select2Widget', 'DatePickerWidget', 'DateTimePickerWidget', 'RenderTemplateWidget', __all__ = ['Select2Widget', 'DatePickerWidget', 'DateTimePickerWidget', 'RenderTemplateWidget', 'Select2TagsWidget', ]
'Select2TagsWidget', ]
class Select2Widget(widgets.Select): class Select2Widget(widgets.Select):
""" """
...@@ -15,10 +13,9 @@ class Select2Widget(widgets.Select): ...@@ -15,10 +13,9 @@ class Select2Widget(widgets.Select):
work. work.
""" """
def __call__(self, field, **kwargs): def __call__(self, field, **kwargs):
kwargs.setdefault('data-role', u'select2')
allow_blank = getattr(field, 'allow_blank', False) allow_blank = getattr(field, 'allow_blank', False)
kwargs['data-role'] = u'select2'
if allow_blank and not self.multiple: if allow_blank and not self.multiple:
kwargs['data-allow-blank'] = u'1' kwargs['data-allow-blank'] = u'1'
...@@ -30,8 +27,8 @@ class Select2TagsWidget(widgets.TextInput): ...@@ -30,8 +27,8 @@ class Select2TagsWidget(widgets.TextInput):
You must include select2.js, form.js and select2 stylesheet for it to work. You must include select2.js, form.js and select2 stylesheet for it to work.
""" """
def __call__(self, field, **kwargs): def __call__(self, field, **kwargs):
kwargs['data-role'] = u'select2' kwargs.setdefault('data-role', u'select2')
kwargs['data-tags'] = u'1' kwargs.setdefault('data-tags', u'1')
return super(Select2TagsWidget, self).__call__(field, **kwargs) return super(Select2TagsWidget, self).__call__(field, **kwargs)
...@@ -43,9 +40,10 @@ class DatePickerWidget(widgets.TextInput): ...@@ -43,9 +40,10 @@ class DatePickerWidget(widgets.TextInput):
You must include bootstrap-datepicker.js and form.js for styling to work. You must include bootstrap-datepicker.js and form.js for styling to work.
""" """
def __call__(self, field, **kwargs): def __call__(self, field, **kwargs):
kwargs['data-role'] = u'datepicker' kwargs.setdefault('data-role', u'datepicker')
kwargs['data-date-format'] = u'yyyy-mm-dd' kwargs.setdefault('data-date-format', u'yyyy-mm-dd')
kwargs['data-date-autoclose'] = u'true' kwargs.setdefault('data-date-autoclose', u'true')
self.date_format = kwargs['data-date-format']
return super(DatePickerWidget, self).__call__(field, **kwargs) return super(DatePickerWidget, self).__call__(field, **kwargs)
...@@ -56,11 +54,11 @@ class DateTimePickerWidget(widgets.TextInput): ...@@ -56,11 +54,11 @@ class DateTimePickerWidget(widgets.TextInput):
You must include bootstrap-datepicker.js and form.js for styling to work. You must include bootstrap-datepicker.js and form.js for styling to work.
""" """
def __call__(self, field, **kwargs): def __call__(self, field, **kwargs):
kwargs['data-role'] = u'datetimepicker' kwargs.setdefault('data-role', u'datetimepicker')
kwargs['data-date-format'] = u'yyyy-mm-dd hh:ii:ss' kwargs.setdefault('data-date-format', u'yyyy-mm-dd hh:ii:ss')
kwargs['data-date-autoclose'] = u'true' kwargs.setdefault('data-date-autoclose', u'true')
kwargs['data-date-today-btn'] = u'linked' kwargs.setdefault('data-date-today-btn', u'linked')
kwargs['data-date-today-highlight'] = u'true' kwargs.setdefault('data-date-today-highlight', u'true')
return super(DateTimePickerWidget, self).__call__(field, **kwargs) return super(DateTimePickerWidget, self).__call__(field, **kwargs)
...@@ -71,9 +69,9 @@ class TimePickerWidget(widgets.TextInput): ...@@ -71,9 +69,9 @@ class TimePickerWidget(widgets.TextInput):
You must include bootstrap-datepicker.js and form.js for styling to work. You must include bootstrap-datepicker.js and form.js for styling to work.
""" """
def __call__(self, field, **kwargs): def __call__(self, field, **kwargs):
kwargs['data-role'] = u'timepicker' kwargs.setdefault('data-role', u'timepicker')
kwargs['data-date-format'] = field.widget_format or 'hh:ii:ss' kwargs.setdefault('data-date-format', u'hh:ii:ss')
kwargs['data-date-autoclose'] = u'true' kwargs.setdefault('data-date-autoclose', u'true')
return super(TimePickerWidget, self).__call__(field, **kwargs) return super(TimePickerWidget, self).__call__(field, **kwargs)
......
...@@ -365,6 +365,15 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -365,6 +365,15 @@ class BaseModelView(BaseView, ActionsMixin):
'style': 'color: black' 'style': 'color: black'
} }
} }
Note, changing the format of a DateTimeField will require changes to both form_widget_args and form_args:
form_args = dict(
start=dict(format='%Y-%m-%d %H:%M') # changes how the input is parsed by strptime
)
form_widget_args = dict(
start={'data-date-format': u'yyyy-mm-dd hh:ii'} # changes how the DateTimeField displays the time
)
""" """
form_extra_fields = None form_extra_fields = None
......
...@@ -30,7 +30,7 @@ def bool_formatter(view, value): ...@@ -30,7 +30,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'
return Markup('<span class="glyphicon glyphicon-%s"></span>' % glyph) return Markup('<span class="glyphicon glyphicon-%s icon-%s"></span>' % (glyph, glyph))
def list_formatter(view, values): def list_formatter(view, values):
......
...@@ -8,7 +8,7 @@ def setup(): ...@@ -8,7 +8,7 @@ def setup():
app.config['SECRET_KEY'] = '1' app.config['SECRET_KEY'] = '1'
app.config['CSRF_ENABLED'] = False app.config['CSRF_ENABLED'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///'
#app.config['SQLALCHEMY_ECHO'] = True app.config['SQLALCHEMY_ECHO'] = True
db = SQLAlchemy(app) db = SQLAlchemy(app)
admin = Admin(app) admin = Admin(app)
......
...@@ -468,42 +468,6 @@ def test_non_int_pk(): ...@@ -468,42 +468,6 @@ def test_non_int_pk():
data = rv.data.decode('utf-8') data = rv.data.decode('utf-8')
ok_('test2' in data) ok_('test2' in data)
def test_multiple__pk():
# Test multiple primary keys - mix int and string together
app, db, admin = setup()
class Model(db.Model):
id = db.Column(db.Integer, primary_key=True)
id2 = db.Column(db.String(20), primary_key=True)
test = db.Column(db.String)
db.create_all()
view = CustomModelView(Model, db.session, form_columns=['id', 'id2', 'test'])
admin.add_view(view)
client = app.test_client()
rv = client.get('/admin/model/')
eq_(rv.status_code, 200)
rv = client.post('/admin/model/new/',
data=dict(id=1, id2='two', test='test3'))
eq_(rv.status_code, 302)
rv = client.get('/admin/model/')
eq_(rv.status_code, 200)
data = rv.data.decode('utf-8')
ok_('test3' in data)
rv = client.get('/admin/model/edit/?id=1&id=two')
eq_(rv.status_code, 200)
data = rv.data.decode('utf-8')
ok_('test3' in data)
# Correct order is mandatory -> fail here
rv = client.get('/admin/model/edit/?id=two&id=1')
eq_(rv.status_code, 302)
def test_form_columns(): def test_form_columns():
app, db, admin = setup() app, db, admin = setup()
......
from nose.tools import eq_, ok_
from . import setup
from .test_basic import CustomModelView
from flask.ext.sqlalchemy import Model
from sqlalchemy.ext.declarative import declarative_base
def test_multiple_pk():
# Test multiple primary keys - mix int and string together
app, db, admin = setup()
class Model(db.Model):
id = db.Column(db.Integer, primary_key=True)
id2 = db.Column(db.String(20), primary_key=True)
test = db.Column(db.String)
db.create_all()
view = CustomModelView(Model, db.session, form_columns=['id', 'id2', 'test'])
admin.add_view(view)
client = app.test_client()
rv = client.get('/admin/model/')
eq_(rv.status_code, 200)
rv = client.post('/admin/model/new/',
data=dict(id=1, id2='two', test='test3'))
eq_(rv.status_code, 302)
rv = client.get('/admin/model/')
eq_(rv.status_code, 200)
data = rv.data.decode('utf-8')
ok_('test3' in data)
rv = client.get('/admin/model/edit/?id=1,two')
eq_(rv.status_code, 200)
data = rv.data.decode('utf-8')
ok_('test3' in data)
# Correct order is mandatory -> fail here
rv = client.get('/admin/model/edit/?id=two,1')
eq_(rv.status_code, 302)
def test_joined_inheritance():
# Test multiple primary keys - mix int and string together
app, db, admin = setup()
class Parent(db.Model):
id = db.Column(db.Integer, primary_key=True)
test = db.Column(db.String)
discriminator = db.Column('type', db.String(50))
__mapper_args__ = {'polymorphic_on': discriminator}
class Child(Parent):
__tablename__ = 'children'
__mapper_args__ = {'polymorphic_identity': 'child'}
id = db.Column(db.ForeignKey(Parent.id), primary_key=True)
name = db.Column(db.String(100))
db.create_all()
view = CustomModelView(Child, db.session, form_columns=['id', 'test', 'name'])
admin.add_view(view)
client = app.test_client()
rv = client.get('/admin/child/')
eq_(rv.status_code, 200)
rv = client.post('/admin/child/new/',
data=dict(id=1, test='foo', name='bar'))
eq_(rv.status_code, 302)
rv = client.get('/admin/child/edit/?id=1')
eq_(rv.status_code, 200)
data = rv.data.decode('utf-8')
ok_('foo' in data)
ok_('bar' in data)
def test_single_table_inheritance():
# Test multiple primary keys - mix int and string together
app, db, admin = setup()
CustomModel = declarative_base(Model, name='Model')
class Parent(CustomModel):
__tablename__ = 'parent'
id = db.Column(db.Integer, primary_key=True)
test = db.Column(db.String)
discriminator = db.Column('type', db.String(50))
__mapper_args__ = {'polymorphic_on': discriminator}
class Child(Parent):
__mapper_args__ = {'polymorphic_identity': 'child'}
name = db.Column(db.String(100))
CustomModel.metadata.create_all(db.engine)
view = CustomModelView(Child, db.session, form_columns=['id', 'test', 'name'])
admin.add_view(view)
client = app.test_client()
rv = client.get('/admin/child/')
eq_(rv.status_code, 200)
rv = client.post('/admin/child/new/',
data=dict(id=1, test='foo', name='bar'))
eq_(rv.status_code, 302)
rv = client.get('/admin/child/edit/?id=1')
eq_(rv.status_code, 200)
data = rv.data.decode('utf-8')
ok_('foo' in data)
ok_('bar' in data)
def test_concrete_table_inheritance():
# Test multiple primary keys - mix int and string together
app, db, admin = setup()
class Parent(db.Model):
id = db.Column(db.Integer, primary_key=True)
test = db.Column(db.String)
class Child(Parent):
__mapper_args__ = {'concrete': True}
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100))
test = db.Column(db.String)
db.create_all()
view = CustomModelView(Child, db.session, form_columns=['id', 'test', 'name'])
admin.add_view(view)
client = app.test_client()
rv = client.get('/admin/child/')
eq_(rv.status_code, 200)
rv = client.post('/admin/child/new/',
data=dict(id=1, test='foo', name='bar'))
eq_(rv.status_code, 302)
rv = client.get('/admin/child/edit/?id=1')
eq_(rv.status_code, 200)
data = rv.data.decode('utf-8')
ok_('foo' in data)
ok_('bar' in data)
def test_concrete_multipk_inheritance():
# Test multiple primary keys - mix int and string together
app, db, admin = setup()
class Parent(db.Model):
id = db.Column(db.Integer, primary_key=True)
test = db.Column(db.String)
class Child(Parent):
__mapper_args__ = {'concrete': True}
id = db.Column(db.Integer, primary_key=True)
id2 = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100))
test = db.Column(db.String)
db.create_all()
view = CustomModelView(Child, db.session, form_columns=['id', 'id2', 'test', 'name'])
admin.add_view(view)
client = app.test_client()
rv = client.get('/admin/child/')
eq_(rv.status_code, 200)
rv = client.post('/admin/child/new/',
data=dict(id=1, id2=2, test='foo', name='bar'))
eq_(rv.status_code, 302)
rv = client.get('/admin/child/edit/?id=1,2')
eq_(rv.status_code, 200)
data = rv.data.decode('utf-8')
ok_('foo' in data)
ok_('bar' in data)
from nose.tools import eq_, ok_
from flask.ext.admin import tools
def test_encode_decode():
eq_(tools.iterdecode(tools.iterencode([1, 2, 3])), (u'1', u'2', u'3'))
eq_(tools.iterdecode(tools.iterencode([',', ',', ','])), (u',', u',', u','))
eq_(tools.iterdecode(tools.iterencode(['.hello.,', ',', ','])), (u'.hello.,', u',', u','))
eq_(tools.iterdecode(tools.iterencode(['.....,,,.,,..,.,,.,'])), (u'.....,,,.,,..,.,,.,',))
eq_(tools.iterdecode(tools.iterencode([])), tuple())
# Malformed inputs should not crash
ok_(tools.iterdecode('.'))
eq_(tools.iterdecode(','), (u'', u''))
...@@ -2,7 +2,10 @@ import sys ...@@ -2,7 +2,10 @@ import sys
import traceback import traceback
# Python 3 compatibility # Python 3 compatibility
from ._compat import reduce from ._compat import reduce, as_unicode
CHAR_ESCAPE = u'.'
CHAR_SEPARATOR = u','
def import_module(name, required=True): def import_module(name, required=True):
...@@ -96,3 +99,47 @@ def get_dict_attr(obj, attr, default=None): ...@@ -96,3 +99,47 @@ def get_dict_attr(obj, attr, default=None):
return obj.__dict__[attr] return obj.__dict__[attr]
return default return default
def iterencode(iter):
"""
Encode enumerable as compact string representation.
:param iter:
Enumerable
"""
return ','.join(as_unicode(v)
.replace(CHAR_ESCAPE, CHAR_ESCAPE + CHAR_ESCAPE)
.replace(CHAR_SEPARATOR, CHAR_ESCAPE + CHAR_SEPARATOR)
for v in iter)
def iterdecode(value):
"""
Decode enumerable from string presentation as a tuple
"""
if not value:
return tuple()
result = []
accumulator = u''
escaped = False
for c in value:
if not escaped:
if c == CHAR_ESCAPE:
escaped = True
continue
elif c == CHAR_SEPARATOR:
result.append(accumulator)
accumulator = u''
continue
else:
escaped = False
accumulator += c
result.append(accumulator)
return tuple(result)
...@@ -45,7 +45,7 @@ setup( ...@@ -45,7 +45,7 @@ setup(
platforms='any', platforms='any',
install_requires=[ install_requires=[
'Flask>=0.7', 'Flask>=0.7',
'wtforms' 'wtforms<2.0'
], ],
tests_require=[ tests_require=[
'nose>=1.0', 'nose>=1.0',
......
Flask>=0.7 Flask>=0.7
wtforms wtforms<2.0
Flask-SQLAlchemy>=0.15 Flask-SQLAlchemy>=0.15
peewee peewee
wtf-peewee wtf-peewee
......
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