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

Refactored inline models form/widget integration

parent 3e2be5a2
......@@ -4,7 +4,7 @@
- Create documentation
- Model Admin
- 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
- Filters
- Use table to draw filters so column names will line up?
......
......@@ -20,7 +20,7 @@ db.init_app(app)
class User(db.Document):
name = db.StringField(max_length=40)
tag = db.StringField(max_length=20)
tags = db.ListField(db.ReferenceField('Tag'))
password = db.StringField(max_length=40)
def __unicode__(self):
......@@ -39,10 +39,27 @@ class Todo(db.Document):
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
class UserView(ModelView):
column_filters = ('name',
User.tag)
column_filters = ['name']
# Flask views
......@@ -55,9 +72,15 @@ if __name__ == '__main__':
# Create admin
admin = admin.Admin(app, 'Simple Models')
#p = Post.objects[0]
#p.inner.append(Comment(name='12345'))
#p.save()
# Add views
admin.add_view(UserView(User))
admin.add_view(ModelView(Todo))
admin.add_view(ModelView(Tag))
admin.add_view(ModelView(Post))
# Start app
app.debug = True
......
......@@ -2,6 +2,8 @@ from flask.ext.admin.babel import gettext
from flask.ext.admin.model import filters
from .tools import parse_like_term
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):
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):
return orm.model_form(model, base_class=base_class, only=only,
exclude=exclude, field_args=field_args,
......
......@@ -12,6 +12,7 @@ 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 .typefmt import MONGOENGINE_FORMATTERS
SORTABLE_FIELDS = set((
......@@ -82,6 +83,11 @@ class ModelView(BaseModelView):
for your model.
"""
list_type_formatters = MONGOENGINE_FORMATTERS
"""
Customized list formatters for MongoEngine
"""
def __init__(self, model, name=None,
category=None, endpoint=None, url=None):
self._search_fields = []
......@@ -117,7 +123,12 @@ class ModelView(BaseModelView):
# Verify type
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)
return columns
......
......@@ -8,9 +8,10 @@ from wtforms.fields import SelectFieldBase, FieldList
from wtforms.validators import ValidationError
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
try:
from sqlalchemy.orm.util import identity_key
has_identity_key = True
......@@ -180,12 +181,11 @@ class QuerySelectMultipleField(QuerySelectField):
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.
"""
widget = InlineFormListWidget()
def __init__(self, form, session, model, prop, **kwargs):
......@@ -210,8 +210,8 @@ class InlineModelFormList(FieldList):
super(InlineModelFormList, self).__init__(InlineModelFormField(form, self._pk), **kwargs)
def __call__(self, **kwargs):
return self.widget(self, template=self.form(), **kwargs)
def display_row_controls(self, field):
return field.get_pk() is not None
def populate_obj(self, obj, name):
values = getattr(obj, name, None)
......@@ -229,7 +229,7 @@ class InlineModelFormList(FieldList):
if field_id in pk_map:
model = pk_map[field_id]
if field.should_delete():
if self.should_delete(field):
self.session.delete(model)
continue
else:
......@@ -239,7 +239,7 @@ class InlineModelFormList(FieldList):
field.populate_obj(model, None)
# Force relation
setattr(self.model, self.prop, obj)
setattr(model, self.prop, obj)
def get_pk_from_identity(obj):
......
......@@ -148,12 +148,10 @@ class RenderTemplateWidget(object):
ctx = _request_ctx_stack.top
jinja_env = ctx.app.jinja_env
kwargs['field'] = field
# Provide i18n support even if flask-babel is not installed
# or enabled.
kwargs['_gettext'] = gettext
kwargs['_ngettext'] = ngettext
kwargs.update({
'field': field,
'_gettext': gettext,
'_ngettext': ngettext})
template = jinja_env.get_template(self.template)
return template.render(kwargs)
......@@ -167,12 +165,13 @@ class Select2TagsWidget(widgets.TextInput):
kwargs['data-role'] = u'select2tags'
return super(Select2TagsWidget, self).__call__(field, **kwargs)
class Select2TagsField(fields.TextField):
"""`Select2 <http://ivaynberg.github.com/select2/#tags>`_ styled text field.
You must include select2.js, form.js and select2 stylesheet for it to work.
"""
widget = Select2TagsWidget()
def __init__(self, label=None, validators=None, save_as_list=False, **kwargs):
"""Initialization
......@@ -190,4 +189,3 @@ class Select2TagsField(fields.TextField):
def _value(self):
return u', '.join(self.data) if isinstance(self.data, list) else self.data
......@@ -746,6 +746,8 @@ class BaseModelView(BaseView, ActionsMixin):
value = rec_getattr(model, name)
print name, type(value)
type_fmt = self.list_type_formatters.get(type(value))
if type_fmt is not None:
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):
......@@ -12,19 +59,6 @@ class InlineModelFormField(FormField):
super(InlineModelFormField, self).__init__(form, **kwargs)
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):
return getattr(self.form, self._pk).data
......
......@@ -38,7 +38,7 @@ def list_formatter(values):
:param values:
Value to check
"""
return u', '.join(values)
return u', '.join(unicode(v) for v in values)
DEFAULT_FORMATTERS = {
......
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):
def __init__(self):
super(InlineFormListWidget, self).__init__('admin/model/inline_form_list.html')
......@@ -62,6 +62,6 @@ form.search-form a.clear i {
}
/* Inline forms */
.fa-inline-form-control {
.fa-inline-field-control {
float: right;
}
\ No newline at end of file
}
......@@ -20,16 +20,16 @@
}
};
this.addInlineModel = function(id, el, template) {
this.addInlineField = function(id, el, template) {
var $el = $(el);
var $template = $(template);
// Figure out new form ID
var lastForm = $el.children('.fa-inline-form').last();
// Figure out new field ID
var lastField = $el.children('.fa-inline-field').last();
var prefix = id + '-0';
if (lastForm.length > 0) {
var parts = $(lastForm[0]).attr('id').split('-');
if (lastField.length > 0) {
var parts = $(lastField[0]).attr('id').split('-');
idx = parseInt(parts[parts.length - 1]) + 1;
prefix = id + '-' + idx;
}
......@@ -61,10 +61,10 @@
};
// Add live event handler
$('.fa-remove-form').live('click', function(e) {
$('.fa-remove-field').live('click', function(e) {
e.preventDefault();
var form = $(this).closest('.fa-inline-form');
var form = $(this).closest('.fa-inline-field');
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 %}
{% macro render_form(form) %}
{% macro render_field(field) %}
{{ lib.render_form_fields(field, True) }}
{% endmacro %}
{% macro render_template(template) -%}
<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>
{{ base.render_inline_fields(field, template, render_field, check) }}
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