Browse Source

implemented infinite scroll for users

funding-tag
Holger Frey 5 years ago
parent
commit
8fd1b3b0ba
  1. 8
      ordr3/events.py
  2. 8
      ordr3/repo.py
  3. 8
      ordr3/resources.py
  4. 85
      ordr3/static/infinite.js
  5. 7
      ordr3/static/infinite.min.js
  6. 7
      ordr3/static/jquery.waypoints.min.js
  7. 19
      ordr3/static/script.js
  8. 15
      ordr3/static/style.css
  9. 21
      ordr3/templates/account/myaccount.jinja2
  10. 2
      ordr3/templates/layout_full.jinja2
  11. 2
      ordr3/templates/layout_small.jinja2
  12. 40
      ordr3/templates/users/list.jinja2
  13. 24
      ordr3/templates/users/list_content.jinja2
  14. 55
      ordr3/views/users.py

8
ordr3/events.py

@ -1,6 +1,6 @@
from collections import namedtuple from collections import namedtuple
from pyramid.events import subscriber from pyramid.events import BeforeRender, subscriber
from pyramid.renderers import render from pyramid.renderers import render
from pyramid_mailer.message import Message from pyramid_mailer.message import Message
@ -92,6 +92,12 @@ def notify_user(event):
event.request.mailer.send(message) event.request.mailer.send(message)
@subscriber(BeforeRender)
def add_global(event):
request = event["request"]
event.rendering_val["new_user_badge"] = request.repo.count_new_users()
def emit(request, event): def emit(request, event):
event.request = request event.request = request
request.registry.notify(event) request.registry.notify(event)

8
ordr3/repo.py

@ -152,6 +152,14 @@ class SqlAlchemyRepository(AbstractOrderRepository):
.all() .all()
) )
def count_new_users(self):
""" count the number of new users that need approval """
return (
self.session.query(models.User)
.filter(models.User.role == models.UserRole.NEW)
.count()
)
def search_vendor(self, reference): def search_vendor(self, reference):
""" search for a vendor by its canonical name """ """ search for a vendor by its canonical name """
vendor = ( vendor = (

8
ordr3/resources.py

@ -43,13 +43,19 @@ class BaseResource(abc.ABC):
return cls(primary_key, parent, sql_model_instance) return cls(primary_key, parent, sql_model_instance)
class UserList(BaseResource):
def __acl__(self):
""" access controll list """
return [(Allow, "role:admin", "view")]
class Root(BaseResource): class Root(BaseResource):
""" Root resource """ """ Root resource """
__name__ = None __name__ = None
__parent__ = None __parent__ = None
nodes = {} nodes = {"users": UserList}
def __init__(self, request): def __init__(self, request):
self.request = request self.request = request

85
ordr3/static/infinite.js

@ -0,0 +1,85 @@
/*!
Waypoints Infinite Scroll Shortcut - 4.0.1
Copyright © 2011-2016 Caleb Troughton
Licensed under the MIT license.
https://github.com/imakewebthings/waypoints/blob/master/licenses.txt
*/
(function() {
'use strict'
var $ = window.jQuery
var Waypoint = window.Waypoint
/* http://imakewebthings.com/waypoints/shortcuts/infinite-scroll */
function Infinite(options) {
this.options = $.extend({}, Infinite.defaults, options)
this.container = this.options.element
if (this.options.container !== 'auto') {
this.container = this.options.container
}
this.$container = $(this.container)
this.$more = $(this.options.more)
if (this.$more.length) {
this.setupHandler()
this.waypoint = new Waypoint(this.options)
}
}
/* Private */
Infinite.prototype.setupHandler = function() {
this.options.handler = $.proxy(function() {
this.options.onBeforePageLoad()
this.destroy()
this.$container.addClass(this.options.loadingClass)
$.get($(this.options.more).attr('href'), $.proxy(function(data) {
var $data = $($.parseHTML(data))
var $newMore = $data.find(this.options.more)
var $items = $data.find(this.options.items)
if (!$items.length) {
$items = $data.filter(this.options.items)
}
this.$container.append($items)
this.$container.removeClass(this.options.loadingClass)
if (!$newMore.length) {
$newMore = $data.filter(this.options.more)
}
if ($newMore.length) {
this.$more.replaceWith($newMore)
this.$more = $newMore
this.waypoint = new Waypoint(this.options)
this.$container.append(this.$more)
}
else {
this.$more.remove()
}
this.options.onAfterPageLoad($items)
}, this))
}, this)
}
/* Public */
Infinite.prototype.destroy = function() {
if (this.waypoint) {
this.waypoint.destroy()
}
}
Infinite.defaults = {
container: 'auto',
items: '.infinite-item',
more: '.infinite-more-link',
offset: 'bottom-in-view',
loadingClass: 'infinite-loading',
onBeforePageLoad: $.noop,
onAfterPageLoad: $.noop
}
Waypoint.Infinite = Infinite
}())
;

7
ordr3/static/infinite.min.js vendored

@ -0,0 +1,7 @@
/*!
Waypoints Infinite Scroll Shortcut - 4.0.1
Copyright © 2011-2016 Caleb Troughton
Licensed under the MIT license.
https://github.com/imakewebthings/waypoints/blob/master/licenses.txt
*/
!function(){"use strict";function t(n){this.options=i.extend({},t.defaults,n),this.container=this.options.element,"auto"!==this.options.container&&(this.container=this.options.container),this.$container=i(this.container),this.$more=i(this.options.more),this.$more.length&&(this.setupHandler(),this.waypoint=new o(this.options))}var i=window.jQuery,o=window.Waypoint;t.prototype.setupHandler=function(){this.options.handler=i.proxy(function(){this.options.onBeforePageLoad(),this.destroy(),this.$container.addClass(this.options.loadingClass),i.get(i(this.options.more).attr("href"),i.proxy(function(t){var n=i(i.parseHTML(t)),e=n.find(this.options.more),s=n.find(this.options.items);s.length||(s=n.filter(this.options.items)),this.$container.append(s),this.$container.removeClass(this.options.loadingClass),e.length||(e=n.filter(this.options.more)),e.length?(this.$more.replaceWith(e),this.$more=e,this.waypoint=new o(this.options)):this.$more.remove(),this.options.onAfterPageLoad(s)},this))},this)},t.prototype.destroy=function(){this.waypoint&&this.waypoint.destroy()},t.defaults={container:"auto",items:".infinite-item",more:".infinite-more-link",offset:"bottom-in-view",loadingClass:"infinite-loading",onBeforePageLoad:i.noop,onAfterPageLoad:i.noop},o.Infinite=t}();

7
ordr3/static/jquery.waypoints.min.js vendored

File diff suppressed because one or more lines are too long

19
ordr3/static/script.js

@ -4,6 +4,22 @@ var capitalize = function(some_string) {
} else { } else {
return(""); return("");
} }
};
var load_more = function(target) {
var next = target.attr("data-next")
var href = new URL(window.location);
href.searchParams.set('o', next);
$.get( href, function( response, tmp, request ) {
next_offset = request.getResponseHeader('O3-Next-Offset');
if (next_offset == "None") {
target.hide()
} else {
target.attr("data-next", next_offset)
}
$(".o3-data-table tbody").append(response)
});
} }
$(function() { $(function() {
@ -47,4 +63,7 @@ $(function() {
$(target).fadeOut( 100 ).delay( 100 ).fadeIn( 100 ); $(target).fadeOut( 100 ).delay( 100 ).fadeIn( 100 );
}); });
var infinite = new Waypoint.Infinite({
element: $('.infinite-container')[0]
});
}); });

