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.rulers": [79],
"editor.tabSize": 4, "editor.tabSize": 4,
"emmet.includeLanguages": {
"jinja-html": "html"
},
"files.associations": {
".flaskenv": "env",
"*.env.tpl": "env",
"*.txt.j2": "jinja"
},
"files.insertFinalNewline": true, "files.insertFinalNewline": true,
"files.trimFinalNewlines": true, "files.trimFinalNewlines": true,
"files.trimTrailingWhitespace": true, "files.trimTrailingWhitespace": true,
"[html]": {
"editor.tabSize": 2
},
"[javascript]": { "[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 # region SocketIO Namespaces
from .namespaces.cqi_over_sio import CQiOverSocketIONamespace from .namespaces.cqi_over_sio import CQiOverSocketIONamespace
socketio.on_namespace(CQiOverSocketIONamespace('/cqi_over_sio')) socketio.on_namespace(CQiOverSocketIONamespace('/cqi_over_sio'))
from .namespaces.users import UsersNamespace
socketio.on_namespace(UsersNamespace('/users'))
# endregion SocketIO Namespaces # endregion SocketIO Namespaces
# region Database event Listeners # region Database event Listeners

View File

@ -15,4 +15,4 @@ def before_request():
pass 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): def wrapper(*args, **kwargs):
if current_user.is_authenticated: if current_user.is_authenticated:
return f(*args, **kwargs) return f(*args, **kwargs)
return {'code': 401, 'body': 'Unauthorized'} return {'status': 401, 'statusText': 'Unauthorized'}
return wrapper return wrapper
@ -35,7 +35,7 @@ def socketio_permission_required(permission):
@wraps(f) @wraps(f)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
if not current_user.can(permission): if not current_user.can(permission):
return {'code': 403, 'body': 'Forbidden'} return {'status': 403, 'statusText': 'Forbidden'}
return f(*args, **kwargs) return f(*args, **kwargs)
return wrapper return wrapper
return decorator return decorator

View File

