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.base import BaseDocument, DocumentMetaclass
from wtforms import fields, validators
from flask.ext.mongoengine.wtf import orm, fields as mongo_fields
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.widgets import InlineFormWidget
from flask.ext.admin._compat import iteritems
from .fields import ModelFormField
......@@ -32,6 +37,10 @@ class CustomModelConverter(orm.ModelConverter):
return None
def convert(self, model, field, field_args):
# Check if it is overridden field
if isinstance(field, FieldPlaceholder):
return form.recreate_field(field.field)
kwargs = {
'label': getattr(field, 'verbose_name', field.name),
'description': field.help_text or '',
......@@ -86,6 +95,7 @@ class CustomModelConverter(orm.ModelConverter):
doc_type = field.field.document_type
return mongo_fields.ModelSelectMultipleField(model=doc_type, **kwargs)
if field.field.choices:
kwargs['multiple'] = True
return self.convert(model, field.field, kwargs)
......@@ -105,7 +115,8 @@ class CustomModelConverter(orm.ModelConverter):
'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)
@orm.converts('ReferenceField')
......@@ -114,8 +125,72 @@ class CustomModelConverter(orm.ModelConverter):
return orm.ModelConverter.conv_Reference(self, model, field, kwargs)
def model_form(model, base_class=form.BaseForm, only=None, exclude=None,
field_args=None, converter=None):
return orm.model_form(model, base_class=base_class, only=only,
exclude=exclude, field_args=field_args,
converter=converter)
def get_form(model, converter,
base_class=form.BaseForm,
only=None,
exclude=None,
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
from flask.ext.admin.actions import action
from flask.ext.admin.form import BaseForm
from .filters import FilterConverter, BaseMongoEngineFilter
from .form import model_form, CustomModelConverter
from .form import get_form, CustomModelConverter
from .typefmt import DEFAULT_FORMATTERS
from .tools import parse_like_term
......@@ -234,13 +234,16 @@ class ModelView(BaseModelView):
return isinstance(filter, BaseMongoEngineFilter)
def scaffold_form(self):
# TODO: Fix base_class
form_class = model_form(self.model,
"""
Create form from the model.
"""
form_class = get_form(self.model,
self.model_form_converter(self),
base_class=BaseForm,
only=self.form_columns,
exclude=self.form_excluded_columns,
field_args=self.form_args,
converter=self.model_form_converter(self))
extra_fields=self.form_extra_fields)
return form_class
......
......@@ -6,7 +6,7 @@ from peewee import (DateTimeField, DateField, TimeField,
from wtfpeewee.orm import ModelConverter, model_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.fields import InlineModelFormField, InlineFieldList
......@@ -103,6 +103,31 @@ class CustomModelConverter(ModelConverter):
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):
"""
Inline model form helper.
......
......@@ -8,11 +8,10 @@ from flask.ext.admin.babel import gettext, ngettext, lazy_gettext
from flask.ext.admin.model import BaseModelView
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.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
......@@ -218,12 +217,12 @@ class ModelView(BaseModelView):
return isinstance(filter, filters.BasePeeweeFilter)
def scaffold_form(self):
form_class = model_form(self.model,
form_class = get_form(self.model, self.model_form_converter(),
base_class=form.BaseForm,
only=self.form_columns,
exclude=self.form_excluded_columns,
field_args=self.form_args,
converter=self.model_form_converter())
extra_fields=self.form_extra_fields)
if self.inline_models:
form_class = self.scaffold_inline_form_models(form_class)
......
......@@ -4,8 +4,10 @@ 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,
InlineFormAdmin, InlineModelConverterBase)
InlineFormAdmin, InlineModelConverterBase,
FieldPlaceholder)
from flask.ext.admin._backwards import get_property
from flask.ext.admin._compat import iteritems
from .validators import Unique
from .fields import QuerySelectField, QuerySelectMultipleField, InlineModelFormList
......@@ -60,6 +62,10 @@ class AdminModelConverter(ModelConverterBase):
return None
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 = {
'validators': [],
'filters': []
......@@ -309,7 +315,8 @@ def get_form(model, converter,
exclude=None,
field_args=None,
hidden_pk=False,
ignore_hidden=True):
ignore_hidden=True,
extra_fields=None):
"""
Generate form from the model.
......@@ -344,6 +351,10 @@ def get_form(model, converter,
props = dict(properties)
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
p = props.get(name)
......@@ -374,6 +385,11 @@ def get_form(model, converter,
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)
return type(model.__name__ + 'Form', (base_class, ), field_dict)
......
......@@ -515,7 +515,8 @@ class ModelView(BaseModelView):
form_class = form.get_form(self.model, converter,
only=self.form_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:
form_class = self.scaffold_inline_form_models(form_class)
......
......@@ -2,6 +2,7 @@ import time
import datetime
from wtforms import form, fields, widgets
from wtforms.fields.core import UnboundField
from flask.globals import _request_ctx_stack
from flask.ext.admin.babel import gettext, ngettext
from flask.ext.admin import helpers as h
......@@ -214,3 +215,16 @@ class Select2TagsField(fields.TextField):
def _value(self):
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
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._backwards import ObsoleteAttr
from flask.ext.admin._compat import as_unicode
from flask.ext.admin._compat import iteritems, as_unicode
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
action_disallowed_list = ObsoleteAttr('action_disallowed_list',
'disallowed_actions',
......@@ -876,7 +899,7 @@ class BaseModelView(BaseView, ActionsMixin):
"""
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
def get_list_value(self, context, model, name):
......
......@@ -165,3 +165,11 @@ class InlineModelConverterBase(object):
return p
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():
eq_(data[0].test1, 'a')
eq_(data[1].test1, 'b')
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():
eq_(data[0].test1, 'a')
eq_(data[1].test1, 'b')
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():
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
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