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
: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
---------------
......
......@@ -23,6 +23,7 @@ if not PY2:
iterkeys = lambda d: iter(d.keys())
itervalues = lambda d: iter(d.values())
iteritems = lambda d: iter(d.items())
filter_list = lambda f, l: list(filter(f, l))
def as_unicode(s):
if isinstance(s, bytes):
......@@ -41,6 +42,7 @@ else:
iterkeys = lambda d: d.iterkeys()
itervalues = lambda d: d.itervalues()
iteritems = lambda d: d.iteritems()
filter_list = filter
def as_unicode(s):
if isinstance(s, str):
......
......@@ -603,7 +603,7 @@ class FileAdmin(BaseView, ActionsMixin):
try:
shutil.rmtree(full_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:
flash(gettext('Failed to delete directory: %(error)s', error=ex), 'error')
else:
......
from werkzeug.datastructures import FileStorage
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 flask.ext.admin.model.fields import InlineFormField
......@@ -47,7 +51,7 @@ class MongoFileField(fields.FileField):
self._should_delete = False
def process(self, formdata, data=_unset_value):
def process(self, formdata, data=unset_value):
if formdata:
marker = '_%s-delete' % self.name
if marker in formdata:
......
......@@ -95,32 +95,32 @@ class FilterConverter(filters.BaseFilterConverter):
enum = (FilterEqual, FilterNotEqual)
def convert(self, type_name, column, name, **kwargs):
if type_name in self.converters:
return self.converters[type_name](column, name, **kwargs)
if type_name.lower() in self.converters:
return self.converters[type_name.lower()](column, name, **kwargs)
return None
@filters.convert('String', 'Unicode', 'Text', 'UnicodeText')
@filters.convert('string', 'unicode', 'text', 'unicodetext')
def conv_string(self, column, name, **kwargs):
return [f(column, name, **kwargs) for f in self.strings]
@filters.convert('Boolean')
@filters.convert('boolean')
def conv_bool(self, column, name, **kwargs):
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):
return [f(column, name, **kwargs) for f in self.numeric]
@filters.convert('Date')
@filters.convert('date')
def conv_date(self, column, name, **kwargs):
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):
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):
if not options:
options = [
......
......@@ -2,7 +2,6 @@ from wtforms import fields, validators
from sqlalchemy import Boolean, Column
from flask.ext.admin import form
from flask.ext.admin.form import Select2Field
from flask.ext.admin.model.form import (converts, ModelConverterBase,
InlineModelConverterBase, FieldPlaceholder)
from flask.ext.admin.model.fields import AjaxSelectField, AjaxSelectMultipleField
......@@ -12,7 +11,7 @@ from flask.ext.admin._compat import iteritems
from .validators import Unique
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
try:
......@@ -152,11 +151,13 @@ class AdminModelConverter(ModelConverterBase):
# Ignore pk/fk
if hasattr(prop, 'columns'):
# Check if more than one column mapped to the property
if len(prop.columns) != 1:
if is_inherited_primary_key(prop):
column = get_column_for_current_model(prop)
else:
if len(prop.columns) > 1:
columns = filter_foreign_columns(model.__table__, prop.columns)
if len(columns) > 1:
raise TypeError('Can not convert multiple-column properties (%s.%s)' % (model, prop.key))
column = columns[0]
else:
# Grab column
column = prop.columns[0]
......@@ -238,7 +239,7 @@ class AdminModelConverter(ModelConverterBase):
if mapper.class_ == self.view.model and form_choices:
choices = form_choices.get(column.key)
if choices:
return Select2Field(
return form.Select2Field(
choices=choices,
allow_blank=column.nullable,
**kwargs
......@@ -292,8 +293,7 @@ class AdminModelConverter(ModelConverterBase):
@converts('DateTime')
def convert_datetime(self, field_args, **extra):
field_args['widget'] = form.DateTimePickerWidget()
return DateTimeField(**field_args)
return form.DateTimeField(**field_args)
@converts('Time')
def convert_time(self, field_args, **extra):
......
......@@ -3,6 +3,10 @@ from sqlalchemy.sql.operators import eq
from sqlalchemy.exc import DBAPIError
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):
if term.startswith('^'):
stmt = '%s%%' % term[1:]
......@@ -14,6 +18,16 @@ def parse_like_term(term):
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):
"""
Return primary key name from a model. If the primary key consists of multiple columns,
......@@ -26,18 +40,11 @@ def get_primary_key(model):
pks = []
for p in props:
if hasattr(p, 'expression'): # expression = primary column or expression for this ColumnProperty
if p.expression.primary_key:
if is_inherited_primary_key(p):
pks.append(get_column_for_current_model(p).key)
else:
if hasattr(p, 'columns'):
for c in filter_foreign_columns(model.__table__, p.columns):
if c.primary_key:
pks.append(p.key)
else:
if hasattr(p, 'columns'):
for c in p.columns:
if c.primary_key:
pks.append(p.key)
break
break
if len(pks) == 1:
return pks[0]
......@@ -46,54 +53,16 @@ def get_primary_key(model):
else:
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):
"""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'):
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):
"""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):
def get_query_for_ids(modelquery, model, ids):
"""
Return a query object, that contains all entities of the given model for
the primary keys provided in the ids-parameter.
The ``pks`` parameter is a tuple, that contains the different primary key values,
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.
Return a query object filtered by primary key values passed in `ids` argument.
Unfortunately, it is not possible to use `in_` filter if model has more than one
primary key.
"""
if has_multiple_pks(model):
model_pk = [getattr(model, pk_name).expression for pk_name in get_primary_key(model)]
ids = [literal_eval(id) for id in ids]
# Decode keys to tuples
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:
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_
# operator really works
query.all()
except DBAPIError:
query = modelquery.filter(tuple_operator_in(model_pk, ids))
query = modelquery.filter(tuple_operator_in(model_pk, decoded_ids))
else:
model_pk = getattr(model, get_primary_key(model))
query = modelquery.filter(model_pk.in_(ids))
return query
......@@ -16,7 +16,7 @@ from flask.ext.admin._backwards import ObsoleteAttr
from flask.ext.admin.contrib.sqla import form, filters, tools
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
......@@ -296,23 +296,20 @@ class ModelView(BaseModelView):
# Scaffolding
def scaffold_pk(self):
"""
Return the primary key name from a model
PK can be a single value or a tuple if multiple PKs exist
Return the primary key name(s) from a model
If model has single primary key, will return a string and tuple otherwise
"""
return tools.get_primary_key(self.model)
def get_pk_value(self, model):
"""
Return the PK value from a model object.
PK can be a single value or a tuple if multiple PKs exist
Return the primary key value from a model object.
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)
except TypeError:
v = []
for attr in self._primary_key:
v.append(getattr(model, attr))
return tuple(v)
def scaffold_list_columns(self):
"""
......@@ -321,26 +318,21 @@ class ModelView(BaseModelView):
columns = []
for p in self._get_model_iterator():
# Verify type
if hasattr(p, 'direction'):
if self.column_display_all_relations or p.direction.name == 'MANYTOONE':
columns.append(p.key)
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 is_inherited_primary_key(p):
column = get_column_for_current_model(p)
else:
if len(filtered) > 1:
# TODO: Skip column and issue a warning
raise TypeError('Can not convert multiple-column properties (%s.%s)' % (self.model, p.key))
column = filtered[0]
else:
# Grab column
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:
continue
......@@ -783,7 +775,7 @@ class ModelView(BaseModelView):
:param id:
Model id
"""
return self.session.query(self.model).get(id)
return self.session.query(self.model).get(tools.iterdecode(id))
# Error handler
def handle_view_exception(self, exc):
......@@ -882,7 +874,6 @@ class ModelView(BaseModelView):
lazy_gettext('Are you sure you want to delete selected models?'))
def action_delete(self, ids):
try:
query = get_query_for_ids(self.get_query(), self.model, ids)
if self.fast_mass_delete:
......
......@@ -7,9 +7,34 @@ from flask.ext.admin._compat import text_type, as_unicode
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):
"""
A text field which stores a `datetime.time` object.
......@@ -30,8 +55,6 @@ class TimeField(fields.Field):
Supported time formats, as a enumerable.
:param default_format:
Default time format. Defaults to '%H:%M:%S'
:param widget_format:
Widget date format. Defaults to 'hh:ii:ss'
:param kwargs:
Any additional parameters
"""
......@@ -42,7 +65,6 @@ class TimeField(fields.Field):
'%I:%M:%S %p', '%I:%M %p')
self.default_format = default_format or '%H:%M:%S'
self.widget_format = widget_format or 'hh:ii:ss'
def _value(self):
if self.raw_data:
......
......@@ -3,9 +3,7 @@ from flask.globals import _request_ctx_stack
from flask.ext.admin.babel import gettext, ngettext
from flask.ext.admin import helpers as h
__all__ = ['Select2Widget', 'DatePickerWidget', 'DateTimePickerWidget', 'RenderTemplateWidget',
'Select2TagsWidget', ]
__all__ = ['Select2Widget', 'DatePickerWidget', 'DateTimePickerWidget', 'RenderTemplateWidget', 'Select2TagsWidget', ]
class Select2Widget(widgets.Select):
"""
......@@ -15,10 +13,9 @@ class Select2Widget(widgets.Select):
work.
"""
def __call__(self, field, **kwargs):
kwargs.setdefault('data-role', u'select2')
allow_blank = getattr(field, 'allow_blank', False)
kwargs['data-role'] = u'select2'
if allow_blank and not self.multiple:
kwargs['data-allow-blank'] = u'1'
......@@ -30,8 +27,8 @@ class Select2TagsWidget(widgets.TextInput):
You must include select2.js, form.js and select2 stylesheet for it to work.
"""
def __call__(self, field, **kwargs):
kwargs['data-role'] = u'select2'
kwargs['data-tags'] = u'1'
kwargs.setdefault('data-role', u'select2')
kwargs.setdefault('data-tags', u'1')
return super(Select2TagsWidget, self).__call__(field, **kwargs)
......@@ -43,9 +40,10 @@ class DatePickerWidget(widgets.TextInput):
You must include bootstrap-datepicker.js and form.js for styling to work.
"""
def __call__(self, field, **kwargs):
kwargs['data-role'] = u'datepicker'
kwargs['data-date-format'] = u'yyyy-mm-dd'
kwargs['data-date-autoclose'] = u'true'
kwargs.setdefault('data-role', u'datepicker')
kwargs.setdefault('data-date-format', u'yyyy-mm-dd')
kwargs.setdefault('data-date-autoclose', u'true')
self.date_format = kwargs['data-date-format']
return super(DatePickerWidget, self).__call__(field, **kwargs)
......@@ -56,11 +54,11 @@ class DateTimePickerWidget(widgets.TextInput):
You must include bootstrap-datepicker.js and form.js for styling to work.
"""
def __call__(self, field, **kwargs):
kwargs['data-role'] = u'datetimepicker'
kwargs['data-date-format'] = u'yyyy-mm-dd hh:ii:ss'
kwargs['data-date-autoclose'] = u'true'
kwargs['data-date-today-btn'] = u'linked'
kwargs['data-date-today-highlight'] = u'true'
kwargs.setdefault('data-role', u'datetimepicker')
kwargs.setdefault('data-date-format', u'yyyy-mm-dd hh:ii:ss')
kwargs.setdefault('data-date-autoclose', u'true')
kwargs.setdefault('data-date-today-btn', u'linked')
kwargs.setdefault('data-date-today-highlight', u'true')
return super(DateTimePickerWidget, self).__call__(field, **kwargs)
......@@ -71,9 +69,9 @@ class TimePickerWidget(widgets.TextInput):
You must include bootstrap-datepicker.js and form.js for styling to work.
"""
def __call__(self, field, **kwargs):
kwargs['data-role'] = u'timepicker'
kwargs['data-date-format'] = field.widget_format or 'hh:ii:ss'
kwargs['data-date-autoclose'] = u'true'
kwargs.setdefault('data-role', u'timepicker')
kwargs.setdefault('data-date-format', u'hh:ii:ss')
kwargs.setdefault('data-date-autoclose', u'true')
return super(TimePickerWidget, self).__call__(field, **kwargs)
......
......@@ -365,6 +365,15 @@ class BaseModelView(BaseView, ActionsMixin):
'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
......
......@@ -30,7 +30,7 @@ def bool_formatter(view, value):
Value to check
"""
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):
......
......@@ -8,7 +8,7 @@ def setup():
app.config['SECRET_KEY'] = '1'
app.config['CSRF_ENABLED'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///'
#app.config['SQLALCHEMY_ECHO'] = True
app.config['SQLALCHEMY_ECHO'] = True
db = SQLAlchemy(app)
admin = Admin(app)
......
......@@ -468,42 +468,6 @@ def test_non_int_pk():
data = rv.data.decode('utf-8')
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():
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
import traceback
# 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):
......@@ -96,3 +99,47 @@ def get_dict_attr(obj, attr, default=None):
return obj.__dict__[attr]
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(
platforms='any',
install_requires=[
'Flask>=0.7',
'wtforms'
'wtforms<2.0'
],
tests_require=[
'nose>=1.0',
......
Flask>=0.7
wtforms
wtforms<2.0
Flask-SQLAlchemy>=0.15
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