29 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
99d7a8bdfc Some fixes and improve jinja2 template performance by reducing include statements 2024-11-19 15:28:43 +01:00
54c4295bf7 Fixes and more descriptions 2024-11-18 13:32:55 +01:00
1e5c26b8e3 Reorganize Socket.IO code 2024-11-18 12:36:37 +01:00
9f56647cf7 highlight active items in top navbar 2024-11-18 12:35:53 +01:00
460257294d Use relative import for sub blueprints 2024-11-18 11:08:28 +01:00
2c43333c94 Check tos accepted in registration form 2024-11-18 11:03:29 +01:00
fc8b11fa66 update auth package 2024-11-15 16:07:29 +01:00
a8ab1bee71 Move some blueprints and rename routes 2024-11-15 15:59:08 +01:00
ee7f64f5be Design update 2024-11-15 15:21:26 +01:00
96 changed files with 1545 additions and 1068 deletions

16
.vscode/settings.json vendored
View File

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

View File

@ -104,15 +104,6 @@ def create_app(config: Config = Config) -> Flask:
from .blueprints.contributions import bp as contributions_blueprint
app.register_blueprint(contributions_blueprint, url_prefix='/contributions')
from .blueprints.spacy_nlp_pipeline_models import bp as spacy_nlp_pipeline_models_blueprint
app.register_blueprint(spacy_nlp_pipeline_models_blueprint, url_prefix='/spacy-nlp-pipeline-models')
from .blueprints.tesseract_ocr_pipeline_models import bp as tesseract_ocr_pipeline_models_blueprint
app.register_blueprint(tesseract_ocr_pipeline_models_blueprint, url_prefix='/tesseract-ocr-pipeline-models')
from.blueprints.transkribus_htr_pipeline_models import bp as transkribus_htr_pipeline_models_blueprint
app.register_blueprint(transkribus_htr_pipeline_models_blueprint, url_prefix='/transkribus-htr-pipeline-models')
from .blueprints.corpora import bp as corpora_blueprint
app.register_blueprint(corpora_blueprint, cli_group='corpus', url_prefix='/corpora')

View File

@ -1,5 +1,29 @@
from flask import Blueprint
from flask import Blueprint, redirect, request, url_for
from flask_login import current_user
from app import db
bp = Blueprint('auth', __name__)
@bp.before_app_request
def before_request():
if not current_user.is_authenticated:
return
current_user.ping()
db.session.commit()
if (
not current_user.confirmed
and request.endpoint
and request.blueprint != 'auth'
and request.endpoint != 'static'
):
return redirect(url_for('auth.unconfirmed'))
if not current_user.terms_of_use_accepted:
return redirect(url_for('main.terms_of_use'))
from . import routes

View File

@ -60,7 +60,11 @@ class RegistrationForm(FlaskForm):
def validate_username(self, field):
if User.query.filter_by(username=field.data).first():
raise ValidationError('Username already in use')
raise ValidationError('Username already registered')
def validate_terms_of_use_accepted(self, field):
if not field.data:
raise ValidationError('Terms of Use not accepted')
class LoginForm(FlaskForm):

View File

@ -12,26 +12,6 @@ from .forms import (
)
@bp.before_app_request
def before_request():
"""
Checks if a user is unconfirmed when visiting specific sites. Redirects to
unconfirmed view if user is unconfirmed.
"""
if not current_user.is_authenticated:
return
current_user.ping()
db.session.commit()
if (not current_user.confirmed
and request.endpoint
and request.blueprint != 'auth'
and request.endpoint != 'static'):
return redirect(url_for('auth.unconfirmed'))
if not current_user.terms_of_use_accepted:
return redirect(url_for('main.terms_of_use'))
@bp.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:

View File

@ -16,3 +16,10 @@ def before_request():
from . import routes
from .spacy_nlp_pipeline_models import bp as spacy_nlp_pipeline_models_bp
bp.register_blueprint(spacy_nlp_pipeline_models_bp, url_prefix='/spacy-nlp-pipeline-models')
from .tesseract_ocr_pipeline_models import bp as tesseract_ocr_pipeline_models_bp
bp.register_blueprint(tesseract_ocr_pipeline_models_bp, url_prefix='/tesseract-ocr-pipeline-models')

View File

@ -1,7 +1,7 @@
from flask import redirect, url_for
from flask import render_template
from . import bp
@bp.route('')
def index():
return redirect(url_for('main.dashboard', _anchor='contributions'))
return render_template('contributions/index.html.j2', title='Contributions')

View File

@ -1,8 +1,8 @@
from flask_wtf.file import FileField, FileRequired
from wtforms import StringField, ValidationError
from wtforms.validators import InputRequired, Length
from app.blueprints.contributions.forms import ContributionBaseForm, UpdateContributionBaseForm
from app.blueprints.services import SERVICES
from ..forms import ContributionBaseForm, UpdateContributionBaseForm
class CreateSpaCyNLPPipelineModelForm(ContributionBaseForm):