15
ordr3/static/style.css

@ -64,6 +64,7 @@
.o3-head-space .col-2, .o3-head-space .col-5, .o3-head-space .col-10 { .o3-head-space .col-2, .o3-head-space .col-5, .o3-head-space .col-10 {
padding-top:1rem; padding-top:1rem;
padding-bottom:2rem;
} }
.o3-sidebar { .o3-sidebar {
@ -112,16 +113,26 @@
padding:.5rem; padding:.5rem;
} }
td.actions a { td.o3-actions a {
color: #6c757d!important; color: #6c757d!important;
margin-right:0.5rem; margin-right:0.5rem;
font-size:1rem; font-size:1rem;
} }
td.actions a:hover { td.o3-actions a:hover {
color: #343a40!important; color: #343a40!important;
} }
.o3-copy { .o3-copy {
cursor:pointer; cursor:pointer;
} }
.o3-data-table th {
border-top:none;
}
.infinite-more-link td {
text-align:center;
}

21
ordr3/templates/account/myaccount.jinja2

@ -1,29 +1,18 @@
{% extends "ordr3:templates/layout_small.jinja2" %} {% extends "ordr3:templates/layout_full.jinja2" %}
{% block subtitle %} My Account {% endblock subtitle %} {% block subtitle %} My Account {% endblock subtitle %}
{% block content %} {% block content %}
<div class="container"> <div class="col-2 o3-sidebar"></div>
<div class="row o3-registration-card"> <div class="col-5">
<div class="col"></div> <h4 class="mb-2 text-muted mb-4">Edit your account</h4>
<div class="col">
<div class="card">
<div class="card-header bg-dark text-light">
<h2 class="card-title text-center pt-1">Ordr</h5>
</div>
<div class="card-body">
<h6 class="card-subtitle mb-2 text-muted text-center mt-1 mb-4">Edit your account</h6>
{{form.render()|safe}} {{form.render()|safe}}
<hr> <hr>
<p class="mt-4"> <p class="mt-4">
<a href="{{request.resource_url(request.root, 'mypassword')}}" class="btn btn-outline-secondary">Change Password</a> <a href="{{request.resource_url(request.root, 'mypassword')}}" class="btn btn-outline-secondary">Change Password</a>
</p> </p>
</div> </div>
</div> <div class="col-5"></div>
</div>
<div class="col"></div>
</div>
</div>
{% endblock content %} {% endblock content %}

