From 4fb5f2f2dcb159144bf1b33c99d5ad24a001d3cc Mon Sep 17 00:00:00 2001 From: Patrick Jentsch Date: Mon, 6 Mar 2023 15:02:46 +0100 Subject: [PATCH 01/11] Add content negotiation related route decorators --- app/decorators.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/app/decorators.py b/app/decorators.py index 47e6d749..5fa3d82b 100644 --- a/app/decorators.py +++ b/app/decorators.py @@ -1,7 +1,9 @@ -from flask import abort, current_app +from flask import abort, current_app, request from flask_login import current_user from functools import wraps from threading import Thread +from typing import List, Union +from werkzeug.exceptions import NotAcceptable from app.models import Permission @@ -61,3 +63,55 @@ def background(f): thread.start() return thread return wrapped + + +def consumes(mime_type: str, *_mime_types: str): + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + provided = request.mimetype + consumeables = {mime_type, *_mime_types} + if provided not in consumeables: + raise NotAcceptable() + return f(*args, **kwargs) + return decorated_function + return decorator + + +def produces(mime_type: str, *_mime_types: str): + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + accepted = {*request.accept_mimetypes.values()} + produceables = {mime_type, *_mime_types} + if len(produceables & accepted) == 0: + raise NotAcceptable() + return f(*args, **kwargs) + return decorated_function + return decorator + + +def content_negotiation( + produces: Union[str, List[str]], + consumes: Union[str, List[str]] +): + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + provided = request.mimetype + if isinstance(consumes, str): + consumeables = {consumes} + else: + consumeables = {*consumes} + accepted = {*request.accept_mimetypes.values()} + if isinstance(produces, str): + produceables = {produces} + else: + produceables = {*produces} + if len(produceables & accepted) == 0: + raise NotAcceptable() + if provided not in consumeables: + raise NotAcceptable() + return f(*args, **kwargs) + return decorated_function + return decorator From b98e30022ef021b232a8d382fb6d6eb87a16165b Mon Sep 17 00:00:00 2001 From: Patrick Jentsch Date: Mon, 6 Mar 2023 15:03:13 +0100 Subject: [PATCH 02/11] restructure corpus routes file and add new decorators --- app/contributions/routes.py | 330 ++++++++++++++++++++---------------- 1 file changed, 183 insertions(+), 147 deletions(-) diff --git a/app/contributions/routes.py b/app/contributions/routes.py index 3bc37eb8..da6e26a3 100644 --- a/app/contributions/routes.py +++ b/app/contributions/routes.py @@ -2,20 +2,18 @@ from flask import ( abort, current_app, flash, + jsonify, Markup, redirect, render_template, + request, url_for ) from flask_login import login_required, current_user from threading import Thread from app import db -from app.decorators import permission_required -from app.models import ( - Permission, - SpaCyNLPPipelineModel, - TesseractOCRPipelineModel -) +from app.decorators import content_negotiation, permission_required +from app.models import SpaCyNLPPipelineModel, TesseractOCRPipelineModel from . import bp from .forms import ( CreateSpaCyNLPPipelineModelForm, @@ -39,6 +37,126 @@ def contributions(): ) +############################################################################## +# SpaCy NLP Pipeline Models # +############################################################################## +#region spacy-nlp-pipeline-models +@bp.route('/spacy-nlp-pipeline-models') +def spacy_nlp_pipeline_models(): + return render_template( + 'contributions/spacy_nlp_pipeline_models.html.j2', + title='SpaCy NLP Pipeline Models' + ) + + +@bp.route('/spacy-nlp-pipeline-models/create', methods=['GET', 'POST']) +def create_spacy_nlp_pipeline_model(): + form = CreateSpaCyNLPPipelineModelForm(prefix='create-spacy-nlp-pipeline-model-form') + if form.is_submitted(): + if not form.validate(): + response = {'errors': form.errors} + return response, 400 + try: + spacy_nlp_pipeline_model = SpaCyNLPPipelineModel.create( + form.spacy_model_file.data, + compatible_service_versions=form.compatible_service_versions.data, + description=form.description.data, + pipeline_name=form.pipeline_name.data, + publisher=form.publisher.data, + publisher_url=form.publisher_url.data, + publishing_url=form.publishing_url.data, + publishing_year=form.publishing_year.data, + is_public=False, + title=form.title.data, + version=form.version.data, + user=current_user + ) + except OSError: + abort(500) + db.session.commit() + spacy_nlp_pipeline_model_url = url_for( + '.spacy_nlp_pipeline_model', + spacy_nlp_pipeline_model_id=spacy_nlp_pipeline_model.id + ) + message = Markup(f'SpaCy NLP Pipeline model "{spacy_nlp_pipeline_model.title}" created') + flash(message) + return {}, 201, {'Location': spacy_nlp_pipeline_model_url} + return render_template( + 'contributions/create_spacy_nlp_pipeline_model.html.j2', + form=form, + title='Create SpaCy NLP Pipeline Model' + ) + + +@bp.route('/spacy-nlp-pipeline-models/', methods=['GET', 'POST']) +def spacy_nlp_pipeline_model(spacy_nlp_pipeline_model_id): + spacy_nlp_pipeline_model = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id) + form = EditSpaCyNLPPipelineModelForm( + data=spacy_nlp_pipeline_model.to_json_serializeable(), + prefix='edit-spacy-nlp-pipeline-model-form' + ) + if form.validate_on_submit(): + form.populate_obj(spacy_nlp_pipeline_model) + if db.session.is_modified(spacy_nlp_pipeline_model): + message = Markup(f'SpaCy NLP Pipeline model "{spacy_nlp_pipeline_model.title}" updated') + flash(message) + db.session.commit() + return redirect(url_for('.spacy_nlp_pipeline_models')) + return render_template( + 'contributions/spacy_nlp_pipeline_model.html.j2', + form=form, + spacy_nlp_pipeline_model=spacy_nlp_pipeline_model, + title=f'{spacy_nlp_pipeline_model.title} {spacy_nlp_pipeline_model.version}' + ) + + +@bp.route('/spacy-nlp-pipeline-models/', methods=['DELETE']) +def delete_spacy_model(spacy_nlp_pipeline_model_id): + def _delete_spacy_model(app, spacy_nlp_pipeline_model_id): + with app.app_context(): + spacy_nlp_pipeline_model = SpaCyNLPPipelineModel.query.get(spacy_nlp_pipeline_model_id) + spacy_nlp_pipeline_model.delete() + db.session.commit() + + spacy_nlp_pipeline_model = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id) + if not (spacy_nlp_pipeline_model.user == current_user or current_user.is_administrator()): + abort(403) + thread = Thread( + target=_delete_spacy_model, + args=(current_app._get_current_object(), spacy_nlp_pipeline_model_id) + ) + thread.start() + return {}, 202 + + +#region json-routes +@bp.route('/spacy-nlp-pipeline-models//is_public', methods=['PUT']) +@login_required +@permission_required('CONTRIBUTE') +@content_negotiation(consumes='application/json', produces='application/json') +def update_spacy_nlp_pipeline_model_is_public(spacy_nlp_pipeline_model_id): + is_public = request.json + if not isinstance(is_public, bool): + response = jsonify('The request body must be a boolean') + response.status_code = 400 + abort(response) + spacy_nlp_pipeline_model = \ + SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id) + spacy_nlp_pipeline_model.is_public = is_public + db.session.commit() + response = jsonify( + f'SpaCy NLP Pipeline Model "{spacy_nlp_pipeline_model.title}"' + f' is now {"public" if is_public else "private"}' + ) + response.status_code = 200 + return response +#endregion json-routes +#endregion spacy-nlp-pipeline-models + +############################################################################## +# Tesseract OCR Pipeline Models # +############################################################################## +#region tesseract-ocr-pipeline-models @bp.route('/tesseract-ocr-pipeline-models') def tesseract_ocr_pipeline_models(): return render_template( @@ -47,6 +165,44 @@ def tesseract_ocr_pipeline_models(): ) +@bp.route('/tesseract-ocr-pipeline-models/create', methods=['GET', 'POST']) +def create_tesseract_ocr_pipeline_model(): + form = CreateTesseractOCRPipelineModelForm(prefix='create-tesseract-ocr-pipeline-model-form') + if form.is_submitted(): + if not form.validate(): + response = {'errors': form.errors} + return response, 400 + try: + tesseract_ocr_pipeline_model = TesseractOCRPipelineModel.create( + form.tesseract_model_file.data, + compatible_service_versions=form.compatible_service_versions.data, + description=form.description.data, + publisher=form.publisher.data, + publisher_url=form.publisher_url.data, + publishing_url=form.publishing_url.data, + publishing_year=form.publishing_year.data, + is_public=False, + title=form.title.data, + version=form.version.data, + user=current_user + ) + except OSError: + abort(500) + db.session.commit() + tesseract_ocr_pipeline_model_url = url_for( + '.tesseract_ocr_pipeline_model', + tesseract_ocr_pipeline_model_id=tesseract_ocr_pipeline_model.id + ) + message = Markup(f'Tesseract OCR Pipeline model "{tesseract_ocr_pipeline_model.title}" created') + flash(message) + return {}, 201, {'Location': tesseract_ocr_pipeline_model_url} + return render_template( + 'contributions/create_tesseract_ocr_pipeline_model.html.j2', + form=form, + title='Create Tesseract OCR Pipeline Model' + ) + + @bp.route('/tesseract-ocr-pipeline-models/', methods=['GET', 'POST']) def tesseract_ocr_pipeline_model(tesseract_ocr_pipeline_model_id): tesseract_ocr_pipeline_model = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id) @@ -88,146 +244,26 @@ def delete_tesseract_model(tesseract_ocr_pipeline_model_id): return {}, 202 -@bp.route('/tesseract-ocr-pipeline-models/create', methods=['GET', 'POST']) -def create_tesseract_ocr_pipeline_model(): - form = CreateTesseractOCRPipelineModelForm(prefix='create-tesseract-ocr-pipeline-model-form') - if form.is_submitted(): - if not form.validate(): - response = {'errors': form.errors} - return response, 400 - try: - tesseract_ocr_pipeline_model = TesseractOCRPipelineModel.create( - form.tesseract_model_file.data, - compatible_service_versions=form.compatible_service_versions.data, - description=form.description.data, - publisher=form.publisher.data, - publisher_url=form.publisher_url.data, - publishing_url=form.publishing_url.data, - publishing_year=form.publishing_year.data, - is_public=False, - title=form.title.data, - version=form.version.data, - user=current_user - ) - except OSError: - abort(500) - db.session.commit() - tesseract_ocr_pipeline_model_url = url_for( - '.tesseract_ocr_pipeline_model', - tesseract_ocr_pipeline_model_id=tesseract_ocr_pipeline_model.id - ) - message = Markup(f'Tesseract OCR Pipeline model "{tesseract_ocr_pipeline_model.title}" created') - flash(message) - return {}, 201, {'Location': tesseract_ocr_pipeline_model_url} - return render_template( - 'contributions/create_tesseract_ocr_pipeline_model.html.j2', - form=form, - title='Create Tesseract OCR Pipeline Model' - ) - -@bp.route('/tesseract-ocr-pipeline-models//toggle-public-status', methods=['POST']) -@permission_required(Permission.CONTRIBUTE) -def toggle_tesseract_ocr_pipeline_model_public_status(tesseract_ocr_pipeline_model_id): - tesseract_ocr_pipeline_model = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id) - if not (tesseract_ocr_pipeline_model.user == current_user or current_user.is_administrator()): - abort(403) - tesseract_ocr_pipeline_model.is_public = not tesseract_ocr_pipeline_model.is_public +#region json-routes +@bp.route('/tesseract-ocr-pipeline-models//is_public', methods=['PUT']) +@login_required +@permission_required('CONTRIBUTE') +@content_negotiation(consumes='application/json', produces='application/json') +def update_tesseract_ocr_pipeline_model_is_public(tesseract_ocr_pipeline_model_id): + is_public = request.json + if not isinstance(is_public, bool): + response = jsonify('The request body must be a boolean') + response.status_code = 400 + abort(response) + tesseract_ocr_pipeline_model = \ + TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id) + tesseract_ocr_pipeline_model.is_public = is_public db.session.commit() - return {}, 201 - - -@bp.route('/spacy-nlp-pipeline-models') -def spacy_nlp_pipeline_models(): - return render_template( - 'contributions/spacy_nlp_pipeline_models.html.j2', - title='SpaCy NLP Pipeline Models' + response = jsonify( + f'Tesseract OCR Pipeline Model "{tesseract_ocr_pipeline_model.title}"' + f' is now {"public" if is_public else "private"}' ) - - -@bp.route('/spacy-nlp-pipeline-models/', methods=['GET', 'POST']) -def spacy_nlp_pipeline_model(spacy_nlp_pipeline_model_id): - spacy_nlp_pipeline_model = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id) - form = EditSpaCyNLPPipelineModelForm( - data=spacy_nlp_pipeline_model.to_json_serializeable(), - prefix='edit-spacy-nlp-pipeline-model-form' - ) - if form.validate_on_submit(): - form.populate_obj(spacy_nlp_pipeline_model) - if db.session.is_modified(spacy_nlp_pipeline_model): - message = Markup(f'SpaCy NLP Pipeline model "{spacy_nlp_pipeline_model.title}" updated') - flash(message) - db.session.commit() - return redirect(url_for('.spacy_nlp_pipeline_models')) - return render_template( - 'contributions/spacy_nlp_pipeline_model.html.j2', - form=form, - spacy_nlp_pipeline_model=spacy_nlp_pipeline_model, - title=f'{spacy_nlp_pipeline_model.title} {spacy_nlp_pipeline_model.version}' - ) - -@bp.route('/spacy-nlp-pipeline-models/', methods=['DELETE']) -def delete_spacy_model(spacy_nlp_pipeline_model_id): - def _delete_spacy_model(app, spacy_nlp_pipeline_model_id): - with app.app_context(): - spacy_nlp_pipeline_model = SpaCyNLPPipelineModel.query.get(spacy_nlp_pipeline_model_id) - spacy_nlp_pipeline_model.delete() - db.session.commit() - - spacy_nlp_pipeline_model = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id) - if not (spacy_nlp_pipeline_model.user == current_user or current_user.is_administrator()): - abort(403) - thread = Thread( - target=_delete_spacy_model, - args=(current_app._get_current_object(), spacy_nlp_pipeline_model_id) - ) - thread.start() - return {}, 202 - - -@bp.route('/spacy-nlp-pipeline-models/create', methods=['GET', 'POST']) -def create_spacy_nlp_pipeline_model(): - form = CreateSpaCyNLPPipelineModelForm(prefix='create-spacy-nlp-pipeline-model-form') - if form.is_submitted(): - if not form.validate(): - response = {'errors': form.errors} - return response, 400 - try: - spacy_nlp_pipeline_model = SpaCyNLPPipelineModel.create( - form.spacy_model_file.data, - compatible_service_versions=form.compatible_service_versions.data, - description=form.description.data, - pipeline_name=form.pipeline_name.data, - publisher=form.publisher.data, - publisher_url=form.publisher_url.data, - publishing_url=form.publishing_url.data, - publishing_year=form.publishing_year.data, - is_public=False, - title=form.title.data, - version=form.version.data, - user=current_user - ) - except OSError: - abort(500) - db.session.commit() - spacy_nlp_pipeline_model_url = url_for( - '.spacy_nlp_pipeline_model', - spacy_nlp_pipeline_model_id=spacy_nlp_pipeline_model.id - ) - message = Markup(f'SpaCy NLP Pipeline model "{spacy_nlp_pipeline_model.title}" created') - flash(message) - return {}, 201, {'Location': spacy_nlp_pipeline_model_url} - return render_template( - 'contributions/create_spacy_nlp_pipeline_model.html.j2', - form=form, - title='Create SpaCy NLP Pipeline Model' - ) - -@bp.route('/spacy-nlp-pipeline-models//toggle-public-status', methods=['POST']) -@permission_required(Permission.CONTRIBUTE) -def toggle_spacy_nlp_pipeline_model_public_status(spacy_nlp_pipeline_model_id): - spacy_nlp_pipeline_model = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id) - if not (spacy_nlp_pipeline_model.user == current_user or current_user.is_administrator()): - abort(403) - spacy_nlp_pipeline_model.is_public = not spacy_nlp_pipeline_model.is_public - db.session.commit() - return {}, 201 + response.status_code = 200 + return response +#endregion json-routes +#endregion tesseract-ocr-pipeline-models From cfa4fa68f2a8b157b4287e0181160f99ccf37d99 Mon Sep 17 00:00:00 2001 From: Patrick Jentsch Date: Mon, 6 Mar 2023 15:03:33 +0100 Subject: [PATCH 03/11] Remove dev route --- app/corpora/routes.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/app/corpora/routes.py b/app/corpora/routes.py index a487e834..274eebfb 100644 --- a/app/corpora/routes.py +++ b/app/corpora/routes.py @@ -31,15 +31,6 @@ from .forms import ( UpdateCorpusFileForm ) -@bp.route('/fake-add') -@login_required -def fake_add(): - pjentsch = User.query.filter_by(username='pjentsch').first() - alice = Corpus.query.filter_by(title='Alice in Wonderland').first() - pjentsch.follow_corpus(alice) - db.session.commit() - return '' - @bp.route('//is_public', methods=['POST']) @login_required From 7770d4d4782ae45d5191a2d8885e0d04aef116c6 Mon Sep 17 00:00:00 2001 From: Patrick Jentsch Date: Mon, 6 Mar 2023 15:04:06 +0100 Subject: [PATCH 04/11] Restructure toggle ispublic requests --- .../SpacyNLPPipelineModelList.js | 6 +- .../TesseractOCRPipelineModelList.js | 6 +- app/static/js/Utils.js | 78 ++++++++++++------- 3 files changed, 61 insertions(+), 29 deletions(-) diff --git a/app/static/js/ResourceLists/SpacyNLPPipelineModelList.js b/app/static/js/ResourceLists/SpacyNLPPipelineModelList.js index f7901528..405c29d6 100644 --- a/app/static/js/ResourceLists/SpacyNLPPipelineModelList.js +++ b/app/static/js/ResourceLists/SpacyNLPPipelineModelList.js @@ -119,7 +119,11 @@ class SpaCyNLPPipelineModelList extends ResourceList { let listAction = listActionElement.dataset.listAction; switch (listAction) { case 'toggle-is-public': { - Utils.spaCyNLPPipelineModelToggleIsPublicRequest(this.userId, itemId); + let newIsPublicValue = listActionElement.checked; + Utils.updateSpaCyNLPPipelineModelIsPublicRequest(itemId, newIsPublicValue) + .catch((response) => { + listActionElement.checked = !newIsPublicValue; + }); break; } default: { diff --git a/app/static/js/ResourceLists/TesseractOCRPipelineModelList.js b/app/static/js/ResourceLists/TesseractOCRPipelineModelList.js index c5e08b1d..8d9ce515 100644 --- a/app/static/js/ResourceLists/TesseractOCRPipelineModelList.js +++ b/app/static/js/ResourceLists/TesseractOCRPipelineModelList.js @@ -128,7 +128,11 @@ class TesseractOCRPipelineModelList extends ResourceList { let listAction = listActionElement.dataset.listAction; switch (listAction) { case 'toggle-is-public': { - Utils.tesseractOCRPipelineModelToggleIsPublicRequest(this.userId, itemId); + let newIsPublicValue = listActionElement.checked; + Utils.updateTesseractOCRPipelineModelIsPublicRequest(itemId, newIsPublicValue) + .catch((response) => { + listActionElement.checked = !newIsPublicValue; + }); break; } default: { diff --git a/app/static/js/Utils.js b/app/static/js/Utils.js index c5a55e89..88dadea1 100644 --- a/app/static/js/Utils.js +++ b/app/static/js/Utils.js @@ -101,13 +101,21 @@ class Utils { static updateCorpusFollowerRole(corpusId, followerId, roleName) { return new Promise((resolve, reject) => { - fetch(`/corpora/${corpusId}/followers/${followerId}/role`, {method: 'POST', headers: {Accept: 'application/json', 'Content-Type': 'application/json'}, body: JSON.stringify({role: roleName})}) + let fetchRessource = `/corpora/${corpusId}/followers/${followerId}/role`; + let fetchOptions = { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({role: roleName}) + }; + fetch(fetchRessource, fetchOptions) .then( (response) => { if (response.ok) { app.flash('Role updated', 'corpus'); resolve(response); - return; } else { app.flash(`${response.statusText}`, 'error'); reject(response); @@ -179,7 +187,15 @@ class Utils { static unfollowCorpusRequest(corpusId, followerId) { return new Promise((resolve, reject) => { - fetch(`/corpora/${corpusId}/followers/${followerId}/unfollow`, {method: 'POST', headers: {Accept: 'application/json'}}) + let fetchRessource = `/corpora/${corpusId}/followers/${followerId}/unfollow`; + let fetchOptions = { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + } + }; + fetch(fetchRessource, fetchOptions) .then( (response) => { if (response.ok) { @@ -683,23 +699,27 @@ class Utils { }); } - static tesseractOCRPipelineModelToggleIsPublicRequest(userId, tesseractOCRPipelineModelId, is_public) { + static updateTesseractOCRPipelineModelIsPublicRequest(tesseractOCRPipelineModelId, newIsPublicValue) { return new Promise((resolve, reject) => { - let tesseractOCRPipelineModel; - try { - tesseractOCRPipelineModel = app.data.users[userId].tesseract_ocr_pipeline_models[tesseractOCRPipelineModelId]; - } catch (error) { - tesseractOCRPipelineModel = {}; - } - - fetch(`/contributions/tesseract-ocr-pipeline-models/${tesseractOCRPipelineModelId}/toggle-public-status`, {method: 'POST', headers: {Accept: 'application/json'}}) + let fetchRessource = `/contributions/tesseract-ocr-pipeline-models/${tesseractOCRPipelineModelId}/is_public`; + let fetchOptions = { + method: 'PUT', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(newIsPublicValue) + }; + fetch(fetchRessource, fetchOptions) .then( (response) => { - if (response.status === 403) { - app.flash('Forbidden', 'error'); + if (response.ok) { + response.json().then((data) => {app.flash(data);}); + resolve(response); + } else { + app.flash(`${response.statusText}`, 'error'); reject(response); } - resolve(response); }, (response) => { app.flash('Something went wrong', 'error'); @@ -709,23 +729,27 @@ class Utils { }); } - static spaCyNLPPipelineModelToggleIsPublicRequest(userId, spaCyNLPPipelineModelId) { + static updateSpaCyNLPPipelineModelIsPublicRequest(SpaCyNLPPipelineModelId, newIsPublicValue) { return new Promise((resolve, reject) => { - let spaCyNLPPipelineModel; - try { - spaCyNLPPipelineModel = app.data.users[userId].spacy_nlp_pipeline_models[spaCyNLPPipelineModelId]; - } catch (error) { - spaCyNLPPipelineModel = {}; - } - - fetch(`/contributions/spacy-nlp-pipeline-models/${spaCyNLPPipelineModelId}/toggle-public-status`, {method: 'POST', headers: {Accept: 'application/json'}}) + let fetchRessource = `/contributions/spacy-nlp-pipeline-models/${SpaCyNLPPipelineModelId}/is_public`; + let fetchOptions = { + method: 'PUT', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(newIsPublicValue) + }; + fetch(fetchRessource, fetchOptions) .then( (response) => { - if (response.status === 403) { - app.flash('Forbidden', 'error'); + if (response.ok) { + response.json().then((data) => {app.flash(data);}); + resolve(response); + } else { + app.flash(`${response.statusText}`, 'error'); reject(response); } - resolve(response); }, (response) => { app.flash('Something went wrong', 'error'); From 9272150212901bbdba5a553d8f074a9cd8000cba Mon Sep 17 00:00:00 2001 From: Patrick Jentsch Date: Tue, 7 Mar 2023 16:32:15 +0100 Subject: [PATCH 05/11] combine content_negotiation related decorators --- app/decorators.py | 52 ++++++++++++++++------------------------------- 1 file changed, 17 insertions(+), 35 deletions(-) diff --git a/app/decorators.py b/app/decorators.py index 5fa3d82b..21527233 100644 --- a/app/decorators.py +++ b/app/decorators.py @@ -65,52 +65,34 @@ def background(f): return wrapped -def consumes(mime_type: str, *_mime_types: str): - def decorator(f): - @wraps(f) - def decorated_function(*args, **kwargs): - provided = request.mimetype - consumeables = {mime_type, *_mime_types} - if provided not in consumeables: - raise NotAcceptable() - return f(*args, **kwargs) - return decorated_function - return decorator - - -def produces(mime_type: str, *_mime_types: str): - def decorator(f): - @wraps(f) - def decorated_function(*args, **kwargs): - accepted = {*request.accept_mimetypes.values()} - produceables = {mime_type, *_mime_types} - if len(produceables & accepted) == 0: - raise NotAcceptable() - return f(*args, **kwargs) - return decorated_function - return decorator - - def content_negotiation( - produces: Union[str, List[str]], - consumes: Union[str, List[str]] + produces: Union[str, List[str], None] = None, + consumes: Union[str, List[str], None] = None ): def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): provided = request.mimetype - if isinstance(consumes, str): + if consumes is None: + consumeables = None + elif isinstance(consumes, str): consumeables = {consumes} - else: + elif isinstance(consumes, list) and all(isinstance(x, str) for x in consumes): consumeables = {*consumes} - accepted = {*request.accept_mimetypes.values()} - if isinstance(produces, str): - produceables = {produces} else: + raise TypeError() + accepted = {*request.accept_mimetypes.values()} + if produces is None: + produceables = None + elif isinstance(produces, str): + produceables = {produces} + elif isinstance(produces, list) and all(isinstance(x, str) for x in produces): produceables = {*produces} - if len(produceables & accepted) == 0: + else: + raise TypeError() + if produceables is not None and len(produceables & accepted) == 0: raise NotAcceptable() - if provided not in consumeables: + if consumeables is not None and provided not in consumeables: raise NotAcceptable() return f(*args, **kwargs) return decorated_function From 09fdad2162b6fb0d8dbedab67a0ef6b3143a2f49 Mon Sep 17 00:00:00 2001 From: Patrick Jentsch Date: Tue, 7 Mar 2023 16:34:49 +0100 Subject: [PATCH 06/11] Standardize code for reference --- app/contributions/routes.py | 54 ++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/app/contributions/routes.py b/app/contributions/routes.py index da6e26a3..4b5b3d1e 100644 --- a/app/contributions/routes.py +++ b/app/contributions/routes.py @@ -110,7 +110,10 @@ def spacy_nlp_pipeline_model(spacy_nlp_pipeline_model_id): ) +#region json-routes @bp.route('/spacy-nlp-pipeline-models/', methods=['DELETE']) +@login_required +@content_negotiation(produces='application/json') def delete_spacy_model(spacy_nlp_pipeline_model_id): def _delete_spacy_model(app, spacy_nlp_pipeline_model_id): with app.app_context(): @@ -118,18 +121,21 @@ def delete_spacy_model(spacy_nlp_pipeline_model_id): spacy_nlp_pipeline_model.delete() db.session.commit() - spacy_nlp_pipeline_model = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id) - if not (spacy_nlp_pipeline_model.user == current_user or current_user.is_administrator()): + snpm = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id) + if not (snpm.user == current_user or current_user.is_administrator()): abort(403) thread = Thread( target=_delete_spacy_model, args=(current_app._get_current_object(), spacy_nlp_pipeline_model_id) ) thread.start() - return {}, 202 + response = jsonify( + f'SpaCy NLP Pipeline Model "{snpm.title}" marked for deletion' + ) + response.status_code = 202 + return response -#region json-routes @bp.route('/spacy-nlp-pipeline-models//is_public', methods=['PUT']) @login_required @permission_required('CONTRIBUTE') @@ -137,15 +143,14 @@ def delete_spacy_model(spacy_nlp_pipeline_model_id): def update_spacy_nlp_pipeline_model_is_public(spacy_nlp_pipeline_model_id): is_public = request.json if not isinstance(is_public, bool): - response = jsonify('The request body must be a boolean') - response.status_code = 400 - abort(response) - spacy_nlp_pipeline_model = \ - SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id) - spacy_nlp_pipeline_model.is_public = is_public + abort(400) + snpm = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id) + if not (snpm.user == current_user or current_user.is_administrator()): + abort(403) + snpm.is_public = is_public db.session.commit() response = jsonify( - f'SpaCy NLP Pipeline Model "{spacy_nlp_pipeline_model.title}"' + f'SpaCy NLP Pipeline Model "{snpm.title}"' f' is now {"public" if is_public else "private"}' ) response.status_code = 200 @@ -225,7 +230,10 @@ def tesseract_ocr_pipeline_model(tesseract_ocr_pipeline_model_id): ) +#region json-routes @bp.route('/tesseract-ocr-pipeline-models/', methods=['DELETE']) +@login_required +@content_negotiation(produces='application/json') def delete_tesseract_model(tesseract_ocr_pipeline_model_id): def _delete_tesseract_ocr_pipeline_model(app, tesseract_ocr_pipeline_model_id): with app.app_context(): @@ -233,18 +241,21 @@ def delete_tesseract_model(tesseract_ocr_pipeline_model_id): tesseract_ocr_pipeline_model.delete() db.session.commit() - tesseract_ocr_pipeline_model = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id) - if not (tesseract_ocr_pipeline_model.user == current_user or current_user.is_administrator()): + topm = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id) + if not (topm.user == current_user or current_user.is_administrator()): abort(403) thread = Thread( target=_delete_tesseract_ocr_pipeline_model, args=(current_app._get_current_object(), tesseract_ocr_pipeline_model_id) ) thread.start() - return {}, 202 + response = jsonify( + f'Tesseract OCR Pipeline Model "{topm.title}" marked for deletion' + ) + response.status_code = 200 + return response -#region json-routes @bp.route('/tesseract-ocr-pipeline-models//is_public', methods=['PUT']) @login_required @permission_required('CONTRIBUTE') @@ -252,15 +263,14 @@ def delete_tesseract_model(tesseract_ocr_pipeline_model_id): def update_tesseract_ocr_pipeline_model_is_public(tesseract_ocr_pipeline_model_id): is_public = request.json if not isinstance(is_public, bool): - response = jsonify('The request body must be a boolean') - response.status_code = 400 - abort(response) - tesseract_ocr_pipeline_model = \ - TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id) - tesseract_ocr_pipeline_model.is_public = is_public + abort(400) + topm = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id) + if not (topm.user == current_user or current_user.is_administrator()): + abort(403) + topm.is_public = is_public db.session.commit() response = jsonify( - f'Tesseract OCR Pipeline Model "{tesseract_ocr_pipeline_model.title}"' + f'Tesseract OCR Pipeline Model "{topm.title}"' f' is now {"public" if is_public else "private"}' ) response.status_code = 200 From 0e7e5933cc9afdd2bf3e416f94fc8db87cf8fb9f Mon Sep 17 00:00:00 2001 From: Patrick Jentsch Date: Wed, 8 Mar 2023 10:34:46 +0100 Subject: [PATCH 07/11] Better exception handling in json-routes --- app/contributions/routes.py | 140 ++++++++++++++++++++++++++++-------- 1 file changed, 109 insertions(+), 31 deletions(-) diff --git a/app/contributions/routes.py b/app/contributions/routes.py index 4b5b3d1e..d9565f88 100644 --- a/app/contributions/routes.py +++ b/app/contributions/routes.py @@ -117,21 +117,37 @@ def spacy_nlp_pipeline_model(spacy_nlp_pipeline_model_id): def delete_spacy_model(spacy_nlp_pipeline_model_id): def _delete_spacy_model(app, spacy_nlp_pipeline_model_id): with app.app_context(): - spacy_nlp_pipeline_model = SpaCyNLPPipelineModel.query.get(spacy_nlp_pipeline_model_id) - spacy_nlp_pipeline_model.delete() + snpm = SpaCyNLPPipelineModel.query.get(spacy_nlp_pipeline_model_id) + snpm.delete() db.session.commit() - snpm = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id) + snpm = SpaCyNLPPipelineModel.query.get(spacy_nlp_pipeline_model_id) + if snpm is None: + resonse_data = { + 'message': f'"{snpm.title}" not found', + 'category': 'error' + } + response = jsonify(resonse_data) + response.status_code = 404 + return response if not (snpm.user == current_user or current_user.is_administrator()): - abort(403) + resonse_data = { + 'message': f'You are not allowed to delete "{snpm.title}"', + 'category': 'error' + } + response = jsonify(resonse_data) + response.status_code = 403 + return response thread = Thread( target=_delete_spacy_model, - args=(current_app._get_current_object(), spacy_nlp_pipeline_model_id) + args=(current_app._get_current_object(), snpm.id) ) thread.start() - response = jsonify( - f'SpaCy NLP Pipeline Model "{snpm.title}" marked for deletion' - ) + resonse_data = { + 'message': \ + f'SpaCy NLP Pipeline Model "{snpm.title}" marked for deletion' + } + response = jsonify(resonse_data) response.status_code = 202 return response @@ -143,16 +159,39 @@ def delete_spacy_model(spacy_nlp_pipeline_model_id): def update_spacy_nlp_pipeline_model_is_public(spacy_nlp_pipeline_model_id): is_public = request.json if not isinstance(is_public, bool): - abort(400) - snpm = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id) + resonse_data = { + 'message': 'Request body must be a boolean', + 'category': 'error' + } + response = jsonify(resonse_data) + response.status_code = 400 + return response + snpm = SpaCyNLPPipelineModel.query.get(spacy_nlp_pipeline_model_id) + if snpm is None: + resonse_data = { + 'message': f'"{snpm.title}" not found', + 'category': 'error' + } + response = jsonify(resonse_data) + response.status_code = 404 + return response if not (snpm.user == current_user or current_user.is_administrator()): - abort(403) + resonse_data = { + 'message': f'You are not allowed to delete "{snpm.title}"', + 'category': 'error' + } + response = jsonify(resonse_data) + response.status_code = 403 + return response snpm.is_public = is_public db.session.commit() - response = jsonify( - f'SpaCy NLP Pipeline Model "{snpm.title}"' - f' is now {"public" if is_public else "private"}' - ) + response_data = { + 'message': ( + f'SpaCy NLP Pipeline Model "{snpm.title}"' + f' is now {"public" if is_public else "private"}' + ) + } + response = jsonify(response_data) response.status_code = 200 return response #endregion json-routes @@ -237,22 +276,38 @@ def tesseract_ocr_pipeline_model(tesseract_ocr_pipeline_model_id): def delete_tesseract_model(tesseract_ocr_pipeline_model_id): def _delete_tesseract_ocr_pipeline_model(app, tesseract_ocr_pipeline_model_id): with app.app_context(): - tesseract_ocr_pipeline_model = TesseractOCRPipelineModel.query.get(tesseract_ocr_pipeline_model_id) - tesseract_ocr_pipeline_model.delete() + topm = TesseractOCRPipelineModel.query.get(tesseract_ocr_pipeline_model_id) + topm.delete() db.session.commit() - topm = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id) + topm = TesseractOCRPipelineModel.query.get(tesseract_ocr_pipeline_model_id) + if topm is None: + resonse_data = { + 'message': f'"{topm.title}" not found', + 'category': 'error' + } + response = jsonify(resonse_data) + response.status_code = 404 + return response if not (topm.user == current_user or current_user.is_administrator()): - abort(403) + resonse_data = { + 'message': f'You are not allowed to delete "{topm.title}"', + 'category': 'error' + } + response = jsonify(resonse_data) + response.status_code = 403 + return response thread = Thread( target=_delete_tesseract_ocr_pipeline_model, - args=(current_app._get_current_object(), tesseract_ocr_pipeline_model_id) + args=(current_app._get_current_object(), topm.id) ) thread.start() - response = jsonify( - f'Tesseract OCR Pipeline Model "{topm.title}" marked for deletion' - ) - response.status_code = 200 + resonse_data = { + 'message': \ + f'Tesseract OCR Pipeline Model "{topm.title}" marked for deletion' + } + response = jsonify(resonse_data) + response.status_code = 202 return response @@ -263,16 +318,39 @@ def delete_tesseract_model(tesseract_ocr_pipeline_model_id): def update_tesseract_ocr_pipeline_model_is_public(tesseract_ocr_pipeline_model_id): is_public = request.json if not isinstance(is_public, bool): - abort(400) - topm = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id) + resonse_data = { + 'message': 'Request body must be a boolean', + 'category': 'error' + } + response = jsonify(resonse_data) + response.status_code = 400 + return response + topm = TesseractOCRPipelineModel.query.get(tesseract_ocr_pipeline_model_id) + if topm is None: + resonse_data = { + 'message': f'"{topm.title}" not found', + 'category': 'error' + } + response = jsonify(resonse_data) + response.status_code = 404 + return response if not (topm.user == current_user or current_user.is_administrator()): - abort(403) + resonse_data = { + 'message': f'You are not allowed to delete "{topm.title}"', + 'category': 'error' + } + response = jsonify(resonse_data) + response.status_code = 403 + return response topm.is_public = is_public db.session.commit() - response = jsonify( - f'Tesseract OCR Pipeline Model "{topm.title}"' - f' is now {"public" if is_public else "private"}' - ) + response_data = { + 'message': ( + f'Tesseract OCR Pipeline Model "{topm.title}"' + f' is now {"public" if is_public else "private"}' + ) + } + response = jsonify(response_data) response.status_code = 200 return response #endregion json-routes From 0d7fca9b0b3a19de5fcbd4ce13831b9e72db2ea1 Mon Sep 17 00:00:00 2001 From: Patrick Jentsch Date: Wed, 8 Mar 2023 10:41:54 +0100 Subject: [PATCH 08/11] Fix logic error --- app/contributions/routes.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/app/contributions/routes.py b/app/contributions/routes.py index d9565f88..f9e4d2c5 100644 --- a/app/contributions/routes.py +++ b/app/contributions/routes.py @@ -124,7 +124,10 @@ def delete_spacy_model(spacy_nlp_pipeline_model_id): snpm = SpaCyNLPPipelineModel.query.get(spacy_nlp_pipeline_model_id) if snpm is None: resonse_data = { - 'message': f'"{snpm.title}" not found', + 'message': ( + 'SpaCy NLP Pipeline Model with id' + f' "{spacy_nlp_pipeline_model_id}" not found' + ), 'category': 'error' } response = jsonify(resonse_data) @@ -169,7 +172,10 @@ def update_spacy_nlp_pipeline_model_is_public(spacy_nlp_pipeline_model_id): snpm = SpaCyNLPPipelineModel.query.get(spacy_nlp_pipeline_model_id) if snpm is None: resonse_data = { - 'message': f'"{snpm.title}" not found', + 'message': ( + 'SpaCy NLP Pipeline Model with id' + f' "{spacy_nlp_pipeline_model_id}" not found' + ), 'category': 'error' } response = jsonify(resonse_data) @@ -283,7 +289,10 @@ def delete_tesseract_model(tesseract_ocr_pipeline_model_id): topm = TesseractOCRPipelineModel.query.get(tesseract_ocr_pipeline_model_id) if topm is None: resonse_data = { - 'message': f'"{topm.title}" not found', + 'message': ( + 'Tesseract OCR Pipeline Model with id' + f' "{tesseract_ocr_pipeline_model_id}" not found' + ), 'category': 'error' } response = jsonify(resonse_data) @@ -328,7 +337,10 @@ def update_tesseract_ocr_pipeline_model_is_public(tesseract_ocr_pipeline_model_i topm = TesseractOCRPipelineModel.query.get(tesseract_ocr_pipeline_model_id) if topm is None: resonse_data = { - 'message': f'"{topm.title}" not found', + 'message': ( + 'Tesseract OCR Pipeline Model with id' + f' "{tesseract_ocr_pipeline_model_id}" not found' + ), 'category': 'error' } response = jsonify(resonse_data) From 6bb4594937619307e66bacf65d0e560547af9964 Mon Sep 17 00:00:00 2001 From: Patrick Jentsch Date: Wed, 8 Mar 2023 11:42:53 +0100 Subject: [PATCH 09/11] Restructure javascript --- app/static/js/Requests/Contributions.js | 51 +++++++++++++++++++ app/static/js/Requests/Requests.js | 37 ++++++++++++++ .../CorpusDisplay.js | 9 +--- .../JobDisplay.js | 2 +- .../ResourceDisplay.js} | 2 +- .../SpacyNLPPipelineModelList.js | 4 +- .../TesseractOCRPipelineModelList.js | 4 +- app/templates/_scripts.html.j2 | 44 ++++++++++++---- 8 files changed, 131 insertions(+), 22 deletions(-) create mode 100644 app/static/js/Requests/Contributions.js create mode 100644 app/static/js/Requests/Requests.js rename app/static/js/{RessourceDisplays => ResourceDisplays}/CorpusDisplay.js (91%) rename app/static/js/{RessourceDisplays => ResourceDisplays}/JobDisplay.js (99%) rename app/static/js/{RessourceDisplays/RessourceDisplay.js => ResourceDisplays/ResourceDisplay.js} (97%) diff --git a/app/static/js/Requests/Contributions.js b/app/static/js/Requests/Contributions.js new file mode 100644 index 00000000..30605135 --- /dev/null +++ b/app/static/js/Requests/Contributions.js @@ -0,0 +1,51 @@ +/***************************************************************************** +* Contributions * +* Fetch requests for /contributions routes * +*****************************************************************************/ +Requests.contributions = {}; + +Requests.contributions.spacy_nlp_pipeline_models = {}; + +Requests.contributions.spacy_nlp_pipeline_models.ent = {}; + +Requests.contributions.spacy_nlp_pipeline_models.ent.delete = (spacyNlpPipelineModelId) => { + let input = `/contributions/spacy-nlp-pipeline-models/${spacyNlpPipelineModelId}`; + let init = { + method: 'DELETE' + }; + return Requests.JSONfetch(input, init); +}; + +Requests.contributions.spacy_nlp_pipeline_models.ent.isPublic = {}; + +Requests.contributions.spacy_nlp_pipeline_models.ent.isPublic.update = (spacyNlpPipelineModelId, value) => { + let input = `/contributions/spacy-nlp-pipeline-models/${spacyNlpPipelineModelId}/is_public`; + let init = { + method: 'PUT', + body: JSON.stringify(value) + }; + return Requests.JSONfetch(input, init); +}; + +Requests.contributions.tesseract_ocr_pipeline_models = {}; + +Requests.contributions.tesseract_ocr_pipeline_models.ent = {}; + +Requests.contributions.tesseract_ocr_pipeline_models.ent.delete = (tesseractOcrPipelineModelId) => { + let input = `/contributions/tesseract-ocr-pipeline-models/${tesseractOcrPipelineModelId}`; + let init = { + method: 'DELETE' + }; + return Requests.JSONfetch(input, init); +}; + +Requests.contributions.tesseract_ocr_pipeline_models.ent.isPublic = {}; + +Requests.contributions.tesseract_ocr_pipeline_models.ent.isPublic.update = (tesseractOcrPipelineModelId, value) => { + let input = `/contributions/tesseract-ocr-pipeline-models/${tesseractOcrPipelineModelId}/is_public`; + let init = { + method: 'PUT', + body: JSON.stringify(value) + }; + return Requests.JSONfetch(input, init); +}; diff --git a/app/static/js/Requests/Requests.js b/app/static/js/Requests/Requests.js new file mode 100644 index 00000000..41f05a7f --- /dev/null +++ b/app/static/js/Requests/Requests.js @@ -0,0 +1,37 @@ +Requests = {}; + +Requests.JSONfetch = (input, init={}) => { + return new Promise((resolve, reject) => { + let fixedInit = {}; + fixedInit.headers = {}; + fixedInit.headers['Accept'] = 'application/json'; + if (init.hasOwnProperty('body')) { + fixedInit.headers['Content-Type'] = 'application/json'; + } + fetch(input, Utils.mergeObjectsDeep(init, fixedInit)) + .then( + (response) => { + response.json() + .then( + (json) => { + let message = json.message || json; + let category = json.category || 'message'; + app.flash(message, category); + }, + (error) => { + app.flash(`[${response.status}]: ${response.statusText}`, 'error'); + } + ); + if (response.ok) { + resolve(response); + } else { + reject(response); + } + }, + (response) => { + app.flash('Something went wrong', 'error'); + reject(response); + } + ); + }); +}; diff --git a/app/static/js/RessourceDisplays/CorpusDisplay.js b/app/static/js/ResourceDisplays/CorpusDisplay.js similarity index 91% rename from app/static/js/RessourceDisplays/CorpusDisplay.js rename to app/static/js/ResourceDisplays/CorpusDisplay.js index ab462e9b..07f0a9be 100644 --- a/app/static/js/RessourceDisplays/CorpusDisplay.js +++ b/app/static/js/ResourceDisplays/CorpusDisplay.js @@ -1,16 +1,11 @@ -class CorpusDisplay extends RessourceDisplay { +class CorpusDisplay extends ResourceDisplay { constructor(displayElement) { super(displayElement); this.corpusId = displayElement.dataset.corpusId; this.displayElement .querySelector('.action-button[data-action="build-request"]') .addEventListener('click', (event) => { - Utils.buildCorpusRequest(this.userId, this.corpusId); - }); - this.displayElement - .querySelector('.action-button[data-action="delete-request"]') - .addEventListener('click', (event) => { - Utils.deleteCorpusRequest(this.userId, this.corpusId); + Requests.corpora.corpus.build(this.corpusId); }); } diff --git a/app/static/js/RessourceDisplays/JobDisplay.js b/app/static/js/ResourceDisplays/JobDisplay.js similarity index 99% rename from app/static/js/RessourceDisplays/JobDisplay.js rename to app/static/js/ResourceDisplays/JobDisplay.js index 03c5601b..8b94e49b 100644 --- a/app/static/js/RessourceDisplays/JobDisplay.js +++ b/app/static/js/ResourceDisplays/JobDisplay.js @@ -1,4 +1,4 @@ -class JobDisplay extends RessourceDisplay { +class JobDisplay extends ResourceDisplay { constructor(displayElement) { super(displayElement); this.jobId = this.displayElement.dataset.jobId; diff --git a/app/static/js/RessourceDisplays/RessourceDisplay.js b/app/static/js/ResourceDisplays/ResourceDisplay.js similarity index 97% rename from app/static/js/RessourceDisplays/RessourceDisplay.js rename to app/static/js/ResourceDisplays/ResourceDisplay.js index a07c2163..24a5dec3 100644 --- a/app/static/js/RessourceDisplays/RessourceDisplay.js +++ b/app/static/js/ResourceDisplays/ResourceDisplay.js @@ -1,4 +1,4 @@ -class RessourceDisplay { +class ResourceDisplay { constructor(displayElement) { this.displayElement = displayElement; this.userId = this.displayElement.dataset.userId; diff --git a/app/static/js/ResourceLists/SpacyNLPPipelineModelList.js b/app/static/js/ResourceLists/SpacyNLPPipelineModelList.js index 405c29d6..5997fb0c 100644 --- a/app/static/js/ResourceLists/SpacyNLPPipelineModelList.js +++ b/app/static/js/ResourceLists/SpacyNLPPipelineModelList.js @@ -120,7 +120,7 @@ class SpaCyNLPPipelineModelList extends ResourceList { switch (listAction) { case 'toggle-is-public': { let newIsPublicValue = listActionElement.checked; - Utils.updateSpaCyNLPPipelineModelIsPublicRequest(itemId, newIsPublicValue) + Requests.contributions.spacy_nlp_pipeline_models.ent.isPublic.update(itemId, newIsPublicValue) .catch((response) => { listActionElement.checked = !newIsPublicValue; }); @@ -141,7 +141,7 @@ class SpaCyNLPPipelineModelList extends ResourceList { let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction; switch (listAction) { case 'delete-request': { - Utils.deleteSpaCyNLPPipelineModelRequest(this.userId, itemId); + Requests.contributions.spacy_nlp_pipeline_models.ent.delete(itemId); break; } case 'view': { diff --git a/app/static/js/ResourceLists/TesseractOCRPipelineModelList.js b/app/static/js/ResourceLists/TesseractOCRPipelineModelList.js index 8d9ce515..29d48dbb 100644 --- a/app/static/js/ResourceLists/TesseractOCRPipelineModelList.js +++ b/app/static/js/ResourceLists/TesseractOCRPipelineModelList.js @@ -129,7 +129,7 @@ class TesseractOCRPipelineModelList extends ResourceList { switch (listAction) { case 'toggle-is-public': { let newIsPublicValue = listActionElement.checked; - Utils.updateTesseractOCRPipelineModelIsPublicRequest(itemId, newIsPublicValue) + Requests.contributions.tesseract_ocr_pipeline_models.ent.isPublic.update(itemId, newIsPublicValue) .catch((response) => { listActionElement.checked = !newIsPublicValue; }); @@ -155,7 +155,7 @@ class TesseractOCRPipelineModelList extends ResourceList { let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction; switch (listAction) { case 'delete-request': { - Utils.deleteTesseractOCRPipelineModelRequest(this.userId, itemId); + Requests.contributions.tesseract_ocr_pipeline_models.ent.delete(itemId); break; } case 'view': { diff --git a/app/templates/_scripts.html.j2 b/app/templates/_scripts.html.j2 index 18cde5b8..a97f1d97 100644 --- a/app/templates/_scripts.html.j2 +++ b/app/templates/_scripts.html.j2 @@ -6,18 +6,37 @@ output='gen/app.%(version)s.js', 'js/App.js', 'js/Utils.js', - 'js/Forms/Form.js', - 'js/Forms/CreateCorpusFileForm.js', - 'js/Forms/CreateJobForm.js', - 'js/Forms/CreateContributionForm.js', 'js/CorpusAnalysis/CQiClient.js', 'js/CorpusAnalysis/CorpusAnalysisApp.js', 'js/CorpusAnalysis/CorpusAnalysisConcordance.js', 'js/CorpusAnalysis/CorpusAnalysisReader.js', 'js/CorpusAnalysis/QueryBuilder.js', - 'js/RessourceDisplays/RessourceDisplay.js', - 'js/RessourceDisplays/CorpusDisplay.js', - 'js/RessourceDisplays/JobDisplay.js', + 'js/XMLtoObject.js' +%} + +{%- endassets %} +{%- assets + filters='rjsmin', + output='gen/Forms.%(version)s.js', + 'js/Forms/Form.js', + 'js/Forms/CreateCorpusFileForm.js', + 'js/Forms/CreateJobForm.js', + 'js/Forms/CreateContributionForm.js' +%} + +{%- endassets %} +{%- assets + filters='rjsmin', + output='gen/ResourceDisplays.%(version)s.js', + 'js/ResourceDisplays/ResourceDisplay.js', + 'js/ResourceDisplays/CorpusDisplay.js', + 'js/ResourceDisplays/JobDisplay.js' +%} + +{%- endassets %} +{%- assets + filters='rjsmin', + output='gen/ResourceLists.%(version)s.js', 'js/ResourceLists/ResourceList.js', 'js/ResourceLists/CorpusFileList.js', 'js/ResourceLists/PublicCorpusFileList.js', @@ -31,8 +50,15 @@ 'js/ResourceLists/TesseractOCRPipelineModelList.js', 'js/ResourceLists/UserList.js', 'js/ResourceLists/AdminUserList.js', - 'js/ResourceLists/CorpusFollowerList.js', - 'js/XMLtoObject.js' + 'js/ResourceLists/CorpusFollowerList.js' +%} + +{%- endassets %} +{%- assets + filters='rjsmin', + output='gen/Requests.%(version)s.js', + 'js/Requests/Requests.js', + 'js/Requests/Contributions.js' %} {%- endassets %} From 53bba2afb0d50b557b736eae5efa1d946c47e1e3 Mon Sep 17 00:00:00 2001 From: Patrick Jentsch Date: Wed, 8 Mar 2023 15:22:40 +0100 Subject: [PATCH 10/11] Restructure Contributions with nested blueprints --- app/contributions/__init__.py | 12 + app/contributions/forms.py | 76 +--- app/contributions/routes.py | 363 +----------------- .../spacy_nlp_pipeline_models/__init__.py | 8 + .../spacy_nlp_pipeline_models/forms.py | 44 +++ .../spacy_nlp_pipeline_models/json_routes.py | 58 +++ .../spacy_nlp_pipeline_models/routes.py | 87 +++++ .../tesseract_ocr_pipeline_models/__init__.py | 8 + .../tesseract_ocr_pipeline_models/forms.py | 35 ++ .../json_routes.py | 58 +++ .../tesseract_ocr_pipeline_models/routes.py | 80 ++++ .../contributions/contributions.html.j2 | 5 +- .../create_spacy_nlp_pipeline_model.html.j2 | 1 - .../spacy_nlp_pipeline_model.html.j2 | 1 - .../spacy_nlp_pipeline_models.html.j2 | 1 - ...reate_tesseract_ocr_pipeline_model.html.j2 | 1 - .../tesseract_ocr_pipeline_model.html.j2 | 1 - .../tesseract_ocr_pipeline_models.html.j2 | 1 - 18 files changed, 396 insertions(+), 444 deletions(-) create mode 100644 app/contributions/spacy_nlp_pipeline_models/__init__.py create mode 100644 app/contributions/spacy_nlp_pipeline_models/forms.py create mode 100644 app/contributions/spacy_nlp_pipeline_models/json_routes.py create mode 100644 app/contributions/spacy_nlp_pipeline_models/routes.py create mode 100644 app/contributions/tesseract_ocr_pipeline_models/__init__.py create mode 100644 app/contributions/tesseract_ocr_pipeline_models/forms.py create mode 100644 app/contributions/tesseract_ocr_pipeline_models/json_routes.py create mode 100644 app/contributions/tesseract_ocr_pipeline_models/routes.py rename app/templates/contributions/{ => spacy_nlp_pipeline_models}/create_spacy_nlp_pipeline_model.html.j2 (97%) rename app/templates/contributions/{ => spacy_nlp_pipeline_models}/spacy_nlp_pipeline_model.html.j2 (96%) rename app/templates/contributions/{ => spacy_nlp_pipeline_models}/spacy_nlp_pipeline_models.html.j2 (91%) rename app/templates/contributions/{ => tesseract_ocr_pipeline_models}/create_tesseract_ocr_pipeline_model.html.j2 (98%) rename app/templates/contributions/{ => tesseract_ocr_pipeline_models}/tesseract_ocr_pipeline_model.html.j2 (95%) rename app/templates/contributions/{ => tesseract_ocr_pipeline_models}/tesseract_ocr_pipeline_models.html.j2 (91%) diff --git a/app/contributions/__init__.py b/app/contributions/__init__.py index af9747a6..5175c0ce 100644 --- a/app/contributions/__init__.py +++ b/app/contributions/__init__.py @@ -3,3 +3,15 @@ from flask import Blueprint bp = Blueprint('contributions', __name__) from . import routes + +from .spacy_nlp_pipeline_models import bp as spacy_nlp_pipeline_models_bp +bp.register_blueprint( + spacy_nlp_pipeline_models_bp, + url_prefix='/spacy-nlp-pipeline-models' +) + +from .tesseract_ocr_pipeline_models import bp as tesseract_ocr_pipeline_models_bp +bp.register_blueprint( + tesseract_ocr_pipeline_models_bp, + url_prefix='/tesseract-ocr-pipeline-models' +) diff --git a/app/contributions/forms.py b/app/contributions/forms.py index eb25babb..acec307f 100644 --- a/app/contributions/forms.py +++ b/app/contributions/forms.py @@ -1,16 +1,11 @@ -from flask import current_app from flask_wtf import FlaskForm -from flask_wtf.file import FileField, FileRequired from wtforms import ( - BooleanField, StringField, SubmitField, SelectMultipleField, - IntegerField, - ValidationError + IntegerField ) from wtforms.validators import InputRequired, Length -from app.services import SERVICES class ContributionBaseForm(FlaskForm): @@ -48,74 +43,5 @@ class ContributionBaseForm(FlaskForm): submit = SubmitField() -class CreateTesseractOCRPipelineModelForm(ContributionBaseForm): - tesseract_model_file = FileField( - 'File', - validators=[FileRequired()] - ) - - def validate_tesseract_model_file(self, field): - if not field.data.filename.lower().endswith('.traineddata'): - raise ValidationError('traineddata files only!') - - def __init__(self, *args, **kwargs): - service_manifest = SERVICES['tesseract-ocr-pipeline'] - super().__init__(*args, **kwargs) - self.compatible_service_versions.choices = [('', 'Choose your option')] - self.compatible_service_versions.choices += [ - (x, x) for x in service_manifest['versions'].keys() - ] - self.compatible_service_versions.default = '' - - -class CreateSpaCyNLPPipelineModelForm(ContributionBaseForm): - spacy_model_file = FileField( - 'File', - validators=[FileRequired()] - ) - pipeline_name = StringField( - 'Pipeline name', - validators=[InputRequired(), Length(max=64)] - ) - - def validate_spacy_model_file(self, field): - if not field.data.filename.lower().endswith('.tar.gz'): - raise ValidationError('.tar.gz files only!') - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - service_manifest = SERVICES['spacy-nlp-pipeline'] - self.compatible_service_versions.choices = [('', 'Choose your option')] - self.compatible_service_versions.choices += [ - (x, x) for x in service_manifest['versions'].keys() - ] - self.compatible_service_versions.default = '' - - class EditContributionBaseForm(ContributionBaseForm): pass - -class EditTesseractOCRPipelineModelForm(EditContributionBaseForm): - def __init__(self, *args, **kwargs): - service_manifest = SERVICES['tesseract-ocr-pipeline'] - super().__init__(*args, **kwargs) - self.compatible_service_versions.choices = [('', 'Choose your option')] - self.compatible_service_versions.choices += [ - (x, x) for x in service_manifest['versions'].keys() - ] - self.compatible_service_versions.default = '' - - -class EditSpaCyNLPPipelineModelForm(EditContributionBaseForm): - pipeline_name = StringField( - 'Pipeline name', - validators=[InputRequired(), Length(max=64)] - ) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - service_manifest = SERVICES['spacy-nlp-pipeline'] - self.compatible_service_versions.choices = [('', 'Choose your option')] - self.compatible_service_versions.choices += [ - (x, x) for x in service_manifest['versions'].keys() - ] - self.compatible_service_versions.default = '' diff --git a/app/contributions/routes.py b/app/contributions/routes.py index f9e4d2c5..6d8b9cc3 100644 --- a/app/contributions/routes.py +++ b/app/contributions/routes.py @@ -1,369 +1,12 @@ -from flask import ( - abort, - current_app, - flash, - jsonify, - Markup, - redirect, - render_template, - request, - url_for -) -from flask_login import login_required, current_user -from threading import Thread -from app import db -from app.decorators import content_negotiation, permission_required -from app.models import SpaCyNLPPipelineModel, TesseractOCRPipelineModel +from flask import render_template +from flask_login import login_required from . import bp -from .forms import ( - CreateSpaCyNLPPipelineModelForm, - CreateTesseractOCRPipelineModelForm, - EditSpaCyNLPPipelineModelForm, - EditTesseractOCRPipelineModelForm -) - - -@bp.before_request -@login_required -def before_request(): - pass @bp.route('/') +@login_required def contributions(): return render_template( 'contributions/contributions.html.j2', title='Contributions' ) - - -############################################################################## -# SpaCy NLP Pipeline Models # -############################################################################## -#region spacy-nlp-pipeline-models -@bp.route('/spacy-nlp-pipeline-models') -def spacy_nlp_pipeline_models(): - return render_template( - 'contributions/spacy_nlp_pipeline_models.html.j2', - title='SpaCy NLP Pipeline Models' - ) - - -@bp.route('/spacy-nlp-pipeline-models/create', methods=['GET', 'POST']) -def create_spacy_nlp_pipeline_model(): - form = CreateSpaCyNLPPipelineModelForm(prefix='create-spacy-nlp-pipeline-model-form') - if form.is_submitted(): - if not form.validate(): - response = {'errors': form.errors} - return response, 400 - try: - spacy_nlp_pipeline_model = SpaCyNLPPipelineModel.create( - form.spacy_model_file.data, - compatible_service_versions=form.compatible_service_versions.data, - description=form.description.data, - pipeline_name=form.pipeline_name.data, - publisher=form.publisher.data, - publisher_url=form.publisher_url.data, - publishing_url=form.publishing_url.data, - publishing_year=form.publishing_year.data, - is_public=False, - title=form.title.data, - version=form.version.data, - user=current_user - ) - except OSError: - abort(500) - db.session.commit() - spacy_nlp_pipeline_model_url = url_for( - '.spacy_nlp_pipeline_model', - spacy_nlp_pipeline_model_id=spacy_nlp_pipeline_model.id - ) - message = Markup(f'SpaCy NLP Pipeline model "{spacy_nlp_pipeline_model.title}" created') - flash(message) - return {}, 201, {'Location': spacy_nlp_pipeline_model_url} - return render_template( - 'contributions/create_spacy_nlp_pipeline_model.html.j2', - form=form, - title='Create SpaCy NLP Pipeline Model' - ) - - -@bp.route('/spacy-nlp-pipeline-models/', methods=['GET', 'POST']) -def spacy_nlp_pipeline_model(spacy_nlp_pipeline_model_id): - spacy_nlp_pipeline_model = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id) - form = EditSpaCyNLPPipelineModelForm( - data=spacy_nlp_pipeline_model.to_json_serializeable(), - prefix='edit-spacy-nlp-pipeline-model-form' - ) - if form.validate_on_submit(): - form.populate_obj(spacy_nlp_pipeline_model) - if db.session.is_modified(spacy_nlp_pipeline_model): - message = Markup(f'SpaCy NLP Pipeline model "{spacy_nlp_pipeline_model.title}" updated') - flash(message) - db.session.commit() - return redirect(url_for('.spacy_nlp_pipeline_models')) - return render_template( - 'contributions/spacy_nlp_pipeline_model.html.j2', - form=form, - spacy_nlp_pipeline_model=spacy_nlp_pipeline_model, - title=f'{spacy_nlp_pipeline_model.title} {spacy_nlp_pipeline_model.version}' - ) - - -#region json-routes -@bp.route('/spacy-nlp-pipeline-models/', methods=['DELETE']) -@login_required -@content_negotiation(produces='application/json') -def delete_spacy_model(spacy_nlp_pipeline_model_id): - def _delete_spacy_model(app, spacy_nlp_pipeline_model_id): - with app.app_context(): - snpm = SpaCyNLPPipelineModel.query.get(spacy_nlp_pipeline_model_id) - snpm.delete() - db.session.commit() - - snpm = SpaCyNLPPipelineModel.query.get(spacy_nlp_pipeline_model_id) - if snpm is None: - resonse_data = { - 'message': ( - 'SpaCy NLP Pipeline Model with id' - f' "{spacy_nlp_pipeline_model_id}" not found' - ), - 'category': 'error' - } - response = jsonify(resonse_data) - response.status_code = 404 - return response - if not (snpm.user == current_user or current_user.is_administrator()): - resonse_data = { - 'message': f'You are not allowed to delete "{snpm.title}"', - 'category': 'error' - } - response = jsonify(resonse_data) - response.status_code = 403 - return response - thread = Thread( - target=_delete_spacy_model, - args=(current_app._get_current_object(), snpm.id) - ) - thread.start() - resonse_data = { - 'message': \ - f'SpaCy NLP Pipeline Model "{snpm.title}" marked for deletion' - } - response = jsonify(resonse_data) - response.status_code = 202 - return response - - -@bp.route('/spacy-nlp-pipeline-models//is_public', methods=['PUT']) -@login_required -@permission_required('CONTRIBUTE') -@content_negotiation(consumes='application/json', produces='application/json') -def update_spacy_nlp_pipeline_model_is_public(spacy_nlp_pipeline_model_id): - is_public = request.json - if not isinstance(is_public, bool): - resonse_data = { - 'message': 'Request body must be a boolean', - 'category': 'error' - } - response = jsonify(resonse_data) - response.status_code = 400 - return response - snpm = SpaCyNLPPipelineModel.query.get(spacy_nlp_pipeline_model_id) - if snpm is None: - resonse_data = { - 'message': ( - 'SpaCy NLP Pipeline Model with id' - f' "{spacy_nlp_pipeline_model_id}" not found' - ), - 'category': 'error' - } - response = jsonify(resonse_data) - response.status_code = 404 - return response - if not (snpm.user == current_user or current_user.is_administrator()): - resonse_data = { - 'message': f'You are not allowed to delete "{snpm.title}"', - 'category': 'error' - } - response = jsonify(resonse_data) - response.status_code = 403 - return response - snpm.is_public = is_public - db.session.commit() - response_data = { - 'message': ( - f'SpaCy NLP Pipeline Model "{snpm.title}"' - f' is now {"public" if is_public else "private"}' - ) - } - response = jsonify(response_data) - response.status_code = 200 - return response -#endregion json-routes -#endregion spacy-nlp-pipeline-models - -############################################################################## -# Tesseract OCR Pipeline Models # -############################################################################## -#region tesseract-ocr-pipeline-models -@bp.route('/tesseract-ocr-pipeline-models') -def tesseract_ocr_pipeline_models(): - return render_template( - 'contributions/tesseract_ocr_pipeline_models.html.j2', - title='Tesseract OCR Pipeline Models' - ) - - -@bp.route('/tesseract-ocr-pipeline-models/create', methods=['GET', 'POST']) -def create_tesseract_ocr_pipeline_model(): - form = CreateTesseractOCRPipelineModelForm(prefix='create-tesseract-ocr-pipeline-model-form') - if form.is_submitted(): - if not form.validate(): - response = {'errors': form.errors} - return response, 400 - try: - tesseract_ocr_pipeline_model = TesseractOCRPipelineModel.create( - form.tesseract_model_file.data, - compatible_service_versions=form.compatible_service_versions.data, - description=form.description.data, - publisher=form.publisher.data, - publisher_url=form.publisher_url.data, - publishing_url=form.publishing_url.data, - publishing_year=form.publishing_year.data, - is_public=False, - title=form.title.data, - version=form.version.data, - user=current_user - ) - except OSError: - abort(500) - db.session.commit() - tesseract_ocr_pipeline_model_url = url_for( - '.tesseract_ocr_pipeline_model', - tesseract_ocr_pipeline_model_id=tesseract_ocr_pipeline_model.id - ) - message = Markup(f'Tesseract OCR Pipeline model "{tesseract_ocr_pipeline_model.title}" created') - flash(message) - return {}, 201, {'Location': tesseract_ocr_pipeline_model_url} - return render_template( - 'contributions/create_tesseract_ocr_pipeline_model.html.j2', - form=form, - title='Create Tesseract OCR Pipeline Model' - ) - - -@bp.route('/tesseract-ocr-pipeline-models/', methods=['GET', 'POST']) -def tesseract_ocr_pipeline_model(tesseract_ocr_pipeline_model_id): - tesseract_ocr_pipeline_model = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id) - form = EditTesseractOCRPipelineModelForm( - data=tesseract_ocr_pipeline_model.to_json_serializeable(), - prefix='edit-tesseract-ocr-pipeline-model-form' - ) - if form.validate_on_submit(): - form.populate_obj(tesseract_ocr_pipeline_model) - if db.session.is_modified(tesseract_ocr_pipeline_model): - message = Markup(f'Tesseract OCR Pipeline model "{tesseract_ocr_pipeline_model.title}" updated') - flash(message) - db.session.commit() - return redirect(url_for('.tesseract_ocr_pipeline_models')) - return render_template( - 'contributions/tesseract_ocr_pipeline_model.html.j2', - form=form, - tesseract_ocr_pipeline_model=tesseract_ocr_pipeline_model, - title=f'{tesseract_ocr_pipeline_model.title} {tesseract_ocr_pipeline_model.version}' - ) - - -#region json-routes -@bp.route('/tesseract-ocr-pipeline-models/', methods=['DELETE']) -@login_required -@content_negotiation(produces='application/json') -def delete_tesseract_model(tesseract_ocr_pipeline_model_id): - def _delete_tesseract_ocr_pipeline_model(app, tesseract_ocr_pipeline_model_id): - with app.app_context(): - topm = TesseractOCRPipelineModel.query.get(tesseract_ocr_pipeline_model_id) - topm.delete() - db.session.commit() - - topm = TesseractOCRPipelineModel.query.get(tesseract_ocr_pipeline_model_id) - if topm is None: - resonse_data = { - 'message': ( - 'Tesseract OCR Pipeline Model with id' - f' "{tesseract_ocr_pipeline_model_id}" not found' - ), - 'category': 'error' - } - response = jsonify(resonse_data) - response.status_code = 404 - return response - if not (topm.user == current_user or current_user.is_administrator()): - resonse_data = { - 'message': f'You are not allowed to delete "{topm.title}"', - 'category': 'error' - } - response = jsonify(resonse_data) - response.status_code = 403 - return response - thread = Thread( - target=_delete_tesseract_ocr_pipeline_model, - args=(current_app._get_current_object(), topm.id) - ) - thread.start() - resonse_data = { - 'message': \ - f'Tesseract OCR Pipeline Model "{topm.title}" marked for deletion' - } - response = jsonify(resonse_data) - response.status_code = 202 - return response - - -@bp.route('/tesseract-ocr-pipeline-models//is_public', methods=['PUT']) -@login_required -@permission_required('CONTRIBUTE') -@content_negotiation(consumes='application/json', produces='application/json') -def update_tesseract_ocr_pipeline_model_is_public(tesseract_ocr_pipeline_model_id): - is_public = request.json - if not isinstance(is_public, bool): - resonse_data = { - 'message': 'Request body must be a boolean', - 'category': 'error' - } - response = jsonify(resonse_data) - response.status_code = 400 - return response - topm = TesseractOCRPipelineModel.query.get(tesseract_ocr_pipeline_model_id) - if topm is None: - resonse_data = { - 'message': ( - 'Tesseract OCR Pipeline Model with id' - f' "{tesseract_ocr_pipeline_model_id}" not found' - ), - 'category': 'error' - } - response = jsonify(resonse_data) - response.status_code = 404 - return response - if not (topm.user == current_user or current_user.is_administrator()): - resonse_data = { - 'message': f'You are not allowed to delete "{topm.title}"', - 'category': 'error' - } - response = jsonify(resonse_data) - response.status_code = 403 - return response - topm.is_public = is_public - db.session.commit() - response_data = { - 'message': ( - f'Tesseract OCR Pipeline Model "{topm.title}"' - f' is now {"public" if is_public else "private"}' - ) - } - response = jsonify(response_data) - response.status_code = 200 - return response -#endregion json-routes -#endregion tesseract-ocr-pipeline-models diff --git a/app/contributions/spacy_nlp_pipeline_models/__init__.py b/app/contributions/spacy_nlp_pipeline_models/__init__.py new file mode 100644 index 00000000..6b73681a --- /dev/null +++ b/app/contributions/spacy_nlp_pipeline_models/__init__.py @@ -0,0 +1,8 @@ +from flask import Blueprint + + +TEMPLATE_FOLDER = 'contributions/spacy_nlp_pipeline_models' + + +bp = Blueprint('spacy_nlp_pipeline_models', __name__) +from . import routes, json_routes diff --git a/app/contributions/spacy_nlp_pipeline_models/forms.py b/app/contributions/spacy_nlp_pipeline_models/forms.py new file mode 100644 index 00000000..2670c1d1 --- /dev/null +++ b/app/contributions/spacy_nlp_pipeline_models/forms.py @@ -0,0 +1,44 @@ +from flask_wtf.file import FileField, FileRequired +from wtforms import StringField, ValidationError +from wtforms.validators import InputRequired, Length +from app.services import SERVICES +from ..forms import ContributionBaseForm, EditContributionBaseForm + + +class CreateSpaCyNLPPipelineModelForm(ContributionBaseForm): + spacy_model_file = FileField( + 'File', + validators=[FileRequired()] + ) + pipeline_name = StringField( + 'Pipeline name', + validators=[InputRequired(), Length(max=64)] + ) + + def validate_spacy_model_file(self, field): + if not field.data.filename.lower().endswith('.tar.gz'): + raise ValidationError('.tar.gz files only!') + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + service_manifest = SERVICES['spacy-nlp-pipeline'] + self.compatible_service_versions.choices = [('', 'Choose your option')] + self.compatible_service_versions.choices += [ + (x, x) for x in service_manifest['versions'].keys() + ] + self.compatible_service_versions.default = '' + + +class EditSpaCyNLPPipelineModelForm(EditContributionBaseForm): + pipeline_name = StringField( + 'Pipeline name', + validators=[InputRequired(), Length(max=64)] + ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + service_manifest = SERVICES['spacy-nlp-pipeline'] + self.compatible_service_versions.choices = [('', 'Choose your option')] + self.compatible_service_versions.choices += [ + (x, x) for x in service_manifest['versions'].keys() + ] + self.compatible_service_versions.default = '' diff --git a/app/contributions/spacy_nlp_pipeline_models/json_routes.py b/app/contributions/spacy_nlp_pipeline_models/json_routes.py new file mode 100644 index 00000000..9247f85b --- /dev/null +++ b/app/contributions/spacy_nlp_pipeline_models/json_routes.py @@ -0,0 +1,58 @@ +from flask import abort, current_app, jsonify, request +from flask_login import login_required, current_user +from threading import Thread +from app import db +from app.decorators import content_negotiation, permission_required +from app.models import SpaCyNLPPipelineModel +from . import bp + + +@bp.route('/', methods=['DELETE']) +@login_required +@content_negotiation(produces='application/json') +def delete_spacy_model(spacy_nlp_pipeline_model_id): + def _delete_spacy_model(app, spacy_nlp_pipeline_model_id): + with app.app_context(): + snpm = SpaCyNLPPipelineModel.query.get(spacy_nlp_pipeline_model_id) + snpm.delete() + db.session.commit() + + snpm = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id) + if not (snpm.user == current_user or current_user.is_administrator()): + abort(403) + thread = Thread( + target=_delete_spacy_model, + args=(current_app._get_current_object(), snpm.id) + ) + thread.start() + resonse_data = { + 'message': \ + f'SpaCy NLP Pipeline Model "{snpm.title}" marked for deletion' + } + response = jsonify(resonse_data) + response.status_code = 202 + return response + + +@bp.route('//is_public', methods=['PUT']) +@login_required +@permission_required('CONTRIBUTE') +@content_negotiation(consumes='application/json', produces='application/json') +def update_spacy_nlp_pipeline_model_is_public(spacy_nlp_pipeline_model_id): + is_public = request.json + if not isinstance(is_public, bool): + abort(400) + snpm = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id) + if not (snpm.user == current_user or current_user.is_administrator()): + abort(403) + snpm.is_public = is_public + db.session.commit() + response_data = { + 'message': ( + f'SpaCy NLP Pipeline Model "{snpm.title}"' + f' is now {"public" if is_public else "private"}' + ) + } + response = jsonify(response_data) + response.status_code = 200 + return response diff --git a/app/contributions/spacy_nlp_pipeline_models/routes.py b/app/contributions/spacy_nlp_pipeline_models/routes.py new file mode 100644 index 00000000..2e416c47 --- /dev/null +++ b/app/contributions/spacy_nlp_pipeline_models/routes.py @@ -0,0 +1,87 @@ +from flask import abort, flash, Markup, redirect, render_template, url_for +from flask_login import login_required, current_user +from app import db +from app.models import SpaCyNLPPipelineModel +from . import bp, TEMPLATE_FOLDER +from .forms import ( + CreateSpaCyNLPPipelineModelForm, + EditSpaCyNLPPipelineModelForm +) + + +@bp.route('') +@login_required +def spacy_nlp_pipeline_models(): + return render_template( + f'{TEMPLATE_FOLDER}/spacy_nlp_pipeline_models.html.j2', + title='SpaCy NLP Pipeline Models' + ) + + +@bp.route('/create', methods=['GET', 'POST']) +@login_required +def create_spacy_nlp_pipeline_model(): + form = CreateSpaCyNLPPipelineModelForm(prefix='create-spacy-nlp-pipeline-model-form') + if form.is_submitted(): + if not form.validate(): + response = {'errors': form.errors} + return response, 400 + try: + spacy_nlp_pipeline_model = SpaCyNLPPipelineModel.create( + form.spacy_model_file.data, + compatible_service_versions=form.compatible_service_versions.data, + description=form.description.data, + pipeline_name=form.pipeline_name.data, + publisher=form.publisher.data, + publisher_url=form.publisher_url.data, + publishing_url=form.publishing_url.data, + publishing_year=form.publishing_year.data, + is_public=False, + title=form.title.data, + version=form.version.data, + user=current_user + ) + except OSError: + abort(500) + db.session.commit() + spacy_nlp_pipeline_model_url = url_for( + '.spacy_nlp_pipeline_model', + spacy_nlp_pipeline_model_id=spacy_nlp_pipeline_model.id + ) + message = Markup( + f'SpaCy NLP Pipeline model "{spacy_nlp_pipeline_model.title}" ' + 'created' + ) + flash(message) + return '', 201, {'Location': spacy_nlp_pipeline_model_url} + return render_template( + f'{TEMPLATE_FOLDER}/create_spacy_nlp_pipeline_model.html.j2', + form=form, + title='Create SpaCy NLP Pipeline Model' + ) + + +@bp.route('/', methods=['GET', 'POST']) +@login_required +def spacy_nlp_pipeline_model(spacy_nlp_pipeline_model_id): + spacy_nlp_pipeline_model = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id) + form = EditSpaCyNLPPipelineModelForm( + data=spacy_nlp_pipeline_model.to_json_serializeable(), + prefix='edit-spacy-nlp-pipeline-model-form' + ) + if form.validate_on_submit(): + form.populate_obj(spacy_nlp_pipeline_model) + if db.session.is_modified(spacy_nlp_pipeline_model): + message = Markup( + f'SpaCy NLP Pipeline model "{spacy_nlp_pipeline_model.title}" ' + 'updated' + ) + flash(message) + db.session.commit() + return redirect(url_for('.spacy_nlp_pipeline_models')) + return render_template( + f'{TEMPLATE_FOLDER}/spacy_nlp_pipeline_model.html.j2', + form=form, + spacy_nlp_pipeline_model=spacy_nlp_pipeline_model, + title=f'{spacy_nlp_pipeline_model.title} {spacy_nlp_pipeline_model.version}' + ) diff --git a/app/contributions/tesseract_ocr_pipeline_models/__init__.py b/app/contributions/tesseract_ocr_pipeline_models/__init__.py new file mode 100644 index 00000000..c60e0915 --- /dev/null +++ b/app/contributions/tesseract_ocr_pipeline_models/__init__.py @@ -0,0 +1,8 @@ +from flask import Blueprint + + +TEMPLATE_FOLDER = 'contributions/tesseract_ocr_pipeline_models' + + +bp = Blueprint('tesseract_ocr_pipeline_models', __name__) +from . import routes, json_routes diff --git a/app/contributions/tesseract_ocr_pipeline_models/forms.py b/app/contributions/tesseract_ocr_pipeline_models/forms.py new file mode 100644 index 00000000..51f0d76c --- /dev/null +++ b/app/contributions/tesseract_ocr_pipeline_models/forms.py @@ -0,0 +1,35 @@ +from flask_wtf.file import FileField, FileRequired +from wtforms import ValidationError +from app.services import SERVICES +from ..forms import ContributionBaseForm, EditContributionBaseForm + + +class CreateTesseractOCRPipelineModelForm(ContributionBaseForm): + tesseract_model_file = FileField( + 'File', + validators=[FileRequired()] + ) + + def validate_tesseract_model_file(self, field): + if not field.data.filename.lower().endswith('.traineddata'): + raise ValidationError('traineddata files only!') + + def __init__(self, *args, **kwargs): + service_manifest = SERVICES['tesseract-ocr-pipeline'] + super().__init__(*args, **kwargs) + self.compatible_service_versions.choices = [('', 'Choose your option')] + self.compatible_service_versions.choices += [ + (x, x) for x in service_manifest['versions'].keys() + ] + self.compatible_service_versions.default = '' + + +class EditTesseractOCRPipelineModelForm(EditContributionBaseForm): + def __init__(self, *args, **kwargs): + service_manifest = SERVICES['tesseract-ocr-pipeline'] + super().__init__(*args, **kwargs) + self.compatible_service_versions.choices = [('', 'Choose your option')] + self.compatible_service_versions.choices += [ + (x, x) for x in service_manifest['versions'].keys() + ] + self.compatible_service_versions.default = '' diff --git a/app/contributions/tesseract_ocr_pipeline_models/json_routes.py b/app/contributions/tesseract_ocr_pipeline_models/json_routes.py new file mode 100644 index 00000000..f90a971f --- /dev/null +++ b/app/contributions/tesseract_ocr_pipeline_models/json_routes.py @@ -0,0 +1,58 @@ +from flask import abort, current_app, jsonify, request +from flask_login import login_required, current_user +from threading import Thread +from app import db +from app.decorators import content_negotiation, permission_required +from app.models import TesseractOCRPipelineModel +from . import bp + + +@bp.route('/', methods=['DELETE']) +@login_required +@content_negotiation(produces='application/json') +def delete_tesseract_model(tesseract_ocr_pipeline_model_id): + def _delete_tesseract_ocr_pipeline_model(app, tesseract_ocr_pipeline_model_id): + with app.app_context(): + topm = TesseractOCRPipelineModel.query.get(tesseract_ocr_pipeline_model_id) + topm.delete() + db.session.commit() + + topm = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id) + if not (topm.user == current_user or current_user.is_administrator()): + abort(403) + thread = Thread( + target=_delete_tesseract_ocr_pipeline_model, + args=(current_app._get_current_object(), topm.id) + ) + thread.start() + resonse_data = { + 'message': \ + f'Tesseract OCR Pipeline Model "{topm.title}" marked for deletion' + } + response = jsonify(resonse_data) + response.status_code = 202 + return response + + +@bp.route('//is_public', methods=['PUT']) +@login_required +@permission_required('CONTRIBUTE') +@content_negotiation(consumes='application/json', produces='application/json') +def update_tesseract_ocr_pipeline_model_is_public(tesseract_ocr_pipeline_model_id): + is_public = request.json + if not isinstance(is_public, bool): + abort(400) + topm = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id) + if not (topm.user == current_user or current_user.is_administrator()): + abort(403) + topm.is_public = is_public + db.session.commit() + response_data = { + 'message': ( + f'Tesseract OCR Pipeline Model "{topm.title}"' + f' is now {"public" if is_public else "private"}' + ) + } + response = jsonify(response_data) + response.status_code = 200 + return response diff --git a/app/contributions/tesseract_ocr_pipeline_models/routes.py b/app/contributions/tesseract_ocr_pipeline_models/routes.py new file mode 100644 index 00000000..823e54d9 --- /dev/null +++ b/app/contributions/tesseract_ocr_pipeline_models/routes.py @@ -0,0 +1,80 @@ +from flask import abort, flash, Markup, redirect, render_template, url_for +from flask_login import login_required, current_user +from app import db +from app.models import TesseractOCRPipelineModel +from . import bp, TEMPLATE_FOLDER +from .forms import ( + CreateTesseractOCRPipelineModelForm, + EditTesseractOCRPipelineModelForm +) + + +@bp.route('') +@login_required +def tesseract_ocr_pipeline_models(): + return render_template( + f'{TEMPLATE_FOLDER}/tesseract_ocr_pipeline_models.html.j2', + title='Tesseract OCR Pipeline Models' + ) + + +@bp.route('/create', methods=['GET', 'POST']) +@login_required +def create_tesseract_ocr_pipeline_model(): + form = CreateTesseractOCRPipelineModelForm(prefix='create-tesseract-ocr-pipeline-model-form') + if form.is_submitted(): + if not form.validate(): + response = {'errors': form.errors} + return response, 400 + try: + tesseract_ocr_pipeline_model = TesseractOCRPipelineModel.create( + form.tesseract_model_file.data, + compatible_service_versions=form.compatible_service_versions.data, + description=form.description.data, + publisher=form.publisher.data, + publisher_url=form.publisher_url.data, + publishing_url=form.publishing_url.data, + publishing_year=form.publishing_year.data, + is_public=False, + title=form.title.data, + version=form.version.data, + user=current_user + ) + except OSError: + abort(500) + db.session.commit() + tesseract_ocr_pipeline_model_url = url_for( + '.tesseract_ocr_pipeline_model', + tesseract_ocr_pipeline_model_id=tesseract_ocr_pipeline_model.id + ) + message = Markup(f'Tesseract OCR Pipeline model "{tesseract_ocr_pipeline_model.title}" created') + flash(message) + return {}, 201, {'Location': tesseract_ocr_pipeline_model_url} + return render_template( + f'{TEMPLATE_FOLDER}/create_tesseract_ocr_pipeline_model.html.j2', + form=form, + title='Create Tesseract OCR Pipeline Model' + ) + + +@bp.route('/', methods=['GET', 'POST']) +@login_required +def tesseract_ocr_pipeline_model(tesseract_ocr_pipeline_model_id): + tesseract_ocr_pipeline_model = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id) + form = EditTesseractOCRPipelineModelForm( + data=tesseract_ocr_pipeline_model.to_json_serializeable(), + prefix='edit-tesseract-ocr-pipeline-model-form' + ) + if form.validate_on_submit(): + form.populate_obj(tesseract_ocr_pipeline_model) + if db.session.is_modified(tesseract_ocr_pipeline_model): + message = Markup(f'Tesseract OCR Pipeline model "{tesseract_ocr_pipeline_model.title}" updated') + flash(message) + db.session.commit() + return redirect(url_for('.tesseract_ocr_pipeline_models')) + return render_template( + f'{TEMPLATE_FOLDER}/tesseract_ocr_pipeline_model.html.j2', + form=form, + tesseract_ocr_pipeline_model=tesseract_ocr_pipeline_model, + title=f'{tesseract_ocr_pipeline_model.title} {tesseract_ocr_pipeline_model.version}' + ) diff --git a/app/templates/contributions/contributions.html.j2 b/app/templates/contributions/contributions.html.j2 index bdbcb10c..99147b56 100644 --- a/app/templates/contributions/contributions.html.j2 +++ b/app/templates/contributions/contributions.html.j2 @@ -1,6 +1,5 @@ {% extends "base.html.j2" %} {% import "materialize/wtf.html.j2" as wtf %} -{% from "contributions/_breadcrumbs.html.j2" import breadcrumbs with context %} {% block page_content %}
@@ -11,7 +10,7 @@
- +
Tesseract OCR Pipeline Models

