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): ...@@ -25,6 +25,16 @@ class User(BaseModel):
return self.username 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): class Post(BaseModel):
title = peewee.CharField(max_length=120) title = peewee.CharField(max_length=120)
text = peewee.TextField(null=False) text = peewee.TextField(null=False)
...@@ -36,6 +46,10 @@ class Post(BaseModel): ...@@ -36,6 +46,10 @@ class Post(BaseModel):
return self.title return self.title
class UserAdmin(peeweemodel.ModelView):
inline_models = (UserInfo,)
class PostAdmin(peeweemodel.ModelView): class PostAdmin(peeweemodel.ModelView):
# Visible columns in the list view # Visible columns in the list view
#list_columns = ('title', 'user') #list_columns = ('title', 'user')
...@@ -60,13 +74,18 @@ def index(): ...@@ -60,13 +74,18 @@ def index():
if __name__ == '__main__': if __name__ == '__main__':
import logging
logging.basicConfig()
logging.getLogger().setLevel(logging.DEBUG)
admin = admin.Admin(app, 'Peewee Models') admin = admin.Admin(app, 'Peewee Models')
admin.add_view(peeweemodel.ModelView(User)) admin.add_view(UserAdmin(User))
admin.add_view(PostAdmin(Post)) admin.add_view(PostAdmin(Post))
try: try:
User.create_table() User.create_table()
UserInfo.create_table()
Post.create_table() Post.create_table()
except: except:
pass pass
......
...@@ -64,9 +64,6 @@ class UserInfo(db.Model): ...@@ -64,9 +64,6 @@ class UserInfo(db.Model):
key = db.Column(db.String(64), nullable=False) key = db.Column(db.String(64), nullable=False)
value = db.Column(db.String(64)) 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_id = db.Column(db.Integer(), db.ForeignKey(User.id))
user = db.relationship(User, backref='info') user = db.relationship(User, backref='info')
...@@ -82,7 +79,7 @@ def index(): ...@@ -82,7 +79,7 @@ def index():
# Customized User model admin # Customized User model admin
class UserAdmin(sqlamodel.ModelView): class UserAdmin(sqlamodel.ModelView):
inline_models = ('info',) inline_models = (UserInfo,)
# Customized Post model admin # Customized Post model admin
......
from wtforms import fields 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 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): class CustomModelConverter(ModelConverter):
...@@ -24,3 +84,68 @@ class CustomModelConverter(ModelConverter): ...@@ -24,3 +84,68 @@ class CustomModelConverter(ModelConverter):
def handle_time(self, model, field, **kwargs): def handle_time(self, model, field, **kwargs):
return field.name, form.TimeField(**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 ...@@ -9,7 +9,8 @@ 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.peeweemodel import filters 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): class ModelView(BaseModelView):
...@@ -49,6 +50,14 @@ class ModelView(BaseModelView): ...@@ -49,6 +50,14 @@ class ModelView(BaseModelView):
for your model. 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, def __init__(self, model, name=None,
category=None, endpoint=None, url=None): category=None, endpoint=None, url=None):
self._search_fields = [] self._search_fields = []
...@@ -64,11 +73,7 @@ class ModelView(BaseModelView): ...@@ -64,11 +73,7 @@ class ModelView(BaseModelView):
return model._meta.get_sorted_fields() return model._meta.get_sorted_fields()
def scaffold_pk(self): def scaffold_pk(self):
for n, f in self._get_model_fields(): return get_primary_key(self.model)
if type(f) == PrimaryKeyField:
return n
return None
def get_pk_value(self, model): def get_pk_value(self, model):
return getattr(model, self._primary_key) return getattr(model, self._primary_key)
...@@ -149,12 +154,17 @@ class ModelView(BaseModelView): ...@@ -149,12 +154,17 @@ class ModelView(BaseModelView):
return isinstance(filter, filters.BasePeeweeFilter) return isinstance(filter, filters.BasePeeweeFilter)
def scaffold_form(self): def scaffold_form(self):
return model_form(self.model, form_class = model_form(self.model,
base_class=form.BaseForm, base_class=form.BaseForm,
only=self.form_columns, only=self.form_columns,
exclude=self.excluded_form_columns, exclude=self.excluded_form_columns,
field_args=self.form_args, field_args=self.form_args,
converter=CustomModelConverter()) 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): def _handle_join(self, query, field, joins):
if field.model != self.model: if field.model != self.model:
...@@ -237,6 +247,10 @@ class ModelView(BaseModelView): ...@@ -237,6 +247,10 @@ class ModelView(BaseModelView):
model = self.model() model = self.model()
form.populate_obj(model) form.populate_obj(model)
model.save() model.save()
# For peewee have to save inline forms after model was saved
save_inline(form, model)
return True return True
except Exception, ex: except Exception, ex:
flash(gettext('Failed to create model. %(error)s', error=str(ex)), 'error') flash(gettext('Failed to create model. %(error)s', error=str(ex)), 'error')
...@@ -252,6 +266,10 @@ class ModelView(BaseModelView): ...@@ -252,6 +266,10 @@ class ModelView(BaseModelView):
try: try:
form.populate_obj(model) form.populate_obj(model)
model.save() model.save()
# For peewee have to save inline forms after model was saved
save_inline(form, model)
return True return True
except Exception, ex: except Exception, ex:
flash(gettext('Failed to update model. %(error)s', error=str(ex)), 'error') flash(gettext('Failed to update model. %(error)s', error=str(ex)), 'error')
......
...@@ -205,7 +205,7 @@ class InlineModelFormList(FieldList): ...@@ -205,7 +205,7 @@ class InlineModelFormList(FieldList):
# Create primary key map # Create primary key map
pk_map = dict((str(getattr(v, self._pk)), v) for v in values) 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: for field in self.entries:
field_id = field.get_pk() field_id = field.get_pk()
......
...@@ -268,34 +268,42 @@ def contribute_inline(session, model, form_class, inline_models): ...@@ -268,34 +268,42 @@ def contribute_inline(session, model, form_class, inline_models):
# Contribute columns # Contribute columns
for p in inline_models: for p in inline_models:
# Figure out # Figure out settings
if isinstance(p, basestring): if isinstance(p, tuple):
info = InlineFormAdmin(p)
elif isinstance(p, tuple):
info = InlineFormAdmin(p[0], **p[1]) info = InlineFormAdmin(p[0], **p[1])
elif isinstance(p, InlineFormAdmin): elif isinstance(p, InlineFormAdmin):
info = p info = p
elif hasattr(p, '_sa_class_manager'):
info = InlineFormAdmin(p)
else: else:
raise Exception('Unknown inline model admin: %s' % repr(p)) raise Exception('Unknown inline model admin: %s' % repr(p))
prop = mapper.get_property(info.field) # Find property from target model to current model
if prop is None: target_mapper = info.model._sa_class_manager.mapper
raise Exception('Inline form property %s.%s was not found' % (model.__name__,
info.field)) reverse_prop = None
if not hasattr(prop, 'direction'): for prop in target_mapper.iterate_properties:
raise Exception('Failed to convert inline admin %s - only one-to-many relations are supported' % info.field) 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': # Find forward property
raise Exception('Failed to convert inline admin %s - only one-to-many relations are supported' % info.field) forward_prop = None
# Find reverse relationship (to exlude from the list) for prop in mapper.iterate_properties:
ignore = [] 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: # Remove reverse property from the list
if hasattr(remote_prop, 'direction') and remote_prop.direction.name == 'MANYTOONE': ignore = [reverse_prop.key]
if remote_prop.mapper.class_ == prop.parent.class_:
ignore.append(remote_prop.key)
if info.exclude: if info.exclude:
exclude = ignore + info.exclude exclude = ignore + info.exclude
...@@ -303,15 +311,18 @@ def contribute_inline(session, model, form_class, inline_models): ...@@ -303,15 +311,18 @@ def contribute_inline(session, model, form_class, inline_models):
exclude = ignore exclude = ignore
# Create field # Create field
remote_model = prop.mapper.class_
converter = AdminModelConverter(session, info) converter = AdminModelConverter(session, info)
child_form = get_form(remote_model, converter, child_form = get_form(info.model,
converter,
only=info.include, only=info.include,
exclude=exclude, exclude=exclude,
hidden_pk=True) hidden_pk=True)
setattr(form_class, p, setattr(form_class,
InlineModelFormList(child_form, session, remote_model, p)) forward_prop.key,
InlineModelFormList(child_form,
session,
info.model,
forward_prop.key))
return form_class return form_class
...@@ -126,13 +126,10 @@ class ModelView(BaseModelView): ...@@ -126,13 +126,10 @@ class ModelView(BaseModelView):
inline_models = None 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 class MyModelView(ModelView):
administration interface by using this code:: inline_models = (Post,)
class MyModelView(BaseModelView):
inline_models = ('posts',)
""" """
def __init__(self, model, session, 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): ...@@ -11,8 +11,8 @@ def converts(*args):
class InlineFormAdmin(object): class InlineFormAdmin(object):
def __init__(self, field, **kwargs): def __init__(self, model, **kwargs):
self.field = field self.model = model
defaults = dict(include=None, defaults = dict(include=None,
exclude=None) exclude=None)
......
...@@ -74,7 +74,7 @@ ...@@ -74,7 +74,7 @@
{%- endmacro %} {%- endmacro %}
{% macro render_form_fields(form, focus_set=False) %} {% 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' %} {% for f in form if f.type != 'HiddenField' and f.type != 'CSRFTokenField' %}
<div class="control-group{% if f.errors %} error{% endif %}"> <div class="control-group{% if f.errors %} error{% endif %}">
......
...@@ -29,5 +29,5 @@ ...@@ -29,5 +29,5 @@
</div> </div>
{% endfor %} {% endfor %}
</div> </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> </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