mirror of
https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
synced 2024-11-14 16:55:42 +00:00
Only reveal hashids to the ui
This commit is contained in:
parent
3e227dc4cf
commit
72ba61f369
@ -6,11 +6,13 @@ from flask_migrate import Migrate
|
||||
from flask_paranoid import Paranoid
|
||||
from flask_socketio import SocketIO
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from hashids import Hashids
|
||||
import flask_assets
|
||||
|
||||
|
||||
assets = flask_assets.Environment()
|
||||
db = SQLAlchemy()
|
||||
hashids = Hashids(min_length=32) # , salt=current_app.config.get('SECRET_KEY')
|
||||
login = LoginManager()
|
||||
login.login_view = 'auth.login'
|
||||
login.login_message = 'Please log in to access this page.'
|
||||
@ -37,6 +39,9 @@ def create_app(config_class=Config):
|
||||
message_queue=app.config.get('NOPAQUE_SOCKETIO_MESSAGE_QUEUE_URI')
|
||||
)
|
||||
|
||||
from .utils import HashidConverter
|
||||
app.url_map.converters['hashid'] = HashidConverter
|
||||
|
||||
from .events import socketio as socketio_events
|
||||
from .events import sqlalchemy as sqlalchemy_events
|
||||
|
||||
|
@ -19,12 +19,13 @@ def index():
|
||||
@login_required
|
||||
@admin_required
|
||||
def users():
|
||||
# users = [user.to_dict() for user in User.query.all()]
|
||||
users = {user.id: user.to_dict() for user in User.query.all()}
|
||||
return render_template('admin/users.html.j2', title='Users', users=users)
|
||||
dict_users = {user.id: user.to_dict(backrefs=True, relationships=False)
|
||||
for user in User.query.all()}
|
||||
return render_template(
|
||||
'admin/users.html.j2', title='Users', dict_users=dict_users)
|
||||
|
||||
|
||||
@bp.route('/users/<int:user_id>')
|
||||
@bp.route('/users/<hashid:user_id>')
|
||||
@login_required
|
||||
@admin_required
|
||||
def user(user_id):
|
||||
@ -32,7 +33,7 @@ def user(user_id):
|
||||
return render_template('admin/user.html.j2', title='User', user=user)
|
||||
|
||||
|
||||
@bp.route('/users/<int:user_id>/delete')
|
||||
@bp.route('/users/<hashid:user_id>/delete')
|
||||
@login_required
|
||||
@admin_required
|
||||
def delete_user(user_id):
|
||||
@ -41,7 +42,7 @@ def delete_user(user_id):
|
||||
return redirect(url_for('.users'))
|
||||
|
||||
|
||||
@bp.route('/users/<int:user_id>/edit', methods=['GET', 'POST']) # noqa
|
||||
@bp.route('/users/<hashid:user_id>/edit', methods=['GET', 'POST']) # noqa
|
||||
@login_required
|
||||
@admin_required
|
||||
def edit_user(user_id):
|
||||
|
@ -27,7 +27,7 @@ class API_Jobs(Resource):
|
||||
pass
|
||||
|
||||
|
||||
@ns.route('/<int:id>')
|
||||
@ns.route('/<hashid:id>')
|
||||
class API_Job(Resource):
|
||||
'''Show a single job and lets you delete it'''
|
||||
|
||||
|
@ -62,7 +62,7 @@ def connect(auth):
|
||||
if corpus is None:
|
||||
# return {'code': 404, 'msg': 'Not Found'}
|
||||
raise ConnectionRefusedError('Not Found')
|
||||
if not (corpus.creator == current_user or current_user.is_administrator()):
|
||||
if not (corpus.user == current_user or current_user.is_administrator()):
|
||||
# return {'code': 403, 'msg': 'Forbidden'}
|
||||
raise ConnectionRefusedError('Forbidden')
|
||||
if corpus.status not in ['prepared', 'start analysis', 'analysing', 'stop analysis']:
|
||||
|
@ -22,7 +22,7 @@ def add_query_result():
|
||||
if form.is_submitted():
|
||||
if not form.validate():
|
||||
return make_response(form.errors, 400)
|
||||
query_result = QueryResult(creator=current_user,
|
||||
query_result = QueryResult(user=current_user,
|
||||
description=form.description.data,
|
||||
filename=form.file.data.filename,
|
||||
title=form.title.data)
|
||||
@ -65,19 +65,19 @@ def add_query_result():
|
||||
form=form, title='Add query result')
|
||||
|
||||
|
||||
@bp.route('/result/<int:query_result_id>')
|
||||
@bp.route('/result/<hashid:query_result_id>')
|
||||
@login_required
|
||||
def query_result(query_result_id):
|
||||
abort(503)
|
||||
query_result = QueryResult.query.get_or_404(query_result_id)
|
||||
if not (query_result.creator == current_user
|
||||
if not (query_result.user == current_user
|
||||
or current_user.is_administrator()):
|
||||
abort(403)
|
||||
return render_template('corpora/query_results/query_result.html.j2',
|
||||
query_result=query_result, title='Query result')
|
||||
|
||||
|
||||
@bp.route('/result/<int:query_result_id>/inspect')
|
||||
@bp.route('/result/<hashid:query_result_id>/inspect')
|
||||
@login_required
|
||||
def inspect_query_result(query_result_id):
|
||||
'''
|
||||
@ -86,7 +86,7 @@ def inspect_query_result(query_result_id):
|
||||
abort(503)
|
||||
query_result = QueryResult.query.get_or_404(query_result_id)
|
||||
query_metadata = query_result.query_metadata
|
||||
if not (query_result.creator == current_user
|
||||
if not (query_result.user == current_user
|
||||
or current_user.is_administrator()):
|
||||
abort(403)
|
||||
display_options_form = DisplayOptionsForm(
|
||||
@ -108,12 +108,12 @@ def inspect_query_result(query_result_id):
|
||||
title='Inspect query result')
|
||||
|
||||
|
||||
@bp.route('/result/<int:query_result_id>/delete')
|
||||
@bp.route('/result/<hashid:query_result_id>/delete')
|
||||
@login_required
|
||||
def delete_query_result(query_result_id):
|
||||
abort(503)
|
||||
query_result = QueryResult.query.get_or_404(query_result_id)
|
||||
if not (query_result.creator == current_user
|
||||
if not (query_result.user == current_user
|
||||
or current_user.is_administrator()):
|
||||
abort(403)
|
||||
flash('Query result "{}" has been marked for deletion!'.format(query_result), 'result') # noqa
|
||||
@ -121,12 +121,12 @@ def delete_query_result(query_result_id):
|
||||
return redirect(url_for('services.service', service="corpus_analysis"))
|
||||
|
||||
|
||||
@bp.route('/result/<int:query_result_id>/download')
|
||||
@bp.route('/result/<hashid:query_result_id>/download')
|
||||
@login_required
|
||||
def download_query_result(query_result_id):
|
||||
abort(503)
|
||||
query_result = QueryResult.query.get_or_404(query_result_id)
|
||||
if not (query_result.creator == current_user
|
||||
if not (query_result.user == current_user
|
||||
or current_user.is_administrator()):
|
||||
abort(403)
|
||||
return send_from_directory(as_attachment=True,
|
||||
|
@ -21,7 +21,7 @@ def add_corpus():
|
||||
form = AddCorpusForm(prefix='add-corpus-form')
|
||||
if form.validate_on_submit():
|
||||
corpus = Corpus(
|
||||
creator=current_user,
|
||||
user=current_user,
|
||||
description=form.description.data,
|
||||
title=form.title.data
|
||||
)
|
||||
@ -52,7 +52,7 @@ def import_corpus():
|
||||
if not form.validate():
|
||||
return make_response(form.errors, 400)
|
||||
corpus = Corpus(
|
||||
creator=current_user,
|
||||
user=current_user,
|
||||
description=form.description.data,
|
||||
title=form.title.data
|
||||
)
|
||||
@ -115,18 +115,18 @@ def import_corpus():
|
||||
title='Import Corpus')
|
||||
|
||||
|
||||
@bp.route('/<int:corpus_id>')
|
||||
@bp.route('/<hashid:corpus_id>')
|
||||
@login_required
|
||||
def corpus(corpus_id):
|
||||
corpus = Corpus.query.get_or_404(corpus_id)
|
||||
if not (corpus.creator == current_user or current_user.is_administrator()):
|
||||
if not (corpus.user == current_user or current_user.is_administrator()):
|
||||
abort(403)
|
||||
corpus_files = [corpus_file.to_dict() for corpus_file in corpus.files]
|
||||
return render_template('corpora/corpus.html.j2', corpus=corpus,
|
||||
corpus_files=corpus_files, title='Corpus')
|
||||
|
||||
|
||||
@bp.route('/<int:corpus_id>/analyse')
|
||||
@bp.route('/<hashid:corpus_id>/analyse')
|
||||
@login_required
|
||||
def analyse_corpus(corpus_id):
|
||||
corpus = Corpus.query.get_or_404(corpus_id)
|
||||
@ -137,37 +137,37 @@ def analyse_corpus(corpus_id):
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/<int:corpus_id>/download')
|
||||
@bp.route('/<hashid:corpus_id>/download')
|
||||
@login_required
|
||||
def download_corpus(corpus_id):
|
||||
abort(503)
|
||||
corpus = Corpus.query.get_or_404(corpus_id)
|
||||
if not (corpus.creator == current_user or current_user.is_administrator()):
|
||||
if not (corpus.user == current_user or current_user.is_administrator()):
|
||||
abort(403)
|
||||
return send_from_directory(
|
||||
as_attachment=True,
|
||||
directory=os.path.join(corpus.creator.path, 'corpora'),
|
||||
directory=os.path.join(corpus.user.path, 'corpora'),
|
||||
filename=corpus.archive_file,
|
||||
mimetype='zip'
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/<int:corpus_id>/delete')
|
||||
@bp.route('/<hashid:corpus_id>/delete')
|
||||
@login_required
|
||||
def delete_corpus(corpus_id):
|
||||
corpus = Corpus.query.get_or_404(corpus_id)
|
||||
if not (corpus.creator == current_user or current_user.is_administrator()):
|
||||
if not (corpus.user == current_user or current_user.is_administrator()):
|
||||
abort(403)
|
||||
flash('Corpus "{}" marked for deletion!'.format(corpus.title), 'corpus')
|
||||
tasks.delete_corpus(corpus_id)
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
|
||||
@bp.route('/<int:corpus_id>/files/add', methods=['GET', 'POST'])
|
||||
@bp.route('/<hashid:corpus_id>/files/add', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def add_corpus_file(corpus_id):
|
||||
corpus = Corpus.query.get_or_404(corpus_id)
|
||||
if not (corpus.creator == current_user or current_user.is_administrator()):
|
||||
if not (corpus.user == current_user or current_user.is_administrator()):
|
||||
abort(403)
|
||||
form = AddCorpusFileForm(corpus, prefix='add-corpus-file-form')
|
||||
if form.is_submitted():
|
||||
@ -200,13 +200,13 @@ def add_corpus_file(corpus_id):
|
||||
form=form, title='Add corpus file')
|
||||
|
||||
|
||||
@bp.route('/<int:corpus_id>/files/<int:corpus_file_id>/delete')
|
||||
@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>/delete')
|
||||
@login_required
|
||||
def delete_corpus_file(corpus_id, corpus_file_id):
|
||||
corpus_file = CorpusFile.query.get_or_404(corpus_file_id)
|
||||
if not corpus_file.corpus_id == corpus_id:
|
||||
abort(404)
|
||||
if not (corpus_file.corpus.creator == current_user
|
||||
if not (corpus_file.corpus.user == current_user
|
||||
or current_user.is_administrator()):
|
||||
abort(403)
|
||||
flash('Corpus file "{}" marked for deletion!'.format(corpus_file.filename), 'corpus') # noqa
|
||||
@ -214,13 +214,13 @@ def delete_corpus_file(corpus_id, corpus_file_id):
|
||||
return redirect(url_for('.corpus', corpus_id=corpus_id))
|
||||
|
||||
|
||||
@bp.route('/<int:corpus_id>/files/<int:corpus_file_id>/download')
|
||||
@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>/download')
|
||||
@login_required
|
||||
def download_corpus_file(corpus_id, corpus_file_id):
|
||||
corpus_file = CorpusFile.query.get_or_404(corpus_file_id)
|
||||
if not corpus_file.corpus_id == corpus_id:
|
||||
abort(404)
|
||||
if not (corpus_file.corpus.creator == current_user
|
||||
if not (corpus_file.corpus.user == current_user
|
||||
or current_user.is_administrator()):
|
||||
abort(403)
|
||||
return send_from_directory(as_attachment=True,
|
||||
@ -228,11 +228,11 @@ def download_corpus_file(corpus_id, corpus_file_id):
|
||||
filename=corpus_file.filename)
|
||||
|
||||
|
||||
@bp.route('/<int:corpus_id>/files/<int:corpus_file_id>', methods=['GET', 'POST'])
|
||||
@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def corpus_file(corpus_id, corpus_file_id):
|
||||
corpus = Corpus.query.get_or_404(corpus_id)
|
||||
if not (corpus.creator == current_user or current_user.is_administrator()):
|
||||
if not (corpus.user == current_user or current_user.is_administrator()):
|
||||
abort(403)
|
||||
corpus_file = CorpusFile.query.get_or_404(corpus_file_id)
|
||||
if corpus_file.corpus != corpus:
|
||||
@ -273,11 +273,11 @@ def corpus_file(corpus_id, corpus_file_id):
|
||||
title='Edit corpus file')
|
||||
|
||||
|
||||
@bp.route('/<int:corpus_id>/prepare')
|
||||
@bp.route('/<hashid:corpus_id>/prepare')
|
||||
@login_required
|
||||
def prepare_corpus(corpus_id):
|
||||
corpus = Corpus.query.get_or_404(corpus_id)
|
||||
if not (corpus.creator == current_user or current_user.is_administrator()):
|
||||
if not (corpus.user == current_user or current_user.is_administrator()):
|
||||
abort(403)
|
||||
if corpus.files.all():
|
||||
tasks.build_corpus(corpus_id)
|
||||
|
@ -21,6 +21,7 @@ class Daemon(CheckCorporaMixin, CheckJobsMixin):
|
||||
self.check_corpora()
|
||||
self.check_jobs()
|
||||
db.session.commit()
|
||||
except:
|
||||
except Exception as e:
|
||||
current_app.logger.warning(e)
|
||||
pass
|
||||
sleep(1.5)
|
||||
|
@ -1,4 +1,4 @@
|
||||
from flask import request
|
||||
from app import hashids
|
||||
from flask_login import current_user
|
||||
from flask_socketio import join_room
|
||||
from .. import socketio
|
||||
@ -6,68 +6,23 @@ from ..decorators import socketio_login_required
|
||||
from ..models import User
|
||||
|
||||
|
||||
'''
|
||||
' A list containing session ids of Socket.IO sessions, to keep track
|
||||
' of all connected sessions, which can be used to determine the runtimes of
|
||||
' associated background tasks.
|
||||
'''
|
||||
sessions = []
|
||||
|
||||
|
||||
###############################################################################
|
||||
# Socket.IO event handlers #
|
||||
###############################################################################
|
||||
@socketio.on('connect')
|
||||
@socketio.on('users.user.get')
|
||||
@socketio_login_required
|
||||
def socketio_connect():
|
||||
'''
|
||||
' The Socket.IO module creates a session id (sid) for each request.
|
||||
' On connect the sid is saved in the sessions list.
|
||||
'''
|
||||
sessions.append(request.sid)
|
||||
# return {'code': 200, 'msg': 'OK'}
|
||||
|
||||
|
||||
@socketio.on('disconnect')
|
||||
def socketio_disconnect():
|
||||
'''
|
||||
' On disconnect the session id gets removed from the sessions list.
|
||||
'''
|
||||
try:
|
||||
sessions.remove(request.sid)
|
||||
except ValueError:
|
||||
pass
|
||||
# return {'code': 200, 'msg': 'OK'}
|
||||
|
||||
|
||||
@socketio.on('start_user_session')
|
||||
@socketio_login_required
|
||||
def socketio_start_user_session(user_id):
|
||||
user = User.query.get(user_id)
|
||||
if user is None:
|
||||
response = {'code': 404, 'msg': 'Not found'}
|
||||
socketio.emit('start_user_session', response, room=request.sid)
|
||||
elif not (user == current_user or current_user.is_administrator):
|
||||
response = {'code': 403, 'msg': 'Forbidden'}
|
||||
socketio.emit('start_user_session', response, room=request.sid)
|
||||
else:
|
||||
response = {'code': 200, 'msg': 'OK'}
|
||||
socketio.emit('start_user_session', response, room=request.sid)
|
||||
socketio.emit('user_{}_init'.format(user.id), user.to_dict(),
|
||||
room=request.sid)
|
||||
room = 'user_{}'.format(user.id)
|
||||
join_room(room)
|
||||
|
||||
|
||||
@socketio.on('users.request')
|
||||
@socketio_login_required
|
||||
def socketio_start_session(user_id):
|
||||
def users_user_get(user_hashid):
|
||||
user_id = hashids.decode(user_hashid)[0]
|
||||
user = User.query.get(user_id)
|
||||
if user is None:
|
||||
response = {'code': 404, 'msg': 'Not found'}
|
||||
elif not (user == current_user or current_user.is_administrator):
|
||||
response = {'code': 403, 'msg': 'Forbidden'}
|
||||
else:
|
||||
response = {'code': 200, 'msg': 'OK', 'payload': user.to_dict()}
|
||||
join_room('users.{}'.format(user.id))
|
||||
response = {
|
||||
'code': 200,
|
||||
'msg': 'OK',
|
||||
'payload': user.to_dict(backrefs=True, relationships=True)
|
||||
}
|
||||
join_room(f'users.{user.hashid}')
|
||||
return response
|
||||
|
@ -1,4 +1,5 @@
|
||||
from datetime import datetime
|
||||
from flask import current_app
|
||||
from .. import db, mail, socketio
|
||||
from ..email import create_message
|
||||
from ..models import Corpus, CorpusFile, Job, JobInput, JobResult, QueryResult
|
||||
@ -14,10 +15,9 @@ from ..models import Corpus, CorpusFile, Job, JobInput, JobResult, QueryResult
|
||||
@db.event.listens_for(JobResult, 'after_delete')
|
||||
@db.event.listens_for(QueryResult, 'after_delete')
|
||||
def ressource_after_delete(mapper, connection, ressource):
|
||||
event = 'user_{}_patch'.format(ressource.user_id)
|
||||
jsonpatch = [{'op': 'remove', 'path': ressource.jsonpatch_path}]
|
||||
room = 'user_{}'.format(ressource.user_id)
|
||||
socketio.emit(event, jsonpatch, room=room)
|
||||
room = f'users.{ressource.user_hashid}'
|
||||
socketio.emit('users.patch', jsonpatch, room=room)
|
||||
|
||||
|
||||
@db.event.listens_for(Corpus, 'after_insert')
|
||||
@ -27,16 +27,12 @@ def ressource_after_delete(mapper, connection, ressource):
|
||||
@db.event.listens_for(JobResult, 'after_insert')
|
||||
@db.event.listens_for(QueryResult, 'after_insert')
|
||||
def ressource_after_insert_handler(mapper, connection, ressource):
|
||||
event = 'user_{}_patch'.format(ressource.user_id)
|
||||
value = ressource.to_dict(backrefs=False, relationships=False)
|
||||
jsonpatch = [
|
||||
{
|
||||
'op': 'add',
|
||||
'path': ressource.jsonpatch_path,
|
||||
'value': ressource.to_dict(include_relationships=False)
|
||||
}
|
||||
{'op': 'add', 'path': ressource.jsonpatch_path, 'value': value}
|
||||
]
|
||||
room = 'user_{}'.format(ressource.user_id)
|
||||
socketio.emit(event, jsonpatch, room=room)
|
||||
room = f'users.{ressource.user_hashid}'
|
||||
socketio.emit('users.patch', jsonpatch, room=room)
|
||||
|
||||
|
||||
@db.event.listens_for(Corpus, 'after_update')
|
||||
@ -63,26 +59,25 @@ def ressource_after_update_handler(mapper, connection, ressource):
|
||||
jsonpatch.append(
|
||||
{
|
||||
'op': 'replace',
|
||||
'path': '{}/{}'.format(ressource.jsonpatch_path, attr.key),
|
||||
'path': f'{ressource.jsonpatch_path}/{attr.key}',
|
||||
'value': new_value
|
||||
}
|
||||
)
|
||||
# Job status update notification if it changed and wanted by the user
|
||||
if isinstance(ressource, Job) and attr.key == 'status':
|
||||
if ressource.creator.setting_job_status_mail_notifications == 'none': # noqa
|
||||
if ressource.user.setting_job_status_mail_notifications == 'none': # noqa
|
||||
pass
|
||||
elif (ressource.creator.setting_job_status_mail_notifications == 'end' # noqa
|
||||
elif (ressource.user.setting_job_status_mail_notifications == 'end' # noqa
|
||||
and ressource.status not in ['complete', 'failed']):
|
||||
pass
|
||||
else:
|
||||
msg = create_message(
|
||||
ressource.creator.email,
|
||||
'Status update for your Job "{}"'.format(ressource.title),
|
||||
ressource.user.email,
|
||||
f'Status update for your Job "{ressource.title}"',
|
||||
'tasks/email/notification',
|
||||
job=ressource
|
||||
)
|
||||
mail.send(msg)
|
||||
if jsonpatch:
|
||||
event = 'user_{}_patch'.format(ressource.user_id)
|
||||
room = 'user_{}'.format(ressource.user_id)
|
||||
socketio.emit(event, jsonpatch, room=room)
|
||||
room = f'users.{ressource.user_hashid}'
|
||||
socketio.emit('users.patch', jsonpatch, room=room)
|
||||
|
@ -8,33 +8,33 @@ from ..models import Job, JobInput, JobResult
|
||||
import os
|
||||
|
||||
|
||||
@bp.route('/<int:job_id>')
|
||||
@bp.route('/<hashid:job_id>')
|
||||
@login_required
|
||||
def job(job_id):
|
||||
job = Job.query.get_or_404(job_id)
|
||||
if not (job.creator == current_user or current_user.is_administrator()):
|
||||
if not (job.user == current_user or current_user.is_administrator()):
|
||||
abort(403)
|
||||
job_inputs = [job_input.to_dict() for job_input in job.inputs]
|
||||
return render_template('jobs/job.html.j2', job=job, job_inputs=job_inputs,
|
||||
title='Job')
|
||||
|
||||
|
||||
@bp.route('/<int:job_id>/delete')
|
||||
@bp.route('/<hashid:job_id>/delete')
|
||||
@login_required
|
||||
def delete_job(job_id):
|
||||
job = Job.query.get_or_404(job_id)
|
||||
if not (job.creator == current_user or current_user.is_administrator()):
|
||||
if not (job.user == current_user or current_user.is_administrator()):
|
||||
abort(403)
|
||||
tasks.delete_job(job_id)
|
||||
flash('Job has been marked for deletion!', 'job')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
|
||||
@bp.route('/<int:job_id>/inputs/<int:job_input_id>/download')
|
||||
@bp.route('/<hashid:job_id>/inputs/<hashid:job_input_id>/download')
|
||||
@login_required
|
||||
def download_job_input(job_id, job_input_id):
|
||||
job_input = JobInput.query.filter(JobInput.job_id == job_id, JobInput.id == job_input_id).first_or_404() # noqa
|
||||
if not (job_input.job.creator == current_user
|
||||
if not (job_input.job.user == current_user
|
||||
or current_user.is_administrator()):
|
||||
abort(403)
|
||||
return send_from_directory(as_attachment=True,
|
||||
@ -42,7 +42,7 @@ def download_job_input(job_id, job_input_id):
|
||||
filename=job_input.filename)
|
||||
|
||||
|
||||
@bp.route('/<int:job_id>/restart')
|
||||
@bp.route('/<hashid:job_id>/restart')
|
||||
@login_required
|
||||
@admin_required
|
||||
def restart(job_id):
|
||||
@ -55,11 +55,11 @@ def restart(job_id):
|
||||
return redirect(url_for('.job', job_id=job_id))
|
||||
|
||||
|
||||
@bp.route('/<int:job_id>/results/<int:job_result_id>/download')
|
||||
@bp.route('/<hashid:job_id>/results/<hashid:job_result_id>/download')
|
||||
@login_required
|
||||
def download_job_result(job_id, job_result_id):
|
||||
job_result = JobResult.query.filter(JobResult.job_id == job_id, JobResult.id == job_result_id).first_or_404() # noqa
|
||||
if not (job_result.job.creator == current_user
|
||||
if not (job_result.job.user == current_user
|
||||
or current_user.is_administrator()):
|
||||
abort(403)
|
||||
return send_from_directory(as_attachment=True,
|
||||
|
770
app/models.py
770
app/models.py
File diff suppressed because it is too large
Load Diff
@ -24,8 +24,8 @@ def service(service):
|
||||
# Check if the requested service exist
|
||||
if service not in SERVICES or service not in AddJobForms:
|
||||
abort(404)
|
||||
version = request.args.get('version',
|
||||
SERVICES[service]['versions']['latest'])
|
||||
version = request.args.get(
|
||||
'version', SERVICES[service]['versions']['latest'])
|
||||
if version not in SERVICES[service]['versions']:
|
||||
abort(404)
|
||||
form = AddJobForms[service](prefix='add-job-form', version=version)
|
||||
@ -44,7 +44,7 @@ def service(service):
|
||||
service_args.append('-l {}'.format(form.language.data))
|
||||
if form.binarization.data:
|
||||
service_args.append('--binarize')
|
||||
job = Job(creator=current_user,
|
||||
job = Job(user=current_user,
|
||||
description=form.description.data,
|
||||
service=service, service_args=json.dumps(service_args),
|
||||
service_version=form.version.data,
|
||||
@ -65,7 +65,8 @@ def service(service):
|
||||
else:
|
||||
for file in form.files.data:
|
||||
filename = secure_filename(file.filename)
|
||||
job_input = JobInput(filename=filename, job=job)
|
||||
job_input = JobInput(
|
||||
filename=filename, job=job, mimetype=file.mimetype)
|
||||
file.save(job_input.path)
|
||||
db.session.add(job_input)
|
||||
job.status = 'submitted'
|
||||
|
80
app/static/js/nopaque/App.js
Normal file
80
app/static/js/nopaque/App.js
Normal file
@ -0,0 +1,80 @@
|
||||
class App {
|
||||
constructor() {
|
||||
this.data = {users: {}};
|
||||
this.eventListeners = {'users.patch': []};
|
||||
this.socket = io({transports: ['websocket'], upgrade: false});
|
||||
this.socket.on('users.patch', patch => this.usersPatchHandler(patch));
|
||||
}
|
||||
|
||||
get users() {return this.data.users;}
|
||||
|
||||
addEventListener(type, listener) {
|
||||
if (!(type in this.eventListeners)) {throw `Unknown event type: ${type}`;}
|
||||
this.eventListeners[type].push(listener);
|
||||
}
|
||||
|
||||
flash(message, category) {
|
||||
let toast, toastCloseActionElement;
|
||||
switch (category) {
|
||||
case "corpus":
|
||||
message = `<i class="left material-icons">book</i>${message}`;
|
||||
break;
|
||||
case "error":
|
||||
message = `<i class="left material-icons error-color-text">error</i>${message}`;
|
||||
break;
|
||||
case "job":
|
||||
message = `<i class="left nopaque-icons">J</i>${message}`;
|
||||
break;
|
||||
default:
|
||||
message = `<i class="left material-icons">notifications</i>${message}`;
|
||||
}
|
||||
toast = M.toast({
|
||||
html: `
|
||||
<span>${message}</span>
|
||||
<button class="btn-flat toast-action white-text" data-action="close">
|
||||
<i class="material-icons">close</i>
|
||||
</button>
|
||||
`.trim()
|
||||
});
|
||||
toastCloseActionElement = toast.el.querySelector('.toast-action[data-action="close"]');
|
||||
toastCloseActionElement.addEventListener('click', () => {toast.dismiss();});
|
||||
}
|
||||
|
||||
getUserById(userId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (userId in this.data.users) {resolve(this.users[userId]);}
|
||||
this.socket.emit('users.user.get', userId, response => {
|
||||
if (response.code === 200) {
|
||||
this.data.users[userId] = response.payload;
|
||||
resolve(this.data.users[userId]);
|
||||
} else {
|
||||
reject(response);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
usersPatchHandler(patch) {
|
||||
let re, match, userId, ressourceId, jobId, relationship;
|
||||
for (let operation of patch.filter(operation => operation.op === 'add')) {
|
||||
re = new RegExp(`^/users/([A-Za-z0-9]*)/corpora/([A-Za-z0-9]*)/(files)`);
|
||||
if (re.test(operation.path)) {
|
||||
[match, userId, ressourceId, relationship] = operation.path.match(re);
|
||||
if (!(relationship in this.users[userId].corpora[ressourceId])) {
|
||||
this.users[userId].corpora[ressourceId][relationship] = {};
|
||||
}
|
||||
continue;
|
||||
}
|
||||
re = new RegExp(`^/users/([A-Za-z0-9]*)/jobs/([A-Za-z0-9]*)/(inputs|results)`);
|
||||
if (re.test(operation.path)) {
|
||||
[match, userId, ressourceId, relationship] = operation.path.match(re);
|
||||
if (!(relationship in this.users[userId].jobs[ressourceId])) {
|
||||
this.users[userId].jobs[ressourceId][relationship] = {};
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
this.data = jsonpatch.apply_patch(this.data, patch);
|
||||
for (let listener of this.eventListeners['users.patch']) {listener(patch);}
|
||||
}
|
||||
}
|
17
app/static/js/nopaque/JobStatusNotifier.js
Normal file
17
app/static/js/nopaque/JobStatusNotifier.js
Normal file
@ -0,0 +1,17 @@
|
||||
class JobStatusNotifier {
|
||||
constructor(userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
usersPatchHandler(patch) {
|
||||
let re, filteredPatch, match, jobId;
|
||||
re = new RegExp(`^/users/${this.userId}/jobs/([A-Za-z0-9]*)/status$`)
|
||||
filteredPatch = patch
|
||||
.filter(operation => operation.op === 'replace')
|
||||
.filter(operation => re.test(operation.path));
|
||||
for (let operation of filteredPatch) {
|
||||
[match, jobId] = operation.path.match(re);
|
||||
app.flash(`[<a href="/jobs/${jobId}">${app.users[this.userId].jobs[jobId].title}</a>] New status: ${operation.value}`, 'job');
|
||||
}
|
||||
}
|
||||
}
|
@ -2,31 +2,34 @@ class CorpusDisplay extends RessourceDisplay {
|
||||
constructor(displayElement) {
|
||||
super(displayElement);
|
||||
this.corpusId = displayElement.dataset.corpusId;
|
||||
this.user.eventListeners.corpus.addEventListener((eventType, payload) => this.eventHandler(eventType, payload), this.corpusId);
|
||||
for (let exportCorpusTriggerElement of this.displayElement.querySelectorAll('.export-corpus-trigger')) {
|
||||
exportCorpusTriggerElement.addEventListener('click', () => this.requestCorpusExport());
|
||||
}
|
||||
app.socket.on(`export_corpus_${this.corpusId}`, () => this.downloadCorpus());
|
||||
}
|
||||
|
||||
init() {
|
||||
for (let exportCorpusTriggerElement of this.displayElement.querySelectorAll('.export-corpus-trigger')) {exportCorpusTriggerElement.addEventListener('click', () => this.requestCorpusExport());}
|
||||
nopaque.appClient.socket.on(`export_corpus_${this.user.data.corpora[this.corpusId].id}`, () => this.downloadCorpus());
|
||||
this.setCreationDate(this.user.data.corpora[this.corpusId].creation_date);
|
||||
this.setDescription(this.user.data.corpora[this.corpusId].description);
|
||||
this.setLastEditedDate(this.user.data.corpora[this.corpusId].last_edited_date);
|
||||
this.setStatus(this.user.data.corpora[this.corpusId].status);
|
||||
this.setTitle(this.user.data.corpora[this.corpusId].title);
|
||||
this.setTokenRatio(this.user.data.corpora[this.corpusId].num_tokens, this.user.data.corpora[this.corpusId].max_num_tokens);
|
||||
init(user) {
|
||||
let corpus;
|
||||
corpus = user.corpora[this.corpusId];
|
||||
this.setCreationDate(corpus.creation_date);
|
||||
this.setDescription(corpus.description);
|
||||
this.setLastEditedDate(corpus.last_edited_date);
|
||||
this.setStatus(corpus.status);
|
||||
this.setTitle(corpus.title);
|
||||
this.setTokenRatio(corpus.num_tokens, corpus.max_num_tokens);
|
||||
}
|
||||
|
||||
patch(patch) {
|
||||
let re;
|
||||
for (let operation of patch) {
|
||||
let re, filteredPatch;
|
||||
re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}`);
|
||||
filteredPatch = patch.filter(operation => re.test(operation.path));
|
||||
for (let operation of filteredPatch) {
|
||||
switch(operation.op) {
|
||||
case 'replace':
|
||||
// Matches: /jobs/{this.job.id}/status
|
||||
re = new RegExp('^/corpora/' + this.user.data.corpora[this.corpusId].id + '/last_edited_date');
|
||||
re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/last_edited_date$`);
|
||||
if (re.test(operation.path)) {this.setLastEditedDate(operation.value); break;}
|
||||
// Matches: /jobs/{this.job.id}/status
|
||||
re = new RegExp('^/corpora/' + this.user.data.corpora[this.corpusId].id + '/status$');
|
||||
if (re.test(operation.path)) {this.setStatus(operation.value); break;}
|
||||
re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/status$`);
|
||||
if (re.test(operation.path)) {this.status$(operation.value); break;}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@ -35,18 +38,19 @@ class CorpusDisplay extends RessourceDisplay {
|
||||
}
|
||||
|
||||
requestCorpusExport() {
|
||||
nopaque.appClient.socket.emit('export_corpus', this.user.data.corpora[this.corpusId].id);
|
||||
nopaque.appClient.flash('Preparing your corpus export...', 'corpus');
|
||||
app.socket.emit('export_corpus', app.users[this.userId].corpora[this.corpusId]);
|
||||
app.flash('Preparing your corpus export...', 'corpus');
|
||||
for (let exportCorpusTriggerElement of this.displayElement.querySelectorAll('.export-corpus-trigger')) {exportCorpusTriggerElement.classList.toggle('disabled', true);}
|
||||
}
|
||||
|
||||
downloadCorpus() {
|
||||
nopaque.appClient.flash('Corpus export is done. Your corpus download is ready!', 'corpus');
|
||||
let downloadButton;
|
||||
app.flash('Corpus download is ready!', 'corpus');
|
||||
for (let exportCorpusTriggerElement of this.displayElement.querySelectorAll('.export-corpus-trigger')) {exportCorpusTriggerElement.classList.toggle('disabled', false);}
|
||||
// Little trick to call the download view after ziping has finished
|
||||
let fakeBtn = document.createElement('a');
|
||||
fakeBtn.href = `/corpora/${this.user.data.corpora[this.corpusId].id}/download`;
|
||||
fakeBtn.click();
|
||||
downloadButton = document.createElement('a');
|
||||
downloadButton.href = `/corpora/${app.users[this.userId].corpora[this.corpusId]}/download`;
|
||||
downloadButton.click();
|
||||
}
|
||||
|
||||
setTitle(title) {
|
||||
@ -70,7 +74,7 @@ class CorpusDisplay extends RessourceDisplay {
|
||||
}
|
||||
}
|
||||
for (let element of this.displayElement.querySelectorAll('.build-corpus-trigger')) {
|
||||
if (status === 'unprepared' && Object.values(this.user.data.corpora[this.corpusId].files).length > 0) {
|
||||
if (status === 'unprepared' && Object.values(app.users[this.userId].corpora[this.corpusId].files).length > 0) {
|
||||
element.classList.remove('disabled');
|
||||
} else {
|
||||
element.classList.add('disabled');
|
||||
@ -90,13 +94,15 @@ class CorpusDisplay extends RessourceDisplay {
|
||||
}
|
||||
}
|
||||
|
||||
setCreationDate(iso8601CreationDate) {
|
||||
let creationDate = new Date(iso8601CreationDate).toLocaleString("en-US");
|
||||
for (let element of this.displayElement.querySelectorAll('.corpus-creation-date')) {this.setElement(element, creationDate);}
|
||||
setCreationDate(creationDate) {
|
||||
for (let element of this.displayElement.querySelectorAll('.corpus-creation-date')) {
|
||||
this.setElement(element, creationDate.toLocaleString("en-US"));
|
||||
}
|
||||
}
|
||||
|
||||
setLastEditedDate(iso8601LastEditedDate) {
|
||||
let endDate = new Date(iso8601LastEditedDate).toLocaleString("en-US");
|
||||
for (let element of this.displayElement.querySelectorAll('.corpus-end-date')) {this.setElement(element, endDate);}
|
||||
setLastEditedDate(lastEditedDate) {
|
||||
for (let element of this.displayElement.querySelectorAll('.corpus-end-date')) {
|
||||
this.setElement(element, lastEditedDate.toLocaleString("en-US"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,32 +1,37 @@
|
||||
class JobDisplay extends RessourceDisplay {
|
||||
constructor(displayElement) {
|
||||
super(displayElement);
|
||||
this.jobId = displayElement.dataset.jobId;
|
||||
this.user.eventListeners.job.addEventListener((eventType, payload) => this.eventHandler(eventType, payload), this.jobId);
|
||||
this.jobId = this.displayElement.dataset.jobId;
|
||||
}
|
||||
|
||||
init(job) {
|
||||
this.setCreationDate(this.user.data.jobs[this.jobId].creation_date);
|
||||
this.setEndDate(this.user.data.jobs[this.jobId].creation_date);
|
||||
this.setDescription(this.user.data.jobs[this.jobId].description);
|
||||
this.setService(this.user.data.jobs[this.jobId].service);
|
||||
this.setServiceArgs(this.user.data.jobs[this.jobId].service_args);
|
||||
this.setServiceVersion(this.user.data.jobs[this.jobId].service_version);
|
||||
this.setStatus(this.user.data.jobs[this.jobId].status);
|
||||
this.setTitle(this.user.data.jobs[this.jobId].title);
|
||||
init(user) {
|
||||
let job = user.jobs[this.jobId];
|
||||
this.setCreationDate(job.creation_date);
|
||||
this.setEndDate(job.creation_date);
|
||||
this.setDescription(job.description);
|
||||
this.setService(job.service);
|
||||
this.setServiceArgs(job.service_args);
|
||||
this.setServiceVersion(job.service_version);
|
||||
this.setStatus(job.status);
|
||||
this.setTitle(job.title);
|
||||
}
|
||||
|
||||
patch(patch) {
|
||||
let re;
|
||||
for (let operation of patch) {
|
||||
usersPatchHandler(patch) {
|
||||
let re = new RegExp(`^/users/${this.userId}/jobs/${this.jobId}`);
|
||||
let filteredPatch = patch.filter(operation => re.test(operation.path));
|
||||
for (let operation of filteredPatch) {
|
||||
switch(operation.op) {
|
||||
case 'replace':
|
||||
// Matches: /jobs/{this.user.data.jobs[this.jobId].id}/status
|
||||
re = new RegExp('^/jobs/' + this.user.data.jobs[this.jobId].id + '/end_date');
|
||||
if (re.test(operation.path)) {this.setEndDate(operation.value); break;}
|
||||
// Matches: /jobs/{this.user.data.jobs[this.jobId].id}/status
|
||||
re = new RegExp('^/jobs/' + this.user.data.jobs[this.jobId].id + '/status$');
|
||||
if (re.test(operation.path)) {this.setStatus(operation.value); break;}
|
||||
re = new RegExp(`^/users/${this.userId}/jobs/${this.jobId}/end_date$`);
|
||||
if (re.test(operation.path)) {
|
||||
this.setEndDate(operation.value);
|
||||
break;
|
||||
}
|
||||
re = new RegExp(`^/users/${this.userId}/jobs/${this.jobId}/status$`);
|
||||
if (re.test(operation.path)) {
|
||||
this.setStatus(operation.value);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@ -63,14 +68,16 @@ class JobDisplay extends RessourceDisplay {
|
||||
}
|
||||
}
|
||||
|
||||
setCreationDate(iso8601CreationDate) {
|
||||
let creationDate = new Date(iso8601CreationDate).toLocaleString("en-US");
|
||||
for (let element of this.displayElement.querySelectorAll('.job-creation-date')) {this.setElement(element, creationDate);}
|
||||
setCreationDate(creationDate) {
|
||||
for (let element of this.displayElement.querySelectorAll('.job-creation-date')) {
|
||||
this.setElement(element, creationDate.toLocaleString('en-US'));
|
||||
}
|
||||
}
|
||||
|
||||
setEndDate(iso8601EndDate) {
|
||||
let endDate = new Date(iso8601EndDate).toLocaleString("en-US");
|
||||
for (let element of this.displayElement.querySelectorAll('.job-end-date')) {this.setElement(element, endDate);}
|
||||
setEndDate(endDate) {
|
||||
for (let element of this.displayElement.querySelectorAll('.job-end-date')) {
|
||||
this.setElement(element, endDate.toLocaleString('en-US'));
|
||||
}
|
||||
}
|
||||
|
||||
setService(service) {
|
||||
|
@ -1,35 +1,14 @@
|
||||
class RessourceDisplay {
|
||||
constructor(displayElement) {
|
||||
if (displayElement.dataset.userId) {
|
||||
if (displayElement.dataset.userId in nopaque.appClient.users) {
|
||||
this.user = nopaque.appClient.users[displayElement.dataset.userId];
|
||||
} else {
|
||||
console.error(`User not found: ${displayElement.dataset.userId}`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this.user = nopaque.appClient.users.self;
|
||||
}
|
||||
this.displayElement = displayElement;
|
||||
this.userId = this.displayElement.dataset.userId;
|
||||
app.addEventListener('users.patch', patch => this.usersPatchHandler(patch));
|
||||
app.getUserById(this.userId).then(user => this.init(user), error => {throw JSON.stringify(error);});
|
||||
}
|
||||
|
||||
eventHandler(eventType, payload) {
|
||||
switch (eventType) {
|
||||
case 'init':
|
||||
this.init(payload);
|
||||
break;
|
||||
case 'patch':
|
||||
this.patch(payload);
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown event type: ${eventType}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
init(user) {throw 'Not implemented';}
|
||||
|
||||
init() {console.error('init method not implemented!');}
|
||||
|
||||
patch() {console.error('patch method not implemented!');}
|
||||
usersPatchHandler(patch) {throw 'Not implemented';}
|
||||
|
||||
setElement(element, value) {
|
||||
switch (element.tagName) {
|
||||
|
@ -2,32 +2,32 @@ class CorpusFileList extends RessourceList {
|
||||
constructor(listElement, options = {}) {
|
||||
super(listElement, {...CorpusFileList.options, ...options});
|
||||
this.corpusId = listElement.dataset.corpusId;
|
||||
this.user.eventListeners.corpus.addEventListener((eventType, payload) => this.eventHandler(eventType, payload), this.corpusId);
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(this.user.data.corpora[this.corpusId].files);
|
||||
init(user) {
|
||||
this._init(user.corpora[this.corpusId].files);
|
||||
}
|
||||
|
||||
onclick(event) {
|
||||
let ressourceElement = event.target.closest('tr');
|
||||
if (ressourceElement === null) {return;}
|
||||
let corpusFileId = ressourceElement.dataset.id;
|
||||
let actionButtonElement = event.target.closest('.action-button');
|
||||
if (actionButtonElement === null) {return;}
|
||||
let corpusFileElement = event.target.closest('tr[data-id]');
|
||||
if (corpusFileElement === null) {throw 'Could not locate corpus file element';}
|
||||
let corpusFileId = corpusFileElement.dataset.id;
|
||||
let actionButtonElement = event.target.closest('.action-button[data-action]');
|
||||
let action = actionButtonElement === null ? 'view' : actionButtonElement.dataset.action;
|
||||
switch (action) {
|
||||
case 'delete':
|
||||
let deleteModalHTML = `<div class="modal">
|
||||
<div class="modal-content">
|
||||
<h4>Confirm corpus deletion</h4>
|
||||
<p>Do you really want to delete the corpus file <b>${this.user.data.corpora[this.corpusId].files[corpusFileId].filename}</b>? It will be permanently deleted!</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
|
||||
<a class="btn modal-close red waves-effect waves-light" href="${this.user.data.corpora[this.corpusId].files[corpusFileId].url}/delete"><i class="material-icons left">delete</i>Delete</a>
|
||||
</div>
|
||||
</div>`;
|
||||
let deleteModalHTML = `
|
||||
<div class="modal">
|
||||
<div class="modal-content">
|
||||
<h4>Confirm corpus deletion</h4>
|
||||
<p>Do you really want to delete the corpus file <b>${app.users[this.userId].corpora[this.corpusId].files[corpusFileId].filename}</b>? It will be permanently deleted!</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
|
||||
<a class="btn modal-close red waves-effect waves-light" href="/corpora/${this.corpusId}/files/${corpusFileId}/delete"><i class="material-icons left">delete</i>Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
`.trim();
|
||||
let deleteModalParentElement = document.querySelector('#modals');
|
||||
deleteModalParentElement.insertAdjacentHTML('beforeend', deleteModalHTML);
|
||||
let deleteModalElement = deleteModalParentElement.lastChild;
|
||||
@ -35,40 +35,37 @@ class CorpusFileList extends RessourceList {
|
||||
deleteModal.open();
|
||||
break;
|
||||
case 'download':
|
||||
window.location.href = this.user.data.corpora[this.corpusId].files[corpusFileId].download_url;
|
||||
window.location.href = `/corpora/${this.corpusId}/files/${corpusFileId}/download`;
|
||||
break;
|
||||
case 'view':
|
||||
if (corpusFileId !== '-1') {window.location.href = this.user.data.corpora[this.corpusId].files[corpusFileId].url;}
|
||||
window.location.href = `/corpora/${this.corpusId}/files/${corpusFileId}`;
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown action: "${action}"`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
patch(patch) {
|
||||
let id, match, re, valueName;
|
||||
for (let operation of patch) {
|
||||
usersPatchHandler(patch) {
|
||||
let re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/files/([A-Za-z0-9]*)`);
|
||||
let filteredPatch = patch.filter(operation => re.test(operation.path));
|
||||
for (let operation of filteredPatch) {
|
||||
switch(operation.op) {
|
||||
case 'add':
|
||||
// Matches the only paths that should be handled here: /corpora/{this.user.data.corpora[this.corpusId].id}/files/{corpusFileId}
|
||||
re = new RegExp('^/corpora/' + this.user.data.corpora[this.corpusId].id + '/files/(\\d+)$');
|
||||
re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/files/([A-Za-z0-9]*)$`);
|
||||
if (re.test(operation.path)) {this.add(operation.value);}
|
||||
break;
|
||||
case 'remove':
|
||||
// See case add ;)
|
||||
re = new RegExp('^/corpora/' + this.user.data.corpora[this.corpusId].id + '/files/(\\d+)$');
|
||||
re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/files/([A-Za-z0-9]*)$`);
|
||||
if (re.test(operation.path)) {
|
||||
[match, id] = operation.path.match(re);
|
||||
this.remove(id);
|
||||
[match, corpusFileId] = operation.path.match(re);
|
||||
this.remove(corpusFileId);
|
||||
}
|
||||
break;
|
||||
case 'replace':
|
||||
// Matches the only paths that should be handled here: /corpora/{corpusId}/{status || description || title}
|
||||
re = new RegExp('^/corpora/' + this.user.data.corpora[this.corpusId].id + '/files/(\\d+)/(author|filename|publishing_year|title)$');
|
||||
re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/files/([A-Za-z0-9]*)/(author|filename|publishing_year|title)$`);
|
||||
if (re.test(operation.path)) {
|
||||
[match, id, valueName] = operation.path.match(re);
|
||||
this.replace(id, valueName, operation.value);
|
||||
[match, corpusFileId, valueName] = operation.path.match(re);
|
||||
this.replace(corpusFileId, valueName, operation.value);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@ -78,20 +75,29 @@ class CorpusFileList extends RessourceList {
|
||||
}
|
||||
|
||||
preprocessRessource(corpusFile) {
|
||||
return {id: corpusFile.id, author: corpusFile.author, filename: corpusFile.filename, publishing_year: corpusFile.publishing_year, title: corpusFile.title};
|
||||
return {
|
||||
id: corpusFile.id,
|
||||
author: corpusFile.author,
|
||||
creationDate: corpusFile.creation_date,
|
||||
filename: corpusFile.filename,
|
||||
publishing_year: corpusFile.publishing_year,
|
||||
title: corpusFile.title
|
||||
};
|
||||
}
|
||||
}
|
||||
CorpusFileList.options = {
|
||||
item: `<tr>
|
||||
<td><span class="filename"></span></td>
|
||||
<td><span class="author"></span></td>
|
||||
<td><span class="title"></span></td>
|
||||
<td><span class="publishing_year"></span></td>
|
||||
<td class="right-align">
|
||||
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
|
||||
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="download" data-position="top" data-tooltip="View"><i class="material-icons">file_download</i></a>
|
||||
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
|
||||
</td>
|
||||
</tr>`,
|
||||
item: `
|
||||
<tr>
|
||||
<td><span class="filename"></span></td>
|
||||
<td><span class="author"></span></td>
|
||||
<td><span class="title"></span></td>
|
||||
<td><span class="publishing_year"></span></td>
|
||||
<td class="right-align">
|
||||
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
|
||||
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="download" data-position="top" data-tooltip="View"><i class="material-icons">file_download</i></a>
|
||||
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
|
||||
</td>
|
||||
</tr>
|
||||
`.trim(),
|
||||
valueNames: [{data: ['id']}, 'author', 'filename', 'publishing_year', 'title']
|
||||
};
|
||||
|
@ -1,31 +1,32 @@
|
||||
class CorpusList extends RessourceList {
|
||||
constructor(listElement, options = {}) {
|
||||
super(listElement, {...CorpusList.options, ...options});
|
||||
this.user.eventListeners.corpus.addEventListener((eventType, payload) => this.eventHandler(eventType, payload));
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(this.user.data.corpora);
|
||||
init(user) {
|
||||
super._init(user.corpora);
|
||||
}
|
||||
|
||||
onclick(event) {
|
||||
let ressourceElement = event.target.closest('tr');
|
||||
if (ressourceElement === null) {return;}
|
||||
let corpusId = ressourceElement.dataset.id;
|
||||
let actionButtonElement = event.target.closest('.action-button');
|
||||
let action = (actionButtonElement === null) ? 'view' : actionButtonElement.dataset.action;
|
||||
let corpusElement = event.target.closest('tr[data-id]');
|
||||
if (corpusElement === null) {throw 'Could not locate corpus element';}
|
||||
let corpusId = corpusElement.dataset.id;
|
||||
let actionButtonElement = event.target.closest('.action-button[data-action]');
|
||||
let action = actionButtonElement === null ? 'view' : actionButtonElement.dataset.action;
|
||||
switch (action) {
|
||||
case 'delete':
|
||||
let deleteModalHTML = `<div class="modal">
|
||||
<div class="modal-content">
|
||||
<h4>Confirm corpus deletion</h4>
|
||||
<p>Do you really want to delete the corpus <b>${this.user.data.corpora[corpusId].title}</b>? All files will be permanently deleted!</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
|
||||
<a class="btn modal-close red waves-effect waves-light" href="${this.user.data.corpora[corpusId].url}/delete"><i class="material-icons left">delete</i>Delete</a>
|
||||
</div>
|
||||
</div>`;
|
||||
let deleteModalHTML = `
|
||||
<div class="modal">
|
||||
<div class="modal-content">
|
||||
<h4>Confirm corpus deletion</h4>
|
||||
<p>Do you really want to delete the corpus <b>${app.users[this.userId].corpora[corpusId].title}</b>? All files will be permanently deleted!</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
|
||||
<a class="btn modal-close red waves-effect waves-light" href="/corpora/${corpusId}/delete"><i class="material-icons left">delete</i>Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
`.trim();
|
||||
let deleteModalParentElement = document.querySelector('#modals');
|
||||
deleteModalParentElement.insertAdjacentHTML('beforeend', deleteModalHTML);
|
||||
let deleteModalElement = deleteModalParentElement.lastChild;
|
||||
@ -33,37 +34,34 @@ class CorpusList extends RessourceList {
|
||||
deleteModal.open();
|
||||
break;
|
||||
case 'view':
|
||||
if (corpusId !== '-1') {window.location.href = this.user.data.corpora[corpusId].url;}
|
||||
window.location.href = `/corpora/${corpusId}`;
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown action: ${action}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
patch(patch) {
|
||||
let id, match, re, valueName;
|
||||
for (let operation of patch) {
|
||||
usersPatchHandler(patch) {
|
||||
let re = new RegExp(`^/users/${this.userId}/corpora/([A-Za-z0-9]*)`);
|
||||
let filteredPatch = patch.filter(operation => re.test(operation.path));
|
||||
for (let operation of filteredPatch) {
|
||||
switch(operation.op) {
|
||||
case 'add':
|
||||
// Matches the only paths that should be handled here: /corpora/{corpusId}
|
||||
re = /^\/corpora\/(\d+)$/;
|
||||
re = new RegExp(`^/users/${this.userId}/corpora/([A-Za-z0-9]*)$`);
|
||||
if (re.test(operation.path)) {this.add(operation.value);}
|
||||
break;
|
||||
case 'remove':
|
||||
// See case 'add' ;)
|
||||
re = /^\/corpora\/(\d+)$/;
|
||||
re = new RegExp(`^/users/${this.userId}/corpora/([A-Za-z0-9]*)$`);
|
||||
if (re.test(operation.path)) {
|
||||
[match, id] = operation.path.match(re);
|
||||
this.remove(id);
|
||||
let [match, corpusId] = operation.path.match(re);
|
||||
this.remove(corpusId);
|
||||
}
|
||||
break;
|
||||
case 'replace':
|
||||
// Matches the only paths that should be handled here: /corpora/{corpusId}/{status || description || title}
|
||||
re = /^\/corpora\/(\d+)\/(status|description|title)$/;
|
||||
re = new RegExp(`^/users/${this.userId}/corpora/([A-Za-z0-9]*)/(status|description|title)$`);
|
||||
if (re.test(operation.path)) {
|
||||
[match, id, valueName] = operation.path.match(re);
|
||||
this.replace(id, valueName, operation.value);
|
||||
let [match, corpusId, valueName] = operation.path.match(re);
|
||||
this.replace(corpusId, valueName, operation.value);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@ -73,21 +71,26 @@ class CorpusList extends RessourceList {
|
||||
}
|
||||
|
||||
preprocessRessource(corpus) {
|
||||
return {id: corpus.id,
|
||||
status: corpus.status,
|
||||
description: corpus.description,
|
||||
title: corpus.title};
|
||||
return {
|
||||
id: corpus.id,
|
||||
creationDate: corpus.creation_date,
|
||||
description: corpus.description,
|
||||
status: corpus.status,
|
||||
title: corpus.title
|
||||
};
|
||||
}
|
||||
}
|
||||
CorpusList.options = {
|
||||
item: `<tr>
|
||||
<td><a class="btn-floating disabled"><i class="material-icons service-color darken" data-service="corpus-analysis">book</i></a></td>
|
||||
<td><b class="title"></b><br><i class="description"></i></td>
|
||||
<td><span class="badge new status status-color status-text" data-badge-caption=""></span></td>
|
||||
<td class="right-align">
|
||||
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
|
||||
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
|
||||
</td>
|
||||
</tr>`,
|
||||
item: `
|
||||
<tr>
|
||||
<td><a class="btn-floating disabled"><i class="material-icons service-color darken" data-service="corpus-analysis">book</i></a></td>
|
||||
<td><b class="title"></b><br><i class="description"></i></td>
|
||||
<td><span class="badge new status status-color status-text" data-badge-caption=""></span></td>
|
||||
<td class="right-align">
|
||||
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
|
||||
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
|
||||
</td>
|
||||
</tr>
|
||||
`.trim(),
|
||||
valueNames: [{data: ['id']}, {name: 'status', attr: 'data-status'}, 'description', 'title']
|
||||
};
|
||||
|
@ -2,40 +2,46 @@ class JobInputList extends RessourceList {
|
||||
constructor(listElement, options = {}) {
|
||||
super(listElement, {...JobInputList.options, ...options});
|
||||
this.jobId = listElement.dataset.jobId;
|
||||
this.user.eventListeners.job.addEventListener((eventType, payload) => this.eventHandler(eventType, payload), this.jobId);
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(this.user.data.jobs[this.jobId].inputs);
|
||||
init(user) {
|
||||
this._init(user.jobs[this.jobId].inputs);
|
||||
}
|
||||
|
||||
onclick(event) {
|
||||
let ressourceElement = event.target.closest('tr');
|
||||
if (ressourceElement === null) {return;}
|
||||
let jobInputId = ressourceElement.dataset.id;
|
||||
let actionButtonElement = event.target.closest('.action-button');
|
||||
let jobInputElement = event.target.closest('tr[data-id]');
|
||||
if (jobInputElement === null) {return;}
|
||||
let jobInputId = jobInputElement.dataset.id;
|
||||
let actionButtonElement = event.target.closest('.action-button[data-action]');
|
||||
if (actionButtonElement === null) {return;}
|
||||
let action = actionButtonElement.dataset.action;
|
||||
switch (action) {
|
||||
case 'download':
|
||||
window.location.href = this.user.data.jobs[this.jobId].inputs[jobInputId].download_url;
|
||||
window.location.href = `/jobs/${this.jobId}/inputs/${jobInputId}/download`;
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown action: "${action}"`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
usersPatchHandler(patch) {return;}
|
||||
|
||||
preprocessRessource(jobInput) {
|
||||
return {id: jobInput.id, filename: jobInput.filename};
|
||||
return {
|
||||
id: jobInput.id,
|
||||
creationDate: jobInput.creation_date,
|
||||
filename: jobInput.filename
|
||||
};
|
||||
}
|
||||
}
|
||||
JobInputList.options = {
|
||||
item: `<tr>
|
||||
<td><span class="filename"></span></td>
|
||||
<td class="right-align">
|
||||
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="download" data-position="top" data-tooltip="View"><i class="material-icons">file_download</i></a>
|
||||
</td>
|
||||
</tr>`,
|
||||
item: `
|
||||
<tr>
|
||||
<td><span class="filename"></span></td>
|
||||
<td class="right-align">
|
||||
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="download" data-position="top" data-tooltip="View"><i class="material-icons">file_download</i></a>
|
||||
</td>
|
||||
</tr>
|
||||
`.trim(),
|
||||
valueNames: [{data: ['id']}, 'filename']
|
||||
};
|
||||
|
@ -1,31 +1,32 @@
|
||||
class JobList extends RessourceList {
|
||||
constructor(listElement, options = {}) {
|
||||
super(listElement, {...JobList.options, ...options});
|
||||
this.user.eventListeners.job.addEventListener((eventType, payload) => this.eventHandler(eventType, payload));
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(this.user.data.jobs);
|
||||
init(user) {
|
||||
this._init(user.jobs);
|
||||
}
|
||||
|
||||
onclick(event) {
|
||||
let ressourceElement = event.target.closest('tr');
|
||||
if (ressourceElement === null) {return;}
|
||||
let jobId = ressourceElement.dataset.id;
|
||||
let actionButtonElement = event.target.closest('.action-button');
|
||||
let jobElement = event.target.closest('tr[data-id]');
|
||||
if (jobElement === null) {throw 'Could not locate job element';}
|
||||
let jobId = jobElement.dataset.id;
|
||||
let actionButtonElement = event.target.closest('.action-button[data-action]');
|
||||
let action = actionButtonElement === null ? 'view' : actionButtonElement.dataset.action;
|
||||
switch (action) {
|
||||
case 'delete':
|
||||
let deleteModalHTML = `<div class="modal">
|
||||
<div class="modal-content">
|
||||
<h4>Confirm job deletion</h4>
|
||||
<p>Do you really want to delete the job <b>${this.user.data.jobs[jobId].title}</b>? All files will be permanently deleted!</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
|
||||
<a class="btn modal-close red waves-effect waves-light" href="${this.user.data.jobs[jobId].url}/delete"><i class="material-icons left">delete</i>Delete</a>
|
||||
</div>
|
||||
</div>`;
|
||||
let deleteModalHTML = `
|
||||
<div class="modal">
|
||||
<div class="modal-content">
|
||||
<h4>Confirm job deletion</h4>
|
||||
<p>Do you really want to delete the job <b>${app.users[this.userId].jobs[jobId].title}</b>? All files will be permanently deleted!</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
|
||||
<a class="btn modal-close red waves-effect waves-light" href="/jobs/${jobId}/delete"><i class="material-icons left">delete</i>Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
`.trim();
|
||||
let deleteModalParentElement = document.querySelector('#modals');
|
||||
deleteModalParentElement.insertAdjacentHTML('beforeend', deleteModalHTML);
|
||||
let deleteModalElement = deleteModalParentElement.lastChild;
|
||||
@ -33,37 +34,34 @@ class JobList extends RessourceList {
|
||||
deleteModal.open();
|
||||
break;
|
||||
case 'view':
|
||||
if (jobId !== '-1') {window.location.href = this.user.data.jobs[jobId].url;}
|
||||
window.location.href = `/jobs/${jobId}`;
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown action: "${action}"`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
patch(patch) {
|
||||
let id, match, re, valueName;
|
||||
for (let operation of patch) {
|
||||
usersPatchHandler(patch) {
|
||||
let re = new RegExp(`^/users/${this.userId}/jobs/([A-Za-z0-9]*)`);
|
||||
let filteredPatch = patch.filter(operation => re.test(operation.path));
|
||||
for (let operation of filteredPatch) {
|
||||
switch(operation.op) {
|
||||
case 'add':
|
||||
// Matches the only paths that should be handled here: /jobs/{jobId}
|
||||
re = /^\/jobs\/(\d+)$/;
|
||||
re = new RegExp(`^/users/${this.userId}/jobs/([A-Za-z0-9]*)$`);
|
||||
if (re.test(operation.path)) {this.add(operation.value);}
|
||||
break;
|
||||
case 'remove':
|
||||
// See case add ;)
|
||||
re = /^\/jobs\/(\d+)$/;
|
||||
re = new RegExp(`^/users/${this.userId}/jobs/([A-Za-z0-9]*)$`);
|
||||
if (re.test(operation.path)) {
|
||||
[match, id] = operation.path.match(re);
|
||||
this.remove(id);
|
||||
let [match, jobId] = operation.path.match(re);
|
||||
this.remove(jobId);
|
||||
}
|
||||
break;
|
||||
case 'replace':
|
||||
// Matches the only paths that should be handled here: /jobs/{jobId}/{service || status || description || title}
|
||||
re = /^\/jobs\/(\d+)\/(service|status|description|title)$/;
|
||||
re = new RegExp(`^/users/${this.userId}/jobs/([A-Za-z0-9]*)/(service|status|description|title)$`);
|
||||
if (re.test(operation.path)) {
|
||||
[match, id, valueName] = operation.path.match(re);
|
||||
this.replace(id, valueName, operation.value);
|
||||
let [match, jobId, valueName] = operation.path.match(re);
|
||||
this.replace(jobId, valueName, operation.value);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@ -73,22 +71,27 @@ class JobList extends RessourceList {
|
||||
}
|
||||
|
||||
preprocessRessource(job) {
|
||||
return {id: job.id,
|
||||
service: job.service,
|
||||
status: job.status,
|
||||
description: job.description,
|
||||
title: job.title};
|
||||
return {
|
||||
id: job.id,
|
||||
creationDate: job.creation_date,
|
||||
description: job.description,
|
||||
service: job.service,
|
||||
status: job.status,
|
||||
title: job.title
|
||||
};
|
||||
}
|
||||
}
|
||||
JobList.options = {
|
||||
item: `<tr>
|
||||
<td><a class="btn-floating disabled"><i class="nopaque-icons service service-color darken service-icon"></i></a></td>
|
||||
<td><b class="title"></b><br><i class="description"></i></td>
|
||||
<td><span class="badge new status status-color status-text" data-badge-caption=""></span></td>
|
||||
<td class="right-align">
|
||||
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
|
||||
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
|
||||
</td>
|
||||
</tr>`,
|
||||
item: `
|
||||
<tr>
|
||||
<td><a class="btn-floating disabled"><i class="nopaque-icons service service-color darken service-icon"></i></a></td>
|
||||
<td><b class="title"></b><br><i class="description"></i></td>
|
||||
<td><span class="badge new status status-color status-text" data-badge-caption=""></span></td>
|
||||
<td class="right-align">
|
||||
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
|
||||
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
|
||||
</td>
|
||||
</tr>
|
||||
`.trim(),
|
||||
valueNames: [{data: ['id']}, {name: 'service', attr: 'data-service'}, {name: 'status', attr: 'data-status'}, 'description', 'title']
|
||||
};
|
||||
|
@ -2,37 +2,35 @@ class JobResultList extends RessourceList {
|
||||
constructor(listElement, options = {}) {
|
||||
super(listElement, {...JobResultList.options, ...options});
|
||||
this.jobId = listElement.dataset.jobId;
|
||||
this.user.eventListeners.job.addEventListener((eventType, payload) => this.eventHandler(eventType, payload), this.jobId);
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(this.user.data.jobs[this.jobId].results);
|
||||
init(user) {
|
||||
super._init(user.jobs[this.jobId].results);
|
||||
}
|
||||
|
||||
onclick(event) {
|
||||
let ressourceElement = event.target.closest('tr');
|
||||
if (ressourceElement === null) {return;}
|
||||
let jobResultId = ressourceElement.dataset.id;
|
||||
let actionButtonElement = event.target.closest('.action-button');
|
||||
let jobResultElement = event.target.closest('tr[data-id]');
|
||||
if (jobResultElement === null) {return;}
|
||||
let jobResultId = jobResultElement.dataset.id;
|
||||
let actionButtonElement = event.target.closest('.action-button[data-action]');
|
||||
if (actionButtonElement === null) {return;}
|
||||
let action = actionButtonElement.dataset.action;
|
||||
switch (action) {
|
||||
case 'download':
|
||||
window.location.href = this.user.data.jobs[this.jobId].results[jobResultId].download_url;
|
||||
window.location.href = `/jobs/${this.jobId}/results/${jobResultId}`;
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown action: "${action}"`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
patch(patch) {
|
||||
let re;
|
||||
for (let operation of patch) {
|
||||
usersPatchHandler(patch) {
|
||||
let re = new RegExp(`^/users/${this.userId}/jobs/${this.jobId}/results/([A-Za-z0-9]*)`);
|
||||
let filteredPatch = patch.filter(operation => re.test(operation.path));
|
||||
for (let operation of filteredPatch) {
|
||||
switch(operation.op) {
|
||||
case 'add':
|
||||
// Matches the only paths that should be handled here: /jobs/{this.user.data.jobs[this.jobId].id}/results/{jobResultId}
|
||||
re = new RegExp('^/jobs/' + this.user.data.jobs[this.jobId].id + '/results/(\\d+)$');
|
||||
re = new RegExp(`^/users/${this.userId}/jobs/${this.jobId}/results/([A-Za-z0-9]*)$`);
|
||||
if (re.test(operation.path)) {this.add(operation.value);}
|
||||
break;
|
||||
default:
|
||||
@ -56,16 +54,23 @@ class JobResultList extends RessourceList {
|
||||
} else {
|
||||
description = 'All result files created during this job';
|
||||
}
|
||||
return {id: jobResult.id, description: description, filename: jobResult.filename};
|
||||
return {
|
||||
id: jobResult.id,
|
||||
creationDate: jobResult.creation_date,
|
||||
description: description,
|
||||
filename: jobResult.filename
|
||||
};
|
||||
}
|
||||
}
|
||||
JobResultList.options = {
|
||||
item: `<tr>
|
||||
<td><span class="description"></span></td>
|
||||
<td><span class="filename"></span></td>
|
||||
<td class="right-align">
|
||||
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="download" data-position="top" data-tooltip="View"><i class="material-icons">file_download</i></a>
|
||||
</td>
|
||||
</tr>`,
|
||||
item: `
|
||||
<tr>
|
||||
<td><span class="description"></span></td>
|
||||
<td><span class="filename"></span></td>
|
||||
<td class="right-align">
|
||||
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="download" data-position="top" data-tooltip="View"><i class="material-icons">file_download</i></a>
|
||||
</td>
|
||||
</tr>
|
||||
`.trim(),
|
||||
valueNames: [{data: ['id']}, 'description', 'filename']
|
||||
};
|
||||
|
@ -1,31 +1,32 @@
|
||||
class QueryResultList extends RessourceList {
|
||||
constructor(listElement, options = {}) {
|
||||
super(listElement, {...QueryResultList.options, ...options});
|
||||
this.user.eventListeners.queryResult.addEventListener((eventType, payload) => this.eventHandler(eventType, payload));
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(this.user.data.query_results);
|
||||
init(user) {
|
||||
super.init(user.query_results);
|
||||
}
|
||||
|
||||
onclick(event) {
|
||||
let ressourceElement = event.target.closest('tr');
|
||||
if (ressourceElement === null) {return;}
|
||||
let queryResultId = ressourceElement.dataset.id;
|
||||
let actionButtonElement = event.target.closest('.action-button');
|
||||
let queryResultElement = event.target.closest('tr[data-id]');
|
||||
if (queryResultElement === null) {return;}
|
||||
let queryResultId = queryResultElement.dataset.id;
|
||||
let actionButtonElement = event.target.closest('.action-button[data-action]');
|
||||
let action = actionButtonElement === null ? 'view' : actionButtonElement.dataset.action;
|
||||
switch (action) {
|
||||
case 'delete':
|
||||
let deleteModalHTML = `<div class="modal">
|
||||
<div class="modal-content">
|
||||
<h4>Confirm query result deletion</h4>
|
||||
<p>Do you really want to delete the query result <b>${this.user.data.query_results[queryResultId].title}</b>? It will be permanently deleted!</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
|
||||
<a class="btn modal-close red waves-effect waves-light" href="${this.user.data.query_results[queryResultId].url}/delete"><i class="material-icons left">delete</i>Delete</a>
|
||||
</div>
|
||||
</div>`;
|
||||
let deleteModalHTML = `
|
||||
<div class="modal">
|
||||
<div class="modal-content">
|
||||
<h4>Confirm query result deletion</h4>
|
||||
<p>Do you really want to delete the query result <b>${app.users[this.userId].query_results[queryResultId].title}</b>? It will be permanently deleted!</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
|
||||
<a class="btn modal-close red waves-effect waves-light" href="/query_results/${queryResultId}/delete"><i class="material-icons left">delete</i>Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
`.trim();
|
||||
let deleteModalParentElement = document.querySelector('#modals');
|
||||
deleteModalParentElement.insertAdjacentHTML('beforeend', deleteModalHTML);
|
||||
let deleteModalElement = deleteModalParentElement.lastChild;
|
||||
@ -33,37 +34,34 @@ class QueryResultList extends RessourceList {
|
||||
deleteModal.open();
|
||||
break;
|
||||
case 'view':
|
||||
if (queryResultId !== '-1') {window.location.href = this.user.data.query_results[queryResultId].url;}
|
||||
window.location.href = `/query_results/${queryResultId}`;
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown action: "${action}"`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
patch(patch) {
|
||||
let id, match, re, valueName;
|
||||
for (let operation of patch) {
|
||||
usersPatchHandler(patch) {
|
||||
let re = new RegExp(`^/users/${this.userId}/query_results/([A-Za-z0-9]*)`);
|
||||
let filteredPatch = patch.filter(operation => re.test(operation.path));
|
||||
for (let operation of filteredPatch) {
|
||||
switch(operation.op) {
|
||||
case 'add':
|
||||
// Matches the only paths that should be handled here: /jobs/{jobId}
|
||||
re = /^\/query_results\/(\d+)$/;
|
||||
re = new RegExp(`^/users/${this.userId}/query_results/([A-Za-z0-9]*)$`);
|
||||
if (re.test(operation.path)) {this.add(operation.value);}
|
||||
break;
|
||||
case 'remove':
|
||||
// See case add ;)
|
||||
re = /^\/query_results\/(\d+)$/;
|
||||
re = new RegExp(`^/users/${this.userId}/query_results/([A-Za-z0-9]*)$`);
|
||||
if (re.test(operation.path)) {
|
||||
[match, id] = operation.path.match(re);
|
||||
this.remove(id);
|
||||
let [match, queryResultId] = operation.path.match(re);
|
||||
this.remove(queryResultId);
|
||||
}
|
||||
break;
|
||||
case 'replace':
|
||||
// Matches the only paths that should be handled here: /jobs/{jobId}/{service || status || description || title}
|
||||
re = /^\/query_results\/(\d+)\/(corpus_title|description|query|title)$/;
|
||||
re = new RegExp(`^/users/${this.userId}/query_results/([A-Za-z0-9]*)/(corpus_title|description|query|title)$`);
|
||||
if (re.test(operation.path)) {
|
||||
[match, id, valueName] = operation.path.match(re);
|
||||
this.replace(id, valueName, operation.value);
|
||||
let [match, queryResultId, valueName] = operation.path.match(re);
|
||||
this.replace(queryResultId, valueName, operation.value);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@ -73,21 +71,26 @@ class QueryResultList extends RessourceList {
|
||||
}
|
||||
|
||||
preprocessRessource(queryResult) {
|
||||
return {id: queryResult.id,
|
||||
corpus_title: queryResult.corpus_title,
|
||||
description: queryResult.description,
|
||||
query: queryResult.query,
|
||||
title: queryResult.title};
|
||||
return {
|
||||
id: queryResult.id,
|
||||
corpus_title: queryResult.corpus_title,
|
||||
creationDate: queryResult.creation_date,
|
||||
description: queryResult.description,
|
||||
query: queryResult.query,
|
||||
title: queryResult.title
|
||||
};
|
||||
}
|
||||
}
|
||||
QueryResultList.options = {
|
||||
item: `<tr>
|
||||
<td><b class="title"></b><br><i class="description"></i><br></td>
|
||||
<td><span class="corpus_title"></span><br><span class="query"></span></td>
|
||||
<td class="right-align">
|
||||
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
|
||||
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
|
||||
</td>
|
||||
</tr>`,
|
||||
item: `
|
||||
<tr>
|
||||
<td><b class="title"></b><br><i class="description"></i><br></td>
|
||||
<td><span class="corpus_title"></span><br><span class="query"></span></td>
|
||||
<td class="right-align">
|
||||
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
|
||||
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
|
||||
</td>
|
||||
</tr>
|
||||
`.trim(),
|
||||
valueNames: [{data: ['id']}, 'corpus_title', 'description', 'query', 'title']
|
||||
};
|
||||
|
@ -4,87 +4,68 @@ class RessourceList {
|
||||
* a base class for concrete ressource list implementations.
|
||||
*/
|
||||
constructor(listElement, options = {}) {
|
||||
if (listElement.dataset.userId) {
|
||||
if (listElement.dataset.userId in nopaque.appClient.users) {
|
||||
this.user = nopaque.appClient.users[listElement.dataset.userId];
|
||||
} else {
|
||||
console.error(`User not found: ${listElement.dataset.userId}`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this.user = nopaque.appClient.users.self;
|
||||
}
|
||||
this.list = new List(listElement, {...RessourceList.options, ...options});
|
||||
this.list.list.innerHTML = `<tr>
|
||||
<td class="row" colspan="100%">
|
||||
<div class="col s12"> </div>
|
||||
<div class="col s3 m2 xl1">
|
||||
<div class="preloader-wrapper active">
|
||||
<div class="spinner-layer spinner-green-only">
|
||||
<div class="circle-clipper left">
|
||||
<div class="circle"></div>
|
||||
</div>
|
||||
<div class="gap-patch">
|
||||
<div class="circle"></div>
|
||||
</div>
|
||||
<div class="circle-clipper right">
|
||||
<div class="circle"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col s9 m6 xl5">
|
||||
<span class="card-title">Waiting for data...</span>
|
||||
<p>This list is not initialized yet.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
this.list.list.innerHTML = `
|
||||
<tr>
|
||||
<td class="row" colspan="100%">
|
||||
<div class="col s12"> </div>
|
||||
<div class="col s3 m2 xl1">
|
||||
<div class="preloader-wrapper active">
|
||||
<div class="spinner-layer spinner-green-only">
|
||||
<div class="circle-clipper left">
|
||||
<div class="circle"></div>
|
||||
</div>
|
||||
<div class="gap-patch">
|
||||
<div class="circle"></div>
|
||||
</div>
|
||||
<div class="circle-clipper right">
|
||||
<div class="circle"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col s9 m6 xl5">
|
||||
<span class="card-title">Waiting for data...</span>
|
||||
<p>This list is not initialized yet.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`.trim();
|
||||
this.list.list.style.cursor = 'pointer';
|
||||
this.userId = listElement.dataset.userId;
|
||||
if (typeof this.onclick === 'function') {this.list.list.addEventListener('click', event => this.onclick(event));}
|
||||
}
|
||||
|
||||
eventHandler(eventType, payload) {
|
||||
switch (eventType) {
|
||||
case 'init':
|
||||
this.init();
|
||||
break;
|
||||
case 'patch':
|
||||
this.patch(payload);
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown event type: ${eventType}`);
|
||||
break;
|
||||
if (this.userId) {
|
||||
app.addEventListener('users.patch', patch => this.usersPatchHandler(patch));
|
||||
app.getUserById(this.userId).then(
|
||||
user => this.init(user),
|
||||
error => {throw JSON.stringify(error);}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
init(ressources) {
|
||||
_init(ressources) {
|
||||
this.list.clear();
|
||||
this.add(Object.values(ressources));
|
||||
this.list.sort('id', {order: 'desc'});
|
||||
let emptyListElementHTML = `<tr class="show-if-only-child" data-id="-1">
|
||||
<td colspan="100%">
|
||||
<span class="card-title"><i class="left material-icons" style="font-size: inherit;">file_download</i>Nothing here...</span>
|
||||
<p>No ressource available.</p>
|
||||
</td>
|
||||
</tr>`;
|
||||
let emptyListElementHTML = `
|
||||
<tr class="show-if-only-child">
|
||||
<td colspan="100%">
|
||||
<span class="card-title"><i class="left material-icons" style="font-size: inherit;">file_download</i>Nothing here...</span>
|
||||
<p>No ressource available.</p>
|
||||
</td>
|
||||
</tr>
|
||||
`.trim();
|
||||
this.list.list.insertAdjacentHTML('afterbegin', emptyListElementHTML);
|
||||
}
|
||||
|
||||
patch(patch) {
|
||||
/*
|
||||
* It's not possible to generalize a patch Handler for all type of
|
||||
* ressources. So this method is meant to be an interface.
|
||||
*/
|
||||
console.error('patch method not implemented!');
|
||||
}
|
||||
init(user) {throw 'Not implemented';}
|
||||
|
||||
usersPatchHandler(patch) {throw 'Not implemented';}
|
||||
|
||||
preprocessRessource() {throw 'Not implemented'}
|
||||
|
||||
add(values) {
|
||||
let ressources = Array.isArray(values) ? values : [values];
|
||||
if (typeof this.preprocessRessource === 'function') {
|
||||
ressources = ressources.map(ressource => this.preprocessRessource(ressource));
|
||||
}
|
||||
// Set a callback function ('() => {return;}') to force List.js perform the
|
||||
// add method asynchronous: https://listjs.com/api/#add
|
||||
ressources = ressources.map(ressource => this.preprocessRessource(ressource));
|
||||
this.list.add(ressources, () => {
|
||||
this.list.sort('id', {order: 'desc'});
|
||||
});
|
||||
|
@ -1,32 +1,32 @@
|
||||
class UserList extends RessourceList {
|
||||
constructor(listElement, options = {}) {
|
||||
super(listElement, {...UserList.options, ...options});
|
||||
users = undefined;
|
||||
}
|
||||
|
||||
init(users) {
|
||||
this.users = users;
|
||||
super.init(users);
|
||||
super._init(Object.values(users));
|
||||
}
|
||||
|
||||
onclick(event) {
|
||||
let ressourceElement = event.target.closest('tr');
|
||||
if (ressourceElement === null) {return;}
|
||||
let userId = ressourceElement.dataset.id;
|
||||
let actionButtonElement = event.target.closest('.action-button');
|
||||
let userElement = event.target.closest('tr[data-id]');
|
||||
if (userElement === null) {return;}
|
||||
let userId = userElement.dataset.id;
|
||||
let actionButtonElement = event.target.closest('.action-button[data-action]');
|
||||
let action = (actionButtonElement === null) ? 'view' : actionButtonElement.dataset.action;
|
||||
switch (action) {
|
||||
case 'delete':
|
||||
let deleteModalHTML = `<div class="modal">
|
||||
<div class="modal-content">
|
||||
<h4>Confirm user deletion</h4>
|
||||
<p>Do you really want to delete the corpus <b>${this.users[userId].username}</b>? All files will be permanently deleted!</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
|
||||
<a class="btn modal-close red waves-effect waves-light" href="/admin/users/${userId}/delete"><i class="material-icons left">delete</i>Delete</a>
|
||||
</div>
|
||||
</div>`;
|
||||
let deleteModalHTML = `
|
||||
<div class="modal">
|
||||
<div class="modal-content">
|
||||
<h4>Confirm user deletion</h4>
|
||||
<p>Do you really want to delete user <b>${userId}</b>? All files will be permanently deleted!</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
|
||||
<a class="btn modal-close red waves-effect waves-light" href="/admin/users/${userId}/delete"><i class="material-icons left">delete</i>Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
`.trim();
|
||||
let deleteModalParentElement = document.querySelector('#modals');
|
||||
deleteModalParentElement.insertAdjacentHTML('beforeend', deleteModalHTML);
|
||||
let deleteModalElement = deleteModalParentElement.lastChild;
|
||||
@ -37,35 +37,38 @@ class UserList extends RessourceList {
|
||||
window.location.href = `/admin/users/${userId}/edit`;
|
||||
break;
|
||||
case 'view':
|
||||
if (userId !== '-1') {window.location.href = `/admin/users/${userId}`;}
|
||||
window.location.href = `/admin/users/${userId}`;
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown action: ${action}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
preprocessRessource(user) {
|
||||
return {id: user.id,
|
||||
id_: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
last_seen: new Date(user.last_seen).toLocaleString("en-US"),
|
||||
role: user.role.name};
|
||||
return {
|
||||
id: user.id,
|
||||
id_: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
last_seen: user.last_seen.toLocaleString("en-US"),
|
||||
role: user.role.name
|
||||
};
|
||||
}
|
||||
}
|
||||
UserList.options = {
|
||||
item: `<tr>
|
||||
<td><span class="id_"></span></td>
|
||||
<td><span class="username"></span></td>
|
||||
<td><span class="email"></span></td>
|
||||
<td><span class="last_seen"></span></td>
|
||||
<td><span class="role"></span></td>
|
||||
<td class="right-align">
|
||||
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
|
||||
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="edit" data-position="top" data-tooltip="Edit"><i class="material-icons">edit</i></a>
|
||||
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
|
||||
</td>
|
||||
</tr>`,
|
||||
item: `
|
||||
<tr>
|
||||
<td><span class="id_"></span></td>
|
||||
<td><span class="username"></span></td>
|
||||
<td><span class="email"></span></td>
|
||||
<td><span class="last_seen"></span></td>
|
||||
<td><span class="role"></span></td>
|
||||
<td class="right-align">
|
||||
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
|
||||
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="edit" data-position="top" data-tooltip="Edit"><i class="material-icons">edit</i></a>
|
||||
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
|
||||
</td>
|
||||
</tr>
|
||||
`.trim(),
|
||||
valueNames: [{data: ['id']}, 'id_', 'username', 'email', 'last_seen', 'role']
|
||||
};
|
||||
|
@ -1,177 +1,3 @@
|
||||
class AppClient {
|
||||
constructor(currentUserId) {
|
||||
if (currentUserId) {
|
||||
this.socket = io({transports: ['websocket'], upgrade: false});
|
||||
this.users = {};
|
||||
this.users.self = this.loadUser(currentUserId);
|
||||
this.users.self.eventListeners.job.addEventListener((eventType, payload) => this.jobEventHandler(eventType, payload));
|
||||
}
|
||||
}
|
||||
|
||||
flash(message, category) {
|
||||
let toast;
|
||||
let toastCloseActionElement;
|
||||
|
||||
switch (category) {
|
||||
case "corpus":
|
||||
message = `<i class="left material-icons">book</i>${message}`;
|
||||
break;
|
||||
case "error":
|
||||
message = `<i class="left material-icons error-color-text">error</i>${message}`;
|
||||
break;
|
||||
case "job":
|
||||
message = `<i class="left nopaque-icons">J</i>${message}`;
|
||||
break;
|
||||
default:
|
||||
message = `<i class="left material-icons">notifications</i>${message}`;
|
||||
}
|
||||
|
||||
toast = M.toast({html: `<span>${message}</span>
|
||||
<button data-action="close" class="btn-flat toast-action white-text">
|
||||
<i class="material-icons">close</i>
|
||||
</button>`});
|
||||
toastCloseActionElement = toast.el.querySelector('.toast-action[data-action="close"]');
|
||||
toastCloseActionElement.addEventListener('click', () => {toast.dismiss();});
|
||||
}
|
||||
|
||||
jobEventHandler(eventType, payload) {
|
||||
switch (eventType) {
|
||||
case 'init':
|
||||
break;
|
||||
case 'patch':
|
||||
this.jobPatch(payload);
|
||||
break;
|
||||
default:
|
||||
console.error(`[AppClient.jobEventHandler] Unknown event type: ${eventType}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
loadUser(userId) {
|
||||
if (userId in this.users) {return this.users[userId];}
|
||||
let user = new User();
|
||||
this.users[userId] = user;
|
||||
this.socket.on(`user_${userId}_init`, msg => user.init(msg));
|
||||
this.socket.on(`user_${userId}_patch`, msg => user.patch(msg));
|
||||
this.socket.emit('start_user_session', userId);
|
||||
return user;
|
||||
}
|
||||
|
||||
jobPatch(patch) {
|
||||
if (this.users.self.data.settings.job_status_site_notifications === 'none') {return;}
|
||||
let jobStatusPatches = patch.filter(operation => operation.op === 'replace' && /^\/jobs\/(\d+)\/status$/.test(operation.path));
|
||||
for (let operation of jobStatusPatches) {
|
||||
let [match, jobId] = operation.path.match(/^\/jobs\/(\d+)\/status$/);
|
||||
if (this.users.self.data.settings.job_status_site_notifications === "end" && !['complete', 'failed'].includes(operation.value)) {continue;}
|
||||
this.flash(`[<a href="/jobs/${jobId}">${this.users.self.data.jobs[jobId].title}</a>] New status: ${operation.value}`, 'job');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class User {
|
||||
constructor() {
|
||||
this.data = undefined;
|
||||
this.eventListeners = {
|
||||
corpus: {
|
||||
addEventListener(listener, corpusId='*') {
|
||||
if (corpusId in this) {this[corpusId].push(listener);} else {this[corpusId] = [listener];}
|
||||
}
|
||||
},
|
||||
job: {
|
||||
addEventListener(listener, jobId='*') {
|
||||
if (jobId in this) {this[jobId].push(listener);} else {this[jobId] = [listener];}
|
||||
}
|
||||
},
|
||||
queryResult: {
|
||||
addEventListener(listener, queryResultId='*') {
|
||||
if (queryResultId in this) {this[queryResultId].push(listener);} else {this[queryResultId] = [listener];}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
init(data) {
|
||||
this.data = data;
|
||||
|
||||
for (let [corpusId, eventListeners] of Object.entries(this.eventListeners.corpus)) {
|
||||
if (corpusId === '*') {
|
||||
for (let eventListener of eventListeners) {eventListener('init');}
|
||||
} else {
|
||||
if (corpusId in this.data.corpora) {
|
||||
for (let eventListener of eventListeners) {eventListener('init');}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let [jobId, eventListeners] of Object.entries(this.eventListeners.job)) {
|
||||
if (jobId === '*') {
|
||||
for (let eventListener of eventListeners) {eventListener('init');}
|
||||
} else {
|
||||
if (jobId in this.data.jobs) {
|
||||
for (let eventListener of eventListeners) {eventListener('init');}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let [queryResultId, eventListeners] of Object.entries(this.eventListeners.queryResult)) {
|
||||
if (queryResultId === '*') {
|
||||
for (let eventListener of eventListeners) {eventListener('init');}
|
||||
} else {
|
||||
if (queryResultId in this.data.query_results) {
|
||||
for (let eventListener of eventListeners) {eventListener('init');}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
patch(patch) {
|
||||
this.data = jsonpatch.apply_patch(this.data, patch);
|
||||
|
||||
let corporaPatch = patch.filter(operation => operation.path.startsWith("/corpora"));
|
||||
if (corporaPatch.length > 0) {
|
||||
for (let [corpusId, eventListeners] of Object.entries(this.eventListeners.corpus)) {
|
||||
if (corpusId === '*') {
|
||||
for (let eventListener of eventListeners) {eventListener('patch', corporaPatch);}
|
||||
} else {
|
||||
let corpusPatch = corporaPatch.filter(operation => operation.path.startsWith(`/corpora/${corpusId}`));
|
||||
if (corpusPatch.length > 0) {
|
||||
for (let eventListener of eventListeners) {eventListener('patch', corpusPatch);}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let jobsPatch = patch.filter(operation => operation.path.startsWith("/jobs"));
|
||||
if (jobsPatch.length > 0) {
|
||||
for (let [jobId, eventListeners] of Object.entries(this.eventListeners.job)) {
|
||||
if (jobId === '*') {
|
||||
for (let eventListener of eventListeners) {eventListener('patch', jobsPatch);}
|
||||
} else {
|
||||
let jobPatch = jobsPatch.filter(operation => operation.path.startsWith(`/jobs/${jobId}`));
|
||||
if (jobPatch.length > 0) {
|
||||
for (let eventListener of eventListeners) {eventListener('patch', jobPatch);}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let queryResultsPatch = patch.filter(operation => operation.path.startsWith("/query_results"));
|
||||
if (queryResultsPatch.length > 0) {
|
||||
for (let [queryResultId, eventListeners] of Object.entries(this.eventListeners.queryResult)) {
|
||||
if (queryResultId === '*') {
|
||||
for (let eventListener of eventListeners) {eventListener('patch', queryResultsPatch);}
|
||||
} else {
|
||||
let queryResultPatch = queryResultsPatch.filter(operation => operation.path.startsWith(`/query_results/${queryResultId}`));
|
||||
if (queryResultPatch.length > 0) {
|
||||
for (let eventListener of eventListeners) {eventListener('patch', queryResultPatch);}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* The nopaque object is used as a namespace for nopaque specific functions and
|
||||
* variables.
|
||||
|
@ -7,6 +7,8 @@
|
||||
<script src="{{ url_for('static', filename='js/jsonpatch.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/list.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/socket.io.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/nopaque/App.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/nopaque/JobStatusNotifier.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/nopaque/main.js') }}"></script>
|
||||
{% assets filters='rjsmin', output="js/nopaque/RessourceDisplays.min.bundle.js",
|
||||
"js/nopaque/RessourceDisplays/RessourceDisplay.js",
|
||||
@ -14,7 +16,7 @@
|
||||
"js/nopaque/RessourceDisplays/JobDisplay.js" %}
|
||||
<script src="{{ ASSET_URL }}"></script>
|
||||
{% endassets %}
|
||||
{% assets filters='rjsmin', output="js/nopaque/RessourceLists.min.bundle.js",
|
||||
{% assets output="js/nopaque/RessourceLists.min.bundle.js",
|
||||
"js/nopaque/RessourceLists/RessourceList.js",
|
||||
"js/nopaque/RessourceLists/CorpusList.js",
|
||||
"js/nopaque/RessourceLists/CorpusFileList.js",
|
||||
@ -31,7 +33,13 @@
|
||||
M.AutoInit();
|
||||
M.CharacterCounter.init(document.querySelectorAll('input[data-length][type="email"], input[data-length][type="password"], input[data-length][type="text"], textarea[data-length]'));
|
||||
M.Dropdown.init(document.querySelectorAll('#nav-more-dropdown-trigger'), {alignment: 'right', constrainWidth: false, coverTrigger: false});
|
||||
nopaque.appClient = new AppClient({% if current_user.is_authenticated %}{{ current_user.id }}{% endif %});
|
||||
var app = new App();
|
||||
{% if current_user.is_authenticated %}
|
||||
var currentUserId = '{{ current_user.hashid }}';
|
||||
let jobStatusNotifier = new JobStatusNotifier(currentUserId);
|
||||
app.addEventListener('users.patch', patch => jobStatusNotifier.usersPatchHandler(patch));
|
||||
app.getUserById(currentUserId).then(user => {}, error => {throw JSON.stringify(error)});
|
||||
{% endif %}
|
||||
nopaque.Forms.init();
|
||||
for (let flashedMessage of {{ get_flashed_messages(with_categories=True)|tojson }}) {nopaque.appClient.flash(flashedMessage[1], flashedMessage[0]);}
|
||||
for (let flashedMessage of {{ get_flashed_messages(with_categories=True)|tojson }}) {app.flash(flashedMessage[1], flashedMessage[0]);}
|
||||
</script>
|
||||
|
@ -12,7 +12,7 @@
|
||||
<div class="col s12 m4">
|
||||
<h2>{{ user.username }}</h2>
|
||||
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren,</p>
|
||||
<a class="waves-effect waves-light btn" href="{{ url_for('.user', user_id=user.id) }}"><i class="material-icons left">arrow_back</i>Back to user administration</a>
|
||||
<a class="waves-effect waves-light btn" href="{{ url_for('.user', user_id=user.hashid) }}"><i class="material-icons left">arrow_back</i>Back to user administration</a>
|
||||
</div>
|
||||
|
||||
<div class="col s12 m8">
|
||||
|
@ -21,11 +21,11 @@
|
||||
<ul>
|
||||
<li>Username: {{ user.username }}</li>
|
||||
<li>Email: {{ user.email }}</li>
|
||||
<li>ID: {{ user.id }}</li>
|
||||
<li>Member since: {{ user.member_since.strftime('%m/%d/%Y, %H:%M:%S %p') }}</li>
|
||||
<li>Id: {{ user.id }}</li>
|
||||
<li>Hashid: {{ user.hashid }}</li>
|
||||
<li>Member since: {{ user.member_since }}</li>
|
||||
<li>Confirmed status: {{ user.confirmed }}</li>
|
||||
<li>Last seen: {{ user.last_seen.strftime('%m/%d/%Y, %H:%M:%S %p') }}</li>
|
||||
<li>Role ID: {{ user.role_id }}</li>
|
||||
<li>Last seen: {{ user.last_seen }}</li>
|
||||
<li>Permissions as Int: {{ user.role.permissions }}</li>
|
||||
<li>Role name: {{ user.role.name }}</li>
|
||||
</ul>
|
||||
@ -37,7 +37,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col s12 l6" id="corpora" data-user-id="{{ user.id }}">
|
||||
<div class="col s12 l6" id="corpora" data-user-id="{{ user.hashid }}">
|
||||
<h3>Corpora</h3>
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
@ -65,7 +65,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col s12 l6" id="jobs" data-user-id="{{ user.id }}">
|
||||
<div class="col s12 l6" id="jobs" data-user-id="{{ user.hashid }}">
|
||||
<h3>Jobs</h3>
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
@ -111,7 +111,6 @@
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
nopaque.appClient.loadUser({{ user.id }});
|
||||
let corpusList = new CorpusList(document.querySelector('#corpora'));
|
||||
let jobList = new JobList(document.querySelector('#jobs'));
|
||||
</script>
|
||||
|
@ -41,6 +41,6 @@
|
||||
{{ super() }}
|
||||
<script>
|
||||
let userList = new UserList(document.querySelector('#users'), {page: 10});
|
||||
userList.init({{ users|tojson }});
|
||||
userList.init({{ dict_users|tojson }});
|
||||
</script>
|
||||
{% endblock scripts %}
|
||||
|
@ -6,7 +6,7 @@
|
||||
{% block page_content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col s12" data-corpus-id="{{ corpus.id }}" data-user-id="{{ corpus.creator.id }}" id="corpus-display">
|
||||
<div class="col s12" data-corpus-id="{{ corpus.hashid }}" data-user-id="{{ corpus.user.hashid }}" id="corpus-display">
|
||||
<div class="row">
|
||||
<div class="col s8 m9 l10">
|
||||
<h1 id="title"><span class="corpus-title"></span></h1>
|
||||
@ -83,7 +83,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col s12" id="corpus-files" data-corpus-id="{{ corpus.id }}" data-user-id="{{ corpus.creator.id }}">
|
||||
<div class="col s12" id="corpus-files" data-corpus-id="{{ corpus.hashid }}" data-user-id="{{ corpus.user.hashid }}">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<span class="card-title" id="files">Corpus files</span>
|
||||
@ -118,7 +118,6 @@
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
nopaque.appClient.loadUser({{ corpus.creator.id }});
|
||||
let corpusDisplay = new CorpusDisplay(document.querySelector('#corpus-display'));
|
||||
let corpusFileList = new CorpusFileList(document.querySelector('#corpus-files'));
|
||||
</script>
|
||||
|
@ -6,7 +6,7 @@
|
||||
{% block page_content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col s12" data-job-id="{{ job.id }}" data-user-id="{{ job.creator.id }}" id="job-display">
|
||||
<div class="col s12" data-job-id="{{ job.hashid }}" data-user-id="{{ job.user.hashid }}" id="job-display">
|
||||
<div class="row">
|
||||
<div class="col s8 m9 l10">
|
||||
<h1 id="title"><i style="font-size: inherit;" class="nopaque-icons service-icon" data-service="{{ job.service }}"></i> <span class="job-title"></span></h1>
|
||||
@ -111,7 +111,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col s12" id="job-inputs" data-job-id="{{ job.id }}" data-user-id="{{ job.creator.id }}">
|
||||
<div class="col s12" id="job-inputs" data-job-id="{{ job.hashid }}" data-user-id="{{ job.user.hashid }}">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div class="row">
|
||||
@ -136,7 +136,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col s12" id="job-results" data-job-id="{{ job.id }}" data-user-id="{{ job.creator.id }}">
|
||||
<div class="col s12" id="job-results" data-job-id="{{ job.hashid }}" data-user-id="{{ job.user.hashid }}">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div class="row">
|
||||
@ -168,7 +168,6 @@
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
nopaque.appClient.loadUser({{ job.creator.id }});
|
||||
let jobDisplay = new JobDisplay(document.querySelector('#job-display'));
|
||||
let jobInputList = new JobInputList(document.querySelector('#job-inputs'));
|
||||
let jobResultList = new JobResultList(document.querySelector('#job-results'));
|
||||
|
@ -18,7 +18,7 @@
|
||||
<li class="tab col s6"><a href="#query-results">Query results</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col s12" id="corpora">
|
||||
<div class="col s12" data-user-id="{{ current_user.hashid }}" id="corpora">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div class="input-field">
|
||||
@ -89,7 +89,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col s12" id="jobs">
|
||||
<div class="col s12" data-user-id="{{ current_user.hashid }}" id="jobs">
|
||||
<h3>My Jobs</h3>
|
||||
<p>A job is the execution of a service provided by nopaque. You can create any number of jobs and let them be processed simultaneously.</p>
|
||||
<div class="card">
|
||||
@ -177,6 +177,6 @@
|
||||
<script>
|
||||
let corpusList = new CorpusList(document.querySelector('#corpora'));
|
||||
let jobList = new JobList(document.querySelector('#jobs'));
|
||||
let queryResultList = new QueryResultList(document.querySelector('#query-results'));
|
||||
//let queryResultList = new QueryResultList(document.querySelector('#query-results'));
|
||||
</script>
|
||||
{% endblock scripts %}
|
||||
|
@ -1,4 +1,4 @@
|
||||
<p>Dear <b>{{ job.creator.username }}</b>,</p>
|
||||
<p>Dear <b>{{ job.user.username }}</b>,</p>
|
||||
|
||||
<p>The status of your Job "<b>{{ job.title }}</b>" has changed!</p>
|
||||
<p>It is now <b>{{ job.status }}</b>!</p>
|
||||
|
@ -1,4 +1,4 @@
|
||||
Dear {{ job.creator.username }},
|
||||
Dear {{ job.user.username }},
|
||||
|
||||
The status of your Job "{{ job.title }}" has changed!
|
||||
It is now {{ job.status }}!
|
||||
|
10
app/utils.py
Normal file
10
app/utils.py
Normal file
@ -0,0 +1,10 @@
|
||||
from app import hashids
|
||||
from werkzeug.routing import BaseConverter
|
||||
|
||||
|
||||
class HashidConverter(BaseConverter):
|
||||
def to_python(self, value):
|
||||
return hashids.decode(value)[0]
|
||||
|
||||
def to_url(self, value):
|
||||
return hashids.encode(value)
|
@ -45,7 +45,7 @@ class Config:
|
||||
NOPAQUE_ADMIN = os.environ.get('NOPAQUE_ADMIN')
|
||||
NOPAQUE_DAEMON_ENABLED = \
|
||||
os.environ.get('NOPAQUE_DAEMON_ENABLED', 'true').lower() == 'true'
|
||||
NOPAQUE_DATA_DIR = os.environ.get('NOPAQUE_DATA_DIR', '/mnt/nopaque')
|
||||
NOPAQUE_DATA_DIR = os.path.abspath(os.environ.get('NOPAQUE_DATA_DIR'))
|
||||
NOPAQUE_DOCKER_REGISTRY = 'gitlab.ub.uni-bielefeld.de:4567'
|
||||
NOPAQUE_DOCKER_IMAGE_PREFIX = f'{NOPAQUE_DOCKER_REGISTRY}/sfb1288inf/'
|
||||
NOPAQUE_DOCKER_REGISTRY_USERNAME = \
|
||||
|
50
migrations/versions/68ed092ffe5e_.py
Normal file
50
migrations/versions/68ed092ffe5e_.py
Normal file
@ -0,0 +1,50 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 68ed092ffe5e
|
||||
Revises: be010d5d708d
|
||||
Create Date: 2021-11-24 15:33:16.258600
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '68ed092ffe5e'
|
||||
down_revision = 'be010d5d708d'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('corpus_files', sa.Column('creation_date', sa.DateTime(), nullable=True))
|
||||
op.add_column('corpus_files', sa.Column('last_edited_date', sa.DateTime(), nullable=True))
|
||||
op.add_column('corpus_files', sa.Column('mimetype', sa.String(length=255), nullable=True))
|
||||
op.add_column('job_inputs', sa.Column('creation_date', sa.DateTime(), nullable=True))
|
||||
op.add_column('job_inputs', sa.Column('last_edited_date', sa.DateTime(), nullable=True))
|
||||
op.add_column('job_inputs', sa.Column('mimetype', sa.String(length=255), nullable=True))
|
||||
op.add_column('job_results', sa.Column('creation_date', sa.DateTime(), nullable=True))
|
||||
op.add_column('job_results', sa.Column('last_edited_date', sa.DateTime(), nullable=True))
|
||||
op.add_column('job_results', sa.Column('mimetype', sa.String(length=255), nullable=True))
|
||||
op.add_column('query_results', sa.Column('creation_date', sa.DateTime(), nullable=True))
|
||||
op.add_column('query_results', sa.Column('last_edited_date', sa.DateTime(), nullable=True))
|
||||
op.add_column('query_results', sa.Column('mimetype', sa.String(length=255), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('query_results', 'mimetype')
|
||||
op.drop_column('query_results', 'last_edited_date')
|
||||
op.drop_column('query_results', 'creation_date')
|
||||
op.drop_column('job_results', 'mimetype')
|
||||
op.drop_column('job_results', 'last_edited_date')
|
||||
op.drop_column('job_results', 'creation_date')
|
||||
op.drop_column('job_inputs', 'mimetype')
|
||||
op.drop_column('job_inputs', 'last_edited_date')
|
||||
op.drop_column('job_inputs', 'creation_date')
|
||||
op.drop_column('corpus_files', 'mimetype')
|
||||
op.drop_column('corpus_files', 'last_edited_date')
|
||||
op.drop_column('corpus_files', 'creation_date')
|
||||
# ### end Alembic commands ###
|
@ -13,6 +13,7 @@ Flask-SocketIO~=5.1
|
||||
Flask-SQLAlchemy
|
||||
Flask-WTF
|
||||
gunicorn
|
||||
hashids
|
||||
hiredis
|
||||
jsonpatch
|
||||
jsonschema
|
||||
|
Loading…
Reference in New Issue
Block a user