diff --git a/app/__init__.py b/app/__init__.py index 68f9f053afa9e964188ad0e5dd42dc54acdc37f2..4c2ef68ee1be236697cb0f620a37c05f65f97ac4 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 6e11a14090da3aa20ab20119750ba26f5e97bee8..d6f11953bdec5907ebeb62dc34504e55366e1efc 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/<token>', methods=['GET', 'POST']) +@bp.route('/reset-password/<token>', 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 7749a27804487e49afd8017b21ebdae07601c85d..22f3e51435089cdf78cacfacf9eb339e27cfae76 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 1ef4fdc79611ef7944c970621e6791a36813b43d..acec307f00b4e409ff962cc7e0021973a442f2fe 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 c44a4c9ccb38f7ccbef335526efe80948d741a53..0000000000000000000000000000000000000000 --- 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<hashid:spacy_nlp_pipeline_model_id>', 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<hashid:spacy_nlp_pipeline_model_id>/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/<hashid:tesseract_ocr_pipeline_model_id>', 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/<hashid:tesseract_ocr_pipeline_model_id>/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 6da7dc120bbb1923e64e9ad092fec4abd4b442c8..dacf5b62d985680c80ea681f47db7cdc1a1e46a5 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, '.', '<i class="material-icons left">new_label</i>Contributions') @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/<hashid:spacy_nlp_pipeline_model_id>', 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/<hashid:tesseract_ocr_pipeline_model_id>', 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 0000000000000000000000000000000000000000..e06bada943853c58a50171893e5d1db1bdce0afc --- /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 0000000000000000000000000000000000000000..2670c1d1c0ba126c1eabf911b09475eda53c3799 --- /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 0000000000000000000000000000000000000000..af6c62d0a7781c72c62332bfec90761df0712be5 --- /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<hashid:spacy_nlp_pipeline_model_id>', 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<hashid:spacy_nlp_pipeline_model_id>/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 0000000000000000000000000000000000000000..7ed0797e175ab0f24007bcdae31de27523df780a --- /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/<hashid:spacy_nlp_pipeline_model_id>', 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 0000000000000000000000000000000000000000..204bc56db0454118f5a644d2d4548cc4d98182eb --- /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 0000000000000000000000000000000000000000..e06bada943853c58a50171893e5d1db1bdce0afc --- /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 0000000000000000000000000000000000000000..51f0d76c2017029c46691719d3131ee779d5c39c --- /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 0000000000000000000000000000000000000000..29a9f37349751ad509f2ae5a59bcf41e2c354910 --- /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/<hashid:tesseract_ocr_pipeline_model_id>', 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/<hashid:tesseract_ocr_pipeline_model_id>/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 0000000000000000000000000000000000000000..71f281ad34286db4f32ebf8817f341df6895abf6 --- /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/<hashid:tesseract_ocr_pipeline_model_id>', 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 0000000000000000000000000000000000000000..d957f2713809bfc8ea3153b039d89066e0bf5714 --- /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 f39ca22bcb8acf6528737e8cc66cd8864ac98b44..af734b0c205e2f5df3d00431650322a97d48caaa 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='<hashid:corpus_id>/files') - -from .followers import bp as followers_bp -bp.register_blueprint(followers_bp, url_prefix='<hashid:corpus_id>/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 a52ff995c05b5c55bb6d5948f72691d99b3f1591..e06bada943853c58a50171893e5d1db1bdce0afc 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 cddf4642ac9d901a4d80e76566190d222781c77e..2e40775dbf28de3f81eeee359db8eab3c5ea9249 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: <hashid:corpus_id>/files # -# This implies that the corpus_id is always in the kwargs of # -# a route that is registered to this blueprint. # -############################################################################## - - -@bp.route('/<hashid:corpus_file_id>', methods=['DELETE']) +@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>', methods=['DELETE']) @login_required @corpus_follower_permission_required('REMOVE_CORPUS_FILE') @content_negotiation(produces='application/json') diff --git a/app/corpora/files/routes.py b/app/corpora/files/routes.py index 5ef4e0d0c4f594044fb65572cf9ba4cb118e8ef0..0215e45e801c24934c1fbc72d0376ef9182a817b 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: <hashid:corpus_id>/files # -# This implies that the corpus_id is always in the kwargs of # -# a route that is registered to this blueprint. # -############################################################################## +@bp.route('/<hashid:corpus_id>/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('/<hashid:corpus_id>/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('/<hashid:corpus_file_id>', methods=['GET', 'POST']) +@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>', 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('/<hashid:corpus_file_id>/download') +@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>/download') @login_required @corpus_follower_permission_required('VIEW') def download_corpus_file(corpus_id, corpus_file_id): diff --git a/app/corpora/files/utils.py b/app/corpora/files/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..2bb102856208293c007e4ceac79e939daa070e9e --- /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 e1e6d14e80602669b7762857ff9d1fd3ee5bc15d..1dbe44f0e354259bbb30d27fb20649a0c6d4d703 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 990717313f02ef02249f7c9599380e53289e381e..88fa81d2fdeb32b6e89970e479dd5339d8c408f4 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: <hashid:corpus_id>/followers # -# This implies that the corpus_id is always in the kwargs of # -# a route that is registered to this blueprint. # -############################################################################## - - -@bp.route('', methods=['POST']) +@bp.route('/<hashid:corpus_id>/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('/<hashid:follower_id>/role', methods=['PUT']) +@bp.route('/<hashid:corpus_id>/followers/<hashid:follower_id>/role', methods=['PUT']) @login_required @corpus_owner_or_admin_required @content_negotiation(consumes='application/json', produces='application/json') @@ -65,7 +57,7 @@ def update_corpus_follower_role(corpus_id, follower_id): return response -@bp.route('/<hashid:follower_id>', methods=['DELETE']) +@bp.route('/<hashid:corpus_id>/followers/<hashid:follower_id>', 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 92e454d52151f340bad6e1458d124833040ba426..ffdb375c797145a27b50ed76b1bfebb7e2f21b45 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, '.', '<i class="nopaque-icons left">I</i>My 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('/<hashid:corpus_id>') +@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('/<hashid:corpus_id>/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('/<hashid:corpus_id>/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 0000000000000000000000000000000000000000..fd0e1c2aa523dc65d1de20ee3d88b55a451be32e --- /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'<i class="material-icons left">book</i>{corpus.title}', + 'url': url_for('.corpus', corpus_id=corpus_id) + } + ] diff --git a/app/jobs/routes.py b/app/jobs/routes.py index f7848db97576a871c5bfbe87505c432e1109020c..0479cc5dd5296eb45b740b29ddfd8ffe107b2b4a 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, '.', '<i class="nopaque-icons left">J</i>My Jobs') +@login_required +def corpora(): + return redirect(url_for('main.dashboard', _anchor='jobs')) @bp.route('/<hashid:job_id>') +@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 0000000000000000000000000000000000000000..8417af042949dc2068238c26868968343c6427de --- /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'<i class="nopaque-icons left service-icons" data-service="{job.service}"></i>{job.title}', + 'url': url_for('.job', job_id=job_id) + } + ] diff --git a/app/main/routes.py b/app/main/routes.py index 5399b5e96aeff112a75a3b2f85295917cf417c22..a4b2ff2e97269a2589fb186bc1cbdcb9fec84b8c 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', '<i class="material-icons left">dashboard</i>Dashboard') @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 5d5a69a025d5a79913a5d280456af189168e53a6..5a6ec7fde8718048cdc53339b4dfa948de80aac0 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', '<i class="nopaque-icons service-icons left" data-service="file-setup-pipeline"></i>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', '<i class="nopaque-icons service-icons left" data-service="tesseract-ocr-pipeline"></i>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', '<i class="nopaque-icons service-icons left" data-service="transkribus-htr-pipeline"></i>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', '<i class="nopaque-icons service-icons left" data-service="spacy-nlp-pipeline"></i>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', '<i class="nopaque-icons service-icons left" data-service="corpus-analysis"></i>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 3b8ea308f51354b2b9b54470b958082a3f24f46d..50cc18cd7387a4992b66591a6334103a727dced2 100644 --- a/app/templates/_roadmap.html.j2 +++ b/app/templates/_roadmap.html.j2 @@ -14,7 +14,7 @@ <li class="tab"><a{%if request.path == url_for('corpora.create_corpus') %} class="active"{% endif %} href="{{ url_for('corpora.create_corpus') }}" target="_self">Create corpus</a></li> <li class="tab disabled"><i class="material-icons">navigate_next</i></li> {% if corpus %} - <li class="tab"><a{%if request.path == url_for('corpora.files.create_corpus_file', corpus_id=corpus.id) %} class="active"{% endif %} href="{{ url_for('corpora.files.create_corpus_file', corpus_id=corpus.id) }}" target="_self">Create corpus file(s)</a></li> + <li class="tab"><a{%if request.path == url_for('corpora.create_corpus_file', corpus_id=corpus.id) %} class="active"{% endif %} href="{{ url_for('corpora.create_corpus_file', corpus_id=corpus.id) }}" target="_self">Create corpus file(s)</a></li> {% else %} <li class="tab disabled tooltipped" data-tooltip="Select a corpus first" target="_self"><a>Create corpus file(s)</a></li> {% endif %} diff --git a/app/templates/contributions/contributions.html.j2 b/app/templates/contributions/contributions.html.j2 index a09447230ef7549b80d9be2444a8eb12df2a17b6..059dc9b230388b7d45a93e048913e315eeaa48c9 100644 --- a/app/templates/contributions/contributions.html.j2 +++ b/app/templates/contributions/contributions.html.j2 @@ -12,7 +12,7 @@ <div class="card extension-selector hoverable service-color" data-service="tesseract-ocr-pipeline"> <a href="{{ url_for('.tesseract_ocr_pipeline_models') }}" style="position: absolute; width: 100%; height: 100%;"></a> <div class="card-content"> - <span class="card-title" data-service="tesseract-ocr-pipeline"><i class="nopaque-icons service-icons" data-service="tesseract-ocr-pipeline"></i>Tesseract OCR Pipeline Models</span> + <span class="card-title" data-service="tesseract-ocr-pipeline">Tesseract OCR Pipeline Models</span> <p>Here you can see and edit the models that you have created. You can also create new models.</p> </div> </div> @@ -22,7 +22,7 @@ <div class="card extension-selector hoverable service-color" data-service="spacy-nlp-pipeline"> <a href="{{ url_for('.spacy_nlp_pipeline_models') }}" style="position: absolute; width: 100%; height: 100%;"></a> <div class="card-content"> - <span class="card-title"><i class="nopaque-icons service-icons" data-service="spacy-nlp-pipeline"></i>SpaCy NLP Pipeline Models</span> + <span class="card-title">SpaCy NLP Pipeline Models</span> <p>Here you can see and edit the models that you have created. You can also create new models.</p> </div> </div> @@ -33,7 +33,7 @@ <div class="card extension-selector hoverable service-color" data-service="transkribus-htr-pipeline"> <a href="" style="position: absolute; width: 100%; height: 100%;"></a> <div class="card-content"> - <span class="card-title"><i class="nopaque-icons service-icons" data-service="transkribus-htr-pipeline"></i>Transkribus HTR Pipeline Models</span> + <span class="card-title">Transkribus HTR Pipeline Models</span> <p>Here you can see and edit the models that you have created. You can also create new models.</p> </div> </div> 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 f17e1e884923b03dabadef535ab255897c68f689..3ee805d11063ddf46ff6f1d3cf44f182cf91a35b 100644 --- a/app/templates/corpora/corpus.html.j2 +++ b/app/templates/corpora/corpus.html.j2 @@ -90,7 +90,7 @@ <div class="corpus-file-list" data-user-id="{{ corpus.user.hashid }}" data-corpus-id="{{ corpus.hashid }}"></div> </div> <div class="card-action right-align"> - <a href="{{ url_for('corpora.files.create_corpus_file', corpus_id=corpus.id) }}" class="btn waves-effect waves-light"><i class="material-icons left">add</i>Add corpus file</a> + <a href="{{ url_for('corpora.create_corpus_file', corpus_id=corpus.id) }}" class="btn waves-effect waves-light"><i class="material-icons left">add</i>Add corpus file</a> </div> </div> </div>