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

Restyled edit form, more documentation.

parent 80b8377d
......@@ -2,9 +2,9 @@
- Pregenerate URLs for menu
- Override base URL (/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
- Sort by foreign key
- Validation of the joins in the query
- Automatic joined load for foreign keys
- Automatic PK detection
......
from flask import Flask
from flaskext.sqlalchemy import SQLAlchemy
from flask.ext import adminex
from flask.ext import adminex, wtf
from flask.ext.adminex.ext import sqlamodel
# Create application
......@@ -52,6 +52,10 @@ class PostAdmin(sqlamodel.ModelView):
sortable_columns = ('title', ('user', User.username))
rename_columns = dict(title='Tiiitle')
form_args = dict(
text=dict(label='Big Text', validators=[wtf.required()])
)
def __init__(self, session):
super(PostAdmin, self).__init__(Post, session)
......
......@@ -84,6 +84,7 @@ class BaseView(object):
def __init__(self, name=None, category=None, endpoint=None, url=None, static_folder=None):
"""
Constructor.
`name`
Name of this view. If not provided, will be defaulted to the class name.
`category`
......@@ -111,6 +112,9 @@ class BaseView(object):
def _set_admin(self, admin):
"""
Associate this view with Admin class instance.
`admin`
Admin instance
"""
self.admin = admin
......@@ -145,6 +149,9 @@ class BaseView(object):
def _prettify_name(self, name):
"""
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)
......@@ -176,6 +183,12 @@ class AdminIndexView(BaseView):
return render_template('adminhome.html')
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):
super(AdminIndexView, self).__init__(name or 'Home', category, endpoint or 'admin', url or '/admin', 'static')
......@@ -186,6 +199,9 @@ class AdminIndexView(BaseView):
class MenuItem(object):
"""
Simple menu tree hierarchy.
"""
def __init__(self, name, view=None):
self.name = name
self._view = view
......@@ -204,6 +220,7 @@ class MenuItem(object):
if self._view is None:
return None
# TODO: Optimize me
return url_for('%s.%s' % (self._view.endpoint, self._view._default_view))
def is_active(self, view):
......@@ -237,7 +254,7 @@ class Admin(object):
Constructor.
`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`
Home page view to use. If not provided, will use `AdminIndexView`.
"""
......@@ -245,7 +262,7 @@ class Admin(object):
self._menu = []
if name is None:
name = 'Flask'
name = 'Admin'
self.name = name
if index_view is None:
......
......@@ -14,6 +14,9 @@ from flask.ext.adminex.model import BaseModelView
class AdminModelConverter(ModelConverter):
"""
SQLAlchemy model to form converter
"""
def __init__(self, session):
super(AdminModelConverter, self).__init__()
......@@ -48,14 +51,40 @@ class AdminModelConverter(ModelConverter):
class ModelView(BaseModelView):
"""
SQLALchemy model view
Usage sample::
admin = ModelView(User, db.session)
"""
def __init__(self, model, session,
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
super(ModelView, self).__init__(model, name, category, endpoint, url)
# Public API
# Scaffolding
def scaffold_list_columns(self):
"""
Return list of columns from the model.
"""
columns = []
mapper = self.model._sa_class_manager.mapper
......@@ -76,6 +105,10 @@ class ModelView(BaseModelView):
return columns
def scaffold_sortable_columns(self):
"""
Return dictionary of sortable columns.
Key is column name, value is sort column/field.
"""
columns = dict()
mapper = self.model._sa_class_manager.mapper
......@@ -93,13 +126,29 @@ class ModelView(BaseModelView):
return columns
def scaffold_form(self):
"""
Create form from the model.
"""
return model_form(self.model,
wtf.Form,
self.form_columns,
field_args=self.form_args,
converter=AdminModelConverter(self.session))
# Database-related API
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)
count = query.count()
......@@ -133,16 +182,29 @@ class ModelView(BaseModelView):
query = query.limit(self.page_size)
# Execute if needed
if execute:
query = query.all()
return count, query
def get_one(self, id):
"""
Return one model by its id.
`id`
Model
"""
return self.session.query(self.model).get(id)
# Model handlers
def create_model(self, form):
"""
Create model from form.
`form`
Form instance
"""
try:
model = self.model()
form.populate_obj(model)
......@@ -154,6 +216,12 @@ class ModelView(BaseModelView):
return False
def update_model(self, form, model):
"""
Update model from form.
`form`
Form instance
"""
try:
form.populate_obj(model)
self.session.commit()
......@@ -163,5 +231,11 @@ class ModelView(BaseModelView):
return False
def delete_model(self, model):
"""
Delete model.
`model`
Model to delete
"""
self.session.delete(model)
self.session.commit()
......@@ -96,6 +96,19 @@ class BaseModelView(BaseView):
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
page_size = 20
"""
......@@ -343,7 +356,7 @@ class BaseModelView(BaseView):
`name`
Name to prettify
"""
return ' '.join(x.capitalize() for x in name.split('_'))
return name.replace('_', ' ').title()
# URL generation helper
def _get_extra_args(self):
......@@ -434,6 +447,9 @@ class BaseModelView(BaseView):
@expose('/new/', methods=('GET', 'POST'))
def create_view(self):
"""
Create model view
"""
return_url = request.args.get('return')
if not self.can_create:
......@@ -449,6 +465,9 @@ class BaseModelView(BaseView):
@expose('/edit/<int:id>/', methods=('GET', 'POST'))
def edit_view(self, id):
"""
Edit model view
"""
return_url = request.args.get('return')
if not self.can_edit:
......@@ -472,6 +491,9 @@ class BaseModelView(BaseView):
@expose('/delete/<int:id>/')
def delete_view(self, id):
"""
Delete model view
"""
return_url = request.args.get('return')
# TODO: Use post
......
{% extends 'admin/master.html' %}
{% block body %}
<form action="" method="POST">
<form action="" method="POST" class="form-horizontal">
<fieldset>
{{ form.csrf }}
<table class="form">
{% for f in form if f.label.text != 'Csrf' %}
<tr>
<td>
{{ f.label }}
</td>
<td>
{% if f.name in form.errors %}
<div class="control-group error">
{% else %}
<div class="control-group">
{% endif %}
{{ f.label(class='control-label') }}
<div class="controls">
<div>
{{ f }}
</td>
</tr>
{% endfor %}
<tr>
<td>
</td>
<td>
{% if form.errors %}
</div>
{% if f.name in form.errors %}
<ul>
{% for fn, fe in form.errors.items() if fe %}
{% for e in fe %}
{% for e in form.errors[f.name] %}
<li>{{ e }}</li>
{% endfor %}
{% endfor %}
</ul>
{% endif %}
</div>
</div>
{% endfor %}
<div class="control-group">
<div class="controls">
<input type="submit" class="btn btn-primary btn-large" />
<a href="{{ return_url }}" class="btn btn-large">Cancel</a>
</td>
</tr>
</table>
</div>
</div>
</fieldset>
</form>
{% endblock %}
......@@ -2,6 +2,16 @@ import sys, traceback
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:
__import__(name, globals(), locals(), [])
except ImportError:
......@@ -14,9 +24,16 @@ def import_module(name, required=True):
def import_attribute(name):
"""
Import attribute using string reference.
Example:
import_attribute('a.b.c.foo')
`name`
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)
module = __import__(path, globals(), locals(), [attr])
......@@ -25,13 +42,15 @@ def import_attribute(name):
def module_not_found(additional_depth=0):
'''Checks if ImportError was raised because module does not exist or
"""
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`
supply int of depth of your call if you're not doing
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]
if len(traceback.extract_tb(tb)) > (1 + additional_depth):
return False
......@@ -39,8 +58,19 @@ def module_not_found(additional_depth=0):
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:
return reduce(getattr, attr.split('.'), obj)
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