Commit 4383eef3 authored by Serge S. Koval's avatar Serge S. Koval

Changed inline form conversion logic, fixed peewee support

parent c1d85d3c
......@@ -13,3 +13,5 @@ make.bat
venv
*.sqlite
*.sublime-*
.coverage
from flask.ext.admin.babel import gettext
from flask.ext.admin.model import filters
from .tools import parse_like_term
class BasePeeweeFilter(filters.BaseFilter):
......@@ -44,7 +45,8 @@ class FilterNotEqual(BasePeeweeFilter):
class FilterLike(BasePeeweeFilter):
def apply(self, query, value):
return query.filter(self.column ** value)
term = parse_like_term(value)
return query.filter(self.column ** term)
def operation(self):
return gettext('contains')
......@@ -52,7 +54,8 @@ class FilterLike(BasePeeweeFilter):
class FilterNotLike(BasePeeweeFilter):
def apply(self, query, value):
return query.filter(~(self.column ** value))
term = parse_like_term(value)
return query.filter(~(self.column ** term))
def operation(self):
return gettext('not contains')
......
......@@ -5,7 +5,7 @@ from peewee import DateTimeField, DateField, TimeField, BaseModel, ForeignKeyFie
from wtfpeewee.orm import ModelConverter, model_form
from flask.ext.admin import form
from flask.ext.admin.model.form import InlineFormAdmin
from flask.ext.admin.model.form import InlineFormAdmin, InlineModelConverterBase
from flask.ext.admin.model.fields import InlineModelFormField
from flask.ext.admin.model.widgets import InlineFormListWidget
......@@ -29,7 +29,8 @@ class InlineModelFormList(fields.FieldList):
def process(self, formdata, data=None):
if not formdata:
data = self.model.select().where(user=data).execute()
attr = getattr(self.model, self.prop)
data = self.model.select().where(attr == data).execute()
else:
data = None
......@@ -41,7 +42,8 @@ class InlineModelFormList(fields.FieldList):
def save_related(self, obj):
model_id = getattr(obj, self._pk)
values = self.model.select().where(user=model_id).execute()
attr = getattr(self.model, self.prop)
values = self.model.select().where(attr == model_id).execute()
pk_map = dict((str(getattr(v, self._pk)), v) for v in values)
......@@ -85,37 +87,18 @@ class CustomModelConverter(ModelConverter):
return field.name, form.TimeField(**kwargs)
def contribute_inline(model, form_class, inline_models):
# Contribute columns
for p in inline_models:
# Figure out settings
if isinstance(p, tuple):
info = InlineFormAdmin(p[0], **p[1])
elif isinstance(p, InlineFormAdmin):
info = p
elif isinstance(p, BaseModel):
info = InlineFormAdmin(p)
else:
model = getattr(p, 'model', None)
if model is None:
raise Exception('Unknown inline model admin: %s' % repr(p))
attrs = dict()
for attr in dir(p):
if not attr.startswith('_') and attr != model:
attrs[attr] = getattr(p, attr)
info = InlineFormAdmin(model, **attrs)
class InlineModelConverter(InlineModelConverterBase):
def contribute(self, converter, model, form_class, inline_model):
# Find property from target model to current model
reverse_field = None
info = self.get_info(inline_model)
for field in info.model._meta.get_fields():
field_type = type(field)
if field_type == ForeignKeyField:
if field.to == model:
if field.rel_model == model:
reverse_field = field
break
else:
......@@ -130,7 +113,6 @@ def contribute_inline(model, form_class, inline_models):
exclude = ignore
# Create field
converter = CustomModelConverter()
child_form = model_form(info.model,
base_class=form.BaseForm,
only=info.form_columns,
......@@ -140,7 +122,6 @@ def contribute_inline(model, form_class, inline_models):
converter=converter)
prop_name = 'fa_%s' % model.__name__
setattr(form_class,
prop_name,
InlineModelFormList(child_form,
......@@ -148,11 +129,11 @@ def contribute_inline(model, form_class, inline_models):
reverse_field.name,
label=info.model.__name__))
setattr(field.to,
setattr(field.rel_model,
prop_name,
property(lambda self: self.id))
return form_class
return form_class
def save_inline(form, model):
......
......@@ -5,3 +5,14 @@ def get_primary_key(model):
for n, f in model._meta.get_sorted_fields():
if type(f) == PrimaryKeyField:
return n
def parse_like_term(term):
if term.startswith('^'):
stmt = '%s%%' % term[1:]
elif term.startswith('='):
stmt = term[1:]
else:
stmt = '%%%s%%' % term
return stmt
import logging
from flask import flash
from flask.ext.admin import form
......@@ -9,8 +11,8 @@ from wtfpeewee.orm import model_form
from flask.ext.admin.actions import action
from flask.ext.admin.contrib.peeweemodel import filters
from .form import CustomModelConverter, contribute_inline, save_inline
from .tools import get_primary_key
from .form import CustomModelConverter, InlineModelConverter, save_inline
from .tools import get_primary_key, parse_like_term
class ModelView(BaseModelView):
......@@ -45,6 +47,20 @@ class ModelView(BaseModelView):
model_form_converter = MyModelConverter
"""
inline_model_form_converter = InlineModelConverter
"""
Inline model conversion class. If you need some kind of post-processing for inline
forms, you can customize behavior by doing something like this::
class MyInlineModelConverter(AdminModelConverter):
def post_process(self, form_class, info):
form_class.value = wtf.TextField('value')
return form_class
class MyAdminView(ModelView):
inline_model_form_converter = MyInlineModelConverter
"""
filter_converter = filters.FilterConverter()
"""
Field to filter converter.
......@@ -165,8 +181,8 @@ class ModelView(BaseModelView):
raise Exception('Failed to find field for filter: %s' % name)
# Check if field is in different model
if attr.model != self.model:
visible_name = '%s / %s' % (self.get_column_name(attr.model.__name__),
if attr.model_class != self.model:
visible_name = '%s / %s' % (self.get_column_name(attr.model_class.__name__),
self.get_column_name(attr.name))
else:
if not isinstance(name, basestring):
......@@ -193,16 +209,28 @@ class ModelView(BaseModelView):
converter=self.model_form_converter())
if self.inline_models:
form_class = contribute_inline(self.model, form_class, self.inline_models)
form_class = self.scaffold_inline_form_models(form_class)
return form_class
def scaffold_inline_form_models(self, form_class):
converter = self.model_form_converter()
inline_converter = self.inline_model_form_converter()
for m in self.inline_models:
form_class = inline_converter.contribute(converter,
self.model,
form_class,
m)
return form_class
def _handle_join(self, query, field, joins):
if field.model != self.model:
model_name = field.model.__name__
if field.model_class != self.model:
model_name = field.model_class.__name__
if model_name not in joins:
query = query.join(field.model)
query = query.join(field.model_class)
joins.add(model_name)
return query
......@@ -215,12 +243,14 @@ class ModelView(BaseModelView):
# Search
if self._search_supported and search:
terms = search.split(' ')
values = search.split(' ')
for term in terms:
if not term:
for value in values:
if not value:
continue
term = parse_like_term(value)
stmt = None
for field in self._search_fields:
query = self._handle_join(query, field, joins)
......@@ -250,14 +280,13 @@ class ModelView(BaseModelView):
sort_field = self._sortable_columns[sort_column]
if isinstance(sort_field, basestring):
query = query.order_by((sort_field, sort_desc and 'desc' or 'asc'))
field = getattr(self.model, sort_field)
query = query.order_by(field.desc() if sort_desc else field.asc())
elif isinstance(sort_field, Field):
if sort_field.model != self.model:
if sort_field.model_class != self.model:
query = self._handle_join(query, sort_field, joins)
query = query.order_by((sort_field.model, sort_field.name, sort_desc and 'desc' or 'asc'))
else:
query = query.order_by((sort_column, sort_desc and 'desc' or 'asc'))
query = query.order_by(sort_field.desc() if sort_desc else sort_field.asc())
# Pagination
if page is not None:
......@@ -286,6 +315,7 @@ class ModelView(BaseModelView):
return True
except Exception, ex:
flash(gettext('Failed to create model. %(error)s', error=str(ex)), 'error')
logging.exception('Failed to create model')
return False
def update_model(self, form, model):
......@@ -300,6 +330,7 @@ class ModelView(BaseModelView):
return True
except Exception, ex:
flash(gettext('Failed to update model. %(error)s', error=str(ex)), 'error')
logging.exception('Failed to update model')
return False
def delete_model(self, model):
......@@ -309,6 +340,7 @@ class ModelView(BaseModelView):
return True
except Exception, ex:
flash(gettext('Failed to delete model. %(error)s', error=str(ex)), 'error')
logging.exception('Failed to delete model')
return False
# Default model actions
......
......@@ -2,7 +2,7 @@ from wtforms import fields, validators
from sqlalchemy import Boolean, Column
from flask.ext.admin import form
from flask.ext.admin.model.form import converts, ModelConverterBase, InlineFormAdmin
from flask.ext.admin.model.form import converts, ModelConverterBase, InlineModelConverterBase
from .validators import Unique
from .fields import QuerySelectField, QuerySelectMultipleField, InlineModelFormList
......@@ -248,6 +248,7 @@ class AdminModelConverter(ModelConverterBase):
def conv_ARRAY(self, field_args, **extra):
return form.Select2TagsField(save_as_list=True, **field_args)
# Get list of fields and generate form
def get_form(model, converter,
base_class=form.BaseForm,
......@@ -319,53 +320,40 @@ def get_form(model, converter,
return type(model.__name__ + 'Form', (base_class, ), field_dict)
def contribute_inline(session, model, form_class, inline_models):
class InlineModelConverter(InlineModelConverterBase):
"""
Generate form fields for inline forms and contribute them to
the `form_class`
:param session:
SQLAlchemy session
:param model:
Model class
:param form_class:
Form to add properties to
:param inline_models:
List of inline model definitions. Can be one of:
- ``tuple``, first value is related model instance,
second is dictionary with options
- ``InlineFormAdmin`` instance
- Model class
:return:
Form class
Inline model form helper.
"""
def __init__(self, session):
self.session = session
# Get mapper
mapper = model._sa_class_manager.mapper
# Contribute columns
for p in inline_models:
# Figure out settings
if isinstance(p, tuple):
info = InlineFormAdmin(p[0], **p[1])
elif isinstance(p, InlineFormAdmin):
info = p
elif hasattr(p, '_sa_class_manager'):
info = InlineFormAdmin(p)
else:
model = getattr(p, 'model', None)
if model is None:
raise Exception('Unknown inline model admin: %s' % repr(p))
attrs = dict()
for attr in dir(p):
if not attr.startswith('_') and attr != 'model':
attrs[attr] = getattr(p, attr)
info = InlineFormAdmin(model, **attrs)
def contribute(self, converter, model, form_class, inline_model):
"""
Generate form fields for inline forms and contribute them to
the `form_class`
:param converter:
ModelConverterBase instance
:param session:
SQLAlchemy session
:param model:
Model class
:param form_class:
Form to add properties to
:param inline_model:
Inline model. Can be one of:
- ``tuple``, first value is related model instance,
second is dictionary with options
- ``InlineFormAdmin`` instance
- Model class
:return:
Form class
"""
mapper = model._sa_class_manager.mapper
info = self.get_info(inline_model)
# Find property from target model to current model
target_mapper = info.model._sa_class_manager.mapper
......@@ -399,8 +387,7 @@ def contribute_inline(session, model, form_class, inline_models):
else:
exclude = ignore
# Create field
converter = AdminModelConverter(session, info)
# Create form
child_form = get_form(info.model,
converter,
only=info.form_columns,
......@@ -408,11 +395,15 @@ def contribute_inline(session, model, form_class, inline_models):
field_args=info.form_args,
hidden_pk=True)
# Post-process form
child_form = info.postprocess_form(child_form)
# Contribute field
setattr(form_class,
forward_prop.key,
InlineModelFormList(child_form,
session,
self.session,
info.model,
forward_prop.key))
return form_class
return form_class
......@@ -124,6 +124,20 @@ class ModelView(BaseModelView):
model_form_converter = MyModelConverter
"""
inline_model_form_converter = form.InlineModelConverter
"""
Inline model conversion class. If you need some kind of post-processing for inline
forms, you can customize behavior by doing something like this::
class MyInlineModelConverter(AdminModelConverter):
def post_process(self, form_class, info):
form_class.value = wtf.TextField('value')
return form_class
class MyAdminView(ModelView):
inline_model_form_converter = MyInlineModelConverter
"""
filter_converter = filters.FilterConverter()
"""
Field to filter converter.
......@@ -425,8 +439,25 @@ class ModelView(BaseModelView):
field_args=self.form_args)
if self.inline_models:
form_class = form.contribute_inline(self.session, self.model,
form_class, self.inline_models)
form_class = self.scaffold_inline_form_models(form_class)
return form_class
def scaffold_inline_form_models(self, form_class):
"""
Contribute inline models to the form
:param form_class:
Form class
"""
converter = self.model_form_converter(self.session, self)
inline_converter = self.inline_model_form_converter(self.session)
for m in self.inline_models:
form_class = inline_converter.contribute(converter,
self.model,
form_class,
m)
return form_class
......
......@@ -40,6 +40,22 @@ class InlineFormAdmin(object):
for k, v in kwargs.iteritems():
setattr(self, k, v)
def postprocess_form(self, form_class):
"""
Post process form. Use this to contribute fields.
For example::
class MyInlineForm(InlineFormAdmin):
def postprocess_form(self, form):
form.value = wtf.TextField('value')
return form
class MyAdmin(ModelView):
inline_models = (MyInlineForm(ValueModel),)
"""
return form_class
class ModelConverterBase(object):
def __init__(self, converters=None, use_mro=True):
......@@ -80,3 +96,36 @@ class ModelConverterBase(object):
only=None, exclude=None,
field_args=None):
raise NotImplemented()
class InlineModelConverterBase(object):
def get_info(self, p):
"""
Figure out InlineFormAdmin information.
:param p:
Inline model. Can be one of:
- ``tuple``, first value is related model instance,
second is dictionary with options
- ``InlineFormAdmin`` instance
- Model class
"""
if isinstance(p, tuple):
return InlineFormAdmin(p[0], **p[1])
elif isinstance(p, InlineFormAdmin):
return p
elif hasattr(p, '_sa_class_manager'):
return InlineFormAdmin(p)
else:
model = getattr(p, 'model', None)
if model is None:
raise Exception('Unknown inline model admin: %s' % repr(p))
attrs = dict()
for attr in dir(p):
if not attr.startswith('_') and attr != 'model':
attrs[attr] = getattr(p, attr)
return InlineFormAdmin(model, **attrs)
from flask import Flask
from flask.ext.admin import Admin
import peewee
def setup():
app = Flask(__name__)
app.config['SECRET_KEY'] = '1'
app.config['CSRF_ENABLED'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///'
db = peewee.SqliteDatabase(':memory:')
admin = Admin(app)
return app, db, admin
from nose.tools import eq_, ok_
import peewee
from flask.ext import wtf
from flask.ext.admin.contrib.peeweemodel import ModelView
from . import setup
class CustomModelView(ModelView):
def __init__(self, model,
name=None, category=None, endpoint=None, url=None,
**kwargs):
for k, v in kwargs.iteritems():
setattr(self, k, v)
super(CustomModelView, self).__init__(model,
name, category,
endpoint, url)
def create_models(db):
class BaseModel(peewee.Model):
class Meta:
database = db
class Model1(BaseModel):
def __init__(self, test1=None, test2=None, test3=None, test4=None):
super(Model1, self).__init__()
self.test1 = test1
self.test2 = test2
self.test3 = test3
self.test4 = test4
test1 = peewee.CharField(max_length=20)
test2 = peewee.CharField(max_length=20)
test3 = peewee.TextField(null=True)
test4 = peewee.TextField(null=True)
class Model2(BaseModel):
int_field = peewee.IntegerField()
bool_field = peewee.BooleanField()
Model1.create_table()
Model2.create_table()
return Model1, Model2
def test_model():
app, db, admin = setup()
Model1, Model2 = create_models(db)
view = CustomModelView(Model1)
admin.add_view(view)
eq_(view.model, Model1)
eq_(view.name, 'Model1')
eq_(view.endpoint, 'model1view')
eq_(view._primary_key, 'id')
ok_('test1' in view._sortable_columns)
ok_('test2' in view._sortable_columns)
ok_('test3' in view._sortable_columns)
ok_('test4' in view._sortable_columns)
ok_(view._create_form_class is not None)
ok_(view._edit_form_class is not None)
eq_(view._search_supported, False)
eq_(view._filters, None)
# Verify form
eq_(view._create_form_class.test1.field_class, wtf.TextField)
eq_(view._create_form_class.test2.field_class, wtf.TextField)
eq_(view._create_form_class.test3.field_class, wtf.TextAreaField)
eq_(view._create_form_class.test4.field_class, wtf.TextAreaField)
# Make some test clients
client = app.test_client()
rv = client.get('/admin/model1view/')
eq_(rv.status_code, 200)
rv = client.get('/admin/model1view/new/')
eq_(rv.status_code, 200)
rv = client.post('/admin/model1view/new/',
data=dict(test1='test1large', test2='test2'))
eq_(rv.status_code, 302)
model = Model1.select().get()
eq_(model.test1, 'test1large')
eq_(model.test2, 'test2')
eq_(model.test3, None)
eq_(model.test4, None)
rv = client.get('/admin/model1view/')
eq_(rv.status_code, 200)
ok_('test1large' in rv.data)
url = '/admin/model1view/edit/?id=%s' % model.id
rv = client.get(url)
eq_(rv.status_code, 200)
rv = client.post(url,
data=dict(test1='test1small', test2='test2large'))
eq_(rv.status_code, 302)
model = Model1.select().get()
eq_(model.test1, 'test1small')
eq_(model.test2, 'test2large')
eq_(model.test3, None)
eq_(model.test4, None)
url = '/admin/model1view/delete/?id=%s' % model.id
rv = client.post(url)
eq_(rv.status_code, 302)
eq_(Model1.select().count(), 0)
......@@ -5,6 +5,7 @@ from flask.ext.admin.contrib.sqlamodel import ModelView
from . import setup
class CustomModelView(ModelView):
def __init__(self, model, session,
name=None, category=None, endpoint=None, url=None,
......@@ -16,6 +17,7 @@ class CustomModelView(ModelView):
name, category,
endpoint, url)
def create_models(db):
class Model1(db.Model):
def __init__(self, test1=None, test2=None, test3=None, test4=None):
......
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