2
ordr3/templates/layout_full.jinja2

@ -14,6 +14,8 @@
<script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script> <script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
<script src="{{request.static_url('ordr3:static/jquery.waypoints.min.js')}}"></script>
<script src="{{request.static_url('ordr3:static/infinite.js')}}"></script>
<script src="{{request.static_url('ordr3:static/script.js')}}"></script> <script src="{{request.static_url('ordr3:static/script.js')}}"></script>
</head> </head>
<body> <body>

2
ordr3/templates/layout_small.jinja2

@ -10,7 +10,7 @@
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<link rel="stylesheet" href="{{request.static_url('ordr3:static/style.css')}}" type="text/css" media="screen" /> <link rel="stylesheet" href="{{request.static_url('ordr3:static/style.css')}}" type="text/css" media="screen" />
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script> <script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
<script src="{{request.static_url('ordr3:static/script.js')}}"></script> <script src="{{request.static_url('ordr3:static/script.js')}}"></script>

40
ordr3/templates/users/list.jinja2

@ -0,0 +1,40 @@
{% extends "ordr3:templates/layout_full.jinja2" %}
{% block subtitle %} Manage Users {% endblock subtitle %}
{% block content %}
<div class="col-2 o3-sidebar">
<nav class="nav nav-pills flex-column">
<div class="nav-link disabled text-small" tabindex="-1" aria-disabled="true">Role</div>
<a class="nav-link {% if filter_role == 'all' %}active{% endif %}" href="{{ request.resource_url(context) }}">All</a>
{% for role in roles %}
<a class="nav-link {% if filter_role == role.name.lower() %}active{% endif %}" href="{{ request.resource_url(context, query={'role':role.name.lower()}) }}">{{role.name.lower()}}</a>
{% endfor %}
</nav>
</div>
<div class="col-10">
<table class="table table-hover o3-data-table">
<thead>
<tr>
<th scope="col">Username</th>
<th scope="col">First Name</th>
<th scope="col">Last Name</th>
<th scope="col">Email</th>
<th scope="col">Role</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody class="infinite-container">
{% include 'ordr3:templates/users/list_content.jinja2' %}
</tbody>
</table>
{% if not users %}
<p class="bg-light text-center pt-2 pb-2">No data available</p>
{% endif %}
</div>
{% endblock content %}

24
ordr3/templates/users/list_content.jinja2

@ -0,0 +1,24 @@
{% import 'ordr3:templates/macros.jinja2' as macros with context %}
{% for user in users %}
<tr class="infinite-item">
<td><a href="{{request.resource_url(request.root, 'orders', query={'user':user.username})}}" title="show orders">{{ user.username }}</a></td>
<td>{{ user.first_name }}</td>
<td>{{ user.last_name }}</td>
<td><a class="o3-copy" title="copy to clipboard">{{ user.email }}</a></td>
<td>{{ user.role.name.capitalize() }}</td>
<td class="o3-actions">
<a href="{{ request.resource_url(context, user.username, 'edit') }}" title="Edit user">{{ macros.icon("pencil")}}</a>
<a href="{{ request.resource_url(context, user.username, 'delete') }}" title="Delete user">{{ macros.icon("trash")}}</a>
</td>
</tr>
{% endfor %}
{% if next_offset %}
<tr class="infinite-more-link" href="{{ request.resource_url(context, query={'o':next_offset, 'role':filter_role.lower()}) }}">
<td colspan="6">
<button class="btn btn-outline-primary btn-small">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
Loading...
</button>
</td>
</tr>
{% endif %}

55
ordr3/views/users.py

@ -0,0 +1,55 @@
from sqlalchemy import func
from pyramid.view import view_config
from .. import models
def _get_role(request):
role_param = request.GET.get("role", "")
try:
return models.UserRole[role_param.upper()]
except KeyError:
return None
def _get_offset(request):
offset_param = request.GET.get("o", 0)
try:
return int(offset_param)
except ValueError:
return 0
@view_config(
context="ordr3:resources.UserList",
permission="view",
request_method="GET",
renderer="ordr3:templates/users/list.jinja2",
)
@view_config(
context="ordr3:resources.UserList",
permission="view",
request_method="GET",
xhr=True,
renderer="ordr3:templates/users/list_content.jinja2",
)
def list(context, request):
role = _get_role(request)
offset = _get_offset(request)
limit = 12
query = request.repo.session.query(models.User)
if role:
query = query.filter(models.User.role == role)
query = query.order_by(func.lower(models.User.username))
users = query[offset : offset + limit] # noqa: E203
next_offset = None if limit != len(users) else (offset + limit)
filter_role = "all" if role is None else role.name.lower()
return {
"filter_role": filter_role,
"roles": models.UserRole,
"users": users,
"next_offset": next_offset,
}
Loading…
Cancel
Save