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

Refactored model admin.

parent 4049b93a
- Core - Core
- Pregenerate URLs for menu - Pregenerate URLs for menu
- Page titles - Page titles
- Create base model-admin class and derive SQLA admin from it
- SQLA Model Admin - SQLA Model Admin
- Ability to override displayed columns in a list
- Ability to override displayed form fields - 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 - Filtering
- Many2Many editing - Many2Many editing
- Many2One editor - Many2One editor
......
...@@ -44,11 +44,17 @@ def index(): ...@@ -44,11 +44,17 @@ def index():
return '<a href="/admin/">Click me to get to Admin!</a>' 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__': if __name__ == '__main__':
# Create admin # Create admin
admin = adminex.Admin() admin = adminex.Admin('Simple Models')
admin.add_view(sqlamodel.ModelView(User, db.session, category='News')) admin.add_view(sqlamodel.ModelView(User, db.session))
admin.add_view(sqlamodel.ModelView(Post, db.session, category='News')) admin.add_view(PostAdmin(db.session))
admin.apply(app) admin.apply(app)
# Create DB # Create DB
......
...@@ -117,53 +117,58 @@ class AdminIndexView(BaseView): ...@@ -117,53 +117,58 @@ class AdminIndexView(BaseView):
return render_template('admin/index.html', view=self) return render_template('admin/index.html', view=self)
class Admin(object): class MenuItem(object):
class MenuItem(object): def __init__(self, name, view=None):
def __init__(self, name, view=None): self.name = name
self.name = name self._view = view
self._view = view self._children = []
self._children = [] self._children_urls = set()
self._children_urls = set()
self.url = None self.url = None
if view is not None: if view is not None:
self.url = view.url self.url = view.url
def add_child(self, view): def add_child(self, view):
self._children.append(view) self._children.append(view)
self._children_urls.add(view.url) self._children_urls.add(view.url)
def get_url(self): def get_url(self):
if self._view is None: if self._view is None:
return None return None
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):
if view == self._view: if view == self._view:
return True return True
return view.url in self._children_urls return view.url in self._children_urls
def is_accessible(self):
if self._view is None:
return False
def is_accessible(self): return self._view.is_accessible()
if self._view is None:
return False
return self._view.is_accessible() def is_category(self):
return self._view is None
def is_category(self): def get_children(self):
return self._view is None return [c for c in self._children if c.is_accessible()]
def get_children(self): def __repr__(self):
return [c for c in self._children if c.is_accessible()] return 'MenuItem %s (%s)' % (self.name, repr(self._children))
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._views = []
self._menu = [] self._menu = []
if name is None:
name = 'Flask'
self.name = name
if index_view is None: if index_view is None:
index_view = AdminIndexView() index_view = AdminIndexView()
...@@ -189,16 +194,16 @@ class Admin(object): ...@@ -189,16 +194,16 @@ class Admin(object):
for v in self._views: for v in self._views:
if v.category is None: if v.category is None:
self._menu.append(self.MenuItem(v.name, v)) self._menu.append(MenuItem(v.name, v))
else: else:
category = categories.get(v.category) category = categories.get(v.category)
if category is None: if category is None:
category = self.MenuItem(v.category) category = MenuItem(v.category)
categories[v.category] = category categories[v.category] = category
self._menu.append(category) self._menu.append(category)
category.add_child(self.MenuItem(v.name, v)) category.add_child(MenuItem(v.name, v))
def menu(self): def menu(self):
return self._menu return self._menu
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
from sqlalchemy.sql.expression import desc from sqlalchemy.sql.expression import desc
from sqlalchemy import exc
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
from flask import request, render_template, url_for, redirect, flash from flask import flash
from flaskext import wtf from flaskext import wtf
from flask.ext.adminex import BaseView, expose from flask.ext.adminex.model import BaseModelView
class AdminModelConverter(ModelConverter): class AdminModelConverter(ModelConverter):
...@@ -46,73 +45,62 @@ class AdminModelConverter(ModelConverter): ...@@ -46,73 +45,62 @@ class AdminModelConverter(ModelConverter):
return super(AdminModelConverter, self).convert(model, mapper, prop, field_args) return super(AdminModelConverter, self).convert(model, mapper, prop, field_args)
class ModelView(BaseView): class ModelView(BaseModelView):
# 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
def __init__(self, model, session, name=None, category=None, endpoint=None, url=None): 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.session = session
self.model = model
# Scaffolding super(ModelView, self).__init__(model, name, category, endpoint, url)
self.list_columns = self.get_list_columns()
self.create_form = self.scaffold_create_form()
self.edit_form = self.scaffold_edit_form()
# Public API # Public API
def get_list_columns(self): def get_list_columns(self):
mapper = self.model._sa_class_manager.mapper columns = self.list_columns
columns = [] if columns is None:
mapper = self.model._sa_class_manager.mapper
for p in mapper.iterate_properties: columns = []
if isinstance(p, RelationshipProperty):
if p.direction is MANYTOONE:
columns.append(p.key)
elif isinstance(p, ColumnProperty):
# TODO: Check for multiple columns
column = p.columns[0]
if column.foreign_keys or column.primary_key: for p in mapper.iterate_properties:
continue if isinstance(p, RelationshipProperty):
if p.direction is MANYTOONE:
columns.append(p.key)
elif isinstance(p, ColumnProperty):
# TODO: Check for multiple columns
column = p.columns[0]
if column.foreign_keys or column.primary_key:
continue
columns.append(p.key) columns.append(p.key)
return [(c, self.prettify_name(c)) for c in columns] return [(c, self.prettify_name(c)) for c in columns]
def scaffold_form(self): 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): # Database-related API
return self.scaffold_form() def get_list(self, page, sort_column, sort_desc):
query = self.session.query(self.model)
def scaffold_edit_form(self): count = query.count()
return self.scaffold_form()
# Database-related API if sort_column is not None:
def get_query(self): # Validate first
return self.session.query(self.model) 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): def get_one(self, id):
return self.session.query(self.model).get(id) return self.session.query(self.model).get(id)
...@@ -126,7 +114,6 @@ class ModelView(BaseView): ...@@ -126,7 +114,6 @@ class ModelView(BaseView):
self.session.commit() self.session.commit()
return True return True
except Exception, ex: except Exception, ex:
# TODO: Error logging
flash('Failed to create model. ' + str(ex), 'error') flash('Failed to create model. ' + str(ex), 'error')
return False return False
...@@ -142,128 +129,3 @@ class ModelView(BaseView): ...@@ -142,128 +129,3 @@ class ModelView(BaseView):
def delete_model(self, model): def delete_model(self, model):
self.session.delete(model) self.session.delete(model)
self.session.commit() 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> <!DOCTYPE html>
<html> <html>
<head> <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 %} {% block head %}
<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="">
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
<div class="navbar navbar-fixed-top"> <div class="navbar navbar-fixed-top">
<div class="navbar-inner"> <div class="navbar-inner">
<div class="container"> <div class="container">
<span class="brand">{{ view.admin.name }}</span>
<ul class="nav"> <ul class="nav">
{% for item in view.admin.menu() %} {% for item in view.admin.menu() %}
{% if item.is_category() %} {% 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