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

Refactored model admin.

parent 4049b93a
- Core
- Pregenerate URLs for menu
- Page titles
- Create base model-admin class and derive SQLA admin from it
- SQLA Model Admin
- Ability to override displayed columns in a list
- Ability to override displayed form fields
- Ability to provide form validators without messing with form creation
- Ability to rename list columns
- Ability to change form without messing with form creation
- Filtering
- Many2Many editing
- Many2One editor
......
......@@ -44,11 +44,17 @@ def index():
return '<a href="/admin/">Click me to get to Admin!</a>'
class PostAdmin(sqlamodel.ModelView):
list_columns = ('title', 'user')
def __init__(self, session):
super(PostAdmin, self).__init__(Post, session)
if __name__ == '__main__':
# Create admin
admin = adminex.Admin()
admin.add_view(sqlamodel.ModelView(User, db.session, category='News'))
admin.add_view(sqlamodel.ModelView(Post, db.session, category='News'))
admin = adminex.Admin('Simple Models')
admin.add_view(sqlamodel.ModelView(User, db.session))
admin.add_view(PostAdmin(db.session))
admin.apply(app)
# Create DB
......
......@@ -117,8 +117,7 @@ class AdminIndexView(BaseView):
return render_template('admin/index.html', view=self)
class Admin(object):
class MenuItem(object):
class MenuItem(object):
def __init__(self, name, view=None):
self.name = name
self._view = view
......@@ -160,10 +159,16 @@ class Admin(object):
def __repr__(self):
return 'MenuItem %s (%s)' % (self.name, repr(self._children))
def __init__(self, index_view=None):
class Admin(object):
def __init__(self, name=None, index_view=None):
self._views = []
self._menu = []
if name is None:
name = 'Flask'
self.name = name
if index_view is None:
index_view = AdminIndexView()
......@@ -189,16 +194,16 @@ class Admin(object):
for v in self._views:
if v.category is None:
self._menu.append(self.MenuItem(v.name, v))
self._menu.append(MenuItem(v.name, v))
else:
category = categories.get(v.category)
if category is None:
category = self.MenuItem(v.category)
category = MenuItem(v.category)
categories[v.category] = category
self._menu.append(category)
category.add_child(self.MenuItem(v.name, v))
category.add_child(MenuItem(v.name, v))
def menu(self):
return self._menu
from sqlalchemy.orm.properties import RelationshipProperty, ColumnProperty
from sqlalchemy.orm.interfaces import MANYTOONE
from sqlalchemy.sql.expression import desc
from sqlalchemy import exc
from wtforms.ext.sqlalchemy.orm import model_form, ModelConverter
from wtforms.ext.sqlalchemy.fields import QuerySelectField
from flask import request, render_template, url_for, redirect, flash
from flask import flash
from flaskext import wtf
from flask.ext.adminex import BaseView, expose
from flask.ext.adminex.model import BaseModelView
class AdminModelConverter(ModelConverter):
......@@ -46,42 +45,17 @@ class AdminModelConverter(ModelConverter):
return super(AdminModelConverter, self).convert(model, mapper, prop, field_args)
class ModelView(BaseView):
# Permissions
can_create = True
can_edit = True
can_delete = True
# Templates
list_template = 'admin/model/list.html'
edit_template = 'admin/model/edit.html'
create_template = 'admin/model/edit.html'
# Various settings
page_size = 3
class ModelView(BaseModelView):
def __init__(self, model, session, name=None, category=None, endpoint=None, url=None):
# If name not provided, it is modelname
if name is None:
name = '%s' % self._prettify_name(model.__name__)
# If endpoint not provided, it is modelname + 'view'
if endpoint is None:
endpoint = ('%sview' % model.__name__).lower()
super(ModelView, self).__init__(name, category, endpoint, url)
self.session = session
self.model = model
# Scaffolding
self.list_columns = self.get_list_columns()
self.create_form = self.scaffold_create_form()
self.edit_form = self.scaffold_edit_form()
super(ModelView, self).__init__(model, name, category, endpoint, url)
# Public API
def get_list_columns(self):
columns = self.list_columns
if columns is None:
mapper = self.model._sa_class_manager.mapper
columns = []
......@@ -102,17 +76,31 @@ class ModelView(BaseView):
return [(c, self.prettify_name(c)) for c in columns]
def scaffold_form(self):
return model_form(self.model, wtf.Form, None, converter=AdminModelConverter(self.session))
return model_form(self.model, wtf.Form, self.form_columns, converter=AdminModelConverter(self.session))
def scaffold_create_form(self):
return self.scaffold_form()
# Database-related API
def get_list(self, page, sort_column, sort_desc):
query = self.session.query(self.model)
def scaffold_edit_form(self):
return self.scaffold_form()
count = query.count()
# Database-related API
def get_query(self):
return self.session.query(self.model)
if sort_column is not None:
# Validate first
if sort_column >= 0 and sort_column < len(self.list_columns):
if sort_desc:
query = query.order_by(desc(self.list_columns[sort_column][0]))
else:
query = query.order_by(self.list_columns[sort_column][0])
if page is not None:
if page < 1:
page = 1
query = query.offset((page - 1) * self.page_size)
query = query.limit(self.page_size)
return count, query.all()
def get_one(self, id):
return self.session.query(self.model).get(id)
......@@ -126,7 +114,6 @@ class ModelView(BaseView):
self.session.commit()
return True
except Exception, ex:
# TODO: Error logging
flash('Failed to create model. ' + str(ex), 'error')
return False
......@@ -142,128 +129,3 @@ class ModelView(BaseView):
def delete_model(self, model):
self.session.delete(model)
self.session.commit()
# Various helpers
def prettify_name(self, name):
return ' '.join(x.capitalize() for x in name.split('_'))
# URL generation helper
def _get_extra_args(self):
page = request.args.get('page', None, type=int)
sort = request.args.get('sort', None, type=int)
sort_desc = request.args.get('desc', None, type=int)
return page, sort, sort_desc
def _get_url(self, view, page, sort, sort_desc):
return url_for(view, page=page, sort=sort, desc=sort_desc)
# Views
@expose('/')
def index_view(self):
data = self.get_query()
page, sort, sort_desc = self._get_extra_args()
# Sorting
if sort is not None:
# Validate first
if sort >= 0 and sort < len(self.list_columns):
if sort_desc:
data = data.order_by(desc(self.list_columns[sort][0]))
else:
data = data.order_by(self.list_columns[sort][0])
# Paging
count = data.count()
num_pages = count / self.page_size
if count % self.page_size != 0:
num_pages += 1
if page is not None:
if page < 1:
page = 1
data = data.offset((page - 1) * self.page_size)
data = data.limit(self.page_size)
# Various URL generation helpers
def pager_url(p):
return self._get_url('.index_view', p, sort, sort_desc)
def sort_url(column, invert=False):
desc = None
if invert and not sort_desc:
desc = 1
return self._get_url('.index_view', page, column, desc)
return render_template(self.list_template,
view=self,
data=data,
# Return URL
return_url=self._get_url('.index_view', page, sort, sort_desc),
# Pagination
pager_url=pager_url,
num_pages=num_pages,
page=page,
# Sorting
sort_column=sort,
sort_desc=sort_desc,
sort_url=sort_url
)
@expose('/new/', methods=('GET', 'POST'))
def create_view(self):
return_url = request.args.get('return')
if not self.can_create:
return redirect(return_url or url_for('.index_view'))
form = self.create_form(request.form)
if form.validate_on_submit():
if self.create_model(form):
return redirect(return_url or url_for('.index_view'))
return render_template(self.create_template, view=self, form=form)
@expose('/edit/<int:id>/', methods=('GET', 'POST'))
def edit_view(self, id):
return_url = request.args.get('return')
if not self.can_edit:
return redirect(return_url or url_for('.index_view'))
model = self.get_one(id)
if model is None:
return redirect(return_url or url_for('.index_view'))
form = self.edit_form(request.form, model)
if form.validate_on_submit():
if self.update_model(form, model):
return redirect(return_url or url_for('.index_view'))
return render_template(self.edit_template,
view=self,
form=form,
return_url=return_url or url_for('.index_view'))
@expose('/delete/<int:id>/')
def delete_view(self, id):
return_url = request.args.get('return')
# TODO: Use post
if not self.can_delete:
return redirect(return_url or url_for('.index_view'))
model = self.get_one(id)
if model:
self.delete_model(model)
return redirect(return_url or url_for('.index_view'))
from flask import request, url_for, render_template, redirect
from .base import BaseView, expose
class BaseModelView(BaseView):
# Permissions
can_create = True
can_edit = True
can_delete = True
# Templates
list_template = 'admin/model/list.html'
edit_template = 'admin/model/edit.html'
create_template = 'admin/model/edit.html'
# Customizations
list_columns = None
form_columns = None
# Various settings
page_size = 20
def __init__(self, model, name=None, category=None, endpoint=None, url=None):
# If name not provided, it is modelname
if name is None:
name = '%s' % self._prettify_name(model.__name__)
# If endpoint not provided, it is modelname + 'view'
if endpoint is None:
endpoint = ('%sview' % model.__name__).lower()
super(BaseModelView, self).__init__(name, category, endpoint, url)
self.model = model
# Scaffolding
self.list_columns = self.get_list_columns()
self.create_form = self.scaffold_create_form()
self.edit_form = self.scaffold_edit_form()
# Public API
def get_list_columns(self):
raise NotImplemented('Please implement get_list_columns method')
def scaffold_form(self):
raise NotImplemented('Please implement scaffold_form method')
def scaffold_create_form(self):
return self.scaffold_form()
def scaffold_edit_form(self):
return self.scaffold_form()
# Database-related API
def get_list(self, page, sort_field, sort_desc):
raise NotImplemented('Please implement get_list method')
def get_one(self, id):
raise NotImplemented('Please implement get_one method')
# Model handlers
def create_model(self, form):
raise NotImplemented()
def update_model(self, form, model):
raise NotImplemented()
def delete_model(self, model):
raise NotImplemented()
# Various helpers
def prettify_name(self, name):
return ' '.join(x.capitalize() for x in name.split('_'))
# URL generation helper
def _get_extra_args(self):
page = request.args.get('page', None, type=int)
sort = request.args.get('sort', None, type=int)
sort_desc = request.args.get('desc', None, type=int)
return page, sort, sort_desc
def _get_url(self, view, page, sort, sort_desc):
return url_for(view, page=page, sort=sort, desc=sort_desc)
# Views
@expose('/')
def index_view(self):
# Grab parameters from URL
page, sort, sort_desc = self._get_extra_args()
# Get count and data
count, data = self.get_list(page, sort, sort_desc)
# Calculate number of pages
num_pages = count / self.page_size
if count % self.page_size != 0:
num_pages += 1
# Various URL generation helpers
def pager_url(p):
return self._get_url('.index_view', p, sort, sort_desc)
def sort_url(column, invert=False):
desc = None
if invert and not sort_desc:
desc = 1
return self._get_url('.index_view', page, column, desc)
return render_template(self.list_template,
view=self,
data=data,
# Return URL
return_url=self._get_url('.index_view', page, sort, sort_desc),
# Pagination
pager_url=pager_url,
num_pages=num_pages,
page=page,
# Sorting
sort_column=sort,
sort_desc=sort_desc,
sort_url=sort_url
)
@expose('/new/', methods=('GET', 'POST'))
def create_view(self):
return_url = request.args.get('return')
if not self.can_create:
return redirect(return_url or url_for('.index_view'))
form = self.create_form(request.form)
if form.validate_on_submit():
if self.create_model(form):
return redirect(return_url or url_for('.index_view'))
return render_template(self.create_template, view=self, form=form)
@expose('/edit/<int:id>/', methods=('GET', 'POST'))
def edit_view(self, id):
return_url = request.args.get('return')
if not self.can_edit:
return redirect(return_url or url_for('.index_view'))
model = self.get_one(id)
if model is None:
return redirect(return_url or url_for('.index_view'))
form = self.edit_form(request.form, model)
if form.validate_on_submit():
if self.update_model(form, model):
return redirect(return_url or url_for('.index_view'))
return render_template(self.edit_template,
view=self,
form=form,
return_url=return_url or url_for('.index_view'))
@expose('/delete/<int:id>/')
def delete_view(self, id):
return_url = request.args.get('return')
# TODO: Use post
if not self.can_delete:
return redirect(return_url or url_for('.index_view'))
model = self.get_one(id)
if model:
self.delete_model(model)
return redirect(return_url or url_for('.index_view'))
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}Flask ExtAdmin{% endblock %}</title>
<title>{% block title %}{% if view.category %}{{ view.category }} - {% endif %}{{ view.name }} - {{ view.admin.name }}{% endblock %}</title>
{% block head %}
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
......@@ -16,6 +16,7 @@
<div class="navbar navbar-fixed-top">
<div class="navbar-inner">
<div class="container">
<span class="brand">{{ view.admin.name }}</span>
<ul class="nav">
{% for item in view.admin.menu() %}
{% if item.is_category() %}
......
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