From c91004d6ba423d00b1bac3b41521667ee6259c6b Mon Sep 17 00:00:00 2001 From: Patrick Jentsch Date: Tue, 14 Mar 2023 11:13:35 +0100 Subject: [PATCH] Implement flask-breadcrumbs everywhere --- app/__init__.py | 4 +- app/auth/routes.py | 6 +- app/contributions/__init__.py | 4 +- app/contributions/forms.py | 81 +------- app/contributions/json_routes.py | 107 ----------- app/contributions/routes.py | 181 +----------------- .../spacy_nlp_pipeline_models/__init__.py | 2 + .../spacy_nlp_pipeline_models/forms.py | 44 +++++ .../spacy_nlp_pipeline_models/json_routes.py | 54 ++++++ .../spacy_nlp_pipeline_models/routes.py | 83 ++++++++ .../spacy_nlp_pipeline_models/utils.py | 13 ++ .../tesseract_ocr_pipeline_models/__init__.py | 2 + .../tesseract_ocr_pipeline_models/forms.py | 35 ++++ .../json_routes.py | 54 ++++++ .../tesseract_ocr_pipeline_models/routes.py | 82 ++++++++ .../tesseract_ocr_pipeline_models/utils.py | 13 ++ app/corpora/__init__.py | 10 +- app/corpora/files/__init__.py | 10 +- app/corpora/files/json_routes.py | 10 +- app/corpora/files/routes.py | 36 ++-- app/corpora/files/utils.py | 15 ++ app/corpora/followers/__init__.py | 5 +- app/corpora/followers/json_routes.py | 14 +- app/corpora/routes.py | 15 +- app/corpora/utils.py | 17 ++ app/jobs/routes.py | 20 +- app/jobs/utils.py | 13 ++ app/main/routes.py | 2 +- app/services/routes.py | 10 +- app/templates/_roadmap.html.j2 | 2 +- .../contributions/contributions.html.j2 | 6 +- .../create_spacy_nlp_pipeline_model.html.j2 | 0 .../spacy_nlp_pipeline_model.html.j2 | 0 .../spacy_nlp_pipeline_models.html.j2 | 0 ...reate_tesseract_ocr_pipeline_model.html.j2 | 0 .../tesseract_ocr_pipeline_model.html.j2 | 0 .../tesseract_ocr_pipeline_models.html.j2 | 0 app/templates/corpora/corpus.html.j2 | 2 +- 38 files changed, 509 insertions(+), 443 deletions(-) delete mode 100644 app/contributions/json_routes.py 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/spacy_nlp_pipeline_models/utils.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 create mode 100644 app/contributions/tesseract_ocr_pipeline_models/utils.py create mode 100644 app/corpora/files/utils.py create mode 100644 app/corpora/utils.py create mode 100644 app/jobs/utils.py rename app/templates/contributions/{ => spacy_nlp_pipeline_models}/create_spacy_nlp_pipeline_model.html.j2 (100%) rename app/templates/contributions/{ => spacy_nlp_pipeline_models}/spacy_nlp_pipeline_model.html.j2 (100%) rename app/templates/contributions/{ => spacy_nlp_pipeline_models}/spacy_nlp_pipeline_models.html.j2 (100%) rename app/templates/contributions/{ => tesseract_ocr_pipeline_models}/create_tesseract_ocr_pipeline_model.html.j2 (100%) rename app/templates/contributions/{ => tesseract_ocr_pipeline_models}/tesseract_ocr_pipeline_model.html.j2 (100%) rename app/templates/contributions/{ => tesseract_ocr_pipeline_models}/tesseract_ocr_pipeline_models.html.j2 (100%) diff --git a/app/__init__.py b/app/__init__.py index 68f9f053..4c2ef68e 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -68,16 +68,18 @@ def create_app(config: Config = Config) -> Flask: from .auth import bp as auth_blueprint default_breadcrumb_root(auth_blueprint, '.') - app.register_blueprint(auth_blueprint, url_prefix='/auth') + app.register_blueprint(auth_blueprint, url_prefix='/') from .contributions import bp as contributions_blueprint default_breadcrumb_root(contributions_blueprint, '.contributions') app.register_blueprint(contributions_blueprint, url_prefix='/contributions') from .corpora import bp as corpora_blueprint + default_breadcrumb_root(corpora_blueprint, '.dashboard.corpora') app.register_blueprint(corpora_blueprint, url_prefix='/corpora') from .jobs import bp as jobs_blueprint + default_breadcrumb_root(jobs_blueprint, '.dashboard.jobs') app.register_blueprint(jobs_blueprint, url_prefix='/jobs') from .main import bp as main_blueprint diff --git a/app/auth/routes.py b/app/auth/routes.py index 6e11a140..d6f11953 100644 --- a/app/auth/routes.py +++ b/app/auth/routes.py @@ -101,7 +101,7 @@ def unconfirmed(): return render_template('auth/unconfirmed.html.j2', title='Unconfirmed') -@bp.route('/confirm') +@bp.route('/confirm-request') @login_required def confirm_request(): if current_user.confirmed: @@ -132,7 +132,7 @@ def confirm(token): return redirect(url_for('.unconfirmed')) -@bp.route('/reset_password', methods=['GET', 'POST']) +@bp.route('/reset-password-request', methods=['GET', 'POST']) @register_breadcrumb(bp, '.reset_password_request', 'Password Reset') def reset_password_request(): if current_user.is_authenticated: @@ -162,7 +162,7 @@ def reset_password_request(): ) -@bp.route('/reset_password/', methods=['GET', 'POST']) +@bp.route('/reset-password/', methods=['GET', 'POST']) @register_breadcrumb(bp, '.reset_password', 'Password Reset') def reset_password(token): if current_user.is_authenticated: diff --git a/app/contributions/__init__.py b/app/contributions/__init__.py index 7749a278..22f3e514 100644 --- a/app/contributions/__init__.py +++ b/app/contributions/__init__.py @@ -2,4 +2,6 @@ from flask import Blueprint bp = Blueprint('contributions', __name__) -from . import json_routes, routes +from . import routes +from . import spacy_nlp_pipeline_models +from . import tesseract_ocr_pipeline_models diff --git a/app/contributions/forms.py b/app/contributions/forms.py index 1ef4fdc7..acec307f 100644 --- a/app/contributions/forms.py +++ b/app/contributions/forms.py @@ -1,14 +1,11 @@ from flask_wtf import FlaskForm -from flask_wtf.file import FileField, FileRequired from wtforms import ( StringField, SubmitField, SelectMultipleField, - IntegerField, - ValidationError + IntegerField ) from wtforms.validators import InputRequired, Length -from app.services import SERVICES class ContributionBaseForm(FlaskForm): @@ -48,79 +45,3 @@ class ContributionBaseForm(FlaskForm): class EditContributionBaseForm(ContributionBaseForm): pass - - -############################################################################## -# /spacy-nlp-pipeline-models # -############################################################################## -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 = '' - - -############################################################################## -# /tesseract-ocr-pipeline-models # -############################################################################## -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/json_routes.py b/app/contributions/json_routes.py deleted file mode 100644 index c44a4c9c..00000000 --- a/app/contributions/json_routes.py +++ /dev/null @@ -1,107 +0,0 @@ -from flask import abort, current_app, 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, TesseractOCRPipelineModel -from . import bp - - -############################################################################## -# /spacy-nlp-pipeline-models # -############################################################################## -@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_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' - } - return resonse_data, 202 - - -@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): - 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"}' - ) - } - return response_data, 200 - - -############################################################################## -# /tesseract-ocr-pipeline-models # -############################################################################## -@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_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() - response_data = { - 'message': \ - f'Tesseract OCR Pipeline Model "{topm.title}" marked for deletion' - } - return response_data, 202 - - -@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): - 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"}' - ) - } - return response_data, 200 diff --git a/app/contributions/routes.py b/app/contributions/routes.py index 6da7dc12..dacf5b62 100644 --- a/app/contributions/routes.py +++ b/app/contributions/routes.py @@ -1,189 +1,14 @@ -from flask import abort, flash, redirect, render_template, request, url_for +from flask import render_template from flask_breadcrumbs import register_breadcrumb -from flask_login import current_user, login_required -from app import db -from app.models import SpaCyNLPPipelineModel, TesseractOCRPipelineModel +from flask_login import login_required from . import bp -from .forms import ( - CreateSpaCyNLPPipelineModelForm, - EditSpaCyNLPPipelineModelForm, - CreateTesseractOCRPipelineModelForm, - EditTesseractOCRPipelineModelForm -) @bp.route('') -@register_breadcrumb(bp, '.', 'Contributions') +@register_breadcrumb(bp, '.', 'new_labelContributions') @login_required def contributions(): return render_template( 'contributions/contributions.html.j2', title='Contributions' ) - - -############################################################################## -# /spacy-nlp-pipeline-models # -############################################################################## -@bp.route('/spacy-nlp-pipeline-models') -@register_breadcrumb(bp, '.spacy_nlp_pipeline_models', 'SpaCy NLP Pipeline Models') -@login_required -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']) -@register_breadcrumb(bp, '.spacy_nlp_pipeline_models.create', 'Create') -@login_required -def create_spacy_nlp_pipeline_model(): - form_prefix = 'create-spacy-nlp-pipeline-model-form' - form = CreateSpaCyNLPPipelineModelForm(prefix=form_prefix) - if form.is_submitted(): - if not form.validate(): - return {'errors': form.errors}, 400 - try: - snpm = 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() - flash(f'SpaCy NLP Pipeline model "{snpm.title}" created') - return {}, 201, {'Location': url_for('.spacy_nlp_pipeline_models')} - return render_template( - 'contributions/create_spacy_nlp_pipeline_model.html.j2', - form=form, - title='Create SpaCy NLP Pipeline Model' - ) - - -def spacy_nlp_pipeline_model_dlc(*args, **kwargs): - snpm_id = request.view_args['spacy_nlp_pipeline_model_id'] - snpm = SpaCyNLPPipelineModel.query.get(snpm_id) - return [ - { - 'text': f'{snpm.title} {snpm.version}', - 'url': url_for('.spacy_nlp_pipeline_model', spacy_nlp_pipeline_model_id=snpm_id) - } - ] - - -@bp.route('/spacy-nlp-pipeline-models/', methods=['GET', 'POST']) -@register_breadcrumb(bp, '.spacy_nlp_pipeline_models.entity', '', dynamic_list_constructor=spacy_nlp_pipeline_model_dlc) -@login_required -def spacy_nlp_pipeline_model(spacy_nlp_pipeline_model_id): - snpm = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id) - form_prefix = 'edit-spacy-nlp-pipeline-model-form' - form = EditSpaCyNLPPipelineModelForm( - data=snpm.to_json_serializeable(), - prefix=form_prefix - ) - if form.validate_on_submit(): - form.populate_obj(snpm) - if db.session.is_modified(snpm): - flash(f'SpaCy NLP Pipeline model "{snpm.title}" updated') - 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=snpm, - title=f'{snpm.title} {snpm.version}' - ) - - -############################################################################## -# /tesseract-ocr-pipeline-models # -############################################################################## -@bp.route('/tesseract-ocr-pipeline-models') -@register_breadcrumb(bp, '.tesseract_ocr_pipeline_models', 'Tesseract OCR Pipeline Models') -@login_required -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']) -@register_breadcrumb(bp, '.tesseract_ocr_pipeline_models.create', 'Create') -@login_required -def create_tesseract_ocr_pipeline_model(): - form_prefix = 'create-tesseract-ocr-pipeline-model-form' - form = CreateTesseractOCRPipelineModelForm(prefix=form_prefix) - if form.is_submitted(): - if not form.validate(): - return {'errors': form.errors}, 400 - try: - topm = 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() - flash(f'Tesseract OCR Pipeline model "{topm.title}" created') - return {}, 201, {'Location': url_for('.tesseract_ocr_pipeline_models')} - return render_template( - 'contributions/create_tesseract_ocr_pipeline_model.html.j2', - form=form, - title='Create Tesseract OCR Pipeline Model' - ) - - -def tesseract_ocr_pipeline_model_dlc(*args, **kwargs): - topm_id = request.view_args['tesseract_ocr_pipeline_model_id'] - topm = TesseractOCRPipelineModel.query.get(topm_id) - return [ - { - 'text': f'{topm.title} {topm.version}', - 'url': url_for('.tesseract_ocr_pipeline_model', tesseract_ocr_pipeline_model_id=topm_id) - } - ] - - -@bp.route('/tesseract-ocr-pipeline-models/', methods=['GET', 'POST']) -@register_breadcrumb(bp, '.tesseract_ocr_pipeline_models.entity', '', dynamic_list_constructor=tesseract_ocr_pipeline_model_dlc) -@login_required -def tesseract_ocr_pipeline_model(tesseract_ocr_pipeline_model_id): - topm = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id) - form_prefix = 'edit-tesseract-ocr-pipeline-model-form' - form = EditTesseractOCRPipelineModelForm( - data=topm.to_json_serializeable(), - prefix=form_prefix - ) - if form.validate_on_submit(): - form.populate_obj(topm) - if db.session.is_modified(topm): - flash(f'Tesseract OCR Pipeline model "{topm.title}" updated') - 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=topm, - title=f'{topm.title} {topm.version}' - ) 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..e06bada9 --- /dev/null +++ b/app/contributions/spacy_nlp_pipeline_models/__init__.py @@ -0,0 +1,2 @@ +from .. import bp +from . import json_routes, 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..af6c62d0 --- /dev/null +++ b/app/contributions/spacy_nlp_pipeline_models/json_routes.py @@ -0,0 +1,54 @@ +from flask import abort, current_app, 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('/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_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' + } + return resonse_data, 202 + + +@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): + 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"}' + ) + } + return response_data, 200 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..7ed0797e --- /dev/null +++ b/app/contributions/spacy_nlp_pipeline_models/routes.py @@ -0,0 +1,83 @@ +from flask import abort, flash, redirect, render_template, url_for +from flask_breadcrumbs import register_breadcrumb +from flask_login import current_user, login_required +from app import db +from app.models import SpaCyNLPPipelineModel +from . import bp +from .forms import ( + CreateSpaCyNLPPipelineModelForm, + EditSpaCyNLPPipelineModelForm +) +from .utils import ( + spacy_nlp_pipeline_model_dlc as spacy_nlp_pipeline_model_dlc +) + + +@bp.route('/spacy-nlp-pipeline-models') +@register_breadcrumb(bp, '.spacy_nlp_pipeline_models', 'SpaCy NLP Pipeline Models') +@login_required +def spacy_nlp_pipeline_models(): + return render_template( + 'contributions/spacy_nlp_pipeline_models/spacy_nlp_pipeline_models.html.j2', + title='SpaCy NLP Pipeline Models' + ) + + +@bp.route('/spacy-nlp-pipeline-models/create', methods=['GET', 'POST']) +@register_breadcrumb(bp, '.spacy_nlp_pipeline_models.create', 'Create') +@login_required +def create_spacy_nlp_pipeline_model(): + form_prefix = 'create-spacy-nlp-pipeline-model-form' + form = CreateSpaCyNLPPipelineModelForm(prefix=form_prefix) + if form.is_submitted(): + if not form.validate(): + return {'errors': form.errors}, 400 + try: + snpm = 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() + flash(f'SpaCy NLP Pipeline model "{snpm.title}" created') + return {}, 201, {'Location': url_for('.spacy_nlp_pipeline_models')} + return render_template( + 'contributions/spacy_nlp_pipeline_models/create_spacy_nlp_pipeline_model.html.j2', + form=form, + title='Create SpaCy NLP Pipeline Model' + ) + + +@bp.route('/spacy-nlp-pipeline-models/', methods=['GET', 'POST']) +@register_breadcrumb(bp, '.spacy_nlp_pipeline_models.entity', '', dynamic_list_constructor=spacy_nlp_pipeline_model_dlc) +@login_required +def spacy_nlp_pipeline_model(spacy_nlp_pipeline_model_id): + snpm = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id) + form_prefix = 'edit-spacy-nlp-pipeline-model-form' + form = EditSpaCyNLPPipelineModelForm( + data=snpm.to_json_serializeable(), + prefix=form_prefix + ) + if form.validate_on_submit(): + form.populate_obj(snpm) + if db.session.is_modified(snpm): + flash(f'SpaCy NLP Pipeline model "{snpm.title}" updated') + db.session.commit() + return redirect(url_for('.spacy_nlp_pipeline_models')) + return render_template( + 'contributions/spacy_nlp_pipeline_models/spacy_nlp_pipeline_model.html.j2', + form=form, + spacy_nlp_pipeline_model=snpm, + title=f'{snpm.title} {snpm.version}' + ) diff --git a/app/contributions/spacy_nlp_pipeline_models/utils.py b/app/contributions/spacy_nlp_pipeline_models/utils.py new file mode 100644 index 00000000..204bc56d --- /dev/null +++ b/app/contributions/spacy_nlp_pipeline_models/utils.py @@ -0,0 +1,13 @@ +from flask import request, url_for +from app.models import SpaCyNLPPipelineModel + + +def spacy_nlp_pipeline_model_dlc(): + snpm_id = request.view_args['spacy_nlp_pipeline_model_id'] + snpm = SpaCyNLPPipelineModel.query.get(snpm_id) + return [ + { + 'text': f'{snpm.title} {snpm.version}', + 'url': url_for('.spacy_nlp_pipeline_model', spacy_nlp_pipeline_model_id=snpm_id) + } + ] 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..e06bada9 --- /dev/null +++ b/app/contributions/tesseract_ocr_pipeline_models/__init__.py @@ -0,0 +1,2 @@ +from .. import bp +from . import json_routes, 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..29a9f373 --- /dev/null +++ b/app/contributions/tesseract_ocr_pipeline_models/json_routes.py @@ -0,0 +1,54 @@ +from flask import abort, current_app, 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('/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_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() + response_data = { + 'message': \ + f'Tesseract OCR Pipeline Model "{topm.title}" marked for deletion' + } + return response_data, 202 + + +@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): + 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"}' + ) + } + return response_data, 200 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..71f281ad --- /dev/null +++ b/app/contributions/tesseract_ocr_pipeline_models/routes.py @@ -0,0 +1,82 @@ +from flask import abort, flash, redirect, render_template, request, url_for +from flask_breadcrumbs import register_breadcrumb +from flask_login import current_user, login_required +from app import db +from app.models import TesseractOCRPipelineModel +from . import bp +from .forms import ( + CreateTesseractOCRPipelineModelForm, + EditTesseractOCRPipelineModelForm +) +from .utils import ( + tesseract_ocr_pipeline_model_dlc as tesseract_ocr_pipeline_model_dlc +) + + +@bp.route('/tesseract-ocr-pipeline-models') +@register_breadcrumb(bp, '.tesseract_ocr_pipeline_models', 'Tesseract OCR Pipeline Models') +@login_required +def tesseract_ocr_pipeline_models(): + return render_template( + 'contributions/tesseract_ocr_pipeline_models/tesseract_ocr_pipeline_models.html.j2', + title='Tesseract OCR Pipeline Models' + ) + + +@bp.route('/tesseract-ocr-pipeline-models/create', methods=['GET', 'POST']) +@register_breadcrumb(bp, '.tesseract_ocr_pipeline_models.create', 'Create') +@login_required +def create_tesseract_ocr_pipeline_model(): + form_prefix = 'create-tesseract-ocr-pipeline-model-form' + form = CreateTesseractOCRPipelineModelForm(prefix=form_prefix) + if form.is_submitted(): + if not form.validate(): + return {'errors': form.errors}, 400 + try: + topm = 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() + flash(f'Tesseract OCR Pipeline model "{topm.title}" created') + return {}, 201, {'Location': url_for('.tesseract_ocr_pipeline_models')} + return render_template( + 'contributions/tesseract_ocr_pipeline_models/create_tesseract_ocr_pipeline_model.html.j2', + form=form, + title='Create Tesseract OCR Pipeline Model' + ) + + +@bp.route('/tesseract-ocr-pipeline-models/', methods=['GET', 'POST']) +@register_breadcrumb(bp, '.tesseract_ocr_pipeline_models.entity', '', dynamic_list_constructor=tesseract_ocr_pipeline_model_dlc) +@login_required +def tesseract_ocr_pipeline_model(tesseract_ocr_pipeline_model_id): + topm = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id) + form_prefix = 'edit-tesseract-ocr-pipeline-model-form' + form = EditTesseractOCRPipelineModelForm( + data=topm.to_json_serializeable(), + prefix=form_prefix + ) + if form.validate_on_submit(): + form.populate_obj(topm) + if db.session.is_modified(topm): + flash(f'Tesseract OCR Pipeline model "{topm.title}" updated') + db.session.commit() + return redirect(url_for('.tesseract_ocr_pipeline_models')) + return render_template( + 'contributions/tesseract_ocr_pipeline_models/tesseract_ocr_pipeline_model.html.j2', + form=form, + tesseract_ocr_pipeline_model=topm, + title=f'{topm.title} {topm.version}' + ) diff --git a/app/contributions/tesseract_ocr_pipeline_models/utils.py b/app/contributions/tesseract_ocr_pipeline_models/utils.py new file mode 100644 index 00000000..d957f271 --- /dev/null +++ b/app/contributions/tesseract_ocr_pipeline_models/utils.py @@ -0,0 +1,13 @@ +from flask import request, url_for +from app.models import TesseractOCRPipelineModel + + +def tesseract_ocr_pipeline_model_dlc(): + topm_id = request.view_args['tesseract_ocr_pipeline_model_id'] + topm = TesseractOCRPipelineModel.query.get(topm_id) + return [ + { + 'text': f'{topm.title} {topm.version}', + 'url': url_for('.tesseract_ocr_pipeline_model', tesseract_ocr_pipeline_model_id=topm_id) + } + ] diff --git a/app/corpora/__init__.py b/app/corpora/__init__.py index f39ca22b..af734b0c 100644 --- a/app/corpora/__init__.py +++ b/app/corpora/__init__.py @@ -2,10 +2,6 @@ from flask import Blueprint bp = Blueprint('corpora', __name__) -from . import cqi_over_socketio, routes, json_routes # noqa - -from .files import bp as files_bp -bp.register_blueprint(files_bp, url_prefix='/files') - -from .followers import bp as followers_bp -bp.register_blueprint(followers_bp, url_prefix='/followers') +from . import cqi_over_socketio, routes, json_routes +from . import files +from . import followers diff --git a/app/corpora/files/__init__.py b/app/corpora/files/__init__.py index a52ff995..e06bada9 100644 --- a/app/corpora/files/__init__.py +++ b/app/corpora/files/__init__.py @@ -1,8 +1,2 @@ -from flask import Blueprint - - -template_base_dir = 'corpora/files' - - -bp = Blueprint('files', __name__) -from . import routes, json_routes +from .. import bp +from . import json_routes, routes diff --git a/app/corpora/files/json_routes.py b/app/corpora/files/json_routes.py index cddf4642..2e40775d 100644 --- a/app/corpora/files/json_routes.py +++ b/app/corpora/files/json_routes.py @@ -8,15 +8,7 @@ from ..decorators import corpus_follower_permission_required from . import bp -############################################################################## -# IMPORTANT NOTE: These routes are prefixed by the blueprint # -# Prefix: /files # -# This implies that the corpus_id is always in the kwargs of # -# a route that is registered to this blueprint. # -############################################################################## - - -@bp.route('/', methods=['DELETE']) +@bp.route('//files/', methods=['DELETE']) @login_required @corpus_follower_permission_required('REMOVE_CORPUS_FILE') @content_negotiation(produces='application/json') diff --git a/app/corpora/files/routes.py b/app/corpora/files/routes.py index 5ef4e0d0..0215e45e 100644 --- a/app/corpora/files/routes.py +++ b/app/corpora/files/routes.py @@ -1,29 +1,34 @@ from flask import ( abort, flash, - Markup, redirect, render_template, - send_from_directory + send_from_directory, + url_for ) -from flask_login import current_user, login_required +from flask_breadcrumbs import register_breadcrumb +from flask_login import login_required import os from app import db from app.models import Corpus, CorpusFile, CorpusStatus from ..decorators import corpus_follower_permission_required -from . import bp, template_base_dir +from ..utils import corpus_endpoint_arguments_constructor as corpus_eac +from . import bp from .forms import CreateCorpusFileForm, UpdateCorpusFileForm +from .utils import ( + corpus_file_dynamic_list_constructor as corpus_file_dlc +) -############################################################################## -# IMPORTANT NOTE: These routes are prefixed by the blueprint # -# Prefix: /files # -# This implies that the corpus_id is always in the kwargs of # -# a route that is registered to this blueprint. # -############################################################################## +@bp.route('//files') +@register_breadcrumb(bp, '.entity.files', 'Files', endpoint_arguments_constructor=corpus_eac) +@login_required +def corpus_files(corpus_id): + return redirect(url_for('.corpus', corpus_id=corpus_id, _anchor='files')) -@bp.route('/create', methods=['GET', 'POST']) +@bp.route('//files/create', methods=['GET', 'POST']) +@register_breadcrumb(bp, '.entity.files.create', 'Create', endpoint_arguments_constructor=corpus_eac) @login_required @corpus_follower_permission_required('ADD_CORPUS_FILE') def create_corpus_file(corpus_id): @@ -58,14 +63,15 @@ def create_corpus_file(corpus_id): flash(f'Corpus File "{corpus_file.filename}" added', category='corpus') return '', 201, {'Location': corpus.url} return render_template( - f'{template_base_dir}/create_corpus_file.html.j2', + 'corpora/files/create_corpus_file.html.j2', corpus=corpus, form=form, title='Add corpus file' ) -@bp.route('/', methods=['GET', 'POST']) +@bp.route('//files/', methods=['GET', 'POST']) +@register_breadcrumb(bp, '.entity.files.entity', '', dynamic_list_constructor=corpus_file_dlc) @login_required @corpus_follower_permission_required('UPDATE_CORPUS_FILE') def corpus_file(corpus_id, corpus_file_id): @@ -79,7 +85,7 @@ def corpus_file(corpus_id, corpus_file_id): flash(f'Corpus file "{corpus_file.filename}" updated', category='corpus') return redirect(corpus_file.corpus.url) return render_template( - f'{template_base_dir}/corpus_file.html.j2', + 'corpora/files/corpus_file.html.j2', corpus=corpus_file.corpus, corpus_file=corpus_file, form=form, @@ -87,7 +93,7 @@ def corpus_file(corpus_id, corpus_file_id): ) -@bp.route('//download') +@bp.route('//files//download') @login_required @corpus_follower_permission_required('VIEW') def download_corpus_file(corpus_id, corpus_file_id): diff --git a/app/corpora/files/utils.py b/app/corpora/files/utils.py new file mode 100644 index 00000000..2bb10285 --- /dev/null +++ b/app/corpora/files/utils.py @@ -0,0 +1,15 @@ +from flask import request, url_for +from app.models import CorpusFile +from ..utils import corpus_endpoint_arguments_constructor as corpus_eac + + +def corpus_file_dynamic_list_constructor(): + corpus_id = request.view_args['corpus_id'] + corpus_file_id = request.view_args['corpus_file_id'] + corpus_file = CorpusFile.query.filter_by(corpus_id=corpus_id, id=corpus_file_id).first_or_404() + return [ + { + 'text': f'{corpus_file.author}: {corpus_file.title} ({corpus_file.publishing_year})', + 'url': url_for('.corpus_file', corpus_id=corpus_id, corpus_file_id=corpus_file_id) + } + ] diff --git a/app/corpora/followers/__init__.py b/app/corpora/followers/__init__.py index e1e6d14e..1dbe44f0 100644 --- a/app/corpora/followers/__init__.py +++ b/app/corpora/followers/__init__.py @@ -1,5 +1,2 @@ -from flask import Blueprint - - -bp = Blueprint('followers', __name__) +from .. import bp from . import json_routes diff --git a/app/corpora/followers/json_routes.py b/app/corpora/followers/json_routes.py index 99071731..88fa81d2 100644 --- a/app/corpora/followers/json_routes.py +++ b/app/corpora/followers/json_routes.py @@ -12,15 +12,7 @@ from ..decorators import corpus_owner_or_admin_required from . import bp -############################################################################## -# IMPORTANT NOTE: These routes are prefixed by the blueprint # -# Prefix: /followers # -# This implies that the corpus_id is always in the kwargs of # -# a route that is registered to this blueprint. # -############################################################################## - - -@bp.route('', methods=['POST']) +@bp.route('//followers', methods=['POST']) @login_required @corpus_owner_or_admin_required @content_negotiation(consumes='application/json', produces='application/json') @@ -42,7 +34,7 @@ def create_corpus_followers(corpus_id): return response -@bp.route('//role', methods=['PUT']) +@bp.route('//followers//role', methods=['PUT']) @login_required @corpus_owner_or_admin_required @content_negotiation(consumes='application/json', produces='application/json') @@ -65,7 +57,7 @@ def update_corpus_follower_role(corpus_id, follower_id): return response -@bp.route('/', methods=['DELETE']) +@bp.route('//followers/', methods=['DELETE']) @login_required @content_negotiation(produces='application/json') def delete_corpus_follower(corpus_id, follower_id): diff --git a/app/corpora/routes.py b/app/corpora/routes.py index 92e454d5..ffdb375c 100644 --- a/app/corpora/routes.py +++ b/app/corpora/routes.py @@ -1,4 +1,5 @@ from flask import abort, flash, redirect, render_template, url_for +from flask_breadcrumbs import register_breadcrumb from flask_login import current_user, login_required from .decorators import corpus_follower_permission_required from app import db @@ -10,15 +11,21 @@ from app.models import ( ) from . import bp from .forms import CreateCorpusForm +from .utils import ( + corpus_endpoint_arguments_constructor as corpus_eac, + corpus_dynamic_list_constructor as corpus_dlc +) @bp.route('') +@register_breadcrumb(bp, '.', 'IMy Corpora') @login_required def corpora(): return redirect(url_for('main.dashboard', _anchor='corpora')) @bp.route('/create', methods=['GET', 'POST']) +@register_breadcrumb(bp, '.create', 'Create') @login_required def create_corpus(): form = CreateCorpusForm() @@ -42,6 +49,7 @@ def create_corpus(): @bp.route('/') +@register_breadcrumb(bp, '.entity', '', dynamic_list_constructor=corpus_dlc) @login_required def corpus(corpus_id): corpus = Corpus.query.get_or_404(corpus_id) @@ -54,7 +62,7 @@ def corpus(corpus_id): corpus=corpus, corpus_follower_roles=corpus_follower_roles, users = users, - title='Corpus' + title=corpus.title ) if current_user.is_following_corpus(corpus) or corpus.is_public: cfa = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=current_user.id).first_or_404() @@ -66,12 +74,13 @@ def corpus(corpus_id): corpus_files=corpus_files, cfa=cfa, owner=owner, - title='Corpus', + title=corpus.title ) abort(403) @bp.route('//analyse') +@register_breadcrumb(bp, '.entity.analyse', 'Analyse', endpoint_arguments_constructor=corpus_eac) @login_required @corpus_follower_permission_required('VIEW') def analyse_corpus(corpus_id): @@ -95,12 +104,14 @@ def follow_corpus(corpus_id, token): @bp.route('/import', methods=['GET', 'POST']) +@register_breadcrumb(bp, '.import', 'Import') @login_required def import_corpus(): abort(503) @bp.route('//export') +@register_breadcrumb(bp, '.entity.export', 'Export', endpoint_arguments_constructor=corpus_eac) @login_required def export_corpus(corpus_id): abort(503) diff --git a/app/corpora/utils.py b/app/corpora/utils.py new file mode 100644 index 00000000..fd0e1c2a --- /dev/null +++ b/app/corpora/utils.py @@ -0,0 +1,17 @@ +from flask import request, url_for +from app.models import Corpus + + +def corpus_endpoint_arguments_constructor(): + return {'corpus_id': request.view_args['corpus_id']} + + +def corpus_dynamic_list_constructor(): + corpus_id = request.view_args['corpus_id'] + corpus = Corpus.query.get(corpus_id) + return [ + { + 'text': f'book{corpus.title}', + 'url': url_for('.corpus', corpus_id=corpus_id) + } + ] diff --git a/app/jobs/routes.py b/app/jobs/routes.py index f7848db9..0479cc5d 100644 --- a/app/jobs/routes.py +++ b/app/jobs/routes.py @@ -1,19 +1,27 @@ from flask import ( abort, - current_app, + redirect, render_template, - send_from_directory + send_from_directory, + url_for ) +from flask_breadcrumbs import register_breadcrumb from flask_login import current_user, login_required -from threading import Thread import os -from app import db -from app.decorators import admin_required -from app.models import Job, JobInput, JobResult, JobStatus +from app.models import Job, JobInput, JobResult from . import bp +from .utils import job_dynamic_list_constructor as job_dlc + + +@bp.route('') +@register_breadcrumb(bp, '.', 'JMy Jobs') +@login_required +def corpora(): + return redirect(url_for('main.dashboard', _anchor='jobs')) @bp.route('/') +@register_breadcrumb(bp, '.job', '', dynamic_list_constructor=job_dlc) @login_required def job(job_id): job = Job.query.get_or_404(job_id) diff --git a/app/jobs/utils.py b/app/jobs/utils.py new file mode 100644 index 00000000..8417af04 --- /dev/null +++ b/app/jobs/utils.py @@ -0,0 +1,13 @@ +from flask import request, url_for +from app.models import Job + + +def job_dynamic_list_constructor(): + job_id = request.view_args['job_id'] + job = Job.query.get(job_id) + return [ + { + 'text': f'{job.title}', + 'url': url_for('.job', job_id=job_id) + } + ] diff --git a/app/main/routes.py b/app/main/routes.py index 5399b5e9..a4b2ff2e 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -28,7 +28,7 @@ def faq(): @bp.route('/dashboard') -@register_breadcrumb(bp, '.dashboard', 'Dashboard') +@register_breadcrumb(bp, '.dashboard', 'dashboardDashboard') @login_required def dashboard(): return render_template('main/dashboard.html.j2', title='Dashboard') diff --git a/app/services/routes.py b/app/services/routes.py index 5d5a69a0..5a6ec7fd 100644 --- a/app/services/routes.py +++ b/app/services/routes.py @@ -27,7 +27,7 @@ def services(): @bp.route('/file-setup-pipeline', methods=['GET', 'POST']) -@register_breadcrumb(bp, '.file_setup_pipeline', 'File Setup') +@register_breadcrumb(bp, '.file_setup_pipeline', 'File Setup') @login_required def file_setup_pipeline(): service = 'file-setup-pipeline' @@ -69,7 +69,7 @@ def file_setup_pipeline(): @bp.route('/tesseract-ocr-pipeline', methods=['GET', 'POST']) -@register_breadcrumb(bp, '.tesseract_ocr_pipeline', 'Tesseract OCR Pipeline') +@register_breadcrumb(bp, '.tesseract_ocr_pipeline', 'Tesseract OCR Pipeline') @login_required def tesseract_ocr_pipeline(): service_name = 'tesseract-ocr-pipeline' @@ -119,7 +119,7 @@ def tesseract_ocr_pipeline(): @bp.route('/transkribus-htr-pipeline', methods=['GET', 'POST']) -@register_breadcrumb(bp, '.transkribus_htr_pipeline', 'Transkribus HTR Pipeline') +@register_breadcrumb(bp, '.transkribus_htr_pipeline', 'Transkribus HTR Pipeline') @login_required def transkribus_htr_pipeline(): if not current_app.config.get('NOPAQUE_TRANSKRIBUS_ENABLED'): @@ -179,7 +179,7 @@ def transkribus_htr_pipeline(): @bp.route('/spacy-nlp-pipeline', methods=['GET', 'POST']) -@register_breadcrumb(bp, '.spacy_nlp_pipeline', 'SpaCy NLP Pipeline') +@register_breadcrumb(bp, '.spacy_nlp_pipeline', 'SpaCy NLP Pipeline') @login_required def spacy_nlp_pipeline(): service = 'spacy-nlp-pipeline' @@ -225,7 +225,7 @@ def spacy_nlp_pipeline(): @bp.route('/corpus-analysis') -@register_breadcrumb(bp, '.corpus_analysis', 'Corpus Analysis') +@register_breadcrumb(bp, '.corpus_analysis', 'Corpus Analysis') @login_required def corpus_analysis(): return render_template( diff --git a/app/templates/_roadmap.html.j2 b/app/templates/_roadmap.html.j2 index 3b8ea308..50cc18cd 100644 --- a/app/templates/_roadmap.html.j2 +++ b/app/templates/_roadmap.html.j2 @@ -14,7 +14,7 @@
  • Create corpus
  • navigate_next
  • {% if corpus %} -
  • Create corpus file(s)
  • +
  • Create corpus file(s)
  • {% else %}
  • Create corpus file(s)
  • {% endif %} diff --git a/app/templates/contributions/contributions.html.j2 b/app/templates/contributions/contributions.html.j2 index a0944723..059dc9b2 100644 --- a/app/templates/contributions/contributions.html.j2 +++ b/app/templates/contributions/contributions.html.j2 @@ -12,7 +12,7 @@
    - Tesseract OCR Pipeline Models + Tesseract OCR Pipeline Models

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

    @@ -22,7 +22,7 @@
    - SpaCy NLP Pipeline Models + SpaCy NLP Pipeline Models

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

    @@ -33,7 +33,7 @@
    - Transkribus HTR Pipeline Models + Transkribus HTR 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 100% 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 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 100% 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 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 100% 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 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 100% 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 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 100% 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 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 100% 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 diff --git a/app/templates/corpora/corpus.html.j2 b/app/templates/corpora/corpus.html.j2 index f17e1e88..3ee805d1 100644 --- a/app/templates/corpora/corpus.html.j2 +++ b/app/templates/corpora/corpus.html.j2 @@ -90,7 +90,7 @@