Skip to content

Commit v0.36

kwmccabe edited this page Apr 23, 2018 · 4 revisions

v0.36 - layout_2col, getItemBrowser(), getItemDetail(), xhr_required()


Files changed (9)

File web/app/decorators.py MODIFIED

  • Add decorator xhr_required() - used by item.item_browse() and item.item_detail().
  • Add CONFIG alias for import config_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]))

File web/app/item/templates/item_browse.html ADDED

  • 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 #}

File web/app/item/templates/item_detail.html ADDED

  • 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 #}

File web/app/item/templates/item_index.html MODIFIED

  • 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 and getItemBrowser() on page load.
  • <div id="item-detail"> replaced via AJAX and getItemDetail() 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 %}

File web/app/item/views.py MODIFIED

  • Import xhr_required() decorator for use by the routes /item/browse/ and /item/detail/<int:id>.
  • Add route /item/browse/, based on item.item_list(), for the new template item_browse.html.
  • Add route /item/detail/, based on item.item_view(), for the new template item_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)

File web/app/main/templates/layout_1col.html ADDED

  • New single column layout overrides {% block main %} from base.html.
  • Corresponds with layout_2col.html and layout_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 #}

File web/app/main/templates/layout_2col.html ADDED

  • New two column layout overrides {% block main %} from base.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 #}

File web/app/main/templates/layout_3col.html ADDED

  • New three column layout overrides {% block main %} from base.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 #}

File web/app/static/js/flaskapp.js MODIFIED

  • 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>');
+        }
+    });
+}
Clone this wiki locally