View File

@ -12,10 +12,7 @@ from .forms import (
@bp.route('/')
@login_required
def index():
return render_template(
'spacy_nlp_pipeline_models/index.html.j2',
title='SpaCy NLP Pipeline Models'
)
return redirect(url_for('contributions.index', _anchor='spacy-nlp-pipeline-models'))
@bp.route('/create', methods=['GET', 'POST'])
@ -46,7 +43,7 @@ def create():
flash(f'SpaCy NLP Pipeline model "{snpm.title}" created')
return {}, 201, {'Location': url_for('.index')}
return render_template(
'spacy_nlp_pipeline_models/create.html.j2',
'contributions/spacy_nlp_pipeline_models/create.html.j2',
title='Create SpaCy NLP Pipeline Model',
form=form
)
@ -54,7 +51,7 @@ def create():
@bp.route('/<hashid:spacy_nlp_pipeline_model_id>', methods=['GET', 'POST'])
@login_required
def spacy_nlp_pipeline_model(spacy_nlp_pipeline_model_id):
def entity(spacy_nlp_pipeline_model_id):
snpm = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id)
if not (snpm.user == current_user or current_user.is_administrator):
abort(403)
@ -66,7 +63,7 @@ def spacy_nlp_pipeline_model(spacy_nlp_pipeline_model_id):
db.session.commit()
return redirect(url_for('.index'))
return render_template(
'spacy_nlp_pipeline_models/spacy_nlp_pipeline_model.html.j2',
'contributions/spacy_nlp_pipeline_models/entity.html.j2',
title=f'{snpm.title} {snpm.version}',
form=form,
spacy_nlp_pipeline_model=snpm

View File

@ -1,7 +1,7 @@
from flask_wtf.file import FileField, FileRequired
from wtforms import ValidationError
from app.blueprints.contributions.forms import ContributionBaseForm, UpdateContributionBaseForm
from app.blueprints.services import SERVICES
from ..forms import ContributionBaseForm, UpdateContributionBaseForm
class CreateTesseractOCRPipelineModelForm(ContributionBaseForm):

View File

@ -11,10 +11,7 @@ from .forms import (
@bp.route('/')
def index():
return render_template(
'tesseract_ocr_pipeline_models/index.html.j2',
title='Tesseract OCR Pipeline Models'
)
return redirect(url_for('contributions.index', _anchor='tesseract-ocr-pipeline-models'))
@bp.route('/create', methods=['GET', 'POST'])
@ -43,14 +40,14 @@ def create():
flash(f'Tesseract OCR Pipeline model "{topm.title}" created')
return {}, 201, {'Location': url_for('.index')}
return render_template(
'tesseract_ocr_pipeline_models/create.html.j2',
'contributions/tesseract_ocr_pipeline_models/create.html.j2',
title='Create Tesseract OCR Pipeline Model',
form=form
)
@bp.route('/<hashid:tesseract_ocr_pipeline_model_id>', methods=['GET', 'POST'])
def tesseract_ocr_pipeline_model(tesseract_ocr_pipeline_model_id):
def entity(tesseract_ocr_pipeline_model_id):
topm = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id)
if not (topm.user == current_user or current_user.is_administrator):
abort(403)
@ -62,7 +59,7 @@ def tesseract_ocr_pipeline_model(tesseract_ocr_pipeline_model_id):
db.session.commit()
return redirect(url_for('.index'))
return render_template(
'tesseract_ocr_pipeline_models/tesseract_ocr_pipeline_model.html.j2',
'contributions/tesseract_ocr_pipeline_models/entity.html.j2',
title=f'{topm.title} {topm.version}',
form=form,
tesseract_ocr_pipeline_model=topm

View File

@ -72,14 +72,14 @@ def terms_of_use():
)
@bp.route('/social-area')
@bp.route('/social')
@login_required
def social_area():
def social():
corpora = Corpus.query.filter(Corpus.is_public == True, Corpus.user != current_user).all()
users = User.query.filter(User.is_public == True, User.id != current_user.id).all()
return render_template(
'main/social_area.html.j2',
title='Social Area',
'main/social.html.j2',
title='Social',
corpora=corpora,
users=users
)

View File

@ -1,18 +0,0 @@
from flask import Blueprint
from flask_login import login_required
bp = Blueprint('transkribus_htr_pipeline_models', __name__)
@bp.before_request
@login_required
def before_request():
'''
Ensures that the routes in this package can only be visited by users that
are logged in.
'''
pass
from . import routes

View File

@ -1,7 +0,0 @@
from flask import abort
from . import bp
@bp.route('/transkribus_htr_pipeline_models')
def index():
return abort(503)

View File

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

View File

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

View File

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

View File

@ -41,7 +41,7 @@ class SpaCyNLPPipelineModel(FileMixin, HashidMixin, db.Model):
@property
def url(self):
return url_for(
'contributions.spacy_nlp_pipeline_model',
'contributions.spacy_nlp_pipeline_models.entity',
spacy_nlp_pipeline_model_id=self.id
)

View File

@ -40,7 +40,7 @@ class TesseractOCRPipelineModel(FileMixin, HashidMixin, db.Model):
@property
def url(self):
return url_for(
'contributions.tesseract_ocr_pipeline_model',
'contributions.tesseract_ocr_pipeline_models.entity',
tesseract_ocr_pipeline_model_id=self.id
)

View File

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

View File

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

View File

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

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

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

View File

@ -1,13 +1,21 @@
from flask import current_app, Flask
from flask_login import current_user
from flask_socketio import join_room, leave_room, Namespace
from app import hashids
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_user(self, user_hashid: str) -> dict:
def on_get(self, user_hashid: str) -> dict:
user_id = hashids.decode(user_hashid)
if not isinstance(user_id, int):
@ -34,7 +42,7 @@ class UsersNamespace(Namespace):
}
@socketio_login_required
def on_subscribe_user(self, user_hashid: str) -> dict:
def on_subscribe(self, user_hashid: str) -> dict:
user_id = hashids.decode(user_hashid)
if not isinstance(user_id, int):
@ -56,7 +64,7 @@ class UsersNamespace(Namespace):
return {'status': 200, 'statusText': 'OK'}
@socketio_login_required
def on_unsubscribe_user(self, user_hashid: str) -> dict:
def on_unsubscribe(self, user_hashid: str) -> dict:
user_id = hashids.decode(user_hashid)
if not isinstance(user_id, int):
@ -76,3 +84,33 @@ class UsersNamespace(Namespace):
leave_room(f'/users/{user.hashid}')
return {'status': 200, 'statusText': 'OK'}
@socketio_login_required
def on_delete(self, user_hashid: str) -> dict:
user_id = hashids.decode(user_hashid)
if not isinstance(user_id, int):
return {'status': 400, 'statusText': 'Bad Request'}
user = User.query.get(user_id)
if user is None:
return {'status': 404, 'statusText': 'Not found'}
if not (
user == current_user
or current_user.is_administrator
):
return {'status': 403, 'statusText': 'Forbidden'}
socketio.start_background_task(
_delete_user,
current_app._get_current_object(),
user.id
)
return {
'body': f'User "{user.username}" marked for deletion',
'status': 202,
'statusText': 'Accepted'
}

