mirror of
https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
synced 2024-12-26 03:14:19 +00:00
Update JS code structure
This commit is contained in:
parent
a2904caea2
commit
12a3ac1d5d
@ -1,11 +1,51 @@
|
|||||||
|
from flask import current_app, Flask
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_socketio import join_room, leave_room
|
from flask_socketio import join_room, leave_room
|
||||||
from app import hashids, socketio
|
from app import db, hashids, socketio
|
||||||
from app.decorators import socketio_login_required
|
from app.decorators import socketio_login_required
|
||||||
from app.models import User
|
from app.models import User
|
||||||
|
|
||||||
|
|
||||||
@socketio.on('users.get_user')
|
def _delete_user(app: Flask, user_id: int):
|
||||||
|
with app.app_context():
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
user.delete()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@socketio.on('users.delete')
|
||||||
|
@socketio_login_required
|
||||||
|
def delete_user(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'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@socketio.on('users.get')
|
||||||
@socketio_login_required
|
@socketio_login_required
|
||||||
def get_user(user_hashid: str) -> dict:
|
def get_user(user_hashid: str) -> dict:
|
||||||
user_id = hashids.decode(user_hashid)
|
user_id = hashids.decode(user_hashid)
|
||||||
@ -34,7 +74,7 @@ def get_user(user_hashid: str) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@socketio.on('users.subscribe_user')
|
@socketio.on('users.subscribe')
|
||||||
@socketio_login_required
|
@socketio_login_required
|
||||||
def subscribe_user(user_hashid: str) -> dict:
|
def subscribe_user(user_hashid: str) -> dict:
|
||||||
user_id = hashids.decode(user_hashid)
|
user_id = hashids.decode(user_hashid)
|
||||||
@ -58,9 +98,9 @@ def subscribe_user(user_hashid: str) -> dict:
|
|||||||
return {'status': 200, 'statusText': 'OK'}
|
return {'status': 200, 'statusText': 'OK'}
|
||||||
|
|
||||||
|
|
||||||
@socketio.on('users.unsubscribe_user')
|
@socketio.on('users.unsubscribe')
|
||||||
@socketio_login_required
|
@socketio_login_required
|
||||||
def on_unsubscribe_user(user_hashid: str) -> dict:
|
def unsubscribe_user(user_hashid: str) -> dict:
|
||||||
user_id = hashids.decode(user_hashid)
|
user_id = hashids.decode(user_hashid)
|
||||||
|
|
||||||
if not isinstance(user_id, int):
|
if not isinstance(user_id, int):
|
||||||
|
@ -43,7 +43,7 @@ def resource_after_delete(mapper, connection, resource):
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
room = f'/users/{resource.user_hashid}'
|
room = f'/users/{resource.user_hashid}'
|
||||||
socketio.emit('patch_user', jsonpatch, room=room)
|
socketio.emit('users.patch', jsonpatch, room=room)
|
||||||
|
|
||||||
|
|
||||||
def cfa_after_delete(mapper, connection, cfa):
|
def cfa_after_delete(mapper, connection, cfa):
|
||||||
@ -55,7 +55,7 @@ def cfa_after_delete(mapper, connection, cfa):
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
room = f'/users/{cfa.corpus.user.hashid}'
|
room = f'/users/{cfa.corpus.user.hashid}'
|
||||||
socketio.emit('patch_user', jsonpatch, room=room)
|
socketio.emit('users.patch', jsonpatch, room=room)
|
||||||
|
|
||||||
|
|
||||||
def resource_after_insert(mapper, connection, resource):
|
def resource_after_insert(mapper, connection, resource):
|
||||||
@ -70,7 +70,7 @@ def resource_after_insert(mapper, connection, resource):
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
room = f'/users/{resource.user_hashid}'
|
room = f'/users/{resource.user_hashid}'
|
||||||
socketio.emit('patch_user', jsonpatch, room=room)
|
socketio.emit('users.patch', jsonpatch, room=room)
|
||||||
|
|
||||||
|
|
||||||
def cfa_after_insert(mapper, connection, cfa):
|
def cfa_after_insert(mapper, connection, cfa):
|
||||||
@ -84,7 +84,7 @@ def cfa_after_insert(mapper, connection, cfa):
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
room = f'/users/{cfa.corpus.user.hashid}'
|
room = f'/users/{cfa.corpus.user.hashid}'
|
||||||
socketio.emit('patch_user', jsonpatch, room=room)
|
socketio.emit('users.patch', jsonpatch, room=room)
|
||||||
|
|
||||||
|
|
||||||
def resource_after_update(mapper, connection, resource):
|
def resource_after_update(mapper, connection, resource):
|
||||||
@ -110,7 +110,7 @@ def resource_after_update(mapper, connection, resource):
|
|||||||
)
|
)
|
||||||
if jsonpatch:
|
if jsonpatch:
|
||||||
room = f'/users/{resource.user_hashid}'
|
room = f'/users/{resource.user_hashid}'
|
||||||
socketio.emit('patch_user', jsonpatch, room=room)
|
socketio.emit('users.patch', jsonpatch, room=room)
|
||||||
|
|
||||||
|
|
||||||
def job_after_update(mapper, connection, job):
|
def job_after_update(mapper, connection, job):
|
||||||
|
@ -1,201 +1,33 @@
|
|||||||
nopaque.App = class App {
|
nopaque.App = class App {
|
||||||
#promises;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.data = {
|
this.data = {};
|
||||||
users: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.#promises = {
|
|
||||||
getUser: {},
|
|
||||||
subscribeUser: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.socket = io({transports: ['websocket'], upgrade: false});
|
this.socket = io({transports: ['websocket'], upgrade: false});
|
||||||
|
|
||||||
this.socket.on('patch_user', (patch) => {this.onPatch(patch);});
|
this.ui = new nopaque.UIExtension(this);
|
||||||
|
this.users = new nopaque.UsersExtension(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
getUser(userId) {
|
// onPatch(patch) {
|
||||||
if (userId in this.#promises.getUser) {
|
// // Filter Patch to only include operations on users that are initialized
|
||||||
return this.#promises.getUser[userId];
|
// let regExp = new RegExp(`^/users/(${Object.keys(this.data.users).join('|')})`);
|
||||||
}
|
// let filteredPatch = patch.filter(operation => regExp.test(operation.path));
|
||||||
|
|
||||||
this.#promises.getUser[userId] = new Promise((resolve, reject) => {
|
// // Handle job status updates
|
||||||
this.socket.emit('users.get_user', userId, (response) => {
|
// let subRegExp = new RegExp(`^/users/([A-Za-z0-9]*)/jobs/([A-Za-z0-9]*)/status$`);
|
||||||
if (response.status === 200) {
|
// let subFilteredPatch = filteredPatch
|
||||||
this.data.users[userId] = response.body;
|
// .filter((operation) => {return operation.op === 'replace';})
|
||||||
resolve(this.data.users[userId]);
|
// .filter((operation) => {return subRegExp.test(operation.path);});
|
||||||
} else {
|
// for (let operation of subFilteredPatch) {
|
||||||
reject(`[${response.status}] ${response.statusText}`);
|
// let [match, userId, jobId] = operation.path.match(subRegExp);
|
||||||
}
|
// this.flash(`[<a href="/jobs/${jobId}">${this.data.users[userId].jobs[jobId].title}</a>] New status: <span class="job-status-text" data-job-status="${operation.value}"></span>`, 'job');
|
||||||
});
|
// }
|
||||||
});
|
|
||||||
|
|
||||||
return this.#promises.getUser[userId];
|
// // Apply Patch
|
||||||
}
|
// jsonpatch.applyPatch(this.data, filteredPatch);
|
||||||
|
// }
|
||||||
subscribeUser(userId) {
|
|
||||||
if (userId in this.#promises.subscribeUser) {
|
|
||||||
return this.#promises.subscribeUser[userId];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#promises.subscribeUser[userId] = new Promise((resolve, reject) => {
|
|
||||||
this.socket.emit('users.subscribe_user', userId, (response) => {
|
|
||||||
if (response.status === 200) {
|
|
||||||
resolve(response);
|
|
||||||
} else {
|
|
||||||
reject(response);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.#promises.subscribeUser[userId];
|
|
||||||
}
|
|
||||||
|
|
||||||
flash(message, category) {
|
|
||||||
let iconPrefix = '';
|
|
||||||
switch (category) {
|
|
||||||
case 'corpus': {
|
|
||||||
iconPrefix = '<i class="left material-icons">book</i>';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'error': {
|
|
||||||
iconPrefix = '<i class="error-color-text left material-icons">error</i>';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'job': {
|
|
||||||
iconPrefix = '<i class="left nopaque-icons">J</i>';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'settings': {
|
|
||||||
iconPrefix = '<i class="left material-icons">settings</i>';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
iconPrefix = '<i class="left material-icons">notifications</i>';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let toast = M.toast(
|
|
||||||
{
|
|
||||||
html: `
|
|
||||||
<span>${iconPrefix}${message}</span>
|
|
||||||
<button class="action-button btn-flat toast-action white-text" data-action="close">
|
|
||||||
<i class="material-icons">close</i>
|
|
||||||
</button>
|
|
||||||
`.trim()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
let toastCloseActionElement = toast.el.querySelector('.action-button[data-action="close"]');
|
|
||||||
toastCloseActionElement.addEventListener('click', () => {toast.dismiss();});
|
|
||||||
}
|
|
||||||
|
|
||||||
onPatch(patch) {
|
|
||||||
// Filter Patch to only include operations on users that are initialized
|
|
||||||
let regExp = new RegExp(`^/users/(${Object.keys(this.data.users).join('|')})`);
|
|
||||||
let filteredPatch = patch.filter(operation => regExp.test(operation.path));
|
|
||||||
|
|
||||||
// Handle job status updates
|
|
||||||
let subRegExp = new RegExp(`^/users/([A-Za-z0-9]*)/jobs/([A-Za-z0-9]*)/status$`);
|
|
||||||
let subFilteredPatch = filteredPatch
|
|
||||||
.filter((operation) => {return operation.op === 'replace';})
|
|
||||||
.filter((operation) => {return subRegExp.test(operation.path);});
|
|
||||||
for (let operation of subFilteredPatch) {
|
|
||||||
let [match, userId, jobId] = operation.path.match(subRegExp);
|
|
||||||
this.flash(`[<a href="/jobs/${jobId}">${this.data.users[userId].jobs[jobId].title}</a>] New status: <span class="job-status-text" data-job-status="${operation.value}"></span>`, 'job');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply Patch
|
|
||||||
jsonpatch.applyPatch(this.data, filteredPatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.initUi();
|
this.ui.init();
|
||||||
}
|
|
||||||
|
|
||||||
initUi() {
|
|
||||||
/* Pre-Initialization fixes */
|
|
||||||
// #region
|
|
||||||
|
|
||||||
// Flask-WTF sets the standard HTML maxlength Attribute on input/textarea
|
|
||||||
// elements to specify their maximum length (in characters). Unfortunatly
|
|
||||||
// Materialize won't recognize the maxlength Attribute, instead it uses
|
|
||||||
// the data-length Attribute. It's conversion time :)
|
|
||||||
for (let elem of document.querySelectorAll('input[maxlength], textarea[maxlength]')) {
|
|
||||||
elem.dataset.length = elem.getAttribute('maxlength');
|
|
||||||
elem.removeAttribute('maxlength');
|
|
||||||
}
|
|
||||||
|
|
||||||
// To work around some limitations with the Form setup of Flask-WTF.
|
|
||||||
// HTML option elements with an empty value are considered as placeholder
|
|
||||||
// elements. The user should not be able to actively select these options.
|
|
||||||
// So they get the disabled attribute.
|
|
||||||
for (let optionElement of document.querySelectorAll('option[value=""]')) {
|
|
||||||
optionElement.disabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Check why we are doing this.
|
|
||||||
for (let optgroupElement of document.querySelectorAll('optgroup[label=""]')) {
|
|
||||||
for (let c of optgroupElement.children) {
|
|
||||||
optgroupElement.parentElement.insertAdjacentElement('afterbegin', c);
|
|
||||||
}
|
|
||||||
optgroupElement.remove();
|
|
||||||
}
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
|
|
||||||
/* Initialize Materialize Components */
|
|
||||||
// #region
|
|
||||||
|
|
||||||
// Automatically initialize Materialize Components that do not require
|
|
||||||
// additional configuration.
|
|
||||||
M.AutoInit();
|
|
||||||
|
|
||||||
// CharacterCounters
|
|
||||||
// Materialize didn't include the CharacterCounter plugin within the
|
|
||||||
// AutoInit method (maybe they forgot it?). Anyway... We do it here. :)
|
|
||||||
M.CharacterCounter.init(document.querySelectorAll('input[data-length]:not(.no-autoinit), textarea[data-length]:not(.no-autoinit)'));
|
|
||||||
|
|
||||||
// Header navigation processes and services Dropdown.
|
|
||||||
M.Dropdown.init(
|
|
||||||
document.querySelector('#navbar-data-processing-and-analysis-dropdown-trigger'),
|
|
||||||
{
|
|
||||||
constrainWidth: false,
|
|
||||||
container: document.querySelector('#dropdowns'),
|
|
||||||
coverTrigger: false
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Header navigation account Dropdown.
|
|
||||||
M.Dropdown.init(
|
|
||||||
document.querySelector('#navbar-account-dropdown-trigger'),
|
|
||||||
{
|
|
||||||
alignment: 'right',
|
|
||||||
constrainWidth: false,
|
|
||||||
container: document.querySelector('#dropdowns'),
|
|
||||||
coverTrigger: false
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Terms of use modal
|
|
||||||
M.Modal.init(
|
|
||||||
document.querySelector('#terms-of-use-modal'),
|
|
||||||
{
|
|
||||||
dismissible: false,
|
|
||||||
onCloseEnd: (modalElement) => {
|
|
||||||
nopaque.requests.users.entity.acceptTermsOfUse();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
|
|
||||||
/* Initialize nopaque Components */
|
|
||||||
// #region
|
|
||||||
nopaque.resource_displays.AutoInit();
|
|
||||||
nopaque.resource_lists.AutoInit();
|
|
||||||
nopaque.forms.AutoInit();
|
|
||||||
// #endregion
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
126
app/static/js/app.ui.js
Normal file
126
app/static/js/app.ui.js
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
nopaque.UIExtension = class UIExtension {
|
||||||
|
constructor(app) {
|
||||||
|
this.app = app;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
/* Pre-Initialization fixes */
|
||||||
|
// #region
|
||||||
|
|
||||||
|
// Flask-WTF sets the standard HTML maxlength Attribute on input/textarea
|
||||||
|
// elements to specify their maximum length (in characters). Unfortunatly
|
||||||
|
// Materialize won't recognize the maxlength Attribute, instead it uses
|
||||||
|
// the data-length Attribute. It's conversion time :)
|
||||||
|
for (let elem of document.querySelectorAll('input[maxlength], textarea[maxlength]')) {
|
||||||
|
elem.dataset.length = elem.getAttribute('maxlength');
|
||||||
|
elem.removeAttribute('maxlength');
|
||||||
|
}
|
||||||
|
|
||||||
|
// To work around some limitations with the Form setup of Flask-WTF.
|
||||||
|
// HTML option elements with an empty value are considered as placeholder
|
||||||
|
// elements. The user should not be able to actively select these options.
|
||||||
|
// So they get the disabled attribute.
|
||||||
|
for (let optionElement of document.querySelectorAll('option[value=""]')) {
|
||||||
|
optionElement.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Check why we are doing this.
|
||||||
|
for (let optgroupElement of document.querySelectorAll('optgroup[label=""]')) {
|
||||||
|
for (let c of optgroupElement.children) {
|
||||||
|
optgroupElement.parentElement.insertAdjacentElement('afterbegin', c);
|
||||||
|
}
|
||||||
|
optgroupElement.remove();
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
|
||||||
|
/* Initialize Materialize Components */
|
||||||
|
// #region
|
||||||
|
|
||||||
|
// Automatically initialize Materialize Components that do not require
|
||||||
|
// additional configuration.
|
||||||
|
M.AutoInit();
|
||||||
|
|
||||||
|
// CharacterCounters
|
||||||
|
// Materialize didn't include the CharacterCounter plugin within the
|
||||||
|
// AutoInit method (maybe they forgot it?). Anyway... We do it here. :)
|
||||||
|
M.CharacterCounter.init(document.querySelectorAll('input[data-length]:not(.no-autoinit), textarea[data-length]:not(.no-autoinit)'));
|
||||||
|
|
||||||
|
// Header navigation processes and services Dropdown.
|
||||||
|
M.Dropdown.init(
|
||||||
|
document.querySelector('#navbar-data-processing-and-analysis-dropdown-trigger'),
|
||||||
|
{
|
||||||
|
constrainWidth: false,
|
||||||
|
container: document.querySelector('#dropdowns'),
|
||||||
|
coverTrigger: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Header navigation account Dropdown.
|
||||||
|
M.Dropdown.init(
|
||||||
|
document.querySelector('#navbar-account-dropdown-trigger'),
|
||||||
|
{
|
||||||
|
alignment: 'right',
|
||||||
|
constrainWidth: false,
|
||||||
|
container: document.querySelector('#dropdowns'),
|
||||||
|
coverTrigger: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Terms of use modal
|
||||||
|
M.Modal.init(
|
||||||
|
document.querySelector('#terms-of-use-modal'),
|
||||||
|
{
|
||||||
|
dismissible: false,
|
||||||
|
onCloseEnd: (modalElement) => {
|
||||||
|
nopaque.requests.users.entity.acceptTermsOfUse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
|
||||||
|
/* Initialize nopaque Components */
|
||||||
|
// #region
|
||||||
|
nopaque.resource_displays.AutoInit();
|
||||||
|
nopaque.resource_lists.AutoInit();
|
||||||
|
nopaque.forms.AutoInit();
|
||||||
|
// #endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
flash(message, category) {
|
||||||
|
let iconPrefix;
|
||||||
|
|
||||||
|
switch (category) {
|
||||||
|
case 'corpus': {
|
||||||
|
iconPrefix = '<i class="material-icons left">book</i>';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'job': {
|
||||||
|
iconPrefix = '<i class="nopaque-icons left">J</i>';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'error': {
|
||||||
|
iconPrefix = '<i class="material-icons left error-color-text">error</i>';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
iconPrefix = '<i class="material-icons left">notifications</i>';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let toast = M.toast(
|
||||||
|
{
|
||||||
|
html: `
|
||||||
|
<span>${iconPrefix}${message}</span>
|
||||||
|
<button class="btn-flat toast-action white-text" data-toast-action="dismiss">
|
||||||
|
<i class="material-icons">close</i>
|
||||||
|
</button>
|
||||||
|
`.trim()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let dismissToastElement = toast.el.querySelector('.toast-action[data-toast-action="dismiss"]');
|
||||||
|
dismissToastElement.addEventListener('click', () => {toast.dismiss();});
|
||||||
|
}
|
||||||
|
}
|
53
app/static/js/app.users.js
Normal file
53
app/static/js/app.users.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
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];
|
||||||
|
}
|
||||||
|
}
|
@ -66,7 +66,7 @@ nopaque.corpus_analysis.ConcordanceExtension = class ConcordanceExtension {
|
|||||||
errorString += `${error.constructor.name}`;
|
errorString += `${error.constructor.name}`;
|
||||||
this.elements.error.innerText = errorString;
|
this.elements.error.innerText = errorString;
|
||||||
this.elements.error.classList.remove('hide');
|
this.elements.error.classList.remove('hide');
|
||||||
app.flash(errorString, 'error');
|
app.ui.flash(errorString, 'error');
|
||||||
this.elements.progress.classList.add('hide');
|
this.elements.progress.classList.add('hide');
|
||||||
}
|
}
|
||||||
this.app.enableActionElements();
|
this.app.enableActionElements();
|
||||||
@ -239,7 +239,7 @@ nopaque.corpus_analysis.ConcordanceExtension = class ConcordanceExtension {
|
|||||||
if (subcorpus.selectedItems.size === 0) {
|
if (subcorpus.selectedItems.size === 0) {
|
||||||
this.elements.progress.classList.add('hide');
|
this.elements.progress.classList.add('hide');
|
||||||
this.app.enableActionElements();
|
this.app.enableActionElements();
|
||||||
app.flash('No matches selected', 'error');
|
app.ui.flash('No matches selected', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
promise = subcorpus.o.partialExport([...subcorpus.selectedItems], 50);
|
promise = subcorpus.o.partialExport([...subcorpus.selectedItems], 50);
|
||||||
@ -298,7 +298,7 @@ nopaque.corpus_analysis.ConcordanceExtension = class ConcordanceExtension {
|
|||||||
let subcorpus = this.data.subcorpora[this.settings.selectedSubcorpus];
|
let subcorpus = this.data.subcorpora[this.settings.selectedSubcorpus];
|
||||||
subcorpus.o.drop().then(
|
subcorpus.o.drop().then(
|
||||||
(cQiStatus) => {
|
(cQiStatus) => {
|
||||||
app.flash(`${subcorpus.o.name} deleted`, 'corpus');
|
app.ui.flash(`${subcorpus.o.name} deleted`, 'corpus');
|
||||||
delete this.data.subcorpora[subcorpus.o.name];
|
delete this.data.subcorpora[subcorpus.o.name];
|
||||||
this.settings.selectedSubcorpus = undefined;
|
this.settings.selectedSubcorpus = undefined;
|
||||||
for (let subcorpusName in this.data.subcorpora) {
|
for (let subcorpusName in this.data.subcorpora) {
|
||||||
@ -320,7 +320,7 @@ nopaque.corpus_analysis.ConcordanceExtension = class ConcordanceExtension {
|
|||||||
},
|
},
|
||||||
(cqiError) => {
|
(cqiError) => {
|
||||||
let errorString = `${cqiError.code}: ${cqiError.constructor.name}`;
|
let errorString = `${cqiError.code}: ${cqiError.constructor.name}`;
|
||||||
app.flash(errorString, 'error');
|
app.ui.flash(errorString, 'error');
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -46,7 +46,7 @@ nopaque.corpus_analysis.ReaderExtension = class ReaderExtension {
|
|||||||
if ('description' in error) {errorString += `: ${error.description}`;}
|
if ('description' in error) {errorString += `: ${error.description}`;}
|
||||||
this.elements.error.innerText = errorString;
|
this.elements.error.innerText = errorString;
|
||||||
this.elements.error.classList.remove('hide');
|
this.elements.error.classList.remove('hide');
|
||||||
app.flash(errorString, 'error');
|
app.ui.flash(errorString, 'error');
|
||||||
this.elements.progress.classList.add('hide');
|
this.elements.progress.classList.add('hide');
|
||||||
}
|
}
|
||||||
this.app.enableActionElements();
|
this.app.enableActionElements();
|
||||||
|
@ -101,7 +101,7 @@ nopaque.forms.BaseForm = class BaseForm {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (request.status === 500) {
|
if (request.status === 500) {
|
||||||
app.flash('Internal Server Error', 'error');
|
app.ui.flash('Internal Server Error', 'error');
|
||||||
}
|
}
|
||||||
modal.close();
|
modal.close();
|
||||||
});
|
});
|
||||||
|
@ -25,16 +25,16 @@ nopaque.requests.JSONfetch = (input, init={}) => {
|
|||||||
let message = json.message;
|
let message = json.message;
|
||||||
let category = json.category || 'message';
|
let category = json.category || 'message';
|
||||||
if (message) {
|
if (message) {
|
||||||
app.flash(message, category);
|
app.ui.flash(message, category);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
app.flash(`[${response.status}]: ${response.statusText}`, 'error');
|
app.ui.flash(`[${response.status}]: ${response.statusText}`, 'error');
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
(response) => {
|
(response) => {
|
||||||
app.flash('Something went wrong', 'error');
|
app.ui.flash('Something went wrong', 'error');
|
||||||
reject(response);
|
reject(response);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -6,13 +6,13 @@ nopaque.resource_displays.ResourceDisplay = class ResourceDisplay {
|
|||||||
this.userId = this.displayElement.dataset.userId;
|
this.userId = this.displayElement.dataset.userId;
|
||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
if (this.userId) {
|
if (this.userId) {
|
||||||
app.subscribeUser(this.userId)
|
app.users.subscribe(this.userId)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
app.socket.on('patch_user', (patch) => {
|
app.socket.on('users.patch', (patch) => {
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
if (this.isInitialized) {this.onPatch(patch);}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
app.getUser(this.userId)
|
app.users.get(this.userId)
|
||||||
.then((user) => {
|
.then((user) => {
|
||||||
this.init(user);
|
this.init(user);
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
|
@ -14,12 +14,12 @@ nopaque.resource_lists.CorpusFileList = class CorpusFileList extends nopaque.res
|
|||||||
this.hasPermissionView = listContainerElement.dataset?.hasPermissionView == 'true' || false;
|
this.hasPermissionView = listContainerElement.dataset?.hasPermissionView == 'true' || false;
|
||||||
this.hasPermissionManageFiles = listContainerElement.dataset?.hasPermissionManageFiles == 'true' || false;
|
this.hasPermissionManageFiles = listContainerElement.dataset?.hasPermissionManageFiles == 'true' || false;
|
||||||
if (this.userId === undefined || this.corpusId === undefined) {return;}
|
if (this.userId === undefined || this.corpusId === undefined) {return;}
|
||||||
app.subscribeUser(this.userId).then((response) => {
|
app.users.subscribe(this.userId).then((response) => {
|
||||||
app.socket.on('patch_user', (patch) => {
|
app.socket.on('users.patch', (patch) => {
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
if (this.isInitialized) {this.onPatch(patch);}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
app.getUser(this.userId).then((user) => {
|
app.users.get(this.userId).then((user) => {
|
||||||
this.add(Object.values(user.corpora[this.corpusId].files || user.followed_corpora[this.corpusId].files));
|
this.add(Object.values(user.corpora[this.corpusId].files || user.followed_corpora[this.corpusId].files));
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
});
|
});
|
||||||
|
@ -12,12 +12,12 @@ nopaque.resource_lists.CorpusFollowerList = class CorpusFollowerList extends nop
|
|||||||
this.userId = listContainerElement.dataset.userId;
|
this.userId = listContainerElement.dataset.userId;
|
||||||
this.corpusId = listContainerElement.dataset.corpusId;
|
this.corpusId = listContainerElement.dataset.corpusId;
|
||||||
if (this.userId === undefined || this.corpusId === undefined) {return;}
|
if (this.userId === undefined || this.corpusId === undefined) {return;}
|
||||||
app.subscribeUser(this.userId).then((response) => {
|
app.users.subscribe(this.userId).then((response) => {
|
||||||
app.socket.on('patch_user', (patch) => {
|
app.socket.on('users.patch', (patch) => {
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
if (this.isInitialized) {this.onPatch(patch);}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
app.getUser(this.userId).then((user) => {
|
app.users.get(this.userId).then((user) => {
|
||||||
// let corpusFollowerAssociations = Object.values(user.corpora[this.corpusId].corpus_follower_associations);
|
// let corpusFollowerAssociations = Object.values(user.corpora[this.corpusId].corpus_follower_associations);
|
||||||
// let filteredList = corpusFollowerAssociations.filter(association => association.follower.id != currentUserId);
|
// let filteredList = corpusFollowerAssociations.filter(association => association.follower.id != currentUserId);
|
||||||
// this.add(filteredList);
|
// this.add(filteredList);
|
||||||
|
@ -11,12 +11,12 @@ nopaque.resource_lists.CorpusList = class CorpusList extends nopaque.resource_li
|
|||||||
this.selectedItemIds = new Set();
|
this.selectedItemIds = new Set();
|
||||||
this.userId = listContainerElement.dataset.userId;
|
this.userId = listContainerElement.dataset.userId;
|
||||||
if (this.userId === undefined) {return;}
|
if (this.userId === undefined) {return;}
|
||||||
app.subscribeUser(this.userId).then((response) => {
|
app.users.subscribe(this.userId).then((response) => {
|
||||||
app.socket.on('patch_user', (patch) => {
|
app.socket.on('users.patch', (patch) => {
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
if (this.isInitialized) {this.onPatch(patch);}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
app.getUser(this.userId).then((user) => {
|
app.users.get(this.userId).then((user) => {
|
||||||
this.add(this.aggregateData(user));
|
this.add(this.aggregateData(user));
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
});
|
});
|
||||||
|
@ -8,8 +8,8 @@ nopaque.resource_lists.JobInputList = class JobInputList extends nopaque.resourc
|
|||||||
this.userId = listContainerElement.dataset.userId;
|
this.userId = listContainerElement.dataset.userId;
|
||||||
this.jobId = listContainerElement.dataset.jobId;
|
this.jobId = listContainerElement.dataset.jobId;
|
||||||
if (this.userId === undefined || this.jobId === undefined) {return;}
|
if (this.userId === undefined || this.jobId === undefined) {return;}
|
||||||
app.subscribeUser(this.userId);
|
app.users.subscribe(this.userId);
|
||||||
app.getUser(this.userId).then((user) => {
|
app.users.get(this.userId).then((user) => {
|
||||||
this.add(Object.values(user.jobs[this.jobId].inputs));
|
this.add(Object.values(user.jobs[this.jobId].inputs));
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
});
|
});
|
||||||
|
@ -12,12 +12,12 @@ nopaque.resource_lists.JobList = class JobList extends nopaque.resource_lists.Re
|
|||||||
this.selectedItemIds = new Set();
|
this.selectedItemIds = new Set();
|
||||||
this.userId = listContainerElement.dataset.userId;
|
this.userId = listContainerElement.dataset.userId;
|
||||||
if (this.userId === undefined) {return;}
|
if (this.userId === undefined) {return;}
|
||||||
app.subscribeUser(this.userId).then((response) => {
|
app.users.subscribe(this.userId).then((response) => {
|
||||||
app.socket.on('patch_user', (patch) => {
|
app.socket.on('users.patch', (patch) => {
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
if (this.isInitialized) {this.onPatch(patch);}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
app.getUser(this.userId).then((user) => {
|
app.users.get(this.userId).then((user) => {
|
||||||
this.add(Object.values(user.jobs));
|
this.add(Object.values(user.jobs));
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
});
|
});
|
||||||
|
@ -8,12 +8,12 @@ nopaque.resource_lists.JobResultList = class JobResultList extends nopaque.resou
|
|||||||
this.userId = listContainerElement.dataset.userId;
|
this.userId = listContainerElement.dataset.userId;
|
||||||
this.jobId = listContainerElement.dataset.jobId;
|
this.jobId = listContainerElement.dataset.jobId;
|
||||||
if (this.userId === undefined || this.jobId === undefined) {return;}
|
if (this.userId === undefined || this.jobId === undefined) {return;}
|
||||||
app.subscribeUser(this.userId).then((response) => {
|
app.users.subscribe(this.userId).then((response) => {
|
||||||
app.socket.on('patch_user', (patch) => {
|
app.socket.on('users.patch', (patch) => {
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
if (this.isInitialized) {this.onPatch(patch);}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
app.getUser(this.userId).then((user) => {
|
app.users.get(this.userId).then((user) => {
|
||||||
this.add(Object.values(user.jobs[this.jobId].results));
|
this.add(Object.values(user.jobs[this.jobId].results));
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
});
|
});
|
||||||
|
@ -8,12 +8,12 @@ nopaque.resource_lists.SpaCyNLPPipelineModelList = class SpaCyNLPPipelineModelLi
|
|||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
this.userId = listContainerElement.dataset.userId;
|
this.userId = listContainerElement.dataset.userId;
|
||||||
if (this.userId === undefined) {return;}
|
if (this.userId === undefined) {return;}
|
||||||
app.subscribeUser(this.userId).then((response) => {
|
app.users.subscribe(this.userId).then((response) => {
|
||||||
app.socket.on('patch_user', (patch) => {
|
app.socket.on('users.patch', (patch) => {
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
if (this.isInitialized) {this.onPatch(patch);}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
app.getUser(this.userId).then((user) => {
|
app.users.get(this.userId).then((user) => {
|
||||||
this.add(Object.values(user.spacy_nlp_pipeline_models));
|
this.add(Object.values(user.spacy_nlp_pipeline_models));
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
});
|
});
|
||||||
|
@ -8,12 +8,12 @@ nopaque.resource_lists.TesseractOCRPipelineModelList = class TesseractOCRPipelin
|
|||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
this.userId = listContainerElement.dataset.userId;
|
this.userId = listContainerElement.dataset.userId;
|
||||||
if (this.userId === undefined) {return;}
|
if (this.userId === undefined) {return;}
|
||||||
app.subscribeUser(this.userId).then((response) => {
|
app.users.subscribe(this.userId).then((response) => {
|
||||||
app.socket.on('patch_user', (patch) => {
|
app.socket.on('users.patch', (patch) => {
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
if (this.isInitialized) {this.onPatch(patch);}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
app.getUser(this.userId).then((user) => {
|
app.users.get(this.userId).then((user) => {
|
||||||
this.add(Object.values(user.tesseract_ocr_pipeline_models));
|
this.add(Object.values(user.tesseract_ocr_pipeline_models));
|
||||||
for (let uncheckedCheckbox of this.listjs.list.querySelectorAll('input[data-checked="True"]')) {
|
for (let uncheckedCheckbox of this.listjs.list.querySelectorAll('input[data-checked="True"]')) {
|
||||||
uncheckedCheckbox.setAttribute('checked', '');
|
uncheckedCheckbox.setAttribute('checked', '');
|
||||||
|
@ -9,6 +9,8 @@
|
|||||||
output='gen/nopaque.%(version)s.js',
|
output='gen/nopaque.%(version)s.js',
|
||||||
'js/index.js',
|
'js/index.js',
|
||||||
'js/app.js',
|
'js/app.js',
|
||||||
|
'js/app.ui.js',
|
||||||
|
'js/app.users.js',
|
||||||
'js/utils.js',
|
'js/utils.js',
|
||||||
|
|
||||||
'js/forms/index.js',
|
'js/forms/index.js',
|
||||||
@ -82,11 +84,11 @@
|
|||||||
const currentUserId = {{ current_user.hashid|tojson }};
|
const currentUserId = {{ current_user.hashid|tojson }};
|
||||||
|
|
||||||
// Subscribe to the current user's data events
|
// Subscribe to the current user's data events
|
||||||
app.subscribeUser(currentUserId)
|
app.users.subscribe(currentUserId)
|
||||||
.catch((error) => {throw JSON.stringify(error);});
|
.catch((error) => {throw JSON.stringify(error);});
|
||||||
|
|
||||||
// Get the current user's data
|
// Get the current user's data
|
||||||
app.getUser(currentUserId, true, true)
|
app.users.get(currentUserId, true, true)
|
||||||
.catch((error) => {throw JSON.stringify(error);});
|
.catch((error) => {throw JSON.stringify(error);});
|
||||||
|
|
||||||
{% if not current_user.terms_of_use_accepted -%}
|
{% if not current_user.terms_of_use_accepted -%}
|
||||||
@ -96,7 +98,7 @@
|
|||||||
|
|
||||||
// Display flashed messages
|
// Display flashed messages
|
||||||
for (let [category, message] of {{ get_flashed_messages(with_categories=True)|tojson }}) {
|
for (let [category, message] of {{ get_flashed_messages(with_categories=True)|tojson }}) {
|
||||||
app.flash(message, message);
|
app.ui.flash(message, message);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -364,8 +364,8 @@ shareLinkModalCreateButtonElement.addEventListener('click', (event) => {
|
|||||||
shareLinkModalOutputCopyButtonElement.addEventListener('click', (event) => {
|
shareLinkModalOutputCopyButtonElement.addEventListener('click', (event) => {
|
||||||
navigator.clipboard.writeText(shareLinkModalOutputFieldElement.value)
|
navigator.clipboard.writeText(shareLinkModalOutputFieldElement.value)
|
||||||
.then(
|
.then(
|
||||||
() => {app.flash('Copied!');},
|
() => {app.ui.flash('Copied!');},
|
||||||
() => {app.flash('Could not copy to clipboard. Please copy manually.', 'error');}
|
() => {app.ui.flash('Could not copy to clipboard. Please copy manually.', 'error');}
|
||||||
);
|
);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -396,8 +396,8 @@ shareLinkModalCreateButtonElement.addEventListener('click', (event) => {
|
|||||||
shareLinkModalOutputCopyButtonElement.addEventListener('click', (event) => {
|
shareLinkModalOutputCopyButtonElement.addEventListener('click', (event) => {
|
||||||
navigator.clipboard.writeText(shareLinkModalOutputFieldElement.value)
|
navigator.clipboard.writeText(shareLinkModalOutputFieldElement.value)
|
||||||
.then(
|
.then(
|
||||||
() => {app.flash('Copied!');},
|
() => {app.ui.flash('Copied!');},
|
||||||
() => {app.flash('Could not copy to clipboard. Please copy manually.', 'error');}
|
() => {app.ui.flash('Could not copy to clipboard. Please copy manually.', 'error');}
|
||||||
);
|
);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user