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

Sorting, paging and UI cleanup.

parent e3aa36b8
......@@ -5,8 +5,6 @@
- Ability to override displayed columns in a list
- Ability to override displayed form fields
- Ability to provide form validators without messing with form creation
- Paging
- Sorting
- Filtering
- Many2Many editing
- Many2One editor
......
......@@ -56,4 +56,4 @@ if __name__ == '__main__':
# Start app
app.debug = True
app.run()
app.run('0.0.0.0')
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
from flask import request, render_template, url_for, redirect, flash
from flaskext import wtf
......@@ -56,6 +57,9 @@ class ModelView(BaseView):
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):
# If name not provided, it is modelname
if name is None:
......@@ -95,7 +99,7 @@ class ModelView(BaseView):
columns.append(p.key)
return columns
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))
......@@ -107,7 +111,7 @@ class ModelView(BaseView):
return self.scaffold_form()
# Database-related API
def get_list(self):
def get_query(self):
return self.session.query(self.model)
def get_one(self, id):
......@@ -115,66 +119,151 @@ class ModelView(BaseView):
# Model handlers
def create_model(self, form):
# TODO: Error handling
try:
model = self.model()
form.populate_obj(model)
self.session.add(model)
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):
try:
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):
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_list()
return render_template(self.list_template, view=self, data=data)
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(url_for('.index_view'))
return redirect(return_url or url_for('.index_view'))
form = self.create_form(request.form)
if form.validate_on_submit():
self.create_model(form)
return redirect(url_for('.index_view'))
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(url_for('.index_view'))
return redirect(return_url or url_for('.index_view'))
model = self.get_one(id)
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)
if form.validate_on_submit():
self.update_model(form, model)
return redirect(url_for('.index_view'))
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 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(url_for('.index_view'))
return redirect(return_url or url_for('.index_view'))
model = self.get_one(id)
if model:
self.delete_model(model)
return redirect(url_for('.index_view'))
return redirect(return_url or url_for('.index_view'))
/* Body */
body
{
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 @@
</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">
{% block body %}{% endblock %}
</div>
......
......@@ -28,6 +28,7 @@
</ul>
{% endif %}
<input type="submit" class="btn btn-primary btn-large" />
<a href="{{ return_url }}" class="btn btn-large">Cancel</a>
</td>
</tr>
</table>
......
{% extends 'admin/master.html' %}
{% import 'admin/lib.html' as lib %}
{% block body %}
<table class="table table-striped table-bordered">
<table class="table table-striped table-bordered model-list">
<thead>
<tr>
<th></th>
{% for c in view.list_columns %}
<th>{{ c }}</th>
<th class="span1">&nbsp;</th>
{% set column = 0 %}
{% 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 %}
</tr>
</thead>
{% for row in data %}
<tr>
<td>
{% if view.can_edit %}<a href="{{ url_for('.edit_view', id=row.id) }}">Edit</a>{% endif %}
{% if view.can_delete %}<a href="{{ url_for('.delete_view', id=row.id) }}">Delete</a>{% endif %}
{%- if view.can_edit -%}
<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>
{% for c in view.list_columns %}
{% for c, name in view.list_columns %}
<td>{{ row[c] }}</td>
{% endfor %}
</tr>
{% endfor %}
</table>
{{ lib.pager(page, num_pages, pager_url) }}
{% if view.can_create %}
<a class="btn btn-primary btn-large" href="{{ url_for('.create_view') }}">Create New</a>
{% 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