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

More complex inline model field example for SQLAlchemy

parent b9eb2cca
...@@ -15,3 +15,4 @@ venv ...@@ -15,3 +15,4 @@ venv
*.sublime-* *.sublime-*
.coverage .coverage
__pycache__ __pycache__
examples/sqla-inline/static
import os
import os.path as op
from werkzeug import secure_filename
from sqlalchemy import event
from flask import Flask, request, render_template
from flask.ext.sqlalchemy import SQLAlchemy
from wtforms import fields
from flask.ext import admin
from flask.ext.admin.form import RenderTemplateWidget
from flask.ext.admin.model.form import InlineFormAdmin
from flask.ext.admin.contrib.sqlamodel import ModelView
from flask.ext.admin.contrib.sqlamodel.form import InlineModelConverter
from flask.ext.admin.contrib.sqlamodel.fields import InlineModelFormList
# Create application
app = Flask(__name__)
# Create dummy secrey key so we can use sessions
app.config['SECRET_KEY'] = '123456790'
# Create in-memory database
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.sqlite'
app.config['SQLALCHEMY_ECHO'] = True
db = SQLAlchemy(app)
# Figure out base upload path
base_path = op.join(op.dirname(__file__), 'static')
# Create models
class Location(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Unicode(64))
class LocationImage(db.Model):
id = db.Column(db.Integer, primary_key=True)
alt = db.Column(db.Unicode(128))
path = db.Column(db.String(64))
location_id = db.Column(db.Integer, db.ForeignKey(Location.id))
location = db.relation(Location, backref='images')
# Register after_delete handler which will delete image file after model gets deleted
@event.listens_for(LocationImage, 'after_delete')
def _handle_image_delete(mapper, conn, target):
try:
if target.path:
os.remove(op.join(base_path, target.path))
except:
pass
# This widget uses custom template for inline field list
class CustomInlineFieldListWidget(RenderTemplateWidget):
def __init__(self):
super(CustomInlineFieldListWidget, self).__init__('field_list.html')
# This InlineModelFormList will use our custom widget
class CustomInlineModelFormList(InlineModelFormList):
widget = CustomInlineFieldListWidget()
# Create custom InlineModelConverter and tell it to use our InlineModelFormList
class CustomInlineModelConverter(InlineModelConverter):
inline_field_list_type = CustomInlineModelFormList
# Customized inline form handler
class InlineModelForm(InlineFormAdmin):
form_excluded_columns = ('path',)
form_label = 'Image'
def __init__(self):
return super(InlineModelForm, self).__init__(LocationImage)
def postprocess_form(self, form_class):
form_class.upload = fields.FileField('Image')
return form_class
def on_model_change(self, form, model):
file_data = request.files.get(form.upload.name)
if file_data:
model.path = secure_filename(file_data.filename)
file_data.save(op.join(base_path, model.path))
# Administrative class
class LocationAdmin(ModelView):
inline_model_form_converter = CustomInlineModelConverter
inline_models = (InlineModelForm(),)
def __init__(self):
super(LocationAdmin, self).__init__(Location, db.session, name='Locations')
# Simple page to show images
@app.route('/')
def index():
locations = db.session.query(Location).all()
return render_template('locations.html', locations=locations)
if __name__ == '__main__':
# Create upload directory
try:
os.mkdir(base_path)
except OSError:
pass
# Create admin
admin = admin.Admin(app, 'Inline Fun')
# Add views
admin.add_view(LocationAdmin())
# Create DB
db.create_all()
# Start app
app.run(debug=True)
{% import 'admin/model/inline_list_base.html' as base with context %}
{% macro render_field(field) %}
{% set model = field.object_data %}
{% if model and model.path %}
{{ field.form.id }}
<img src="{{ url_for('static', filename=model.path) }}" style="max-width: 300px;"></img>
{% else %}
{{ field }}
{% endif %}
{% endmacro %}
{{ base.render_inline_fields(field, template, render_field) }}
<html>
<body>
{% for loc in locations %}
<h2>{{ loc.name }}</h2>
{% for img in loc.images %}
<img src="{{ url_for('static', filename=img.path) }}" alt="{{ img.alt }}" style="max-width: 300px"></img>
{% endfor %}
<hr/>
{% endfor %}
<a href="/admin">Open admin to upload some images.</a>
</body>
</html>
...@@ -162,6 +162,9 @@ class InlineModelConverter(InlineModelConverterBase): ...@@ -162,6 +162,9 @@ class InlineModelConverter(InlineModelConverterBase):
exclude = ignore exclude = ignore
# Create field # Create field
child_form = info.get_form()
if child_form is None:
child_form = model_form(info.model, child_form = model_form(info.model,
base_class=form.BaseForm, base_class=form.BaseForm,
only=info.form_columns, only=info.form_columns,
......
...@@ -305,7 +305,8 @@ def _resolve_prop(prop): ...@@ -305,7 +305,8 @@ def _resolve_prop(prop):
# Get list of fields and generate form # Get list of fields and generate form
def get_form(model, converter, def get_form(model, converter,
base_class=form.BaseForm, base_class=form.BaseForm,
only=None, exclude=None, only=None,
exclude=None,
field_args=None, field_args=None,
hidden_pk=False, hidden_pk=False,
ignore_hidden=True): ignore_hidden=True):
...@@ -473,11 +474,14 @@ class InlineModelConverter(InlineModelConverterBase): ...@@ -473,11 +474,14 @@ class InlineModelConverter(InlineModelConverterBase):
ignore = [reverse_prop.key] ignore = [reverse_prop.key]
if info.form_excluded_columns: if info.form_excluded_columns:
exclude = ignore + info.form_excluded_columns exclude = ignore + list(info.form_excluded_columns)
else: else:
exclude = ignore exclude = ignore
# Create form # Create form
child_form = info.get_form()
if child_form is None:
child_form = get_form(info.model, child_form = get_form(info.model,
converter, converter,
only=info.form_columns, only=info.form_columns,
......
...@@ -41,6 +41,13 @@ class InlineFormAdmin(object): ...@@ -41,6 +41,13 @@ class InlineFormAdmin(object):
for k, v in iteritems(kwargs): for k, v in iteritems(kwargs):
setattr(self, k, v) setattr(self, k, v)
def get_form(self):
"""
If you want to use completely custom form for inline field, you can override
Flask-Admin form generation logic by overriding this method and returning your form.
"""
return None
def postprocess_form(self, form_class): def postprocess_form(self, form_class):
""" """
Post process form. Use this to contribute fields. Post process form. Use this to contribute fields.
......
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