@ -50,7 +50,7 @@ def _create_build_corpus_service(corpus: Corpus):
''' ## Constraints ## ''' ''' ## Constraints ## '''
constraints = ['node.role==worker'] constraints = ['node.role==worker']
''' ## Image ## ''' ''' ## 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 ## '''
labels = { labels = {
'nopaque.server_name': current_app.config['SERVER_NAME'] 'nopaque.server_name': current_app.config['SERVER_NAME']
@ -141,7 +141,7 @@ def _create_cqpserver_container(corpus: Corpus):
''' ## Entrypoint ## ''' ''' ## Entrypoint ## '''
entrypoint = ['bash', '-c'] entrypoint = ['bash', '-c']
''' ## Image ## ''' ''' ## 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 ## '''
name = f'nopaque-cqpserver-{corpus.id}' name = f'nopaque-cqpserver-{corpus.id}'
''' ## Network ## ''' ''' ## Network ## '''

View File

@ -42,8 +42,9 @@ def resource_after_delete(mapper, connection, resource):
'path': resource.jsonpatch_path 'path': resource.jsonpatch_path
} }
] ]
namespace = '/users'
room = f'/users/{resource.user_hashid}' room = f'/users/{resource.user_hashid}'
socketio.emit('patch_user', jsonpatch, namespace='/users', room=room) socketio.emit('patch', jsonpatch, namespace=namespace, room=room)
def cfa_after_delete(mapper, connection, cfa): def cfa_after_delete(mapper, connection, cfa):
@ -54,8 +55,9 @@ def cfa_after_delete(mapper, connection, cfa):
'path': jsonpatch_path 'path': jsonpatch_path
} }
] ]
namespace = '/users'
room = f'/users/{cfa.corpus.user.hashid}' room = f'/users/{cfa.corpus.user.hashid}'
socketio.emit('patch_user', jsonpatch, namespace='/users', room=room) socketio.emit('patch', jsonpatch, namespace=namespace, room=room)
def resource_after_insert(mapper, connection, resource): def resource_after_insert(mapper, connection, resource):
@ -69,8 +71,9 @@ def resource_after_insert(mapper, connection, resource):
'value': jsonpatch_value 'value': jsonpatch_value
} }
] ]
namespace = '/users'
room = f'/users/{resource.user_hashid}' room = f'/users/{resource.user_hashid}'
socketio.emit('patch_user', jsonpatch, namespace='/users', room=room) socketio.emit('patch', jsonpatch, namespace=namespace, room=room)
def cfa_after_insert(mapper, connection, cfa): def cfa_after_insert(mapper, connection, cfa):
@ -83,8 +86,9 @@ def cfa_after_insert(mapper, connection, cfa):
'value': jsonpatch_value 'value': jsonpatch_value
} }
] ]
namespace = '/users'
room = f'/users/{cfa.corpus.user.hashid}' room = f'/users/{cfa.corpus.user.hashid}'
socketio.emit('patch_user', jsonpatch, namespace='/users', room=room) socketio.emit('patch', jsonpatch, namespace=namespace, room=room)
def resource_after_update(mapper, connection, resource): def resource_after_update(mapper, connection, resource):
@ -109,8 +113,9 @@ def resource_after_update(mapper, connection, resource):
} }
) )
if jsonpatch: if jsonpatch:
namespace = '/users'
room = f'/users/{resource.user_hashid}' room = f'/users/{resource.user_hashid}'
socketio.emit('patch_user', jsonpatch, namespace='/users', room=room) socketio.emit('patch', jsonpatch, namespace=namespace, room=room)
def job_after_update(mapper, connection, job): 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 import db, docker_client, hashids, socketio
from app.decorators import socketio_login_required from app.decorators import socketio_login_required
from app.models import Corpus, CorpusStatus from app.models import Corpus, CorpusStatus
from . import cqi_extensions from . import cqi_extension_functions
from .utils import CQiOverSocketIOSessionManager from .utils import SessionManager
''' '''
@ -85,6 +85,16 @@ CQI_API_FUNCTION_NAMES = [
] ]
CQI_EXTENSION_FUNCTION_NAMES = [
'ext_corpus_update_db',
'ext_corpus_static_data',
'ext_corpus_paginate_corpus',
'ext_cqp_paginate_subcorpus',
'ext_cqp_partial_export_subcorpus',
'ext_cqp_export_subcorpus',
]
class CQiOverSocketIONamespace(Namespace): class CQiOverSocketIONamespace(Namespace):
@socketio_login_required @socketio_login_required
def on_connect(self): def on_connect(self):
@ -135,25 +145,25 @@ class CQiOverSocketIONamespace(Namespace):
cqi_client = CQiClient(cqpserver_ip_address) cqi_client = CQiClient(cqpserver_ip_address)
cqi_client_lock = Lock() cqi_client_lock = Lock()
CQiOverSocketIOSessionManager.setup() SessionManager.setup()
CQiOverSocketIOSessionManager.set_corpus_id(corpus_id) SessionManager.set_corpus_id(corpus_id)
CQiOverSocketIOSessionManager.set_cqi_client(cqi_client) SessionManager.set_cqi_client(cqi_client)
CQiOverSocketIOSessionManager.set_cqi_client_lock(cqi_client_lock) SessionManager.set_cqi_client_lock(cqi_client_lock)
return {'code': 200, 'msg': 'OK'} return {'code': 200, 'msg': 'OK'}
@socketio_login_required @socketio_login_required
def on_exec(self, fn_name: str, fn_args: dict = {}) -> dict: def on_exec(self, fn_name: str, fn_args: dict = {}) -> dict:
try: try:
cqi_client = CQiOverSocketIOSessionManager.get_cqi_client() cqi_client = SessionManager.get_cqi_client()
cqi_client_lock = CQiOverSocketIOSessionManager.get_cqi_client_lock() cqi_client_lock = SessionManager.get_cqi_client_lock()
except KeyError: except KeyError:
return {'code': 424, 'msg': 'Failed Dependency'} return {'code': 424, 'msg': 'Failed Dependency'}
if fn_name in CQI_API_FUNCTION_NAMES: if fn_name in CQI_API_FUNCTION_NAMES:
fn = getattr(cqi_client.api, fn_name) fn = getattr(cqi_client.api, fn_name)
elif fn_name in cqi_extensions.CQI_EXTENSION_FUNCTION_NAMES: elif fn_name in CQI_EXTENSION_FUNCTION_NAMES:
fn = getattr(cqi_extensions, fn_name) fn = getattr(cqi_extension_functions, fn_name)
else: else:
return {'code': 400, 'msg': 'Bad Request'} return {'code': 400, 'msg': 'Bad Request'}
@ -198,10 +208,10 @@ class CQiOverSocketIONamespace(Namespace):
def on_disconnect(self): def on_disconnect(self):
try: try:
corpus_id = CQiOverSocketIOSessionManager.get_corpus_id() corpus_id = SessionManager.get_corpus_id()
cqi_client = CQiOverSocketIOSessionManager.get_cqi_client() cqi_client = SessionManager.get_cqi_client()
cqi_client_lock = CQiOverSocketIOSessionManager.get_cqi_client_lock() cqi_client_lock = SessionManager.get_cqi_client_lock()
CQiOverSocketIOSessionManager.teardown() SessionManager.teardown()
except KeyError: except KeyError:
return return

