Commit 2845e4b2 authored by Serge S. Koval's avatar Serge S. Koval

Added inline model editing support for peewee backend

parent 81555678
......@@ -25,6 +25,16 @@ class User(BaseModel):
return self.username
class UserInfo(BaseModel):
key = peewee.CharField(max_length=64)
value = peewee.CharField(max_length=64)
user = peewee.ForeignKeyField(User)
def __unicode__(self):
return '%s - %s' % (self.key, self.value)
class Post(BaseModel):
title = peewee.CharField(max_length=120)
text = peewee.TextField(null=False)
......@@ -36,6 +46,10 @@ class Post(BaseModel):
return self.title
class UserAdmin(peeweemodel.ModelView):
inline_models = (UserInfo,)
class PostAdmin(peeweemodel.ModelView):
# Visible columns in the list view
#list_columns = ('title', 'user')
......@@ -60,13 +74,18 @@ def index():
if __name__ == '__main__':
import logging
logging.basicConfig()
logging.getLogger().setLevel(logging.DEBUG)
admin = admin.Admin(app, 'Peewee Models')
admin.add_view(peeweemodel.ModelView(User))
admin.add_view(UserAdmin(User))
admin.add_view(PostAdmin(Post))
try:
User.create_table()
UserInfo.create_table()
Post.create_table()
except:
pass
......
......@@ -64,9 +64,6 @@ class UserInfo(db.Model):
key = db.Column(db.String(64), nullable=False)
value = db.Column(db.String(64))
tag_id = db.Column(db.Integer(), db.ForeignKey(Tag.id), nullable=False)
tags = db.relationship(Tag, backref='userinfos')
user_id = db.Column(db.Integer(), db.ForeignKey(User.id))
user = db.relationship(User, backref='info')
......@@ -82,7 +79,7 @@ def index():
# Customized User model admin
class UserAdmin(sqlamodel.ModelView):
inline_models = ('info',)
inline_models = (UserInfo,)
# Customized Post model admin
......
from wtforms import fields
from peewee import DateTimeField, DateField, TimeField
from peewee import DateTimeField, DateField, TimeField, BaseModel, ForeignKeyField
from wtfpeewee.orm import ModelConverter
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.fields import InlineModelFormField
from flask.ext.admin.model.widgets import InlineFormListWidget
from .tools import get_primary_key
class InlineModelFormList(fields.FieldList):
widget = InlineFormListWidget()
def __init__(self, form, model, prop, **kwargs):
self.form = form
self.model = model
self.prop = prop
# TODO: Fix me
self._pk = get_primary_key(model)
super(InlineModelFormList, self).__init__(InlineModelFormField(form, self._pk), **kwargs)
def __call__(self, **kwargs):
return self.widget(self, template=self.form(), **kwargs)
def process(self, formdata, data=None):
if not formdata:
data = self.model.select().where(user=data).execute()
else:
data = None
return super(InlineModelFormList, self).process(formdata, data)
def populate_obj(self, obj, name):
pass
def save_related(self, obj):
model_id = getattr(obj, self._pk)
values = self.model.select().where(user=model_id).execute()
pk_map = dict((str(getattr(v, self._pk)), v) for v in values)
# Handle request data
for field in self.entries:
field_id = field.get_pk()
if field_id in pk_map:
model = pk_map[field_id]
if field.should_delete():
model.delete_instance(recursive=True)
continue
else:
model = self.model()
field.populate_obj(model, None)
# Force relation
setattr(model, self.prop, model_id)
model.save()
class CustomModelConverter(ModelConverter):
......@@ -24,3 +84,68 @@ class CustomModelConverter(ModelConverter):
def handle_time(self, model, field, **kwargs):
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:
raise Exception('Unknown inline model admin: %s' % repr(p))
# Find property from target model to current model
reverse_field = None
for field in info.model._meta.get_fields():
field_type = type(field)
if field_type == ForeignKeyField:
if field.to == model:
reverse_field = field
break
else:
raise Exception('Cannot find reverse relation for model %s' % info.model)
# Remove reverse property from the list
ignore = [reverse_field.name]
if info.exclude:
exclude = ignore + info.exclude
else:
exclude = ignore
# Create field
converter = CustomModelConverter()
child_form = model_form(info.model,
base_class=form.BaseForm,
only=info.include,
exclude=exclude,
allow_pk=True,
converter=converter)
prop_name = 'fa_%s' % model.__name__
setattr(form_class,
prop_name,
InlineModelFormList(child_form,
info.model,
reverse_field.name,
label=info.model.__name__))
setattr(field.to,
prop_name,
property(lambda self: self.id))
return form_class
def save_inline(form, model):
for _, f in form._fields.iteritems():
if f.type == 'InlineModelFormList':
f.save_related(model)
from peewee import PrimaryKeyField
def get_primary_key(model):
for n, f in model._meta.get_sorted_fields():
if type(f) == PrimaryKeyField:
return n
......@@ -9,7 +9,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
from .form import CustomModelConverter, contribute_inline, save_inline
from .tools import get_primary_key
class ModelView(BaseModelView):
......@@ -49,6 +50,14 @@ class ModelView(BaseModelView):
for your model.
"""
inline_models = None
"""
Inline related-model editing for parent to child relation::
class MyModelView(ModelView):
inline_models = (Post,)
"""
def __init__(self, model, name=None,
category=None, endpoint=None, url=None):
self._search_fields = []
......@@ -64,11 +73,7 @@ class ModelView(BaseModelView):
return model._meta.get_sorted_fields()
def scaffold_pk(self):
for n, f in self._get_model_fields():
if type(f) == PrimaryKeyField:
return n
return None
return get_primary_key(self.model)
def get_pk_value(self, model):
return getattr(model, self._primary_key)
......@@ -149,12 +154,17 @@ class ModelView(BaseModelView):
return isinstance(filter, filters.BasePeeweeFilter)
def scaffold_form(self):
return model_form(self.model,
base_class=form.BaseForm,
only=self.form_columns,
exclude=self.excluded_form_columns,
field_args=self.form_args,
converter=CustomModelConverter())
form_class = model_form(self.model,
base_class=form.BaseForm,
only=self.form_columns,
exclude=self.excluded_form_columns,
field_args=self.form_args,
converter=CustomModelConverter())
if self.inline_models:
form_class = contribute_inline(self.model, form_class, self.inline_models)
return form_class
def _handle_join(self, query, field, joins):
if field.model != self.model:
......@@ -237,6 +247,10 @@ class ModelView(BaseModelView):
model = self.model()
form.populate_obj(model)
model.save()
# For peewee have to save inline forms after model was saved
save_inline(form, model)
return True
except Exception, ex:
flash(gettext('Failed to create model. %(error)s', error=str(ex)), 'error')
......@@ -252,6 +266,10 @@ class ModelView(BaseModelView):
try:
form.populate_obj(model)
model.save()
# For peewee have to save inline forms after model was saved
save_inline(form, model)
return True
except Exception, ex:
flash(gettext('Failed to update model. %(error)s', error=str(ex)), 'error')
......
......@@ -205,7 +205,7 @@ class InlineModelFormList(FieldList):
# Create primary key map
pk_map = dict((str(getattr(v, self._pk)), v) for v in values)
# Create fake object to work around wtforms limitations
# Handle request data
for field in self.entries:
field_id = field.get_pk()
......
......@@ -268,34 +268,42 @@ def contribute_inline(session, model, form_class, inline_models):
# Contribute columns
for p in inline_models:
# Figure out
if isinstance(p, basestring):
info = InlineFormAdmin(p)
elif isinstance(p, tuple):
# 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:
raise Exception('Unknown inline model admin: %s' % repr(p))
prop = mapper.get_property(info.field)
if prop is None:
raise Exception('Inline form property %s.%s was not found' % (model.__name__,
info.field))
# Find property from target model to current model
target_mapper = info.model._sa_class_manager.mapper
reverse_prop = None
if not hasattr(prop, 'direction'):
raise Exception('Failed to convert inline admin %s - only one-to-many relations are supported' % info.field)
for prop in target_mapper.iterate_properties:
if hasattr(prop, 'direction') and prop.direction.name == 'MANYTOONE':
if prop.mapper.class_ == model:
reverse_prop = prop
break
else:
raise Exception('Cannot find reverse relation for model %s' % info.model)
if prop.direction.name != 'ONETOMANY':
raise Exception('Failed to convert inline admin %s - only one-to-many relations are supported' % info.field)
# Find forward property
forward_prop = None
# Find reverse relationship (to exlude from the list)
ignore = []
for prop in mapper.iterate_properties:
if hasattr(prop, 'direction') and prop.direction.name == 'ONETOMANY':
if prop.mapper.class_ == target_mapper.class_:
forward_prop = prop
break
else:
raise Exception('Cannot find forward relation for model %s' % info.model)
for remote_prop in prop.mapper.iterate_properties:
if hasattr(remote_prop, 'direction') and remote_prop.direction.name == 'MANYTOONE':
if remote_prop.mapper.class_ == prop.parent.class_:
ignore.append(remote_prop.key)
# Remove reverse property from the list
ignore = [reverse_prop.key]
if info.exclude:
exclude = ignore + info.exclude
......@@ -303,15 +311,18 @@ def contribute_inline(session, model, form_class, inline_models):
exclude = ignore
# Create field
remote_model = prop.mapper.class_
converter = AdminModelConverter(session, info)
child_form = get_form(remote_model, converter,
child_form = get_form(info.model,
converter,
only=info.include,
exclude=exclude,
hidden_pk=True)
setattr(form_class, p,
InlineModelFormList(child_form, session, remote_model, p))
setattr(form_class,
forward_prop.key,
InlineModelFormList(child_form,
session,
info.model,
forward_prop.key))
return form_class
......@@ -126,13 +126,10 @@ class ModelView(BaseModelView):
inline_models = None
"""
Inline related-model editing for parent to child relation.
Inline related-model editing for parent to child relation::
If you have child relation with name 'posts', you can generate inline
administration interface by using this code::
class MyModelView(BaseModelView):
inline_models = ('posts',)
class MyModelView(ModelView):
inline_models = (Post,)
"""
def __init__(self, model, session,
......
from wtforms.fields import FormField
class InlineModelFormField(FormField):
def __init__(self, form, pk, **kwargs):
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
def populate_obj(self, obj, name):
for name, field in self.form._fields.iteritems():
if name != self._pk:
field.populate_obj(obj, name)
......@@ -11,8 +11,8 @@ def converts(*args):
class InlineFormAdmin(object):
def __init__(self, field, **kwargs):
self.field = field
def __init__(self, model, **kwargs):
self.model = model
defaults = dict(include=None,
exclude=None)
......
......@@ -74,7 +74,7 @@
{%- endmacro %}
{% macro render_form_fields(form, focus_set=False) %}
{{ form.hidden_tag() }}
{{ form.hidden_tag() if form.hidden_tag is defined }}
{% for f in form if f.type != 'HiddenField' and f.type != 'CSRFTokenField' %}
<div class="control-group{% if f.errors %} error{% endif %}">
......
......@@ -29,5 +29,5 @@
</div>
{% endfor %}
</div>
<a href="#" class="btn" onclick="faForm.addInlineModel('{{ field.id }}', '#{{ field.id }}-forms', {{ render_template(template)|tojson }});">Add {{ field.name }}</a>
<a href="#" class="btn" onclick="faForm.addInlineModel('{{ field.id }}', '#{{ field.id }}-forms', {{ render_template(template)|tojson }});">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