Commit 37ae5a07 authored by Serge S. Koval's avatar Serge S. Koval

Multi-select for many-to-one relationships, jQuery Chosen for drop downs, fixes.

parent 52131105
...@@ -3,14 +3,12 @@ ...@@ -3,14 +3,12 @@
- Override base URL (/admin/) - Override base URL (/admin/)
- Model Admin - Model Admin
- Ability to sort by fields that are not visible? - Ability to sort by fields that are not visible?
- Proper error display
- SQLA Model Admin - SQLA Model Admin
- Validation of the joins in the query - Validation of the joins in the query
- Automatic joined load for foreign keys - Automatic joined load for foreign keys
- Automatic PK detection - Automatic PK detection
- Filtering - Filtering
- Many2Many editing - Many2Many editing
- One2Many editor
- File admin - File admin
- Documentation - Documentation
- Examples - Examples
......
...@@ -37,6 +37,9 @@ class Post(db.Model): ...@@ -37,6 +37,9 @@ class Post(db.Model):
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='posts') user = db.relationship(User, backref='posts')
def __unicode__(self):
return self.title
# Flask routes # Flask routes
@app.route('/') @app.route('/')
......
from sqlalchemy.orm.properties import RelationshipProperty, ColumnProperty from sqlalchemy.orm.properties import RelationshipProperty, ColumnProperty
from sqlalchemy.orm.interfaces import MANYTOONE from sqlalchemy.orm.interfaces import MANYTOONE, ONETOMANY
from sqlalchemy.orm.attributes import InstrumentedAttribute from sqlalchemy.orm.attributes import InstrumentedAttribute
from sqlalchemy.sql.expression import desc from sqlalchemy.sql.expression import desc
from wtforms.ext.sqlalchemy.orm import model_form, ModelConverter from wtforms.ext.sqlalchemy.orm import model_form, ModelConverter
from wtforms.ext.sqlalchemy.fields import QuerySelectField from wtforms.ext.sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField
from flask import flash from flask import flash
from flaskext import wtf from flaskext import wtf
from flask.ext.adminex.model import BaseModelView from flask.ext.adminex.model import BaseModelView
from flask.ext.adminex.form import AdminForm
class AdminModelConverter(ModelConverter): class AdminModelConverter(ModelConverter):
""" """
SQLAlchemy model to form converter SQLAlchemy model to form converter
""" """
def __init__(self, session): def __init__(self, view):
super(AdminModelConverter, self).__init__() super(AdminModelConverter, self).__init__()
self.session = session self.view = view
def convert(self, model, mapper, prop, field_args): def convert(self, model, mapper, prop, field_args):
if isinstance(prop, RelationshipProperty): if isinstance(prop, RelationshipProperty):
local_column = prop.local_remote_pairs[0][0]
remote_model = prop.mapper.class_
kwargs = { kwargs = {
'validators': [], 'validators': [],
'filters': [], 'filters': [],
'allow_blank': local_column.nullable,
'default': None 'default': None
} }
if field_args: if field_args:
kwargs.update(field_args) kwargs.update(field_args)
if prop.direction is MANYTOONE: def query_factory():
def query_factory(): return self.view.session.query(remote_model)
return self.session.query(prop.argument)
if prop.direction is MANYTOONE:
return QuerySelectField(query_factory=query_factory, **kwargs) return QuerySelectField(query_factory=query_factory, **kwargs)
elif prop.direction is ONETOMANY:
# Skip backrefs
if not local_column.foreign_keys and self.view.hide_backrefs:
return None
return QuerySelectMultipleField(query_factory=query_factory, **kwargs)
else: else:
# Ignore pk/fk # Ignore pk/fk
if isinstance(prop, ColumnProperty): if isinstance(prop, ColumnProperty):
...@@ -58,6 +69,12 @@ class ModelView(BaseModelView): ...@@ -58,6 +69,12 @@ class ModelView(BaseModelView):
admin = ModelView(User, db.session) admin = ModelView(User, db.session)
""" """
hide_backrefs = True
"""
Set this to False if you want to see multiselect for model backrefs.
"""
def __init__(self, model, session, def __init__(self, model, session,
name=None, category=None, endpoint=None, url=None): name=None, category=None, endpoint=None, url=None):
""" """
...@@ -115,9 +132,14 @@ class ModelView(BaseModelView): ...@@ -115,9 +132,14 @@ class ModelView(BaseModelView):
for p in mapper.iterate_properties: for p in mapper.iterate_properties:
if isinstance(p, ColumnProperty): if isinstance(p, ColumnProperty):
# TODO: Check for multiple columns # Sanity check
if len(p.columns) > 1:
raise Exception('Automatic form scaffolding is not supported' +
' for multi-column properties (%s.%s)' % (self.model.__name__, p.key))
column = p.columns[0] column = p.columns[0]
# Can't sort by on primary and foreign keys by default
if column.foreign_keys or column.primary_key: if column.foreign_keys or column.primary_key:
continue continue
...@@ -130,10 +152,10 @@ class ModelView(BaseModelView): ...@@ -130,10 +152,10 @@ class ModelView(BaseModelView):
Create form from the model. Create form from the model.
""" """
return model_form(self.model, return model_form(self.model,
wtf.Form, AdminForm,
self.form_columns, self.form_columns,
field_args=self.form_args, field_args=self.form_args,
converter=AdminModelConverter(self.session)) converter=AdminModelConverter(self))
# Database-related API # Database-related API
def get_list(self, page, sort_column, sort_desc, execute=True): def get_list(self, page, sort_column, sort_desc, execute=True):
......
from flask.ext import wtf
class AdminForm(wtf.Form):
@property
def has_file_field(self):
# TODO: Optimize me
for f in self:
if isinstance(f, wtf.FileField):
return True
return False
...@@ -220,7 +220,7 @@ class BaseModelView(BaseView): ...@@ -220,7 +220,7 @@ class BaseModelView(BaseView):
def scaffold_form(self): def scaffold_form(self):
""" """
Create WTForm class from the model. Must be implemented in Create `form.AdminForm` class from the model. Must be implemented in
the child class. the child class.
""" """
raise NotImplemented('Please implement scaffold_form method') raise NotImplemented('Please implement scaffold_form method')
......
This diff is collapsed.
This diff is collapsed.
...@@ -2,14 +2,18 @@ ...@@ -2,14 +2,18 @@
<html> <html>
<head> <head>
<title>{% block title %}{% if view.category %}{{ view.category }} - {% endif %}{{ view.name }} - {{ view.admin.name }}{% endblock %}</title> <title>{% block title %}{% if view.category %}{{ view.category }} - {% endif %}{{ view.name }} - {{ view.admin.name }}{% endblock %}</title>
{% block head %} {% block head_meta %}
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content=""> <meta name="description" content="">
<meta name="author" content=""> <meta name="author" content="">
{% endblock %}
{% block head_css %}
<link href="{{ url_for('admin.static', filename='bootstrap/css/bootstrap.css') }}" rel="stylesheet"> <link href="{{ url_for('admin.static', filename='bootstrap/css/bootstrap.css') }}" rel="stylesheet">
<link href="{{ url_for('admin.static', filename='bootstrap/css/bootstrap-responsive.css') }}" rel="stylesheet"> <link href="{{ url_for('admin.static', filename='bootstrap/css/bootstrap-responsive.css') }}" rel="stylesheet">
<link href="{{ url_for('admin.static', filename='css/admin.css') }}" rel="stylesheet"> <link href="{{ url_for('admin.static', filename='css/admin.css') }}" rel="stylesheet">
{% endblock %} {% endblock %}
{% block head %}
{% endblock %}
</head> </head>
<body> <body>
{% block page_body %} {% block page_body %}
...@@ -68,5 +72,9 @@ ...@@ -68,5 +72,9 @@
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script>
<script src="{{ url_for('admin.static', filename='bootstrap/js/bootstrap.min.js') }}" type="text/javascript"></script> <script src="{{ url_for('admin.static', filename='bootstrap/js/bootstrap.min.js') }}" type="text/javascript"></script>
<script src="{{ url_for('admin.static', filename='chosen/chosen.jquery.min.js') }}" type="text/javascript"></script>
{% block tail %}
{% endblock %}
</body> </body>
</html> </html>
{% extends 'admin/master.html' %} {% extends 'admin/master.html' %}
{% block head %}
<link href="{{ url_for('admin.static', filename='chosen/chosen.css') }}" rel="stylesheet">
{% endblock %}
{% block body %} {% block body %}
<form action="" method="POST" class="form-horizontal"> <form action="" method="POST" class="form-horizontal"{% if form.has_file_field %} enctype="multipart/form-data"{% endif %}>
<fieldset> <fieldset>
{{ form.csrf }} {{ form.csrf }}
{% for f in form if f.label.text != 'Csrf' %} {% for f in form if f.label.text != 'Csrf' %}
{% if f.name in form.errors %} <div class="control-group{% if f.errors %} error{% endif %}">
<div class="control-group error">
{% else %}
<div class="control-group">
{% endif %}
{{ f.label(class='control-label') }} {{ f.label(class='control-label') }}
<div class="controls"> <div class="controls">
<div> <div>
{{ f }} {{ f }}
</div> </div>
{% if f.name in form.errors %} {% if f.errors %}
<ul> <ul>
{% for e in form.errors[f.name] %} {% for e in f.errors %}
<li>{{ e }}</li> <li>{{ e }}</li>
{% endfor %} {% endfor %}
</ul> </ul>
...@@ -35,3 +35,9 @@ ...@@ -35,3 +35,9 @@
</fieldset> </fieldset>
</form> </form>
{% endblock %} {% endblock %}
{% block tail %}
<script>
$("select").chosen({allow_single_deselect: true});
</script>
{% endblock %}
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