Here you can see and edit the models that you have created. You can also create new models.

@@ -21,7 +20,7 @@
- +
SpaCy NLP Pipeline Models

Here you can see and edit the models that you have created. You can also create new models.

diff --git a/app/templates/contributions/create_spacy_nlp_pipeline_model.html.j2 b/app/templates/contributions/spacy_nlp_pipeline_models/create_spacy_nlp_pipeline_model.html.j2 similarity index 97% rename from app/templates/contributions/create_spacy_nlp_pipeline_model.html.j2 rename to app/templates/contributions/spacy_nlp_pipeline_models/create_spacy_nlp_pipeline_model.html.j2 index e17ac9e5..091c61ad 100644 --- a/app/templates/contributions/create_spacy_nlp_pipeline_model.html.j2 +++ b/app/templates/contributions/spacy_nlp_pipeline_models/create_spacy_nlp_pipeline_model.html.j2 @@ -1,6 +1,5 @@ {% extends "base.html.j2" %} {% import "materialize/wtf.html.j2" as wtf %} -{% from "contributions/_breadcrumbs.html.j2" import breadcrumbs with context %} {% block main_attribs %} class="service-scheme" data-service="spacy-nlp-pipeline"{% endblock main_attribs %} diff --git a/app/templates/contributions/spacy_nlp_pipeline_model.html.j2 b/app/templates/contributions/spacy_nlp_pipeline_models/spacy_nlp_pipeline_model.html.j2 similarity index 96% rename from app/templates/contributions/spacy_nlp_pipeline_model.html.j2 rename to app/templates/contributions/spacy_nlp_pipeline_models/spacy_nlp_pipeline_model.html.j2 index 32f27303..467effc9 100644 --- a/app/templates/contributions/spacy_nlp_pipeline_model.html.j2 +++ b/app/templates/contributions/spacy_nlp_pipeline_models/spacy_nlp_pipeline_model.html.j2 @@ -1,6 +1,5 @@ {% extends "base.html.j2" %} {% import "materialize/wtf.html.j2" as wtf %} -{% from "contributions/_breadcrumbs.html.j2" import breadcrumbs with context %} {% block main_attribs %} class="service-scheme" data-service="spacy-nlp-pipeline"{% endblock main_attribs %} diff --git a/app/templates/contributions/spacy_nlp_pipeline_models.html.j2 b/app/templates/contributions/spacy_nlp_pipeline_models/spacy_nlp_pipeline_models.html.j2 similarity index 91% rename from app/templates/contributions/spacy_nlp_pipeline_models.html.j2 rename to app/templates/contributions/spacy_nlp_pipeline_models/spacy_nlp_pipeline_models.html.j2 index f3c7a40e..b57507be 100644 --- a/app/templates/contributions/spacy_nlp_pipeline_models.html.j2 +++ b/app/templates/contributions/spacy_nlp_pipeline_models/spacy_nlp_pipeline_models.html.j2 @@ -1,6 +1,5 @@ {% extends "base.html.j2" %} {% import "materialize/wtf.html.j2" as wtf %} -{% from "contributions/_breadcrumbs.html.j2" import breadcrumbs with context %} {% block page_content %}
diff --git a/app/templates/contributions/create_tesseract_ocr_pipeline_model.html.j2 b/app/templates/contributions/tesseract_ocr_pipeline_models/create_tesseract_ocr_pipeline_model.html.j2 similarity index 98% rename from app/templates/contributions/create_tesseract_ocr_pipeline_model.html.j2 rename to app/templates/contributions/tesseract_ocr_pipeline_models/create_tesseract_ocr_pipeline_model.html.j2 index ecede20a..43ad0a13 100644 --- a/app/templates/contributions/create_tesseract_ocr_pipeline_model.html.j2 +++ b/app/templates/contributions/tesseract_ocr_pipeline_models/create_tesseract_ocr_pipeline_model.html.j2 @@ -1,6 +1,5 @@ {% extends "base.html.j2" %} {% import "materialize/wtf.html.j2" as wtf %} -{% from "contributions/_breadcrumbs.html.j2" import breadcrumbs with context %} {% block main_attribs %} class="service-scheme" data-service="tesseract-ocr-pipeline"{% endblock main_attribs %} diff --git a/app/templates/contributions/tesseract_ocr_pipeline_model.html.j2 b/app/templates/contributions/tesseract_ocr_pipeline_models/tesseract_ocr_pipeline_model.html.j2 similarity index 95% rename from app/templates/contributions/tesseract_ocr_pipeline_model.html.j2 rename to app/templates/contributions/tesseract_ocr_pipeline_models/tesseract_ocr_pipeline_model.html.j2 index 02322d8a..c7cc2d5c 100644 --- a/app/templates/contributions/tesseract_ocr_pipeline_model.html.j2 +++ b/app/templates/contributions/tesseract_ocr_pipeline_models/tesseract_ocr_pipeline_model.html.j2 @@ -1,6 +1,5 @@ {% extends "base.html.j2" %} {% import "materialize/wtf.html.j2" as wtf %} -{% from "contributions/_breadcrumbs.html.j2" import breadcrumbs with context %} {% block main_attribs %} class="service-scheme" data-service="tesseract-ocr-pipeline"{% endblock main_attribs %} diff --git a/app/templates/contributions/tesseract_ocr_pipeline_models.html.j2 b/app/templates/contributions/tesseract_ocr_pipeline_models/tesseract_ocr_pipeline_models.html.j2 similarity index 91% rename from app/templates/contributions/tesseract_ocr_pipeline_models.html.j2 rename to app/templates/contributions/tesseract_ocr_pipeline_models/tesseract_ocr_pipeline_models.html.j2 index 3d43d727..a2f18c2a 100644 --- a/app/templates/contributions/tesseract_ocr_pipeline_models.html.j2 +++ b/app/templates/contributions/tesseract_ocr_pipeline_models/tesseract_ocr_pipeline_models.html.j2 @@ -1,6 +1,5 @@ {% extends "base.html.j2" %} {% import "materialize/wtf.html.j2" as wtf %} -{% from "contributions/_breadcrumbs.html.j2" import breadcrumbs with context %} {% block page_content %}
From fdad10991ce0ae58ed3c3cd7d3f252d5723bb24c Mon Sep 17 00:00:00 2001 From: Patrick Jentsch Date: Thu, 9 Mar 2023 12:07:16 +0100 Subject: [PATCH 11/11] More restructuring --- app/corpora/routes.py | 326 +++++++++++------- app/static/js/Requests/Contributions.js | 51 --- app/static/js/Requests/Corpora.js | 0 .../Requests/contributions/contributions.js | 5 + .../spacy_nlp_pipeline_models.js | 26 ++ .../tesseract_ocr_pipeline_models.js | 26 ++ app/static/js/Requests/corpora/corpora.js | 78 +++++ .../SpacyNLPPipelineModelList.js | 34 +- .../TesseractOCRPipelineModelList.js | 34 +- app/templates/_scripts.html.j2 | 5 +- .../contributions/contributions.html.j2 | 2 +- app/templates/corpora/corpus.html.j2 | 23 +- 12 files changed, 417 insertions(+), 193 deletions(-) delete mode 100644 app/static/js/Requests/Contributions.js create mode 100644 app/static/js/Requests/Corpora.js create mode 100644 app/static/js/Requests/contributions/contributions.js create mode 100644 app/static/js/Requests/contributions/spacy_nlp_pipeline_models.js create mode 100644 app/static/js/Requests/contributions/tesseract_ocr_pipeline_models.js create mode 100644 app/static/js/Requests/corpora/corpora.js diff --git a/app/corpora/routes.py b/app/corpora/routes.py index 5d1b1a41..8a3e5e66 100644 --- a/app/corpora/routes.py +++ b/app/corpora/routes.py @@ -16,6 +16,7 @@ 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 app.models import ( Corpus, CorpusFile, @@ -32,93 +33,6 @@ from .forms import ( ) -@bp.route('//is_public', methods=['POST']) -@login_required -@corpus_owner_or_admin_required -def update_corpus_is_public(corpus_id): - is_public = request.json - if not isinstance(is_public, bool): - response = jsonify('The request body must be a boolean') - response.status_code = 400 - abort(response) - corpus = Corpus.query.get_or_404(corpus_id) - corpus.is_public = is_public - db.session.commit() - return '', 204 - - -@bp.route('//followers/add', methods=['POST']) -@login_required -@corpus_owner_or_admin_required -def add_corpus_followers(corpus_id): - usernames = request.json - if not (isinstance(usernames, list) or all(isinstance(u, str) for u in usernames)): - response = jsonify('The request body must be a list of strings') - response.status_code = 400 - abort(response) - 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() - return '', 204 - - -@bp.route('//follow/') -@login_required -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') - return redirect(url_for('corpora.corpus', corpus_id=corpus_id)) - abort(403) - - -@bp.route('//followers//unfollow', methods=['POST']) -@login_required -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() - flash(f'{follower.username} is not following {corpus.title} anymore', category='corpus') - return '', 204 - - -@bp.route('//followers//role', methods=['POST']) -@corpus_follower_permission_required('UPDATE_FOLLOWER') -def add_permission(corpus_id, follower_id): - corpus_follower_association = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=follower_id).first_or_404() - if not (corpus_follower_association.corpus.user == current_user or current_user.is_administrator()): - abort(403) - role_name = request.json.get('role') - if role_name is None: - abort(400) - corpus_follower_role = CorpusFollowerRole.query.filter_by(name=role_name).first_or_404() - corpus_follower_association.role = corpus_follower_role - db.session.commit() - return '', 204 - - -@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' - ) - - @bp.route('/create', methods=['GET', 'POST']) @login_required def create_corpus(): @@ -145,6 +59,24 @@ 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): @@ -172,6 +104,18 @@ def corpus(corpus_id): abort(403) +@bp.route('//analyse') +@login_required +@corpus_follower_permission_required('VIEW') +def analyse_corpus(corpus_id): + corpus = Corpus.query.get_or_404(corpus_id) + return render_template( + 'corpora/analyse_corpus.html.j2', + corpus=corpus, + title=f'Analyse Corpus {corpus.title}' + ) + + @bp.route('//generate-corpus-share-link', methods=['GET', 'POST']) @login_required @corpus_follower_permission_required('GENERATE_SHARE_LINK') @@ -186,9 +130,34 @@ def generate_corpus_share_link(corpus_id): return link +@bp.route('//follow/') +@login_required +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') + return redirect(url_for('corpora.corpus', corpus_id=corpus_id)) + abort(403) + + +@bp.route('/import', methods=['GET', 'POST']) +@login_required +def import_corpus(): + abort(503) + + +@bp.route('//export') +@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(): @@ -199,27 +168,22 @@ def delete_corpus(corpus_id): corpus = Corpus.query.get_or_404(corpus_id) thread = Thread( target=_delete_corpus, - args=(current_app._get_current_object(), corpus_id) + args=(current_app._get_current_object(), corpus.id) ) thread.start() - return {}, 202 - - -@bp.route('//analyse') -@login_required -@corpus_follower_permission_required('VIEW') -def analyse_corpus(corpus_id): - corpus = Corpus.query.get_or_404(corpus_id) - return render_template( - 'corpora/analyse_corpus.html.j2', - corpus=corpus, - title=f'Analyse Corpus {corpus.title}' - ) + 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(): @@ -230,18 +194,51 @@ def build_corpus(corpus_id): corpus = Corpus.query.get_or_404(corpus_id) if not (corpus.user == current_user or current_user.is_administrator()): abort(403) - # Check if the corpus has corpus files - if not corpus.files.all(): - response = {'errors': {'message': 'Corpus file(s) required'}} - return response, 409 + if len(corpus.files.all()) == 0: + abort(409) thread = Thread( target=_build_corpus, args=(current_app._get_current_object(), corpus_id) ) thread.start() - return {}, 202 + response_data = { + 'message': f'Corpus "{corpus.title}" marked for building', + 'category': 'corpus' + } + response = jsonify(response_data) + response.status_code = 202 + 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') @@ -292,7 +289,7 @@ def create_corpus_file(corpus_id): @bp.route('//files/', methods=['GET', 'POST']) @login_required -@corpus_follower_permission_required('ADD_CORPUS_FILE', 'UPDATE_CORPUS_FILE', 'REMOVE_CORPUS_FILE') +@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()) @@ -313,27 +310,6 @@ def corpus_file(corpus_id, corpus_file_id): ) -@bp.route('//files/', methods=['DELETE']) -@login_required -@corpus_follower_permission_required('REMOVE_CORPUS_FILE') -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() - if not (corpus_file.corpus.user == current_user or current_user.is_administrator()): - abort(403) - thread = Thread( - target=_delete_corpus_file, - args=(current_app._get_current_object(), corpus_file_id) - ) - thread.start() - return {}, 202 - - @bp.route('//files//download') @login_required @corpus_follower_permission_required('VIEW') @@ -350,13 +326,95 @@ def download_corpus_file(corpus_id, corpus_file_id): ) -@bp.route('/import', methods=['GET', 'POST']) +#region json-routes +@bp.route('//files/', methods=['DELETE']) @login_required -def import_corpus(): - abort(503) +@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('//export') +@bp.route('//followers/', methods=['DELETE']) @login_required -def export_corpus(corpus_id): - abort(503) +@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_or_404() + 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/static/js/Requests/Contributions.js b/app/static/js/Requests/Contributions.js deleted file mode 100644 index 30605135..00000000 --- a/app/static/js/Requests/Contributions.js +++ /dev/null @@ -1,51 +0,0 @@ -/***************************************************************************** -* Contributions * -* Fetch requests for /contributions routes * -*****************************************************************************/ -Requests.contributions = {}; - -Requests.contributions.spacy_nlp_pipeline_models = {}; - -Requests.contributions.spacy_nlp_pipeline_models.ent = {}; - -Requests.contributions.spacy_nlp_pipeline_models.ent.delete = (spacyNlpPipelineModelId) => { - let input = `/contributions/spacy-nlp-pipeline-models/${spacyNlpPipelineModelId}`; - let init = { - method: 'DELETE' - }; - return Requests.JSONfetch(input, init); -}; - -Requests.contributions.spacy_nlp_pipeline_models.ent.isPublic = {}; - -Requests.contributions.spacy_nlp_pipeline_models.ent.isPublic.update = (spacyNlpPipelineModelId, value) => { - let input = `/contributions/spacy-nlp-pipeline-models/${spacyNlpPipelineModelId}/is_public`; - let init = { - method: 'PUT', - body: JSON.stringify(value) - }; - return Requests.JSONfetch(input, init); -}; - -Requests.contributions.tesseract_ocr_pipeline_models = {}; - -Requests.contributions.tesseract_ocr_pipeline_models.ent = {}; - -Requests.contributions.tesseract_ocr_pipeline_models.ent.delete = (tesseractOcrPipelineModelId) => { - let input = `/contributions/tesseract-ocr-pipeline-models/${tesseractOcrPipelineModelId}`; - let init = { - method: 'DELETE' - }; - return Requests.JSONfetch(input, init); -}; - -Requests.contributions.tesseract_ocr_pipeline_models.ent.isPublic = {}; - -Requests.contributions.tesseract_ocr_pipeline_models.ent.isPublic.update = (tesseractOcrPipelineModelId, value) => { - let input = `/contributions/tesseract-ocr-pipeline-models/${tesseractOcrPipelineModelId}/is_public`; - let init = { - method: 'PUT', - body: JSON.stringify(value) - }; - return Requests.JSONfetch(input, init); -}; diff --git a/app/static/js/Requests/Corpora.js b/app/static/js/Requests/Corpora.js new file mode 100644 index 00000000..e69de29b diff --git a/app/static/js/Requests/contributions/contributions.js b/app/static/js/Requests/contributions/contributions.js new file mode 100644 index 00000000..2d9cf26a --- /dev/null +++ b/app/static/js/Requests/contributions/contributions.js @@ -0,0 +1,5 @@ +/***************************************************************************** +* Contributions * +* Fetch requests for /contributions routes * +*****************************************************************************/ +Requests.contributions = {}; diff --git a/app/static/js/Requests/contributions/spacy_nlp_pipeline_models.js b/app/static/js/Requests/contributions/spacy_nlp_pipeline_models.js new file mode 100644 index 00000000..e1422c1e --- /dev/null +++ b/app/static/js/Requests/contributions/spacy_nlp_pipeline_models.js @@ -0,0 +1,26 @@ +/***************************************************************************** +* SpaCy NLP Pipeline Models * +* Fetch requests for /contributions/spacy-nlp-pipeline-models routes * +*****************************************************************************/ +Requests.contributions.spacy_nlp_pipeline_models = {}; + +Requests.contributions.spacy_nlp_pipeline_models.entity = {}; + +Requests.contributions.spacy_nlp_pipeline_models.entity.delete = (spacyNlpPipelineModelId) => { + let input = `/contributions/spacy-nlp-pipeline-models/${spacyNlpPipelineModelId}`; + let init = { + method: 'DELETE' + }; + return Requests.JSONfetch(input, init); +}; + +Requests.contributions.spacy_nlp_pipeline_models.entity.isPublic = {}; + +Requests.contributions.spacy_nlp_pipeline_models.entity.isPublic.update = (spacyNlpPipelineModelId, value) => { + let input = `/contributions/spacy-nlp-pipeline-models/${spacyNlpPipelineModelId}/is_public`; + let init = { + method: 'PUT', + body: JSON.stringify(value) + }; + return Requests.JSONfetch(input, init); +}; diff --git a/app/static/js/Requests/contributions/tesseract_ocr_pipeline_models.js b/app/static/js/Requests/contributions/tesseract_ocr_pipeline_models.js new file mode 100644 index 00000000..13feb42a --- /dev/null +++ b/app/static/js/Requests/contributions/tesseract_ocr_pipeline_models.js @@ -0,0 +1,26 @@ +/***************************************************************************** +* Tesseract OCR Pipeline Models * +* Fetch requests for /contributions/tesseract-ocr-pipeline-models routes * +*****************************************************************************/ +Requests.contributions.tesseract_ocr_pipeline_models = {}; + +Requests.contributions.tesseract_ocr_pipeline_models.entity = {}; + +Requests.contributions.tesseract_ocr_pipeline_models.entity.delete = (tesseractOcrPipelineModelId) => { + let input = `/contributions/tesseract-ocr-pipeline-models/${tesseractOcrPipelineModelId}`; + let init = { + method: 'DELETE' + }; + return Requests.JSONfetch(input, init); +}; + +Requests.contributions.tesseract_ocr_pipeline_models.entity.isPublic = {}; + +Requests.contributions.tesseract_ocr_pipeline_models.entity.isPublic.update = (tesseractOcrPipelineModelId, value) => { + let input = `/contributions/tesseract-ocr-pipeline-models/${tesseractOcrPipelineModelId}/is_public`; + let init = { + method: 'PUT', + body: JSON.stringify(value) + }; + return Requests.JSONfetch(input, init); +}; diff --git a/app/static/js/Requests/corpora/corpora.js b/app/static/js/Requests/corpora/corpora.js new file mode 100644 index 00000000..051fb07f --- /dev/null +++ b/app/static/js/Requests/corpora/corpora.js @@ -0,0 +1,78 @@ +/***************************************************************************** +* Corpora * +* Fetch requests for /corpora routes * +*****************************************************************************/ +Requests.corpora = {}; + +Requests.corpora.ent = {}; + +Requests.corpora.ent.delete = (corpusId) => { + let input = `/corpora/${corpusId}`; + let init = { + method: 'DELETE' + }; + return Requests.JSONfetch(input, init); +}; + +Requests.corpora.ent.build = (corpusId) => { + let input = `/corpora/${corpusId}/build`; + let init = { + method: 'POST', + }; + return Requests.JSONfetch(input, init); +}; + +Requests.corpora.ent.isPublic = {}; + +Requests.corpora.ent.isPublic.update = (corpusId, value) => { + let input = `/corpora/${corpusId}/is_public`; + let init = { + method: 'PUT', + body: JSON.stringify(value) + }; + return Requests.JSONfetch(input, init); +}; + +Requests.corpora.ent.files = {}; + +Requests.corpora.ent.files.ent = {}; + +Requests.corpora.ent.files.ent.delete = (corpusId, corpusFileId) => { + let input = `/corpora/${corpusId}/files/${corpusFileId}`; + let init = { + method: 'DELETE', + }; + return Requests.JSONfetch(input, init); +}; + +Requests.corpora.ent.followers = {}; + +Requests.corpora.ent.followers.add = (corpusId, usernames) => { + let input = `/corpora/${corpusId}/followers`; + let init = { + method: 'POST', + body: JSON.stringify(usernames) + }; + return Requests.JSONfetch(input, init); +}; + +Requests.corpora.ent.followers.ent = {}; + +Requests.corpora.ent.followers.ent.delete = (corpusId, followerId) => { + let input = `/corpora/${corpusId}/followers/${followerId}`; + let init = { + method: 'DELETE', + }; + return Requests.JSONfetch(input, init); +}; + +Requests.corpora.ent.followers.ent.role = {}; + +Requests.corpora.ent.followers.ent.role.update = (corpusId, followerId, value) => { + let input = `/corpora/${corpusId}/followers/${followerId}/role`; + let init = { + method: 'PUT', + body: JSON.stringify(value) + }; + return Requests.JSONfetch(input, init); +}; diff --git a/app/static/js/ResourceLists/SpacyNLPPipelineModelList.js b/app/static/js/ResourceLists/SpacyNLPPipelineModelList.js index 5997fb0c..46d3739d 100644 --- a/app/static/js/ResourceLists/SpacyNLPPipelineModelList.js +++ b/app/static/js/ResourceLists/SpacyNLPPipelineModelList.js @@ -120,7 +120,7 @@ class SpaCyNLPPipelineModelList extends ResourceList { switch (listAction) { case 'toggle-is-public': { let newIsPublicValue = listActionElement.checked; - Requests.contributions.spacy_nlp_pipeline_models.ent.isPublic.update(itemId, newIsPublicValue) + Requests.contributions.spacy_nlp_pipeline_models.entity.isPublic.update(itemId, newIsPublicValue) .catch((response) => { listActionElement.checked = !newIsPublicValue; }); @@ -141,7 +141,37 @@ class SpaCyNLPPipelineModelList extends ResourceList { let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction; switch (listAction) { case 'delete-request': { - Requests.contributions.spacy_nlp_pipeline_models.ent.delete(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.contributions.spacy_nlp_pipeline_models.entity.delete(itemId); + }); + modal.open(); break; } case 'view': { diff --git a/app/static/js/ResourceLists/TesseractOCRPipelineModelList.js b/app/static/js/ResourceLists/TesseractOCRPipelineModelList.js index 29d48dbb..9d632b29 100644 --- a/app/static/js/ResourceLists/TesseractOCRPipelineModelList.js +++ b/app/static/js/ResourceLists/TesseractOCRPipelineModelList.js @@ -129,7 +129,7 @@ class TesseractOCRPipelineModelList extends ResourceList { switch (listAction) { case 'toggle-is-public': { let newIsPublicValue = listActionElement.checked; - Requests.contributions.tesseract_ocr_pipeline_models.ent.isPublic.update(itemId, newIsPublicValue) + Requests.contributions.tesseract_ocr_pipeline_models.entity.isPublic.update(itemId, newIsPublicValue) .catch((response) => { listActionElement.checked = !newIsPublicValue; }); @@ -155,7 +155,37 @@ class TesseractOCRPipelineModelList extends ResourceList { let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction; switch (listAction) { case 'delete-request': { - Requests.contributions.tesseract_ocr_pipeline_models.ent.delete(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.contributions.tesseract_ocr_pipeline_models.entity.delete(itemId); + }); + modal.open(); break; } case 'view': { diff --git a/app/templates/_scripts.html.j2 b/app/templates/_scripts.html.j2 index a97f1d97..6fef8d1a 100644 --- a/app/templates/_scripts.html.j2 +++ b/app/templates/_scripts.html.j2 @@ -58,7 +58,10 @@ filters='rjsmin', output='gen/Requests.%(version)s.js', 'js/Requests/Requests.js', - 'js/Requests/Contributions.js' + 'js/Requests/contributions/contributions.js', + 'js/Requests/contributions/spacy_nlp_pipeline_models.js', + 'js/Requests/contributions/tesseract_ocr_pipeline_models.js', + 'js/Requests/Corpora.js' %} {%- endassets %} diff --git a/app/templates/contributions/contributions.html.j2 b/app/templates/contributions/contributions.html.j2 index 99147b56..efefcbbb 100644 --- a/app/templates/contributions/contributions.html.j2 +++ b/app/templates/contributions/contributions.html.j2 @@ -20,7 +20,7 @@
- +
SpaCy NLP Pipeline Models

Here you can see and edit the models that you have created. You can also create new models.

diff --git a/app/templates/corpora/corpus.html.j2 b/app/templates/corpora/corpus.html.j2 index 172dac90..3174713f 100644 --- a/app/templates/corpora/corpus.html.j2 +++ b/app/templates/corpora/corpus.html.j2 @@ -68,7 +68,7 @@ publishPublishing
Social @@ -131,6 +131,17 @@
+ +