Commit a214377c authored by Serge S. Koval's avatar Serge S. Koval

Added form_extra_columns. Fixed #256

parent 0b7107fb
from operator import itemgetter
from mongoengine import ReferenceField from mongoengine import ReferenceField
from mongoengine.base import BaseDocument, DocumentMetaclass
from wtforms import fields, validators from wtforms import fields, validators
from flask.ext.mongoengine.wtf import orm, fields as mongo_fields from flask.ext.mongoengine.wtf import orm, fields as mongo_fields
from flask.ext.admin import form from flask.ext.admin import form
from flask.ext.admin.model.form import FieldPlaceholder
from flask.ext.admin.model.fields import InlineFieldList from flask.ext.admin.model.fields import InlineFieldList
from flask.ext.admin.model.widgets import InlineFormWidget from flask.ext.admin.model.widgets import InlineFormWidget
from flask.ext.admin._compat import iteritems
from .fields import ModelFormField from .fields import ModelFormField
...@@ -32,6 +37,10 @@ class CustomModelConverter(orm.ModelConverter): ...@@ -32,6 +37,10 @@ class CustomModelConverter(orm.ModelConverter):
return None return None
def convert(self, model, field, field_args): def convert(self, model, field, field_args):
# Check if it is overridden field
if isinstance(field, FieldPlaceholder):
return form.recreate_field(field.field)
kwargs = { kwargs = {
'label': getattr(field, 'verbose_name', field.name), 'label': getattr(field, 'verbose_name', field.name),
'description': field.help_text or '', 'description': field.help_text or '',
...@@ -86,6 +95,7 @@ class CustomModelConverter(orm.ModelConverter): ...@@ -86,6 +95,7 @@ class CustomModelConverter(orm.ModelConverter):
doc_type = field.field.document_type doc_type = field.field.document_type
return mongo_fields.ModelSelectMultipleField(model=doc_type, **kwargs) return mongo_fields.ModelSelectMultipleField(model=doc_type, **kwargs)
if field.field.choices: if field.field.choices:
kwargs['multiple'] = True kwargs['multiple'] = True
return self.convert(model, field.field, kwargs) return self.convert(model, field.field, kwargs)
...@@ -105,7 +115,8 @@ class CustomModelConverter(orm.ModelConverter): ...@@ -105,7 +115,8 @@ class CustomModelConverter(orm.ModelConverter):
'widget': InlineFormWidget() 'widget': InlineFormWidget()
} }
form_class = model_form(field.document_type_obj, field_args={}) # TODO: Configurable params?
form_class = get_form(field.document_type_obj, self, field_args={})
return ModelFormField(field.document_type_obj, form_class, **kwargs) return ModelFormField(field.document_type_obj, form_class, **kwargs)
@orm.converts('ReferenceField') @orm.converts('ReferenceField')
...@@ -114,8 +125,72 @@ class CustomModelConverter(orm.ModelConverter): ...@@ -114,8 +125,72 @@ class CustomModelConverter(orm.ModelConverter):
return orm.ModelConverter.conv_Reference(self, model, field, kwargs) return orm.ModelConverter.conv_Reference(self, model, field, kwargs)
def model_form(model, base_class=form.BaseForm, only=None, exclude=None, def get_form(model, converter,
field_args=None, converter=None): base_class=form.BaseForm,
return orm.model_form(model, base_class=base_class, only=only, only=None,
exclude=exclude, field_args=field_args, exclude=None,
converter=converter) field_args=None,
extra_fields=None):
"""
Create a wtforms Form for a given mongoengine Document schema::
from flask.ext.mongoengine.wtf import model_form
from myproject.myapp.schemas import Article
ArticleForm = model_form(Article)
:param model:
A mongoengine Document schema class
:param base_class:
Base form class to extend from. Must be a ``wtforms.Form`` subclass.
:param only:
An optional iterable with the property names that should be included in
the form. Only these properties will have fields.
:param exclude:
An optional iterable with the property names that should be excluded
from the form. All other properties will have fields.
:param field_args:
An optional dictionary of field names mapping to keyword arguments used
to construct each field object.
:param converter:
A converter to generate the fields based on the model properties. If
not set, ``ModelConverter`` is used.
"""
if not isinstance(model, (BaseDocument, DocumentMetaclass)):
raise TypeError('Model must be a mongoengine Document schema')
field_args = field_args or {}
# Find properties
properties = ((k, v) for k, v in iteritems(model._fields))
if only:
props = dict(properties)
def find(name):
if extra_fields and name in extra_fields:
return FieldPlaceholder(extra_fields[name])
p = props.get(name)
if p is not None:
return p
raise ValueError('Invalid model property name %s.%s' % (model, name))
properties = ((p, find(p)) for p in only)
elif exclude:
properties = (p for p in properties in p[0] not in exclude)
# Create fields
field_dict = {}
for name, p in properties:
field = converter.convert(model, p, field_args.get(name))
if field is not None:
field_dict[name] = field
# Contribute extra fields
if not only and extra_fields:
for name, field in iteritems(extra_fields):
field_dict[name] = form.recreate_field(field)
field_dict['model_class'] = model
return type(model.__name__ + 'Form', (base_class,), field_dict)
...@@ -12,7 +12,7 @@ from bson.objectid import ObjectId ...@@ -12,7 +12,7 @@ from bson.objectid import ObjectId
from flask.ext.admin.actions import action from flask.ext.admin.actions import action
from flask.ext.admin.form import BaseForm from flask.ext.admin.form import BaseForm
from .filters import FilterConverter, BaseMongoEngineFilter from .filters import FilterConverter, BaseMongoEngineFilter
from .form import model_form, CustomModelConverter from .form import get_form, CustomModelConverter
from .typefmt import DEFAULT_FORMATTERS from .typefmt import DEFAULT_FORMATTERS
from .tools import parse_like_term from .tools import parse_like_term
...@@ -234,13 +234,16 @@ class ModelView(BaseModelView): ...@@ -234,13 +234,16 @@ class ModelView(BaseModelView):
return isinstance(filter, BaseMongoEngineFilter) return isinstance(filter, BaseMongoEngineFilter)
def scaffold_form(self): def scaffold_form(self):
# TODO: Fix base_class """
form_class = model_form(self.model, Create form from the model.
base_class=BaseForm, """
only=self.form_columns, form_class = get_form(self.model,
exclude=self.form_excluded_columns, self.model_form_converter(self),
field_args=self.form_args, base_class=BaseForm,
converter=self.model_form_converter(self)) only=self.form_columns,
exclude=self.form_excluded_columns,
field_args=self.form_args,
extra_fields=self.form_extra_fields)
return form_class return form_class
......
...@@ -6,7 +6,7 @@ from peewee import (DateTimeField, DateField, TimeField, ...@@ -6,7 +6,7 @@ from peewee import (DateTimeField, DateField, TimeField,
from wtfpeewee.orm import ModelConverter, model_form from wtfpeewee.orm import ModelConverter, model_form
from flask.ext.admin import form from flask.ext.admin import form
from flask.ext.admin._compat import itervalues from flask.ext.admin._compat import iteritems, itervalues
from flask.ext.admin.model.form import InlineFormAdmin, InlineModelConverterBase from flask.ext.admin.model.form import InlineFormAdmin, InlineModelConverterBase
from flask.ext.admin.model.fields import InlineModelFormField, InlineFieldList from flask.ext.admin.model.fields import InlineModelFormField, InlineFieldList
...@@ -103,6 +103,31 @@ class CustomModelConverter(ModelConverter): ...@@ -103,6 +103,31 @@ class CustomModelConverter(ModelConverter):
return field.name, form.TimeField(**kwargs) return field.name, form.TimeField(**kwargs)
def get_form(model, converter,
base_class=form.BaseForm,
only=None,
exclude=None,
field_args=None,
allow_pk=True,
extra_fields=None):
"""
Create form from peewee model and contribute extra fields, if necessary
"""
result = model_form(model,
base_class=base_class,
only=only,
exclude=exclude,
field_args=field_args,
allow_pk=allow_pk,
converter=converter)
if extra_fields:
for name, field in iteritems(extra_fields):
setattr(result, name, form.recreate_field(field))
return result
class InlineModelConverter(InlineModelConverterBase): class InlineModelConverter(InlineModelConverterBase):
""" """
Inline model form helper. Inline model form helper.
......
...@@ -8,11 +8,10 @@ from flask.ext.admin.babel import gettext, ngettext, lazy_gettext ...@@ -8,11 +8,10 @@ from flask.ext.admin.babel import gettext, ngettext, lazy_gettext
from flask.ext.admin.model import BaseModelView from flask.ext.admin.model import BaseModelView
from peewee import PrimaryKeyField, ForeignKeyField, Field, CharField, TextField from peewee import PrimaryKeyField, ForeignKeyField, Field, CharField, TextField
from wtfpeewee.orm import model_form
from flask.ext.admin.actions import action from flask.ext.admin.actions import action
from flask.ext.admin.contrib.peewee import filters from flask.ext.admin.contrib.peewee import filters
from .form import CustomModelConverter, InlineModelConverter, save_inline from .form import get_form, CustomModelConverter, InlineModelConverter, save_inline
from .tools import get_primary_key, parse_like_term from .tools import get_primary_key, parse_like_term
...@@ -218,12 +217,12 @@ class ModelView(BaseModelView): ...@@ -218,12 +217,12 @@ class ModelView(BaseModelView):
return isinstance(filter, filters.BasePeeweeFilter) return isinstance(filter, filters.BasePeeweeFilter)
def scaffold_form(self): def scaffold_form(self):
form_class = model_form(self.model, form_class = get_form(self.model, self.model_form_converter(),
base_class=form.BaseForm, base_class=form.BaseForm,
only=self.form_columns, only=self.form_columns,
exclude=self.form_excluded_columns, exclude=self.form_excluded_columns,
field_args=self.form_args, field_args=self.form_args,
converter=self.model_form_converter()) extra_fields=self.form_extra_fields)
if self.inline_models: if self.inline_models:
form_class = self.scaffold_inline_form_models(form_class) form_class = self.scaffold_inline_form_models(form_class)
......
...@@ -4,8 +4,10 @@ from sqlalchemy import Boolean, Column ...@@ -4,8 +4,10 @@ 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.form import Select2Field
from flask.ext.admin.model.form import (converts, ModelConverterBase, from flask.ext.admin.model.form import (converts, ModelConverterBase,
InlineFormAdmin, InlineModelConverterBase) InlineFormAdmin, InlineModelConverterBase,
FieldPlaceholder)
from flask.ext.admin._backwards import get_property from flask.ext.admin._backwards import get_property
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
...@@ -60,6 +62,10 @@ class AdminModelConverter(ModelConverterBase): ...@@ -60,6 +62,10 @@ class AdminModelConverter(ModelConverterBase):
return None return None
def convert(self, model, mapper, prop, field_args, hidden_pk): def convert(self, model, mapper, prop, field_args, hidden_pk):
# Properly handle forced fields
if isinstance(prop, FieldPlaceholder):
return form.recreate_field(prop.field)
kwargs = { kwargs = {
'validators': [], 'validators': [],
'filters': [] 'filters': []
...@@ -309,7 +315,8 @@ def get_form(model, converter, ...@@ -309,7 +315,8 @@ def get_form(model, converter,
exclude=None, exclude=None,
field_args=None, field_args=None,
hidden_pk=False, hidden_pk=False,
ignore_hidden=True): ignore_hidden=True,
extra_fields=None):
""" """
Generate form from the model. Generate form from the model.
...@@ -344,6 +351,10 @@ def get_form(model, converter, ...@@ -344,6 +351,10 @@ def get_form(model, converter,
props = dict(properties) props = dict(properties)
def find(name): def find(name):
# If field is in extra_fields, it has higher priority
if extra_fields and name in extra_fields:
return FieldPlaceholder(extra_fields[name])
# Try to look it up in properties list first # Try to look it up in properties list first
p = props.get(name) p = props.get(name)
...@@ -374,6 +385,11 @@ def get_form(model, converter, ...@@ -374,6 +385,11 @@ def get_form(model, converter,
if field is not None: if field is not None:
field_dict[name] = field field_dict[name] = field
# Contribute extra fields
if not only and extra_fields:
for name, field in iteritems(extra_fields):
field_dict[name] = form.recreate_field(field)
return type(model.__name__ + 'Form', (base_class, ), field_dict) return type(model.__name__ + 'Form', (base_class, ), field_dict)
......
...@@ -515,7 +515,8 @@ class ModelView(BaseModelView): ...@@ -515,7 +515,8 @@ class ModelView(BaseModelView):
form_class = form.get_form(self.model, converter, form_class = form.get_form(self.model, converter,
only=self.form_columns, only=self.form_columns,
exclude=self.form_excluded_columns, exclude=self.form_excluded_columns,
field_args=self.form_args) field_args=self.form_args,
extra_fields=self.form_extra_fields)
if self.inline_models: if self.inline_models:
form_class = self.scaffold_inline_form_models(form_class) form_class = self.scaffold_inline_form_models(form_class)
......
...@@ -2,6 +2,7 @@ import time ...@@ -2,6 +2,7 @@ import time
import datetime import datetime
from wtforms import form, fields, widgets from wtforms import form, fields, widgets
from wtforms.fields.core import UnboundField
from flask.globals import _request_ctx_stack 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
...@@ -214,3 +215,16 @@ class Select2TagsField(fields.TextField): ...@@ -214,3 +215,16 @@ class Select2TagsField(fields.TextField):
def _value(self): def _value(self):
return u', '.join(self.data) if isinstance(self.data, list) else self.data return u', '.join(self.data) if isinstance(self.data, list) else self.data
def recreate_field(unbound):
"""
Create new instance of the unbound field, resetting wtforms creation counter.
:param unbound:
UnboundField instance
"""
if not isinstance(unbound, UnboundField):
raise ValueError('recreate_field expects UnboundField instance, %s was passed.' % type(unbound))
return unbound.field_class(*unbound.args, **unbound.kwargs)
...@@ -12,7 +12,7 @@ from flask.ext.admin.actions import ActionsMixin ...@@ -12,7 +12,7 @@ from flask.ext.admin.actions import ActionsMixin
from flask.ext.admin.helpers import get_form_data, validate_form_on_submit from flask.ext.admin.helpers import get_form_data, validate_form_on_submit
from flask.ext.admin.tools import rec_getattr from flask.ext.admin.tools import rec_getattr
from flask.ext.admin._backwards import ObsoleteAttr from flask.ext.admin._backwards import ObsoleteAttr
from flask.ext.admin._compat import as_unicode from flask.ext.admin._compat import iteritems, as_unicode
class BaseModelView(BaseView, ActionsMixin): class BaseModelView(BaseView, ActionsMixin):
...@@ -318,6 +318,29 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -318,6 +318,29 @@ class BaseModelView(BaseView, ActionsMixin):
} }
""" """
form_extra_fields = None
"""
Dictionary of additional fields.
Example::
class MyModelView(BaseModelView):
form_extra_fields = {
password: PasswordField('Password')
}
You can control order of form fields using ``form_columns`` property. For example::
class MyModelView(BaseModelView):
form_columns = ('name', 'email', 'password', 'secret')
form_extra_fields = {
password: PasswordField('Password')
}
In this case, password field will be put between email and secret fields that are autogenerated.
"""
# Actions # Actions
action_disallowed_list = ObsoleteAttr('action_disallowed_list', action_disallowed_list = ObsoleteAttr('action_disallowed_list',
'disallowed_actions', 'disallowed_actions',
...@@ -876,7 +899,7 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -876,7 +899,7 @@ class BaseModelView(BaseView, ActionsMixin):
""" """
Return flattened filter dictionary which can be JSON-serialized. Return flattened filter dictionary which can be JSON-serialized.
""" """
return dict((as_unicode(k), v) for k, v in self._filter_dict.iteritems()) return dict((as_unicode(k), v) for k, v in iteritems(self._filter_dict))
@contextfunction @contextfunction
def get_list_value(self, context, model, name): def get_list_value(self, context, model, name):
......
...@@ -165,3 +165,11 @@ class InlineModelConverterBase(object): ...@@ -165,3 +165,11 @@ class InlineModelConverterBase(object):
return p return p
return None return None
class FieldPlaceholder(object):
"""
Field placeholder for model convertors.
"""
def __init__(self, field):
self.field = field
...@@ -139,3 +139,55 @@ def test_default_sort(): ...@@ -139,3 +139,55 @@ def test_default_sort():
eq_(data[0].test1, 'a') eq_(data[0].test1, 'a')
eq_(data[1].test1, 'b') eq_(data[1].test1, 'b')
eq_(data[2].test1, 'c') eq_(data[2].test1, 'c')
def test_extra_fields():
app, db, admin = setup()
Model1, _ = create_models(db)
view = CustomModelView(
Model1,
form_extra_fields={
'extra_field': fields.TextField('Extra Field')
}
)
admin.add_view(view)
client = app.test_client()
rv = client.get('/admin/model1view/new/')
eq_(rv.status_code, 200)
# Check presence and order
data = rv.data.decode('utf-8')
ok_('Extra Field' in data)
pos1 = data.find('Extra Field')
pos2 = data.find('Test1')
ok_(pos2 < pos1)
def test_extra_field_order():
app, db, admin = setup()
Model1, _ = create_models(db)
view = CustomModelView(
Model1,
form_columns=('extra_field', 'test1'),
form_extra_fields={
'extra_field': fields.TextField('Extra Field')
}
)
admin.add_view(view)
client = app.test_client()
rv = client.get('/admin/model1view/new/')
eq_(rv.status_code, 200)
# Check presence and order
data = rv.data.decode('utf-8')
pos1 = data.find('Extra Field')
pos2 = data.find('Test1')
ok_(pos2 > pos1)
...@@ -147,3 +147,29 @@ def test_default_sort(): ...@@ -147,3 +147,29 @@ def test_default_sort():
eq_(data[0].test1, 'a') eq_(data[0].test1, 'a')
eq_(data[1].test1, 'b') eq_(data[1].test1, 'b')
eq_(data[2].test1, 'c') eq_(data[2].test1, 'c')
def test_extra_fields():
app, db, admin = setup()
Model1, _ = create_models(db)
view = CustomModelView(
Model1,
form_extra_fields={
'extra_field': fields.TextField('Extra Field')
}
)
admin.add_view(view)
client = app.test_client()
rv = client.get('/admin/model1view/new/')
eq_(rv.status_code, 200)
# Check presence and order
data = rv.data.decode('utf-8')
ok_('Extra Field' in data)
pos1 = data.find('Extra Field')
pos2 = data.find('Test1')
ok_(pos2 < pos1)
...@@ -567,4 +567,55 @@ def test_default_sort(): ...@@ -567,4 +567,55 @@ def test_default_sort():
eq_(data[2].test1, 'c') eq_(data[2].test1, 'c')
def test_extra_fields():
app, db, admin = setup()
Model1, _ = create_models(db)
view = CustomModelView(
Model1, db.session,
form_extra_fields={
'extra_field': fields.TextField('Extra Field')
}
)
admin.add_view(view)
client = app.test_client()
rv = client.get('/admin/model1view/new/')
eq_(rv.status_code, 200)
# Check presence and order
data = rv.data.decode('utf-8')
ok_('Extra Field' in data)
pos1 = data.find('Extra Field')
pos2 = data.find('Test1')
ok_(pos2 < pos1)
def test_extra_field_order():
app, db, admin = setup()
Model1, _ = create_models(db)
view = CustomModelView(
Model1, db.session,
form_columns=('extra_field', 'test1'),
form_extra_fields={
'extra_field': fields.TextField('Extra Field')
}
)
admin.add_view(view)
client = app.test_client()
rv = client.get('/admin/model1view/new/')
eq_(rv.status_code, 200)
# Check presence and order
data = rv.data.decode('utf-8')
pos1 = data.find('Extra Field')
pos2 = data.find('Test1')
ok_(pos2 > pos1)
# TODO: Babel tests # TODO: Babel tests
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