View File

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

View File

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

109
app/namespaces/jobs.py Normal file
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"; --corpus-status-content: "unprepared";
} }
[data-corpus-status="SUBMITTED"] {
--corpus-status-content: "submitted";
}
[data-corpus-status="QUEUED"] { [data-corpus-status="QUEUED"] {
--corpus-status-content: "queued"; --corpus-status-content: "queued";
} }

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 { nopaque.App = class App {
#promises;
constructor() { constructor() {
this.data = {
users: {}
};
this.#promises = {
getUser: {},
subscribeUser: {}
};
this.socket = io({transports: ['websocket'], upgrade: false}); this.socket = io({transports: ['websocket'], upgrade: false});
this.socket.on('patch_user', (patch) => {this.onPatch(patch);}); // Endpoints
} this.users = new nopaque.app.endpoints.Users(this);
getUser(userId) { // Extensions
if (userId in this.#promises.getUser) { this.toaster = new nopaque.app.extensions.Toaster(this);
return this.#promises.getUser[userId]; this.ui = new nopaque.app.extensions.UI(this);
} this.userHub = new nopaque.app.extensions.UserHub(this);
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);
} }
init() { init() {
this.initUi(); // Initialize extensions
} this.toaster.init();
this.ui.init();
initUi() { this.userHub.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
} }
}; };

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

View File

