diff --git a/.vscode/settings.json b/.vscode/settings.json index 0967ef42..c2a8567e 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 cc747a89..dcb58e47 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 705fbdb4..f4c4ca79 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 83cecec5..f39ca22b 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='/files') + +from .followers import bp as followers_bp +bp.register_blueprint(followers_bp, url_prefix='/followers') diff --git a/app/corpora/files/__init__.py b/app/corpora/files/__init__.py new file mode 100644 index 00000000..2f47a450 --- /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 00000000..f5b0f0c8 --- /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 00000000..cddf4642 --- /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: /files # +# This implies that the corpus_id is always in the kwargs of # +# a route that is registered to this blueprint. # +############################################################################## + + +@bp.route('/', 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 00000000..7715f417 --- /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: /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('/', 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('//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 00000000..e1e6d14e --- /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 00000000..99071731 --- /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: /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('//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('/', 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 8403e621..36ee34c8 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 00000000..0494e1e5 --- /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('/', 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('//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('//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('//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 148f5775..5484fb50 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 "{corpus.title}" 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('/') @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('/', 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('//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('//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('//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('//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'"{corpus_file.filename}" 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('//files/', 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 "{corpus_file.filename}" 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('//files//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('//files/', 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('//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('//followers/', 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('//followers//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 0d79af48..1f0480b4 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 cc6c9268..5a6c413d 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 287a04f5..918f7414 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 e69de29b..00000000 diff --git a/app/static/js/Requests/corpora/corpora.js b/app/static/js/Requests/corpora/corpora.js index 34593d95..b8031e6e 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 c052b2ea..0ce96e2d 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( + ` + + ` + ); + 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 7d431673..955bb424 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( + ` + + ` + ); + 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 e7c5404e..822ab775 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( - ` - - ` - ); - 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( - ` - - ` - ); - 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 50cc18cd..3b8ea308 100644 --- a/app/templates/_roadmap.html.j2 +++ b/app/templates/_roadmap.html.j2 @@ -14,7 +14,7 @@
  • Create corpus
  • navigate_next
  • {% if corpus %} -
  • Create corpus file(s)
  • +
  • Create corpus file(s)
  • {% else %}
  • Create corpus file(s)
  • {% endif %} diff --git a/app/templates/base.html.j2 b/app/templates/base.html.j2 index dbecd35e..c9189e13 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') %} + {% 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 43ad0a13..84eda745 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 @@ {% endblock page_content %} - -{% block modals %} -{{ super() }} - -{% endblock modals %} diff --git a/app/templates/corpora/_breadcrumbs.html.j2 b/app/templates/corpora/_breadcrumbs.html.j2 index af6d2b78..bdbe0f34 100644 --- a/app/templates/corpora/_breadcrumbs.html.j2 +++ b/app/templates/corpora/_breadcrumbs.html.j2 @@ -1,4 +1,4 @@ -{% set breadcrumbs %} +{# {% set breadcrumbs %}
  • navigate_next
  • My corpora
  • navigate_next
  • @@ -25,4 +25,4 @@
  • navigate_next
  • {{ corpus_file.author }}: {{ corpus_file.title }} ({{ corpus_file.publishing_year }})
  • {% endif %} -{% endset %} +{% endset %} #} diff --git a/app/templates/corpora/corpus.html.j2 b/app/templates/corpora/corpus.html.j2 index d8577907..053dbdc0 100644 --- a/app/templates/corpora/corpus.html.j2 +++ b/app/templates/corpora/corpus.html.j2 @@ -91,7 +91,7 @@
    @@ -215,132 +215,3 @@ {% endblock modals %} - -{% block scripts %} -{{ super() }} - -{% endblock scripts %} diff --git a/app/templates/corpora/corpus.js.j2 b/app/templates/corpora/corpus.js.j2 new file mode 100644 index 00000000..7e815cf3 --- /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 44e9ff82..8cff13a6 100644 --- a/app/templates/corpora/create_corpus_file.html.j2 +++ b/app/templates/corpora/files/create_corpus_file.html.j2 @@ -36,7 +36,7 @@
    - closeCancel + closeCancel {{ wtf.render_field(form.submit, material_icon='send') }}
    diff --git a/app/templates/corpora/public_corpora.html.j2 b/app/templates/corpora/public_corpora.html.j2 deleted file mode 100644 index 9be377f3..00000000 --- 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 %} -
    -
    -
    -
    -
    -
    -

    ICorpora

    -
    -
    -
    - search - - -
    -
    -
    -
    -
    - - -
    -
    -
    -
    -
    - - - - - - - - - - -
    Title and DescriptionStatus
    -
      -
      -
      -
      -
      -
      -
      -{% endblock page_content %} - - -{% block scripts %} -{{ super() }} - -{% endblock scripts %} diff --git a/app/templates/corpora/public_corpus.html.j2 b/app/templates/corpora/public_corpus.html.j2 index 41f5798b..ff445e38 100644 --- a/app/templates/corpora/public_corpus.html.j2 +++ b/app/templates/corpora/public_corpus.html.j2 @@ -101,30 +101,3 @@ {% endblock page_content %} - -{% block scripts %} -{{ super() }} - -{% 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 00000000..0b8205f4 --- /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 d45aaa04..00000000 --- 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 %} -
      -
      -
      -
      -

      Dashboard

      -
      -
      -
      -

      Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.

      -
      - -
      -
      - - -
      -
      -
      -
      -
      -
      -
      - search - - -
      -
      -
      -
      -
      - -
      -
      -
      -
      -

      My Corpora

      -

      Create a corpus to interactively perform linguistic analysis.

      -

      Or browse our users public corpora.

      -
      -
      -
      -
      -
      - - - - - - - - - - -
      Title and DescriptionStatus
      -
        -
        -
        - -
        -
        -
        - -
        -
        -
        - -
        -
        -
        -
        -
        -
        -
        - search - - -
        -
        -
        -
        -
        - -
        -
        -
        -
        -

        My Jobs

        -

        - 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 - strongly recommend 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. -

        -

        Where is my Job data? Don't worry, please read this news entry

        -
        -
        -
        -
        -
        - - - - - - - - - - -
        Title and DescriptionStatus
        -
          -
          -
          - -
          -
          -
          - -
          -
          -
          - -
          -
          -
          -
          -
          -
          -
          - search - - -
          -
          -
          -
          -
          - -
          -
          -
          -
          -

          My Groups

          -

          Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.

          -
          -
          -
          -
          -
          - - - - - - - - - - -
          Title and DescriptionStatus
          -
            -
            -
            - -
            -
            -
            - -
            -
            -
            -{% endblock page_content %} - -{% block modals %} -{{ super() }} - -{% endblock modals %} - - -{% block scripts %} -{{ super() }} - -{% endblock scripts %} diff --git a/app/templates/services/spacy_nlp_pipeline.html.j2 b/app/templates/services/spacy_nlp_pipeline.html.j2 index 030ea163..8f466e3d 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 }} help_outline - new_label + new_label diff --git a/app/templates/services/tesseract_ocr_pipeline.html.j2 b/app/templates/services/tesseract_ocr_pipeline.html.j2 index ff4fd38b..11d65c64 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 }} help_outline - new_label + new_label {% for error in form.model.errors %} {{ error }}