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

Refactored inline models form/widget integration

parent 3e2be5a2
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
- Create documentation - Create documentation
- Model Admin - Model Admin
- Simplify scaffold_list_columns - remove excluded_list_columns check, update documentation - Simplify scaffold_list_columns - remove excluded_list_columns check, update documentation
- Simplify InlineModelFormList implementation - accept property instead of property name
- Reduce number of parameters passed to list view - Reduce number of parameters passed to list view
- Filters - Filters
- Use table to draw filters so column names will line up? - Use table to draw filters so column names will line up?
......
...@@ -20,7 +20,7 @@ db.init_app(app) ...@@ -20,7 +20,7 @@ db.init_app(app)
class User(db.Document): class User(db.Document):
name = db.StringField(max_length=40) name = db.StringField(max_length=40)
tag = db.StringField(max_length=20) tags = db.ListField(db.ReferenceField('Tag'))
password = db.StringField(max_length=40) password = db.StringField(max_length=40)
def __unicode__(self): def __unicode__(self):
...@@ -39,10 +39,27 @@ class Todo(db.Document): ...@@ -39,10 +39,27 @@ class Todo(db.Document):
return self.title return self.title
class Tag(db.Document):
name = db.StringField(max_length=10)
def __unicode__(self):
return self.name
class Comment(db.EmbeddedDocument):
name = db.StringField(max_length=20)
value = db.StringField(max_length=20)
class Post(db.Document):
name = db.StringField(max_length=20)
value = db.StringField(max_length=20)
inner = db.ListField(db.EmbeddedDocumentField(Comment))
# Customized admin views # Customized admin views
class UserView(ModelView): class UserView(ModelView):
column_filters = ('name', column_filters = ['name']
User.tag)
# Flask views # Flask views
...@@ -55,9 +72,15 @@ if __name__ == '__main__': ...@@ -55,9 +72,15 @@ if __name__ == '__main__':
# Create admin # Create admin
admin = admin.Admin(app, 'Simple Models') admin = admin.Admin(app, 'Simple Models')
#p = Post.objects[0]
#p.inner.append(Comment(name='12345'))
#p.save()
# Add views # Add views
admin.add_view(UserView(User)) admin.add_view(UserView(User))
admin.add_view(ModelView(Todo)) admin.add_view(ModelView(Todo))
admin.add_view(ModelView(Tag))
admin.add_view(ModelView(Post))
# Start app # Start app
app.debug = True app.debug = True
......
...@@ -2,6 +2,8 @@ from flask.ext.admin.babel import gettext ...@@ -2,6 +2,8 @@ from flask.ext.admin.babel import gettext
from flask.ext.admin.model import filters from flask.ext.admin.model import filters
from .tools import parse_like_term
class BaseMongoEngineFilter(filters.BaseFilter): class BaseMongoEngineFilter(filters.BaseFilter):
""" """
......
from flask.ext.mongoengine.wtf import orm from mongoengine import ReferenceField, EmbeddedDocumentField
from flask.ext.admin.form import BaseForm from flask.ext.mongoengine.wtf import orm, fields
from flask.ext.admin import form
from flask.ext.admin.model.fields import InlineFieldList
class CustomModelConverter(orm.ModelConverter): class CustomModelConverter(orm.ModelConverter):
pass @orm.converts('DateTimeField')
def conv_DateTime(self, model, field, kwargs):
kwargs['widget'] = form.DateTimePickerWidget()
return orm.ModelConverter.conv_DateTime(self, model, field, kwargs)
@orm.converts('ListField')
def conv_List(self, model, field, kwargs):
if isinstance(field.field, ReferenceField):
kwargs['widget'] = form.Select2Widget(multiple=True)
doc_type = field.field.document_type
return fields.ModelSelectMultipleField(model=doc_type, **kwargs)
if field.field.choices:
kwargs['multiple'] = True
return self.convert(model, field.field, kwargs)
unbound_field = self.convert(model, field.field, {})
kwargs = {
'validators': [],
'filters': [],
}
return InlineFieldList(unbound_field, min_entries=0, **kwargs)
@orm.converts('ReferenceField')
def conv_Reference(self, model, field, kwargs):
kwargs['widget'] = form.Select2Widget()
return orm.ModelConverter.conv_Reference(self, model, field, kwargs)
def model_form(model, base_class=BaseForm, only=None, exclude=None, def model_form(model, base_class=form.BaseForm, only=None, exclude=None,
field_args=None, converter=None): field_args=None, converter=None):
return orm.model_form(model, base_class=base_class, only=only, return orm.model_form(model, base_class=base_class, only=only,
exclude=exclude, field_args=field_args, exclude=exclude, field_args=field_args,
......
...@@ -12,6 +12,7 @@ from flask.ext.admin.actions import action ...@@ -12,6 +12,7 @@ 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 model_form, CustomModelConverter
from .typefmt import MONGOENGINE_FORMATTERS
SORTABLE_FIELDS = set(( SORTABLE_FIELDS = set((
...@@ -82,6 +83,11 @@ class ModelView(BaseModelView): ...@@ -82,6 +83,11 @@ class ModelView(BaseModelView):
for your model. for your model.
""" """
list_type_formatters = MONGOENGINE_FORMATTERS
"""
Customized list formatters for MongoEngine
"""
def __init__(self, model, name=None, def __init__(self, model, name=None,
category=None, endpoint=None, url=None): category=None, endpoint=None, url=None):
self._search_fields = [] self._search_fields = []
...@@ -117,7 +123,12 @@ class ModelView(BaseModelView): ...@@ -117,7 +123,12 @@ class ModelView(BaseModelView):
# Verify type # Verify type
field_class = type(f) field_class = type(f)
if self.list_display_pk or field_class != mongoengine.ObjectIdField: if (field_class == mongoengine.ListField and
isinstance(f.field, mongoengine.EmbeddedDocumentField)):
continue
if field_class == mongoengine.EmbeddedDocumentField:
continue
elif self.list_display_pk or field_class != mongoengine.ObjectIdField:
columns.append(n) columns.append(n)
return columns return columns
......
...@@ -8,9 +8,10 @@ from wtforms.fields import SelectFieldBase, FieldList ...@@ -8,9 +8,10 @@ from wtforms.fields import SelectFieldBase, FieldList
from wtforms.validators import ValidationError from wtforms.validators import ValidationError
from .tools import get_primary_key from .tools import get_primary_key
from flask.ext.admin.model.fields import InlineModelFormField from flask.ext.admin.model.fields import InlineFieldList, InlineModelFormField
from flask.ext.admin.model.widgets import InlineFormListWidget from flask.ext.admin.model.widgets import InlineFormListWidget
try: try:
from sqlalchemy.orm.util import identity_key from sqlalchemy.orm.util import identity_key
has_identity_key = True has_identity_key = True
...@@ -180,12 +181,11 @@ class QuerySelectMultipleField(QuerySelectField): ...@@ -180,12 +181,11 @@ class QuerySelectMultipleField(QuerySelectField):
raise ValidationError(self.gettext('Not a valid choice')) raise ValidationError(self.gettext('Not a valid choice'))
class InlineModelFormList(FieldList): class InlineModelFormList(InlineFieldList):
""" """
Customizied ``wtforms.fields.FieldList`` class which will work with SQLAlchemy Customised ``wtforms.fields.FieldList`` class which will work with SQLAlchemy
model instances. model instances.
""" """
widget = InlineFormListWidget() widget = InlineFormListWidget()
def __init__(self, form, session, model, prop, **kwargs): def __init__(self, form, session, model, prop, **kwargs):
...@@ -210,8 +210,8 @@ class InlineModelFormList(FieldList): ...@@ -210,8 +210,8 @@ class InlineModelFormList(FieldList):
super(InlineModelFormList, self).__init__(InlineModelFormField(form, self._pk), **kwargs) super(InlineModelFormList, self).__init__(InlineModelFormField(form, self._pk), **kwargs)
def __call__(self, **kwargs): def display_row_controls(self, field):
return self.widget(self, template=self.form(), **kwargs) return field.get_pk() is not None
def populate_obj(self, obj, name): def populate_obj(self, obj, name):
values = getattr(obj, name, None) values = getattr(obj, name, None)
...@@ -229,7 +229,7 @@ class InlineModelFormList(FieldList): ...@@ -229,7 +229,7 @@ class InlineModelFormList(FieldList):
if field_id in pk_map: if field_id in pk_map:
model = pk_map[field_id] model = pk_map[field_id]
if field.should_delete(): if self.should_delete(field):
self.session.delete(model) self.session.delete(model)
continue continue
else: else:
...@@ -239,7 +239,7 @@ class InlineModelFormList(FieldList): ...@@ -239,7 +239,7 @@ class InlineModelFormList(FieldList):
field.populate_obj(model, None) field.populate_obj(model, None)
# Force relation # Force relation
setattr(self.model, self.prop, obj) setattr(model, self.prop, obj)
def get_pk_from_identity(obj): def get_pk_from_identity(obj):
......
...@@ -148,12 +148,10 @@ class RenderTemplateWidget(object): ...@@ -148,12 +148,10 @@ class RenderTemplateWidget(object):
ctx = _request_ctx_stack.top ctx = _request_ctx_stack.top
jinja_env = ctx.app.jinja_env jinja_env = ctx.app.jinja_env
kwargs['field'] = field kwargs.update({
'field': field,
# Provide i18n support even if flask-babel is not installed '_gettext': gettext,
# or enabled. '_ngettext': ngettext})
kwargs['_gettext'] = gettext
kwargs['_ngettext'] = ngettext
template = jinja_env.get_template(self.template) template = jinja_env.get_template(self.template)
return template.render(kwargs) return template.render(kwargs)
...@@ -167,12 +165,13 @@ class Select2TagsWidget(widgets.TextInput): ...@@ -167,12 +165,13 @@ class Select2TagsWidget(widgets.TextInput):
kwargs['data-role'] = u'select2tags' kwargs['data-role'] = u'select2tags'
return super(Select2TagsWidget, self).__call__(field, **kwargs) return super(Select2TagsWidget, self).__call__(field, **kwargs)
class Select2TagsField(fields.TextField): class Select2TagsField(fields.TextField):
"""`Select2 <http://ivaynberg.github.com/select2/#tags>`_ styled text field. """`Select2 <http://ivaynberg.github.com/select2/#tags>`_ styled text field.
You must include select2.js, form.js and select2 stylesheet for it to work. You must include select2.js, form.js and select2 stylesheet for it to work.
""" """
widget = Select2TagsWidget() widget = Select2TagsWidget()
def __init__(self, label=None, validators=None, save_as_list=False, **kwargs): def __init__(self, label=None, validators=None, save_as_list=False, **kwargs):
"""Initialization """Initialization
...@@ -190,4 +189,3 @@ class Select2TagsField(fields.TextField): ...@@ -190,4 +189,3 @@ 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
...@@ -746,6 +746,8 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -746,6 +746,8 @@ class BaseModelView(BaseView, ActionsMixin):
value = rec_getattr(model, name) value = rec_getattr(model, name)
print name, type(value)
type_fmt = self.list_type_formatters.get(type(value)) type_fmt = self.list_type_formatters.get(type(value))
if type_fmt is not None: if type_fmt is not None:
value = type_fmt(value) value = type_fmt(value)
......
from wtforms.fields import FormField from wtforms.fields import FieldList, FormField
from .widgets import InlineFieldListWidget
class InlineFieldList(FieldList):
widget = InlineFieldListWidget()
def __init__(self, *args, **kwargs):
super(InlineFieldList, self).__init__(*args, **kwargs)
# Create template
self.template = self.unbound_field.bind(form=None, name='', prefix='', separator='')
self.template.process(None)
def __call__(self, **kwargs):
return self.widget(self,
template=self.template,
check=self.display_row_controls,
**kwargs)
def display_row_controls(self, field):
return True
def process(self, formdata, data=None):
res = super(InlineFieldList, self).process(formdata, data)
# Postprocess - contribute flag
if formdata:
for f in self.entries:
key = 'del-%s' % f.id
f._should_delete = key in formdata
return res
def should_delete(self, field):
return getattr(field, '_should_delete', False)
def populate_obj(self, obj, name):
result = []
for f in self.entries:
if not self.should_delete(f):
field = self.field_type()
f.populate_obj(field, None)
result.append(field)
setattr(obj, self.prop, result)
class InlineModelFormField(FormField): class InlineModelFormField(FormField):
...@@ -12,19 +59,6 @@ class InlineModelFormField(FormField): ...@@ -12,19 +59,6 @@ class InlineModelFormField(FormField):
super(InlineModelFormField, self).__init__(form, **kwargs) super(InlineModelFormField, self).__init__(form, **kwargs)
self._pk = pk self._pk = pk
self._should_delete = False
def process(self, formdata, data=None):
super(InlineModelFormField, self).process(formdata, data)
# Grab delete key
if formdata:
key = 'del-%s' % self.id
if key in formdata:
self._should_delete = True
def should_delete(self):
return self._should_delete
def get_pk(self): def get_pk(self):
return getattr(self.form, self._pk).data return getattr(self.form, self._pk).data
......
...@@ -38,7 +38,7 @@ def list_formatter(values): ...@@ -38,7 +38,7 @@ def list_formatter(values):
:param values: :param values:
Value to check Value to check
""" """
return u', '.join(values) return u', '.join(unicode(v) for v in values)
DEFAULT_FORMATTERS = { DEFAULT_FORMATTERS = {
......
from flask.ext.admin.form import RenderTemplateWidget from flask.ext.admin.form import RenderTemplateWidget
class InlineFieldListWidget(RenderTemplateWidget):
def __init__(self):
super(InlineFieldListWidget, self).__init__('admin/model/inline_field_list.html')
class InlineFormListWidget(RenderTemplateWidget): class InlineFormListWidget(RenderTemplateWidget):
def __init__(self): def __init__(self):
super(InlineFormListWidget, self).__init__('admin/model/inline_form_list.html') super(InlineFormListWidget, self).__init__('admin/model/inline_form_list.html')
...@@ -62,6 +62,6 @@ form.search-form a.clear i { ...@@ -62,6 +62,6 @@ form.search-form a.clear i {
} }
/* Inline forms */ /* Inline forms */
.fa-inline-form-control { .fa-inline-field-control {
float: right; float: right;
} }
\ No newline at end of file
...@@ -20,16 +20,16 @@ ...@@ -20,16 +20,16 @@
} }
}; };
this.addInlineModel = function(id, el, template) { this.addInlineField = function(id, el, template) {
var $el = $(el); var $el = $(el);
var $template = $(template); var $template = $(template);
// Figure out new form ID // Figure out new field ID
var lastForm = $el.children('.fa-inline-form').last(); var lastField = $el.children('.fa-inline-field').last();
var prefix = id + '-0'; var prefix = id + '-0';
if (lastForm.length > 0) { if (lastField.length > 0) {
var parts = $(lastForm[0]).attr('id').split('-'); var parts = $(lastField[0]).attr('id').split('-');
idx = parseInt(parts[parts.length - 1]) + 1; idx = parseInt(parts[parts.length - 1]) + 1;
prefix = id + '-' + idx; prefix = id + '-' + idx;
} }
...@@ -61,10 +61,10 @@ ...@@ -61,10 +61,10 @@
}; };
// Add live event handler // Add live event handler
$('.fa-remove-form').live('click', function(e) { $('.fa-remove-field').live('click', function(e) {
e.preventDefault(); e.preventDefault();
var form = $(this).closest('.fa-inline-form'); var form = $(this).closest('.fa-inline-field');
form.remove(); form.remove();
}); });
......
{% import 'admin/model/inline_list_base.html' as base with context %}
{% macro render_field(field) %}
{{ field }}
{% endmacro %}
{{ base.render_inline_fields(field, template, render_field) }}
{% import 'admin/model/inline_list_base.html' as base with context %}
{% import 'admin/lib.html' as lib with context %} {% import 'admin/lib.html' as lib with context %}
{% macro render_form(form) %} {% macro render_field(field) %}
{{ lib.render_form_fields(field, True) }}
{% endmacro %} {% endmacro %}
{% macro render_template(template) -%} {{ base.render_inline_fields(field, template, render_field, check) }}
<div class="fa-inline-form">
<div class="fa-inline-form-control">
<a href="#" class="fa-remove-form"><i class="icon-remove"></i></a>
</div>
{{ lib.render_form_fields(template) }}
<hr/>
</div>
{%- endmacro %}
<div class="well">
<div id="{{ field.id }}-forms">
{% for subfield in field %}
<div id="{{ subfield.id }}" class="fa-inline-form">
{% set pk = subfield.get_pk() %}
{%- if pk %}
<div class="fa-inline-form-control">
<input type="checkbox" name="del-{{ subfield.id }}" id="del-{{ subfield.id }}" />
<label for="del-{{ subfield.id }}" style="display: inline">{{ _gettext('Delete?') }}</label>
</div>
{%- endif -%}
{{ lib.render_form_fields(subfield, True) }}
<hr/>
</div>
{% endfor %}
</div>
<a href="#" class="btn" onclick="faForm.addInlineModel('{{ field.id }}', '#{{ field.id }}-forms', {{ render_template(template)|tojson }});">{{ _gettext('Add') }} {{ field.label.text }}</a>
</div>
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