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.csrf }}
<table class="form">
<form action="" method="POST" class="form-horizontal">
<fieldset>
{{ form.csrf }}
{% for f in form if f.label.text != 'Csrf' %}
<tr>
<td>
{{ f.label }}
</td>
<td>
{{ f }}
</td>
</tr>
{% 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 }}
</div>
{% if f.name in form.errors %}
<ul>
{% for e in form.errors[f.name] %}
<li>{{ e }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
{% endfor %}
<tr>
<td>
</td>
<td>
{% if form.errors %}
<ul>
{% for fn, fe in form.errors.items() if fe %}
{% 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>
<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>
</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:
......@@ -13,10 +23,17 @@ def import_module(name, required=True):
def import_attribute(name):
"""
Import attribute using string reference.
Example:
import_attribute('a.b.c.foo')
Throws ImportError or AttributeError if module or attribute do not exist.
Import attribute using string reference.
`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
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
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
'''
`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