20 Commits

Author SHA1 Message Date
713a7645db Bump nopaque version 2024-12-05 15:34:11 +01:00
0c64c07925 Update corpus analysis loading modal 2024-12-05 15:33:15 +01:00
a6ddf4c980 Remove import corpus button 2024-12-05 15:12:53 +01:00
cab5f7ea05 More js enhancements 2024-12-05 15:07:13 +01:00
07f09cdbd9 fix cqi_over_socketio 2024-12-05 15:07:03 +01:00
c97b2a886e Further js refactoring 2024-12-05 14:26:05 +01:00
df2bffe0fd implement first version of jobs socketio namespace 2024-12-03 16:09:14 +01:00
aafb3ca3ec Update javascript app structure 2024-12-03 15:59:08 +01:00
12a3ac1d5d Update JS code structure 2024-12-02 09:34:17 +01:00
a2904caea2 Update cqpserver image version 2024-11-28 10:02:27 +01:00
e325552100 Update corpus analysis tabs to look the same as before base template update 2024-11-28 10:02:00 +01:00
e269156925 fix socketio emits from database event listeners 2024-11-27 15:46:54 +01:00
9c9de242ca Remove unsed css 2024-11-27 11:35:51 +01:00
ec54fdc3bb Restore service scheme on pages 2024-11-27 11:34:21 +01:00
2263a8d27d codestyle enhancements in base template 2024-11-21 11:22:57 +01:00
143cdd91f9 update workspace settings 2024-11-21 11:22:46 +01:00
b5f7478e14 Update templates 2024-11-21 11:12:11 +01:00
a95b8d979d Fix forms 2024-11-20 15:56:48 +01:00
18d5ab160e Optimize jinja wtf macros 2024-11-20 15:56:29 +01:00
7439edacef Add background color to job list entries 2024-11-20 15:55:59 +01:00
64 changed files with 1518 additions and 1042 deletions

16
.vscode/settings.json vendored
View File

@ -1,10 +1,24 @@
{
"editor.rulers": [79],
"editor.tabSize": 4,
"emmet.includeLanguages": {
"jinja-html": "html"
},
"files.associations": {
".flaskenv": "env",
"*.env.tpl": "env",
"*.txt.j2": "jinja"
},
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true,
"files.trimTrailingWhitespace": true,
"[html]": {
"editor.tabSize": 2
},
"[javascript]": {
"editor.tabSize": 2,
"editor.tabSize": 2
},
"[jinja-html]": {
"editor.tabSize": 2
}
}

View File

@ -132,6 +132,9 @@ def create_app(config: Config = Config) -> Flask:
# region SocketIO Namespaces
from .namespaces.cqi_over_sio import CQiOverSocketIONamespace
socketio.on_namespace(CQiOverSocketIONamespace('/cqi_over_sio'))
from .namespaces.users import UsersNamespace
socketio.on_namespace(UsersNamespace('/users'))
# endregion SocketIO Namespaces
# region Database event Listeners

View File

@ -15,4 +15,4 @@ def before_request():
pass
from . import cli, events, json_routes, routes, settings
from . import cli, json_routes, routes, settings

View File

@ -1,82 +0,0 @@
from flask_login import current_user
from flask_socketio import join_room, leave_room
from app import hashids, socketio
from app.decorators import socketio_login_required
from app.models import User
@socketio.on('users.get_user')
@socketio_login_required
def get_user(user_hashid: str) -> dict:
user_id = hashids.decode(user_hashid)
if not isinstance(user_id, int):
return {'status': 400, 'statusText': 'Bad Request'}
user = User.query.get(user_id)
if user is None:
return {'status': 404, 'statusText': 'Not found'}
if not (
user == current_user
or current_user.is_administrator
):
return {'status': 403, 'statusText': 'Forbidden'}
return {
'body': user.to_json_serializeable(
backrefs=True,
relationships=True
),
'status': 200,
'statusText': 'OK'
}
@socketio.on('users.subscribe_user')
@socketio_login_required
def subscribe_user(user_hashid: str) -> dict:
user_id = hashids.decode(user_hashid)
if not isinstance(user_id, int):
return {'status': 400, 'statusText': 'Bad Request'}
user = User.query.get(user_id)
if user is None:
return {'status': 404, 'statusText': 'Not found'}
if not (
user == current_user
or current_user.is_administrator
):
return {'status': 403, 'statusText': 'Forbidden'}
join_room(f'/users/{user.hashid}')
return {'status': 200, 'statusText': 'OK'}
@socketio.on('users.unsubscribe_user')
@socketio_login_required
def on_unsubscribe_user(user_hashid: str) -> dict:
user_id = hashids.decode(user_hashid)
if not isinstance(user_id, int):
return {'status': 400, 'statusText': 'Bad Request'}
user = User.query.get(user_id)
if user is None:
return {'status': 404, 'statusText': 'Not found'}
if not (
user == current_user
or current_user.is_administrator
):
return {'status': 403, 'statusText': 'Forbidden'}
leave_room(f'/users/{user.hashid}')
return {'status': 200, 'statusText': 'OK'}

View File

@ -26,7 +26,7 @@ def socketio_login_required(f):
def wrapper(*args, **kwargs):
if current_user.is_authenticated:
return f(*args, **kwargs)
return {'code': 401, 'body': 'Unauthorized'}
return {'status': 401, 'statusText': 'Unauthorized'}
return wrapper
@ -35,7 +35,7 @@ def socketio_permission_required(permission):
@wraps(f)
def wrapper(*args, **kwargs):
if not current_user.can(permission):
return {'code': 403, 'body': 'Forbidden'}
return {'status': 403, 'statusText': 'Forbidden'}
return f(*args, **kwargs)
return wrapper
return decorator

View File

@ -50,7 +50,7 @@ def _create_build_corpus_service(corpus: Corpus):
''' ## Constraints ## '''
constraints = ['node.role==worker']
''' ## Image ## '''
image = f'{current_app.config["NOPAQUE_DOCKER_IMAGE_PREFIX"]}cwb:r1879'
image = f'{current_app.config["NOPAQUE_DOCKER_IMAGE_PREFIX"]}cwb:r1887'
''' ## Labels ## '''
labels = {
'nopaque.server_name': current_app.config['SERVER_NAME']
@ -141,7 +141,7 @@ def _create_cqpserver_container(corpus: Corpus):
''' ## Entrypoint ## '''
entrypoint = ['bash', '-c']
''' ## Image ## '''
image = f'{current_app.config["NOPAQUE_DOCKER_IMAGE_PREFIX"]}cwb:r1879'
image = f'{current_app.config["NOPAQUE_DOCKER_IMAGE_PREFIX"]}cwb:r1887'
''' ## Name ## '''
name = f'nopaque-cqpserver-{corpus.id}'
''' ## Network ## '''

View File

@ -42,8 +42,9 @@ def resource_after_delete(mapper, connection, resource):
'path': resource.jsonpatch_path
}
]
namespace = '/users'
room = f'/users/{resource.user_hashid}'
socketio.emit('patch_user', jsonpatch, namespace='/users', room=room)
socketio.emit('patch', jsonpatch, namespace=namespace, room=room)
def cfa_after_delete(mapper, connection, cfa):
@ -54,8 +55,9 @@ def cfa_after_delete(mapper, connection, cfa):
'path': jsonpatch_path
}
]
namespace = '/users'
room = f'/users/{cfa.corpus.user.hashid}'
socketio.emit('patch_user', jsonpatch, namespace='/users', room=room)
socketio.emit('patch', jsonpatch, namespace=namespace, room=room)
def resource_after_insert(mapper, connection, resource):
@ -69,8 +71,9 @@ def resource_after_insert(mapper, connection, resource):
'value': jsonpatch_value
}
]
namespace = '/users'
room = f'/users/{resource.user_hashid}'
socketio.emit('patch_user', jsonpatch, namespace='/users', room=room)
socketio.emit('patch', jsonpatch, namespace=namespace, room=room)
def cfa_after_insert(mapper, connection, cfa):
@ -83,8 +86,9 @@ def cfa_after_insert(mapper, connection, cfa):
'value': jsonpatch_value
}
]
namespace = '/users'
room = f'/users/{cfa.corpus.user.hashid}'
socketio.emit('patch_user', jsonpatch, namespace='/users', room=room)
socketio.emit('patch', jsonpatch, namespace=namespace, room=room)
def resource_after_update(mapper, connection, resource):
@ -109,8 +113,9 @@ def resource_after_update(mapper, connection, resource):
}
)
if jsonpatch:
namespace = '/users'
room = f'/users/{resource.user_hashid}'
socketio.emit('patch_user', jsonpatch, namespace='/users', room=room)
socketio.emit('patch', jsonpatch, namespace=namespace, room=room)
def job_after_update(mapper, connection, job):

View File

