From 4e5957eea2703fda53ae293b639592d58f88d968 Mon Sep 17 00:00:00 2001 From: Patrick Jentsch <p.jentsch@uni-bielefeld.de> Date: Fri, 8 Jul 2022 11:46:47 +0200 Subject: [PATCH] Change the Subscription Logic for Socket.IO Data exchange --- app/corpora/routes.py | 5 +- app/static/js/App.js | 47 +++++++++---- app/static/js/JobStatusNotifier.js | 16 +++-- .../js/RessourceDisplays/CorpusDisplay.js | 10 +-- app/static/js/RessourceDisplays/JobDisplay.js | 6 +- .../js/RessourceDisplays/RessourceDisplay.js | 12 +++- .../js/RessourceLists/CorpusFileList.js | 4 +- app/static/js/RessourceLists/CorpusList.js | 4 +- app/static/js/RessourceLists/JobList.js | 5 +- app/static/js/RessourceLists/JobResultList.js | 2 + app/static/js/RessourceLists/RessourceList.js | 13 ++-- app/static/js/RessourceLists/UserList.js | 2 +- app/templates/_scripts.html.j2 | 6 +- app/templates/jobs/_breadcrumbs.html.j2 | 2 +- app/templates/users/users.html.j2 | 68 ++++++------------- app/users/events.py | 18 ++++- app/users/routes.py | 49 +++---------- 17 files changed, 136 insertions(+), 133 deletions(-) diff --git a/app/corpora/routes.py b/app/corpora/routes.py index 3a3c76e3..532f0249 100644 --- a/app/corpora/routes.py +++ b/app/corpora/routes.py @@ -11,8 +11,6 @@ from flask import ( send_from_directory ) from flask_login import current_user, login_required -from werkzeug.utils import secure_filename -from zipfile import ZipFile from . import bp from . import tasks from .forms import ( @@ -24,7 +22,6 @@ from .forms import ( import os import shutil import tempfile -import glob import xml.etree.ElementTree as ET @@ -350,4 +347,4 @@ def download_corpus_file(corpus_id, corpus_file_id): attachment_filename=corpus_file.filename, directory=os.path.dirname(corpus_file.path), filename=os.path.basename(corpus_file.path) - ) \ No newline at end of file + ) diff --git a/app/static/js/App.js b/app/static/js/App.js index 349eaedf..27ddb0eb 100644 --- a/app/static/js/App.js +++ b/app/static/js/App.js @@ -1,30 +1,53 @@ class App { constructor() { - this.data = {users: {}}; - this.promises = {users: {}}; + this.data = { + promises: {getUser: {}, subscribeUser: {}}, + users: {}, + }; this.socket = io({transports: ['websocket'], upgrade: false}); - this.socket.on('PATCH', (patch) => {this.data = jsonpatch.applyPatch(this.data, patch).newDocument;}); + this.socket.on('PATCH', (patch) => { + const re = new RegExp(`^/users/(${Object.keys(this.data.users).join('|')})`); + const filteredPatch = patch.filter(operation => re.test(operation.path)); + + jsonpatch.applyPatch(this.data, filteredPatch); + }); } - get users() { - return this.data.users; + getUser(userId) { + if (userId in this.data.promises.getUser) { + return this.data.promises.getUser[userId]; + } + + this.data.promises.getUser[userId] = new Promise((resolve, reject) => { + this.socket.emit('GET /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.data.promises.getUser[userId]; } subscribeUser(userId) { - if (userId in this.promises.users) { - return this.promises.users[userId]; + if (userId in this.data.promises.subscribeUser) { + return this.data.promises.subscribeUser[userId]; } - this.promises.users[userId] = new Promise((resolve, reject) => { - this.socket.emit('SUBSCRIBE /users/<user_id>', userId, response => { + + this.data.promises.subscribeUser[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]); + resolve(response); } else { reject(response); } }); }); - return this.promises.users[userId]; + + return this.data.promises.subscribeUser[userId]; } flash(message, category) { diff --git a/app/static/js/JobStatusNotifier.js b/app/static/js/JobStatusNotifier.js index 57ca0135..bef2e5c7 100644 --- a/app/static/js/JobStatusNotifier.js +++ b/app/static/js/JobStatusNotifier.js @@ -1,10 +1,18 @@ class JobStatusNotifier { constructor(userId) { this.userId = userId; - app.socket.on('PATCH', (patch) => {this.onPATCH(patch);}); + this.isInitialized = false; + app.subscribeUser(this.userId).then((response) => { + app.socket.on('PATCH', (patch) => {this.onPATCH(patch);}); + }); + app.getUser(this.userId).then((user) => { + this.isInitialized = true; + }); } onPATCH(patch) { + if (!this.isInitialized) {return;} + let filteredPatch; let jobId; let match; @@ -13,11 +21,11 @@ class JobStatusNotifier { 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)); + .filter((operation) => {return operation.op === 'replace';}) + .filter((operation) => {return re.test(operation.path);}); for (operation of filteredPatch) { [match, jobId] = operation.path.match(re); - app.flash(`[<a href="/jobs/${jobId}">${app.users[this.userId].jobs[jobId].title}</a>] New status: <span class="job-status-text" data-job-status="${operation.value}"></span>`, 'job'); + app.flash(`[<a href="/jobs/${jobId}">${app.data.users[this.userId].jobs[jobId].title}</a>] New status: <span class="job-status-text" data-job-status="${operation.value}"></span>`, 'job'); } } } diff --git a/app/static/js/RessourceDisplays/CorpusDisplay.js b/app/static/js/RessourceDisplays/CorpusDisplay.js index 39d30e89..3fe7e96e 100644 --- a/app/static/js/RessourceDisplays/CorpusDisplay.js +++ b/app/static/js/RessourceDisplays/CorpusDisplay.js @@ -5,9 +5,8 @@ class CorpusDisplay extends RessourceDisplay { } init(user) { - let corpus; + const corpus = user.corpora[this.corpusId]; - corpus = user.corpora[this.corpusId]; this.setCreationDate(corpus.creation_date); this.setDescription(corpus.description); this.setLastEditedDate(corpus.last_edited_date); @@ -17,12 +16,15 @@ class CorpusDisplay extends RessourceDisplay { } onPATCH(patch) { + if (!this.isInitialized) {return;} + let filteredPatch; let operation; let re; re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}`); filteredPatch = patch.filter(operation => re.test(operation.path)); + for (operation of filteredPatch) { switch(operation.op) { case 'replace': @@ -55,7 +57,7 @@ class CorpusDisplay extends RessourceDisplay { setNumTokens(numTokens) { this.setElements( this.displayElement.querySelectorAll('.corpus-token-ratio'), - `${numTokens}/${app.users[this.userId].corpora[this.corpusId].max_num_tokens}` + `${numTokens}/${app.data.users[this.userId].corpora[this.corpusId].max_num_tokens}` ); } @@ -77,7 +79,7 @@ class CorpusDisplay extends RessourceDisplay { } elements = this.displayElement.querySelectorAll('.corpus-build-trigger'); for (element of elements) { - if (['UNPREPARED', 'FAILED'].includes(status) && Object.values(app.users[this.userId].corpora[this.corpusId].files).length > 0) { + if (['UNPREPARED', 'FAILED'].includes(status) && Object.values(app.data.users[this.userId].corpora[this.corpusId].files).length > 0) { element.classList.remove('disabled'); } else { element.classList.add('disabled'); diff --git a/app/static/js/RessourceDisplays/JobDisplay.js b/app/static/js/RessourceDisplays/JobDisplay.js index 98c16913..dc1bd777 100644 --- a/app/static/js/RessourceDisplays/JobDisplay.js +++ b/app/static/js/RessourceDisplays/JobDisplay.js @@ -5,9 +5,8 @@ class JobDisplay extends RessourceDisplay { } init(user) { - let job; + const job = user.jobs[this.jobId]; - job = user.jobs[this.jobId]; this.setCreationDate(job.creation_date); this.setEndDate(job.creation_date); this.setDescription(job.description); @@ -19,12 +18,15 @@ class JobDisplay extends RessourceDisplay { } onPATCH(patch) { + if (!this.isInitialized) {return;} + let filteredPatch; let operation; let re; re = new RegExp(`^/users/${this.userId}/jobs/${this.jobId}`); filteredPatch = patch.filter(operation => re.test(operation.path)); + for (operation of filteredPatch) { switch(operation.op) { case 'replace': diff --git a/app/static/js/RessourceDisplays/RessourceDisplay.js b/app/static/js/RessourceDisplays/RessourceDisplay.js index d98a9635..0fde4640 100644 --- a/app/static/js/RessourceDisplays/RessourceDisplay.js +++ b/app/static/js/RessourceDisplays/RessourceDisplay.js @@ -2,8 +2,16 @@ class RessourceDisplay { constructor(displayElement) { this.displayElement = displayElement; this.userId = this.displayElement.dataset.userId; - app.socket.on('PATCH', (patch) => {this.onPATCH(patch);}); - app.subscribeUser(this.userId).then((user) => {this.init(user);}); + this.isInitialized = false; + if (this.userId) { + app.subscribeUser(this.userId).then((response) => { + app.socket.on('PATCH', (patch) => {this.onPATCH(patch);}); + }); + app.getUser(this.userId).then((user) => { + this.init(user); + this.isInitialized = true; + }); + } } init(user) {throw 'Not implemented';} diff --git a/app/static/js/RessourceLists/CorpusFileList.js b/app/static/js/RessourceLists/CorpusFileList.js index b61d624b..7ffd18b6 100644 --- a/app/static/js/RessourceLists/CorpusFileList.js +++ b/app/static/js/RessourceLists/CorpusFileList.js @@ -65,7 +65,7 @@ class CorpusFileList extends RessourceList { <div class="modal"> <div class="modal-content"> <h4>Confirm corpus deletion</h4> - <p>Do you really want to delete the corpus file <b>${app.users[this.userId].corpora[this.corpusId].files[corpusFileId].filename}</b>? It will be permanently deleted!</p> + <p>Do you really want to delete the corpus file <b>${app.data.users[this.userId].corpora[this.corpusId].files[corpusFileId].filename}</b>? It will be permanently deleted!</p> </div> <div class="modal-footer"> <a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a> @@ -97,6 +97,8 @@ class CorpusFileList extends RessourceList { } onPATCH(patch) { + if (!this.isInitialized) {return;} + let corpusFileId; let filteredPatch; let match; diff --git a/app/static/js/RessourceLists/CorpusList.js b/app/static/js/RessourceLists/CorpusList.js index d6b75bec..b2727737 100644 --- a/app/static/js/RessourceLists/CorpusList.js +++ b/app/static/js/RessourceLists/CorpusList.js @@ -60,7 +60,7 @@ class CorpusList extends RessourceList { <div class="modal"> <div class="modal-content"> <h4>Confirm corpus deletion</h4> - <p>Do you really want to delete the corpus <b>${app.users[this.userId].corpora[corpusId].title}</b>? All files will be permanently deleted!</p> + <p>Do you really want to delete the corpus <b>${app.data.users[this.userId].corpora[corpusId].title}</b>? All files will be permanently deleted!</p> </div> <div class="modal-footer"> <a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a> @@ -89,6 +89,8 @@ class CorpusList extends RessourceList { } onPATCH(patch) { + if (!this.isInitialized) {return;} + let corpusId; let filteredPatch; let match; diff --git a/app/static/js/RessourceLists/JobList.js b/app/static/js/RessourceLists/JobList.js index ef027b0e..97895a0e 100644 --- a/app/static/js/RessourceLists/JobList.js +++ b/app/static/js/RessourceLists/JobList.js @@ -36,7 +36,6 @@ class JobList extends RessourceList { ] }; - constructor(listElement, options = {}) { super(listElement, {...JobList.options, ...options}); } @@ -66,7 +65,7 @@ class JobList extends RessourceList { <div class="modal"> <div class="modal-content"> <h4>Confirm job deletion</h4> - <p>Do you really want to delete the job <b>${app.users[this.userId].jobs[jobId].title}</b>? All files will be permanently deleted!</p> + <p>Do you really want to delete the job <b>${app.data.users[this.userId].jobs[jobId].title}</b>? All files will be permanently deleted!</p> </div> <div class="modal-footer"> <a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a> @@ -95,6 +94,8 @@ class JobList extends RessourceList { } onPATCH(patch) { + if (!this.isInitialized) {return;} + let filteredPatch; let jobId; let match; diff --git a/app/static/js/RessourceLists/JobResultList.js b/app/static/js/RessourceLists/JobResultList.js index 85c3c865..16c390df 100644 --- a/app/static/js/RessourceLists/JobResultList.js +++ b/app/static/js/RessourceLists/JobResultList.js @@ -58,6 +58,8 @@ class JobResultList extends RessourceList { } onPATCH(patch) { + if (!this.isInitialized) {return;} + let filteredPatch; let operation; let re; diff --git a/app/static/js/RessourceLists/RessourceList.js b/app/static/js/RessourceLists/RessourceList.js index b36b4d92..29a94663 100644 --- a/app/static/js/RessourceLists/RessourceList.js +++ b/app/static/js/RessourceLists/RessourceList.js @@ -90,12 +90,15 @@ class RessourceList { this.listjs.list.style.cursor = 'pointer'; this.userId = this.listjs.listContainer.dataset.userId; this.listjs.list.addEventListener('click', event => this.onclick(event)); + this.isInitialized = false; if (this.userId) { - app.socket.on('PATCH', (patch) => {this.onPATCH(patch);}); - app.subscribeUser(this.userId).then( - (user) => {this.init(user);}, - (error) => {throw JSON.stringify(error);} - ); + app.subscribeUser(this.userId).then((response) => { + app.socket.on('PATCH', (patch) => {this.onPATCH(patch);}); + }); + app.getUser(this.userId).then((user) => { + this.init(user); + this.isInitialized = true; + }); } } diff --git a/app/static/js/RessourceLists/UserList.js b/app/static/js/RessourceLists/UserList.js index 5f488f47..f1f7e42a 100644 --- a/app/static/js/RessourceLists/UserList.js +++ b/app/static/js/RessourceLists/UserList.js @@ -20,7 +20,7 @@ class UserList extends RessourceList { 'id-1': user.id, 'username': user.username, 'email': user.email, - 'last-seen': new Date(user.last_seen).toLocaleString("en-US"), + 'last-seen': new Date(user.last_seen).toLocaleString('en-US'), 'member-since': user.member_since, 'role': user.role.name }; diff --git a/app/templates/_scripts.html.j2 b/app/templates/_scripts.html.j2 index 9a98ee9b..1956d6e7 100644 --- a/app/templates/_scripts.html.j2 +++ b/app/templates/_scripts.html.j2 @@ -32,11 +32,7 @@ const jobStatusNotifier = new JobStatusNotifier(currentUserId); // Initialize components for current user - app.subscribeUser(currentUserId) - .then( - (user) => {return;}, - (error) => {throw JSON.stringify(error);} - ); + app.subscribeUser(currentUserId).catch((error) => {throw JSON.stringify(error);}); {%- endif %} // Disable all option elements with no value diff --git a/app/templates/jobs/_breadcrumbs.html.j2 b/app/templates/jobs/_breadcrumbs.html.j2 index 5bad7de3..e3de43f3 100644 --- a/app/templates/jobs/_breadcrumbs.html.j2 +++ b/app/templates/jobs/_breadcrumbs.html.j2 @@ -1,8 +1,8 @@ {% set breadcrumbs %} <li class="tab disabled"><i class="material-icons">navigate_next</i></li> <li class="tab"><a href="{{ url_for('main.dashboard', _anchor='jobs') }}" target="_self">My jobs</a></li> -<li class="tab disabled"><i class="material-icons">navigate_next</i></li> {% if request.path == url_for('.job', job_id=job.id) %} +<li class="tab disabled"><i class="material-icons">navigate_next</i></li> <li class="tab"><a class="active" href="{{ url_for('.job', job_id=job.id) }}" target="_self">{{ job.title }}</a></li> {% endif %} {% endset %} diff --git a/app/templates/users/users.html.j2 b/app/templates/users/users.html.j2 index ff9b27ca..56713948 100644 --- a/app/templates/users/users.html.j2 +++ b/app/templates/users/users.html.j2 @@ -7,10 +7,28 @@ <h1 id="title">{{ title }}</h1> </div> - <div class="col s12"> + <div class="col s12 nopaque-ressource-list no-autoinit" data-ressource-type="User" id="users"> <div class="card"> <div class="card-content"> - <table class="" id="users"></table> + <div class="input-field"> + <i class="material-icons prefix">search</i> + <input id="search-user" class="search" type="text"></input> + <label for="search-user">Search user</label> + </div> + <table> + <thead> + <tr> + <th>Id</th> + <th>Username</th> + <th>Email</th> + <th>Last seen</th> + <th>Role</th> + <th></th> + </tr> + </thead> + <tbody class="list"></tbody> + </table> + <ul class="pagination"></ul> </div> </div> </div> @@ -18,52 +36,10 @@ </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')); + let userList = new UserList(document.querySelector('#users')); + userList.init({{ dict_users|tojson }}); </script> {% endblock scripts %} diff --git a/app/users/events.py b/app/users/events.py index 1bda2b7b..4b6f2976 100644 --- a/app/users/events.py +++ b/app/users/events.py @@ -5,7 +5,7 @@ from flask_login import current_user from flask_socketio import join_room, leave_room -@socketio.on('SUBSCRIBE /users/<user_id>') +@socketio.on('GET /users/<user_id>') @socketio_login_required def subscribe_user(user_hashid): user_id = hashids.decode(user_hashid) @@ -15,10 +15,24 @@ def subscribe_user(user_hashid): 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('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} + return {'code': 200, 'msg': 'OK'} + + @socketio.on('UNSUBSCRIBE /users/<user_id>') @socketio_login_required def subscribe_user(user_hashid): diff --git a/app/users/routes.py b/app/users/routes.py index 6813aa96..b657107d 100644 --- a/app/users/routes.py +++ b/app/users/routes.py @@ -1,50 +1,17 @@ +from app.decorators import admin_required from app.models import User -from flask import render_template, request, url_for +from flask import render_template, request +from flask_login import login_required from . import bp @bp.route('/') +@login_required +@admin_required def users(): + dict_users = [u.to_dict(backrefs=True, relationships=False) for u in User.query.all()] return render_template( 'users/users.html.j2', - title='Users' + title='Users', + dict_users=dict_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