@ -46,7 +46,7 @@ nopaque.corpus_analysis.ReaderExtension = class ReaderExtension {
if ('description' in error) {errorString += `: ${error.description}`;} if ('description' in error) {errorString += `: ${error.description}`;}
this.elements.error.innerText = errorString; this.elements.error.innerText = errorString;
this.elements.error.classList.remove('hide'); this.elements.error.classList.remove('hide');
app.flash(errorString, 'error'); app.ui.flash(errorString, 'error');
this.elements.progress.classList.add('hide'); this.elements.progress.classList.add('hide');
} }
this.app.enableActionElements(); this.app.enableActionElements();
@ -205,7 +205,7 @@ nopaque.corpus_analysis.ReaderExtension = class ReaderExtension {
` `
); );
this.elements.corpusPagination.appendChild(pageElement); this.elements.corpusPagination.appendChild(pageElement);
for (let paginateTriggerElement of this.elements.corpusPagination.querySelectorAll('.pagination-trigger[data-target]')) { for (let paginateTriggerElement of this.elements.corpusPagination.querySelectorAll('.pagination-trigger[data-target]')) {
paginateTriggerElement.addEventListener('click', (event) => { paginateTriggerElement.addEventListener('click', (event) => {
event.preventDefault(); event.preventDefault();

View File

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

View File

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

View File

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

View File

@ -5,19 +5,14 @@ nopaque.resource_displays.ResourceDisplay = class ResourceDisplay {
this.displayElement = displayElement; this.displayElement = displayElement;
this.userId = this.displayElement.dataset.userId; this.userId = this.displayElement.dataset.userId;
this.isInitialized = false; this.isInitialized = false;
if (this.userId) { if (this.userId === undefined) {return;}
app.subscribeUser(this.userId) app.userHub.addEventListener('patch', (event) => {
.then((response) => { if (this.isInitialized) {this.onPatch(event.detail);}
app.socket.on('patch_user', (patch) => { });
if (this.isInitialized) {this.onPatch(patch);} app.userHub.get(this.userId).then((user) => {
}); this.init(user);
}); this.isInitialized = true;
app.getUser(this.userId) });
.then((user) => {
this.init(user);
this.isInitialized = true;
});
}
} }
init(user) {throw 'Not implemented';} 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.hasPermissionView = listContainerElement.dataset?.hasPermissionView == 'true' || false;
this.hasPermissionManageFiles = listContainerElement.dataset?.hasPermissionManageFiles == 'true' || false; this.hasPermissionManageFiles = listContainerElement.dataset?.hasPermissionManageFiles == 'true' || false;
if (this.userId === undefined || this.corpusId === undefined) {return;} if (this.userId === undefined || this.corpusId === undefined) {return;}
app.subscribeUser(this.userId).then((response) => { app.userHub.addEventListener('patch', (event) => {
app.socket.on('patch_user', (patch) => { if (this.isInitialized) {this.onPatch(event.detail);}
if (this.isInitialized) {this.onPatch(patch);}
});
}); });
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.add(Object.values(user.corpora[this.corpusId].files || user.followed_corpora[this.corpusId].files));
this.isInitialized = true; this.isInitialized = true;
}); });

View File

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

View File

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

View File

@ -12,12 +12,10 @@ nopaque.resource_lists.JobList = class JobList extends nopaque.resource_lists.Re
this.selectedItemIds = new Set(); this.selectedItemIds = new Set();
this.userId = listContainerElement.dataset.userId; this.userId = listContainerElement.dataset.userId;
if (this.userId === undefined) {return;} if (this.userId === undefined) {return;}
app.subscribeUser(this.userId).then((response) => { app.userHub.addEventListener('patch', (event) => {
app.socket.on('patch_user', (patch) => { if (this.isInitialized) {this.onPatch(event.detail);}
if (this.isInitialized) {this.onPatch(patch);}
});
}); });
app.getUser(this.userId).then((user) => { app.userHub.get(this.userId).then((user) => {
this.add(Object.values(user.jobs)); this.add(Object.values(user.jobs));
this.isInitialized = true; this.isInitialized = true;
}); });
@ -25,19 +23,19 @@ nopaque.resource_lists.JobList = class JobList extends nopaque.resource_lists.Re
get item() { get item() {
return ` return `
<tr class="list-item clickable hoverable"> <tr class="list-item clickable hoverable service-color lighten">
<td> <td>
<label class="list-action-trigger" data-list-action="select"> <label class="list-action-trigger" data-list-action="select">
<input class="select-checkbox" type="checkbox"> <input class="select-checkbox" type="checkbox">
<span class="disable-on-click"></span> <span class="disable-on-click"></span>
</label> </label>
</td> </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><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><span class="status badge new job-status-color job-status-text" data-badge-caption=""></span></td>
<td class="right-align"> <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 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> </td>
</tr> </tr>
`.trim(); `.trim();

View File

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

View File

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

View File

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

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 %} {% block page_content %}
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col s12 m8 offset-m2"> <div class="col s12 l8 offset-l2">
<h1 id="title">{{ title }}</h1> <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> <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.user, material_icon='person') }}
{{ wtf.render_field(form.password, material_icon='vpn_key') }} {{ wtf.render_field(form.password, material_icon='vpn_key') }}
<div class="row"> <div class="row">
<div class="col s6 left-align"> <div class="col s12 l6">
<a href="{{ url_for('.reset_password_request') }}">Forgot your password?</a>
</div>
<div class="col s6 right-align">
{{ wtf.render_field(form.remember_me) }} {{ wtf.render_field(form.remember_me) }}
</div> </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> </div>
{{ wtf.render_field(form.submit, material_icon='send', class_='width-100') }}
</div> </div>
</form> </form>
</div> </div>

