From 76924956dec46ef8619d8f4a61ba513e2b93ccba Mon Sep 17 00:00:00 2001 From: Patrick Jentsch <p.jentsch@uni-bielefeld.de> Date: Mon, 4 Jul 2022 14:09:17 +0200 Subject: [PATCH] Change the user session SocketIO Logic --- app/main/__init__.py | 2 +- app/main/events.py | 41 ----------- app/models.py | 10 +-- app/static/js/App.js | 46 ++++--------- app/static/js/JobStatusNotifier.js | 5 +- .../js/RessourceDisplays/CorpusDisplay.js | 2 +- app/static/js/RessourceDisplays/JobDisplay.js | 2 +- .../js/RessourceDisplays/RessourceDisplay.js | 6 +- .../js/RessourceLists/CorpusFileList.js | 2 +- app/static/js/RessourceLists/CorpusList.js | 2 +- app/static/js/RessourceLists/JobInputList.js | 2 +- app/static/js/RessourceLists/JobList.js | 2 +- app/static/js/RessourceLists/JobResultList.js | 2 +- .../js/RessourceLists/QueryResultList.js | 2 +- app/static/js/RessourceLists/RessourceList.js | 10 +-- app/templates/_scripts.html.j2 | 7 +- app/templates/users/users.html.j2 | 69 +++++++++++++++++++ app/users/__init__.py | 5 ++ app/users/events.py | 32 +++++++++ app/users/routes.py | 50 ++++++++++++++ 20 files changed, 200 insertions(+), 99 deletions(-) delete mode 100644 app/main/events.py create mode 100644 app/templates/users/users.html.j2 create mode 100644 app/users/__init__.py create mode 100644 app/users/events.py create mode 100644 app/users/routes.py diff --git a/app/main/__init__.py b/app/main/__init__.py index aa4f232e..65630224 100644 --- a/app/main/__init__.py +++ b/app/main/__init__.py @@ -2,4 +2,4 @@ from flask import Blueprint bp = Blueprint('main', __name__) -from . import events, routes +from . import routes diff --git a/app/main/events.py b/app/main/events.py deleted file mode 100644 index c727d171..00000000 --- a/app/main/events.py +++ /dev/null @@ -1,41 +0,0 @@ -from app import hashids, socketio -from app.models import User -from flask_login import current_user -from flask_socketio import join_room -from app.decorators import socketio_login_required - - -@socketio.on('users.user.get') -@socketio_login_required -def users_user_get(user_hashid): - user_id = hashids.decode(user_hashid) - user = User.query.get(user_id) - if user is None: - return {'code': 404, 'msg': 'Not found'} - if not (user == current_user or current_user.is_administrator): - return {'code': 403, 'msg': 'Forbidden'} - # corpora = [x.to_dict() for x in user.corpora] - # jobs = [x.to_dict() for x in user.jobs] - # transkribus_htr_models = TranskribusHTRModel.query.filter( - # (TranskribusHTRModel.shared == True) | (TranskribusHTRModel.user == user) - # ).all() - # tesseract_ocr_models = TesseractOCRModel.query.filter( - # (TesseractOCRModel.shared == True) | (TesseractOCRModel.user == user) - # ).all() - # response = { - # 'code': 200, - # 'msg': 'OK', - # 'payload': { - # 'user': user.to_dict(), - # 'corpora': corpora, - # 'jobs': jobs, - # 'transkribus_htr_models': transkribus_htr_models, - # 'tesseract_ocr_models': tesseract_ocr_models - # } - # } - join_room(f'users.{user.hashid}') - return { - 'code': 200, - 'msg': 'OK', - 'payload': user.to_dict(backrefs=True, relationships=True) - } diff --git a/app/models.py b/app/models.py index 94b57453..ce3686c8 100644 --- a/app/models.py +++ b/app/models.py @@ -1033,6 +1033,8 @@ def ressource_after_delete(mapper, connection, ressource): jsonpatch = [{'op': 'remove', 'path': ressource.jsonpatch_path}] room = f'users.{ressource.user_hashid}' socketio.emit('users.patch', jsonpatch, room=room) + room = f'/users/{ressource.user_hashid}' + socketio.emit('PATCH', jsonpatch, room=room) @db.event.listens_for(Corpus, 'after_insert') @@ -1047,8 +1049,8 @@ def ressource_after_insert_handler(mapper, connection, ressource): jsonpatch = [ {'op': 'add', 'path': ressource.jsonpatch_path, 'value': value} ] - room = f'users.{ressource.user_hashid}' - socketio.emit('users.patch', jsonpatch, room=room) + room = f'/users/{ressource.user_hashid}' + socketio.emit('PATCH', jsonpatch, room=room) @db.event.listens_for(Corpus, 'after_update') @@ -1077,8 +1079,8 @@ def ressource_after_update_handler(mapper, connection, ressource): } ) if jsonpatch: - room = f'users.{ressource.user_hashid}' - socketio.emit('users.patch', jsonpatch, room=room) + room = f'/users/{ressource.user_hashid}' + socketio.emit('PATCH', jsonpatch, room=room) @db.event.listens_for(Job, 'after_update') diff --git a/app/static/js/App.js b/app/static/js/App.js index 3761be8c..349eaedf 100644 --- a/app/static/js/App.js +++ b/app/static/js/App.js @@ -1,21 +1,30 @@ class App { constructor() { this.data = {users: {}}; - this.eventListeners = {'users.patch': []}; this.promises = {users: {}}; this.socket = io({transports: ['websocket'], upgrade: false}); - this.socket.on('users.patch', patch => this.usersPatchHandler(patch)); + this.socket.on('PATCH', (patch) => {this.data = jsonpatch.applyPatch(this.data, patch).newDocument;}); } get users() { return this.data.users; } - addEventListener(type, listener) { - if (!(type in this.eventListeners)) { - throw `Unknown event type: ${type}`; + subscribeUser(userId) { + if (userId in this.promises.users) { + return this.promises.users[userId]; } - this.eventListeners[type].push(listener); + this.promises.users[userId] = new Promise((resolve, reject) => { + this.socket.emit('SUBSCRIBE /users/<user_id>', userId, response => { + if (response.code === 200) { + this.data.users[userId] = response.payload; + resolve(this.data.users[userId]); + } else { + reject(response); + } + }); + }); + return this.promises.users[userId]; } flash(message, category) { @@ -50,29 +59,4 @@ class App { toastCloseActionElement = toast.el.querySelector('.toast-action[data-action="close"]'); toastCloseActionElement.addEventListener('click', () => {toast.dismiss();}); } - - getUserById(userId) { - if (userId in this.promises.users) { - return this.promises.users[userId]; - } - this.promises.users[userId] = new Promise((resolve, reject) => { - this.socket.emit('users.user.get', userId, response => { - if (response.code === 200) { - this.data.users[userId] = response.payload; - resolve(this.data.users[userId]); - } else { - reject(response); - } - }); - }); - return this.promises.users[userId]; - } - - usersPatchHandler(patch) { - let listener; - - this.data = jsonpatch.applyPatch(this.data, patch).newDocument; - //this.data = jsonpatch.apply_patch(this.data, patch); - for (listener of this.eventListeners['users.patch']) {listener(patch);} - } } diff --git a/app/static/js/JobStatusNotifier.js b/app/static/js/JobStatusNotifier.js index bb0ca44d..57ca0135 100644 --- a/app/static/js/JobStatusNotifier.js +++ b/app/static/js/JobStatusNotifier.js @@ -1,16 +1,17 @@ class JobStatusNotifier { constructor(userId) { this.userId = userId; + app.socket.on('PATCH', (patch) => {this.onPATCH(patch);}); } - usersPatchHandler(patch) { + onPATCH(patch) { let filteredPatch; let jobId; let match; let operation; let re; - re = new RegExp(`^/users/${this.userId}/jobs/([A-Za-z0-9]*)/status$`) + re = new RegExp(`^/users/${this.userId}/jobs/([A-Za-z0-9]*)/status$`); filteredPatch = patch .filter(operation => operation.op === 'replace') .filter(operation => re.test(operation.path)); diff --git a/app/static/js/RessourceDisplays/CorpusDisplay.js b/app/static/js/RessourceDisplays/CorpusDisplay.js index 6920b3bc..39d30e89 100644 --- a/app/static/js/RessourceDisplays/CorpusDisplay.js +++ b/app/static/js/RessourceDisplays/CorpusDisplay.js @@ -16,7 +16,7 @@ class CorpusDisplay extends RessourceDisplay { this.setNumTokens(corpus.num_tokens); } - usersPatchHandler(patch) { + onPATCH(patch) { let filteredPatch; let operation; let re; diff --git a/app/static/js/RessourceDisplays/JobDisplay.js b/app/static/js/RessourceDisplays/JobDisplay.js index 1c252837..98c16913 100644 --- a/app/static/js/RessourceDisplays/JobDisplay.js +++ b/app/static/js/RessourceDisplays/JobDisplay.js @@ -18,7 +18,7 @@ class JobDisplay extends RessourceDisplay { this.setTitle(job.title); } - usersPatchHandler(patch) { + onPATCH(patch) { let filteredPatch; let operation; let re; diff --git a/app/static/js/RessourceDisplays/RessourceDisplay.js b/app/static/js/RessourceDisplays/RessourceDisplay.js index c8c16be1..d98a9635 100644 --- a/app/static/js/RessourceDisplays/RessourceDisplay.js +++ b/app/static/js/RessourceDisplays/RessourceDisplay.js @@ -2,13 +2,13 @@ class RessourceDisplay { constructor(displayElement) { this.displayElement = displayElement; this.userId = this.displayElement.dataset.userId; - app.addEventListener('users.patch', patch => this.usersPatchHandler(patch)); - app.getUserById(this.userId).then(user => this.init(user)); + app.socket.on('PATCH', (patch) => {this.onPATCH(patch);}); + app.subscribeUser(this.userId).then((user) => {this.init(user);}); } init(user) {throw 'Not implemented';} - usersPatchHandler(patch) {throw 'Not implemented';} + onPATCH(patch) {throw 'Not implemented';} setElement(element, value) { switch (element.tagName) { diff --git a/app/static/js/RessourceLists/CorpusFileList.js b/app/static/js/RessourceLists/CorpusFileList.js index a4d76dad..b61d624b 100644 --- a/app/static/js/RessourceLists/CorpusFileList.js +++ b/app/static/js/RessourceLists/CorpusFileList.js @@ -96,7 +96,7 @@ class CorpusFileList extends RessourceList { } } - usersPatchHandler(patch) { + onPATCH(patch) { let corpusFileId; let filteredPatch; let match; diff --git a/app/static/js/RessourceLists/CorpusList.js b/app/static/js/RessourceLists/CorpusList.js index 068f446a..d6b75bec 100644 --- a/app/static/js/RessourceLists/CorpusList.js +++ b/app/static/js/RessourceLists/CorpusList.js @@ -88,7 +88,7 @@ class CorpusList extends RessourceList { } } - usersPatchHandler(patch) { + onPATCH(patch) { let corpusId; let filteredPatch; let match; diff --git a/app/static/js/RessourceLists/JobInputList.js b/app/static/js/RessourceLists/JobInputList.js index c1f31312..d86ff8ca 100644 --- a/app/static/js/RessourceLists/JobInputList.js +++ b/app/static/js/RessourceLists/JobInputList.js @@ -54,5 +54,5 @@ class JobInputList extends RessourceList { } } - usersPatchHandler(patch) {return;} + onPATCH(patch) {return;} } diff --git a/app/static/js/RessourceLists/JobList.js b/app/static/js/RessourceLists/JobList.js index a487c557..ef027b0e 100644 --- a/app/static/js/RessourceLists/JobList.js +++ b/app/static/js/RessourceLists/JobList.js @@ -94,7 +94,7 @@ class JobList extends RessourceList { } } - usersPatchHandler(patch) { + onPATCH(patch) { let filteredPatch; let jobId; let match; diff --git a/app/static/js/RessourceLists/JobResultList.js b/app/static/js/RessourceLists/JobResultList.js index 5f9da3b5..85c3c865 100644 --- a/app/static/js/RessourceLists/JobResultList.js +++ b/app/static/js/RessourceLists/JobResultList.js @@ -57,7 +57,7 @@ class JobResultList extends RessourceList { } } - usersPatchHandler(patch) { + onPATCH(patch) { let filteredPatch; let operation; let re; diff --git a/app/static/js/RessourceLists/QueryResultList.js b/app/static/js/RessourceLists/QueryResultList.js index c78ea3cb..8d0c1329 100644 --- a/app/static/js/RessourceLists/QueryResultList.js +++ b/app/static/js/RessourceLists/QueryResultList.js @@ -89,7 +89,7 @@ class QueryResultList extends RessourceList { } } - usersPatchHandler(patch) { + onPATCH(patch) { let filteredPatch; let match; let operation; diff --git a/app/static/js/RessourceLists/RessourceList.js b/app/static/js/RessourceLists/RessourceList.js index ca23d820..b36b4d92 100644 --- a/app/static/js/RessourceLists/RessourceList.js +++ b/app/static/js/RessourceLists/RessourceList.js @@ -91,10 +91,10 @@ class RessourceList { this.userId = this.listjs.listContainer.dataset.userId; this.listjs.list.addEventListener('click', event => this.onclick(event)); if (this.userId) { - app.addEventListener('users.patch', patch => this.usersPatchHandler(patch)); - app.getUserById(this.userId).then( - user => this.init(user), - error => {throw JSON.stringify(error);} + app.socket.on('PATCH', (patch) => {this.onPATCH(patch);}); + app.subscribeUser(this.userId).then( + (user) => {this.init(user);}, + (error) => {throw JSON.stringify(error);} ); } } @@ -117,7 +117,7 @@ class RessourceList { onclick(event) {throw 'Not implemented';} - usersPatchHandler(patch) {throw 'Not implemented';} + onPATCH(patch) {throw 'Not implemented';} add(ressources) { let values = Array.isArray(ressources) ? ressources : [ressources]; diff --git a/app/templates/_scripts.html.j2 b/app/templates/_scripts.html.j2 index 89a672dc..9a98ee9b 100644 --- a/app/templates/_scripts.html.j2 +++ b/app/templates/_scripts.html.j2 @@ -32,11 +32,10 @@ const jobStatusNotifier = new JobStatusNotifier(currentUserId); // Initialize components for current user - app.addEventListener('users.patch', patch => jobStatusNotifier.usersPatchHandler(patch)); - app.getUserById(currentUserId) + app.subscribeUser(currentUserId) .then( - user => {return;}, - error => {throw JSON.stringify(error);} + (user) => {return;}, + (error) => {throw JSON.stringify(error);} ); {%- endif %} diff --git a/app/templates/users/users.html.j2 b/app/templates/users/users.html.j2 new file mode 100644 index 00000000..ff9b27ca --- /dev/null +++ b/app/templates/users/users.html.j2 @@ -0,0 +1,69 @@ +{% extends "base.html.j2" %} + +{% block page_content %} +<div class="container"> + <div class="row"> + <div class="col s12"> + <h1 id="title">{{ title }}</h1> + </div> + + <div class="col s12"> + <div class="card"> + <div class="card-content"> + <table class="" id="users"></table> + </div> + </div> + </div> + </div> +</div> +{% endblock page_content %} + + +{% block scripts %} +{{ super() }} +<script src="https://unpkg.com/gridjs/dist/gridjs.umd.js"></script> +<script> + const updateUrl = (prev, query) => { + return prev + (prev.indexOf('?') >= 0 ? '&' : '?') + new URLSearchParams(query).toString(); + }; + + new gridjs.Grid({ + columns: [ + { id: 'username', name: 'Username' }, + { id: 'email', name: 'Email' }, + ], + server: { + url: '/users/api_users', + then: results => results.data, + total: results => results.total, + }, + search: { + enabled: true, + server: { + url: (prev, search) => { + return updateUrl(prev, {search}); + }, + }, + }, + sort: { + enabled: true, + multiColumn: true, + server: { + url: (prev, columns) => { + const columnIds = ['username', 'email']; + const sort = columns.map(col => (col.direction === 1 ? '+' : '-') + columnIds[col.index]); + return updateUrl(prev, {sort}); + }, + }, + }, + pagination: { + enabled: true, + server: { + url: (prev, page, limit) => { + return updateUrl(prev, {offset: page * limit, limit: limit}); + }, + }, + } + }).render(document.getElementById('users')); +</script> +{% endblock scripts %} diff --git a/app/users/__init__.py b/app/users/__init__.py new file mode 100644 index 00000000..878fd913 --- /dev/null +++ b/app/users/__init__.py @@ -0,0 +1,5 @@ +from flask import Blueprint + + +bp = Blueprint('users', __name__) +from . import events, routes # noqa diff --git a/app/users/events.py b/app/users/events.py new file mode 100644 index 00000000..1bda2b7b --- /dev/null +++ b/app/users/events.py @@ -0,0 +1,32 @@ +from app import hashids, socketio +from app.decorators import socketio_login_required +from app.models import User +from flask_login import current_user +from flask_socketio import join_room, leave_room + + +@socketio.on('SUBSCRIBE /users/<user_id>') +@socketio_login_required +def subscribe_user(user_hashid): + user_id = hashids.decode(user_hashid) + user = User.query.get(user_id) + if user is None: + return {'code': 404, 'msg': 'Not found'} + if not (user == current_user or current_user.is_administrator): + return {'code': 403, 'msg': 'Forbidden'} + dict_user = user.to_dict(backrefs=True, relationships=True) + join_room(f'/users/{user.hashid}') + return {'code': 200, 'msg': 'OK', 'payload': dict_user} + + +@socketio.on('UNSUBSCRIBE /users/<user_id>') +@socketio_login_required +def subscribe_user(user_hashid): + user_id = hashids.decode(user_hashid) + user = User.query.get(user_id) + if user is None: + return {'code': 404, 'msg': 'Not found'} + if not (user == current_user or current_user.is_administrator): + return {'code': 403, 'msg': 'Forbidden'} + leave_room(f'/users/{user.hashid}') + return {'code': 200, 'msg': 'OK'} diff --git a/app/users/routes.py b/app/users/routes.py new file mode 100644 index 00000000..6813aa96 --- /dev/null +++ b/app/users/routes.py @@ -0,0 +1,50 @@ +from app.models import User +from flask import render_template, request, url_for +from . import bp + + +@bp.route('/') +def users(): + return render_template( + 'users/users.html.j2', + title='Users' + ) + + +@bp.route('/api_users') +def api_users(): + query = User.query + + # search filter + search = request.args.get('search') + if search: + query = query.filter(User.username.like(f'%{search}%') | User.email.like(f'%{search}%')) + total = query.count() + + # sorting + sort = request.args.get('sort') + if sort: + order = [] + for s in sort.split(','): + direction = s[0] + name = s[1:] + if name not in ['username', 'email']: + name = 'username' + col = getattr(User, name) + if direction == '-': + col = col.desc() + order.append(col) + if order: + query = query.order_by(*order) + + # pagination + offset = request.args.get('offset', type=int, default=-1) + limit = request.args.get('limit', type=int, default=-1) + if offset != -1 and limit != -1: + query = query.offset(offset).limit(limit) + + # response + return { + 'data': [user.to_dict() for user in query], + 'total': total + } -- GitLab