-
Notifications
You must be signed in to change notification settings - Fork 8
Commit v0.36
kwmccabe edited this page Apr 23, 2018
·
4 revisions
v0.36 - layout_2col, getItemBrowser(), getItemDetail(), xhr_required()
- +19 -4 [M] web/app/decorators.py
- +53 -0 [A] web/app/item/templates/item_browse.html
- +21 -0 [A] web/app/item/templates/item_detail.html
- +15 -1 [M] web/app/item/templates/item_index.html
- +62 -3 [M] web/app/item/views.py
- +17 -0 [A] web/app/main/templates/layout_1col.html
- +20 -0 [A] web/app/main/templates/layout_2col.html
- +23 -0 [A] web/app/main/templates/layout_3col.html
- +53 -0 [M] web/app/static/js/flaskapp.js
- Add decorator
xhr_required()
- used byitem.item_browse()
anditem.item_detail()
. - Add
CONFIG
alias for importconfig_default
.
-from flask import request, session
+from flask import abort, request, session
from flask_login import current_user
-from . import config_default, db, login_manager
+from . import config_default as CONFIG
+from . import login_manager
...
+# check ajax route is XHR request
+def xhr_required():
+ def _decorator(f):
+ @wraps(f)
+ def _decorated(*args, **kwargs):
+ logging.debug('xhr_required() : %s' % (request.environ['PATH_INFO']))
+ if not request.is_xhr and not CONFIG.LOG_LEVEL == logging.DEBUG:
+ logging.error('Abort non-XHR request : %s' % (request.environ['PATH_INFO']))
+ abort(404)
+ return f(*args, **kwargs)
+ return _decorated
+ return _decorator
...
-def role_required( role=getattr(config_default,'USER_ROLE_VIEW') ):
+def role_required( role=CONFIG.USER_ROLE_VIEW ):
...
- logging.debug('role_required( %s >= %s )' % (config_default.USER_ROLE[current_user.user_role],config_default.USER_ROLE[role]))
+ logging.debug('role_required( %s >= %s )' % (CONFIG.USER_ROLE[current_user.user_role],CONFIG.USER_ROLE[role]))
- New template for AJAX route
/item/browse/
. - Adapted from
item_list.html
, removing surrounding layout HTML.
+<div id="item_browse_panel" class="panel panel-default">
+ <div class="panel-heading">ITEMS</div>
+ <div class="panel-body">
+
+<div class="table-responsive">
+<table class="table table-condensed table-hover">
+<tr>
+{% for col in cols %}
+ <th>
+ {% if col == session[opts_key]['sort'] and session[opts_key]['order'] == 'asc' %}
+ <a href="{{ url_for('.item_page') }}?sort={{ col }}&order=desc"><span class="glyphicon glyphicon-sort-by-attributes" aria-hidden="true"></span></a>
+ {% elif col == session[opts_key]['sort'] %}
+ <a href="{{ url_for('.item_page') }}?sort={{ col }}&order=asc"><span class="glyphicon glyphicon-sort-by-attributes-alt" aria-hidden="true"></span></a>
+ {% else %}
+ <a href="{{ url_for('.item_page') }}?sort={{ col }}"><span class="glyphicon glyphicon-sort" aria-hidden="true"></span></a>
+ {% endif %}
+ {% if col == 'owner_id' %}Owner{% else %}{{ col }}{% endif %}
+ </th>
+{% endfor %}
+<th></th>
+</tr>
+
+{% for row in rows %}
+ <tr class="{%
+ if row.item_status == config['ITEM_STATUS_COMPLETED'] %}success{%
+ elif row.item_status == config['ITEM_STATUS_DRAFT'] %}warning{%
+ elif row.item_status == config['ITEM_STATUS_HIDDEN'] %}danger{%
+ else %}{%
+ endif %}">
+ {% for col in cols %}
+ <td onclick="getItemDetail({{ row.id }}, 'item-detail');">
+ {% if col == "owner_id" and row.owner %}
+ {{ row.owner.keyname }}
+ {% elif col == "item_status" %}
+ {{ config['ITEM_STATUS'][(row[col]|int)] }}
+ {% else %}
+ {{ row[col] }}
+ {% endif %}
+ </td>
+ {% endfor %}
+ <!-- td>
+ <a href="{{ url_for('.item_edit', id=row.id) }}"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a>
+ </td -->
+ </tr>
+{% endfor %}
+
+</table>
+</div>
+
+ </div> <!-- end class="panel-body -->
+</div> <!-- end class="panel -->
+
+{# end web/app/item/templates/item_browse.html #}
- New template for AJAX route
/item/detail/<int:id>
. - Adapted from
item_view.html
, removing surrounding layout HTML.
+<h3>Item Detail :: {{ item.keyname }}</h3>
+
+<div class="table-responsive">
+<table class="table table-condensed table-hover">
+<tr>
+ <th>column</th>
+ <th>value</th>
+</tr>
+
+{% for col in cols %}
+ <tr>
+ <td>{{ col }}</td>
+ <td>{{ item[col] }}</td>
+ </tr>
+{% endfor %}
+
+</table>
+</div>
+
+
+{# end web/app/item/templates/item_detail.html #}
- Switch to new
layout_2col.html
. - Add
<div id="item-browse"></div>
inside new{% block content_left %}
. - Add
<div id="item-detail"></div>
inside existing{% block content %}
. -
<div id="item-browse">
replaced via AJAX andgetItemBrowser()
on page load. -
<div id="item-detail">
replaced via AJAX andgetItemDetail()
on row click.
-{% extends "base.html" %}
+{% extends "layout_2col.html" %}
...
+<!-- BLOCK: content_left -->
+{% block content_left %}
+
+<div id="item-browse">
+<script language="javascript">
+$( document ).ready(function() { getItemBrowser('item-browse'); });
+</script>
+</div>
+
+{% endblock %}
<!-- BLOCK: content -->
{% block content %}
<p>This is public landing page for the Item Module.</p>
+<div id="item-detail"></div>
{% endblock %}
- Import
xhr_required()
decorator for use by the routes/item/browse/
and/item/detail/<int:id>
. - Add route
/item/browse/
, based onitem.item_list()
, for the new templateitem_browse.html
. - Add route
/item/detail/
, based onitem.item_view()
, for the new templateitem_detail.html
.
-from ..decorators import get_list_opts, role_required
+from ..decorators import get_list_opts, role_required, xhr_required
...
+@item.route('/item/detail/<int:id>')
+@role_required(CONFIG.USER_ROLE_VIEW)
+@xhr_required()
+def item_detail( id ):
+ item = ItemModel.query.get_or_404(id)
+ cols = ItemModel.__table__.columns.keys()
+ return render_template('item_detail.html', cols=cols, item=item)
+
+
+@item.route('/item/browse/', methods=['GET','POST'])
+@get_list_opts('item_browse_opts')
+@role_required(CONFIG.USER_ROLE_VIEW)
+@xhr_required()
+def item_browse():
+ cols = ItemModel.__table__.columns.keys()
+ cols_filtered = ['keyname']
+ rows = db.session.query(ItemModel)
+
+ opts_key = 'item_browse_opts'
+ S = session[opts_key]
+
+ if S['item_status'] >= current_app.config['ITEM_STATUS_HIDDEN']:
+ rows = rows.filter(ItemModel.item_status == S['item_status'])
+
+ S['itemcnt'] = rows.count()
+ S['pagecnt'] = int(math.ceil( float(S['itemcnt'])/float(S['limit']) ))
+
+ if S['page'] > S['pagecnt']:
+ S['page'] = S['pagecnt']
+ S['offset'] = 0
+ if ((S['page'] - 1) * S['limit']) < S['itemcnt']:
+ S['offset'] = (S['page'] - 1) * S['limit']
+ session[opts_key] = S
+
+ if S['sort'] == 'owner_id':
+ rows = rows.outerjoin(UserModel)
+ #rows = rows.options( db.joinedload(ItemModel.owner_id).load_only("keyname", "user_email") )
+ rows = rows.options( \
+ db.Load(ItemModel).defer("item_text"), \
+ db.Load(UserModel).load_only("keyname", "user_email"), \
+ )
+
+ if S['order'] == 'desc':
+ rows = rows.order_by(getattr( UserModel, 'keyname' ).desc())
+ else:
+ rows = rows.order_by(getattr( UserModel, 'keyname' ).asc())
+ elif S['sort'] in cols_filtered:
+ if S['order'] == 'desc':
+ rows = rows.order_by(getattr( ItemModel, S['sort'] ).desc())
+ else:
+ rows = rows.order_by(getattr( ItemModel, S['sort'] ).asc())
+ if S['offset'] > 0:
+ rows = rows.offset(S['offset'])
+ if S['limit'] > 0:
+ rows = rows.limit(S['limit'])
+
+ rowcnt = rows.count()
+ logging.debug('item_browse - %s' % (rowcnt))
+ return render_template('item_browse.html', cols=cols_filtered,rows=rows,rowcnt=rowcnt,opts_key=opts_key)
- New single column layout overrides
{% block main %}
frombase.html
. - Corresponds with
layout_2col.html
andlayout_3col.html
. - Same output as
base.html
alone.
+{% extends "base.html" %}
+
+<!-- BLOCK: main : content -->
+{% block main %}
+<div id="main" class="container-fluid layout-1col">
+ <div class="row">
+ <div id="content" class="col-md-12">
+ {% block content %}block content{% endblock %}
+ </div>
+ </div>
+</div>
+{% endblock %}
+
+<!-- BLOCK: templates -->
+{% block templates %}{{super()}} - layout_1col.html{% endblock %}
+
+{# end web/app/main/templates/layout_1col.html #}
- New two column layout overrides
{% block main %}
frombase.html
. -
content_left
column is 1/4 container width. -
content
column is 3/4 container width.
+{% extends "base.html" %}
+
+<!-- BLOCK: main : content_left, content -->
+{% block main %}
+<div id="main" class="container-fluid layout-2col">
+ <div class="row">
+ <div id="content_left" class="col-md-3">
+ {% block content_left %}block content_left{% endblock %}
+ </div>
+ <div id="content" class="col-md-9">
+ {% block content %}block content{% endblock %}
+ </div>
+ </div>
+</div>
+{% endblock %}
+
+<!-- BLOCK: templates -->
+{% block templates %}{{super()}} - layout_2col.html{% endblock %}
+
+{# end web/app/main/templates/layout_2col.html #}
- New three column layout overrides
{% block main %}
frombase.html
. -
content_left
column is 1/6 container width. -
content column
is 2/3 container width. -
content_right
column is 1/6 container width.
+{% extends "base.html" %}
+
+<!-- BLOCK: main : content_left, content, content_right -->
+{% block main %}
+<div id="main" class="container-fluid layout-3col">
+ <div class="row">
+ <div id="content_left" class="col-md-2">
+ {% block content_left %}block content_left{% endblock %}
+ </div>
+ <div id="content" class="col-md-8">
+ {% block content %}block content{% endblock %}
+ </div>
+ <div id="content_right" class="col-md-2">
+ {% block content_right %}block content_right{% endblock %}
+ </div>
+ </div>
+</div>
+{% endblock %}
+
+<!-- BLOCK: templates -->
+{% block templates %}{{super()}} - layout_3col.html{% endblock %}
+
+{# end web/app/main/templates/layout_3col.html #}
- Two new AJAX functions to fetch HTML and replace the contents of a
target
div. -
getItemBrowser( target )
requests and fetches/item/browse/
. -
getItemDetail( item_id, target )
requests and fetches/item/detail/<int:id>
.
+/**
+ * load /item/browse into target div
+ * @see item_index.html
+ */
+function getItemBrowser( target )
+{
+//alert("getItemBrowser('"+target+"')");
+
+ var urlParams = new URLSearchParams(window.location.search);
+ var post_data = {}
+ if (urlParams.has('status')) { post_data['status'] = urlParams.get('status'); }
+ if (urlParams.has('sort')) { post_data['sort'] = urlParams.get('sort'); }
+ if (urlParams.has('order')) { post_data['order'] = urlParams.get('order'); }
+ if (urlParams.has('page')) { post_data['page'] = urlParams.get('page'); }
+ if (urlParams.has('limit')) { post_data['limit'] = urlParams.get('limit'); }
+
+ $.ajax({
+ type: "POST"
+ , url: '/item/browse/'
+ , data: post_data
+ , success: function(data) {
+ $('#'+target).html(data);
+ }
+ , error: function (jqXHR, textStatus, errorThrown) {
+ console.log(jqXHR);
+ $('#'+target).html('<div class="error">AJAX Error:\n'+errorThrown+' - '+jqXHR.responseText+'</div>');
+ }
+ });
+}
+
+/**
+ * load /item/detail into target div
+ * @see item_detail.html
+ */
+function getItemDetail( item_id, target )
+{
+//alert("getItemDetail("+item_id+")",'"+target+"')");
+
+ $.ajax({
+ type: "GET"
+ , url: '/item/detail/'+item_id
+ , success: function(data) {
+ $('#'+target).html(data);
+ }
+ , error: function (jqXHR, textStatus, errorThrown) {
+ console.log(jqXHR);
+ $('#'+target).html('<div class="error">AJAX Error:\n'+errorThrown+' - '+jqXHR.responseText+'</div>');
+ }
+ });
+}
- FlaskApp Tutorial
- Table of Contents
- About
- Application Setup
- Modules, Templates, and Layouts
- Database Items, Forms, and CRUD
- List Filter, Sort, and Paginate
- Users and Login
- Database Relationships
- API Module, HTTPAuth and JSON
- Refactoring User Roles and Item Status
- AJAX and Public Pages