Commit 675c2846 authored by Paul Brown's avatar Paul Brown

add hstore column converter to SQLA backend

parent 572f1d39
......@@ -16,7 +16,8 @@ services:
before_script:
- psql -U postgres -c 'CREATE DATABASE flask_admin_test;'
- psql -U postgres -c "CREATE EXTENSION postgis;" flask_admin_test
- psql -U postgres -c 'CREATE EXTENSION postgis;' flask_admin_test
- psql -U postgres -c 'CREATE EXTENSION hstore;' flask_admin_test
install:
- pip install "wtforms<$WTFORMS_VERSION.99"
......
......@@ -4,15 +4,19 @@
import operator
from wtforms import widgets
from wtforms.fields import SelectFieldBase
from wtforms.fields import SelectFieldBase, TextField
from wtforms.validators import ValidationError
try:
from wtforms.fields import _unset_value as unset_value
except ImportError:
from wtforms.utils import unset_value
from .tools import get_primary_key
from flask_admin._compat import text_type, string_types
from flask_admin.form import FormOpts
from flask_admin._compat import text_type, string_types, iteritems
from flask_admin.form import FormOpts, BaseForm
from flask_admin.model.fields import InlineFieldList, InlineModelFormField
from flask_admin.model.widgets import InlineFormWidget
from flask_admin.babel import lazy_gettext
try:
from sqlalchemy.orm.util import identity_key
......@@ -178,6 +182,46 @@ class QuerySelectMultipleField(QuerySelectField):
raise ValidationError(self.gettext(u'Not a valid choice'))
class HstoreForm(BaseForm):
""" Form used in InlineFormField/InlineHstoreList for HSTORE columns """
key = TextField(lazy_gettext('Key'))
value = TextField(lazy_gettext('Value'))
class KeyValue(object):
""" Used by InlineHstoreList to simulate a key and a value field instead of
the single HSTORE column. """
def __init__(self, key=None, value=None):
self.key = key
self.value = value
class InlineHstoreList(InlineFieldList):
""" Version of InlineFieldList for use with Postgres HSTORE columns """
def process(self, formdata, data=unset_value):
""" SQLAlchemy returns a dict for HSTORE columns, but WTForms cannot
process a dict. This overrides `process` to convert the dict
returned by SQLAlchemy to a list of classes before processing. """
if isinstance(data, dict):
data = [KeyValue(k, v) for k, v in iteritems(data)]
super(InlineHstoreList, self).process(formdata, data)
def populate_obj(self, obj, name):
""" Combines each FormField key/value into a dictionary for storage """
_fake = type(str('_fake'), (object, ), {})
output = {}
for form_field in self.entries:
if not self.should_delete(form_field):
fake_obj = _fake()
fake_obj.data = KeyValue()
form_field.populate_obj(fake_obj, 'data')
output[fake_obj.data.key] = fake_obj.data.value
setattr(obj, name, output)
class InlineModelFormList(InlineFieldList):
"""
Customized inline model form list field.
......
......@@ -12,7 +12,9 @@ from flask_admin._backwards import get_property
from flask_admin._compat import iteritems
from .validators import Unique
from .fields import QuerySelectField, QuerySelectMultipleField, InlineModelFormList
from .fields import (QuerySelectField, QuerySelectMultipleField,
InlineModelFormList, InlineHstoreList, HstoreForm)
from flask_admin.model.fields import InlineFormField
from .tools import has_multiple_pks, filter_foreign_columns
from .ajax import create_ajax_loader
......@@ -350,6 +352,11 @@ class AdminModelConverter(ModelConverterBase):
def conv_ARRAY(self, field_args, **extra):
return form.Select2TagsField(save_as_list=True, **field_args)
@converts('HSTORE')
def conv_HSTORE(self, field_args, **extra):
inner_form = field_args.pop('form', HstoreForm)
return InlineHstoreList(InlineFormField(inner_form), **field_args)
def _resolve_prop(prop):
"""
......
......@@ -40,7 +40,7 @@ class InlineFieldList(FieldList):
def display_row_controls(self, field):
return True
def process(self, formdata, data=None):
def process(self, formdata, data=unset_value):
res = super(InlineFieldList, self).process(formdata, data)
# Postprocess - contribute flag
......
......@@ -14,3 +14,15 @@ def setup():
admin = Admin(app)
return app, db, admin
def setup_postgres():
app = Flask(__name__)
app.config['SECRET_KEY'] = '1'
app.config['CSRF_ENABLED'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://localhost/flask_admin_test'
app.config['SQLALCHEMY_ECHO'] = True
db = SQLAlchemy(app)
admin = Admin(app)
return app, db, admin
from nose.tools import eq_, ok_
from . import setup_postgres
from .test_basic import CustomModelView
from sqlalchemy.dialects.postgresql import HSTORE
def test_hstore():
app, db, admin = setup_postgres()
class Model(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
hstore_test = db.Column(HSTORE)
db.create_all()
view = CustomModelView(Model, db.session)
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={
'hstore_test-0-key': 'test_val1',
'hstore_test-0-value': 'test_val2'
})
eq_(rv.status_code, 302)
rv = client.get('/admin/model/')
eq_(rv.status_code, 200)
data = rv.data.decode('utf-8')
ok_('test_val1' in data)
ok_('test_val2' in data)
rv = client.get('/admin/model/edit/?id=1')
eq_(rv.status_code, 200)
data = rv.data.decode('utf-8')
ok_('test_val1' in data)
ok_('test_val2' in data)
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