diff --git a/app/__init__.py b/app/__init__.py index eb4f7bd6..9953a1cf 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -132,6 +132,9 @@ def create_app(config: Config = Config) -> Flask: # region SocketIO Namespaces from .namespaces.cqi_over_sio import CQiOverSocketIONamespace socketio.on_namespace(CQiOverSocketIONamespace('/cqi_over_sio')) + + from .namespaces.users import UsersNamespace + socketio.on_namespace(UsersNamespace('/users')) # endregion SocketIO Namespaces # region Database event Listeners diff --git a/app/blueprints/users/__init__.py b/app/blueprints/users/__init__.py index d305e242..21e9c382 100644 --- a/app/blueprints/users/__init__.py +++ b/app/blueprints/users/__init__.py @@ -15,4 +15,4 @@ def before_request(): pass -from . import cli, events, json_routes, routes, settings +from . import cli, json_routes, routes, settings diff --git a/app/decorators.py b/app/decorators.py index 85fdd4ba..d54cfcd3 100644 --- a/app/decorators.py +++ b/app/decorators.py @@ -26,7 +26,7 @@ def socketio_login_required(f): def wrapper(*args, **kwargs): if current_user.is_authenticated: return f(*args, **kwargs) - return {'code': 401, 'body': 'Unauthorized'} + return {'status': 401, 'statusText': 'Unauthorized'} return wrapper @@ -35,7 +35,7 @@ def socketio_permission_required(permission): @wraps(f) def wrapper(*args, **kwargs): if not current_user.can(permission): - return {'code': 403, 'body': 'Forbidden'} + return {'status': 403, 'statusText': 'Forbidden'} return f(*args, **kwargs) return wrapper return decorator diff --git a/app/models/event_listeners.py b/app/models/event_listeners.py index 98abe9ff..b9f9a652 100644 --- a/app/models/event_listeners.py +++ b/app/models/event_listeners.py @@ -42,8 +42,9 @@ def resource_after_delete(mapper, connection, resource): 'path': resource.jsonpatch_path } ] + namespace = '/users' room = f'/users/{resource.user_hashid}' - socketio.emit('users.patch', jsonpatch, room=room) + socketio.emit('patch', jsonpatch, namespace=namespace, room=room) def cfa_after_delete(mapper, connection, cfa): @@ -54,8 +55,9 @@ def cfa_after_delete(mapper, connection, cfa): 'path': jsonpatch_path } ] + namespace = '/users' room = f'/users/{cfa.corpus.user.hashid}' - socketio.emit('users.patch', jsonpatch, room=room) + socketio.emit('patch', jsonpatch, namespace=namespace, room=room) def resource_after_insert(mapper, connection, resource): @@ -69,8 +71,9 @@ def resource_after_insert(mapper, connection, resource): 'value': jsonpatch_value } ] + namespace = '/users' room = f'/users/{resource.user_hashid}' - socketio.emit('users.patch', jsonpatch, room=room) + socketio.emit('patch', jsonpatch, namespace=namespace, room=room) def cfa_after_insert(mapper, connection, cfa): @@ -83,8 +86,9 @@ def cfa_after_insert(mapper, connection, cfa): 'value': jsonpatch_value } ] + namespace = '/users' room = f'/users/{cfa.corpus.user.hashid}' - socketio.emit('users.patch', jsonpatch, room=room) + socketio.emit('patch', jsonpatch, namespace=namespace, room=room) def resource_after_update(mapper, connection, resource): @@ -109,8 +113,9 @@ def resource_after_update(mapper, connection, resource): } ) if jsonpatch: + namespace = '/users' room = f'/users/{resource.user_hashid}' - socketio.emit('users.patch', jsonpatch, room=room) + socketio.emit('patch', jsonpatch, namespace=namespace, room=room) def job_after_update(mapper, connection, job): diff --git a/app/namespaces/cqi_over_sio/__init__.py b/app/namespaces/cqi_over_sio/__init__.py index 0783ba3e..b219aeef 100644 --- a/app/namespaces/cqi_over_sio/__init__.py +++ b/app/namespaces/cqi_over_sio/__init__.py @@ -9,8 +9,8 @@ from threading import Lock from app import db, docker_client, hashids, socketio from app.decorators import socketio_login_required from app.models import Corpus, CorpusStatus -from . import cqi_extensions -from .utils import CQiOverSocketIOSessionManager +from . import cqi_extension_functions +from .utils import SessionManager ''' @@ -85,6 +85,16 @@ CQI_API_FUNCTION_NAMES = [ ] +CQI_EXTENSION_FUNCTION_NAMES = [ + 'ext_corpus_update_db', + 'ext_corpus_static_data', + 'ext_corpus_paginate_corpus', + 'ext_cqp_paginate_subcorpus', + 'ext_cqp_partial_export_subcorpus', + 'ext_cqp_export_subcorpus', +] + + class CQiOverSocketIONamespace(Namespace): @socketio_login_required def on_connect(self): @@ -135,25 +145,25 @@ class CQiOverSocketIONamespace(Namespace): cqi_client = CQiClient(cqpserver_ip_address) cqi_client_lock = Lock() - CQiOverSocketIOSessionManager.setup() - CQiOverSocketIOSessionManager.set_corpus_id(corpus_id) - CQiOverSocketIOSessionManager.set_cqi_client(cqi_client) - CQiOverSocketIOSessionManager.set_cqi_client_lock(cqi_client_lock) + SessionManager.setup() + SessionManager.set_corpus_id(corpus_id) + SessionManager.set_cqi_client(cqi_client) + SessionManager.set_cqi_client_lock(cqi_client_lock) return {'code': 200, 'msg': 'OK'} @socketio_login_required def on_exec(self, fn_name: str, fn_args: dict = {}) -> dict: try: - cqi_client = CQiOverSocketIOSessionManager.get_cqi_client() - cqi_client_lock = CQiOverSocketIOSessionManager.get_cqi_client_lock() + cqi_client = SessionManager.get_cqi_client() + cqi_client_lock = SessionManager.get_cqi_client_lock() except KeyError: return {'code': 424, 'msg': 'Failed Dependency'} if fn_name in CQI_API_FUNCTION_NAMES: fn = getattr(cqi_client.api, fn_name) - elif fn_name in cqi_extensions.CQI_EXTENSION_FUNCTION_NAMES: - fn = getattr(cqi_extensions, fn_name) + elif fn_name in cqi_extension_functions.CQI_EXTENSION_FUNCTION_NAMES: + fn = getattr(cqi_extension_functions, fn_name) else: return {'code': 400, 'msg': 'Bad Request'} @@ -198,10 +208,10 @@ class CQiOverSocketIONamespace(Namespace): def on_disconnect(self): try: - corpus_id = CQiOverSocketIOSessionManager.get_corpus_id() - cqi_client = CQiOverSocketIOSessionManager.get_cqi_client() - cqi_client_lock = CQiOverSocketIOSessionManager.get_cqi_client_lock() - CQiOverSocketIOSessionManager.teardown() + corpus_id = SessionManager.get_corpus_id() + cqi_client = SessionManager.get_cqi_client() + cqi_client_lock = SessionManager.get_cqi_client_lock() + SessionManager.teardown() except KeyError: return diff --git a/app/namespaces/cqi_over_sio/cqi_extensions.py b/app/namespaces/cqi_over_sio/cqi_extension_functions.py similarity index 95% rename from app/namespaces/cqi_over_sio/cqi_extensions.py rename to app/namespaces/cqi_over_sio/cqi_extension_functions.py index e0322672..ac05fa85 100644 --- a/app/namespaces/cqi_over_sio/cqi_extensions.py +++ b/app/namespaces/cqi_over_sio/cqi_extension_functions.py @@ -8,22 +8,12 @@ import json import math from app import db from app.models import Corpus -from .utils import CQiOverSocketIOSessionManager - - -CQI_EXTENSION_FUNCTION_NAMES = [ - 'ext_corpus_update_db', - 'ext_corpus_static_data', - 'ext_corpus_paginate_corpus', - 'ext_cqp_paginate_subcorpus', - 'ext_cqp_partial_export_subcorpus', - 'ext_cqp_export_subcorpus', -] +from .utils import SessionManager def ext_corpus_update_db(corpus: str) -> CQiStatusOk: - corpus_id = CQiOverSocketIOSessionManager.get_corpus_id() - cqi_client = CQiOverSocketIOSessionManager.get_cqi_client() + corpus_id = SessionManager.get_corpus_id() + cqi_client = SessionManager.get_cqi_client() db_corpus = Corpus.query.get(corpus_id) cqi_corpus = cqi_client.corpora.get(corpus) db_corpus.num_tokens = cqi_corpus.size @@ -32,7 +22,7 @@ def ext_corpus_update_db(corpus: str) -> CQiStatusOk: def ext_corpus_static_data(corpus: str) -> dict: - corpus_id = CQiOverSocketIOSessionManager.get_corpus_id() + corpus_id = SessionManager.get_corpus_id() db_corpus = Corpus.query.get(corpus_id) static_data_file_path = db_corpus.path / 'cwb' / 'static.json.gz' @@ -40,7 +30,7 @@ def ext_corpus_static_data(corpus: str) -> dict: with static_data_file_path.open('rb') as f: return f.read() - cqi_client = CQiOverSocketIOSessionManager.get_cqi_client() + cqi_client = SessionManager.get_cqi_client() cqi_corpus = cqi_client.corpora.get(corpus) cqi_p_attrs = cqi_corpus.positional_attributes.list() cqi_s_attrs = cqi_corpus.structural_attributes.list() @@ -168,7 +158,7 @@ def ext_corpus_paginate_corpus( page: int = 1, per_page: int = 20 ) -> dict: - cqi_client = CQiOverSocketIOSessionManager.get_cqi_client() + cqi_client = SessionManager.get_cqi_client() cqi_corpus = cqi_client.corpora.get(corpus) # Sanity checks if ( @@ -215,7 +205,7 @@ def ext_cqp_paginate_subcorpus( per_page: int = 20 ) -> dict: corpus_name, subcorpus_name = subcorpus.split(':', 1) - cqi_client = CQiOverSocketIOSessionManager.get_cqi_client() + cqi_client = SessionManager.get_cqi_client() cqi_corpus = cqi_client.corpora.get(corpus_name) cqi_subcorpus = cqi_corpus.subcorpora.get(subcorpus_name) # Sanity checks @@ -262,7 +252,7 @@ def ext_cqp_partial_export_subcorpus( context: int = 50 ) -> dict: corpus_name, subcorpus_name = subcorpus.split(':', 1) - cqi_client = CQiOverSocketIOSessionManager.get_cqi_client() + cqi_client = SessionManager.get_cqi_client() cqi_corpus = cqi_client.corpora.get(corpus_name) cqi_subcorpus = cqi_corpus.subcorpora.get(subcorpus_name) cqi_subcorpus_partial_export = _partial_export_subcorpus(cqi_subcorpus, match_id_list, context=context) @@ -271,7 +261,7 @@ def ext_cqp_partial_export_subcorpus( def ext_cqp_export_subcorpus(subcorpus: str, context: int = 50) -> dict: corpus_name, subcorpus_name = subcorpus.split(':', 1) - cqi_client = CQiOverSocketIOSessionManager.get_cqi_client() + cqi_client = SessionManager.get_cqi_client() cqi_corpus = cqi_client.corpora.get(corpus_name) cqi_subcorpus = cqi_corpus.subcorpora.get(subcorpus_name) cqi_subcorpus_export = _export_subcorpus(cqi_subcorpus, context=context) diff --git a/app/namespaces/cqi_over_sio/utils.py b/app/namespaces/cqi_over_sio/utils.py index 5bef9f82..c94f0e0d 100644 --- a/app/namespaces/cqi_over_sio/utils.py +++ b/app/namespaces/cqi_over_sio/utils.py @@ -3,7 +3,7 @@ from threading import Lock from flask import session -class CQiOverSocketIOSessionManager: +class SessionManager: @staticmethod def setup(): session['cqi_over_sio'] = {} diff --git a/app/namespaces/users.py b/app/namespaces/users.py new file mode 100644 index 00000000..03f017ef --- /dev/null +++ b/app/namespaces/users.py @@ -0,0 +1,116 @@ +from flask import current_app, Flask +from flask_login import current_user +from flask_socketio import join_room, leave_room, Namespace +from app import db, hashids, socketio +from app.decorators import socketio_login_required +from app.models import User + + +def _delete_user(app: Flask, user_id: int): + with app.app_context(): + user = User.query.get(user_id) + user.delete() + db.session.commit() + + +class UsersNamespace(Namespace): + @socketio_login_required + def on_get(self, user_hashid: str) -> dict: + user_id = hashids.decode(user_hashid) + + if not isinstance(user_id, int): + return {'status': 400, 'statusText': 'Bad Request'} + + user = User.query.get(user_id) + + if user is None: + return {'status': 404, 'statusText': 'Not found'} + + if not ( + user == current_user + or current_user.is_administrator + ): + return {'status': 403, 'statusText': 'Forbidden'} + + return { + 'body': user.to_json_serializeable( + backrefs=True, + relationships=True + ), + 'status': 200, + 'statusText': 'OK' + } + + @socketio_login_required + def on_subscribe(self, user_hashid: str) -> dict: + user_id = hashids.decode(user_hashid) + + if not isinstance(user_id, int): + return {'status': 400, 'statusText': 'Bad Request'} + + user = User.query.get(user_id) + + if user is None: + return {'status': 404, 'statusText': 'Not found'} + + if not ( + user == current_user + or current_user.is_administrator + ): + return {'status': 403, 'statusText': 'Forbidden'} + + join_room(f'/users/{user.hashid}') + + return {'status': 200, 'statusText': 'OK'} + + @socketio_login_required + def on_unsubscribe(self, user_hashid: str) -> dict: + user_id = hashids.decode(user_hashid) + + if not isinstance(user_id, int): + return {'status': 400, 'statusText': 'Bad Request'} + + user = User.query.get(user_id) + + if user is None: + return {'status': 404, 'statusText': 'Not found'} + + if not ( + user == current_user + or current_user.is_administrator + ): + return {'status': 403, 'statusText': 'Forbidden'} + + leave_room(f'/users/{user.hashid}') + + return {'status': 200, 'statusText': 'OK'} + + @socketio_login_required + def on_delete(self, user_hashid: str) -> dict: + user_id = hashids.decode(user_hashid) + + if not isinstance(user_id, int): + return {'status': 400, 'statusText': 'Bad Request'} + + user = User.query.get(user_id) + + if user is None: + return {'status': 404, 'statusText': 'Not found'} + + if not ( + user == current_user + or current_user.is_administrator + ): + return {'status': 403, 'statusText': 'Forbidden'} + + socketio.start_background_task( + _delete_user, + current_app._get_current_object(), + user.id + ) + + return { + 'body': f'User "{user.username}" marked for deletion', + 'status': 202, + 'statusText': 'Accepted' + } diff --git a/app/static/js/app.js b/app/static/js/app.js index 439bb9c1..e8ffddfc 100644 --- a/app/static/js/app.js +++ b/app/static/js/app.js @@ -1,10 +1,9 @@ nopaque.App = class App { constructor() { - this.data = {}; - this.socket = io({transports: ['websocket'], upgrade: false}); this.ui = new nopaque.UIExtension(this); + this.liveUserRegistry = new nopaque.LiveUserRegistryExtension(this); this.users = new nopaque.UsersExtension(this); } @@ -29,5 +28,7 @@ nopaque.App = class App { init() { this.ui.init(); + this.liveUserRegistry.init(); + this.users.init(); } }; diff --git a/app/static/js/app.users.js b/app/static/js/app.users.js deleted file mode 100644 index fc3f0d3e..00000000 --- a/app/static/js/app.users.js +++ /dev/null @@ -1,53 +0,0 @@ -nopaque.UsersExtension = class UsersExtension { - #data; - #promises; - - constructor(app) { - this.app = app; - - this.#data = {}; - this.app.data.users = this.#data; - - this.#promises = { - get: {}, - subscribe: {} - }; - } - - async #get(userId) { - const response = await this.app.socket.emitWithAck('users.get', userId); - - if (response.status != 200) { - throw new Error(`[${response.status}] ${response.statusText}`); - } - - this.#data[userId] = response.body; - return this.#data[userId]; - } - - get(userId) { - if (userId in this.#promises.get) { - return this.#promises.get[userId]; - } - - this.#promises.get[userId] = this.#get(userId); - return this.#promises.get[userId]; - } - - async #subscribe(userId) { - const response = await this.app.socket.emitWithAck('users.subscribe', userId); - - if (response.status != 200) { - throw new Error(`[${response.status}] ${response.statusText}`); - } - } - - subscribe(userId) { - if (userId in this.#promises.subscribe) { - return this.#promises.subscribe[userId]; - } - - this.#promises.subscribe[userId] = this.#subscribe(userId); - return this.#promises.subscribe[userId]; - } -} diff --git a/app/static/js/app.ui.js b/app/static/js/app/ui.js similarity index 100% rename from app/static/js/app.ui.js rename to app/static/js/app/ui.js diff --git a/app/static/js/app/user-live-registry.js b/app/static/js/app/user-live-registry.js new file mode 100644 index 00000000..27c1fe34 --- /dev/null +++ b/app/static/js/app/user-live-registry.js @@ -0,0 +1,70 @@ +nopaque.LiveUserRegistryExtension = class LiveUserRegistryExtension extends EventTarget { + #data; + + constructor(app) { + super(); + + this.app = app; + + this.#data = { + users: {}, + promises: {} + }; + } + + init() { + this.app.users.socket.on('patch', (patch) => {this.#onPatch(patch)}); + } + + add(userId) { + if (!(userId in this.#data.promises)) { + this.#data.promises[userId] = this.#add(userId); + } + + return this.#data.promises[userId]; + } + + async #add(userId) { + await this.app.users.subscribe(userId); + this.#data.users[userId] = await this.app.users.get(userId); + } + + async get(userId) { + await this.add(userId); + return this.#data.users[userId]; + } + + #onPatch(patch) { + // Filter patch to only include operations on users that are initialized + let filterRegExp = new RegExp(`^/users/(${Object.keys(this.#data.users).join('|')})`); + let filteredPatch = patch.filter(operation => filterRegExp.test(operation.path)); + + // Apply patch + jsonpatch.applyPatch(this.#data, filteredPatch); + + // Notify event listeners + let event = new CustomEvent('patch', {detail: filteredPatch}); + this.dispatchEvent(event); + + /* + // Notify event listeners. Event type: "patch *" + let event = new CustomEvent('patch *', {detail: filteredPatch}); + this.dispatchEvent(event); + + // Group patches by user id: {: [op, ...], ...} + let patches = {}; + let matchRegExp = new RegExp(`^/users/([A-Za-z0-9]+)`); + for (let operation of filteredPatch) { + let [match, userId] = operation.path.match(matchRegExp); + if (!(userId in patches)) {patches[userId] = [];} + patches[userId].push(operation); + } + + // Notify event listeners. Event type: "patch " + for (let [userId, patch] of Object.entries(patches)) { + let event = new CustomEvent(`patch ${userId}`, {detail: patch}); + this.dispatchEvent(event); + } + */ + } +} diff --git a/app/static/js/app/users.js b/app/static/js/app/users.js new file mode 100644 index 00000000..0b98f95d --- /dev/null +++ b/app/static/js/app/users.js @@ -0,0 +1,43 @@ +nopaque.UsersExtension = class UsersExtension { + constructor(app) { + this.app = app; + + this.socket = io('/users', {transports: ['websocket'], upgrade: false}); + } + + init() {} + + async get(userId) { + const response = await this.socket.emitWithAck('get', userId); + + if (response.status !== 200) { + throw new Error(`[${response.status}] ${response.statusText}`); + } + + return response.body; + } + + async subscribe(userId) { + const response = await this.socket.emitWithAck('subscribe', userId); + + if (response.status != 200) { + throw new Error(`[${response.status}] ${response.statusText}`); + } + } + + async unsubscribe(userId) { + const response = await this.socket.emitWithAck('unsubscribe', userId); + + if (response.status != 200) { + throw new Error(`[${response.status}] ${response.statusText}`); + } + } + + async delete(userId) { + const response = await this.socket.emitWithAck('delete', userId); + + if (response.status != 202) { + throw new Error(`[${response.status}] ${response.statusText}`); + } + } +} diff --git a/app/static/js/resource-displays/resource-display.js b/app/static/js/resource-displays/resource-display.js index e1036ef9..9591834e 100644 --- a/app/static/js/resource-displays/resource-display.js +++ b/app/static/js/resource-displays/resource-display.js @@ -5,19 +5,14 @@ nopaque.resource_displays.ResourceDisplay = class ResourceDisplay { this.displayElement = displayElement; this.userId = this.displayElement.dataset.userId; this.isInitialized = false; - if (this.userId) { - app.users.subscribe(this.userId) - .then((response) => { - app.socket.on('users.patch', (patch) => { - if (this.isInitialized) {this.onPatch(patch);} - }); - }); - app.users.get(this.userId) - .then((user) => { - this.init(user); - this.isInitialized = true; - }); - } + if (this.userId === undefined) {return;} + app.liveUserRegistry.addEventListener('patch', (event) => { + if (this.isInitialized) {this.onPatch(event.detail);} + }); + app.liveUserRegistry.get(this.userId).then((user) => { + this.init(user); + this.isInitialized = true; + }); } init(user) {throw 'Not implemented';} diff --git a/app/static/js/resource-lists/corpus-file-list.js b/app/static/js/resource-lists/corpus-file-list.js index 3d819398..88761a47 100644 --- a/app/static/js/resource-lists/corpus-file-list.js +++ b/app/static/js/resource-lists/corpus-file-list.js @@ -14,12 +14,11 @@ nopaque.resource_lists.CorpusFileList = class CorpusFileList extends nopaque.res this.hasPermissionView = listContainerElement.dataset?.hasPermissionView == 'true' || false; this.hasPermissionManageFiles = listContainerElement.dataset?.hasPermissionManageFiles == 'true' || false; if (this.userId === undefined || this.corpusId === undefined) {return;} - app.users.subscribe(this.userId).then((response) => { - app.socket.on('users.patch', (patch) => { - if (this.isInitialized) {this.onPatch(patch);} - }); + app.liveUserRegistry.addEventListener('patch', (event) => { + if (this.isInitialized) {this.onPatch(event.detail);} }); - app.users.get(this.userId).then((user) => { + app.liveUserRegistry.get(this.userId).then((user) => { + // TODO: Make this better understandable this.add(Object.values(user.corpora[this.corpusId].files || user.followed_corpora[this.corpusId].files)); this.isInitialized = true; }); diff --git a/app/static/js/resource-lists/corpus-follower-list.js b/app/static/js/resource-lists/corpus-follower-list.js index 98c78605..8aa1f4d0 100644 --- a/app/static/js/resource-lists/corpus-follower-list.js +++ b/app/static/js/resource-lists/corpus-follower-list.js @@ -12,15 +12,16 @@ nopaque.resource_lists.CorpusFollowerList = class CorpusFollowerList extends nop this.userId = listContainerElement.dataset.userId; this.corpusId = listContainerElement.dataset.corpusId; if (this.userId === undefined || this.corpusId === undefined) {return;} - app.users.subscribe(this.userId).then((response) => { - app.socket.on('users.patch', (patch) => { - if (this.isInitialized) {this.onPatch(patch);} - }); + app.liveUserRegistry.addEventListener('patch', (event) => { + if (this.isInitialized) {this.onPatch(event.detail);} }); - app.users.get(this.userId).then((user) => { + app.liveUserRegistry.get(this.userId).then((user) => { + // TODO: Check if the following is better // let corpusFollowerAssociations = Object.values(user.corpora[this.corpusId].corpus_follower_associations); // let filteredList = corpusFollowerAssociations.filter(association => association.follower.id != currentUserId); // this.add(filteredList); + + // TODO: Make this better understandable this.add(Object.values(user.corpora[this.corpusId].corpus_follower_associations)); this.isInitialized = true; }); diff --git a/app/static/js/resource-lists/corpus-list.js b/app/static/js/resource-lists/corpus-list.js index 6fac836f..21060dbc 100644 --- a/app/static/js/resource-lists/corpus-list.js +++ b/app/static/js/resource-lists/corpus-list.js @@ -11,12 +11,10 @@ nopaque.resource_lists.CorpusList = class CorpusList extends nopaque.resource_li this.selectedItemIds = new Set(); this.userId = listContainerElement.dataset.userId; if (this.userId === undefined) {return;} - app.users.subscribe(this.userId).then((response) => { - app.socket.on('users.patch', (patch) => { - if (this.isInitialized) {this.onPatch(patch);} - }); + app.liveUserRegistry.addEventListener('patch', (event) => { + if (this.isInitialized) {this.onPatch(event.detail);} }); - app.users.get(this.userId).then((user) => { + app.liveUserRegistry.get(this.userId).then((user) => { this.add(this.aggregateData(user)); this.isInitialized = true; }); diff --git a/app/static/js/resource-lists/job-input-list.js b/app/static/js/resource-lists/job-input-list.js index 7de10e45..7c74e852 100644 --- a/app/static/js/resource-lists/job-input-list.js +++ b/app/static/js/resource-lists/job-input-list.js @@ -8,8 +8,10 @@ nopaque.resource_lists.JobInputList = class JobInputList extends nopaque.resourc this.userId = listContainerElement.dataset.userId; this.jobId = listContainerElement.dataset.jobId; if (this.userId === undefined || this.jobId === undefined) {return;} - app.users.subscribe(this.userId); - app.users.get(this.userId).then((user) => { + // app.liveUserRegistry.addEventListener('patch', (event) => { + // if (this.isInitialized) {this.onPatch(event.detail);} + // }); + app.liveUserRegistry.get(this.userId).then((user) => { this.add(Object.values(user.jobs[this.jobId].inputs)); this.isInitialized = true; }); diff --git a/app/static/js/resource-lists/job-list.js b/app/static/js/resource-lists/job-list.js index a3fb87f1..8c352e7e 100644 --- a/app/static/js/resource-lists/job-list.js +++ b/app/static/js/resource-lists/job-list.js @@ -12,12 +12,10 @@ nopaque.resource_lists.JobList = class JobList extends nopaque.resource_lists.Re this.selectedItemIds = new Set(); this.userId = listContainerElement.dataset.userId; if (this.userId === undefined) {return;} - app.users.subscribe(this.userId).then((response) => { - app.socket.on('users.patch', (patch) => { - if (this.isInitialized) {this.onPatch(patch);} - }); + app.liveUserRegistry.addEventListener('patch', (event) => { + if (this.isInitialized) {this.onPatch(event.detail);} }); - app.users.get(this.userId).then((user) => { + app.liveUserRegistry.get(this.userId).then((user) => { this.add(Object.values(user.jobs)); this.isInitialized = true; }); diff --git a/app/static/js/resource-lists/job-result-list.js b/app/static/js/resource-lists/job-result-list.js index bc48e9a2..be3d618c 100644 --- a/app/static/js/resource-lists/job-result-list.js +++ b/app/static/js/resource-lists/job-result-list.js @@ -8,12 +8,10 @@ nopaque.resource_lists.JobResultList = class JobResultList extends nopaque.resou this.userId = listContainerElement.dataset.userId; this.jobId = listContainerElement.dataset.jobId; if (this.userId === undefined || this.jobId === undefined) {return;} - app.users.subscribe(this.userId).then((response) => { - app.socket.on('users.patch', (patch) => { - if (this.isInitialized) {this.onPatch(patch);} - }); + app.liveUserRegistry.addEventListener('patch', (event) => { + if (this.isInitialized) {this.onPatch(event.detail);} }); - app.users.get(this.userId).then((user) => { + app.liveUserRegistry.get(this.userId).then((user) => { this.add(Object.values(user.jobs[this.jobId].results)); this.isInitialized = true; }); diff --git a/app/static/js/resource-lists/spacy-nlp-pipeline-model-list.js b/app/static/js/resource-lists/spacy-nlp-pipeline-model-list.js index 6b9f5e2a..e44e35b9 100644 --- a/app/static/js/resource-lists/spacy-nlp-pipeline-model-list.js +++ b/app/static/js/resource-lists/spacy-nlp-pipeline-model-list.js @@ -8,12 +8,10 @@ nopaque.resource_lists.SpaCyNLPPipelineModelList = class SpaCyNLPPipelineModelLi this.isInitialized = false; this.userId = listContainerElement.dataset.userId; if (this.userId === undefined) {return;} - app.users.subscribe(this.userId).then((response) => { - app.socket.on('users.patch', (patch) => { - if (this.isInitialized) {this.onPatch(patch);} - }); + app.liveUserRegistry.addEventListener('patch', (event) => { + if (this.isInitialized) {this.onPatch(event.detail);} }); - app.users.get(this.userId).then((user) => { + app.liveUserRegistry.get(this.userId).then((user) => { this.add(Object.values(user.spacy_nlp_pipeline_models)); this.isInitialized = true; }); diff --git a/app/static/js/resource-lists/tesseract-ocr-pipeline-model-list.js b/app/static/js/resource-lists/tesseract-ocr-pipeline-model-list.js index 609dc1d1..0b0cd384 100644 --- a/app/static/js/resource-lists/tesseract-ocr-pipeline-model-list.js +++ b/app/static/js/resource-lists/tesseract-ocr-pipeline-model-list.js @@ -8,21 +8,11 @@ nopaque.resource_lists.TesseractOCRPipelineModelList = class TesseractOCRPipelin this.isInitialized = false; this.userId = listContainerElement.dataset.userId; if (this.userId === undefined) {return;} - app.users.subscribe(this.userId).then((response) => { - app.socket.on('users.patch', (patch) => { - if (this.isInitialized) {this.onPatch(patch);} - }); + app.liveUserRegistry.addEventListener('patch', (event) => { + if (this.isInitialized) {this.onPatch(event.detail);} }); - app.users.get(this.userId).then((user) => { + app.liveUserRegistry.get(this.userId).then((user) => { this.add(Object.values(user.tesseract_ocr_pipeline_models)); - for (let uncheckedCheckbox of this.listjs.list.querySelectorAll('input[data-checked="True"]')) { - uncheckedCheckbox.setAttribute('checked', ''); - } - if (user.role.name !== ('Administrator' || 'Contributor')) { - for (let switchElement of this.listjs.list.querySelectorAll('.is_public')) { - switchElement.setAttribute('disabled', ''); - } - } this.isInitialized = true; }); } diff --git a/app/templates/_base/scripts.html.j2 b/app/templates/_base/scripts.html.j2 index fd37daef..9559ab1c 100644 --- a/app/templates/_base/scripts.html.j2 +++ b/app/templates/_base/scripts.html.j2 @@ -9,8 +9,9 @@ output='gen/nopaque.%(version)s.js', 'js/index.js', 'js/app.js', - 'js/app.ui.js', - 'js/app.users.js', + 'js/app/ui.js', + 'js/app/user-live-registry.js', + 'js/app/users.js', 'js/utils.js', 'js/forms/index.js', @@ -75,41 +76,35 @@ {% endassets -%}