Only reveal hashids to the ui

This commit is contained in:
Patrick Jentsch 2021-11-30 16:22:16 +01:00
parent 3e227dc4cf
commit 72ba61f369
39 changed files with 1098 additions and 1083 deletions

View File

@ -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

View File

@ -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):

View File

@ -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'''

View File

@ -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']:

View File

@ -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,

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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,

File diff suppressed because it is too large Load Diff

View File

@ -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'

View 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);}
}
}

View 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');
}
}
}

View File

@ -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"));
}
}
}

View File

@ -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) {

View File

@ -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) {

View File

@ -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']
};

View File

@ -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']
};

View File

@ -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']
};

View File

@ -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']
};

View File

@ -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']
};

View File

@ -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']
};

View File

@ -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">&nbsp;</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">&nbsp;</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'});
});

View File

@ -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']
};

View File

@ -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.

View File

@ -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>

View File

@ -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">

View File

@ -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>

View File

@ -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 %}

View File

@ -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>

View File

@ -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'));

View File

@ -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 %}

View File

@ -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>

View File

@ -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
View 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)

View File

@ -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 = \

View 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 ###

View File

@ -13,6 +13,7 @@ Flask-SocketIO~=5.1
Flask-SQLAlchemy
Flask-WTF
gunicorn
hashids
hiredis
jsonpatch
jsonschema