View File

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

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

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

View File

@ -1,36 +1,3 @@
/* #region sidenav-fixed */
/*
* The sidenav-fixed class is used which causes the sidenav to be fixed and open
* on large screens and hides to the regular functionality on smaller screens.
* In order to prevent the sidenav to overlap the content, the content (header, main and footer)
* gets an offset equal to the width of the sidenav.
*
* Read more: https://materializecss.com/sidenav.html#variations
*/
body[data-sidenav-fixed="true" i] main,
body[data-sidenav-fixed="true" i] footer {
padding-left: 300px;
}
@media only screen and (max-width: 992px) {
body[data-sidenav-fixed="true" i] main,
body[data-sidenav-fixed="true" i] footer {
padding-left: 0;
}
body[data-sidenav-fixed="true" i] .navbar-fixed > nav {
width: 100%;
}
}
@media only screen and (min-width: 993px) {
body[data-sidenav-fixed="true" i] .sidenav-fixed {
margin-top: 64px;
z-index: 996;
}
}
/* #endregion sidenav-fixed */
/* #region sticky-footer */
/*
* Sticky Footer:
@ -43,13 +10,13 @@ body[data-sidenav-fixed="true" i] footer {
*
* Read more: https://materializecss.com/footer.html#sticky-footer
*/
body[data-sticky-footer="true" i] {
body {
display: flex;
min-height: 100vh;
flex-direction: column;
}
body[data-sticky-footer="true" i] main {
main {
flex: 1 0 auto;
}
/* #endregion sticky-footer */

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

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,32 +0,0 @@
<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>

View File

@ -1,6 +1,60 @@
{% if current_user.is_authenticated %}
<ul class="dropdown-content" id="nav-account-dropdown-content">
<li><a href="{{ url_for('settings.settings') }}"><i class="material-icons left">settings</i>Settings</a></li>
<li><a href="{{ url_for('auth.logout') }}"><i class="material-icons left">logout</i>Log out</a></li>
<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

@ -1,40 +1,45 @@
<div class="container">
<div class="row">
<div class="col s6 m3">
<a href="https://www.dfg.de/">
<img class="responsive-img" src="{{ url_for('static', filename='images/logo_-_dfg.gif') }}">
</a>
</div>
<div class="col s6 m3 offset-m1 center-align">
<a href="https://www.uni-bielefeld.de/sfb1288/">
<img class="responsive-img" src="{{ url_for('static', filename='images/logo_-_sfb_1288.png') }}">
</a>
</div>
<div class="col s12 m3 offset-m1">
<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>
<li></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">
<div class="row" style="margin-bottom: 0;">
<div class="col s12 m3">
<span>© 2020 Bielefeld University</span>
</div>
<div class="col s12 m2">
<span class="right"><b>Version {{ config.NOPAQUE_VERSION }}</b></span>
</div>
<div class="col s12 m7 right-align">
<a class="btn-small waves-effect waves-light" href="{{ url_for('main.faq') }}"><i class="left material-icons">info_outline</i>Frequently Asked Questions</a>
<a class="btn-small waves-effect waves-light" href="mailto:{{ config.NOPAQUE_SERVICE_DESK }}"><i class="left material-icons">mail</i>Report an issue</a>
<a class="btn-small waves-effect waves-light" href="https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque" target="_blank"><i class="left material-icons">code</i>GitLab</a>
</div>
</div>
© 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

@ -1,3 +1,34 @@
{% if current_user.is_authenticated and not current_user.terms_of_use_accepted %}
{% include "_base/_modals/terms_of_use.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 %}

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

@ -1,38 +0,0 @@
{% if current_user.is_authenticated %}
<!-- menu icon -->
<!-- small/medium devices -->
<a href="#!" class="sidenav-trigger" data-target="sidenav"><i class="material-icons">menu</i></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>
<!-- large devices -->
<a href="{{ url_for('main.index') }}" class="brand-logo hide-on-med-and-down" style="width: 300px; height: 100%;">
<img src="{{ url_for('static', filename='images/nopaque_-_logo+wordmark.png') }}" alt="" class="py-3 ml-4" style="height: 100%;">
</a>
<!-- right aligned navigation links -->
<!-- large devices -->
<ul class="right hide-on-med-and-down" style="height: 64px;">
{% if current_user.is_authenticated %}
<!-- avatar, username and email -->
<li style="height: 100%;">
<a href="#!" class="dropdown-trigger no-autoinit" data-target="nav-account-dropdown-content" id="nav-account-dropdown-trigger" style="height: 100%;">
<img src="{{ url_for('users.user_avatar', user_id=current_user.id) }}" alt="" class="left circle py-3" style="height: 100%;">
<span class="ml-2">{{ current_user.username }} ({{ current_user.email }})</span>
</a>
</li>
{% else %}
<!-- log in -->
<li {% if request.path == url_for('auth.login') %}class="active"{% endif %}>
<a href="{{ url_for('auth.login') }}"><i class="material-icons left">login</i>Log in</a>
</li>
<!-- register -->
<li {% if request.path == url_for('auth.register') %}class="active"{% endif %}>
<a href="{{ url_for('auth.register') }}"><i class="material-icons left">assignment</i>Register</a>
</li>
{% endif %}
</ul>

View File

@ -9,6 +9,13 @@
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',
@ -77,26 +84,22 @@
const app = new nopaque.App();
app.init();
{% if current_user.is_authenticated -%}
// TODO: Set this as a property of the app object
{% if current_user.is_authenticated %}
const currentUserId = {{ current_user.hashid|tojson }};
// Subscribe to the current user's data events
app.subscribeUser(currentUserId)
app.userHub.add(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 -%}
{% if not current_user.terms_of_use_accepted %}
M.Modal.getInstance(document.querySelector('#terms-of-use-modal')).open();
{% endif -%}
{% endif -%}
{% endif %}
{% else %}
const currentUserId = null;
{% endif %}
// Display flashed messages
for (let [category, message] of {{ get_flashed_messages(with_categories=True)|tojson }}) {
app.flash(message, message);
app.ui.flash(message, message);
}
</script>
@ -104,7 +107,7 @@
let languageModalSwitch = document.querySelector('#terms-of-use-modal-switch');
let termsOfUseModalContent = document.querySelectorAll('.terms-of-use-modal-content');
if (languageModalSwitch) {
languageModalSwitch.addEventListener('change', function() {
languageModalSwitch.addEventListener('change', () => {
termsOfUseModalContent.forEach(content => {
content.classList.toggle('hide');
});

View File

@ -1,7 +1,7 @@
<ul class="sidenav sidenav-fixed" id="sidenav">
<ul class="sidenav" id="sidenav">
{% if current_user.is_authenticated %}
{# user view #}
{# shown for small/medium devices #}
<li class="hide-on-large-only">
<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>
@ -9,86 +9,115 @@
<a><span class="white-text email">{{ current_user.email }}</span></a>
</div>
</li>
{% endif %}
{# general items #}
{% if current_user.can('USE_API') %}
<li>
<a class="waves-effect" href="{{ url_for('apifairy.docs') }}"><i class="material-icons">api</i>API</a>
{% 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 %}
<li>
<a class="waves-effect" href="{{ url_for('main.manual') }}"><i class="material-icons">school</i>Manual</a>
</li>
<li>
<a class="waves-effect" href="{{ url_for('main.news') }}"><i class="material-icons">email</i>News</a>
{# 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>
{# dashboard items #}
<li><div class="divider"></div></li>
<li><a class="subheader">Dashboard</a></li>
<li {% if request.path == url_for('corpora.corpora') %}class="active"{% endif %}>
<a class="waves-effect" href="{{ url_for('corpora.corpora') }}"><i class="nopaque-icons">I</i>My Corpora</a>
</li>
<li {% if request.path == url_for('jobs.jobs') %}class="active"{% endif %}>
<a class="waves-effect" href="{{ url_for('jobs.jobs') }}"><i class="nopaque-icons">J</i>My Jobs</a>
</li>
<li>
<a class="waves-effect" href="{{ url_for('contributions.index') }}"><i class="material-icons">new_label</i>My Contributions</a>
{# 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>
{# processes & services items #}
{% if current_user.is_authenticated %}
{# data processing & analysis section #}
<li><div class="divider"></div></li>
<li><a class="subheader">Processes & Services</a></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 %}
{# social items #}
{# account section #}
<li><div class="divider"></div></li>
<li><a class="subheader">Social</a></li>
<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>
<li>
<a class="waves-effect" href="{{ url_for('main.social_area', _anchor='public-users') }}"><i class="material-icons">group</i>Public Users</a>
</li>
<li>
<a class="waves-effect" href="{{ url_for('main.social_area', _anchor='public-corpora') }}"><i class="nopaque-icons">I</i>Public Corpora</a>
{# 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>
{# administration items #}
{% if current_user.can('ADMINISTRATE') %}
{# 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 %}
{# account items #}
<li class="hide-on-large-only"><div class="divider"></div></li>
<li class="hide-on-large-only"><a class="subheader">Account</a></li>
<li class="hide-on-large-only">
<a class="waves-effect" href="{{ url_for('settings.settings') }}"><i class="material-icons">settings</i>Settings</a>
</li>
<li class="hide-on-large-only">
<a class="waves-effect" href="{{ url_for('auth.logout') }}"><i class="material-icons">logout</i>Log out</a>
</li>
</ul>

View File

@ -7,6 +7,7 @@
'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',
@ -15,7 +16,8 @@
'css/s-attr-colors.css',
'css/spacing.css',
'css/status-spinner.css',
'css/utils.css'
-%}
'css/utils.css',
'css/width.css'
%}
<link href="{{ ASSET_URL }}" rel="stylesheet">
{% endassets -%}
{% endassets %}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,103 +1,72 @@
{% if title is not defined %}
{% set title = 'nopaque' %}
{% endif %}
{% if sidenav_fixed is not defined %}
{% set sidenav_fixed = current_user.is_authenticated %}
{% endif %}
{% if sticky_footer is not defined %}
{% set sticky_footer = true %}
{% endif %}
{% if navbar_fixed is not defined %}
{% set navbar_fixed = true %}
{% endif %}
{% if navbar_extended is not defined %}
{% set navbar_extended = false %}
{% set title = 'nopaque' %}
{% endif %}
{% block doc %}
<!DOCTYPE html>
<html {% block html_attribs %}lang="en"{% endblock html_attribs %}>
<html {% block html_attributes %}lang="en"{% endblock html_attributes %}>
{% block html %}
<head>
<head {% block head_attributes %}{% endblock head_attributes %}>
{% block head %}
{% block metas %}
{% include "_base/metas.html.j2" %}
{% include '_base/metas.html.j2' %}
{% endblock metas %}
<title {% block title_attribs %}{% endblock title_attribs %}>
{%- block title %}{{ title }}{% endblock title -%}
{% 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 %}
{% block stylesheets %}
{% include "_base/stylesheets.html.j2" %}
{% endblock stylesheets %}
{% block styles %}
{% include '_base/stylesheets.html.j2' %}
{% endblock styles %}
{% endblock head %}
</head>
<body {% block body_attribs %}data-sidenav-fixed="{{ sidenav_fixed }}" data-sticky-footer="{{ sticky_footer }}"{% endblock body_attribs %}>
<body {% block body_attributes %}{% endblock body_attributes %}>
{% block body %}
<header {% block header_attribs %}{% endblock header_attribs %}>
<header {% block header_attributes %}{% endblock header_attributes %}>
{% block header %}
{% if navbar_fixed %}
<div class="navbar-fixed">
{% endif %}
<nav {% block navbar_attribs %}{% if navbar_extended %}class="nav-extended"{% endif %}{% endblock navbar_attribs %}>
{% block navbar %}
<div {% block navbar_primary_content_attribs %}class="nav-wrapper"{% endblock navbar_primary_content_attribs %}>
{% block navbar_primary_content %}
{% include "_base/navbar_primary_content.html.j2" %}
{% endblock navbar_primary_content %}
</div>
{% if navbar_extended %}
<div {% block navbar_secondary_content_attribs %}class="nav-content"{% endblock navbar_secondary_content_attribs %}>
{% block navbar_secondary_content %}
{% endblock navbar_secondary_content %}
</div>
{% endif %}
{% include '_base/navbar.html.j2' %}
{% endblock navbar %}
</nav>
{% if navbar_fixed %}
</div>
{% endif %}
{% block sidenav %}
{% if current_user.is_authenticated %}
{% include "_base/sidenav.html.j2" %}
{% endif %}
{% include '_base/sidenav.html.j2' %}
{% endblock sidenav %}
{% endblock header %}
</header>
<main {% block main_attribs %}{% endblock main_attribs %}>
<main {% block main_attributes %}{% endblock main_attributes %}>
{% block main %}
{% block page_content %}{% endblock page_content %}
{% endblock main %}
<div id="dropdowns">
{% block dropdowns %}
{% include "_base/dropdowns.html.j2" %}
{% endblock dropdowns %}
</div>
<div id="modals">
{% block modals %}
{% include "_base/modals.html.j2" %}
{% endblock modals %}
</div>
</main>
<footer {% block footer_attribs %}class="page-footer"{% endblock footer_attribs %}>
<footer {% block footer_attributes %}class="page-footer"{% endblock footer_attributes %}>
{% block footer %}
{% include "_base/footer.html.j2" %}
{% include '_base/footer.html.j2' %}
{% endblock footer %}
</footer>
<div {% block dropdowns_attributes %}id="dropdowns"{% endblock dropdowns_attributes %}>
{% block dropdowns %}
{% include '_base/dropdowns.html.j2' %}
{% endblock dropdowns %}
</div>
<div {% block modals_attributes %}id="modals"{% endblock modals_attributes %}>
{% block modals %}
{% include '_base/modals.html.j2' %}
{% endblock modals %}
</div>
{% block scripts %}
{% include "_base/scripts.html.j2" %}
{% include '_base/scripts.html.j2' %}
{% endblock scripts %}
{% endblock body %}
</body>

View File

@ -0,0 +1,48 @@
{% extends "base.html.j2" %}
{% block page_content %}
<div class="container">
<div class="row">
<div class="col s12">
<h1 id="title">{{ title }}</h1>
<p>
Upload your own language models into nopaque. This is useful for
working with different languages that are not available as standard in
nopaque or if a you want to work with a language model that you have
developed by yourself. Uploaded models can be found in the model list
of the corresponding service and can be used immediately.
</p>
</div>
<div class="col s12 l4">
<h4>Tesseract OCR Pipeline Models</h4>
</div>
<div class="col s12 l8">
<div class="card">
<div class="card-content">
<div class="tesseract-ocr-pipeline-model-list" data-user-id="{{ current_user.hashid }}"></div>
</div>
<div class="card-action right-align">
<a href="{{ url_for('.tesseract_ocr_pipeline_models.create') }}" class="btn waves-effect waves-light"><i class="material-icons left">add</i>Create</a>
</div>
</div>
</div>
<div class="col s12 l4">
<h4>SpaCy NLP Pipeline Models</h4>
</div>
<div class="col s12 l8">
<div class="card">
<div class="card-content">
<div class="spacy-nlp-pipeline-model-list" data-user-id="{{ current_user.hashid }}"></div>
</div>
<div class="card-action right-align">
<a href="{{ url_for('.spacy_nlp_pipeline_models.create') }}" class="btn waves-effect waves-light"><i class="material-icons left">add</i>Create</a>
</div>
</div>
</div>
</div>
</div>
{% endblock page_content %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -41,97 +41,58 @@
<div class="job-list" data-user-id="{{ current_user.hashid }}"></div>
</div>
<div class="card-action right-align">
<p><a class="btn modal-trigger waves-effect waves-light" data-target="create-job-modal">Create job<i class="material-icons right">add</i></a></p>
<p><a data-target="dashboard-create-job-dropdown-content" class="btn waves-effect waves-light dropdown-trigger no-autoinit" id="dashboard-create-job-dropdown-trigger">Create job<i class="material-icons right">add</i></a></p>
</div>
</div>
</div>
<div class="col s12" id="contributions">
<h2>My Contributions</h2>
</div>
<div class="col s4">
<div class="card extension-selector hoverable service-color" data-service="tesseract-ocr-pipeline">
<a href="{{ url_for('tesseract_ocr_pipeline_models.index') }}" style="position: absolute; width: 100%; height: 100%;"></a>
<div class="card-content">
<span class="card-title">Tesseract OCR Pipeline Models</span>
<p>Here you can see and edit the models that you have created. You can also create new models.</p>
</div>
</div>
</div>
<div class="col s4">
<div class="card extension-selector hoverable service-color" data-service="spacy-nlp-pipeline">
<a href="{{ url_for('spacy_nlp_pipeline_models.index') }}" style="position: absolute; width: 100%; height: 100%;"></a>
<div class="card-content">
<span class="card-title">SpaCy NLP Pipeline Models</span>
<p>Here you can see and edit the models that you have created. You can also create new models.</p>
</div>
</div>
</div>
{% if config.NOPAQUE_TRANSKRIBUS_ENABLED %}
<div class="col s4">
<div class="card extension-selector hoverable service-color" data-service="transkribus-htr-pipeline">
<a href="{{ url_for('transkribus_htr_pipeline_models.index') }}" style="position: absolute; width: 100%; height: 100%;"></a>
<div class="card-content">
<span class="card-title">Transkribus HTR Pipeline Models</span>
<p>Here you can see and edit the models that you have created. You can also create new models.</p>
</div>
</div>
</div>
{% endif %}
</div>
</div>
{% endblock page_content %}
{% block modals %}
{% block dropdowns %}
{{ super() }}
<div id="create-job-modal" class="modal">
<div class="modal-content">
<h4>Select a service</h4>
<p>&nbsp;</p>
<div class="row">
<div class="col s12 m4">
<div class="card-panel center-align hoverable">
<br>
<a href="{{ url_for('services.file_setup_pipeline') }}" class="btn-floating btn-large waves-effect waves-light" style="transform: scale(2);">
<i class="nopaque-icons service-color darken service-icons" data-service="file-setup-pipeline"></i>
<ul class="dropdown-content" id="dashboard-create-job-dropdown-content">
<li>
<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>
<br><br>
<p class="service-color-text darken" data-service="file-setup-pipeline"><b>File setup</b></p>
<p class="light">Digital copies of text based research data (books, letters, etc.) often comprise various files and formats. nopaque converts and merges those files to facilitate further processing.</p>
<a href="{{ url_for('services.file_setup_pipeline') }}" class="waves-effect waves-light btn service-color darken" data-service="file-setup-pipeline">Create Job</a>
</div>
</div>
<div class="col s12 m4">
<div class="card-panel center-align hoverable">
<br>
<a href="{{ url_for('services.tesseract_ocr_pipeline') }}" class="btn-floating btn-large waves-effect waves-light" style="transform: scale(2);">
<i class="nopaque-icons service-color darken service-icons" data-service="tesseract-ocr-pipeline" style="font-size: 2.5rem;"></i>
</li>
<li>
<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>
<br><br>
<p class="service-color-text darken" data-service="tesseract-ocr-pipeline"><b>Optical Character Recognition</b></p>
<p class="light">nopaque converts your image data like photos or scans into text data through a process called OCR. This step enables you to proceed with further computational analysis of your documents.</p>
<a href="{{ url_for('services.tesseract_ocr_pipeline') }}" class="waves-effect waves-light btn service-color darken" data-service="tesseract-ocr-pipeline">Create Job</a>
</div>
</div>
<div class="col s12 m4">
<div class="card-panel center-align hoverable">
<br>
<a href="{{ url_for('services.spacy_nlp_pipeline') }}" class="btn-floating btn-large waves-effect waves-light" style="transform: scale(2);">
<i class="nopaque-icons service-color darken service-icons" data-service="spacy-nlp-pipeline" style="font-size: 2.5rem;"></i>
</li>
{% if config.NOPAQUE_TRANSKRIBUS_ENABLED %}
<li>
<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>
<br><br>
<p class="service-color-text darken" data-service="spacy-nlp-pipeline"><b>Natural Language Processing</b></p>
<p class="light">By means of computational linguistic data processing (tokenization, lemmatization, part-of-speech tagging and named-entity recognition) nopaque extracts additional information from your text.</p>
<a href="{{ url_for('services.spacy_nlp_pipeline') }}" class="waves-effect waves-light btn service-color darken" data-service="spacy-nlp-pipeline">Create Job</a>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<a class="btn-flat modal-close waves-effect waves-light">Close</a>
</div>
</div>
{% endblock modals %}
</li>
{% endif %}
<li>
<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>
</ul>
{% endblock dropdowns %}
{% block scripts %}
{{ super() }}
<script>
M.Dropdown.init(
document.querySelector('#dashboard-create-job-dropdown-trigger'),
{
constrainWidth: false,
container: document.querySelector('#dropdowns'),
coverTrigger: false
}
);
</script>
{% endblock scripts %}

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
{% extends "base.html.j2" %}
{% import "wtf.html.j2" as wtf %}
{% block main_attribs %}class="service-color lighten" data-service="tesseract-ocr-pipeline"{% endblock main_attribs %}
{% block main_attributes %}class="service-color lighten" data-service="tesseract-ocr-pipeline"{% endblock main_attributes %}
{% block page_content %}
<div class="container">
@ -14,12 +14,12 @@
<div class="center-align">
<p class="hide-on-small-only">&nbsp;</p>
<p class="hide-on-small-only">&nbsp;</p>
<i class="large nopaque-icons service-icons service-color-text text-darken" data-service="tesseract-ocr-pipeline"></i>
<i class="large nopaque-icons service-icons service-color-text text-darken"></i>
</div>
</div>
<div class="col s12 m9 pull-m3">
<div class="card service-color-border border-darken" data-service="tesseract-ocr-pipeline" style="border-top: 10px solid;">
<div class="card service-color-border border-darken" style="border-top: 10px solid;">
<div class="card-content">
<div class="row">
<div class="col s12">
@ -56,7 +56,7 @@
{{ form.model.label }}
<span class="helper-text">
<a class="modal-trigger tooltipped" href="#models-modal" data-position="bottom" data-tooltip="See more information about models"><i class="material-icons">help_outline</i></a>
<a class="tooltipped" href="{{ url_for('tesseract_ocr_pipeline_models.create') }}" data-position="bottom" data-tooltip="Add your own Tesseract OCR models"><i class="material-icons">new_label</i></a>
<a class="tooltipped" href="{{ url_for('contributions.tesseract_ocr_pipeline_models.create') }}" data-position="bottom" data-tooltip="Add your own Tesseract OCR models"><i class="material-icons">new_label</i></a>
</span>
{% for error in form.model.errors %}
<span class="helper-text error-color-text">{{ error }}</span>
@ -145,15 +145,26 @@
{% block scripts %}
{{ super() }}
<script>
function initPage() {
let createJobFormPdfElement = document.querySelector('#{{ form.pdf.id }}');
createJobFormPdfElement.parentElement.classList.add('service-color', 'darken');
// Disable user model selection if no models are available
{% if user_tesseract_ocr_pipeline_models_count == 0 %}
optionGroupOptions = document.querySelectorAll(".optgroup-option");
optionGroupOptions[0].classList.add("disabled");
{% endif %}
{% if user_tesseract_ocr_pipeline_models_count == 0 %}
// Disable user model selection if no models are available
optionGroupOptions = document.querySelectorAll(".optgroup-option");
optionGroupOptions[0].classList.add("disabled");
{% endif %}
document.querySelector('#create-job-form-binarization').addEventListener('change', (event) => {
document.querySelector('#create-job-form-ocropus_nlbin_threshold-container').classList.toggle('hide');
});
let createJobFormBinarizationElement = document.querySelector('#{{ form.binarization.id }}');
let createJobFormOcropusNlbinThresholdContainerElement = document.querySelector('#create-job-form-ocropus_nlbin_threshold-container');
createJobFormBinarizationElement.addEventListener('change', (event) => {
createJobFormOcropusNlbinThresholdContainerElement.classList.toggle('hide');
});
let createJobFormSubmitElement = document.querySelector('#{{ form.submit.id }}');
createJobFormSubmitElement.classList.add('service-color', 'darken');
};
initPage();
</script>
{% endblock scripts %}

View File

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

View File

@ -1,24 +0,0 @@
{% extends "base.html.j2" %}
{% import "wtf.html.j2" as wtf %}
{% block page_content %}
<div class="container">
<div class="row">
<div class="col s12">
<h1 id="title">{{ title }}</h1>
<p>Here you can see and edit the models that you have created. You can also create new models.</p>
</div>
<div class="col s12">
<div class="card">
<div class="card-content">
<div class="spacy-nlp-pipeline-model-list" data-user-id="{{ current_user.hashid }}"></div>
</div>
<div class="card-action right-align">
<a href="{{ url_for('.create') }}" class="btn waves-effect waves-light"><i class="material-icons left">add</i>Create</a>
</div>
</div>
</div>
</div>
</div>
{% endblock page_content %}

View File

@ -1,25 +0,0 @@
{% extends "base.html.j2" %}
{% import "wtf.html.j2" as wtf %}
{% block page_content %}
<div class="container">
<div class="row">
<div class="col s12">
<h1 id="title">{{ title }}</h1>
<p>Here you can see and edit the models that you have created. You can also create new models.</p>
</div>
<div class="col s12">
<div class="card">
<div class="card-content">
<div class="tesseract-ocr-pipeline-model-list" data-user-id="{{ current_user.hashid }}"></div>
</div>
<div class="card-action right-align">
<a href="{{ url_for('.create') }}" class="btn waves-effect waves-light"><i class="material-icons left">add</i>Create</a>
</div>
</div>
</div>
</div>
</div>
{% endblock page_content %}

View File

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

View File

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