diff --git a/.vscode/settings.json b/.vscode/settings.json index 0967ef424bce6791893e9a57bb952f80fd536e93..c2a8567eeb3aa10c172abd83c00915f76c89762a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1 +1,22 @@ -{} +{ + "editor.rulers": [79], + "files.insertFinalNewline": true, + "[css]": { + "editor.tabSize": 2 + }, + "[scss]": { + "editor.tabSize": 2 + }, + "[html]": { + "editor.tabSize": 2 + }, + "[javascript]": { + "editor.tabSize": 2 + }, + "[jinja-html]": { + "editor.tabSize": 2 + }, + "[jinja-js]": { + "editor.tabSize": 2 + }, +} diff --git a/app/__init__.py b/app/__init__.py index cc747a89846a837f77acfba87bafd127b999c25f..dcb58e47b9943b4efcc77f9bc8f5ba060f9b0d78 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -54,6 +54,9 @@ def create_app(config: Config = Config) -> Flask: scheduler.init_app(app) socketio.init_app(app, message_queue=app.config['NOPAQUE_SOCKETIO_MESSAGE_QUEUE_URI']) # noqa + from .errors import init_app as init_error_handlers + init_error_handlers(app) + from .admin import bp as admin_blueprint app.register_blueprint(admin_blueprint, url_prefix='/admin') @@ -69,9 +72,6 @@ def create_app(config: Config = Config) -> Flask: from .corpora import bp as corpora_blueprint app.register_blueprint(corpora_blueprint, url_prefix='/corpora') - from .errors import bp as errors_blueprint - app.register_blueprint(errors_blueprint) - from .jobs import bp as jobs_blueprint app.register_blueprint(jobs_blueprint, url_prefix='/jobs') diff --git a/app/contributions/tesseract_ocr_pipeline_models/json_routes.py b/app/contributions/tesseract_ocr_pipeline_models/json_routes.py index 705fbdb46bc2fb3ce5a080b2e5ed46dbf742b54f..f4c4ca799bec6e42ca651f8f740b685c8c0b9bc5 100644 --- a/app/contributions/tesseract_ocr_pipeline_models/json_routes.py +++ b/app/contributions/tesseract_ocr_pipeline_models/json_routes.py @@ -39,8 +39,6 @@ def delete_tesseract_model(tesseract_ocr_pipeline_model_id): @permission_required('CONTRIBUTE') @content_negotiation(consumes='application/json', produces='application/json') def update_tesseract_ocr_pipeline_model_is_public(tesseract_ocr_pipeline_model_id): - # body: jsonify({'is_public': True}) - # body: jsonify(False) is_public = request.json if not isinstance(is_public, bool): abort(400) diff --git a/app/corpora/__init__.py b/app/corpora/__init__.py index 83cecec56186293b6c43ed147cb358c99224acca..f39ca22bcb8acf6528737e8cc66cd8864ac98b44 100644 --- a/app/corpora/__init__.py +++ b/app/corpora/__init__.py @@ -2,4 +2,10 @@ from flask import Blueprint bp = Blueprint('corpora', __name__) -from . import cqi_over_socketio, routes # noqa +from . import cqi_over_socketio, routes, json_routes # noqa + +from .files import bp as files_bp +bp.register_blueprint(files_bp, url_prefix='<hashid:corpus_id>/files') + +from .followers import bp as followers_bp +bp.register_blueprint(followers_bp, url_prefix='<hashid:corpus_id>/followers') diff --git a/app/corpora/files/__init__.py b/app/corpora/files/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2f47a450e713842199c04457a3110a87ab6ac384 --- /dev/null +++ b/app/corpora/files/__init__.py @@ -0,0 +1,8 @@ +from flask import Blueprint + + +TEMPLATE_FOLDER = 'corpora/files' + + +bp = Blueprint('files', __name__) +from . import routes, json_routes diff --git a/app/corpora/files/forms.py b/app/corpora/files/forms.py new file mode 100644 index 0000000000000000000000000000000000000000..f5b0f0c86492471840b76826897c131584723de1 --- /dev/null +++ b/app/corpora/files/forms.py @@ -0,0 +1,54 @@ +from flask_wtf import FlaskForm +from flask_wtf.file import FileField, FileRequired +from wtforms import ( + StringField, + SubmitField, + ValidationError, + IntegerField +) +from wtforms.validators import InputRequired, Length + + +class CorpusFileBaseForm(FlaskForm): + author = StringField( + 'Author', + validators=[InputRequired(), Length(max=255)] + ) + publishing_year = IntegerField( + 'Publishing year', + validators=[InputRequired()] + ) + title = StringField( + 'Title', + validators=[InputRequired(), Length(max=255)] + ) + address = StringField('Adress', validators=[Length(max=255)]) + booktitle = StringField('Booktitle', validators=[Length(max=255)]) + chapter = StringField('Chapter', validators=[Length(max=255)]) + editor = StringField('Editor', validators=[Length(max=255)]) + institution = StringField('Institution', validators=[Length(max=255)]) + journal = StringField('Journal', validators=[Length(max=255)]) + pages = StringField('Pages', validators=[Length(max=255)]) + publisher = StringField('Publisher', validators=[Length(max=255)]) + school = StringField('School', validators=[Length(max=255)]) + submit = SubmitField() + + +class CreateCorpusFileForm(CorpusFileBaseForm): + vrt = FileField('File', validators=[FileRequired()]) + + def __init__(self, *args, **kwargs): + if 'prefix' not in kwargs: + kwargs['prefix'] = 'create-corpus-file-form' + super().__init__(*args, **kwargs) + + def validate_vrt(self, field): + if not field.data.filename.lower().endswith('.vrt'): + raise ValidationError('VRT files only!') + + +class UpdateCorpusFileForm(CorpusFileBaseForm): + def __init__(self, *args, **kwargs): + if 'prefix' not in kwargs: + kwargs['prefix'] = 'update-corpus-file-form' + super().__init__(*args, **kwargs) diff --git a/app/corpora/files/json_routes.py b/app/corpora/files/json_routes.py new file mode 100644 index 0000000000000000000000000000000000000000..cddf4642ac9d901a4d80e76566190d222781c77e --- /dev/null +++ b/app/corpora/files/json_routes.py @@ -0,0 +1,42 @@ +from flask import current_app, jsonify +from flask_login import login_required +from threading import Thread +from app import db +from app.decorators import content_negotiation +from app.models import CorpusFile +from ..decorators import corpus_follower_permission_required +from . import bp + + +############################################################################## +# IMPORTANT NOTE: These routes are prefixed by the blueprint # +# Prefix: <hashid:corpus_id>/files # +# This implies that the corpus_id is always in the kwargs of # +# a route that is registered to this blueprint. # +############################################################################## + + +@bp.route('/<hashid:corpus_file_id>', methods=['DELETE']) +@login_required +@corpus_follower_permission_required('REMOVE_CORPUS_FILE') +@content_negotiation(produces='application/json') +def delete_corpus_file(corpus_id, corpus_file_id): + def _delete_corpus_file(app, corpus_file_id): + with app.app_context(): + corpus_file = CorpusFile.query.get(corpus_file_id) + corpus_file.delete() + db.session.commit() + + corpus_file = CorpusFile.query.filter_by(corpus_id=corpus_id, id=corpus_file_id).first_or_404() + thread = Thread( + target=_delete_corpus_file, + args=(current_app._get_current_object(), corpus_file.id) + ) + thread.start() + response_data = { + 'message': f'Corpus File "{corpus_file.title}" marked for deletion', + 'category': 'corpus' + } + response = jsonify(response_data) + response.status_code = 202 + return response diff --git a/app/corpora/files/routes.py b/app/corpora/files/routes.py new file mode 100644 index 0000000000000000000000000000000000000000..7715f4174d7905783fdcdf605153b1d4585ecb47 --- /dev/null +++ b/app/corpora/files/routes.py @@ -0,0 +1,101 @@ +from flask import ( + abort, + flash, + Markup, + redirect, + render_template, + send_from_directory +) +from flask_login import current_user, login_required +import os +from app import db +from app.models import Corpus, CorpusFile, CorpusStatus +from ..decorators import corpus_follower_permission_required +from . import bp, TEMPLATE_FOLDER +from .forms import CreateCorpusFileForm, UpdateCorpusFileForm + + +############################################################################## +# IMPORTANT NOTE: These routes are prefixed by the blueprint # +# Prefix: <hashid:corpus_id>/files # +# This implies that the corpus_id is always in the kwargs of # +# a route that is registered to this blueprint. # +############################################################################## + + +@bp.route('/create', methods=['GET', 'POST']) +@login_required +@corpus_follower_permission_required('ADD_CORPUS_FILE') +def create_corpus_file(corpus_id): + corpus = Corpus.query.get_or_404(corpus_id) + form = CreateCorpusFileForm() + if form.is_submitted(): + if not form.validate(): + response = {'errors': form.errors} + return response, 400 + try: + corpus_file = CorpusFile.create( + form.vrt.data, + address=form.address.data, + author=form.author.data, + booktitle=form.booktitle.data, + chapter=form.chapter.data, + editor=form.editor.data, + institution=form.institution.data, + journal=form.journal.data, + pages=form.pages.data, + publisher=form.publisher.data, + publishing_year=form.publishing_year.data, + school=form.school.data, + title=form.title.data, + mimetype='application/vrt+xml', + corpus=corpus + ) + except (AttributeError, OSError): + abort(500) + corpus.status = CorpusStatus.UNPREPARED + db.session.commit() + flash(f'Corpus File "{corpus_file.filename}" added', category='corpus') + return '', 201, {'Location': corpus.url} + return render_template( + f'{TEMPLATE_FOLDER}/create_corpus_file.html.j2', + corpus=corpus, + form=form, + title='Add corpus file' + ) + + +@bp.route('/<hashid:corpus_file_id>', methods=['GET', 'POST']) +@login_required +@corpus_follower_permission_required('UPDATE_CORPUS_FILE') +def corpus_file(corpus_id, corpus_file_id): + corpus_file = CorpusFile.query.filter_by(corpus_id=corpus_id, id=corpus_file_id).first_or_404() + form = UpdateCorpusFileForm(data=corpus_file.to_json_serializeable()) + if form.validate_on_submit(): + form.populate_obj(corpus_file) + if db.session.is_modified(corpus_file): + corpus_file.corpus.status = CorpusStatus.UNPREPARED + db.session.commit() + flash(f'Corpus file "{corpus_file.filename}" updated', category='corpus') + return redirect(corpus_file.corpus.url) + return render_template( + f'{TEMPLATE_FOLDER}/corpus_file.html.j2', + corpus=corpus_file.corpus, + corpus_file=corpus_file, + form=form, + title='Edit corpus file' + ) + + +@bp.route('/<hashid:corpus_file_id>/download') +@login_required +@corpus_follower_permission_required('VIEW') +def download_corpus_file(corpus_id, corpus_file_id): + corpus_file = CorpusFile.query.filter_by(corpus_id=corpus_id, id=corpus_file_id).first_or_404() + return send_from_directory( + os.path.dirname(corpus_file.path), + os.path.basename(corpus_file.path), + as_attachment=True, + attachment_filename=corpus_file.filename, + mimetype=corpus_file.mimetype + ) diff --git a/app/corpora/followers/__init__.py b/app/corpora/followers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e1e6d14e80602669b7762857ff9d1fd3ee5bc15d --- /dev/null +++ b/app/corpora/followers/__init__.py @@ -0,0 +1,5 @@ +from flask import Blueprint + + +bp = Blueprint('followers', __name__) +from . import json_routes diff --git a/app/corpora/followers/json_routes.py b/app/corpora/followers/json_routes.py new file mode 100644 index 0000000000000000000000000000000000000000..990717313f02ef02249f7c9599380e53289e381e --- /dev/null +++ b/app/corpora/followers/json_routes.py @@ -0,0 +1,87 @@ +from flask import abort, jsonify, request +from flask_login import current_user, login_required +from app import db +from app.decorators import content_negotiation +from app.models import ( + Corpus, + CorpusFollowerAssociation, + CorpusFollowerRole, + User +) +from ..decorators import corpus_owner_or_admin_required +from . import bp + + +############################################################################## +# IMPORTANT NOTE: These routes are prefixed by the blueprint # +# Prefix: <hashid:corpus_id>/followers # +# This implies that the corpus_id is always in the kwargs of # +# a route that is registered to this blueprint. # +############################################################################## + + +@bp.route('', methods=['POST']) +@login_required +@corpus_owner_or_admin_required +@content_negotiation(consumes='application/json', produces='application/json') +def create_corpus_followers(corpus_id): + usernames = request.json + if not (isinstance(usernames, list) or all(isinstance(u, str) for u in usernames)): + abort(400) + corpus = Corpus.query.get_or_404(corpus_id) + for username in usernames: + user = User.query.filter_by(username=username, is_public=True).first_or_404() + user.follow_corpus(corpus) + db.session.commit() + resonse_data = { + 'message': f'Users are now following "{corpus.title}"', + 'category': 'corpus' + } + response = jsonify(resonse_data) + response.status_code = 200 + return response + + +@bp.route('/<hashid:follower_id>/role', methods=['PUT']) +@login_required +@corpus_owner_or_admin_required +@content_negotiation(consumes='application/json', produces='application/json') +def update_corpus_follower_role(corpus_id, follower_id): + role_name = request.json + if not isinstance(role_name, str): + abort(400) + cfr = CorpusFollowerRole.query.filter_by(name=role_name).first() + if cfr is None: + abort(400) + cfa = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=follower_id).first_or_404() + cfa.role = cfr + db.session.commit() + resonse_data = { + 'message': f'User "{cfa.follower.username}" is now {cfa.role.name}', + 'category': 'corpus' + } + response = jsonify(resonse_data) + response.status_code = 200 + return response + + +@bp.route('/<hashid:follower_id>', methods=['DELETE']) +@login_required +@content_negotiation(produces='application/json') +def delete_corpus_follower(corpus_id, follower_id): + corpus = Corpus.query.get_or_404(corpus_id) + follower = User.query.get_or_404(follower_id) + if not (corpus.user == current_user or follower == current_user or current_user.is_administrator()): + abort(403) + if not follower.is_following_corpus(corpus): + abort(409) + follower.unfollow_corpus(corpus) + db.session.commit() + response_data = { + 'message': \ + f'"{follower.username}" is not following "{corpus.title}" anymore', + 'category': 'corpus' + } + response = jsonify(response_data) + response.status_code = 200 + return response diff --git a/app/corpora/forms.py b/app/corpora/forms.py index 8403e621932deac8779a6f1b4fc27f9322984a29..36ee34c8ccc43c754f46b8d5d98bba53e1fd139e 100644 --- a/app/corpora/forms.py +++ b/app/corpora/forms.py @@ -1,13 +1,5 @@ from flask_wtf import FlaskForm -from flask_wtf.file import FileField, FileRequired -from wtforms import ( - BooleanField, - StringField, - SubmitField, - TextAreaField, - ValidationError, - IntegerField -) +from wtforms import StringField, SubmitField, TextAreaField from wtforms.validators import InputRequired, Length @@ -34,50 +26,5 @@ class UpdateCorpusForm(CorpusBaseForm): super().__init__(*args, **kwargs) -class CorpusFileBaseForm(FlaskForm): - author = StringField( - 'Author', - validators=[InputRequired(), Length(max=255)] - ) - publishing_year = IntegerField( - 'Publishing year', - validators=[InputRequired()] - ) - title = StringField( - 'Title', - validators=[InputRequired(), Length(max=255)] - ) - address = StringField('Adress', validators=[Length(max=255)]) - booktitle = StringField('Booktitle', validators=[Length(max=255)]) - chapter = StringField('Chapter', validators=[Length(max=255)]) - editor = StringField('Editor', validators=[Length(max=255)]) - institution = StringField('Institution', validators=[Length(max=255)]) - journal = StringField('Journal', validators=[Length(max=255)]) - pages = StringField('Pages', validators=[Length(max=255)]) - publisher = StringField('Publisher', validators=[Length(max=255)]) - school = StringField('School', validators=[Length(max=255)]) - submit = SubmitField() - - -class CreateCorpusFileForm(CorpusFileBaseForm): - vrt = FileField('File', validators=[FileRequired()]) - - def __init__(self, *args, **kwargs): - if 'prefix' not in kwargs: - kwargs['prefix'] = 'create-corpus-file-form' - super().__init__(*args, **kwargs) - - def validate_vrt(self, field): - if not field.data.filename.lower().endswith('.vrt'): - raise ValidationError('VRT files only!') - - -class UpdateCorpusFileForm(CorpusFileBaseForm): - def __init__(self, *args, **kwargs): - if 'prefix' not in kwargs: - kwargs['prefix'] = 'update-corpus-file-form' - super().__init__(*args, **kwargs) - - class ImportCorpusForm(FlaskForm): pass diff --git a/app/corpora/json_routes.py b/app/corpora/json_routes.py new file mode 100644 index 0000000000000000000000000000000000000000..0494e1e5e81a3888b47441010bfe84f620a369d8 --- /dev/null +++ b/app/corpora/json_routes.py @@ -0,0 +1,130 @@ +from datetime import datetime +from flask import ( + abort, + current_app, + jsonify, + request, + url_for +) +from flask_login import current_user, login_required +from threading import Thread +from .decorators import corpus_follower_permission_required, corpus_owner_or_admin_required +from app import db, hashids +from app.decorators import content_negotiation +from app.models import Corpus, CorpusFollowerRole +from . import bp + + +@bp.route('/<hashid:corpus_id>', methods=['DELETE']) +@login_required +@corpus_owner_or_admin_required +@content_negotiation(produces='application/json') +def delete_corpus(corpus_id): + def _delete_corpus(app, corpus_id): + with app.app_context(): + corpus = Corpus.query.get(corpus_id) + corpus.delete() + db.session.commit() + + corpus = Corpus.query.get_or_404(corpus_id) + thread = Thread( + target=_delete_corpus, + args=(current_app._get_current_object(), corpus.id) + ) + thread.start() + response_data = { + 'message': f'Corpus "{corpus.title}" marked for deletion', + 'category': 'corpus' + } + response = jsonify(response_data) + response.status_code = 200 + return response + + +@bp.route('/<hashid:corpus_id>/build', methods=['POST']) +@login_required +@corpus_owner_or_admin_required +@content_negotiation(produces='application/json') +def build_corpus(corpus_id): + def _build_corpus(app, corpus_id): + with app.app_context(): + corpus = Corpus.query.get(corpus_id) + corpus.build() + db.session.commit() + + print(corpus_id) + corpus = Corpus.query.get_or_404(corpus_id) + if len(corpus.files.all()) == 0: + abort(409) + thread = Thread( + target=_build_corpus, + args=(current_app._get_current_object(), corpus_id) + ) + thread.start() + response_data = { + 'message': f'Corpus "{corpus.title}" marked for building', + 'category': 'corpus' + } + response = jsonify(response_data) + response.status_code = 202 + return response + + +@bp.route('/<hashid:corpus_id>/generate-share-link', methods=['POST']) +@login_required +@corpus_follower_permission_required('GENERATE_SHARE_LINK') +@content_negotiation(consumes='application/json', produces='application/json') +def generate_corpus_share_link(corpus_id): + corpus_hashid = hashids.encode(corpus_id) + data = request.json + if not isinstance(data, dict): + abort(400) + expiration = data.get('expiration') + if not isinstance(expiration, str): + abort(400) + role_name = data.get('role') + if not isinstance(role_name, str): + abort(400) + expiration_date = datetime.strptime(expiration, '%b %d, %Y') + cfr = CorpusFollowerRole.query.filter_by(name=role_name).first() + if cfr is None: + abort(400) + token = current_user.generate_follow_corpus_token(corpus_hashid, role_name, expiration_date) + corpus_share_link = url_for( + 'corpora.follow_corpus', + corpus_id=corpus_id, + token=token, + _external=True + ) + response_data = { + 'message': 'Corpus share link generated', + 'category': 'corpus', + 'corpusShareLink': corpus_share_link + } + response = jsonify(response_data) + response.status_code = 200 + return response + + + +@bp.route('/<hashid:corpus_id>/is_public', methods=['PUT']) +@login_required +@corpus_owner_or_admin_required +@content_negotiation(consumes='application/json', produces='application/json') +def update_corpus_is_public(corpus_id): + is_public = request.json + if not isinstance(is_public, bool): + abort(400) + corpus = Corpus.query.get_or_404(corpus_id) + corpus.is_public = is_public + db.session.commit() + response_data = { + 'message': ( + f'Corpus "{corpus.title}" is now' + f' {"public" if is_public else "private"}' + ), + 'category': 'corpus' + } + response = jsonify(response_data) + response.status_code = 200 + return response diff --git a/app/corpora/routes.py b/app/corpora/routes.py index 148f5775aa40401ceb272d7d519bf22a43e3b51a..5484fb50af31a6a80984eeffa475aa1e02cc4953 100644 --- a/app/corpora/routes.py +++ b/app/corpora/routes.py @@ -1,36 +1,21 @@ -from datetime import datetime from flask import ( abort, - current_app, flash, - jsonify, Markup, redirect, render_template, - request, - send_from_directory, url_for ) from flask_login import current_user, login_required -from threading import Thread -import os -from .decorators import corpus_follower_permission_required, corpus_owner_or_admin_required -from app import db, hashids -from app.decorators import content_negotiation +from .decorators import corpus_follower_permission_required +from app import db from app.models import ( Corpus, - CorpusFile, CorpusFollowerAssociation, CorpusFollowerRole, - CorpusStatus, - User ) from . import bp -from .forms import ( - CreateCorpusFileForm, - CreateCorpusForm, - UpdateCorpusFileForm -) +from .forms import CreateCorpusForm @bp.route('/create', methods=['GET', 'POST']) @@ -47,10 +32,7 @@ def create_corpus(): except OSError: abort(500) db.session.commit() - message = Markup( - f'Corpus "<a href="{corpus.url}">{corpus.title}</a>" created' - ) - flash(message, 'corpus') + flash(f'Corpus "{corpus.title}" created', 'corpus') return redirect(corpus.url) return render_template( 'corpora/create_corpus.html.j2', @@ -59,29 +41,12 @@ def create_corpus(): ) -@bp.route('/public') -@login_required -def public_corpora(): - corpora = [ - c.to_json_serializeable() - for c in Corpus.query.filter(Corpus.is_public == True).all() - ] - return render_template( - 'corpora/public_corpora.html.j2', - corpora=corpora, - title='Corpora' - ) - - -############################################################################## -# Corpus # -############################################################################## -#region corpus @bp.route('/<hashid:corpus_id>') @login_required def corpus(corpus_id): corpus = Corpus.query.get_or_404(corpus_id) corpus_follower_roles = CorpusFollowerRole.query.all() + # TODO: Add URL query option to toggle view if corpus.user == current_user or current_user.is_administrator(): return render_template( 'corpora/corpus.html.j2', @@ -122,7 +87,7 @@ def follow_corpus(corpus_id, token): corpus = Corpus.query.get_or_404(corpus_id) if current_user.follow_corpus_by_token(token): db.session.commit() - flash(f'You are following {corpus.title} now', category='corpus') + flash(f'You are following "{corpus.title}" now', category='corpus') return redirect(url_for('corpora.corpus', corpus_id=corpus_id)) abort(403) @@ -137,309 +102,3 @@ def import_corpus(): @login_required def export_corpus(corpus_id): abort(503) - - -#region json-routes -@bp.route('/<hashid:corpus_id>', methods=['DELETE']) -@login_required -@corpus_owner_or_admin_required -@content_negotiation(produces='application/json') -def delete_corpus(corpus_id): - def _delete_corpus(app, corpus_id): - with app.app_context(): - corpus = Corpus.query.get(corpus_id) - corpus.delete() - db.session.commit() - - corpus = Corpus.query.get_or_404(corpus_id) - thread = Thread( - target=_delete_corpus, - args=(current_app._get_current_object(), corpus.id) - ) - thread.start() - response_data = { - 'message': f'Corpus "{corpus.title}" marked for deletion', - 'category': 'corpus' - } - response = jsonify(response_data) - response.status_code = 200 - return response - - -@bp.route('/<hashid:corpus_id>/build', methods=['POST']) -@login_required -@corpus_owner_or_admin_required -@content_negotiation(produces='application/json') -def build_corpus(corpus_id): - def _build_corpus(app, corpus_id): - with app.app_context(): - corpus = Corpus.query.get(corpus_id) - corpus.build() - db.session.commit() - - corpus = Corpus.query.get_or_404(corpus_id) - if not (corpus.user == current_user or current_user.is_administrator()): - abort(403) - if len(corpus.files.all()) == 0: - abort(409) - thread = Thread( - target=_build_corpus, - args=(current_app._get_current_object(), corpus_id) - ) - thread.start() - response_data = { - 'message': f'Corpus "{corpus.title}" marked for building', - 'category': 'corpus' - } - response = jsonify(response_data) - response.status_code = 202 - return response - - -@bp.route('/<hashid:corpus_id>/generate-corpus-share-link', methods=['POST']) -@login_required -@corpus_follower_permission_required('GENERATE_SHARE_LINK') -@content_negotiation(consumes='application/json', produces='application/json') -def generate_corpus_share_link(corpus_id): - corpus_hashid = hashids.encode(corpus_id) - data = request.json - if not isinstance(data, dict): - abort(400) - expiration = data.get('expiration') - if not isinstance(expiration, str): - abort(400) - role_name = data.get('role') - if not isinstance(role_name, str): - abort(400) - expiration_date = datetime.strptime(expiration, '%b %d, %Y') - cfr = CorpusFollowerRole.query.filter_by(name=role_name).first() - if cfr is None: - abort(400) - token = current_user.generate_follow_corpus_token(corpus_hashid, role_name, expiration_date) - corpus_share_link = url_for( - 'corpora.follow_corpus', - corpus_id=corpus_id, - token=token, - _external=True - ) - response_data = { - 'message': 'Corpus share link generated', - 'category': 'corpus', - 'corpusShareLink': corpus_share_link - } - response = jsonify(response_data) - response.status_code = 200 - return response - - - -@bp.route('/<hashid:corpus_id>/is_public', methods=['PUT']) -@login_required -@corpus_owner_or_admin_required -@content_negotiation(consumes='application/json', produces='application/json') -def update_corpus_is_public(corpus_id): - is_public = request.json - if not isinstance(is_public, bool): - abort(400) - corpus = Corpus.query.get_or_404(corpus_id) - corpus.is_public = is_public - db.session.commit() - response_data = { - 'message': ( - f'Corpus "{corpus.title}" is now' - f' {"public" if is_public else "private"}' - ), - 'category': 'corpus' - } - response = jsonify(response_data) - response.status_code = 200 - return response -#endregion json-routes -#endregion corpus - - -############################################################################## -# Corpus/Files # -############################################################################## -#region files -@bp.route('/<hashid:corpus_id>/files/create', methods=['GET', 'POST']) -@login_required -@corpus_follower_permission_required('ADD_CORPUS_FILE') -def create_corpus_file(corpus_id): - corpus = Corpus.query.get_or_404(corpus_id) - if not (corpus.user == current_user or current_user.is_administrator()): - abort(403) - form = CreateCorpusFileForm() - if form.is_submitted(): - if not form.validate(): - response = {'errors': form.errors} - return response, 400 - try: - corpus_file = CorpusFile.create( - form.vrt.data, - address=form.address.data, - author=form.author.data, - booktitle=form.booktitle.data, - chapter=form.chapter.data, - editor=form.editor.data, - institution=form.institution.data, - journal=form.journal.data, - pages=form.pages.data, - publisher=form.publisher.data, - publishing_year=form.publishing_year.data, - school=form.school.data, - title=form.title.data, - mimetype='application/vrt+xml', - corpus=corpus - ) - except (AttributeError, OSError): - abort(500) - corpus.status = CorpusStatus.UNPREPARED - db.session.commit() - message = Markup( - 'Corpus file' - f'"<a href="{corpus_file.url}">{corpus_file.filename}</a>" added' - ) - flash(message, category='corpus') - return {}, 201, {'Location': corpus.url} - return render_template( - 'corpora/create_corpus_file.html.j2', - corpus=corpus, - form=form, - title='Add corpus file' - ) - - -@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>', methods=['GET', 'POST']) -@login_required -@corpus_follower_permission_required('UPDATE_CORPUS_FILE') -def corpus_file(corpus_id, corpus_file_id): - corpus_file = CorpusFile.query.filter_by(corpus_id = corpus_id, id=corpus_file_id).first_or_404() - form = UpdateCorpusFileForm(data=corpus_file.to_json_serializeable()) - if form.validate_on_submit(): - form.populate_obj(corpus_file) - if db.session.is_modified(corpus_file): - corpus_file.corpus.status = CorpusStatus.UNPREPARED - db.session.commit() - message = Markup(f'Corpus file "<a href="{corpus_file.url}">{corpus_file.filename}</a>" updated') - flash(message, category='corpus') - return redirect(corpus_file.corpus.url) - return render_template( - 'corpora/corpus_file.html.j2', - corpus=corpus_file.corpus, - corpus_file=corpus_file, - form=form, - title='Edit corpus file' - ) - - -@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>/download') -@login_required -@corpus_follower_permission_required('VIEW') -def download_corpus_file(corpus_id, corpus_file_id): - corpus_file = CorpusFile.query.filter_by(corpus_id = corpus_id, id=corpus_file_id).first_or_404() - if not (corpus_file.corpus.user == current_user or current_user.is_administrator()): - abort(403) - return send_from_directory( - os.path.dirname(corpus_file.path), - os.path.basename(corpus_file.path), - as_attachment=True, - attachment_filename=corpus_file.filename, - mimetype=corpus_file.mimetype - ) - - -#region json-routes -@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>', methods=['DELETE']) -@login_required -@corpus_follower_permission_required('REMOVE_CORPUS_FILE') -@content_negotiation(produces='application/json') -def delete_corpus_file(corpus_id, corpus_file_id): - def _delete_corpus_file(app, corpus_file_id): - with app.app_context(): - corpus_file = CorpusFile.query.get(corpus_file_id) - corpus_file.delete() - db.session.commit() - - corpus_file = CorpusFile.query.filter_by(corpus_id=corpus_id, id=corpus_file_id).first_or_404() - thread = Thread( - target=_delete_corpus_file, - args=(current_app._get_current_object(), corpus_file.id) - ) - thread.start() - return {}, 202 -#endregion json-routes -#endregion files - -############################################################################## -# Corpus/Followers # -############################################################################## -#region followers -#region json-routes -@bp.route('/<hashid:corpus_id>/followers', methods=['POST']) -@login_required -@corpus_owner_or_admin_required -@content_negotiation(consumes='application/json', produces='application/json') -def add_corpus_followers(corpus_id): - usernames = request.json - if not (isinstance(usernames, list) or all(isinstance(u, str) for u in usernames)): - abort(400) - corpus = Corpus.query.get_or_404(corpus_id) - for username in usernames: - user = User.query.filter_by(username=username, is_public=True).first_or_404() - user.follow_corpus(corpus) - db.session.commit() - resonse_data = { - 'message': f'Users are now following "{corpus.title}"', - 'category': 'corpus' - } - response = jsonify(resonse_data) - response.status_code = 200 - return response - - -@bp.route('/<hashid:corpus_id>/followers/<hashid:follower_id>', methods=['DELETE']) -@login_required -@content_negotiation(produces='application/json') -def unfollow_corpus(corpus_id, follower_id): - corpus = Corpus.query.get_or_404(corpus_id) - follower = User.query.get_or_404(follower_id) - if not (corpus.user == current_user or follower == current_user or current_user.is_administrator()): - abort(403) - if not follower.is_following_corpus(corpus): - abort(409) # 'User is not following the corpus' - follower.unfollow_corpus(corpus) - db.session.commit() - response_data = { - 'message': \ - f'"{follower.username}" is not following "{corpus.title}" anymore', - 'category': 'corpus' - } - response = jsonify(response_data) - response.status_code = 200 - return response - - -@bp.route('/<hashid:corpus_id>/followers/<hashid:follower_id>/role', methods=['PUT']) -@login_required -@corpus_owner_or_admin_required -@content_negotiation(consumes='application/json', produces='application/json') -def add_permission(corpus_id, follower_id): - role_name = request.json - if not isinstance(role_name, str): - abort(400) - cfr = CorpusFollowerRole.query.filter_by(name=role_name).first() - if cfr is None: - abort(400) - cfa = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=follower_id).first_or_404() - cfa.role = cfr - db.session.commit() - resonse_data = { - 'message': f'User "{cfa.follower.username}" is now {cfa.role.name}', - 'category': 'corpus' - } - response = jsonify(resonse_data) - response.status_code = 200 - return response -#endregion json-routes -#endregion followers diff --git a/app/errors/__init__.py b/app/errors/__init__.py index 0d79af4857891d0c86c260b95416c3ddac5f80b3..1f0480b4d12b7ddb76d17a898fcd2a2a15f0f716 100644 --- a/app/errors/__init__.py +++ b/app/errors/__init__.py @@ -1,5 +1,6 @@ -from flask import Blueprint +from werkzeug.exceptions import HTTPException +from .handlers import generic_error_handler -bp = Blueprint('errors', __name__) -from . import handlers +def init_app(app): + app.register_error_handler(HTTPException, generic_error_handler) diff --git a/app/errors/handlers.py b/app/errors/handlers.py index cc6c926869536362764b0cab2ac8d75eed1dc0ea..5a6c413d3b2205cbed1e377b8c5fd7f7a8648fae 100644 --- a/app/errors/handlers.py +++ b/app/errors/handlers.py @@ -1,11 +1,6 @@ -from flask import render_template, request -from werkzeug.exceptions import HTTPException -from . import bp +from flask import render_template -@bp.errorhandler(HTTPException) def generic_error_handler(e): - if (request.accept_mimetypes.accept_json - and not request.accept_mimetypes.accept_html): - return {'errors': {'message': e.description}}, e.code + print('test') return render_template('errors/error.html.j2', error=e), e.code diff --git a/app/main/routes.py b/app/main/routes.py index 287a04f5b162c13d80d18b001508f90c61f213a5..918f7414d2415f510dbf28ca0a92aa63878b6fc7 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -30,12 +30,6 @@ def dashboard(): return render_template('main/dashboard.html.j2', title='Dashboard') -@bp.route('/dashboard2') -@login_required -def dashboard2(): - return render_template('main/dashboard2.html.j2', title='Dashboard') - - @bp.route('/user_manual') def user_manual(): return render_template('main/user_manual.html.j2', title='User manual') @@ -55,6 +49,7 @@ def privacy_policy(): def terms_of_use(): return render_template('main/terms_of_use.html.j2', title='Terms of Use') + @bp.route('/social-area') def social_area(): users = [ diff --git a/app/static/js/Requests/Corpora.js b/app/static/js/Requests/Corpora.js deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/app/static/js/Requests/corpora/corpora.js b/app/static/js/Requests/corpora/corpora.js index 34593d952b5d8c274e5902ed8984c79cef010487..b8031e6e8c665b4c1409470a7e3a30dbb2329294 100644 --- a/app/static/js/Requests/corpora/corpora.js +++ b/app/static/js/Requests/corpora/corpora.js @@ -33,11 +33,11 @@ Requests.corpora.entity.generateShareLink = (corpusId, role, expiration) => { Requests.corpora.entity.isPublic = {}; -Requests.corpora.entity.isPublic.update = (corpusId, value) => { +Requests.corpora.entity.isPublic.update = (corpusId, isPublic) => { let input = `/corpora/${corpusId}/is_public`; let init = { method: 'PUT', - body: JSON.stringify(value) + body: JSON.stringify(isPublic) }; return Requests.JSONfetch(input, init); }; diff --git a/app/static/js/ResourceLists/CorpusFileList.js b/app/static/js/ResourceLists/CorpusFileList.js index c052b2eaf529467ac659e3f83815cd99fbe23260..0ce96e2dbaf1703c66ecef5860b243961e7e4faf 100644 --- a/app/static/js/ResourceLists/CorpusFileList.js +++ b/app/static/js/ResourceLists/CorpusFileList.js @@ -100,7 +100,37 @@ class CorpusFileList extends ResourceList { let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction; switch (listAction) { case 'delete': { - Utils.deleteCorpusFileRequest(this.userId, this.corpusId, itemId); + let values = this.listjs.get('id', itemId)[0].values(); + let modalElement = Utils.HTMLToElement( + ` + <div class="modal"> + <div class="modal-content"> + <h4>Confirm Corpus File deletion</h4> + <p>Do you really want to delete the Corpus File <b>${values.title}</b>? All files will be permanently deleted!</p> + </div> + <div class="modal-footer"> + <a class="btn modal-close waves-effect waves-light">Cancel</a> + <a class="action-button btn modal-close red waves-effect waves-light" data-action="confirm">Delete</a> + </div> + </div> + ` + ); + document.querySelector('#modals').appendChild(modalElement); + let modal = M.Modal.init( + modalElement, + { + dismissible: false, + onCloseEnd: () => { + modal.destroy(); + modalElement.remove(); + } + } + ); + let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]'); + confirmElement.addEventListener('click', (event) => { + Requests.corpora.entity.files.ent.delete(this.corpusId, itemId); + }); + modal.open(); break; } case 'download': { diff --git a/app/static/js/ResourceLists/CorpusList.js b/app/static/js/ResourceLists/CorpusList.js index 7d43167380c04ee67d08b4d6f7a9727793243b69..955bb424b205be309c7115c48c8b2e01dc51cc33 100644 --- a/app/static/js/ResourceLists/CorpusList.js +++ b/app/static/js/ResourceLists/CorpusList.js @@ -95,7 +95,37 @@ class CorpusList extends ResourceList { let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction; switch (listAction) { case 'delete-request': { - Requests.corpora.entity.delete(this.userId, itemId); + let values = this.listjs.get('id', itemId)[0].values(); + let modalElement = Utils.HTMLToElement( + ` + <div class="modal"> + <div class="modal-content"> + <h4>Confirm Corpus deletion</h4> + <p>Do you really want to delete the Corpus <b>${values.title}</b>? All files will be permanently deleted!</p> + </div> + <div class="modal-footer"> + <a class="btn modal-close waves-effect waves-light">Cancel</a> + <a class="action-button btn modal-close red waves-effect waves-light" data-action="confirm">Delete</a> + </div> + </div> + ` + ); + document.querySelector('#modals').appendChild(modalElement); + let modal = M.Modal.init( + modalElement, + { + dismissible: false, + onCloseEnd: () => { + modal.destroy(); + modalElement.remove(); + } + } + ); + let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]'); + confirmElement.addEventListener('click', (event) => { + Requests.corpora.entity.delete(itemId); + }); + modal.open(); break; } case 'view': { diff --git a/app/static/js/Utils.js b/app/static/js/Utils.js index e7c5404e66f82722ef81bbc8dcf984264c98ea2c..822ab7757c03e55acd7b7590b0631a89f60a8ea1 100644 --- a/app/static/js/Utils.js +++ b/app/static/js/Utils.js @@ -69,118 +69,6 @@ class Utils { return Utils.mergeObjectsDeep(mergedObject, ...objects.slice(2)); } - static deleteCorpusRequest(userId, corpusId) { - return new Promise((resolve, reject) => { - let corpus; - try { - corpus = app.data.users[userId].corpora[corpusId]; - } catch (error) { - corpus = {}; - } - - let modalElement = Utils.HTMLToElement( - ` - <div class="modal"> - <div class="modal-content"> - <h4>Confirm Corpus deletion</h4> - <p>Do you really want to delete the Corpus <b>${corpus?.title}</b>? All files will be permanently deleted!</p> - </div> - <div class="modal-footer"> - <a class="action-button btn modal-close waves-effect waves-light" data-action="cancel">Cancel</a> - <a class="action-button btn modal-close red waves-effect waves-light" data-action="confirm">Delete</a> - </div> - </div> - ` - ); - document.querySelector('#modals').appendChild(modalElement); - let modal = M.Modal.init( - modalElement, - { - dismissible: false, - onCloseEnd: () => { - modal.destroy(); - modalElement.remove(); - } - } - ); - - let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]'); - confirmElement.addEventListener('click', (event) => { - let corpusTitle = corpus?.title; - fetch(`/corpora/${corpusId}`, {method: 'DELETE', headers: {Accept: 'application/json'}}) - .then( - (response) => { - if (response.status === 403) {app.flash('Forbidden', 'error'); reject(response);} - if (response.status === 404) {app.flash('Not Found', 'error'); reject(response);} - app.flash(`Corpus "${corpusTitle}" marked for deletion`, 'corpus'); - resolve(response); - }, - (response) => { - app.flash('Something went wrong', 'error'); - reject(response); - } - ); - }); - modal.open(); - }); - } - - static deleteCorpusFileRequest(userId, corpusId, corpusFileId) { - return new Promise((resolve, reject) => { - let corpusFile; - try { - corpusFile = app.data.users[userId].corpora[corpusId].files[corpusFileId]; - } catch (error) { - corpusFile = {}; - } - - let modalElement = Utils.HTMLToElement( - ` - <div class="modal"> - <div class="modal-content"> - <h4>Confirm Corpus File deletion</h4> - <p>Do you really want to delete the Corpus File <b>${corpusFile?.title}</b>? All files will be permanently deleted!</p> - </div> - <div class="modal-footer"> - <a class="action-button btn modal-close waves-effect waves-light" data-action="cancel">Cancel</a> - <a class="action-button btn modal-close red waves-effect waves-light" data-action="confirm">Delete</a> - </div> - </div> - ` - ); - document.querySelector('#modals').appendChild(modalElement); - let modal = M.Modal.init( - modalElement, - { - dismissible: false, - onCloseEnd: () => { - modal.destroy(); - modalElement.remove(); - } - } - ); - - let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]'); - confirmElement.addEventListener('click', (event) => { - let corpusFileTitle = corpusFile?.title; - fetch(`/corpora/${corpusId}/files/${corpusFileId}`, {method: 'DELETE', headers: {Accept: 'application/json'}}) - .then( - (response) => { - if (response.status === 403) {app.flash('Forbidden', 'error'); reject(response);} - if (response.status === 404) {app.flash('Not Found', 'error'); reject(response);} - app.flash(`Corpus File "${corpusFileTitle}" deleted`, 'corpus'); - resolve(response); - }, - (response) => { - app.flash('Something went wrong', 'error'); - reject(response); - } - ); - }); - modal.open(); - }); - } - static deleteProfileAvatarRequest(userId) { return new Promise((resolve, reject) => { let modalElement = Utils.HTMLToElement( diff --git a/app/templates/_roadmap.html.j2 b/app/templates/_roadmap.html.j2 index 50cc18cd7387a4992b66591a6334103a727dced2..3b8ea308f51354b2b9b54470b958082a3f24f46d 100644 --- a/app/templates/_roadmap.html.j2 +++ b/app/templates/_roadmap.html.j2 @@ -14,7 +14,7 @@ <li class="tab"><a{%if request.path == url_for('corpora.create_corpus') %} class="active"{% endif %} href="{{ url_for('corpora.create_corpus') }}" target="_self">Create corpus</a></li> <li class="tab disabled"><i class="material-icons">navigate_next</i></li> {% if corpus %} - <li class="tab"><a{%if request.path == url_for('corpora.create_corpus_file', corpus_id=corpus.id) %} class="active"{% endif %} href="{{ url_for('corpora.create_corpus_file', corpus_id=corpus.id) }}" target="_self">Create corpus file(s)</a></li> + <li class="tab"><a{%if request.path == url_for('corpora.files.create_corpus_file', corpus_id=corpus.id) %} class="active"{% endif %} href="{{ url_for('corpora.files.create_corpus_file', corpus_id=corpus.id) }}" target="_self">Create corpus file(s)</a></li> {% else %} <li class="tab disabled tooltipped" data-tooltip="Select a corpus first" target="_self"><a>Create corpus file(s)</a></li> {% endif %} diff --git a/app/templates/base.html.j2 b/app/templates/base.html.j2 index dbecd35e1e76419baff35b973af85bf4beb9b2bb..c9189e13cfb83f9bc865560a7c50f828487a1864 100644 --- a/app/templates/base.html.j2 +++ b/app/templates/base.html.j2 @@ -49,4 +49,8 @@ {% block scripts %} {{ super() }} {% include "_scripts.html.j2" %} +{% set page_script = self._TemplateReference__context.name|replace('.html.j2', '.js.j2') %} +<script> +{% include page_script ignore missing %} +</script> {% endblock scripts %} diff --git a/app/templates/contributions/tesseract_ocr_pipeline_models/create_tesseract_ocr_pipeline_model.html.j2 b/app/templates/contributions/tesseract_ocr_pipeline_models/create_tesseract_ocr_pipeline_model.html.j2 index 43ad0a136437deaae1ac299c0b13a2f8ca64b2df..84eda745a0534e99fcc1b22eb81231d4de06ec21 100644 --- a/app/templates/contributions/tesseract_ocr_pipeline_models/create_tesseract_ocr_pipeline_model.html.j2 +++ b/app/templates/contributions/tesseract_ocr_pipeline_models/create_tesseract_ocr_pipeline_model.html.j2 @@ -79,37 +79,3 @@ </div> </div> {% endblock page_content %} - -{% block modals %} -{{ super() }} -<div id="models-modal" class="modal"> - <div class="modal-content"> - <h4>Tesseract OCR Pipeline models</h4> - <table> - <thead> - <tr> - <th>Title</th> - <th>Description</th> - <th>Biblio</th> - </tr> - </thead> - <tbody> - {% for m in tesseract_ocr_pipeline_models %} - <tr id="tesseract-ocr-pipeline-model-{{ m.hashid }}"> - <td>{{ m.title }}</td> - {% if m.description == '' %} - <td>Description is not available.</td> - {% else %} - <td>{{ m.description }}</td> - {% endif %} - <td><a href="{{ m.publisher_url }}">{{ m.publisher }}</a> ({{ m.publishing_year }}), {{ m.title }} {{ m.version}}, <a href="{{ m.publishing_url }}">{{ m.publishing_url }}</a></td> - </tr> - {% endfor %} - </tbody> - </table> - </div> - <div class="modal-footer"> - <a href="#!" class="modal-close waves-effect waves-light btn">Close</a> - </div> -</div> -{% endblock modals %} diff --git a/app/templates/corpora/_breadcrumbs.html.j2 b/app/templates/corpora/_breadcrumbs.html.j2 index af6d2b78eec36b366fe81854fc64c3ae89945888..bdbe0f3449c9a514c920e01055f728aba56d1419 100644 --- a/app/templates/corpora/_breadcrumbs.html.j2 +++ b/app/templates/corpora/_breadcrumbs.html.j2 @@ -1,4 +1,4 @@ -{% set breadcrumbs %} +{# {% set breadcrumbs %} <li class="tab disabled"><i class="material-icons">navigate_next</i></li> <li class="tab"><a href="{{ url_for('main.dashboard', _anchor='jobs') }}" target="_self">My corpora</a></li> <li class="tab disabled"><i class="material-icons">navigate_next</i></li> @@ -25,4 +25,4 @@ <li class="tab disabled"><i class="material-icons">navigate_next</i></li> <li class="tab"><a class="active" href="{{ url_for('.corpus_file', corpus_file_id=corpus_file.id, corpus_id=corpus.id) }}" target="_self">{{ corpus_file.author }}: {{ corpus_file.title }} ({{ corpus_file.publishing_year }})</a></li> {% endif %} -{% endset %} +{% endset %} #} diff --git a/app/templates/corpora/corpus.html.j2 b/app/templates/corpora/corpus.html.j2 index d8577907d3ed93e84651236d7131c0600d082c53..053dbdc0a775997a7c0b89158d729796dcf28b7f 100644 --- a/app/templates/corpora/corpus.html.j2 +++ b/app/templates/corpora/corpus.html.j2 @@ -91,7 +91,7 @@ <div class="corpus-file-list" data-user-id="{{ corpus.user.hashid }}" data-corpus-id="{{ corpus.hashid }}"></div> </div> <div class="card-action right-align"> - <a href="{{ url_for('corpora.create_corpus_file', corpus_id=corpus.id) }}" class="btn waves-effect waves-light"><i class="material-icons left">add</i>Add corpus file</a> + <a href="{{ url_for('corpora.files.create_corpus_file', corpus_id=corpus.id) }}" class="btn waves-effect waves-light"><i class="material-icons left">add</i>Add corpus file</a> </div> </div> </div> @@ -215,132 +215,3 @@ </div> </div> {% endblock modals %} - -{% block scripts %} -{{ super() }} -<script> - let corpusId = {{ corpus.hashid|tojson }}; - let corpusDisplay = new CorpusDisplay(document.querySelector('#corpus-display')); - - // #region publishing_modal_js - let publishingModalIsPublicSwitchElement = document.querySelector('#publishing-modal-is-public-switch'); - publishingModalIsPublicSwitchElement.addEventListener('change', (event) => { - let newIsPublic = publishingModalIsPublicSwitchElement.checked; - Requests.corpora.entity.isPublic.update(corpusId, newIsPublic) - .catch((response) => { - publishingModalIsPublicSwitchElement.checked = !newIsPublic; - }); - }); - // #endregion publishing_modal_js - - // #region delete_modal_js - let deleteModalDeleteButtonElement = document.querySelector('#delete-modal-delete-button'); - deleteModalDeleteButtonElement.addEventListener('click', (event) => { - Requests.corpora.entity.delete(corpusId) - .then((response) => {window.location.href = '/dashboard';}); - }); - // #endregion delete_modal_js - - // #region invite_user_modal_js - let inviteUserModalElement = document.querySelector('#invite-user-modal'); - let inviteUserModalSearchElement = document.querySelector('#invite-user-modal-search'); - let inviteUserModalInviteButtonElement = document.querySelector('#invite-user-modal-invite-button'); - - let inviteUserModalSearch = M.Chips.init( - inviteUserModalSearchElement, - { - autocompleteOptions: { - data: { - 'nopaque': '/users/3V8Aqpg74JvxOd9o/avatar', - 'pjentsch': '/users/3V8Aqpg74JvxOd9o/avatar', - 'pjentsch2': '/users/3V8Aqpg74JvxOd9o/avatar' - } - }, - limit: 3, - onChipAdd: (a, chipElement) => { - if (!(chipElement.firstChild.data in inviteUserModalSearch.autocomplete.options.data)) { - chipElement.firstElementChild.click(); - } - }, - placeholder: 'Enter a username', - secondaryPlaceholder: 'Add more users' - } - ); - - M.Modal.init( - inviteUserModalElement, - { - onOpenStart: (modalElement, modalTriggerElement) => { - while (inviteUserModalSearch.chipsData.length > 0) { - inviteUserModalSearch.deleteChip(0); - } - } - } - ) - - inviteUserModalInviteButtonElement.addEventListener('click', (event) => { - let usernames = inviteUserModalSearch.chipsData.map((chipData) => chipData.tag); - Requests.corpora.entity.followers.add(corpusId, usernames); - }); - // #endregion invite_user_modal_js - - // #region share_link_modal_js - let shareLinkModalElement = document.querySelector('#share-link-modal'); - let shareLinkModalCorpusFollowerRoleSelectElement = document.querySelector('#share-link-modal-corpus-follower-role-select'); - let shareLinkModalExpirationDateDatepickerElement = document.querySelector('#share-link-modal-expiration-date-datepicker'); - let shareLinkModalCreateButtonElement = document.querySelector('#share-link-modal-create-button'); - let shareLinkModalOutputContainerElement = document.querySelector('#share-link-modal-output-container'); - let shareLinkModalOutputFieldElement = document.querySelector('#share-link-modal-output-field'); - let shareLinkModalOutputCopyButtonElement = document.querySelector('#share-link-modal-output-copy-button'); - - let today = new Date(); - let tomorrow = new Date(); - tomorrow.setDate(today.getDate() + 1); - let oneWeekLater = new Date(); - oneWeekLater.setDate(today.getDate() + 7); - let fourWeeksLater = new Date(); - fourWeeksLater.setDate(today.getDate() + 28); - - M.Datepicker.init( - shareLinkModalExpirationDateDatepickerElement, - { - container: document.querySelector('main'), - defaultDate: oneWeekLater, - setDefaultDate: true, - minDate: tomorrow, - maxDate: fourWeeksLater - } - ); - - M.Modal.init( - shareLinkModalElement, - { - onOpenStart: (modalElement, modalTriggerElement) => { - shareLinkModalOutputFieldElement.value = ''; - shareLinkModalOutputContainerElement.classList.add('hide'); - } - } - ) - - shareLinkModalCreateButtonElement.addEventListener('click', (event) => { - Requests.corpora.entity.generateShareLink(corpusId, shareLinkModalCorpusFollowerRoleSelectElement.value, shareLinkModalExpirationDateDatepickerElement.value) - .then((response) => { - response.json() - .then((json) => { - shareLinkModalOutputContainerElement.classList.remove('hide'); - shareLinkModalOutputFieldElement.value = json.corpusShareLink; - }); - }); - }); - - shareLinkModalOutputCopyButtonElement.addEventListener('click', (event) => { - navigator.clipboard.writeText(shareLinkModalOutputFieldElement.value) - .then( - () => {app.flash('Copied!');}, - () => {app.flash('Could not copy to clipboard. Please copy manually.', 'error');} - ); - - }); - // #endregion share_link_modal_js -</script> -{% endblock scripts %} diff --git a/app/templates/corpora/corpus.js.j2 b/app/templates/corpora/corpus.js.j2 new file mode 100644 index 0000000000000000000000000000000000000000..7e815cf373cf6e63999bb9d745eb01a50d4d2395 --- /dev/null +++ b/app/templates/corpora/corpus.js.j2 @@ -0,0 +1,127 @@ +let corpusId = {{ corpus.hashid|tojson }}; +let corpusDisplay = new CorpusDisplay(document.querySelector('#corpus-display')); + +// #region Publishing +let publishingModalIsPublicSwitchElement = document.querySelector('#publishing-modal-is-public-switch'); +publishingModalIsPublicSwitchElement.addEventListener('change', (event) => { + let newIsPublic = publishingModalIsPublicSwitchElement.checked; + Requests.corpora.entity.isPublic.update(corpusId, newIsPublic) + .catch((response) => { + publishingModalIsPublicSwitchElement.checked = !newIsPublic; + }); +}); +// #endregion Publishing + +// #region Delete +let deleteModalDeleteButtonElement = document.querySelector('#delete-modal-delete-button'); +deleteModalDeleteButtonElement.addEventListener('click', (event) => { + Requests.corpora.entity.delete(corpusId) + .then((response) => { + window.location.href = {{ url_for('main.dashboard')|tojson }}; + }); +}); +// #endregion Delete + +// #region Invite users +let inviteUserModalElement = document.querySelector('#invite-user-modal'); +let inviteUserModalSearchElement = document.querySelector('#invite-user-modal-search'); +let inviteUserModalInviteButtonElement = document.querySelector('#invite-user-modal-invite-button'); + +let inviteUserModalSearch = M.Chips.init( + inviteUserModalSearchElement, + { + autocompleteOptions: { + data: { + 'nopaque': '/users/3V8Aqpg74JvxOd9o/avatar', + 'pjentsch': '/users/3V8Aqpg74JvxOd9o/avatar', + 'pjentsch2': '/users/3V8Aqpg74JvxOd9o/avatar' + } + }, + limit: 3, + onChipAdd: (a, chipElement) => { + if (!(chipElement.firstChild.data in inviteUserModalSearch.autocomplete.options.data)) { + chipElement.firstElementChild.click(); + } + }, + placeholder: 'Enter a username', + secondaryPlaceholder: 'Add more users' + } +); + +M.Modal.init( + inviteUserModalElement, + { + onOpenStart: (modalElement, modalTriggerElement) => { + while (inviteUserModalSearch.chipsData.length > 0) { + inviteUserModalSearch.deleteChip(0); + } + } + } +) + +inviteUserModalInviteButtonElement.addEventListener('click', (event) => { + let usernames = inviteUserModalSearch.chipsData.map((chipData) => chipData.tag); + Requests.corpora.entity.followers.add(corpusId, usernames); +}); +// #endregion Invite users + +// #region Share link +let shareLinkModalElement = document.querySelector('#share-link-modal'); +let shareLinkModalCorpusFollowerRoleSelectElement = document.querySelector('#share-link-modal-corpus-follower-role-select'); +let shareLinkModalExpirationDateDatepickerElement = document.querySelector('#share-link-modal-expiration-date-datepicker'); +let shareLinkModalCreateButtonElement = document.querySelector('#share-link-modal-create-button'); +let shareLinkModalOutputContainerElement = document.querySelector('#share-link-modal-output-container'); +let shareLinkModalOutputFieldElement = document.querySelector('#share-link-modal-output-field'); +let shareLinkModalOutputCopyButtonElement = document.querySelector('#share-link-modal-output-copy-button'); + +let today = new Date(); +let tomorrow = new Date(); +tomorrow.setDate(today.getDate() + 1); +let oneWeekLater = new Date(); +oneWeekLater.setDate(today.getDate() + 7); +let fourWeeksLater = new Date(); +fourWeeksLater.setDate(today.getDate() + 28); + +M.Datepicker.init( + shareLinkModalExpirationDateDatepickerElement, + { + container: document.querySelector('main'), + defaultDate: oneWeekLater, + setDefaultDate: true, + minDate: tomorrow, + maxDate: fourWeeksLater + } +); + +M.Modal.init( + shareLinkModalElement, + { + onOpenStart: (modalElement, modalTriggerElement) => { + shareLinkModalOutputFieldElement.value = ''; + shareLinkModalOutputContainerElement.classList.add('hide'); + } + } +) + +shareLinkModalCreateButtonElement.addEventListener('click', (event) => { + let role = shareLinkModalCorpusFollowerRoleSelectElement.value; + let expiration = shareLinkModalExpirationDateDatepickerElement.value + Requests.corpora.entity.generateShareLink(corpusId, role, expiration) + .then((response) => { + response.json() + .then((json) => { + shareLinkModalOutputContainerElement.classList.remove('hide'); + shareLinkModalOutputFieldElement.value = json.corpusShareLink; + }); + }); +}); + +shareLinkModalOutputCopyButtonElement.addEventListener('click', (event) => { + navigator.clipboard.writeText(shareLinkModalOutputFieldElement.value) + .then( + () => {app.flash('Copied!');}, + () => {app.flash('Could not copy to clipboard. Please copy manually.', 'error');} + ); + +}); +// #endregion Share link diff --git a/app/templates/corpora/corpus_file.html.j2 b/app/templates/corpora/files/corpus_file.html.j2 similarity index 100% rename from app/templates/corpora/corpus_file.html.j2 rename to app/templates/corpora/files/corpus_file.html.j2 diff --git a/app/templates/corpora/create_corpus_file.html.j2 b/app/templates/corpora/files/create_corpus_file.html.j2 similarity index 95% rename from app/templates/corpora/create_corpus_file.html.j2 rename to app/templates/corpora/files/create_corpus_file.html.j2 index 44e9ff82ff51adbc332350675bc9473113843dcd..8cff13a61803d50a68efa89ffa954f10cb44af0e 100644 --- a/app/templates/corpora/create_corpus_file.html.j2 +++ b/app/templates/corpora/files/create_corpus_file.html.j2 @@ -36,7 +36,7 @@ </div> </div> <div class="card-action right-align"> - <a class="waves-effect waves-light btn red" href="{{ url_for('.corpus', corpus_id=corpus.id) }}"><i class="material-icons left">close</i>Cancel</a> + <a class="waves-effect waves-light btn red" href="{{ url_for('corpora.corpus', corpus_id=corpus.id) }}"><i class="material-icons left">close</i>Cancel</a> {{ wtf.render_field(form.submit, material_icon='send') }} </div> </div> diff --git a/app/templates/corpora/public_corpora.html.j2 b/app/templates/corpora/public_corpora.html.j2 deleted file mode 100644 index 9be377f32a9fc7b1444f376b89d57a72242629dc..0000000000000000000000000000000000000000 --- a/app/templates/corpora/public_corpora.html.j2 +++ /dev/null @@ -1,72 +0,0 @@ -{% extends "base.html.j2" %} - -{% block main_attribs %} class="service-scheme" data-service="corpus-analysis"{% endblock main_attribs %} - -{% block page_content %} -<div class="corpus-list no-autoinit" id="corpus-list"> - <div class="parallax-container"> - <div class="parallax"><img src="{{ url_for('static', filename='images/parallax_hq/canvas.png') }}"></div> - <div style="position: absolute; bottom: 0; width: 100%;"> - <div class="container"> - <div class="white-text"> - <h1 id="title"><i class="nopaque-icons" style="font-size: inherit;">I</i>Corpora</h1> - </div> - <div class="white" style="padding: 1px 35px 0 10px; border-radius: 35px;"> - <div class="input-field"> - <i class="material-icons prefix">search</i> - <input class="search" id="corpus-list-search" type="text"> - <label for="corpus-list-search">Search corpus</label> - </div> - </div> - </div> - </div> - </div> - - - <div class="row"> - <div class="col s12" id="corpora"> - <div class="card"> - <div class="card-content"> - <div> - <table> - <thead> - <tr> - <th></th> - <th>Title and Description</th> - <th>Status</th> - <th></th> - </tr> - </thead> - <tbody class="list"></tbody> - </table> - <ul class="pagination"></ul> - </div> - </div> - </div> - </div> - </div> -</div> -{% endblock page_content %} - - -{% block scripts %} -{{ super() }} -<script> - let corpusListElement = document.querySelector('#corpus-list'); - let corpusListOptions = { - initialHtmlGenerator: null, - item: ` - <tr class="clickable hoverable"> - <td><a class="btn-floating disabled"><i class="material-icons service-color darken" data-service="corpus-analysis">book</i></a></td> - <td><b class="title"></b><br><i class="description"></i></td> - <td><span class="status badge new corpus-status-color corpus-status-text" data-badge-caption=""></span></td> - <td class="right-align"> - <a class="action-button btn-floating service-color darken waves-effect waves-light" data-action="view" data-service="corpus-analysis"><i class="material-icons">send</i></a> - </td> - </tr> - `.trim(), - }; - let corpusList = new CorpusList(corpusListElement, corpusListOptions); - corpusList._init({{ corpora|tojson }}); -</script> -{% endblock scripts %} diff --git a/app/templates/corpora/public_corpus.html.j2 b/app/templates/corpora/public_corpus.html.j2 index 41f5798baca90f8fe5cd756f45e8113e27e251ed..ff445e38cd2829d7f3d54bbc228f5b6dc9e6adcb 100644 --- a/app/templates/corpora/public_corpus.html.j2 +++ b/app/templates/corpora/public_corpus.html.j2 @@ -101,30 +101,3 @@ </div> </div> {% endblock page_content %} - -{% block scripts %} -{{ super() }} -<script> - let corpusFileList = new PublicCorpusFileList(document.querySelector('.corpus-file-list')); - corpusFileList.add({{ corpus_files|tojson }}); - - let unfollowRequestElement = document.querySelector('.action-button[data-action="unfollow-request"]'); - unfollowRequestElement.addEventListener('click', () => { - return new Promise((resolve, reject) => { - fetch('{{ url_for("corpora.unfollow_corpus", corpus_id=corpus.id, follower_id=current_user.id) }}', {method: 'POST', headers: {Accept: 'application/json'}}) - .then( - (response) => { - if (response.status === 403) {app.flash('Forbidden', 'error'); reject(response);} - if (response.status === 404) {app.flash('Not Found', 'error'); reject(response);} - resolve(response); - window.location.href = '{{ url_for("main.dashboard") }}'; - }, - (response) => { - app.flash('Something went wrong', 'error'); - reject(response); - } - ); - }); - }); -</script> -{% endblock scripts %} diff --git a/app/templates/corpora/public_corpus.js.j2 b/app/templates/corpora/public_corpus.js.j2 new file mode 100644 index 0000000000000000000000000000000000000000..0b8205f42be1699481b58a27478a81af09b1a8a6 --- /dev/null +++ b/app/templates/corpora/public_corpus.js.j2 @@ -0,0 +1,11 @@ +let corpusId = {{ corpus.hashid|tojson }}; +let corpusFileList = new PublicCorpusFileList(document.querySelector('.corpus-file-list')); +corpusFileList.add({{ corpus_files|tojson }}); + +let unfollowRequestElement = document.querySelector('.action-button[data-action="unfollow-request"]'); +unfollowRequestElement.addEventListener('click', () => { + Requests.corpora.entity.followers.entity.delete(corpusId, currentUserId) + .then((response) => { + window.location.href = {{ url_for('main.dashboard')|tojson }}; + }); +}); diff --git a/app/templates/main/dashboard2.html.j2 b/app/templates/main/dashboard2.html.j2 deleted file mode 100644 index d45aaa041d55a2b123aaeb3dc63f82ab3858c61a..0000000000000000000000000000000000000000 --- a/app/templates/main/dashboard2.html.j2 +++ /dev/null @@ -1,274 +0,0 @@ -{% extends "base.html.j2" %} -{% from "main/_breadcrumbs.html.j2" import breadcrumbs with context %} - -{% block page_content %} -<div class="section scrollspy" id="dashboard"> - <div class="row"> - <div class="col s1"></div> - <div class="col s11"> - <h1 id="title">Dashboard</h1> - </div> - <div class="col s1"></div> - <div class="col s3"> - <p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.</p> - </div> - <div class="col s8"> - <a class="btn waves-effect waves-light" href="#my-corpora"><i class="nopaque-icons left">I</i>My Corpora</a> - <a class="btn waves-effect waves-light" href="#my-jobs"><i class="nopaque-icons left">J</i>My Jobs</a> - <a class="btn waves-effect waves-light" href="#my-groups"><i class="material-icons left">groups</i>My Groups</a> - </div> - </div> -</div> - - -<div class="corpus-list no-autoinit" id="corpus-list" data-user-id="{{ current_user.hashid }}"> - <div class="parallax-container"> - <div class="parallax"><img src="{{ url_for('static', filename='images/parallax_hq/canvas.png') }}"></div> - <div style="position: absolute; bottom: 0; width: 100%;"> - <div class="container"> - <div class="white" style="padding: 1px 35px 0 10px; border-radius: 35px;"> - <div class="input-field"> - <i class="material-icons prefix">search</i> - <input class="search" id="corpus-list-search" type="text"> - <label for="corpus-list-search">Search Corpus</label> - </div> - </div> - </div> - </div> - </div> - - <div class="section scrollspy" id="my-corpora"> - <div class="row"> - <div class="col s1"></div> - <div class="col s2"> - <h2>My Corpora</h2> - <p>Create a corpus to interactively perform linguistic analysis.</p> - <p>Or browse our users public corpora.<span class="new badge"></span></p> - </div> - <div class="col s6"> - <div class="card"> - <div class="card-content"> - <div> - <table> - <thead> - <tr> - <th></th> - <th>Title and Description</th> - <th>Status</th> - <th></th> - </tr> - </thead> - <tbody class="list"></tbody> - </table> - <ul class="pagination"></ul> - </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> - </div> - </div> - </div> - <div class="col s1"></div> - <div class="col s2"> - <ul class="section table-of-contents"> - <li><a href="#dashboard">Dashboard</a></li> - <li><a href="#my-corpora">My Corpora</a></li> - <li><a href="#my-jobs">My Jobs</a></li> - <li><a href="#my-groups">My Groups</a></li> - </ul> - </div> - </div> - </div> -</div> - -<div class="job-list no-autoinit" id="job-list" data-user-id="{{ current_user.hashid }}"> - <div class="parallax-container"> - <div class="parallax"><img src="{{ url_for('static', filename='images/parallax_hq/canvas.png') }}"></div> - <div style="position: absolute; bottom: 0; width: 100%;"> - <div class="container"> - <div class="white" style="padding: 1px 35px 0 10px; border-radius: 35px;"> - <div class="input-field"> - <i class="material-icons prefix">search</i> - <input class="search" id="job-list-search" type="text"> - <label for="job-list-search">Search Job</label> - </div> - </div> - </div> - </div> - </div> - - <div class="section scrollspy" id="my-jobs"> - <div class="row"> - <div class="col s1"></div> - <div class="col s2"> - <h2>My Jobs</h2> - <p> - A job is the execution of a service provided by nopaque. You can - create any number of jobs and let them be processed simultaneously. We - <b>strongly recommend</b> that you create a folder on your computer where you - save the various files that nopaque provides you with after each - pre-processing step. You will need the result of each step for the - next step. - </p> - <p><b>Where is my Job data?</b> Don't worry, please read <a href="{{ url_for('main.news', _anchor='april-2022-update') }}">this news</a> entry</p> - </div> - <div class="col s6"> - <div class="card"> - <div class="card-content"> - <div> - <table> - <thead> - <tr> - <th></th> - <th>Title and Description</th> - <th>Status</th> - <th></th> - </tr> - </thead> - <tbody class="list"></tbody> - </table> - <ul class="pagination"></ul> - </div> - </div> - <div class="card-action right-align"> - <a class="btn modal-trigger waves-effect waves-light" data-target="create-job-modal"><i class="material-icons left">add</i>Create job</a> - </div> - </div> - </div> - <div class="col s1"></div> - <div class="col s2"> - <ul class="section table-of-contents"> - <li><a href="#dashboard">Dashboard</a></li> - <li><a href="#my-corpora">My Corpora</a></li> - <li><a href="#my-jobs">My Jobs</a></li> - <li><a href="#my-groups">My Groups</a></li> - </ul> - </div> - </div> - </div> -</div> - -<div class="group-list no-autoinit" id="group-list" data-user-id="{{ current_user.hashid }}"> - <div class="parallax-container"> - <div class="parallax"><img src="{{ url_for('static', filename='images/parallax_hq/canvas.png') }}"></div> - <div style="position: absolute; bottom: 0; width: 100%;"> - <div class="container"> - <div class="white" style="padding: 1px 35px 0 10px; border-radius: 35px;"> - <div class="input-field"> - <i class="material-icons prefix">search</i> - <input class="search" id="group-list-search" type="text"> - <label for="group-list-search">Search Group</label> - </div> - </div> - </div> - </div> - </div> - - <div class="section scrollspy" id="my-groups"> - <div class="row"> - <div class="col s1"></div> - <div class="col s2"> - <h2>My Groups</h2> - <p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.</p> - </div> - <div class="col s6"> - <div class="card"> - <div class="card-content"> - <div> - <table> - <thead> - <tr> - <th></th> - <th>Title and Description</th> - <th>Status</th> - <th></th> - </tr> - </thead> - <tbody class="list"></tbody> - </table> - <ul class="pagination"></ul> - </div> - </div> - <div class="card-action right-align"> - <a class="btn waves-effect waves-light"><i class="material-icons left">add</i>Create group</a> - </div> - </div> - </div> - <div class="col s1"></div> - <div class="col s2"> - <ul class="section table-of-contents"> - <li><a href="#dashboard">Dashboard</a></li> - <li><a href="#my-corpora">My Corpora</a></li> - <li><a href="#my-jobs">My Jobs</a></li> - <li><a href="#my-groups">My Groups</a></li> - </ul> - </div> - </div> - </div> -</div> -{% endblock page_content %} - -{% block modals %} -{{ 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> - </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> - </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> - </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 %} - - -{% block scripts %} -{{ super() }} -<script> - let corpusListElement = document.querySelector('#corpus-list'); - let corpusListOptions = {initialHtmlGenerator: null}; - let corpusList = new CorpusList(corpusListElement, corpusListOptions); - let jobListElement = document.querySelector('#job-list'); - let jobListOptions = {initialHtmlGenerator: null}; - let jobList = new JobList(jobListElement, jobListOptions); -</script> -{% endblock scripts %} diff --git a/app/templates/services/spacy_nlp_pipeline.html.j2 b/app/templates/services/spacy_nlp_pipeline.html.j2 index 030ea163456407eb186bced0a079c4dcba6da513..8f466e3dc9431583ecef8e641dd9740d572daa2b 100644 --- a/app/templates/services/spacy_nlp_pipeline.html.j2 +++ b/app/templates/services/spacy_nlp_pipeline.html.j2 @@ -77,7 +77,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" style="color:#0064A3;">help_outline</i></a> - <a class="tooltipped" href="{{ url_for('contributions.create_spacy_nlp_pipeline_model') }}" data-position="bottom" data-tooltip="Add your own spaCy NLP models"><i class="material-icons" style="color:#0064A3">new_label</i></a> + <a class="tooltipped" href="{{ url_for('contributions.spacy_nlp_pipeline_models.create_spacy_nlp_pipeline_model') }}" data-position="bottom" data-tooltip="Add your own spaCy NLP models"><i class="material-icons" style="color:#0064A3">new_label</i></a> </span> </div> </div> diff --git a/app/templates/services/tesseract_ocr_pipeline.html.j2 b/app/templates/services/tesseract_ocr_pipeline.html.j2 index ff4fd38b779260ab86f53ad87a5bfa115f1904a5..11d65c6439850bfb2f18787de2f911bb56efde3a 100644 --- a/app/templates/services/tesseract_ocr_pipeline.html.j2 +++ b/app/templates/services/tesseract_ocr_pipeline.html.j2 @@ -59,7 +59,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" style="color:#00A58B;">help_outline</i></a> - <a class="tooltipped" href="{{ url_for('contributions.create_tesseract_ocr_pipeline_model') }}" data-position="bottom" data-tooltip="Add your own Tesseract OCR models"><i class="material-icons" style="color:#00A58B">new_label</i></a> + <a class="tooltipped" href="{{ url_for('contributions.tesseract_ocr_pipeline_models.create_tesseract_ocr_pipeline_model') }}" data-position="bottom" data-tooltip="Add your own Tesseract OCR models"><i class="material-icons" style="color:#00A58B">new_label</i></a> </span> {% for error in form.model.errors %} <span class="helper-text error-color-text">{{ error }}</span>