mirror of
https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
synced 2025-07-03 03:10:33 +00:00
Compare commits
29 Commits
6aacac2419
...
1.1.0
Author | SHA1 | Date | |
---|---|---|---|
713a7645db | |||
0c64c07925 | |||
a6ddf4c980 | |||
cab5f7ea05 | |||
07f09cdbd9 | |||
c97b2a886e | |||
df2bffe0fd | |||
aafb3ca3ec | |||
12a3ac1d5d | |||
a2904caea2 | |||
e325552100 | |||
e269156925 | |||
9c9de242ca | |||
ec54fdc3bb | |||
2263a8d27d | |||
143cdd91f9 | |||
b5f7478e14 | |||
a95b8d979d | |||
18d5ab160e | |||
7439edacef | |||
99d7a8bdfc | |||
54c4295bf7 | |||
1e5c26b8e3 | |||
9f56647cf7 | |||
460257294d | |||
2c43333c94 | |||
fc8b11fa66 | |||
a8ab1bee71 | |||
ee7f64f5be |
16
.vscode/settings.json
vendored
16
.vscode/settings.json
vendored
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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):
|
@ -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
|
@ -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):
|
@ -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
|
@ -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
|
||||
)
|
||||
|
@ -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
|
@ -1,7 +0,0 @@
|
||||
from flask import abort
|
||||
from . import bp
|
||||
|
||||
|
||||
@bp.route('/transkribus_htr_pipeline_models')
|
||||
def index():
|
||||
return abort(503)
|
@ -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
|
||||
|
@ -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 ## '''
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
@ -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
109
app/namespaces/jobs.py
Normal 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'
|
||||
}
|
@ -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'
|
||||
}
|
@ -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
47
app/static/css/height.css
Normal 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;
|
||||
}
|
@ -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
47
app/static/css/width.css
Normal 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;
|
||||
}
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
@ -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();
|
||||
}
|
||||
};
|
||||
|
1
app/static/js/app/endpoints/index.js
Normal file
1
app/static/js/app/endpoints/index.js
Normal file
@ -0,0 +1 @@
|
||||
nopaque.app.endpoints = {};
|
41
app/static/js/app/endpoints/users.js
Normal file
41
app/static/js/app/endpoints/users.js
Normal 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}`);
|
||||
}
|
||||
}
|
||||
}
|
1
app/static/js/app/extensions/index.js
Normal file
1
app/static/js/app/extensions/index.js
Normal file
@ -0,0 +1 @@
|
||||
nopaque.app.extensions = {};
|
56
app/static/js/app/extensions/toaster.js
Normal file
56
app/static/js/app/extensions/toaster.js
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
126
app/static/js/app/extensions/ui.js
Normal file
126
app/static/js/app/extensions/ui.js
Normal 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();});
|
||||
}
|
||||
}
|
68
app/static/js/app/extensions/user-hub.js
Normal file
68
app/static/js/app/extensions/user-hub.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
1
app/static/js/app/index.js
Normal file
1
app/static/js/app/index.js
Normal file
@ -0,0 +1 @@
|
||||
nopaque.app = {};
|
@ -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');
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
);
|
||||
|
@ -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');
|
||||
|
@ -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';}
|
||||
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
@ -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>
|
@ -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 %}
|
||||
|
@ -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">♥</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>
|
||||
|
1
app/templates/_base/icons.html.j2
Normal file
1
app/templates/_base/icons.html.j2
Normal file
@ -0,0 +1 @@
|
||||
<link href="{{ url_for('static', filename='images/nopaque_-_favicon.png') }}" rel="icon">
|
@ -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 %}
|
||||
|
105
app/templates/_base/navbar.html.j2
Normal file
105
app/templates/_base/navbar.html.j2
Normal 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>
|
@ -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>
|
@ -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');
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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 %}
|
||||
|
@ -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>
|
||||
|
@ -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 %}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
48
app/templates/contributions/index.html.j2
Normal file
48
app/templates/contributions/index.html.j2
Normal 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 %}
|
@ -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>.
|
||||
|
@ -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');}
|
||||
);
|
||||
|
||||
});
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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');}
|
||||
);
|
||||
|
||||
});
|
||||
|
@ -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">
|
||||
|
@ -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> </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 %}
|
||||
|
@ -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>
|
||||
|
@ -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"> </p>
|
||||
<p class="hide-on-small-only"> </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 %}
|
||||
|
@ -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"> </p>
|
||||
<p class="hide-on-small-only"> </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 %}
|
||||
|
@ -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"> </p>
|
||||
<p class="hide-on-small-only"> </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 %}
|
||||
|
@ -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"> </p>
|
||||
<p class="hide-on-small-only"> </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 %}
|
||||
|
@ -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 %}
|
@ -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 %}
|
@ -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 %}
|
||||
|
Reference in New Issue
Block a user