View File

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

View File

@ -13,7 +13,9 @@
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{{ wtf.render_field(form.password) }} {{ wtf.render_field(form.password) }}
{{ wtf.render_field(form.password_2) }} {{ 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> </div>
</form> </form>
</div> </div>

View File

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

View File

@ -1,544 +1,75 @@
{% if title is not defined %}
{% set title = 'nopaque' %}
{% endif %}
{% block doc %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html {% block html_attributes %}lang="en"{% endblock html_attributes %}>
<head> {% block html %}
<!-- #region meta --> <head {% block head_attributes %}{% endblock head_attributes %}>
<meta charset="UTF-8"> {% block head %}
<meta name="viewport" content="width=device-width, initial-scale=1.0"> {% block metas %}
<!-- #endregion meta --> {% 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 styles %}
{% block stylesheets %} {% include '_base/stylesheets.html.j2' %}
<link href="{{ url_for('static', filename='external/material-design-icons/css/material-icons.css') }}" rel="stylesheet"> {% endblock styles %}
{% assets {% endblock head %}
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 -->
</head> </head>
<body> <body {% block body_attributes %}{% endblock body_attributes %}>
<header> {% block body %}
<div class="navbar-fixed"> <header {% block header_attributes %}{% endblock header_attributes %}>
<nav> {% block header %}
<div class="nav-wrapper"> {% block navbar %}
{# menu icon #} {% include '_base/navbar.html.j2' %}
{# small/medium devices #} {% endblock navbar %}
<a href="#!" class="sidenav-trigger" data-target="sidenav"><i class="material-icons">menu</i></a>
{% if current_user.is_authenticated %} {% block sidenav %}
{# nopaque logo #} {% include '_base/sidenav.html.j2' %}
{# large devices #} {% endblock sidenav %}
<a href="{{ url_for('main.index') }}" class="brand-logo hide-on-med-and-down" style="height: 100%;"> {% endblock header %}
<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>
</header> </header>
<main {% block main_attribs %}{% endblock main_attribs %}> <main {% block main_attributes %}{% endblock main_attributes %}>
{% block main %}
{% block page_content %}{% endblock page_content %} {% block page_content %}{% endblock page_content %}
{% endblock main %}
</main> </main>
<footer class="page-footer"> <footer {% block footer_attributes %}class="page-footer"{% endblock footer_attributes %}>
<div class="container"> {% block footer %}
<div class="row"> {% include '_base/footer.html.j2' %}
<div class="col s12 l3"> {% endblock footer %}
<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> </footer>
<div id="dropdowns"> <div {% block dropdowns_attributes %}id="dropdowns"{% endblock dropdowns_attributes %}>
{% block dropdowns %} {% block dropdowns %}
{% if current_user.is_authenticated %} {% include '_base/dropdowns.html.j2' %}
<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 %}
{% endblock dropdowns %} {% endblock dropdowns %}
</div> </div>
<div id="modals"> <div {% block modals_attributes %}id="modals"{% endblock modals_attributes %}>
{% block modals %} {% block modals %}
{% if current_user.is_authenticated and not current_user.terms_of_use_accepted %} {% include '_base/modals.html.j2' %}
<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 %}
{% endblock modals %} {% endblock modals %}
</div> </div>
<!-- #region scripts -->
{% block scripts %} {% block scripts %}
<script src="{{ url_for('static', filename='external/materialize/js/materialize.min.js') }}"></script> {% include '_base/scripts.html.j2' %}
<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>
{% endblock scripts %} {% endblock scripts %}
<!-- #endregion scripts --> {% endblock body %}
</body> </body>
{% endblock html %}
</html> </html>
{% endblock doc %}

View File

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

View File

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

View File

@ -1,7 +1,7 @@
{% extends "base.html.j2" %} {% extends "base.html.j2" %}
{% import "wtf.html.j2" as wtf %} {% 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 %} {% block page_content %}
<div class="container"> <div class="container">

View File

@ -1,7 +1,7 @@
{% extends "base.html.j2" %} {% extends "base.html.j2" %}
{% import "wtf.html.j2" as wtf %} {% 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 %} {% block page_content %}
<div class="container"> <div class="container">

View File

@ -1,7 +1,7 @@
{% extends "base.html.j2" %} {% extends "base.html.j2" %}
{% import "wtf.html.j2" as wtf %} {% 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 %} {% block page_content %}
<div class="container"> <div class="container">
@ -396,8 +396,8 @@ shareLinkModalCreateButtonElement.addEventListener('click', (event) => {
shareLinkModalOutputCopyButtonElement.addEventListener('click', (event) => { shareLinkModalOutputCopyButtonElement.addEventListener('click', (event) => {
navigator.clipboard.writeText(shareLinkModalOutputFieldElement.value) navigator.clipboard.writeText(shareLinkModalOutputFieldElement.value)
.then( .then(
() => {app.flash('Copied!');}, () => {app.ui.flash('Copied!');},
() => {app.flash('Could not copy to clipboard. Please copy manually.', 'error');} () => {app.ui.flash('Could not copy to clipboard. Please copy manually.', 'error');}
); );
}); });

View File

@ -1,6 +1,6 @@
{% extends "base.html.j2" %} {% 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 %} {% block page_content %}
<div class="container"> <div class="container">

View File

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

View File

@ -1,10 +1,10 @@
{% extends "base.html.j2" %} {% extends "base.html.j2" %}
{% import "wtf.html.j2" as wtf %} {% 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 %} {% block page_content %}
<div class="container" data-service="file-setup-pipeline"> <div class="container">
<div class="row"> <div class="row">
<div class="col s12"> <div class="col s12">
<h1 id="title">{{ title }}</h1> <h1 id="title">{{ title }}</h1>
@ -14,12 +14,12 @@
<div class="center-align"> <div class="center-align">
<p class="hide-on-small-only">&nbsp;</p> <p class="hide-on-small-only">&nbsp;</p>
<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> </div>
<div class="col s12 m9 pull-m3"> <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="card-content">
<div class="row"> <div class="row">
<div class="col s12"> <div class="col s12">
@ -47,7 +47,7 @@
{{ wtf.render_field(form.description, material_icon='description') }} {{ wtf.render_field(form.description, material_icon='description') }}
</div> </div>
<div class="col s12 l9"> <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>
<div class="col s12 l3"> <div class="col s12 l3">
{{ wtf.render_field(form.version, material_icon='apps') }} {{ wtf.render_field(form.version, material_icon='apps') }}
@ -63,3 +63,18 @@
</div> </div>
</div> </div>
{% endblock page_content %} {% 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" %} {% extends "base.html.j2" %}
{% import "wtf.html.j2" as wtf %} {% 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 %} {% block page_content %}
<div class="container"> <div class="container">
@ -14,12 +14,12 @@
<div class="center-align"> <div class="center-align">
<p class="hide-on-small-only">&nbsp;</p> <p class="hide-on-small-only">&nbsp;</p>
<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> </div>
<div class="col s12 m9 pull-m3"> <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="card-content">
<div class="row"> <div class="row">
<div class="col s12 m6"> <div class="col s12 m6">
@ -165,10 +165,20 @@
{% block scripts %} {% block scripts %}
{{ super() }} {{ super() }}
<script> <script>
// Disable user model selection if no models are available function initPage() {
{% if user_spacy_nlp_pipeline_models_count == 0 %} let createJobFormTxtElement = document.querySelector('#{{ form.txt.id }}');
optionGroupOptions = document.querySelectorAll(".optgroup-option"); createJobFormTxtElement.parentElement.classList.add('service-color', 'darken');
optionGroupOptions[0].classList.add("disabled");
{% endif %} {% 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> </script>
{% endblock scripts %} {% endblock scripts %}

View File

@ -1,7 +1,7 @@
{% extends "base.html.j2" %} {% extends "base.html.j2" %}
{% import "wtf.html.j2" as wtf %} {% 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 %} {% block page_content %}
<div class="container"> <div class="container">
@ -14,12 +14,12 @@
<div class="center-align"> <div class="center-align">
<p class="hide-on-small-only">&nbsp;</p> <p class="hide-on-small-only">&nbsp;</p>
<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> </div>
<div class="col s12 m9 pull-m3"> <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="card-content">
<div class="row"> <div class="row">
<div class="col s12"> <div class="col s12">
@ -145,15 +145,26 @@
{% block scripts %} {% block scripts %}
{{ super() }} {{ super() }}
<script> <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 %}
{% if user_tesseract_ocr_pipeline_models_count == 0 %} // Disable user model selection if no models are available
optionGroupOptions = document.querySelectorAll(".optgroup-option"); optionGroupOptions = document.querySelectorAll(".optgroup-option");
optionGroupOptions[0].classList.add("disabled"); optionGroupOptions[0].classList.add("disabled");
{% endif %} {% endif %}
document.querySelector('#create-job-form-binarization').addEventListener('change', (event) => { let createJobFormBinarizationElement = document.querySelector('#{{ form.binarization.id }}');
document.querySelector('#create-job-form-ocropus_nlbin_threshold-container').classList.toggle('hide'); 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> </script>
{% endblock scripts %} {% endblock scripts %}

View File

@ -1,7 +1,7 @@
{% extends "base.html.j2" %} {% extends "base.html.j2" %}
{% import "wtf.html.j2" as wtf %} {% 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 %} {% block page_content %}
<div class="container"> <div class="container">
@ -14,7 +14,7 @@
<div class="center-align"> <div class="center-align">
<p class="hide-on-small-only">&nbsp;</p> <p class="hide-on-small-only">&nbsp;</p>
<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>
</div> </div>
@ -138,3 +138,18 @@
</div> </div>
</div> </div>
{% endblock modals %} {% 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) %} {% macro render_field(field) %}
{% if field.type == 'BooleanField' %} {% if field.type == 'BooleanField' %}
{{ render_boolean_field(field, *args, **kwargs) }} {{ render_boolean_field(field, *args, **kwargs) }}
{% elif field.type == 'DecimalRangeField' %} {% elif field.type == 'FileField' %}
{{ render_decimal_range_field(field, *args, **kwargs) }} {{ 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' %} {% elif field.type == 'SubmitField' %}
{{ render_submit_field(field, *args, **kwargs) }} {{ render_submit_field(field, *args, **kwargs) }}
{% elif field.type in ['FileField', 'MultipleFileField'] %} {% elif field.type == 'TextAreaField' %}
{{ render_file_field(field, *args, **kwargs) }} {{ render_text_area_field(field, *args, **kwargs) }}
{% else %} {% 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) }} {{ render_generic_field(field, *args, **kwargs) }}
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}
{% macro render_boolean_field(field) %} {% macro render_boolean_field(field) %}
{% set label = kwargs.pop('label', True) %} <div>
<div class="switch">
{% if 'material_icon' in kwargs %}
<i class="material-icons prefix">{{ kwargs.pop('material_icon') }}</i>
{% endif %}
<label> <label>
{{ field(*args, **kwargs) }} <input id="{{ field.id }}" name="{{ field.name }}" type="checkbox">
<span class="lever"></span> <span>{{ field.label.text }}</span>
{% if label %} {% for error in field.errors %}
{{ field.label.text }} <span class="helper-text error-color-text">{{ error }}</span>
{% endif %} {% endfor %}
</label> </label>
{% for error in field.errors %}
<span class="helper-text error-color-text">{{ error }}</span>
{% endfor %}
</div> </div>
{% endmacro %} {% endmacro %}
{% macro render_file_field(field) %} {% macro render_file_field(field) %}
{% set placeholder = kwargs.pop('placeholder', '') %} {% set placeholder = kwargs.pop('placeholder', '') %}
<div class="file-field input-field"> <div class="file-field input-field">
<div class="btn"> <div class="btn">
<span>{{ field.label.text }}</span> <span>{{ field.label.text }}</span>
{{ field(*args, **kwargs) }} <input id="{{ field.id }}" name="{{ field.name }}" type="file">
</div> </div>
<div class="file-path-wrapper"> <div class="file-path-wrapper">
<input class="file-path validate" type="text" placeholder="{{ placeholder }}"> <input class="file-path validate" type="text" placeholder="{{ placeholder }}">
@ -52,50 +47,78 @@
</div> </div>
{% endmacro %} {% endmacro %}
{% macro render_generic_field(field) %}
{% if field.type == 'TextAreaField' and 'materialize-textarea' not in kwargs['class_'] %} {% macro render_multiple_file_field(field) %}
{% set tmp = kwargs.update({'class_': kwargs['class_'] + ' materialize-textarea'}) %} {% set placeholder = kwargs.pop('placeholder', '') %}
{% elif field.type == 'IntegerField' %}
{% set tmp = kwargs.update({'type': 'number'}) %} <div class="file-field input-field">
{% endif %} <div class="btn">
{% set label = kwargs.pop('label', True) %} <span>{{ field.label.text }}</span>
<div class="input-field"> <input id="{{ field.id }}" name="{{ field.name }}" type="file" multiple>
{% if 'material_icon' in kwargs %} </div>
<i class="material-icons prefix">{{ kwargs.pop('material_icon') }}</i> <div class="file-path-wrapper">
{% endif %} <input class="file-path validate" type="text" placeholder="{{ placeholder }}">
{{ field(*args, **kwargs) }} </div>
{% if label %}
{{ field.label }}
{% endif %}
{% for error in field.errors %} {% for error in field.errors %}
<span class="helper-text error-color-text">{{ error }}</span> <span class="helper-text error-color-text">{{ error }}</span>
{% endfor %} {% endfor %}
</div> </div>
{% endmacro %} {% 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) %} {% macro render_submit_field(field) %}
{% if 'class_' in kwargs and 'btn' not in kwargs['class_'] %} <button class="btn waves-effect waves-light" id="{{ field.id }}" name="{{ field.name }}" type="submit">
{% 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 %}>
{{ field.label.text }} {{ field.label.text }}
{% if 'material_icon' in kwargs %} {% if 'material_icon' in kwargs %}
<i class="material-icons right">{{ kwargs.pop('material_icon') }}</i> <i class="material-icons right">{{ kwargs.pop('material_icon') }}</i>
{% endif %} {% endif %}
</button> </button>
{% endmacro %} {% 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_USERNAME = os.environ.get('NOPAQUE_READCOOP_USERNAME')
NOPAQUE_READCOOP_PASSWORD = os.environ.get('NOPAQUE_READCOOP_PASSWORD') NOPAQUE_READCOOP_PASSWORD = os.environ.get('NOPAQUE_READCOOP_PASSWORD')
NOPAQUE_VERSION='1.0.2' NOPAQUE_VERSION='1.1.0'
# endregion nopaque # endregion nopaque