mirror of
https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
synced 2025-07-01 18:30:34 +00:00
Compare commits
8 Commits
12a3ac1d5d
...
1.1.0
Author | SHA1 | Date | |
---|---|---|---|
713a7645db | |||
0c64c07925 | |||
a6ddf4c980 | |||
cab5f7ea05 | |||
07f09cdbd9 | |||
c97b2a886e | |||
df2bffe0fd | |||
aafb3ca3ec |
@ -132,6 +132,9 @@ def create_app(config: Config = Config) -> Flask:
|
|||||||
# region SocketIO Namespaces
|
# region SocketIO Namespaces
|
||||||
from .namespaces.cqi_over_sio import CQiOverSocketIONamespace
|
from .namespaces.cqi_over_sio import CQiOverSocketIONamespace
|
||||||
socketio.on_namespace(CQiOverSocketIONamespace('/cqi_over_sio'))
|
socketio.on_namespace(CQiOverSocketIONamespace('/cqi_over_sio'))
|
||||||
|
|
||||||
|
from .namespaces.users import UsersNamespace
|
||||||
|
socketio.on_namespace(UsersNamespace('/users'))
|
||||||
# endregion SocketIO Namespaces
|
# endregion SocketIO Namespaces
|
||||||
|
|
||||||
# region Database event Listeners
|
# region Database event Listeners
|
||||||
|
@ -15,4 +15,4 @@ def before_request():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
from . import cli, events, json_routes, routes, settings
|
from . import cli, json_routes, routes, settings
|
||||||
|
@ -1,122 +0,0 @@
|
|||||||
from flask import current_app, Flask
|
|
||||||
from flask_login import current_user
|
|
||||||
from flask_socketio import join_room, leave_room
|
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
@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
|
|
||||||
def get_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'}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'body': user.to_json_serializeable(
|
|
||||||
backrefs=True,
|
|
||||||
relationships=True
|
|
||||||
),
|
|
||||||
'status': 200,
|
|
||||||
'statusText': 'OK'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@socketio.on('users.subscribe')
|
|
||||||
@socketio_login_required
|
|
||||||
def subscribe_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'}
|
|
||||||
|
|
||||||
join_room(f'/users/{user.hashid}')
|
|
||||||
|
|
||||||
return {'status': 200, 'statusText': 'OK'}
|
|
||||||
|
|
||||||
|
|
||||||
@socketio.on('users.unsubscribe')
|
|
||||||
@socketio_login_required
|
|
||||||
def unsubscribe_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'}
|
|
||||||
|
|
||||||
leave_room(f'/users/{user.hashid}')
|
|
||||||
|
|
||||||
return {'status': 200, 'statusText': 'OK'}
|
|
@ -26,7 +26,7 @@ def socketio_login_required(f):
|
|||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
return {'code': 401, 'body': 'Unauthorized'}
|
return {'status': 401, 'statusText': 'Unauthorized'}
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ def socketio_permission_required(permission):
|
|||||||
@wraps(f)
|
@wraps(f)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
if not current_user.can(permission):
|
if not current_user.can(permission):
|
||||||
return {'code': 403, 'body': 'Forbidden'}
|
return {'status': 403, 'statusText': 'Forbidden'}
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
return wrapper
|
return wrapper
|
||||||
return decorator
|
return decorator
|
||||||
|
@ -42,8 +42,9 @@ def resource_after_delete(mapper, connection, resource):
|
|||||||
'path': resource.jsonpatch_path
|
'path': resource.jsonpatch_path
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
namespace = '/users'
|
||||||
room = f'/users/{resource.user_hashid}'
|
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):
|
def cfa_after_delete(mapper, connection, cfa):
|
||||||
@ -54,8 +55,9 @@ def cfa_after_delete(mapper, connection, cfa):
|
|||||||
'path': jsonpatch_path
|
'path': jsonpatch_path
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
namespace = '/users'
|
||||||
room = f'/users/{cfa.corpus.user.hashid}'
|
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):
|
def resource_after_insert(mapper, connection, resource):
|
||||||
@ -69,8 +71,9 @@ def resource_after_insert(mapper, connection, resource):
|
|||||||
'value': jsonpatch_value
|
'value': jsonpatch_value
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
namespace = '/users'
|
||||||
room = f'/users/{resource.user_hashid}'
|
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):
|
def cfa_after_insert(mapper, connection, cfa):
|
||||||
@ -83,8 +86,9 @@ def cfa_after_insert(mapper, connection, cfa):
|
|||||||
'value': jsonpatch_value
|
'value': jsonpatch_value
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
namespace = '/users'
|
||||||
room = f'/users/{cfa.corpus.user.hashid}'
|
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):
|
def resource_after_update(mapper, connection, resource):
|
||||||
@ -109,8 +113,9 @@ def resource_after_update(mapper, connection, resource):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
if jsonpatch:
|
if jsonpatch:
|
||||||
|
namespace = '/users'
|
||||||
room = f'/users/{resource.user_hashid}'
|
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):
|
def job_after_update(mapper, connection, job):
|
||||||
|
@ -9,8 +9,8 @@ from threading import Lock
|
|||||||
from app import db, docker_client, hashids, socketio
|
from app import db, docker_client, hashids, socketio
|
||||||
from app.decorators import socketio_login_required
|
from app.decorators import socketio_login_required
|
||||||
from app.models import Corpus, CorpusStatus
|
from app.models import Corpus, CorpusStatus
|
||||||
from . import cqi_extensions
|
from . import cqi_extension_functions
|
||||||
from .utils import CQiOverSocketIOSessionManager
|
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):
|
class CQiOverSocketIONamespace(Namespace):
|
||||||
@socketio_login_required
|
@socketio_login_required
|
||||||
def on_connect(self):
|
def on_connect(self):
|
||||||
@ -135,25 +145,25 @@ class CQiOverSocketIONamespace(Namespace):
|
|||||||
cqi_client = CQiClient(cqpserver_ip_address)
|
cqi_client = CQiClient(cqpserver_ip_address)
|
||||||
cqi_client_lock = Lock()
|
cqi_client_lock = Lock()
|
||||||
|
|
||||||
CQiOverSocketIOSessionManager.setup()
|
SessionManager.setup()
|
||||||
CQiOverSocketIOSessionManager.set_corpus_id(corpus_id)
|
SessionManager.set_corpus_id(corpus_id)
|
||||||
CQiOverSocketIOSessionManager.set_cqi_client(cqi_client)
|
SessionManager.set_cqi_client(cqi_client)
|
||||||
CQiOverSocketIOSessionManager.set_cqi_client_lock(cqi_client_lock)
|
SessionManager.set_cqi_client_lock(cqi_client_lock)
|
||||||
|
|
||||||
return {'code': 200, 'msg': 'OK'}
|
return {'code': 200, 'msg': 'OK'}
|
||||||
|
|
||||||
@socketio_login_required
|
@socketio_login_required
|
||||||
def on_exec(self, fn_name: str, fn_args: dict = {}) -> dict:
|
def on_exec(self, fn_name: str, fn_args: dict = {}) -> dict:
|
||||||
try:
|
try:
|
||||||
cqi_client = CQiOverSocketIOSessionManager.get_cqi_client()
|
cqi_client = SessionManager.get_cqi_client()
|
||||||
cqi_client_lock = CQiOverSocketIOSessionManager.get_cqi_client_lock()
|
cqi_client_lock = SessionManager.get_cqi_client_lock()
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return {'code': 424, 'msg': 'Failed Dependency'}
|
return {'code': 424, 'msg': 'Failed Dependency'}
|
||||||
|
|
||||||
if fn_name in CQI_API_FUNCTION_NAMES:
|
if fn_name in CQI_API_FUNCTION_NAMES:
|
||||||
fn = getattr(cqi_client.api, fn_name)
|
fn = getattr(cqi_client.api, fn_name)
|
||||||
elif fn_name in cqi_extensions.CQI_EXTENSION_FUNCTION_NAMES:
|
elif fn_name in CQI_EXTENSION_FUNCTION_NAMES:
|
||||||
fn = getattr(cqi_extensions, fn_name)
|
fn = getattr(cqi_extension_functions, fn_name)
|
||||||
else:
|
else:
|
||||||
return {'code': 400, 'msg': 'Bad Request'}
|
return {'code': 400, 'msg': 'Bad Request'}
|
||||||
|
|
||||||
@ -198,10 +208,10 @@ class CQiOverSocketIONamespace(Namespace):
|
|||||||
|
|
||||||
def on_disconnect(self):
|
def on_disconnect(self):
|
||||||
try:
|
try:
|
||||||
corpus_id = CQiOverSocketIOSessionManager.get_corpus_id()
|
corpus_id = SessionManager.get_corpus_id()
|
||||||
cqi_client = CQiOverSocketIOSessionManager.get_cqi_client()
|
cqi_client = SessionManager.get_cqi_client()
|
||||||
cqi_client_lock = CQiOverSocketIOSessionManager.get_cqi_client_lock()
|
cqi_client_lock = SessionManager.get_cqi_client_lock()
|
||||||
CQiOverSocketIOSessionManager.teardown()
|
SessionManager.teardown()
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -8,22 +8,12 @@ import json
|
|||||||
import math
|
import math
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import Corpus
|
from app.models import Corpus
|
||||||
from .utils import CQiOverSocketIOSessionManager
|
from .utils import SessionManager
|
||||||
|
|
||||||
|
|
||||||
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',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def ext_corpus_update_db(corpus: str) -> CQiStatusOk:
|
def ext_corpus_update_db(corpus: str) -> CQiStatusOk:
|
||||||
corpus_id = CQiOverSocketIOSessionManager.get_corpus_id()
|
corpus_id = SessionManager.get_corpus_id()
|
||||||
cqi_client = CQiOverSocketIOSessionManager.get_cqi_client()
|
cqi_client = SessionManager.get_cqi_client()
|
||||||
db_corpus = Corpus.query.get(corpus_id)
|
db_corpus = Corpus.query.get(corpus_id)
|
||||||
cqi_corpus = cqi_client.corpora.get(corpus)
|
cqi_corpus = cqi_client.corpora.get(corpus)
|
||||||
db_corpus.num_tokens = cqi_corpus.size
|
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:
|
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)
|
db_corpus = Corpus.query.get(corpus_id)
|
||||||
|
|
||||||
static_data_file_path = db_corpus.path / 'cwb' / 'static.json.gz'
|
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:
|
with static_data_file_path.open('rb') as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
cqi_client = CQiOverSocketIOSessionManager.get_cqi_client()
|
cqi_client = SessionManager.get_cqi_client()
|
||||||
cqi_corpus = cqi_client.corpora.get(corpus)
|
cqi_corpus = cqi_client.corpora.get(corpus)
|
||||||
cqi_p_attrs = cqi_corpus.positional_attributes.list()
|
cqi_p_attrs = cqi_corpus.positional_attributes.list()
|
||||||
cqi_s_attrs = cqi_corpus.structural_attributes.list()
|
cqi_s_attrs = cqi_corpus.structural_attributes.list()
|
||||||
@ -168,7 +158,7 @@ def ext_corpus_paginate_corpus(
|
|||||||
page: int = 1,
|
page: int = 1,
|
||||||
per_page: int = 20
|
per_page: int = 20
|
||||||
) -> dict:
|
) -> dict:
|
||||||
cqi_client = CQiOverSocketIOSessionManager.get_cqi_client()
|
cqi_client = SessionManager.get_cqi_client()
|
||||||
cqi_corpus = cqi_client.corpora.get(corpus)
|
cqi_corpus = cqi_client.corpora.get(corpus)
|
||||||
# Sanity checks
|
# Sanity checks
|
||||||
if (
|
if (
|
||||||
@ -215,7 +205,7 @@ def ext_cqp_paginate_subcorpus(
|
|||||||
per_page: int = 20
|
per_page: int = 20
|
||||||
) -> dict:
|
) -> dict:
|
||||||
corpus_name, subcorpus_name = subcorpus.split(':', 1)
|
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_corpus = cqi_client.corpora.get(corpus_name)
|
||||||
cqi_subcorpus = cqi_corpus.subcorpora.get(subcorpus_name)
|
cqi_subcorpus = cqi_corpus.subcorpora.get(subcorpus_name)
|
||||||
# Sanity checks
|
# Sanity checks
|
||||||
@ -262,7 +252,7 @@ def ext_cqp_partial_export_subcorpus(
|
|||||||
context: int = 50
|
context: int = 50
|
||||||
) -> dict:
|
) -> dict:
|
||||||
corpus_name, subcorpus_name = subcorpus.split(':', 1)
|
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_corpus = cqi_client.corpora.get(corpus_name)
|
||||||
cqi_subcorpus = cqi_corpus.subcorpora.get(subcorpus_name)
|
cqi_subcorpus = cqi_corpus.subcorpora.get(subcorpus_name)
|
||||||
cqi_subcorpus_partial_export = _partial_export_subcorpus(cqi_subcorpus, match_id_list, context=context)
|
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:
|
def ext_cqp_export_subcorpus(subcorpus: str, context: int = 50) -> dict:
|
||||||
corpus_name, subcorpus_name = subcorpus.split(':', 1)
|
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_corpus = cqi_client.corpora.get(corpus_name)
|
||||||
cqi_subcorpus = cqi_corpus.subcorpora.get(subcorpus_name)
|
cqi_subcorpus = cqi_corpus.subcorpora.get(subcorpus_name)
|
||||||
cqi_subcorpus_export = _export_subcorpus(cqi_subcorpus, context=context)
|
cqi_subcorpus_export = _export_subcorpus(cqi_subcorpus, context=context)
|
@ -3,7 +3,7 @@ from threading import Lock
|
|||||||
from flask import session
|
from flask import session
|
||||||
|
|
||||||
|
|
||||||
class CQiOverSocketIOSessionManager:
|
class SessionManager:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def setup():
|
def setup():
|
||||||
session['cqi_over_sio'] = {}
|
session['cqi_over_sio'] = {}
|
||||||
|
109
app/namespaces/jobs.py
Normal file
109
app/namespaces/jobs.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
from flask import current_app, Flask
|
||||||
|
from flask_login import current_user
|
||||||
|
from flask_socketio import Namespace
|
||||||
|
from app import db, hashids, socketio
|
||||||
|
from app.decorators import socketio_admin_required, socketio_login_required
|
||||||
|
from app.models import Job, JobStatus
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_job(app: Flask, job_id: int):
|
||||||
|
with app.app_context():
|
||||||
|
job = Job.query.get(job_id)
|
||||||
|
job.delete()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def _restart_job(app, job_id):
|
||||||
|
with app.app_context():
|
||||||
|
job = Job.query.get(job_id)
|
||||||
|
job.restart()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
class UsersNamespace(Namespace):
|
||||||
|
@socketio_login_required
|
||||||
|
def on_delete(self, job_hashid: str) -> dict:
|
||||||
|
job_id = hashids.decode(job_hashid)
|
||||||
|
|
||||||
|
if not isinstance(job_id, int):
|
||||||
|
return {'status': 400, 'statusText': 'Bad Request'}
|
||||||
|
|
||||||
|
job = Job.query.get(job_id)
|
||||||
|
|
||||||
|
if job is None:
|
||||||
|
return {'status': 404, 'statusText': 'Not found'}
|
||||||
|
|
||||||
|
if not (
|
||||||
|
job.user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
return {'status': 403, 'statusText': 'Forbidden'}
|
||||||
|
|
||||||
|
socketio.start_background_task(
|
||||||
|
_delete_job,
|
||||||
|
current_app._get_current_object(),
|
||||||
|
job_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'body': f'Job "{job.title}" marked for deletion',
|
||||||
|
'status': 202,
|
||||||
|
'statusText': 'Accepted'
|
||||||
|
}
|
||||||
|
|
||||||
|
@socketio_admin_required
|
||||||
|
def on_log(self, job_hashid: str):
|
||||||
|
job_id = hashids.decode(job_hashid)
|
||||||
|
|
||||||
|
if not isinstance(job_id, int):
|
||||||
|
return {'status': 400, 'statusText': 'Bad Request'}
|
||||||
|
|
||||||
|
job = Job.query.get(job_id)
|
||||||
|
|
||||||
|
if job is None:
|
||||||
|
return {'status': 404, 'statusText': 'Not found'}
|
||||||
|
|
||||||
|
if job.status not in [JobStatus.COMPLETED, JobStatus.FAILED]:
|
||||||
|
return {'status': 409, 'statusText': 'Conflict'}
|
||||||
|
|
||||||
|
with open(job.path / 'pipeline_data' / 'logs' / 'pyflow_log.txt') as log_file:
|
||||||
|
log = log_file.read()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'body': log,
|
||||||
|
'status': 200,
|
||||||
|
'statusText': 'Forbidden'
|
||||||
|
}
|
||||||
|
|
||||||
|
socketio_login_required
|
||||||
|
def on_restart(self, job_hashid: str):
|
||||||
|
job_id = hashids.decode(job_hashid)
|
||||||
|
|
||||||
|
if not isinstance(job_id, int):
|
||||||
|
return {'status': 400, 'statusText': 'Bad Request'}
|
||||||
|
|
||||||
|
job = Job.query.get(job_id)
|
||||||
|
|
||||||
|
if job is None:
|
||||||
|
return {'status': 404, 'statusText': 'Not found'}
|
||||||
|
|
||||||
|
if not (
|
||||||
|
job.user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
return {'status': 403, 'statusText': 'Forbidden'}
|
||||||
|
|
||||||
|
if job.status == JobStatus.FAILED:
|
||||||
|
return {'status': 409, 'statusText': 'Conflict'}
|
||||||
|
|
||||||
|
socketio.start_background_task(
|
||||||
|
_restart_job,
|
||||||
|
current_app._get_current_object(),
|
||||||
|
job_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'body': f'Job "{job.title}" marked for restarting',
|
||||||
|
'status': 202,
|
||||||
|
'statusText': 'Accepted'
|
||||||
|
}
|
116
app/namespaces/users.py
Normal file
116
app/namespaces/users.py
Normal file
@ -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'
|
||||||
|
}
|
@ -2,6 +2,10 @@
|
|||||||
--corpus-status-content: "unprepared";
|
--corpus-status-content: "unprepared";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-corpus-status="SUBMITTED"] {
|
||||||
|
--corpus-status-content: "submitted";
|
||||||
|
}
|
||||||
|
|
||||||
[data-corpus-status="QUEUED"] {
|
[data-corpus-status="QUEUED"] {
|
||||||
--corpus-status-content: "queued";
|
--corpus-status-content: "queued";
|
||||||
}
|
}
|
||||||
|
@ -1,33 +1,20 @@
|
|||||||
nopaque.App = class App {
|
nopaque.App = class App {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.data = {};
|
|
||||||
|
|
||||||
this.socket = io({transports: ['websocket'], upgrade: false});
|
this.socket = io({transports: ['websocket'], upgrade: false});
|
||||||
|
|
||||||
this.ui = new nopaque.UIExtension(this);
|
// Endpoints
|
||||||
this.users = new nopaque.UsersExtension(this);
|
this.users = new nopaque.app.endpoints.Users(this);
|
||||||
|
|
||||||
|
// Extensions
|
||||||
|
this.toaster = new nopaque.app.extensions.Toaster(this);
|
||||||
|
this.ui = new nopaque.app.extensions.UI(this);
|
||||||
|
this.userHub = new nopaque.app.extensions.UserHub(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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() {
|
||||||
|
// Initialize extensions
|
||||||
|
this.toaster.init();
|
||||||
this.ui.init();
|
this.ui.init();
|
||||||
|
this.userHub.init();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -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];
|
|
||||||
}
|
|
||||||
}
|
|
1
app/static/js/app/endpoints/index.js
Normal file
1
app/static/js/app/endpoints/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
nopaque.app.endpoints = {};
|
41
app/static/js/app/endpoints/users.js
Normal file
41
app/static/js/app/endpoints/users.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
nopaque.app.endpoints.Users = class Users {
|
||||||
|
constructor(app) {
|
||||||
|
this.app = app;
|
||||||
|
|
||||||
|
this.socket = io('/users', {transports: ['websocket'], upgrade: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
app/static/js/app/extensions/index.js
Normal file
1
app/static/js/app/extensions/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
nopaque.app.extensions = {};
|
56
app/static/js/app/extensions/toaster.js
Normal file
56
app/static/js/app/extensions/toaster.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
nopaque.app.extensions.Toaster = class Toaster {
|
||||||
|
constructor(app) {
|
||||||
|
this.app = app;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.app.userHub.addEventListener('patch', (event) => {this.#onPatch(event.detail);});
|
||||||
|
}
|
||||||
|
|
||||||
|
async #onPatch(patch) {
|
||||||
|
// Handle corpus updates
|
||||||
|
const corpusRegExp = new RegExp(`^/users/([A-Za-z0-9]+)/corpora/([A-Za-z0-9]+)`);
|
||||||
|
const corpusPatch = patch.filter((operation) => {return corpusRegExp.test(operation.path);});
|
||||||
|
|
||||||
|
this.#onCorpusPatch(corpusPatch);
|
||||||
|
|
||||||
|
// Handle job updates
|
||||||
|
const jobRegExp = new RegExp(`^/users/([A-Za-z0-9]+)/jobs/([A-Za-z0-9]+)`);
|
||||||
|
const jobPatch = patch.filter((operation) => {return jobRegExp.test(operation.path);});
|
||||||
|
|
||||||
|
this.#onJobPatch(jobPatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
async #onCorpusPatch(patch) {
|
||||||
|
return;
|
||||||
|
// Handle corpus status updates
|
||||||
|
const corpusStatusRegExp = new RegExp(`^/users/([A-Za-z0-9]+)/corpora/([A-Za-z0-9]+)/status$`);
|
||||||
|
const corpusStatusPatch = patch
|
||||||
|
.filter((operation) => {return corpusStatusRegExp.test(operation.path);})
|
||||||
|
.filter((operation) => {return operation.op === 'replace';});
|
||||||
|
|
||||||
|
for (let operation of corpusStatusPatch) {
|
||||||
|
const [match, userId, corpusId] = operation.path.match(corpusStatusRegExp);
|
||||||
|
const user = await this.app.userHub.get(userId);
|
||||||
|
const corpus = user.corpora[corpusId];
|
||||||
|
|
||||||
|
this.app.ui.flash(`[<a href="/corpora/${corpusId}">${corpus.title}</a>] New status: <span class="corpus-status-text" data-corpus-status="${operation.value}"></span>`, 'corpus');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #onJobPatch(patch) {
|
||||||
|
// Handle job status updates
|
||||||
|
const jobStatusRegExp = new RegExp(`^/users/([A-Za-z0-9]+)/jobs/([A-Za-z0-9]+)/status$`);
|
||||||
|
const jobStatusPatch = patch
|
||||||
|
.filter((operation) => {return jobStatusRegExp.test(operation.path);})
|
||||||
|
.filter((operation) => {return operation.op === 'replace';});
|
||||||
|
|
||||||
|
for (let operation of jobStatusPatch) {
|
||||||
|
const [match, userId, jobId] = operation.path.match(jobStatusRegExp);
|
||||||
|
const user = await this.app.userHub.get(userId);
|
||||||
|
const job = user.jobs[jobId];
|
||||||
|
|
||||||
|
this.app.ui.flash(`[<a href="/jobs/${jobId}">${job.title}</a>] New status: <span class="job-status-text" data-job-status="${operation.value}"></span>`, 'job');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
nopaque.UIExtension = class UIExtension {
|
nopaque.app.extensions.UI = class UI {
|
||||||
constructor(app) {
|
constructor(app) {
|
||||||
this.app = app;
|
this.app = app;
|
||||||
}
|
}
|
68
app/static/js/app/extensions/user-hub.js
Normal file
68
app/static/js/app/extensions/user-hub.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
nopaque.app.extensions.UserHub = class UserHub 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
|
||||||
|
const filterRegExp = new RegExp(`^/users/(${Object.keys(this.#data.users).join('|')})`);
|
||||||
|
const filteredPatch = patch.filter(operation => filterRegExp.test(operation.path));
|
||||||
|
|
||||||
|
// Apply patch
|
||||||
|
jsonpatch.applyPatch(this.#data, filteredPatch);
|
||||||
|
|
||||||
|
// Notify event listeners
|
||||||
|
const patchEventa = new CustomEvent('patch', {detail: filteredPatch});
|
||||||
|
this.dispatchEvent(patchEventa);
|
||||||
|
|
||||||
|
// Notify event listeners. Event type: "patch *"
|
||||||
|
const patchEvent = new CustomEvent('patch *', {detail: filteredPatch});
|
||||||
|
this.dispatchEvent(patchEvent);
|
||||||
|
|
||||||
|
// Group patches by user id: {<user-id>: [op, ...], ...}
|
||||||
|
const patches = {};
|
||||||
|
const matchRegExp = new RegExp(`^/users/([A-Za-z0-9]+)`);
|
||||||
|
for (let operation of filteredPatch) {
|
||||||
|
const [match, userId] = operation.path.match(matchRegExp);
|
||||||
|
if (!(userId in patches)) {patches[userId] = [];}
|
||||||
|
patches[userId].push(operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify event listeners. Event type: "patch <user-id>"
|
||||||
|
for (let [userId, patch] of Object.entries(patches)) {
|
||||||
|
const userPatchEvent = new CustomEvent(`patch ${userId}`, {detail: patch});
|
||||||
|
this.dispatchEvent(userPatchEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
app/static/js/app/index.js
Normal file
1
app/static/js/app/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
nopaque.app = {};
|
@ -52,22 +52,23 @@ nopaque.resource_displays.CorpusDisplay = class CorpusDisplay extends nopaque.re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setTitle(title) {
|
async setTitle(title) {
|
||||||
this.setElements(this.displayElement.querySelectorAll('.corpus-title'), title);
|
const corpusTitleElements = this.displayElement.querySelectorAll('.corpus-title');
|
||||||
|
this.setElements(corpusTitleElements, title);
|
||||||
}
|
}
|
||||||
|
|
||||||
setNumTokens(numTokens) {
|
setNumTokens(numTokens) {
|
||||||
this.setElements(
|
const corpusTokenRatioElements = this.displayElement.querySelectorAll('.corpus-token-ratio');
|
||||||
this.displayElement.querySelectorAll('.corpus-token-ratio'),
|
const maxNumTokens = 2147483647;
|
||||||
`${numTokens}/${app.data.users[this.userId].corpora[this.corpusId].max_num_tokens}`
|
|
||||||
);
|
this.setElements(corpusTokenRatioElements, `${numTokens}/${maxNumTokens}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
setDescription(description) {
|
setDescription(description) {
|
||||||
this.setElements(this.displayElement.querySelectorAll('.corpus-description'), description);
|
this.setElements(this.displayElement.querySelectorAll('.corpus-description'), description);
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatus(status) {
|
async setStatus(status) {
|
||||||
let elements = this.displayElement.querySelectorAll('.action-button[data-action="analyze"]');
|
let elements = this.displayElement.querySelectorAll('.action-button[data-action="analyze"]');
|
||||||
for (let element of elements) {
|
for (let element of elements) {
|
||||||
if (['BUILT', 'STARTING_ANALYSIS_SESSION', 'RUNNING_ANALYSIS_SESSION', 'CANCELING_ANALYSIS_SESSION'].includes(status)) {
|
if (['BUILT', 'STARTING_ANALYSIS_SESSION', 'RUNNING_ANALYSIS_SESSION', 'CANCELING_ANALYSIS_SESSION'].includes(status)) {
|
||||||
@ -77,8 +78,10 @@ nopaque.resource_displays.CorpusDisplay = class CorpusDisplay extends nopaque.re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
elements = this.displayElement.querySelectorAll('.action-button[data-action="build-request"]');
|
elements = this.displayElement.querySelectorAll('.action-button[data-action="build-request"]');
|
||||||
|
const user = await app.userHub.get(this.userId);
|
||||||
|
const corpusFiles = user.corpora[this.corpusId].files;
|
||||||
for (let element of elements) {
|
for (let element of elements) {
|
||||||
if (['UNPREPARED', 'FAILED'].includes(status) && Object.values(app.data.users[this.userId].corpora[this.corpusId].files).length > 0) {
|
if (['UNPREPARED', 'FAILED'].includes(status) && Object.values(corpusFiles.length > 0)) {
|
||||||
element.classList.remove('disabled');
|
element.classList.remove('disabled');
|
||||||
} else {
|
} else {
|
||||||
element.classList.add('disabled');
|
element.classList.add('disabled');
|
||||||
|
@ -5,20 +5,15 @@ nopaque.resource_displays.ResourceDisplay = class ResourceDisplay {
|
|||||||
this.displayElement = displayElement;
|
this.displayElement = displayElement;
|
||||||
this.userId = this.displayElement.dataset.userId;
|
this.userId = this.displayElement.dataset.userId;
|
||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
if (this.userId) {
|
if (this.userId === undefined) {return;}
|
||||||
app.users.subscribe(this.userId)
|
app.userHub.addEventListener('patch', (event) => {
|
||||||
.then((response) => {
|
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||||
app.socket.on('users.patch', (patch) => {
|
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
|
||||||
});
|
});
|
||||||
});
|
app.userHub.get(this.userId).then((user) => {
|
||||||
app.users.get(this.userId)
|
|
||||||
.then((user) => {
|
|
||||||
this.init(user);
|
this.init(user);
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
init(user) {throw 'Not implemented';}
|
init(user) {throw 'Not implemented';}
|
||||||
|
|
||||||
|
@ -14,12 +14,11 @@ 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.users.subscribe(this.userId).then((response) => {
|
app.userHub.addEventListener('patch', (event) => {
|
||||||
app.socket.on('users.patch', (patch) => {
|
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
|
||||||
});
|
});
|
||||||
});
|
app.userHub.get(this.userId).then((user) => {
|
||||||
app.users.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.add(Object.values(user.corpora[this.corpusId].files || user.followed_corpora[this.corpusId].files));
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
});
|
});
|
||||||
|
@ -12,15 +12,16 @@ 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.users.subscribe(this.userId).then((response) => {
|
app.userHub.addEventListener('patch', (event) => {
|
||||||
app.socket.on('users.patch', (patch) => {
|
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
|
||||||
});
|
});
|
||||||
});
|
app.userHub.get(this.userId).then((user) => {
|
||||||
app.users.get(this.userId).then((user) => {
|
// TODO: Check if the following is better
|
||||||
// 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);
|
||||||
|
|
||||||
|
// TODO: Make this better understandable
|
||||||
this.add(Object.values(user.corpora[this.corpusId].corpus_follower_associations));
|
this.add(Object.values(user.corpora[this.corpusId].corpus_follower_associations));
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
});
|
});
|
||||||
|
@ -11,12 +11,10 @@ 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.users.subscribe(this.userId).then((response) => {
|
app.userHub.addEventListener('patch', (event) => {
|
||||||
app.socket.on('users.patch', (patch) => {
|
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
|
||||||
});
|
});
|
||||||
});
|
app.userHub.get(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,10 @@ 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.users.subscribe(this.userId);
|
// app.userHub.addEventListener('patch', (event) => {
|
||||||
app.users.get(this.userId).then((user) => {
|
// if (this.isInitialized) {this.onPatch(event.detail);}
|
||||||
|
// });
|
||||||
|
app.userHub.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,10 @@ 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.users.subscribe(this.userId).then((response) => {
|
app.userHub.addEventListener('patch', (event) => {
|
||||||
app.socket.on('users.patch', (patch) => {
|
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
|
||||||
});
|
});
|
||||||
});
|
app.userHub.get(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,10 @@ 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.users.subscribe(this.userId).then((response) => {
|
app.userHub.addEventListener('patch', (event) => {
|
||||||
app.socket.on('users.patch', (patch) => {
|
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
|
||||||
});
|
});
|
||||||
});
|
app.userHub.get(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,10 @@ 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.users.subscribe(this.userId).then((response) => {
|
app.userHub.addEventListener('patch', (event) => {
|
||||||
app.socket.on('users.patch', (patch) => {
|
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
|
||||||
});
|
});
|
||||||
});
|
app.userHub.get(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,21 +8,11 @@ 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.users.subscribe(this.userId).then((response) => {
|
app.userHub.addEventListener('patch', (event) => {
|
||||||
app.socket.on('users.patch', (patch) => {
|
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
|
||||||
});
|
});
|
||||||
});
|
app.userHub.get(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"]')) {
|
|
||||||
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;
|
this.isInitialized = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -9,8 +9,13 @@
|
|||||||
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/index.js',
|
||||||
'js/app.users.js',
|
'js/app/endpoints/index.js',
|
||||||
|
'js/app/endpoints/users.js',
|
||||||
|
'js/app/extensions/index.js',
|
||||||
|
'js/app/extensions/toaster.js',
|
||||||
|
'js/app/extensions/ui.js',
|
||||||
|
'js/app/extensions/user-hub.js',
|
||||||
'js/utils.js',
|
'js/utils.js',
|
||||||
|
|
||||||
'js/forms/index.js',
|
'js/forms/index.js',
|
||||||
@ -79,22 +84,18 @@
|
|||||||
const app = new nopaque.App();
|
const app = new nopaque.App();
|
||||||
app.init();
|
app.init();
|
||||||
|
|
||||||
{% if current_user.is_authenticated -%}
|
{% if current_user.is_authenticated %}
|
||||||
// TODO: Set this as a property of the app object
|
|
||||||
const currentUserId = {{ current_user.hashid|tojson }};
|
const currentUserId = {{ current_user.hashid|tojson }};
|
||||||
|
|
||||||
// Subscribe to the current user's data events
|
app.userHub.add(currentUserId)
|
||||||
app.users.subscribe(currentUserId)
|
|
||||||
.catch((error) => {throw JSON.stringify(error);});
|
.catch((error) => {throw JSON.stringify(error);});
|
||||||
|
|
||||||
// Get the current user's data
|
{% if not current_user.terms_of_use_accepted %}
|
||||||
app.users.get(currentUserId, true, true)
|
|
||||||
.catch((error) => {throw JSON.stringify(error);});
|
|
||||||
|
|
||||||
{% if not current_user.terms_of_use_accepted -%}
|
|
||||||
M.Modal.getInstance(document.querySelector('#terms-of-use-modal')).open();
|
M.Modal.getInstance(document.querySelector('#terms-of-use-modal')).open();
|
||||||
{% endif -%}
|
{% endif %}
|
||||||
{% endif -%}
|
{% else %}
|
||||||
|
const currentUserId = null;
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
// 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 }}) {
|
||||||
@ -106,7 +107,7 @@
|
|||||||
let languageModalSwitch = document.querySelector('#terms-of-use-modal-switch');
|
let languageModalSwitch = document.querySelector('#terms-of-use-modal-switch');
|
||||||
let termsOfUseModalContent = document.querySelectorAll('.terms-of-use-modal-content');
|
let termsOfUseModalContent = document.querySelectorAll('.terms-of-use-modal-content');
|
||||||
if (languageModalSwitch) {
|
if (languageModalSwitch) {
|
||||||
languageModalSwitch.addEventListener('change', function() {
|
languageModalSwitch.addEventListener('change', () => {
|
||||||
termsOfUseModalContent.forEach(content => {
|
termsOfUseModalContent.forEach(content => {
|
||||||
content.classList.toggle('hide');
|
content.classList.toggle('hide');
|
||||||
});
|
});
|
||||||
|
@ -69,10 +69,9 @@
|
|||||||
{{ super() }}
|
{{ super() }}
|
||||||
<div class="modal no-autoinit" id="corpus-analysis-init-modal">
|
<div class="modal no-autoinit" id="corpus-analysis-init-modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="card-panel service-color darken white-text" data-service="corpus-analysis">
|
<div class="card-panel primary-color white-text" data-service="corpus-analysis">
|
||||||
<h4 class="m-3"><i class="material-icons left" style="font-size: inherit; line-height: inherit;">hourglass_empty</i>We are preparing your analysis session</h4>
|
<h4 class="m-3"><i class="material-icons left" style="font-size: inherit; line-height: inherit;">hourglass_empty</i>We are preparing your analysis session</h4>
|
||||||
</div>
|
</div>
|
||||||
<h4>We are preparing your analysis session</h4>
|
|
||||||
<p>
|
<p>
|
||||||
Our server works as hard as it can to prepare your analysis session. Please be patient and give it some time.<br>
|
Our server works as hard as it can to prepare your analysis session. Please be patient and give it some time.<br>
|
||||||
If initialization takes longer than usual or an error occurs, <a onclick="window.location.reload()" href="#">reload the page</a>.
|
If initialization takes longer than usual or an error occurs, <a onclick="window.location.reload()" href="#">reload the page</a>.
|
||||||
|
@ -26,7 +26,6 @@
|
|||||||
<div class="corpus-list" data-user-id="{{ current_user.hashid }}"></div>
|
<div class="corpus-list" data-user-id="{{ current_user.hashid }}"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-action right-align">
|
<div class="card-action right-align">
|
||||||
<a class="btn service-color darken disabled waves-effect waves-light" href="{{ url_for('corpora.import_corpus') }}">Import Corpus<i class="material-icons right">import_export</i></a>
|
|
||||||
<a class="btn service-color darken waves-effect waves-light" href="{{ url_for('corpora.create_corpus') }}">Create corpus<i class="material-icons right">add</i></a>
|
<a class="btn service-color darken waves-effect waves-light" href="{{ url_for('corpora.create_corpus') }}">Create corpus<i class="material-icons right">add</i></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -94,5 +94,5 @@ class Config:
|
|||||||
NOPAQUE_READCOOP_USERNAME = os.environ.get('NOPAQUE_READCOOP_USERNAME')
|
NOPAQUE_READCOOP_USERNAME = os.environ.get('NOPAQUE_READCOOP_USERNAME')
|
||||||
NOPAQUE_READCOOP_PASSWORD = os.environ.get('NOPAQUE_READCOOP_PASSWORD')
|
NOPAQUE_READCOOP_PASSWORD = os.environ.get('NOPAQUE_READCOOP_PASSWORD')
|
||||||
|
|
||||||
NOPAQUE_VERSION='1.0.2'
|
NOPAQUE_VERSION='1.1.0'
|
||||||
# endregion nopaque
|
# endregion nopaque
|
||||||
|
Reference in New Issue
Block a user