Commit 4049b93a authored by Serge S. Koval's avatar Serge S. Koval

Sorting, paging and UI cleanup.

parent e3aa36b8
...@@ -5,8 +5,6 @@ ...@@ -5,8 +5,6 @@
- Ability to override displayed columns in a list - 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 provide form validators without messing with form creation
- Paging
- Sorting
- Filtering - Filtering
- Many2Many editing - Many2Many editing
- Many2One editor - Many2One editor
......
...@@ -56,4 +56,4 @@ if __name__ == '__main__': ...@@ -56,4 +56,4 @@ if __name__ == '__main__':
# Start app # Start app
app.debug = True app.debug = True
app.run() app.run('0.0.0.0')
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 import exc 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 from flask import request, render_template, url_for, redirect, flash
from flaskext import wtf from flaskext import wtf
...@@ -56,6 +57,9 @@ class ModelView(BaseView): ...@@ -56,6 +57,9 @@ class ModelView(BaseView):
edit_template = 'admin/model/edit.html' edit_template = 'admin/model/edit.html'
create_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 not provided, it is modelname
if name is None: if name is None:
...@@ -95,7 +99,7 @@ class ModelView(BaseView): ...@@ -95,7 +99,7 @@ class ModelView(BaseView):
columns.append(p.key) columns.append(p.key)
return 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, None, converter=AdminModelConverter(self.session))
...@@ -107,7 +111,7 @@ class ModelView(BaseView): ...@@ -107,7 +111,7 @@ class ModelView(BaseView):
return self.scaffold_form() return self.scaffold_form()
# Database-related API # Database-related API
def get_list(self): def get_query(self):
return self.session.query(self.model) return self.session.query(self.model)
def get_one(self, id): def get_one(self, id):
...@@ -115,66 +119,151 @@ class ModelView(BaseView): ...@@ -115,66 +119,151 @@ class ModelView(BaseView):
# Model handlers # Model handlers
def create_model(self, form): def create_model(self, form):
# TODO: Error handling try:
model = self.model() model = self.model()
form.populate_obj(model) form.populate_obj(model)
self.session.add(model) self.session.add(model)
self.session.commit() self.session.commit()
return True
except Exception, ex:
# TODO: Error logging
flash('Failed to create model. ' + str(ex), 'error')
return False
def update_model(self, form, model): def update_model(self, form, model):
form.populate_obj(model) try:
self.session.commit() form.populate_obj(model)
self.session.commit()
return True
except Exception, ex:
flash('Failed to update model. ' + str(ex), 'error')
return False
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 # Views
@expose('/') @expose('/')
def index_view(self): def index_view(self):
data = self.get_list() data = self.get_query()
return render_template(self.list_template, view=self, data=data)
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')) @expose('/new/', methods=('GET', 'POST'))
def create_view(self): def create_view(self):
return_url = request.args.get('return')
if not self.can_create: if not self.can_create:
return redirect(url_for('.index_view')) return redirect(return_url or url_for('.index_view'))
form = self.create_form(request.form) form = self.create_form(request.form)
if form.validate_on_submit(): if form.validate_on_submit():
self.create_model(form) if self.create_model(form):
return redirect(url_for('.index_view')) return redirect(return_url or url_for('.index_view'))
return render_template(self.create_template, view=self, form=form) return render_template(self.create_template, view=self, form=form)
@expose('/edit/<int:id>/', methods=('GET', 'POST')) @expose('/edit/<int:id>/', methods=('GET', 'POST'))
def edit_view(self, id): def edit_view(self, id):
return_url = request.args.get('return')
if not self.can_edit: if not self.can_edit:
return redirect(url_for('.index_view')) return redirect(return_url or url_for('.index_view'))
model = self.get_one(id) model = self.get_one(id)
if model is None: if model is None:
return redirect(url_for('.index_view')) return redirect(return_url or url_for('.index_view'))
form = self.edit_form(request.form, model) form = self.edit_form(request.form, model)
if form.validate_on_submit(): if form.validate_on_submit():
self.update_model(form, model) if self.update_model(form, model):
return redirect(url_for('.index_view')) return redirect(return_url or url_for('.index_view'))
return render_template(self.edit_template, view=self, form=form) return render_template(self.edit_template,
view=self,
form=form,
return_url=return_url or url_for('.index_view'))
@expose('/delete/<int:id>/') @expose('/delete/<int:id>/')
def delete_view(self, id): def delete_view(self, id):
return_url = request.args.get('return')
# TODO: Use post # TODO: Use post
if not self.can_delete: if not self.can_delete:
return redirect(url_for('.index_view')) return redirect(return_url or url_for('.index_view'))
model = self.get_one(id) model = self.get_one(id)
if model: if model:
self.delete_model(model) self.delete_model(model)
return redirect(url_for('.index_view')) return redirect(return_url or url_for('.index_view'))
/* Body */
body body
{ {
padding-top: 50px; padding-top: 50px;
......
{% macro pager(page, pages, generator) -%}
{% if pages > 1 %}
<div class="pagination">
<ul>
{% if not page %}
{% set page = 1 %}
{% endif %}
{% set min = page - 3 %}
{% set max = page + 3 + 1 %}
{% if min < 0 %}
{% set max = max - min + 1 %}
{% endif %}
{% if max > pages %}
{% set min = min + pages - max - 1 %}
{% endif %}
{% if min < 1 %}
{% set min = 1 %}
{% endif %}
{% if max > pages + 1 %}
{% set max = pages + 1 %}
{% endif %}
{% if min > 1 %}
<li>
<a href="{{ generator(1) }}">&lt;&lt;</a>
</li>
{% endif %}
{% if page > 1 %}
<li>
<a href="{{ generator(page-1) }}">&lt;</a>
</li>
{% endif %}
{% for p in range(min, max) %}
{% if page == p %}
<li class="disabled">
<a href="#">{{ p }}</a>
</li>
{% else %}
<li>
<a href="{{ generator(p) }}">{{ p }}</a>
</li>
{% endif %}
{% endfor %}
{% if page+1 <= pages %}
<li>
<a href="{{ generator(page+1) }}">&gt;</a>
</li>
{% endif %}
{% if max <= pages %}
<li>
<a href="{{ generator(pages) }}">&gt;&gt;</a>
</li>
{% endif %}
</ul>
</div>
{% endif %}
{%- endmacro %}
...@@ -44,6 +44,22 @@ ...@@ -44,6 +44,22 @@
</div> </div>
</div> </div>
</div> </div>
{% with messages = get_flashed_messages(with_categories=True) %}
{% if messages %}
{% for category, m in messages %}
{% if category == 'error' %}
<div class="alert alert-error">
{% else %}
<div class="alert">
{% endif %}
<a href="#" class="close" data-dismiss="alert">x</a>
{{ m }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="container"> <div class="container">
{% block body %}{% endblock %} {% block body %}{% endblock %}
</div> </div>
......
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
</ul> </ul>
{% endif %} {% endif %}
<input type="submit" class="btn btn-primary btn-large" /> <input type="submit" class="btn btn-primary btn-large" />
<a href="{{ return_url }}" class="btn btn-large">Cancel</a>
</td> </td>
</tr> </tr>
</table> </table>
......
{% extends 'admin/master.html' %} {% extends 'admin/master.html' %}
{% import 'admin/lib.html' as lib %}
{% block body %} {% block body %}
<table class="table table-striped table-bordered"> <table class="table table-striped table-bordered model-list">
<thead> <thead>
<tr> <tr>
<th></th> <th class="span1">&nbsp;</th>
{% for c in view.list_columns %} {% set column = 0 %}
<th>{{ c }}</th> {% for c, name in view.list_columns %}
<th>
{% if sort_column == column %}
<a href="{{ sort_url(column, True) }}">
{{ name }}
{% if sort_desc %}
<i class="icon-chevron-up"></i>
{% else %}
<i class="icon-chevron-down"></i>
{% endif %}
</a>
{% else %}
<a href="{{ sort_url(column) }}">{{ name }}</a>
{% endif %}
</th>
{% set column = column + 1 %}
{% endfor %} {% endfor %}
</tr> </tr>
</thead> </thead>
{% for row in data %} {% for row in data %}
<tr> <tr>
<td> <td>
{% if view.can_edit %}<a href="{{ url_for('.edit_view', id=row.id) }}">Edit</a>{% endif %} {%- if view.can_edit -%}
{% if view.can_delete %}<a href="{{ url_for('.delete_view', id=row.id) }}">Delete</a>{% endif %} <a href="{{ url_for('.edit_view', id=row.id, return=return_url) }}">
<i class="icon-pencil"></i>
</a>
{%- endif -%}
{%- if view.can_delete -%}
<a href="{{ url_for('.delete_view', id=row.id, return=return_url) }}" onclick="return confirm('You sure you want to delete this item?')">
<i class="icon-remove"></i>
</a>
{%- endif -%}
</td> </td>
{% for c in view.list_columns %} {% for c, name in view.list_columns %}
<td>{{ row[c] }}</td> <td>{{ row[c] }}</td>
{% endfor %} {% endfor %}
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
{{ lib.pager(page, num_pages, pager_url) }}
{% if view.can_create %} {% if view.can_create %}
<a class="btn btn-primary btn-large" href="{{ url_for('.create_view') }}">Create New</a> <a class="btn btn-primary btn-large" href="{{ url_for('.create_view') }}">Create New</a>
{% endif %} {% endif %}
......
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