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

Restyled edit form, more documentation.

parent 80b8377d
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
- Pregenerate URLs for menu - Pregenerate URLs for menu
- 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
- Sort by foreign key
- 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
......
from flask import Flask from flask import Flask
from flaskext.sqlalchemy import SQLAlchemy from flaskext.sqlalchemy import SQLAlchemy
from flask.ext import adminex from flask.ext import adminex, wtf
from flask.ext.adminex.ext import sqlamodel from flask.ext.adminex.ext import sqlamodel
# Create application # Create application
...@@ -52,6 +52,10 @@ class PostAdmin(sqlamodel.ModelView): ...@@ -52,6 +52,10 @@ class PostAdmin(sqlamodel.ModelView):
sortable_columns = ('title', ('user', User.username)) sortable_columns = ('title', ('user', User.username))
rename_columns = dict(title='Tiiitle') rename_columns = dict(title='Tiiitle')
form_args = dict(
text=dict(label='Big Text', validators=[wtf.required()])
)
def __init__(self, session): def __init__(self, session):
super(PostAdmin, self).__init__(Post, session) super(PostAdmin, self).__init__(Post, session)
......
...@@ -84,6 +84,7 @@ class BaseView(object): ...@@ -84,6 +84,7 @@ class BaseView(object):
def __init__(self, name=None, category=None, endpoint=None, url=None, static_folder=None): def __init__(self, name=None, category=None, endpoint=None, url=None, static_folder=None):
""" """
Constructor. Constructor.
`name` `name`
Name of this view. If not provided, will be defaulted to the class name. Name of this view. If not provided, will be defaulted to the class name.
`category` `category`
...@@ -111,6 +112,9 @@ class BaseView(object): ...@@ -111,6 +112,9 @@ class BaseView(object):
def _set_admin(self, admin): def _set_admin(self, admin):
""" """
Associate this view with Admin class instance. Associate this view with Admin class instance.
`admin`
Admin instance
""" """
self.admin = admin self.admin = admin
...@@ -145,6 +149,9 @@ class BaseView(object): ...@@ -145,6 +149,9 @@ class BaseView(object):
def _prettify_name(self, name): def _prettify_name(self, name):
""" """
Prettify class name by splitting name by capital characters. So, 'MySuperClass' will look like 'My Super Class' Prettify class name by splitting name by capital characters. So, 'MySuperClass' will look like 'My Super Class'
`name`
String to prettify
""" """
return sub(r'(?<=.)([A-Z])', r' \1', name) return sub(r'(?<=.)([A-Z])', r' \1', name)
...@@ -176,6 +183,12 @@ class AdminIndexView(BaseView): ...@@ -176,6 +183,12 @@ class AdminIndexView(BaseView):
return render_template('adminhome.html') return render_template('adminhome.html')
admin = Admin(index_view=MyHomeView) admin = Admin(index_view=MyHomeView)
By default, has following rules:
1. If name is not provided, will use 'Home'
2. If endpoint is not provided, will use 'admin'
3. If url is not provided, will use '/admin'
4. Automatically associates with static folder.
""" """
def __init__(self, name=None, category=None, endpoint=None, url=None): def __init__(self, name=None, category=None, endpoint=None, url=None):
super(AdminIndexView, self).__init__(name or 'Home', category, endpoint or 'admin', url or '/admin', 'static') super(AdminIndexView, self).__init__(name or 'Home', category, endpoint or 'admin', url or '/admin', 'static')
...@@ -186,6 +199,9 @@ class AdminIndexView(BaseView): ...@@ -186,6 +199,9 @@ class AdminIndexView(BaseView):
class MenuItem(object): class MenuItem(object):
"""
Simple menu tree hierarchy.
"""
def __init__(self, name, view=None): def __init__(self, name, view=None):
self.name = name self.name = name
self._view = view self._view = view
...@@ -204,6 +220,7 @@ class MenuItem(object): ...@@ -204,6 +220,7 @@ class MenuItem(object):
if self._view is None: if self._view is None:
return None return None
# TODO: Optimize me
return url_for('%s.%s' % (self._view.endpoint, self._view._default_view)) return url_for('%s.%s' % (self._view.endpoint, self._view._default_view))
def is_active(self, view): def is_active(self, view):
...@@ -237,7 +254,7 @@ class Admin(object): ...@@ -237,7 +254,7 @@ class Admin(object):
Constructor. Constructor.
`name` `name`
Application name. Will be displayed in main menu and as a page title. If not provided, defaulted to "Flask" Application name. Will be displayed in main menu and as a page title. If not provided, defaulted to "Admin"
`index_view` `index_view`
Home page view to use. If not provided, will use `AdminIndexView`. Home page view to use. If not provided, will use `AdminIndexView`.
""" """
...@@ -245,7 +262,7 @@ class Admin(object): ...@@ -245,7 +262,7 @@ class Admin(object):
self._menu = [] self._menu = []
if name is None: if name is None:
name = 'Flask' name = 'Admin'
self.name = name self.name = name
if index_view is None: if index_view is None:
......
...@@ -14,6 +14,9 @@ from flask.ext.adminex.model import BaseModelView ...@@ -14,6 +14,9 @@ from flask.ext.adminex.model import BaseModelView
class AdminModelConverter(ModelConverter): class AdminModelConverter(ModelConverter):
"""
SQLAlchemy model to form converter
"""
def __init__(self, session): def __init__(self, session):
super(AdminModelConverter, self).__init__() super(AdminModelConverter, self).__init__()
...@@ -48,14 +51,40 @@ class AdminModelConverter(ModelConverter): ...@@ -48,14 +51,40 @@ class AdminModelConverter(ModelConverter):
class ModelView(BaseModelView): class ModelView(BaseModelView):
"""
SQLALchemy model view
Usage sample::
admin = ModelView(User, db.session)
"""
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):
"""
Constructor.
`model`
Model class
`session`
SQLALchemy session
`name`
View name. If not set, will default to model name
`category`
Category name
`endpoint`
Endpoint name. If not set, will default to model name
`url`
Base URL. If not set, will default to '/admin/' + endpoint
"""
self.session = session self.session = session
super(ModelView, self).__init__(model, name, category, endpoint, url) super(ModelView, self).__init__(model, name, category, endpoint, url)
# Public API # Scaffolding
def scaffold_list_columns(self): def scaffold_list_columns(self):
"""
Return list of columns from the model.
"""
columns = [] columns = []
mapper = self.model._sa_class_manager.mapper mapper = self.model._sa_class_manager.mapper
...@@ -76,6 +105,10 @@ class ModelView(BaseModelView): ...@@ -76,6 +105,10 @@ class ModelView(BaseModelView):
return columns return columns
def scaffold_sortable_columns(self): def scaffold_sortable_columns(self):
"""
Return dictionary of sortable columns.
Key is column name, value is sort column/field.
"""
columns = dict() columns = dict()
mapper = self.model._sa_class_manager.mapper mapper = self.model._sa_class_manager.mapper
...@@ -93,13 +126,29 @@ class ModelView(BaseModelView): ...@@ -93,13 +126,29 @@ class ModelView(BaseModelView):
return columns return columns
def scaffold_form(self): def scaffold_form(self):
"""
Create form from the model.
"""
return model_form(self.model, return model_form(self.model,
wtf.Form, wtf.Form,
self.form_columns, self.form_columns,
field_args=self.form_args,
converter=AdminModelConverter(self.session)) converter=AdminModelConverter(self.session))
# 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):
"""
Return models from the database.
`page`
Page number
`sort_column`
Sort column name
`sort_desc`
Descending or ascending sort
`execute`
Execute query immediately? Default is `True`
"""
query = self.session.query(self.model) query = self.session.query(self.model)
count = query.count() count = query.count()
...@@ -133,16 +182,29 @@ class ModelView(BaseModelView): ...@@ -133,16 +182,29 @@ class ModelView(BaseModelView):
query = query.limit(self.page_size) query = query.limit(self.page_size)
# Execute if needed
if execute: if execute:
query = query.all() query = query.all()
return count, query return count, query
def get_one(self, id): def get_one(self, id):
"""
Return one model by its id.
`id`
Model
"""
return self.session.query(self.model).get(id) return self.session.query(self.model).get(id)
# Model handlers # Model handlers
def create_model(self, form): def create_model(self, form):
"""
Create model from form.
`form`
Form instance
"""
try: try:
model = self.model() model = self.model()
form.populate_obj(model) form.populate_obj(model)
...@@ -154,6 +216,12 @@ class ModelView(BaseModelView): ...@@ -154,6 +216,12 @@ class ModelView(BaseModelView):
return False return False
def update_model(self, form, model): def update_model(self, form, model):
"""
Update model from form.
`form`
Form instance
"""
try: try:
form.populate_obj(model) form.populate_obj(model)
self.session.commit() self.session.commit()
...@@ -163,5 +231,11 @@ class ModelView(BaseModelView): ...@@ -163,5 +231,11 @@ class ModelView(BaseModelView):
return False return False
def delete_model(self, model): def delete_model(self, model):
"""
Delete model.
`model`
Model to delete
"""
self.session.delete(model) self.session.delete(model)
self.session.commit() self.session.commit()
...@@ -96,6 +96,19 @@ class BaseModelView(BaseView): ...@@ -96,6 +96,19 @@ class BaseModelView(BaseView):
list_columns = ('name', 'email') list_columns = ('name', 'email')
""" """
form_args = None
"""
Dictionary of form field arguments. Refer to WTForm documentation on
list of possible options.
Example::
class MyModelView(BaseModelView):
form_args = dict(
name=dict(label='First Name', validators=[wtf.required()])
}
"""
# Various settings # Various settings
page_size = 20 page_size = 20
""" """
...@@ -343,7 +356,7 @@ class BaseModelView(BaseView): ...@@ -343,7 +356,7 @@ class BaseModelView(BaseView):
`name` `name`
Name to prettify Name to prettify
""" """
return ' '.join(x.capitalize() for x in name.split('_')) return name.replace('_', ' ').title()
# URL generation helper # URL generation helper
def _get_extra_args(self): def _get_extra_args(self):
...@@ -434,6 +447,9 @@ class BaseModelView(BaseView): ...@@ -434,6 +447,9 @@ class BaseModelView(BaseView):
@expose('/new/', methods=('GET', 'POST')) @expose('/new/', methods=('GET', 'POST'))
def create_view(self): def create_view(self):
"""
Create model view
"""
return_url = request.args.get('return') return_url = request.args.get('return')
if not self.can_create: if not self.can_create:
...@@ -449,6 +465,9 @@ class BaseModelView(BaseView): ...@@ -449,6 +465,9 @@ class BaseModelView(BaseView):
@expose('/edit/<int:id>/', methods=('GET', 'POST')) @expose('/edit/<int:id>/', methods=('GET', 'POST'))
def edit_view(self, id): def edit_view(self, id):
"""
Edit model view
"""
return_url = request.args.get('return') return_url = request.args.get('return')
if not self.can_edit: if not self.can_edit:
...@@ -472,6 +491,9 @@ class BaseModelView(BaseView): ...@@ -472,6 +491,9 @@ class BaseModelView(BaseView):
@expose('/delete/<int:id>/') @expose('/delete/<int:id>/')
def delete_view(self, id): def delete_view(self, id):
"""
Delete model view
"""
return_url = request.args.get('return') return_url = request.args.get('return')
# TODO: Use post # TODO: Use post
......
{% extends 'admin/master.html' %} {% extends 'admin/master.html' %}
{% block body %} {% block body %}
<form action="" method="POST"> <form action="" method="POST" class="form-horizontal">
{{ form.csrf }} <fieldset>
<table class="form"> {{ form.csrf }}
{% for f in form if f.label.text != 'Csrf' %} {% for f in form if f.label.text != 'Csrf' %}
<tr> {% if f.name in form.errors %}
<td> <div class="control-group error">
{{ f.label }} {% else %}
</td> <div class="control-group">
<td> {% endif %}
{{ f }} {{ f.label(class='control-label') }}
</td> <div class="controls">
</tr> <div>
{{ f }}
</div>
{% if f.name in form.errors %}
<ul>
{% for e in form.errors[f.name] %}
<li>{{ e }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
{% endfor %} {% endfor %}
<tr> <div class="control-group">
<td> <div class="controls">
</td> <input type="submit" class="btn btn-primary btn-large" />
<td> <a href="{{ return_url }}" class="btn btn-large">Cancel</a>
{% if form.errors %} </div>
<ul> </div>
{% for fn, fe in form.errors.items() if fe %} </fieldset>
{% for e in fe %}
<li>{{ e }}</li>
{% endfor %}
{% endfor %}
</ul>
{% endif %}
<input type="submit" class="btn btn-primary btn-large" />
<a href="{{ return_url }}" class="btn btn-large">Cancel</a>
</td>
</tr>
</table>
</form> </form>
{% endblock %} {% endblock %}
...@@ -2,6 +2,16 @@ import sys, traceback ...@@ -2,6 +2,16 @@ import sys, traceback
def import_module(name, required=True): def import_module(name, required=True):
"""
Import module by name
`name`
Module name
`required`
If set to `True` and module was not found - will throw exception.
If set to `False` and module was not found - will return None.
Default is `True`.
"""
try: try:
__import__(name, globals(), locals(), []) __import__(name, globals(), locals(), [])
except ImportError: except ImportError:
...@@ -13,10 +23,17 @@ def import_module(name, required=True): ...@@ -13,10 +23,17 @@ def import_module(name, required=True):
def import_attribute(name): def import_attribute(name):
""" """
Import attribute using string reference. Import attribute using string reference.
Example:
import_attribute('a.b.c.foo') `name`
Throws ImportError or AttributeError if module or attribute do not exist. String reference.
Throws ImportError or AttributeError if module or attribute do not exist.
Example::
import_attribute('a.b.c.foo')
""" """
path, attr = name.rsplit('.', 1) path, attr = name.rsplit('.', 1)
module = __import__(path, globals(), locals(), [attr]) module = __import__(path, globals(), locals(), [attr])
...@@ -25,13 +42,15 @@ def import_attribute(name): ...@@ -25,13 +42,15 @@ def import_attribute(name):
def module_not_found(additional_depth=0): def module_not_found(additional_depth=0):
'''Checks if ImportError was raised because module does not exist or """
something inside it raised ImportError Checks if ImportError was raised because module does not exist or
something inside it raised ImportError
- additional_depth - supply int of depth of your call if you're not doing `additional_depth`
import on the same level of code - f.e., if you call function, which is supply int of depth of your call if you're not doing
doing import, you should pass 1 for single additional level of depth import on the same level of code - f.e., if you call function, which is
''' doing import, you should pass 1 for single additional level of depth
"""
tb = sys.exc_info()[2] tb = sys.exc_info()[2]
if len(traceback.extract_tb(tb)) > (1 + additional_depth): if len(traceback.extract_tb(tb)) > (1 + additional_depth):
return False return False
...@@ -39,8 +58,19 @@ def module_not_found(additional_depth=0): ...@@ -39,8 +58,19 @@ def module_not_found(additional_depth=0):
def rec_getattr(obj, attr, default=None): def rec_getattr(obj, attr, default=None):
"""
Recursive getattr.
`attr`
Dot delimited attribute name
`default`
Default value
Example::
rec_getattr(obj, 'a.b.c')
"""
try: try:
return reduce(getattr, attr.split('.'), obj) return reduce(getattr, attr.split('.'), obj)
except AttributeError: except AttributeError:
return None return default
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