@ -9,8 +9,8 @@ from threading import Lock
from app import db, docker_client, hashids, socketio
from app.decorators import socketio_login_required
from app.models import Corpus, CorpusStatus
from . import cqi_extensions
from .utils import CQiOverSocketIOSessionManager
from . import cqi_extension_functions
from .utils import SessionManager
'''
@ -85,6 +85,16 @@ CQI_API_FUNCTION_NAMES = [
]
CQI_EXTENSION_FUNCTION_NAMES = [
'ext_corpus_update_db',
'ext_corpus_static_data',
'ext_corpus_paginate_corpus',
'ext_cqp_paginate_subcorpus',
'ext_cqp_partial_export_subcorpus',
'ext_cqp_export_subcorpus',
]
class CQiOverSocketIONamespace(Namespace):
@socketio_login_required
def on_connect(self):
@ -135,25 +145,25 @@ class CQiOverSocketIONamespace(Namespace):
cqi_client = CQiClient(cqpserver_ip_address)
cqi_client_lock = Lock()
CQiOverSocketIOSessionManager.setup()
CQiOverSocketIOSessionManager.set_corpus_id(corpus_id)
CQiOverSocketIOSessionManager.set_cqi_client(cqi_client)
CQiOverSocketIOSessionManager.set_cqi_client_lock(cqi_client_lock)
SessionManager.setup()
SessionManager.set_corpus_id(corpus_id)
SessionManager.set_cqi_client(cqi_client)
SessionManager.set_cqi_client_lock(cqi_client_lock)
return {'code': 200, 'msg': 'OK'}
@socketio_login_required
def on_exec(self, fn_name: str, fn_args: dict = {}) -> dict:
try:
cqi_client = CQiOverSocketIOSessionManager.get_cqi_client()
cqi_client_lock = CQiOverSocketIOSessionManager.get_cqi_client_lock()
cqi_client = SessionManager.get_cqi_client()
cqi_client_lock = SessionManager.get_cqi_client_lock()
except KeyError:
return {'code': 424, 'msg': 'Failed Dependency'}
if fn_name in CQI_API_FUNCTION_NAMES:
fn = getattr(cqi_client.api, fn_name)
elif fn_name in cqi_extensions.CQI_EXTENSION_FUNCTION_NAMES:
fn = getattr(cqi_extensions, fn_name)
elif fn_name in CQI_EXTENSION_FUNCTION_NAMES:
fn = getattr(cqi_extension_functions, fn_name)
else:
return {'code': 400, 'msg': 'Bad Request'}
@ -198,10 +208,10 @@ class CQiOverSocketIONamespace(Namespace):
def on_disconnect(self):
try:
corpus_id = CQiOverSocketIOSessionManager.get_corpus_id()
cqi_client = CQiOverSocketIOSessionManager.get_cqi_client()
cqi_client_lock = CQiOverSocketIOSessionManager.get_cqi_client_lock()
CQiOverSocketIOSessionManager.teardown()
corpus_id = SessionManager.get_corpus_id()
cqi_client = SessionManager.get_cqi_client()
cqi_client_lock = SessionManager.get_cqi_client_lock()
SessionManager.teardown()
except KeyError:
return

View File

@ -8,22 +8,12 @@ import json
import math
from app import db
from app.models import Corpus
from .utils import CQiOverSocketIOSessionManager
CQI_EXTENSION_FUNCTION_NAMES = [
'ext_corpus_update_db',
'ext_corpus_static_data',
'ext_corpus_paginate_corpus',
'ext_cqp_paginate_subcorpus',
'ext_cqp_partial_export_subcorpus',
'ext_cqp_export_subcorpus',
]
from .utils import SessionManager
def ext_corpus_update_db(corpus: str) -> CQiStatusOk:
corpus_id = CQiOverSocketIOSessionManager.get_corpus_id()
cqi_client = CQiOverSocketIOSessionManager.get_cqi_client()
corpus_id = SessionManager.get_corpus_id()
cqi_client = SessionManager.get_cqi_client()
db_corpus = Corpus.query.get(corpus_id)
cqi_corpus = cqi_client.corpora.get(corpus)
db_corpus.num_tokens = cqi_corpus.size
@ -32,7 +22,7 @@ def ext_corpus_update_db(corpus: str) -> CQiStatusOk:
def ext_corpus_static_data(corpus: str) -> dict:
corpus_id = CQiOverSocketIOSessionManager.get_corpus_id()
corpus_id = SessionManager.get_corpus_id()
db_corpus = Corpus.query.get(corpus_id)
static_data_file_path = db_corpus.path / 'cwb' / 'static.json.gz'
@ -40,7 +30,7 @@ def ext_corpus_static_data(corpus: str) -> dict:
with static_data_file_path.open('rb') as f:
return f.read()
cqi_client = CQiOverSocketIOSessionManager.get_cqi_client()
cqi_client = SessionManager.get_cqi_client()
cqi_corpus = cqi_client.corpora.get(corpus)
cqi_p_attrs = cqi_corpus.positional_attributes.list()
cqi_s_attrs = cqi_corpus.structural_attributes.list()
@ -168,7 +158,7 @@ def ext_corpus_paginate_corpus(
page: int = 1,
per_page: int = 20
) -> dict:
cqi_client = CQiOverSocketIOSessionManager.get_cqi_client()
cqi_client = SessionManager.get_cqi_client()
cqi_corpus = cqi_client.corpora.get(corpus)
# Sanity checks
if (
@ -215,7 +205,7 @@ def ext_cqp_paginate_subcorpus(
per_page: int = 20
) -> dict:
corpus_name, subcorpus_name = subcorpus.split(':', 1)
cqi_client = CQiOverSocketIOSessionManager.get_cqi_client()
cqi_client = SessionManager.get_cqi_client()
cqi_corpus = cqi_client.corpora.get(corpus_name)
cqi_subcorpus = cqi_corpus.subcorpora.get(subcorpus_name)
# Sanity checks
@ -262,7 +252,7 @@ def ext_cqp_partial_export_subcorpus(
context: int = 50
) -> dict:
corpus_name, subcorpus_name = subcorpus.split(':', 1)
cqi_client = CQiOverSocketIOSessionManager.get_cqi_client()
cqi_client = SessionManager.get_cqi_client()
cqi_corpus = cqi_client.corpora.get(corpus_name)
cqi_subcorpus = cqi_corpus.subcorpora.get(subcorpus_name)
cqi_subcorpus_partial_export = _partial_export_subcorpus(cqi_subcorpus, match_id_list, context=context)
@ -271,7 +261,7 @@ def ext_cqp_partial_export_subcorpus(
def ext_cqp_export_subcorpus(subcorpus: str, context: int = 50) -> dict:
corpus_name, subcorpus_name = subcorpus.split(':', 1)
cqi_client = CQiOverSocketIOSessionManager.get_cqi_client()
cqi_client = SessionManager.get_cqi_client()
cqi_corpus = cqi_client.corpora.get(corpus_name)
cqi_subcorpus = cqi_corpus.subcorpora.get(subcorpus_name)
cqi_subcorpus_export = _export_subcorpus(cqi_subcorpus, context=context)

View File

@ -3,7 +3,7 @@ from threading import Lock
from flask import session
class CQiOverSocketIOSessionManager:
class SessionManager:
@staticmethod
def setup():
session['cqi_over_sio'] = {}

109
app/namespaces/jobs.py Normal file
View File

@ -0,0 +1,109 @@
from flask import current_app, Flask
from flask_login import current_user
from flask_socketio import Namespace
from app import db, hashids, socketio
from app.decorators import socketio_admin_required, socketio_login_required
from app.models import Job, JobStatus
def _delete_job(app: Flask, job_id: int):
with app.app_context():
job = Job.query.get(job_id)
job.delete()
db.session.commit()
def _restart_job(app, job_id):
with app.app_context():
job = Job.query.get(job_id)
job.restart()
db.session.commit()
class UsersNamespace(Namespace):
@socketio_login_required
def on_delete(self, job_hashid: str) -> dict:
job_id = hashids.decode(job_hashid)
if not isinstance(job_id, int):
return {'status': 400, 'statusText': 'Bad Request'}
job = Job.query.get(job_id)
if job is None:
return {'status': 404, 'statusText': 'Not found'}
if not (
job.user == current_user
or current_user.is_administrator
):
return {'status': 403, 'statusText': 'Forbidden'}
socketio.start_background_task(
_delete_job,
current_app._get_current_object(),
job_id
)
return {
'body': f'Job "{job.title}" marked for deletion',
'status': 202,
'statusText': 'Accepted'
}
@socketio_admin_required
def on_log(self, job_hashid: str):
job_id = hashids.decode(job_hashid)
if not isinstance(job_id, int):
return {'status': 400, 'statusText': 'Bad Request'}
job = Job.query.get(job_id)
if job is None:
return {'status': 404, 'statusText': 'Not found'}
if job.status not in [JobStatus.COMPLETED, JobStatus.FAILED]:
return {'status': 409, 'statusText': 'Conflict'}
with open(job.path / 'pipeline_data' / 'logs' / 'pyflow_log.txt') as log_file:
log = log_file.read()
return {
'body': log,
'status': 200,
'statusText': 'Forbidden'
}
socketio_login_required
def on_restart(self, job_hashid: str):
job_id = hashids.decode(job_hashid)
if not isinstance(job_id, int):
return {'status': 400, 'statusText': 'Bad Request'}
job = Job.query.get(job_id)
if job is None:
return {'status': 404, 'statusText': 'Not found'}
if not (
job.user == current_user
or current_user.is_administrator
):
return {'status': 403, 'statusText': 'Forbidden'}
if job.status == JobStatus.FAILED:
return {'status': 409, 'statusText': 'Conflict'}
socketio.start_background_task(
_restart_job,
current_app._get_current_object(),
job_id
)
return {
'body': f'Job "{job.title}" marked for restarting',
'status': 202,
'statusText': 'Accepted'
}

116
app/namespaces/users.py Normal file
View File

@ -0,0 +1,116 @@
from flask import current_app, Flask
from flask_login import current_user
from flask_socketio import join_room, leave_room, Namespace
from app import db, hashids, socketio
from app.decorators import socketio_login_required
from app.models import User
def _delete_user(app: Flask, user_id: int):
with app.app_context():
user = User.query.get(user_id)
user.delete()
db.session.commit()
class UsersNamespace(Namespace):
@socketio_login_required
def on_get(self, user_hashid: str) -> dict:
user_id = hashids.decode(user_hashid)
if not isinstance(user_id, int):
return {'status': 400, 'statusText': 'Bad Request'}
user = User.query.get(user_id)
if user is None:
return {'status': 404, 'statusText': 'Not found'}
if not (
user == current_user
or current_user.is_administrator
):
return {'status': 403, 'statusText': 'Forbidden'}
return {
'body': user.to_json_serializeable(
backrefs=True,
relationships=True
),
'status': 200,
'statusText': 'OK'
}
@socketio_login_required
def on_subscribe(self, user_hashid: str) -> dict:
user_id = hashids.decode(user_hashid)
if not isinstance(user_id, int):
return {'status': 400, 'statusText': 'Bad Request'}
user = User.query.get(user_id)
if user is None:
return {'status': 404, 'statusText': 'Not found'}
if not (
user == current_user
or current_user.is_administrator
):
return {'status': 403, 'statusText': 'Forbidden'}
join_room(f'/users/{user.hashid}')
return {'status': 200, 'statusText': 'OK'}
@socketio_login_required
def on_unsubscribe(self, user_hashid: str) -> dict:
user_id = hashids.decode(user_hashid)
if not isinstance(user_id, int):
return {'status': 400, 'statusText': 'Bad Request'}
user = User.query.get(user_id)
if user is None:
return {'status': 404, 'statusText': 'Not found'}
if not (
user == current_user
or current_user.is_administrator
):
return {'status': 403, 'statusText': 'Forbidden'}
leave_room(f'/users/{user.hashid}')
return {'status': 200, 'statusText': 'OK'}
@socketio_login_required
def on_delete(self, user_hashid: str) -> dict:
user_id = hashids.decode(user_hashid)
if not isinstance(user_id, int):
return {'status': 400, 'statusText': 'Bad Request'}
user = User.query.get(user_id)
if user is None:
return {'status': 404, 'statusText': 'Not found'}
if not (
user == current_user
or current_user.is_administrator
):
return {'status': 403, 'statusText': 'Forbidden'}
socketio.start_background_task(
_delete_user,
current_app._get_current_object(),
user.id
)
return {
'body': f'User "{user.username}" marked for deletion',
'status': 202,
'statusText': 'Accepted'
}

View File

@ -2,6 +2,10 @@
--corpus-status-content: "unprepared";
}
[data-corpus-status="SUBMITTED"] {
--corpus-status-content: "submitted";
}
[data-corpus-status="QUEUED"] {
--corpus-status-content: "queued";
}

47
app/static/css/height.css Normal file
View File

@ -0,0 +1,47 @@
.h-10 {
height: 10% !important;
}
.h-20 {
height: 20% !important;
}
.h-25 {
height: 25% !important;
}
.h-30 {
height: 30% !important;
}
.h-40 {
height: 40% !important;
}
.h-50 {
height: 50% !important;
}
.h-60 {
height: 60% !important;
}
.h-70 {
height: 70% !important;
}
.h-75 {
height: 75% !important;
}
.h-80 {
height: 80% !important;
}
.h-90 {
height: 90% !important;
}
.h-100 {
height: 100% !important;
}

47
app/static/css/width.css Normal file
View File

@ -0,0 +1,47 @@
.w-10 {
width: 10% !important;
}
.w-20 {
width: 20% !important;
}
.w-25 {
width: 25% !important;
}
.w-30 {
width: 30% !important;
}
.w-40 {
width: 40% !important;
}
.w-50 {
width: 50% !important;
}
.w-60 {
width: 60% !important;
}
.w-70 {
width: 70% !important;
}
.w-75 {
width: 75% !important;
}
.w-80 {
width: 80% !important;
}
.w-90 {
width: 90% !important;
}
.w-100 {
width: 100% !important;
}

View File

@ -1,201 +1,20 @@
nopaque.App = class App {
#promises;
constructor() {
this.data = {
users: {}
};
this.#promises = {
getUser: {},
subscribeUser: {}
};
this.socket = io({transports: ['websocket'], upgrade: false});
this.socket.on('patch_user', (patch) => {this.onPatch(patch);});
}
// Endpoints
this.users = new nopaque.app.endpoints.Users(this);
getUser(userId) {
if (userId in this.#promises.getUser) {
return this.#promises.getUser[userId];
}
this.#promises.getUser[userId] = new Promise((resolve, reject) => {
this.socket.emit('users.get_user', userId, (response) => {
if (response.status === 200) {
this.data.users[userId] = response.body;
resolve(this.data.users[userId]);
} else {
reject(`[${response.status}] ${response.statusText}`);
}
});
});
return this.#promises.getUser[userId];
}
subscribeUser(userId) {
if (userId in this.#promises.subscribeUser) {
return this.#promises.subscribeUser[userId];
}
this.#promises.subscribeUser[userId] = new Promise((resolve, reject) => {
this.socket.emit('users.subscribe_user', userId, (response) => {
if (response.status === 200) {
resolve(response);
} else {
reject(response);
}
});
});
return this.#promises.subscribeUser[userId];
}
flash(message, category) {
let iconPrefix = '';
switch (category) {
case 'corpus': {
iconPrefix = '<i class="left material-icons">book</i>';
break;
}
case 'error': {
iconPrefix = '<i class="error-color-text left material-icons">error</i>';
break;
}
case 'job': {
iconPrefix = '<i class="left nopaque-icons">J</i>';
break;
}
case 'settings': {
iconPrefix = '<i class="left material-icons">settings</i>';
break;
}
default: {
iconPrefix = '<i class="left material-icons">notifications</i>';
break;
}
}
let toast = M.toast(
{
html: `
<span>${iconPrefix}${message}</span>
<button class="action-button btn-flat toast-action white-text" data-action="close">
<i class="material-icons">close</i>
</button>
`.trim()
}
);
let toastCloseActionElement = toast.el.querySelector('.action-button[data-action="close"]');
toastCloseActionElement.addEventListener('click', () => {toast.dismiss();});
}
onPatch(patch) {
// Filter Patch to only include operations on users that are initialized
let regExp = new RegExp(`^/users/(${Object.keys(this.data.users).join('|')})`);
let filteredPatch = patch.filter(operation => regExp.test(operation.path));
// Handle job status updates
let subRegExp = new RegExp(`^/users/([A-Za-z0-9]*)/jobs/([A-Za-z0-9]*)/status$`);
let subFilteredPatch = filteredPatch
.filter((operation) => {return operation.op === 'replace';})
.filter((operation) => {return subRegExp.test(operation.path);});
for (let operation of subFilteredPatch) {
let [match, userId, jobId] = operation.path.match(subRegExp);
this.flash(`[<a href="/jobs/${jobId}">${this.data.users[userId].jobs[jobId].title}</a>] New status: <span class="job-status-text" data-job-status="${operation.value}"></span>`, 'job');
}
// Apply Patch
jsonpatch.applyPatch(this.data, filteredPatch);
// Extensions
this.toaster = new nopaque.app.extensions.Toaster(this);
this.ui = new nopaque.app.extensions.UI(this);
this.userHub = new nopaque.app.extensions.UserHub(this);
}
init() {
this.initUi();
}
initUi() {
/* Pre-Initialization fixes */
// #region
// Flask-WTF sets the standard HTML maxlength Attribute on input/textarea
// elements to specify their maximum length (in characters). Unfortunatly
// Materialize won't recognize the maxlength Attribute, instead it uses
// the data-length Attribute. It's conversion time :)
for (let elem of document.querySelectorAll('input[maxlength], textarea[maxlength]')) {
elem.dataset.length = elem.getAttribute('maxlength');
elem.removeAttribute('maxlength');
}
// To work around some limitations with the Form setup of Flask-WTF.
// HTML option elements with an empty value are considered as placeholder
// elements. The user should not be able to actively select these options.
// So they get the disabled attribute.
for (let optionElement of document.querySelectorAll('option[value=""]')) {
optionElement.disabled = true;
}
// TODO: Check why we are doing this.
for (let optgroupElement of document.querySelectorAll('optgroup[label=""]')) {
for (let c of optgroupElement.children) {
optgroupElement.parentElement.insertAdjacentElement('afterbegin', c);
}
optgroupElement.remove();
}
// #endregion
/* Initialize Materialize Components */
// #region
// Automatically initialize Materialize Components that do not require
// additional configuration.
M.AutoInit();
// CharacterCounters
// Materialize didn't include the CharacterCounter plugin within the
// AutoInit method (maybe they forgot it?). Anyway... We do it here. :)
M.CharacterCounter.init(document.querySelectorAll('input[data-length]:not(.no-autoinit), textarea[data-length]:not(.no-autoinit)'));
// Header navigation processes and services Dropdown.
M.Dropdown.init(
document.querySelector('#navbar-data-processing-and-analysis-dropdown-trigger'),
{
constrainWidth: false,
container: document.querySelector('#dropdowns'),
coverTrigger: false
}
);
// Header navigation account Dropdown.
M.Dropdown.init(
document.querySelector('#navbar-account-dropdown-trigger'),
{
alignment: 'right',
constrainWidth: false,
container: document.querySelector('#dropdowns'),
coverTrigger: false
}
);
// Terms of use modal
M.Modal.init(
document.querySelector('#terms-of-use-modal'),
{
dismissible: false,
onCloseEnd: (modalElement) => {
nopaque.requests.users.entity.acceptTermsOfUse();
}
}
);
// #endregion
/* Initialize nopaque Components */
// #region
nopaque.resource_displays.AutoInit();
nopaque.resource_lists.AutoInit();
nopaque.forms.AutoInit();
// #endregion
// Initialize extensions
this.toaster.init();
this.ui.init();
this.userHub.init();
}
};

View File

@ -0,0 +1 @@
nopaque.app.endpoints = {};

View File

@ -0,0 +1,41 @@
nopaque.app.endpoints.Users = class Users {
constructor(app) {
this.app = app;
this.socket = io('/users', {transports: ['websocket'], upgrade: false});
}
async get(userId) {
const response = await this.socket.emitWithAck('get', userId);
if (response.status !== 200) {
throw new Error(`[${response.status}] ${response.statusText}`);
}
return response.body;
}
async subscribe(userId) {
const response = await this.socket.emitWithAck('subscribe', userId);
if (response.status != 200) {
throw new Error(`[${response.status}] ${response.statusText}`);
}
}
async unsubscribe(userId) {
const response = await this.socket.emitWithAck('unsubscribe', userId);
if (response.status != 200) {
throw new Error(`[${response.status}] ${response.statusText}`);
}
}
async delete(userId) {
const response = await this.socket.emitWithAck('delete', userId);
if (response.status != 202) {
throw new Error(`[${response.status}] ${response.statusText}`);
}
}
}

View File

@ -0,0 +1 @@
nopaque.app.extensions = {};

View File

@ -0,0 +1,56 @@
nopaque.app.extensions.Toaster = class Toaster {
constructor(app) {
this.app = app;
}
init() {
this.app.userHub.addEventListener('patch', (event) => {this.#onPatch(event.detail);});
}
async #onPatch(patch) {
// Handle corpus updates
const corpusRegExp = new RegExp(`^/users/([A-Za-z0-9]+)/corpora/([A-Za-z0-9]+)`);
const corpusPatch = patch.filter((operation) => {return corpusRegExp.test(operation.path);});
this.#onCorpusPatch(corpusPatch);
// Handle job updates
const jobRegExp = new RegExp(`^/users/([A-Za-z0-9]+)/jobs/([A-Za-z0-9]+)`);
const jobPatch = patch.filter((operation) => {return jobRegExp.test(operation.path);});
this.#onJobPatch(jobPatch);
}
async #onCorpusPatch(patch) {
return;
// Handle corpus status updates
const corpusStatusRegExp = new RegExp(`^/users/([A-Za-z0-9]+)/corpora/([A-Za-z0-9]+)/status$`);
const corpusStatusPatch = patch
.filter((operation) => {return corpusStatusRegExp.test(operation.path);})
.filter((operation) => {return operation.op === 'replace';});
for (let operation of corpusStatusPatch) {
const [match, userId, corpusId] = operation.path.match(corpusStatusRegExp);
const user = await this.app.userHub.get(userId);
const corpus = user.corpora[corpusId];
this.app.ui.flash(`[<a href="/corpora/${corpusId}">${corpus.title}</a>] New status: <span class="corpus-status-text" data-corpus-status="${operation.value}"></span>`, 'corpus');
}
}
async #onJobPatch(patch) {
// Handle job status updates
const jobStatusRegExp = new RegExp(`^/users/([A-Za-z0-9]+)/jobs/([A-Za-z0-9]+)/status$`);
const jobStatusPatch = patch
.filter((operation) => {return jobStatusRegExp.test(operation.path);})
.filter((operation) => {return operation.op === 'replace';});
for (let operation of jobStatusPatch) {
const [match, userId, jobId] = operation.path.match(jobStatusRegExp);
const user = await this.app.userHub.get(userId);
const job = user.jobs[jobId];
this.app.ui.flash(`[<a href="/jobs/${jobId}">${job.title}</a>] New status: <span class="job-status-text" data-job-status="${operation.value}"></span>`, 'job');
}
}
}

View File

@ -0,0 +1,126 @@
nopaque.app.extensions.UI = class UI {
constructor(app) {
this.app = app;
}
init() {
/* Pre-Initialization fixes */
// #region
// Flask-WTF sets the standard HTML maxlength Attribute on input/textarea
// elements to specify their maximum length (in characters). Unfortunatly
// Materialize won't recognize the maxlength Attribute, instead it uses
// the data-length Attribute. It's conversion time :)
for (let elem of document.querySelectorAll('input[maxlength], textarea[maxlength]')) {
elem.dataset.length = elem.getAttribute('maxlength');
elem.removeAttribute('maxlength');
}
// To work around some limitations with the Form setup of Flask-WTF.
// HTML option elements with an empty value are considered as placeholder
// elements. The user should not be able to actively select these options.
// So they get the disabled attribute.
for (let optionElement of document.querySelectorAll('option[value=""]')) {
optionElement.disabled = true;
}
// TODO: Check why we are doing this.
for (let optgroupElement of document.querySelectorAll('optgroup[label=""]')) {
for (let c of optgroupElement.children) {
optgroupElement.parentElement.insertAdjacentElement('afterbegin', c);
}
optgroupElement.remove();
}
// #endregion
/* Initialize Materialize Components */
// #region
// Automatically initialize Materialize Components that do not require
// additional configuration.
M.AutoInit();
// CharacterCounters
// Materialize didn't include the CharacterCounter plugin within the
// AutoInit method (maybe they forgot it?). Anyway... We do it here. :)
M.CharacterCounter.init(document.querySelectorAll('input[data-length]:not(.no-autoinit), textarea[data-length]:not(.no-autoinit)'));
// Header navigation processes and services Dropdown.
M.Dropdown.init(
document.querySelector('#navbar-data-processing-and-analysis-dropdown-trigger'),
{
constrainWidth: false,
container: document.querySelector('#dropdowns'),
coverTrigger: false
}
);
// Header navigation account Dropdown.
M.Dropdown.init(
document.querySelector('#navbar-account-dropdown-trigger'),
{
alignment: 'right',
constrainWidth: false,
container: document.querySelector('#dropdowns'),
coverTrigger: false
}
);
// Terms of use modal
M.Modal.init(
document.querySelector('#terms-of-use-modal'),
{
dismissible: false,
onCloseEnd: (modalElement) => {
nopaque.requests.users.entity.acceptTermsOfUse();
}
}
);
// #endregion
/* Initialize nopaque Components */
// #region
nopaque.resource_displays.AutoInit();
nopaque.resource_lists.AutoInit();
nopaque.forms.AutoInit();
// #endregion
}
flash(message, category) {
let iconPrefix;
switch (category) {
case 'corpus': {
iconPrefix = '<i class="material-icons left">book</i>';
break;
}
case 'job': {
iconPrefix = '<i class="nopaque-icons left">J</i>';
break;
}
case 'error': {
iconPrefix = '<i class="material-icons left error-color-text">error</i>';
break;
}
default: {
iconPrefix = '<i class="material-icons left">notifications</i>';
break;
}
}
let toast = M.toast(
{
html: `
<span>${iconPrefix}${message}</span>
<button class="btn-flat toast-action white-text" data-toast-action="dismiss">
<i class="material-icons">close</i>
</button>
`.trim()
}
);
let dismissToastElement = toast.el.querySelector('.toast-action[data-toast-action="dismiss"]');
dismissToastElement.addEventListener('click', () => {toast.dismiss();});
}
}

View File

@ -0,0 +1,68 @@
nopaque.app.extensions.UserHub = class UserHub extends EventTarget {
#data;
constructor(app) {
super();
this.app = app;
this.#data = {
users: {},
promises: {}
};
}
init() {
this.app.users.socket.on('patch', (patch) => {this.#onPatch(patch)});
}
add(userId) {
if (!(userId in this.#data.promises)) {
this.#data.promises[userId] = this.#add(userId);
}
return this.#data.promises[userId];
}
async #add(userId) {
await this.app.users.subscribe(userId);
this.#data.users[userId] = await this.app.users.get(userId);
}
async get(userId) {
await this.add(userId);
return this.#data.users[userId];
}
#onPatch(patch) {
// Filter patch to only include operations on users that are initialized
const filterRegExp = new RegExp(`^/users/(${Object.keys(this.#data.users).join('|')})`);
const filteredPatch = patch.filter(operation => filterRegExp.test(operation.path));
// Apply patch
jsonpatch.applyPatch(this.#data, filteredPatch);
// Notify event listeners
const patchEventa = new CustomEvent('patch', {detail: filteredPatch});
this.dispatchEvent(patchEventa);
// Notify event listeners. Event type: "patch *"
const patchEvent = new CustomEvent('patch *', {detail: filteredPatch});
this.dispatchEvent(patchEvent);
// Group patches by user id: {<user-id>: [op, ...], ...}
const patches = {};
const matchRegExp = new RegExp(`^/users/([A-Za-z0-9]+)`);
for (let operation of filteredPatch) {
const [match, userId] = operation.path.match(matchRegExp);
if (!(userId in patches)) {patches[userId] = [];}
patches[userId].push(operation);
}
// Notify event listeners. Event type: "patch <user-id>"
for (let [userId, patch] of Object.entries(patches)) {
const userPatchEvent = new CustomEvent(`patch ${userId}`, {detail: patch});
this.dispatchEvent(userPatchEvent);
}
}
}

View File

@ -0,0 +1 @@
nopaque.app = {};

View File

@ -66,7 +66,7 @@ nopaque.corpus_analysis.ConcordanceExtension = class ConcordanceExtension {
errorString += `${error.constructor.name}`;
this.elements.error.innerText = errorString;
this.elements.error.classList.remove('hide');
app.flash(errorString, 'error');
app.ui.flash(errorString, 'error');
this.elements.progress.classList.add('hide');
}
this.app.enableActionElements();
@ -239,7 +239,7 @@ nopaque.corpus_analysis.ConcordanceExtension = class ConcordanceExtension {
if (subcorpus.selectedItems.size === 0) {
this.elements.progress.classList.add('hide');
this.app.enableActionElements();
app.flash('No matches selected', 'error');
app.ui.flash('No matches selected', 'error');
return;
}
promise = subcorpus.o.partialExport([...subcorpus.selectedItems], 50);
@ -298,7 +298,7 @@ nopaque.corpus_analysis.ConcordanceExtension = class ConcordanceExtension {
let subcorpus = this.data.subcorpora[this.settings.selectedSubcorpus];
subcorpus.o.drop().then(
(cQiStatus) => {
app.flash(`${subcorpus.o.name} deleted`, 'corpus');
app.ui.flash(`${subcorpus.o.name} deleted`, 'corpus');
delete this.data.subcorpora[subcorpus.o.name];
this.settings.selectedSubcorpus = undefined;
for (let subcorpusName in this.data.subcorpora) {
@ -320,7 +320,7 @@ nopaque.corpus_analysis.ConcordanceExtension = class ConcordanceExtension {
},
(cqiError) => {
let errorString = `${cqiError.code}: ${cqiError.constructor.name}`;
app.flash(errorString, 'error');
app.ui.flash(errorString, 'error');
}
);
});

View File

@ -46,7 +46,7 @@ nopaque.corpus_analysis.ReaderExtension = class ReaderExtension {
if ('description' in error) {errorString += `: ${error.description}`;}
this.elements.error.innerText = errorString;
this.elements.error.classList.remove('hide');
app.flash(errorString, 'error');
app.ui.flash(errorString, 'error');
this.elements.progress.classList.add('hide');
}
this.app.enableActionElements();

View File

@ -101,7 +101,7 @@ nopaque.forms.BaseForm = class BaseForm {
}
}
if (request.status === 500) {
app.flash('Internal Server Error', 'error');
app.ui.flash('Internal Server Error', 'error');
}
modal.close();
});

View File

@ -25,16 +25,16 @@ nopaque.requests.JSONfetch = (input, init={}) => {
let message = json.message;
let category = json.category || 'message';
if (message) {
app.flash(message, category);
app.ui.flash(message, category);
}
},
(error) => {
app.flash(`[${response.status}]: ${response.statusText}`, 'error');
app.ui.flash(`[${response.status}]: ${response.statusText}`, 'error');
}
);
},
(response) => {
app.flash('Something went wrong', 'error');
app.ui.flash('Something went wrong', 'error');
reject(response);
}
);

View File

@ -52,22 +52,23 @@ nopaque.resource_displays.CorpusDisplay = class CorpusDisplay extends nopaque.re
}
}
setTitle(title) {
this.setElements(this.displayElement.querySelectorAll('.corpus-title'), title);
async setTitle(title) {
const corpusTitleElements = this.displayElement.querySelectorAll('.corpus-title');
this.setElements(corpusTitleElements, title);
}
setNumTokens(numTokens) {
this.setElements(
this.displayElement.querySelectorAll('.corpus-token-ratio'),
`${numTokens}/${app.data.users[this.userId].corpora[this.corpusId].max_num_tokens}`
);
const corpusTokenRatioElements = this.displayElement.querySelectorAll('.corpus-token-ratio');
const maxNumTokens = 2147483647;
this.setElements(corpusTokenRatioElements, `${numTokens}/${maxNumTokens}`);
}
setDescription(description) {
this.setElements(this.displayElement.querySelectorAll('.corpus-description'), description);
}
setStatus(status) {
async setStatus(status) {
let elements = this.displayElement.querySelectorAll('.action-button[data-action="analyze"]');
for (let element of elements) {
if (['BUILT', 'STARTING_ANALYSIS_SESSION', 'RUNNING_ANALYSIS_SESSION', 'CANCELING_ANALYSIS_SESSION'].includes(status)) {
@ -77,8 +78,10 @@ nopaque.resource_displays.CorpusDisplay = class CorpusDisplay extends nopaque.re
}
}
elements = this.displayElement.querySelectorAll('.action-button[data-action="build-request"]');
const user = await app.userHub.get(this.userId);
const corpusFiles = user.corpora[this.corpusId].files;
for (let element of elements) {
if (['UNPREPARED', 'FAILED'].includes(status) && Object.values(app.data.users[this.userId].corpora[this.corpusId].files).length > 0) {
if (['UNPREPARED', 'FAILED'].includes(status) && Object.values(corpusFiles.length > 0)) {
element.classList.remove('disabled');
} else {
element.classList.add('disabled');

View File

@ -5,20 +5,15 @@ nopaque.resource_displays.ResourceDisplay = class ResourceDisplay {
this.displayElement = displayElement;
this.userId = this.displayElement.dataset.userId;
this.isInitialized = false;
if (this.userId) {
app.subscribeUser(this.userId)
.then((response) => {
app.socket.on('patch_user', (patch) => {
if (this.isInitialized) {this.onPatch(patch);}
if (this.userId === undefined) {return;}
app.userHub.addEventListener('patch', (event) => {
if (this.isInitialized) {this.onPatch(event.detail);}
});
});
app.getUser(this.userId)
.then((user) => {
app.userHub.get(this.userId).then((user) => {
this.init(user);
this.isInitialized = true;
});
}
}
init(user) {throw 'Not implemented';}

View File

@ -14,12 +14,11 @@ nopaque.resource_lists.CorpusFileList = class CorpusFileList extends nopaque.res
this.hasPermissionView = listContainerElement.dataset?.hasPermissionView == 'true' || false;
this.hasPermissionManageFiles = listContainerElement.dataset?.hasPermissionManageFiles == 'true' || false;
if (this.userId === undefined || this.corpusId === undefined) {return;}
app.subscribeUser(this.userId).then((response) => {
app.socket.on('patch_user', (patch) => {
if (this.isInitialized) {this.onPatch(patch);}
app.userHub.addEventListener('patch', (event) => {
if (this.isInitialized) {this.onPatch(event.detail);}
});
});
app.getUser(this.userId).then((user) => {
app.userHub.get(this.userId).then((user) => {
// TODO: Make this better understandable
this.add(Object.values(user.corpora[this.corpusId].files || user.followed_corpora[this.corpusId].files));
this.isInitialized = true;
});

View File

@ -12,15 +12,16 @@ nopaque.resource_lists.CorpusFollowerList = class CorpusFollowerList extends nop
this.userId = listContainerElement.dataset.userId;
this.corpusId = listContainerElement.dataset.corpusId;
if (this.userId === undefined || this.corpusId === undefined) {return;}
app.subscribeUser(this.userId).then((response) => {
app.socket.on('patch_user', (patch) => {
if (this.isInitialized) {this.onPatch(patch);}
app.userHub.addEventListener('patch', (event) => {
if (this.isInitialized) {this.onPatch(event.detail);}
});
});
app.getUser(this.userId).then((user) => {
app.userHub.get(this.userId).then((user) => {
// TODO: Check if the following is better
// let corpusFollowerAssociations = Object.values(user.corpora[this.corpusId].corpus_follower_associations);
// let filteredList = corpusFollowerAssociations.filter(association => association.follower.id != currentUserId);
// this.add(filteredList);
// TODO: Make this better understandable
this.add(Object.values(user.corpora[this.corpusId].corpus_follower_associations));
this.isInitialized = true;
});

View File

@ -11,12 +11,10 @@ nopaque.resource_lists.CorpusList = class CorpusList extends nopaque.resource_li
this.selectedItemIds = new Set();
this.userId = listContainerElement.dataset.userId;
if (this.userId === undefined) {return;}
app.subscribeUser(this.userId).then((response) => {
app.socket.on('patch_user', (patch) => {
if (this.isInitialized) {this.onPatch(patch);}
app.userHub.addEventListener('patch', (event) => {
if (this.isInitialized) {this.onPatch(event.detail);}
});
});
app.getUser(this.userId).then((user) => {
app.userHub.get(this.userId).then((user) => {
this.add(this.aggregateData(user));
this.isInitialized = true;
});
@ -69,6 +67,7 @@ nopaque.resource_lists.CorpusList = class CorpusList extends nopaque.resource_li
<span class="disable-on-click"></span>
</label>
</td>
<td><a class="btn-floating service-color darken" data-service="corpus-analysis"><i class="material-icons">book</i></a></td>
<td>
<b class="title"></b><br>
<i class="description"></i>
@ -80,7 +79,7 @@ nopaque.resource_lists.CorpusList = class CorpusList extends nopaque.resource_li
<td>${values['current-user-is-following'] ? '<span><i class="left material-icons">visibility</i>Following</span>' : ''}</td>
<td class="right-align">
<a class="list-action-trigger btn-floating red waves-effect waves-light" data-list-action="delete-request"><i class="material-icons">delete</i></a>
<a class="list-action-trigger btn-floating darken waves-effect waves-light" data-list-action="view"><i class="material-icons">send</i></a>
<a class="list-action-trigger btn-floating waves-effect waves-light" data-list-action="view"><i class="material-icons">send</i></a>
</td>
</tr>
`.trim();
@ -119,6 +118,7 @@ nopaque.resource_lists.CorpusList = class CorpusList extends nopaque.resource_li
<span></span>
</label>
</th>
<th></th>
<th>Title and Description</th>
<th>Owner</th>
<th>Status</th>

View File

@ -8,8 +8,10 @@ nopaque.resource_lists.JobInputList = class JobInputList extends nopaque.resourc
this.userId = listContainerElement.dataset.userId;
this.jobId = listContainerElement.dataset.jobId;
if (this.userId === undefined || this.jobId === undefined) {return;}
app.subscribeUser(this.userId);
app.getUser(this.userId).then((user) => {
// app.userHub.addEventListener('patch', (event) => {
// if (this.isInitialized) {this.onPatch(event.detail);}
// });
app.userHub.get(this.userId).then((user) => {
this.add(Object.values(user.jobs[this.jobId].inputs));
this.isInitialized = true;
});

View File

@ -12,12 +12,10 @@ nopaque.resource_lists.JobList = class JobList extends nopaque.resource_lists.Re
this.selectedItemIds = new Set();
this.userId = listContainerElement.dataset.userId;
if (this.userId === undefined) {return;}
app.subscribeUser(this.userId).then((response) => {
app.socket.on('patch_user', (patch) => {
if (this.isInitialized) {this.onPatch(patch);}
app.userHub.addEventListener('patch', (event) => {
if (this.isInitialized) {this.onPatch(event.detail);}
});
});
app.getUser(this.userId).then((user) => {
app.userHub.get(this.userId).then((user) => {
this.add(Object.values(user.jobs));
this.isInitialized = true;
});
@ -25,19 +23,19 @@ nopaque.resource_lists.JobList = class JobList extends nopaque.resource_lists.Re
get item() {
return `
<tr class="list-item clickable hoverable">
<tr class="list-item clickable hoverable service-color lighten">
<td>
<label class="list-action-trigger" data-list-action="select">
<input class="select-checkbox" type="checkbox">
<span class="disable-on-click"></span>
</label>
</td>
<td><a class="btn-floating service-color darken" data-service="inherit"><i class="nopaque-icons service-icons" data-service="inherit"></i></a></td>
<td><a class="btn-floating service-color darken"><i class="nopaque-icons service-icons"></i></a></td>
<td><b class="title"></b><br><i class="description"></i></td>
<td><span class="status badge new job-status-color job-status-text" data-badge-caption=""></span></td>
<td class="right-align">
<a class="list-action-trigger btn-floating red waves-effect waves-light" data-list-action="delete-request"><i class="material-icons">delete</i></a>
<a class="list-action-trigger btn-floating darken waves-effect waves-light" data-list-action="view"><i class="material-icons">send</i></a>
<a class="list-action-trigger btn-floating service-color darken waves-effect waves-light" data-list-action="view"><i class="material-icons">send</i></a>
</td>
</tr>
`.trim();

View File

@ -8,12 +8,10 @@ nopaque.resource_lists.JobResultList = class JobResultList extends nopaque.resou
this.userId = listContainerElement.dataset.userId;
this.jobId = listContainerElement.dataset.jobId;
if (this.userId === undefined || this.jobId === undefined) {return;}
app.subscribeUser(this.userId).then((response) => {
app.socket.on('patch_user', (patch) => {
if (this.isInitialized) {this.onPatch(patch);}
app.userHub.addEventListener('patch', (event) => {
if (this.isInitialized) {this.onPatch(event.detail);}
});
});
app.getUser(this.userId).then((user) => {
app.userHub.get(this.userId).then((user) => {
this.add(Object.values(user.jobs[this.jobId].results));
this.isInitialized = true;
});

View File

@ -8,12 +8,10 @@ nopaque.resource_lists.SpaCyNLPPipelineModelList = class SpaCyNLPPipelineModelLi
this.isInitialized = false;
this.userId = listContainerElement.dataset.userId;
if (this.userId === undefined) {return;}
app.subscribeUser(this.userId).then((response) => {
app.socket.on('patch_user', (patch) => {
if (this.isInitialized) {this.onPatch(patch);}
app.userHub.addEventListener('patch', (event) => {
if (this.isInitialized) {this.onPatch(event.detail);}
});
});
app.getUser(this.userId).then((user) => {
app.userHub.get(this.userId).then((user) => {
this.add(Object.values(user.spacy_nlp_pipeline_models));
this.isInitialized = true;
});

View File

@ -8,21 +8,11 @@ nopaque.resource_lists.TesseractOCRPipelineModelList = class TesseractOCRPipelin
this.isInitialized = false;
this.userId = listContainerElement.dataset.userId;
if (this.userId === undefined) {return;}
app.subscribeUser(this.userId).then((response) => {
app.socket.on('patch_user', (patch) => {
if (this.isInitialized) {this.onPatch(patch);}
app.userHub.addEventListener('patch', (event) => {
if (this.isInitialized) {this.onPatch(event.detail);}
});
});
app.getUser(this.userId).then((user) => {
app.userHub.get(this.userId).then((user) => {
this.add(Object.values(user.tesseract_ocr_pipeline_models));
for (let uncheckedCheckbox of this.listjs.list.querySelectorAll('input[data-checked="True"]')) {
uncheckedCheckbox.setAttribute('checked', '');
}
if (user.role.name !== ('Administrator' || 'Contributor')) {
for (let switchElement of this.listjs.list.querySelectorAll('.is_public')) {
switchElement.setAttribute('disabled', '');
}
}
this.isInitialized = true;
});
}

View File

@ -0,0 +1,60 @@
{% if current_user.is_authenticated %}
<ul class="dropdown-content" id="navbar-data-processing-and-analysis-dropdown-content">
<li {% if request.path == url_for('services.file_setup_pipeline') %}class="active"{% endif %}>
<a href="{{ url_for('services.file_setup_pipeline') }}">
<i class="nopaque-icons service-icons service-color-text text-darken" data-service="file-setup-pipeline"></i>
File Setup Pipeline
</a>
</li>
<li {% if request.path == url_for('services.tesseract_ocr_pipeline') %}class="active"{% endif %}>
<a href="{{ url_for('services.tesseract_ocr_pipeline') }}">
<i class="nopaque-icons service-icons service-color-text text-darken" data-service="tesseract-ocr-pipeline"></i>
Tesseract OCR Pipeline
</a>
</li>
{% if config.NOPAQUE_TRANSKRIBUS_ENABLED %}
<li {% if request.path == url_for('services.transkribus_htr_pipeline') %}class="active"{% endif %}>
<a href="{{ url_for('services.transkribus_htr_pipeline') }}">
<i class="nopaque-icons service-icons service-color-text text-darken" data-service="transkribus-htr-pipeline"></i>
Transkribus HTR Pipeline
</a>
</li>
{% endif %}
<li {% if request.path == url_for('services.spacy_nlp_pipeline') %}class="active"{% endif %}>
<a href="{{ url_for('services.spacy_nlp_pipeline') }}">
<i class="nopaque-icons service-icons service-color-text text-darken" data-service="spacy-nlp-pipeline"></i>
SpaCy NLP Pipeline
</a>
</li>
<li class="divider" tabindex="-1"></li>
<li {% if request.path == url_for('services.corpus_analysis') %}class="active"{% endif %}>
<a href="{{ url_for('services.corpus_analysis') }}">
<i class="nopaque-icons service-icons service-color-text text-darken" data-service="corpus-analysis"></i>
Corpus Analyis
</a>
</li>
</ul>
{% endif %}
{% if current_user.is_authenticated %}
<ul class="dropdown-content" id="navbar-account-dropdown-content">
<li {% if request.path == url_for('users.user', user_id=current_user.id) %}class="active"{% endif %}>
<a href="{{ url_for('users.user', user_id=current_user.id) }}">
<i class="material-icons">person</i>
Your profile
</a>
</li>
<li {% if request.path == url_for('settings.settings') %}class="active"{% endif %}>
<a href="{{ url_for('settings.settings') }}">
<i class="material-icons">settings</i>
Settings
</a>
</li>
<li>
<a href="{{ url_for('auth.logout') }}">
<i class="material-icons">logout</i>
Log out
</a>
</li>
</ul>
{% endif %}

View File

@ -0,0 +1,45 @@
<div class="container">
<div class="row">
<div class="col s12 l3">
<h5 class="white-text">Legal Notice</h5>
<ul>
<li><a class="grey-text text-lighten-3" href="https://www.uni-bielefeld.de/(en)/impressum/">Legal Notice</a></li>
<li><a class="grey-text text-lighten-3" href="{{ url_for('main.privacy_policy') }}">Privacy statement (GDPR)</a></li>
<li><a class="grey-text text-lighten-3" href="{{ url_for('main.terms_of_use') }}">Terms of use</a></li>
</ul>
</div>
<div class="col s12 l3">
<h5 class="white-text">More Resources</h5>
<ul>
<li><a class="grey-text text-lighten-3" href="{{ url_for('main.faq') }}">Frequently asked questions</a></li>
<li><a class="grey-text text-lighten-3" href="mailto:{{ config.NOPAQUE_SERVICE_DESK }}">Report an issue</a></li>
<li><a class="grey-text text-lighten-3" href="https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque">GitLab (source code)</a></li>
</ul>
</div>
<div class="col s12 l4">
<h5 class="white-text">Who made this?</h5>
<p class="grey-text text-lighten-4">
This software is developed by the SFB 1288 INF project at Bielefeld University.
Thanks to all the people who made nopaque possible.
<span class="red-text">&hearts;</span>
</p>
</div>
<div class="col s12 l2">
<br class="hide-on-med-and-down">
<br class="hide-on-med-and-down">
<a href="https://www.dfg.de/">
<img class="responsive-img" src="{{ url_for('static', filename='images/logo_-_dfg.gif') }}">
</a>
</div>
</div>
</div>
<div class="footer-copyright">
<div class="container">
© 2024 Bielefeld University
<a class="grey-text text-lighten-4 right" href="https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque/-/releases/{{ config.NOPAQUE_VERSION }}">Version {{ config.NOPAQUE_VERSION }}</a>
</div>
</div>

View File

@ -0,0 +1 @@
<link href="{{ url_for('static', filename='images/nopaque_-_favicon.png') }}" rel="icon">

View File

@ -0,0 +1,2 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">

View File

@ -0,0 +1,34 @@
{% if current_user.is_authenticated and not current_user.terms_of_use_accepted %}
<div id="terms-of-use-modal" class="modal modal-fixed-footer">
<div class="modal-content">
<div class="container">
<div class="row">
<div class="col s12">
<h1 id="title">Terms of use</h1>
</div>
<div class="col s12">
<div class="switch">
<label>
DE
<input type="checkbox" id="terms-of-use-modal-switch">
<span class="lever"></span>
EN
</label>
</div>
<br>
</div>
<div class="terms-of-use-modal-content hide">
{% include "main/terms_of_use_en.html.j2" %}
</div>
<div class="terms-of-use-modal-content">
{% include "main/terms_of_use_de.html.j2" %}
</div>
</div>
</div>
</div>
<div class="modal-footer">
<span style="margin-right:20px;">I have taken note of the new GTC and agree to their validity in the context of my further use.</span>
<a href="#!" class="modal-close waves-effect waves-green btn">Yes</a>
</div>
</div>
{% endif %}

View File

@ -0,0 +1,105 @@
<div class="navbar-fixed">
<nav>
<div class="nav-wrapper">
{# menu icon #}
{# small/medium devices #}
<a href="#!" class="sidenav-trigger" data-target="sidenav">
<i class="material-icons">menu</i>
</a>
{% if current_user.is_authenticated %}
{# nopaque logo #}
{# large devices #}
<a href="{{ url_for('main.index') }}" class="brand-logo hide-on-med-and-down h-100">
<img src="{{ url_for('static', filename='images/nopaque_-_logo.png') }}" alt="" class="mx-3 py-3 h-100">
</a>
{# left aligned navigation items #}
{# large devices #}
<ul class="hide-on-med-and-down" style="margin-left: calc(57px + 1.5rem);">
{# dashboard #}
<li {% if request.path == url_for('main.dashboard') %}class="active"{% endif %}>
<a href="{{ url_for('main.dashboard') }}">
<i class="material-icons left">dashboard</i>
Dashboard
</a>
</li>
{# data processing & analysis #}
<li>
<a href="#!" class="dropdown-trigger no-autoinit" data-target="navbar-data-processing-and-analysis-dropdown-content" id="navbar-data-processing-and-analysis-dropdown-trigger">
<i class="material-icons left">miscellaneous_services</i>
Data Processing & Analysis
</a>
</li>
{# contributions #}
<li {% if request.path == url_for('contributions.index') %}class="active"{% endif %}>
<a href="{{ url_for('contributions.index') }}">
<i class="material-icons left">new_label</i>
Contributions
</a>
</li>
{# social #}
<li {% if request.path == url_for('main.social') %}class="active"{% endif %}>
<a href="{{ url_for('main.social') }}">
<i class="material-icons left">groups</i>
Social
</a>
</li>
</ul>
{% else %}
{# nopaque logo+wordmark+slogan #}
{# large devices #}
<a href="{{ url_for('main.index') }}" class="brand-logo hide-on-med-and-down h-100">
<img src="{{ url_for('static', filename='images/nopaque_-_logo+wordmark+slogan.png') }}" alt="" class="mx-3 py-3 h-100">
</a>
{% endif %}
{# nopaque logo+wordmark #}
{# small/medium devices #}
<a href="{{ url_for('main.index') }}" class="brand-logo center hide-on-large-only h-100">
<img src="{{ url_for('static', filename='images/nopaque_-_logo+wordmark.png') }}" alt="" class="py-3 h-100">
</a>
{# right aligned navigation items #}
{# large devices #}
<ul class="right hide-on-med-and-down h-100">
{# manual #}
<li class="tooltipped {% if request.path == url_for('main.manual') %}active{% endif %}" data-position="bottom" data-tooltip="Manual">
<a href="{{ url_for('main.manual') }}">
<i class="material-icons">help_outline</i>
</a>
</li>
{# news #}
<li class="tooltipped {% if request.path == url_for('main.news') %}active{% endif %}" data-position="bottom" data-tooltip="News">
<a href="{{ url_for('main.news') }}">
<i class="material-icons">newspaper</i>
</a>
</li>
{% if current_user.is_authenticated %}
{# avatar #}
<li class="h-100">
<a href="#!" class="dropdown-trigger no-autoinit h-100" data-target="navbar-account-dropdown-content" id="navbar-account-dropdown-trigger">
<span class="mr-3">{{ current_user.username }}</span>
<img src="{{ url_for('users.user_avatar', user_id=current_user.id) }}" alt="" class="circle py-3 h-100 right">
</a>
</li>
{% else %}
{# log in #}
<li {% if request.path == url_for('auth.login') %}class="active"{% endif %}>
<a href="{{ url_for('auth.login') }}">Log in</a>
</li>
{# register #}
<li {% if request.path == url_for('auth.register') %}class="active"{% endif %}>
<a href="{{ url_for('auth.register') }}" class="btn waves-effect waves-light primary-color lighten">Register</a>
</li>
{% endif %}
</ul>
</div>
</nav>
</div>

View File

@ -0,0 +1,116 @@
<script src="{{ url_for('static', filename='external/materialize/js/materialize.min.js') }}"></script>
<script src="{{ url_for('static', filename='external/JSON-Patch/js/fast-json-patch.min.js') }}"></script>
<script src="{{ url_for('static', filename='external/list.js/js/list.min.js') }}"></script>
<script src="{{ url_for('static', filename='external/pako/js/pako_inflate.min.js') }}"></script>
<script src="{{ url_for('static', filename='external/plotly.js/js/plotly.min.js') }}"></script>
<script src="{{ url_for('static', filename='external/socket.io/js/socket.io.min.js') }}"></script>
{% assets
filters='rjsmin',
output='gen/nopaque.%(version)s.js',
'js/index.js',
'js/app.js',
'js/app/index.js',
'js/app/endpoints/index.js',
'js/app/endpoints/users.js',
'js/app/extensions/index.js',
'js/app/extensions/toaster.js',
'js/app/extensions/ui.js',
'js/app/extensions/user-hub.js',
'js/utils.js',
'js/forms/index.js',
'js/forms/base-form.js',
'js/forms/create-contribution-form.js',
'js/forms/create-corpus-file-form.js',
'js/forms/create-job-form.js',
'js/resource-displays/index.js',
'js/resource-displays/resource-display.js',
'js/resource-displays/corpus-display.js',
'js/resource-displays/job-display.js',
'js/resource-lists/index.js',
'js/resource-lists/resource-list.js',
'js/resource-lists/admin-user-list.js',
'js/resource-lists/corpus-file-list.js',
'js/resource-lists/corpus-follower-list.js',
'js/resource-lists/corpus-list.js',
'js/resource-lists/corpus-text-info-list.js',
'js/resource-lists/corpus-token-list.js',
'js/resource-lists/detailed-public-corpus-list.js',
'js/resource-lists/job-input-list.js',
'js/resource-lists/job-list.js',
'js/resource-lists/job-result-list.js',
'js/resource-lists/public-corpus-list.js',
'js/resource-lists/public-user-list.js',
'js/resource-lists/spacy-nlp-pipeline-model-list.js',
'js/resource-lists/tesseract-ocr-pipeline-model-list.js',
'js/requests/index.js',
'js/requests/admin.js',
'js/requests/contributions.js',
'js/requests/corpora.js',
'js/requests/jobs.js',
'js/requests/users.js',
'js/corpus-analysis/index.js',
'js/corpus-analysis/cqi/index.js',
'js/corpus-analysis/cqi/constants.js',
'js/corpus-analysis/cqi/errors.js',
'js/corpus-analysis/cqi/status.js',
'js/corpus-analysis/cqi/api/index.js',
'js/corpus-analysis/cqi/api/client.js',
'js/corpus-analysis/cqi/models/index.js',
'js/corpus-analysis/cqi/models/resource.js',
'js/corpus-analysis/cqi/models/attributes.js',
'js/corpus-analysis/cqi/models/subcorpora.js',
'js/corpus-analysis/cqi/models/corpora.js',
'js/corpus-analysis/cqi/client.js',
'js/corpus-analysis/query-builder/index.js',
'js/corpus-analysis/query-builder/element-references.js',
'js/corpus-analysis/query-builder/query-builder.js',
'js/corpus-analysis/query-builder/structural-attribute-builder-functions.js',
'js/corpus-analysis/query-builder/token-attribute-builder-functions.js',
'js/corpus-analysis/app.js',
'js/corpus-analysis/concordance-extension.js',
'js/corpus-analysis/reader-extension.js',
'js/corpus-analysis/static-visualization-extension.js'
-%}
<script src="{{ ASSET_URL }}"></script>
{% endassets -%}
<script>
// TODO: Implement an app.run method and use this for all of the following
const app = new nopaque.App();
app.init();
{% if current_user.is_authenticated %}
const currentUserId = {{ current_user.hashid|tojson }};
app.userHub.add(currentUserId)
.catch((error) => {throw JSON.stringify(error);});
{% if not current_user.terms_of_use_accepted %}
M.Modal.getInstance(document.querySelector('#terms-of-use-modal')).open();
{% endif %}
{% else %}
const currentUserId = null;
{% endif %}
// Display flashed messages
for (let [category, message] of {{ get_flashed_messages(with_categories=True)|tojson }}) {
app.ui.flash(message, message);
}
</script>
<script>
let languageModalSwitch = document.querySelector('#terms-of-use-modal-switch');
let termsOfUseModalContent = document.querySelectorAll('.terms-of-use-modal-content');
if (languageModalSwitch) {
languageModalSwitch.addEventListener('change', () => {
termsOfUseModalContent.forEach(content => {
content.classList.toggle('hide');
});
});
}
</script>

View File

@ -0,0 +1,123 @@
<ul class="sidenav" id="sidenav">
{% if current_user.is_authenticated %}
{# user view #}
<li>
<div class="user-view">
<div class="background primary-color"></div>
<a><img class="circle" src="{{ url_for('users.user_avatar', user_id=current_user.id) }}" alt=""></a>
<a><span class="white-text name">{{ current_user.username }}</span></a>
<a><span class="white-text email">{{ current_user.email }}</span></a>
</div>
</li>
{% endif %}
{% if current_user.is_authenticated %}
{# dashboard #}
<li {% if request.path == url_for('main.dashboard') %}class="active"{% endif %}>
<a class="waves-effect" href="{{ url_for('main.dashboard') }}"><i class="material-icons">dashboard</i>Dashboard</a>
</li>
{# contributions #}
<li {% if request.path == url_for('contributions.index') %}class="active"{% endif %}>
<a href="{{ url_for('contributions.index') }}">
<i class="material-icons left">new_label</i>
Contributions
</a>
</li>
{# social #}
<li {% if request.path == url_for('main.social') %}class="active"{% endif %}>
<a class="waves-effect" href="{{ url_for('main.social') }}"><i class="material-icons">groups</i>Social</a>
</li>
{% endif %}
{# news #}
<li {% if request.path == url_for('main.news') %}class="active"{% endif %}>
<a class="waves-effect" href="{{ url_for('main.news') }}"><i class="material-icons">newspaper</i>News</a>
</li>
{# manual #}
<li {% if request.path == url_for('main.manual') %}class="active"{% endif %}>
<a class="waves-effect" href="{{ url_for('main.manual') }}"><i class="material-icons">help_outline</i>Manual</a>
</li>
{% if current_user.is_authenticated %}
{# data processing & analysis section #}
<li><div class="divider"></div></li>
<li><a class="subheader">Data Processing & Analysis</a></li>
{# file setup pipeline #}
<li class="service-color service-color-border border-darken" data-service="file-setup-pipeline" style="border-left: 10px solid;">
<a class="waves-effect" href="{{ url_for('services.file_setup_pipeline') }}"><i class="nopaque-icons service-icons" data-service="file-setup-pipeline"></i>File setup</a>
</li>
{# tesseract ocr pipeline #}
<li class="service-color service-color-border border-darken mt-1" data-service="tesseract-ocr-pipeline" style="border-left: 10px solid;">
<a class="waves-effect" href="{{ url_for('services.tesseract_ocr_pipeline') }}"><i class="nopaque-icons service-icons" data-service="tesseract-ocr-pipeline"></i>OCR</a>
</li>
{% if config.NOPAQUE_TRANSKRIBUS_ENABLED %}
{# transkribus htr pipeline #}
<li class="service-color service-color-border border-darken mt-1" data-service="transkribus-htr-pipeline" style="border-left: 10px solid;">
<a class="waves-effect" href="{{ url_for('services.transkribus_htr_pipeline') }}"><i class="nopaque-icons service-icons" data-service="transkribus-htr-pipeline"></i>HTR</a>
</li>
{% endif %}
{# spacy nlp pipeline #}
<li class="service-color service-color-border border-darken mt-1" data-service="spacy-nlp-pipeline" style="border-left: 10px solid;">
<a class="waves-effect" href="{{ url_for('services.spacy_nlp_pipeline') }}"><i class="nopaque-icons service-icons" data-service="spacy-nlp-pipeline"></i>NLP</a>
</li>
{# corpus analysis #}
<li class="service-color service-color-border border-darken mt-1" data-service="corpus-analysis" style="border-left: 10px solid;">
<a class="waves-effect" href="{{ url_for('services.corpus_analysis') }}"><i class="nopaque-icons service-icons" data-service="corpus-analysis"></i>Corpus Analysis</a>
</li>
{% endif %}
{# account section #}
<li><div class="divider"></div></li>
<li><a class="subheader">Account</a></li>
{% if current_user.is_authenticated %}
{# my profile #}
<li {% if request.path == url_for('users.user', user_id=current_user.id) %}class="active"{% endif %}>
<a href="{{ url_for('users.user', user_id=current_user.id) }}"><i class="material-icons">person</i>My Profile</a>
</li>
{# settings #}
<li {% if request.path == url_for('settings.settings') %}class="active"{% endif %}>
<a class="waves-effect" href="{{ url_for('settings.settings') }}"><i class="material-icons">settings</i>Settings</a>
</li>
{# log out #}
<li>
<a class="waves-effect" href="{{ url_for('auth.logout') }}"><i class="material-icons">logout</i>Log out</a>
</li>
{% else %}
{# log in #}
<li {% if request.path == url_for('auth.login') %}class="active"{% endif %}>
<a href="{{ url_for('auth.login') }}">Log in</a>
</li>
{# register #}
<li {% if request.path == url_for('auth.register') %}class="active"{% endif %}>
<a href="{{ url_for('auth.register') }}" class="btn waves-effect waves-light">Register</a>
</li>
{% endif %}
{% if current_user.is_authenticated and current_user.can('ADMINISTRATE') %}
{# administration section #}
<li><div class="divider"></div></li>
<li><a class="subheader">Administration</a></li>
{# corpora #}
<li>
<a class="waves-effect" href="{{ url_for('admin.corpora') }}"><i class="nopaque-icons">I</i>Corpora</a>
</li>
{# users #}
<li>
<a class="waves-effect" href="{{ url_for('admin.users') }}"><i class="material-icons">manage_accounts</i>Users</a>
</li>
{% endif %}
</ul>

View File

@ -0,0 +1,23 @@
<link href="{{ url_for('static', filename='external/material-design-icons/css/material-icons.css') }}" rel="stylesheet">
{% assets
output='gen/nopaque.%(version)s.css',
'css/materialize.css',
'css/materialize.override.css',
'css/nopaque-icons.css',
'css/theme-colors.css',
'css/corpus-status-colors.css',
'css/corpus-status-text.css',
'css/height.css',
'css/job-status-colors.css',
'css/job-status-text.css',
'css/service-colors.css',
'css/pagination.css',
'css/service-icons.css',
'css/s-attr-colors.css',
'css/spacing.css',
'css/status-spinner.css',
'css/utils.css',
'css/width.css'
%}
<link href="{{ ASSET_URL }}" rel="stylesheet">
{% endassets %}

View File

@ -5,7 +5,7 @@
{% block page_content %}
<div class="container">
<div class="row">
<div class="col s12 m8 offset-m2">
<div class="col s12 l8 offset-l2">
<h1 id="title">{{ title }}</h1>
<p>Want to boost your research and get going? Nopaque is free and no download is needed. <a href="{{ url_for('.register') }}">Register now</a>!</p>
@ -15,14 +15,14 @@
{{ wtf.render_field(form.user, material_icon='person') }}
{{ wtf.render_field(form.password, material_icon='vpn_key') }}
<div class="row">
<div class="col s6 left-align">
<a href="{{ url_for('.reset_password_request') }}">Forgot your password?</a>
</div>
<div class="col s6 right-align">
<div class="col s12 l6">
{{ wtf.render_field(form.remember_me) }}
</div>
<div class="col s12 l6 right-align">
<a class="mr-3" href="{{ url_for('.reset_password_request') }}">Forgot your password?</a>
{{ wtf.render_field(form.submit, material_icon='send') }}
</div>
</div>
{{ wtf.render_field(form.submit, material_icon='send', class_='width-100') }}
</div>
</form>
</div>

View File

@ -5,7 +5,7 @@
{% block page_content %}
<div class="container">
<div class="row">
<div class="col s12 m8 offset-m2">
<div class="col s12 l8 offset-l2">
<h1 id="title">{{ title }}</h1>
<p>
Simply enter a username and password to receive your registration email.
@ -22,12 +22,15 @@
{{ wtf.render_field(form.username, material_icon='person') }}
{{ wtf.render_field(form.password, material_icon='vpn_key') }}
{{ wtf.render_field(form.password_2, material_icon='vpn_key') }}
{{ wtf.render_field(form.email, class_='validate', material_icon='email', type='email') }}
<br>
{{ wtf.render_field(form.terms_of_use_accepted, type='checkbox')}}
<p></p>
<br>
{{ wtf.render_field(form.submit, class_='width-100', material_icon='send') }}
{{ wtf.render_field(form.email, material_icon='email', type='email') }}
<div class="row">
<div class="col s12 l6">
{{ wtf.render_field(form.terms_of_use_accepted)}}
</div>
<div class="col s12 l6 right-align">
{{ wtf.render_field(form.submit, material_icon='send') }}
</div>
</div>
</div>
</form>
</div>

View File

@ -13,7 +13,9 @@
{{ form.hidden_tag() }}
{{ wtf.render_field(form.password) }}
{{ wtf.render_field(form.password_2) }}
{{ wtf.render_field(form.submit, class_='width-100', material_icon='send') }}
<div class="right-align">
{{ wtf.render_field(form.submit, material_icon='send') }}
</div>
</div>
</form>
</div>

View File

@ -4,15 +4,17 @@
{% block page_content %}
<div class="container">
<div class="row">
<div class="col s12 m8 offset-m2">
<div class="col s12 l8 offset-l2">
<h1 id="title">{{ title }}</h1>
<p>After entering your email address you will receive instructions on how to reset your password.</p>
<form method="POST">
<div class="card-panel">
{{ form.hidden_tag() }}
{{ wtf.render_field(form.email, class_='validate', material_icon='email', type='email') }}
{{ wtf.render_field(form.submit, class_='width-100', material_icon='send') }}
{{ wtf.render_field(form.email, material_icon='email', type='email') }}
<div class="right-align">
{{ wtf.render_field(form.submit, material_icon='send') }}
</div>
</div>
</form>
</div>

View File

@ -1,544 +1,75 @@
{% if title is not defined %}
{% set title = 'nopaque' %}
{% endif %}
{% block doc %}
<!DOCTYPE html>
<html lang="en">
<head>
<!-- #region meta -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- #endregion meta -->
<html {% block html_attributes %}lang="en"{% endblock html_attributes %}>
{% block html %}
<head {% block head_attributes %}{% endblock head_attributes %}>
{% block head %}
{% block metas %}
{% include '_base/metas.html.j2' %}
{% endblock metas %}
<title>{{ title if title is defined else 'nopaque' }}</title>
<title {% block title_attribs %}{% endblock title_attribs %}>
{% block title %}
{{ title }}
{% endblock title %}
</title>
<link href="{{ url_for('static', filename='images/nopaque_-_favicon.png') }}" rel="icon">
{% block icons %}
{% include '_base/icons.html.j2' %}
{% endblock icons %}
<!-- #region stylesheets -->
{% block stylesheets %}
<link href="{{ url_for('static', filename='external/material-design-icons/css/material-icons.css') }}" rel="stylesheet">
{% assets
output='gen/nopaque.%(version)s.css',
'css/materialize.css',
'css/materialize.override.css',
'css/nopaque-icons.css',
'css/theme-colors.css',
'css/corpus-status-colors.css',
'css/corpus-status-text.css',
'css/job-status-colors.css',
'css/job-status-text.css',
'css/service-colors.css',
'css/pagination.css',
'css/service-icons.css',
'css/s-attr-colors.css',
'css/spacing.css',
'css/status-spinner.css',
'css/utils.css'
-%}
<link href="{{ ASSET_URL }}" rel="stylesheet">
{% endassets -%}
{% endblock stylesheets %}
<!-- #endregion stylesheets -->
{% block styles %}
{% include '_base/stylesheets.html.j2' %}
{% endblock styles %}
{% endblock head %}
</head>
<body>
<header>
<div class="navbar-fixed">
<nav>
<div class="nav-wrapper">
{# menu icon #}
{# small/medium devices #}
<a href="#!" class="sidenav-trigger" data-target="sidenav"><i class="material-icons">menu</i></a>
<body {% block body_attributes %}{% endblock body_attributes %}>
{% block body %}
<header {% block header_attributes %}{% endblock header_attributes %}>
{% block header %}
{% block navbar %}
{% include '_base/navbar.html.j2' %}
{% endblock navbar %}
{% if current_user.is_authenticated %}
{# nopaque logo #}
{# large devices #}
<a href="{{ url_for('main.index') }}" class="brand-logo hide-on-med-and-down" style="height: 100%;">
<img src="{{ url_for('static', filename='images/nopaque_-_logo.png') }}" alt="" class="mx-3 py-3" style="height: 100%;">
</a>
{# left aligned navigation items #}
{# large devices #}
<ul class="hide-on-med-and-down" style="margin-left: calc(57px + 1.5rem);">
{# dashboard #}
<li {% if request.path == url_for('main.dashboard') %}class="active"{% endif %}>
<a href="{{ url_for('main.dashboard') }}">
<i class="material-icons left">dashboard</i>
Dashboard
</a>
</li>
{# data processing & analysis #}
<li>
<a href="#!" class="dropdown-trigger no-autoinit" data-target="navbar-data-processing-and-analysis-dropdown-content" id="navbar-data-processing-and-analysis-dropdown-trigger">
<i class="material-icons left">miscellaneous_services</i>
Data Processing & Analysis
</a>
</li>
{# contributions #}
<li {% if request.path == url_for('contributions.index') %}class="active"{% endif %}>
<a href="{{ url_for('contributions.index') }}">
<i class="material-icons left">new_label</i>
Contributions
</a>
</li>
{# social #}
<li {% if request.path == url_for('main.social') %}class="active"{% endif %}>
<a href="{{ url_for('main.social') }}">
<i class="material-icons left">groups</i>
Social
</a>
</li>
</ul>
{% else %}
{# nopaque logo+wordmark+slogan #}
{# large devices #}
<a href="{{ url_for('main.index') }}" class="brand-logo hide-on-med-and-down" style="height: 100%;">
<img src="{{ url_for('static', filename='images/nopaque_-_logo+wordmark+slogan.png') }}" alt="" class="mx-3 py-3" style="height: 100%;">
</a>
{% endif %}
{# nopaque logo+wordmark #}
{# small/medium devices #}
<a href="{{ url_for('main.index') }}" class="brand-logo center hide-on-large-only" style="height: 100%;">
<img src="{{ url_for('static', filename='images/nopaque_-_logo+wordmark.png') }}" alt="" class="py-3" style="height: 100%;">
</a>
{# right aligned navigation items #}
{# large devices #}
<ul class="right hide-on-med-and-down" style="height: 64px;">
{# manual #}
<li class="tooltipped {% if request.path == url_for('main.manual') %}active{% endif %}" data-position="bottom" data-tooltip="Manual">
<a href="{{ url_for('main.manual') }}">
<i class="material-icons">help_outline</i>
</a>
</li>
{# news #}
<li class="tooltipped {% if request.path == url_for('main.news') %}active{% endif %}" data-position="bottom" data-tooltip="News">
<a href="{{ url_for('main.news') }}">
<i class="material-icons">newspaper</i>
</a>
</li>
{% if current_user.is_authenticated %}
{# avatar #}
<li style="height: 100%;">
<a href="#!" class="dropdown-trigger no-autoinit" data-target="navbar-account-dropdown-content" id="navbar-account-dropdown-trigger" style="height: 100%;">
<span class="mr-3">{{ current_user.username }}</span>
<img src="{{ url_for('users.user_avatar', user_id=current_user.id) }}" alt="" class="circle py-3 right" style="height: 100%;">
</a>
</li>
{% else %}
{# log in #}
<li {% if request.path == url_for('auth.login') %}class="active"{% endif %}>
<a href="{{ url_for('auth.login') }}">Log in</a>
</li>
{# register #}
<li {% if request.path == url_for('auth.register') %}class="active"{% endif %}>
<a href="{{ url_for('auth.register') }}" class="btn waves-effect waves-light primary-color lighten">Register</a>
</li>
{% endif %}
</ul>
</div>
</nav>
</div>
<ul class="sidenav" id="sidenav">
{% if current_user.is_authenticated %}
{# user view #}
<li>
<div class="user-view">
<div class="background primary-color"></div>
<a><img class="circle" src="{{ url_for('users.user_avatar', user_id=current_user.id) }}" alt=""></a>
<a><span class="white-text name">{{ current_user.username }}</span></a>
<a><span class="white-text email">{{ current_user.email }}</span></a>
</div>
</li>
{% endif %}
{% if current_user.is_authenticated %}
{# dashboard #}
<li {% if request.path == url_for('main.dashboard') %}class="active"{% endif %}>
<a class="waves-effect" href="{{ url_for('main.dashboard') }}"><i class="material-icons">dashboard</i>Dashboard</a>
</li>
{# contributions #}
<li {% if request.path == url_for('contributions.index') %}class="active"{% endif %}>
<a href="{{ url_for('contributions.index') }}">
<i class="material-icons left">new_label</i>
Contributions
</a>
</li>
{# social #}
<li {% if request.path == url_for('main.social') %}class="active"{% endif %}>
<a class="waves-effect" href="{{ url_for('main.social') }}"><i class="material-icons">groups</i>Social</a>
</li>
{% endif %}
{# news #}
<li {% if request.path == url_for('main.news') %}class="active"{% endif %}>
<a class="waves-effect" href="{{ url_for('main.news') }}"><i class="material-icons">newspaper</i>News</a>
</li>
{# manual #}
<li {% if request.path == url_for('main.manual') %}class="active"{% endif %}>
<a class="waves-effect" href="{{ url_for('main.manual') }}"><i class="material-icons">help_outline</i>Manual</a>
</li>
{% if current_user.is_authenticated %}
{# data processing & analysis section #}
<li><div class="divider"></div></li>
<li><a class="subheader">Data Processing & Analysis</a></li>
{# file setup pipeline #}
<li class="service-color service-color-border border-darken" data-service="file-setup-pipeline" style="border-left: 10px solid;">
<a class="waves-effect" href="{{ url_for('services.file_setup_pipeline') }}"><i class="nopaque-icons service-icons" data-service="file-setup-pipeline"></i>File setup</a>
</li>
{# tesseract ocr pipeline #}
<li class="service-color service-color-border border-darken mt-1" data-service="tesseract-ocr-pipeline" style="border-left: 10px solid;">
<a class="waves-effect" href="{{ url_for('services.tesseract_ocr_pipeline') }}"><i class="nopaque-icons service-icons" data-service="tesseract-ocr-pipeline"></i>OCR</a>
</li>
{% if config.NOPAQUE_TRANSKRIBUS_ENABLED %}
{# transkribus htr pipeline #}
<li class="service-color service-color-border border-darken mt-1" data-service="transkribus-htr-pipeline" style="border-left: 10px solid;">
<a class="waves-effect" href="{{ url_for('services.transkribus_htr_pipeline') }}"><i class="nopaque-icons service-icons" data-service="transkribus-htr-pipeline"></i>HTR</a>
</li>
{% endif %}
{# spacy nlp pipeline #}
<li class="service-color service-color-border border-darken mt-1" data-service="spacy-nlp-pipeline" style="border-left: 10px solid;">
<a class="waves-effect" href="{{ url_for('services.spacy_nlp_pipeline') }}"><i class="nopaque-icons service-icons" data-service="spacy-nlp-pipeline"></i>NLP</a>
</li>
{# corpus analysis #}
<li class="service-color service-color-border border-darken mt-1" data-service="corpus-analysis" style="border-left: 10px solid;">
<a class="waves-effect" href="{{ url_for('services.corpus_analysis') }}"><i class="nopaque-icons service-icons" data-service="corpus-analysis"></i>Corpus Analysis</a>
</li>
{% endif %}
{# account section #}
<li><div class="divider"></div></li>
<li><a class="subheader">Account</a></li>
{% if current_user.is_authenticated %}
{# my profile #}
<li {% if request.path == url_for('users.user', user_id=current_user.id) %}class="active"{% endif %}>
<a href="{{ url_for('users.user', user_id=current_user.id) }}"><i class="material-icons">person</i>My Profile</a>
</li>
{# settings #}
<li {% if request.path == url_for('settings.settings') %}class="active"{% endif %}>
<a class="waves-effect" href="{{ url_for('settings.settings') }}"><i class="material-icons">settings</i>Settings</a>
</li>
{# log out #}
<li>
<a class="waves-effect" href="{{ url_for('auth.logout') }}"><i class="material-icons">logout</i>Log out</a>
</li>
{% else %}
{# log in #}
<li {% if request.path == url_for('auth.login') %}class="active"{% endif %}>
<a href="{{ url_for('auth.login') }}">Log in</a>
</li>
{# register #}
<li {% if request.path == url_for('auth.register') %}class="active"{% endif %}>
<a href="{{ url_for('auth.register') }}" class="btn waves-effect waves-light">Register</a>
</li>
{% endif %}
{% if current_user.is_authenticated and current_user.can('ADMINISTRATE') %}
{# administration section #}
<li><div class="divider"></div></li>
<li><a class="subheader">Administration</a></li>
{# corpora #}
<li>
<a class="waves-effect" href="{{ url_for('admin.corpora') }}"><i class="nopaque-icons">I</i>Corpora</a>
</li>
{# users #}
<li>
<a class="waves-effect" href="{{ url_for('admin.users') }}"><i class="material-icons">manage_accounts</i>Users</a>
</li>
{% endif %}
</ul>
{% block sidenav %}
{% include '_base/sidenav.html.j2' %}
{% endblock sidenav %}
{% endblock header %}
</header>
<main {% block main_attribs %}{% endblock main_attribs %}>
<main {% block main_attributes %}{% endblock main_attributes %}>
{% block main %}
{% block page_content %}{% endblock page_content %}
{% endblock main %}
</main>
<footer class="page-footer">
<div class="container">
<div class="row">
<div class="col s12 l3">
<h5 class="white-text">Legal Notice</h5>
<ul>
<li><a class="grey-text text-lighten-3" href="https://www.uni-bielefeld.de/(en)/impressum/">Legal Notice</a></li>
<li><a class="grey-text text-lighten-3" href="{{ url_for('main.privacy_policy') }}">Privacy statement (GDPR)</a></li>
<li><a class="grey-text text-lighten-3" href="{{ url_for('main.terms_of_use') }}">Terms of use</a></li>
</ul>
</div>
<div class="col s12 l3">
<h5 class="white-text">More Resources</h5>
<ul>
<li><a class="grey-text text-lighten-3" href="{{ url_for('main.faq') }}">Frequently asked questions</a></li>
<li><a class="grey-text text-lighten-3" href="mailto:{{ config.NOPAQUE_SERVICE_DESK }}">Report an issue</a></li>
<li><a class="grey-text text-lighten-3" href="https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque">GitLab (source code)</a></li>
</ul>
</div>
<div class="col s12 l4">
<h5 class="white-text">Who made this?</h5>
<p class="grey-text text-lighten-4">
This software is developed by the SFB 1288 INF project at Bielefeld University.
Thanks to all the people who made nopaque possible.
<span class="red-text">&hearts;</span>
</p>
</div>
<div class="col s12 l2">
<br class="hide-on-med-and-down">
<br class="hide-on-med-and-down">
<a href="https://www.dfg.de/">
<img class="responsive-img" src="{{ url_for('static', filename='images/logo_-_dfg.gif') }}">
</a>
</div>
</div>
</div>
<div class="footer-copyright">
<div class="container">
© 2024 Bielefeld University
<a class="grey-text text-lighten-4 right" href="https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque/-/releases/{{ config.NOPAQUE_VERSION }}">Version {{ config.NOPAQUE_VERSION }}</a>
</div>
</div>
<footer {% block footer_attributes %}class="page-footer"{% endblock footer_attributes %}>
{% block footer %}
{% include '_base/footer.html.j2' %}
{% endblock footer %}
</footer>
<div id="dropdowns">
<div {% block dropdowns_attributes %}id="dropdowns"{% endblock dropdowns_attributes %}>
{% block dropdowns %}
{% if current_user.is_authenticated %}
<ul class="dropdown-content" id="navbar-data-processing-and-analysis-dropdown-content">
<li {% if request.path == url_for('services.file_setup_pipeline') %}class="active"{% endif %}>
<a href="{{ url_for('services.file_setup_pipeline') }}">
<i class="nopaque-icons service-icons service-color-text text-darken" data-service="file-setup-pipeline"></i>
File Setup Pipeline
</a>
</li>
<li {% if request.path == url_for('services.tesseract_ocr_pipeline') %}class="active"{% endif %}>
<a href="{{ url_for('services.tesseract_ocr_pipeline') }}">
<i class="nopaque-icons service-icons service-color-text text-darken" data-service="tesseract-ocr-pipeline"></i>
Tesseract OCR Pipeline
</a>
</li>
{% if config.NOPAQUE_TRANSKRIBUS_ENABLED %}
<li {% if request.path == url_for('services.transkribus_htr_pipeline') %}class="active"{% endif %}>
<a href="{{ url_for('services.transkribus_htr_pipeline') }}">
<i class="nopaque-icons service-icons service-color-text text-darken" data-service="transkribus-htr-pipeline"></i>
Transkribus HTR Pipeline
</a>
</li>
{% endif %}
<li {% if request.path == url_for('services.spacy_nlp_pipeline') %}class="active"{% endif %}>
<a href="{{ url_for('services.spacy_nlp_pipeline') }}">
<i class="nopaque-icons service-icons service-color-text text-darken" data-service="spacy-nlp-pipeline"></i>
SpaCy NLP Pipeline
</a>
</li>
<li class="divider" tabindex="-1"></li>
<li {% if request.path == url_for('services.corpus_analysis') %}class="active"{% endif %}>
<a href="{{ url_for('services.corpus_analysis') }}">
<i class="nopaque-icons service-icons service-color-text text-darken" data-service="corpus-analysis"></i>
Corpus Analyis
</a>
</li>
</ul>
{% endif %}
{% if current_user.is_authenticated %}
<ul class="dropdown-content" id="navbar-account-dropdown-content">
<li {% if request.path == url_for('users.user', user_id=current_user.id) %}class="active"{% endif %}>
<a href="{{ url_for('users.user', user_id=current_user.id) }}">
<i class="material-icons">person</i>
Your profile
</a>
</li>
<li {% if request.path == url_for('settings.settings') %}class="active"{% endif %}>
<a href="{{ url_for('settings.settings') }}">
<i class="material-icons">settings</i>
Settings
</a>
</li>
<li>
<a href="{{ url_for('auth.logout') }}">
<i class="material-icons">logout</i>
Log out
</a>
</li>
</ul>
{% endif %}
{% include '_base/dropdowns.html.j2' %}
{% endblock dropdowns %}
</div>
<div id="modals">
<div {% block modals_attributes %}id="modals"{% endblock modals_attributes %}>
{% block modals %}
{% if current_user.is_authenticated and not current_user.terms_of_use_accepted %}
<div id="terms-of-use-modal" class="modal modal-fixed-footer">
<div class="modal-content">
<div class="container">
<div class="row">
<div class="col s12">
<h1 id="title">Terms of use</h1>
</div>
<div class="col s12">
<div class="switch">
<label>
DE
<input type="checkbox" id="terms-of-use-modal-switch">
<span class="lever"></span>
EN
</label>
</div>
<br>
</div>
<div class="terms-of-use-modal-content hide">
{% include "main/terms_of_use_en.html.j2" %}
</div>
<div class="terms-of-use-modal-content">
{% include "main/terms_of_use_de.html.j2" %}
</div>
</div>
</div>
</div>
<div class="modal-footer">
<span style="margin-right:20px;">I have taken note of the new GTC and agree to their validity in the context of my further use.</span>
<a href="#!" class="modal-close waves-effect waves-green btn">Yes</a>
</div>
</div>
{% endif %}
{% include '_base/modals.html.j2' %}
{% endblock modals %}
</div>
<!-- #region scripts -->
{% block scripts %}
<script src="{{ url_for('static', filename='external/materialize/js/materialize.min.js') }}"></script>
<script src="{{ url_for('static', filename='external/JSON-Patch/js/fast-json-patch.min.js') }}"></script>
<script src="{{ url_for('static', filename='external/list.js/js/list.min.js') }}"></script>
<script src="{{ url_for('static', filename='external/pako/js/pako_inflate.min.js') }}"></script>
<script src="{{ url_for('static', filename='external/plotly.js/js/plotly.min.js') }}"></script>
<script src="{{ url_for('static', filename='external/socket.io/js/socket.io.min.js') }}"></script>
{% assets
filters='rjsmin',
output='gen/nopaque.%(version)s.js',
'js/index.js',
'js/app.js',
'js/utils.js',
'js/forms/index.js',
'js/forms/base-form.js',
'js/forms/create-contribution-form.js',
'js/forms/create-corpus-file-form.js',
'js/forms/create-job-form.js',
'js/resource-displays/index.js',
'js/resource-displays/resource-display.js',
'js/resource-displays/corpus-display.js',
'js/resource-displays/job-display.js',
'js/resource-lists/index.js',
'js/resource-lists/resource-list.js',
'js/resource-lists/admin-user-list.js',
'js/resource-lists/corpus-file-list.js',
'js/resource-lists/corpus-follower-list.js',
'js/resource-lists/corpus-list.js',
'js/resource-lists/corpus-text-info-list.js',
'js/resource-lists/corpus-token-list.js',
'js/resource-lists/detailed-public-corpus-list.js',
'js/resource-lists/job-input-list.js',
'js/resource-lists/job-list.js',
'js/resource-lists/job-result-list.js',
'js/resource-lists/public-corpus-list.js',
'js/resource-lists/public-user-list.js',
'js/resource-lists/spacy-nlp-pipeline-model-list.js',
'js/resource-lists/tesseract-ocr-pipeline-model-list.js',
'js/requests/index.js',
'js/requests/admin.js',
'js/requests/contributions.js',
'js/requests/corpora.js',
'js/requests/jobs.js',
'js/requests/users.js',
'js/corpus-analysis/index.js',
'js/corpus-analysis/cqi/index.js',
'js/corpus-analysis/cqi/constants.js',
'js/corpus-analysis/cqi/errors.js',
'js/corpus-analysis/cqi/status.js',
'js/corpus-analysis/cqi/api/index.js',
'js/corpus-analysis/cqi/api/client.js',
'js/corpus-analysis/cqi/models/index.js',
'js/corpus-analysis/cqi/models/resource.js',
'js/corpus-analysis/cqi/models/attributes.js',
'js/corpus-analysis/cqi/models/subcorpora.js',
'js/corpus-analysis/cqi/models/corpora.js',
'js/corpus-analysis/cqi/client.js',
'js/corpus-analysis/query-builder/index.js',
'js/corpus-analysis/query-builder/element-references.js',
'js/corpus-analysis/query-builder/query-builder.js',
'js/corpus-analysis/query-builder/structural-attribute-builder-functions.js',
'js/corpus-analysis/query-builder/token-attribute-builder-functions.js',
'js/corpus-analysis/app.js',
'js/corpus-analysis/concordance-extension.js',
'js/corpus-analysis/reader-extension.js',
'js/corpus-analysis/static-visualization-extension.js'
-%}
<script src="{{ ASSET_URL }}"></script>
{% endassets -%}
<script>
// TODO: Implement an app.run method and use this for all of the following
const app = new nopaque.App();
app.init();
{% if current_user.is_authenticated -%}
// TODO: Set this as a property of the app object
const currentUserId = {{ current_user.hashid|tojson }};
// Subscribe to the current user's data events
app.subscribeUser(currentUserId)
.catch((error) => {throw JSON.stringify(error);});
// Get the current user's data
app.getUser(currentUserId, true, true)
.catch((error) => {throw JSON.stringify(error);});
{% if not current_user.terms_of_use_accepted -%}
M.Modal.getInstance(document.querySelector('#terms-of-use-modal')).open();
{% endif -%}
{% endif -%}
// Display flashed messages
for (let [category, message] of {{ get_flashed_messages(with_categories=True)|tojson }}) {
app.flash(message, message);
}
</script>
<script>
let languageModalSwitch = document.querySelector('#terms-of-use-modal-switch');
let termsOfUseModalContent = document.querySelectorAll('.terms-of-use-modal-content');
if (languageModalSwitch) {
languageModalSwitch.addEventListener('change', function() {
termsOfUseModalContent.forEach(content => {
content.classList.toggle('hide');
});
});
}
</script>
{% include '_base/scripts.html.j2' %}
{% endblock scripts %}
<!-- #endregion scripts -->
{% endblock body %}
</body>
{% endblock html %}
</html>
{% endblock doc %}

View File

@ -14,8 +14,11 @@
{% endblock stylesheets %}
{% block navbar_secondary_content %}
<ul class="tabs tabs-transparent no-autoinit" id="corpus-analysis-extension-tabs">
{% block main_attributes %}class="service-color lighten" data-service="corpus-analysis" id="corpus-analysis-container"{% endblock main_attributes %}
{% block page_content %}
<ul class="tabs no-autoinit" id="corpus-analysis-extension-tabs">
<li class="tab">
<a class="active" href="#corpus-analysis-home-container"><i class="nopaque-icons service-icons left" data-service="corpus-analysis" style="line-height: inherit;"></i>Corpus analysis</a>
</li>
@ -26,13 +29,7 @@
<a href="#corpus-analysis-reader-container"><i class="material-icons left" style="line-height: inherit;">{{ reader_extension.icon }}</i>{{ reader_extension.name }}</a>
</li>
</ul>
{% endblock navbar_secondary_content %}
{% block main_attribs %} class="service-color lighten" data-service="corpus-analysis" id="corpus-analysis-container" style="margin-top: 48px;"{% endblock main_attribs %}
{% block page_content %}
<div id="corpus-analysis-home-container">
<h1>{{ title }}</h1>
@ -72,10 +69,9 @@
{{ super() }}
<div class="modal no-autoinit" id="corpus-analysis-init-modal">
<div class="modal-content">
<div class="card-panel service-color darken white-text" data-service="corpus-analysis">
<div class="card-panel primary-color white-text" data-service="corpus-analysis">
<h4 class="m-3"><i class="material-icons left" style="font-size: inherit; line-height: inherit;">hourglass_empty</i>We are preparing your analysis session</h4>
</div>
<h4>We are preparing your analysis session</h4>
<p>
Our server works as hard as it can to prepare your analysis session. Please be patient and give it some time.<br>
If initialization takes longer than usual or an error occurs, <a onclick="window.location.reload()" href="#">reload the page</a>.

View File

@ -8,7 +8,7 @@
{% endblock stylesheets %}
{% block main_attribs %} class="service-color lighten" data-service="corpus-analysis"{% endblock main_attribs %}
{% block main_attributes %} class="service-color lighten" data-service="corpus-analysis"{% endblock main_attributes %}
{% block page_content %}
<div class="container">
@ -364,8 +364,8 @@ shareLinkModalCreateButtonElement.addEventListener('click', (event) => {
shareLinkModalOutputCopyButtonElement.addEventListener('click', (event) => {
navigator.clipboard.writeText(shareLinkModalOutputFieldElement.value)
.then(
() => {app.flash('Copied!');},
() => {app.flash('Could not copy to clipboard. Please copy manually.', 'error');}
() => {app.ui.flash('Copied!');},
() => {app.ui.flash('Could not copy to clipboard. Please copy manually.', 'error');}
);
});

View File

@ -1,7 +1,7 @@
{% extends "base.html.j2" %}
{% import "wtf.html.j2" as wtf %}
{% block main_attribs %} class="service-scheme" data-service="corpus-analysis"{% endblock main_attribs %}
{% block main_attributes %}class="service-color lighten" data-service="corpus-analysis"{% endblock main_attributes %}
{% block page_content %}
<div class="container">

View File

@ -1,7 +1,7 @@
{% extends "base.html.j2" %}
{% import "wtf.html.j2" as wtf %}
{% block main_attribs %} class="service-scheme" data-service="corpus-analysis"{% endblock main_attribs %}
{% block main_attributes %}class="service-color lighten" data-service="corpus-analysis"{% endblock main_attributes %}
{% block page_content %}
<div class="container">

View File

@ -1,7 +1,7 @@
{% extends "base.html.j2" %}
{% import "wtf.html.j2" as wtf %}
{% block main_attribs %} class="service-scheme" data-service="corpus-analysis"{% endblock main_attribs %}
{% block main_attributes %}class="service-color lighten" data-service="corpus-analysis"{% endblock main_attributes %}
{% block page_content %}
<div class="container">
@ -396,8 +396,8 @@ shareLinkModalCreateButtonElement.addEventListener('click', (event) => {
shareLinkModalOutputCopyButtonElement.addEventListener('click', (event) => {
navigator.clipboard.writeText(shareLinkModalOutputFieldElement.value)
.then(
() => {app.flash('Copied!');},
() => {app.flash('Could not copy to clipboard. Please copy manually.', 'error');}
() => {app.ui.flash('Copied!');},
() => {app.ui.flash('Could not copy to clipboard. Please copy manually.', 'error');}
);
});

View File

@ -1,6 +1,6 @@
{% extends "base.html.j2" %}
{% block main_attribs %} class="service-scheme" data-service="{{ job.service }}"{% endblock main_attribs %}
{% block main_attributes %} class="service-color lighten" data-service="{{ job.service }}"{% endblock main_attributes %}
{% block page_content %}
<div class="container">

View File

@ -1,6 +1,6 @@
{% extends "base.html.j2" %}
{% block main_attribs %}class="service-color lighten" data-service="corpus-analysis"{% endblock main_attribs %}
{% block main_attributes %}class="service-color lighten" data-service="corpus-analysis"{% endblock main_attributes %}
{% block page_content %}
<div class="container">
@ -11,7 +11,7 @@
<div class="col s12 m3 push-m9">
<div class="center-align">
<i class="large nopaque-icons service-icons service-color-text text-darken" data-service="corpus-analysis"></i>
<i class="large nopaque-icons service-icons service-color-text text-darken"></i>
</div>
</div>
@ -26,8 +26,7 @@
<div class="corpus-list" data-user-id="{{ current_user.hashid }}"></div>
</div>
<div class="card-action right-align">
<a class="btn disabled waves-effect waves-light" href="{{ url_for('corpora.import_corpus') }}">Import Corpus<i class="material-icons right">import_export</i></a>
<a class="btn waves-effect waves-light" href="{{ url_for('corpora.create_corpus') }}">Create corpus<i class="material-icons right">add</i></a>
<a class="btn service-color darken waves-effect waves-light" href="{{ url_for('corpora.create_corpus') }}">Create corpus<i class="material-icons right">add</i></a>
</div>
</div>
</div>

View File

@ -1,10 +1,10 @@
{% extends "base.html.j2" %}
{% import "wtf.html.j2" as wtf %}
{% block main_attribs %}class="service-color lighten" data-service="file-setup-pipeline"{% endblock main_attribs %}
{% block main_attributes %}class="service-color lighten" data-service="file-setup-pipeline"{% endblock main_attributes %}
{% block page_content %}
<div class="container" data-service="file-setup-pipeline">
<div class="container">
<div class="row">
<div class="col s12">
<h1 id="title">{{ title }}</h1>
@ -14,12 +14,12 @@
<div class="center-align">
<p class="hide-on-small-only">&nbsp;</p>
<p class="hide-on-small-only">&nbsp;</p>
<i class="large nopaque-icons service-icons service-color-text text-darken" data-service="file-setup-pipeline"></i>
<i class="large nopaque-icons service-icons service-color-text text-darken"></i>
</div>
</div>
<div class="col s12 m9 pull-m3">
<div class="card service-color-border border-darken" data-service="file-setup-pipeline" style="border-top: 10px solid;">
<div class="card service-color-border border-darken" style="border-top: 10px solid;">
<div class="card-content">
<div class="row">
<div class="col s12">
@ -47,7 +47,7 @@
{{ wtf.render_field(form.description, material_icon='description') }}
</div>
<div class="col s12 l9">
{{ wtf.render_field(form.images, accept='image/jpeg, image/png, image/tiff', class_='file-setup-pipeline-color darken', placeholder='Choose JPEG, PNG or TIFF files') }}
{{ wtf.render_field(form.images, accept='image/jpeg, image/png, image/tiff', placeholder='Choose JPEG, PNG or TIFF files') }}
</div>
<div class="col s12 l3">
{{ wtf.render_field(form.version, material_icon='apps') }}
@ -63,3 +63,18 @@
</div>
</div>
{% endblock page_content %}
{% block scripts %}
{{ super() }}
<script>
function initPage() {
let createJobFormImagesElement = document.querySelector('#{{ form.images.id }}');
createJobFormImagesElement.parentElement.classList.add('service-color', 'darken');
let createJobFormSubmitElement = document.querySelector('#{{ form.submit.id }}');
createJobFormSubmitElement.classList.add('service-color', 'darken');
};
initPage();
</script>
{% endblock scripts %}

View File

@ -1,7 +1,7 @@
{% extends "base.html.j2" %}
{% import "wtf.html.j2" as wtf %}
{% block main_attribs %}class="service-color lighten" data-service="spacy-nlp-pipeline"{% endblock main_attribs %}
{% block main_attributes %}class="service-color lighten" data-service="spacy-nlp-pipeline"{% endblock main_attributes %}
{% block page_content %}
<div class="container">
@ -14,12 +14,12 @@
<div class="center-align">
<p class="hide-on-small-only">&nbsp;</p>
<p class="hide-on-small-only">&nbsp;</p>
<i class="large nopaque-icons service-icons service-color-text text-darken" data-service="spacy-nlp-pipeline"></i>
<i class="large nopaque-icons service-icons service-color-text text-darken"></i>
</div>
</div>
<div class="col s12 m9 pull-m3">
<div class="card service-color-border border-darken" data-service="spacy-nlp-pipeline" style="border-top: 10px solid;">
<div class="card service-color-border border-darken" style="border-top: 10px solid;">
<div class="card-content">
<div class="row">
<div class="col s12 m6">
@ -165,10 +165,20 @@
{% block scripts %}
{{ super() }}
<script>
// Disable user model selection if no models are available
function initPage() {
let createJobFormTxtElement = document.querySelector('#{{ form.txt.id }}');
createJobFormTxtElement.parentElement.classList.add('service-color', 'darken');
{% if user_spacy_nlp_pipeline_models_count == 0 %}
// Disable user model selection if no models are available
optionGroupOptions = document.querySelectorAll(".optgroup-option");
optionGroupOptions[0].classList.add("disabled");
{% endif %}
let createJobFormSubmitElement = document.querySelector('#{{ form.submit.id }}');
createJobFormSubmitElement.classList.add('service-color', 'darken');
};
initPage();
</script>
{% endblock scripts %}

View File

@ -1,7 +1,7 @@
{% extends "base.html.j2" %}
{% import "wtf.html.j2" as wtf %}
{% block main_attribs %}class="service-color lighten" data-service="tesseract-ocr-pipeline"{% endblock main_attribs %}
{% block main_attributes %}class="service-color lighten" data-service="tesseract-ocr-pipeline"{% endblock main_attributes %}
{% block page_content %}
<div class="container">
@ -14,12 +14,12 @@
<div class="center-align">
<p class="hide-on-small-only">&nbsp;</p>
<p class="hide-on-small-only">&nbsp;</p>
<i class="large nopaque-icons service-icons service-color-text text-darken" data-service="tesseract-ocr-pipeline"></i>
<i class="large nopaque-icons service-icons service-color-text text-darken"></i>
</div>
</div>
<div class="col s12 m9 pull-m3">
<div class="card service-color-border border-darken" data-service="tesseract-ocr-pipeline" style="border-top: 10px solid;">
<div class="card service-color-border border-darken" style="border-top: 10px solid;">
<div class="card-content">
<div class="row">
<div class="col s12">
@ -145,15 +145,26 @@
{% block scripts %}
{{ super() }}
<script>
function initPage() {
let createJobFormPdfElement = document.querySelector('#{{ form.pdf.id }}');
createJobFormPdfElement.parentElement.classList.add('service-color', 'darken');
// Disable user model selection if no models are available
{% if user_tesseract_ocr_pipeline_models_count == 0 %}
// Disable user model selection if no models are available
optionGroupOptions = document.querySelectorAll(".optgroup-option");
optionGroupOptions[0].classList.add("disabled");
{% endif %}
document.querySelector('#create-job-form-binarization').addEventListener('change', (event) => {
document.querySelector('#create-job-form-ocropus_nlbin_threshold-container').classList.toggle('hide');
let createJobFormBinarizationElement = document.querySelector('#{{ form.binarization.id }}');
let createJobFormOcropusNlbinThresholdContainerElement = document.querySelector('#create-job-form-ocropus_nlbin_threshold-container');
createJobFormBinarizationElement.addEventListener('change', (event) => {
createJobFormOcropusNlbinThresholdContainerElement.classList.toggle('hide');
});
let createJobFormSubmitElement = document.querySelector('#{{ form.submit.id }}');
createJobFormSubmitElement.classList.add('service-color', 'darken');
};
initPage();
</script>
{% endblock scripts %}

View File

@ -1,7 +1,7 @@
{% extends "base.html.j2" %}
{% import "wtf.html.j2" as wtf %}
{% block main_attribs %}class="service-color lighten" data-service="transkribus-htr-pipeline"{% endblock main_attribs %}
{% block main_attributes %}class="service-color lighten" data-service="transkribus-htr-pipeline"{% endblock main_attributes %}
{% block page_content %}
<div class="container">
@ -14,7 +14,7 @@
<div class="center-align">
<p class="hide-on-small-only">&nbsp;</p>
<p class="hide-on-small-only">&nbsp;</p>
<i class="large nopaque-icons service-icons service-color-text text-darken" data-service="transkribus-htr-pipeline"></i>
<i class="large nopaque-icons service-icons service-color-text text-darken"></i>
</div>
</div>
@ -138,3 +138,18 @@
</div>
</div>
{% endblock modals %}
{% block scripts %}
{{ super() }}
<script>
function initPage() {
let createJobFormPdfElement = document.querySelector('#{{ form.pdf.id }}');
createJobFormPdfElement.parentElement.classList.add('service-color', 'darken');
let createJobFormSubmitElement = document.querySelector('#{{ form.submit.id }}');
createJobFormSubmitElement.classList.add('service-color', 'darken');
};
initPage();
</script>
{% endblock scripts %}

View File

@ -1,47 +1,42 @@
{% macro render_field(field) %}
{% if field.type == 'BooleanField' %}
{{ render_boolean_field(field, *args, **kwargs) }}
{% elif field.type == 'DecimalRangeField' %}
{{ render_decimal_range_field(field, *args, **kwargs) }}
{% elif field.type == 'FileField' %}
{{ render_file_field(field, *args, **kwargs) }}
{% elif field.type == 'IntegerField' %}
{{ render_integer_field(field, *args, **kwargs) }}
{% elif field.type == 'MultipleFileField' %}
{{ render_multiple_file_field(field, *args, **kwargs) }}
{% elif field.type == 'SubmitField' %}
{{ render_submit_field(field, *args, **kwargs) }}
{% elif field.type in ['FileField', 'MultipleFileField'] %}
{{ render_file_field(field, *args, **kwargs) }}
{% elif field.type == 'TextAreaField' %}
{{ render_text_area_field(field, *args, **kwargs) }}
{% else %}
{% if 'class_' in kwargs and 'validate' not in kwargs['class_'] %}
{% set tmp = kwargs.update({'class_': kwargs['class_'] + ' validate'}) %}
{% else %}
{% set tmp = kwargs.update({'class_': 'validate'}) %}
{% endif %}
{{ render_generic_field(field, *args, **kwargs) }}
{% endif %}
{% endmacro %}
{% macro render_boolean_field(field) %}
{% set label = kwargs.pop('label', True) %}
<div class="switch">
{% if 'material_icon' in kwargs %}
<i class="material-icons prefix">{{ kwargs.pop('material_icon') }}</i>
{% endif %}
<div>
<label>
{{ field(*args, **kwargs) }}
<span class="lever"></span>
{% if label %}
{{ field.label.text }}
{% endif %}
</label>
<input id="{{ field.id }}" name="{{ field.name }}" type="checkbox">
<span>{{ field.label.text }}</span>
{% for error in field.errors %}
<span class="helper-text error-color-text">{{ error }}</span>
{% endfor %}
</label>
</div>
{% endmacro %}
{% macro render_file_field(field) %}
{% set placeholder = kwargs.pop('placeholder', '') %}
<div class="file-field input-field">
<div class="btn">
<span>{{ field.label.text }}</span>
{{ field(*args, **kwargs) }}
<input id="{{ field.id }}" name="{{ field.name }}" type="file">
</div>
<div class="file-path-wrapper">
<input class="file-path validate" type="text" placeholder="{{ placeholder }}">
@ -52,50 +47,78 @@
</div>
{% endmacro %}
{% macro render_generic_field(field) %}
{% if field.type == 'TextAreaField' and 'materialize-textarea' not in kwargs['class_'] %}
{% set tmp = kwargs.update({'class_': kwargs['class_'] + ' materialize-textarea'}) %}
{% elif field.type == 'IntegerField' %}
{% set tmp = kwargs.update({'type': 'number'}) %}
{% endif %}
{% set label = kwargs.pop('label', True) %}
<div class="input-field">
{% if 'material_icon' in kwargs %}
<i class="material-icons prefix">{{ kwargs.pop('material_icon') }}</i>
{% endif %}
{{ field(*args, **kwargs) }}
{% if label %}
{{ field.label }}
{% endif %}
{% macro render_multiple_file_field(field) %}
{% set placeholder = kwargs.pop('placeholder', '') %}
<div class="file-field input-field">
<div class="btn">
<span>{{ field.label.text }}</span>
<input id="{{ field.id }}" name="{{ field.name }}" type="file" multiple>
</div>
<div class="file-path-wrapper">
<input class="file-path validate" type="text" placeholder="{{ placeholder }}">
</div>
{% for error in field.errors %}
<span class="helper-text error-color-text">{{ error }}</span>
{% endfor %}
</div>
{% endmacro %}
{% macro render_integer_field(field) %}
<div class="input-field">
{% if 'material_icon' in kwargs %}
<i class="material-icons prefix">{{ kwargs.pop('material_icon') }}</i>
{% endif %}
<input class="validate" id="{{ field.id }}" name="{{ field.name }}" type="number">
<label for="{{ field.id }}">{{ field.label.text }}</label>
{% for error in field.errors %}
<span class="helper-text error-color-text">{{ error }}</span>
{% endfor %}
</div>
{% endmacro %}
{% macro render_submit_field(field) %}
{% if 'class_' in kwargs and 'btn' not in kwargs['class_'] %}
{% set tmp = kwargs.update({'class_': kwargs['class_'] + ' btn'}) %}
{% else %}
{% set tmp = kwargs.update({'class_': 'btn'}) %}
{% endif %}
{% if 'waves-effect' not in kwargs['class_'] %}
{% set tmp = kwargs.update({'class_': kwargs['class_'] + ' waves-effect'}) %}
{% endif %}
{% if 'waves-light' not in kwargs['class_'] %}
{% set tmp = kwargs.update({'class_': kwargs['class_'] + ' waves-light'}) %}
{% endif %}
<button class="{{ kwargs['class_'] }}"
id="{{ field.id }}"
name="{{ field.name }}"
type="submit"
value="{{ field.label.text }}"
{% if 'style' in kwargs %}
style="{{ kwargs.pop('style') }}"
{% endif %}>
<button class="btn waves-effect waves-light" id="{{ field.id }}" name="{{ field.name }}" type="submit">
{{ field.label.text }}
{% if 'material_icon' in kwargs %}
<i class="material-icons right">{{ kwargs.pop('material_icon') }}</i>
{% endif %}
</button>
{% endmacro %}
{% macro render_text_area_field(field) %}
<div class="input-field">
{% if 'material_icon' in kwargs %}
<i class="material-icons prefix">{{ kwargs.pop('material_icon') }}</i>
{% endif %}
<textarea class="materialize-textarea validate" id="{{ field.id }}" name="{{ field.name }}"></textarea>
<label for="{{ field.id }}">{{ field.label.text }}</label>
{% for error in field.errors %}
<span class="helper-text error-color-text">{{ error }}</span>
{% endfor %}
</div>
{% endmacro %}
{% macro render_generic_field(field) %}
{% set classes_ = kwargs.pop('class_', '').split(' ') %}
{% if 'validate' not in classes_ %}
{% set _ = classes_.append('validate') %}
{% endif %}
{% set _ = kwargs.update({'class_': ' '.join(classes_)}) %}
<div class="input-field">
{% if 'material_icon' in kwargs %}
<i class="material-icons prefix">{{ kwargs.pop('material_icon') }}</i>
{% endif %}
{{ field(*args, **kwargs) }}
{{ field.label }}
{% for error in field.errors %}
<span class="helper-text error-color-text">{{ error }}</span>
{% endfor %}
</div>
{% endmacro %}

View File

@ -94,5 +94,5 @@ class Config:
NOPAQUE_READCOOP_USERNAME = os.environ.get('NOPAQUE_READCOOP_USERNAME')
NOPAQUE_READCOOP_PASSWORD = os.environ.get('NOPAQUE_READCOOP_PASSWORD')
NOPAQUE_VERSION='1.0.2'
NOPAQUE_VERSION='1.1.0'
# endregion nopaque