diff --git a/app/__init__.py b/app/__init__.py index dcb58e47..59d6d5c9 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -4,6 +4,7 @@ from docker import DockerClient from flask import Flask from flask_apscheduler import APScheduler from flask_assets import Environment +from flask_breadcrumbs import Breadcrumbs, default_breadcrumb_root from flask_login import LoginManager from flask_mail import Mail from flask_marshmallow import Marshmallow @@ -16,6 +17,7 @@ from flask_hashids import Hashids apifairy = APIFairy() assets = Environment() +breadcrumbs = Breadcrumbs() db = SQLAlchemy() docker_client = DockerClient() hashids = Hashids() @@ -44,6 +46,7 @@ def create_app(config: Config = Config) -> Flask: apifairy.init_app(app) assets.init_app(app) + breadcrumbs.init_app(app) db.init_app(app) hashids.init_app(app) login.init_app(app) @@ -64,9 +67,11 @@ def create_app(config: Config = Config) -> Flask: app.register_blueprint(api_blueprint, url_prefix='/api') from .auth import bp as auth_blueprint + default_breadcrumb_root(auth_blueprint, '.') app.register_blueprint(auth_blueprint, url_prefix='/auth') 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 @@ -76,9 +81,11 @@ def create_app(config: Config = Config) -> Flask: app.register_blueprint(jobs_blueprint, url_prefix='/jobs') from .main import bp as main_blueprint + default_breadcrumb_root(main_blueprint, '.') app.register_blueprint(main_blueprint, url_prefix='/') from .services import bp as services_blueprint + default_breadcrumb_root(services_blueprint, '.services') app.register_blueprint(services_blueprint, url_prefix='/services') from .settings import bp as settings_blueprint diff --git a/app/auth/routes.py b/app/auth/routes.py index 5655d0dc..6e11a140 100644 --- a/app/auth/routes.py +++ b/app/auth/routes.py @@ -1,11 +1,5 @@ -from flask import ( - abort, - flash, - redirect, - render_template, - request, - url_for -) +from flask import abort, flash, redirect, render_template, request, url_for +from flask_breadcrumbs import register_breadcrumb from flask_login import current_user, login_user, login_required, logout_user from app import db from app.email import create_message, send @@ -36,6 +30,7 @@ def before_request(): @bp.route('/register', methods=['GET', 'POST']) +@register_breadcrumb(bp, '.register', 'Register') def register(): if current_user.is_authenticated: return redirect(url_for('main.dashboard')) @@ -71,6 +66,7 @@ def register(): @bp.route('/login', methods=['GET', 'POST']) +@register_breadcrumb(bp, '.login', 'Login') def login(): if current_user.is_authenticated: return redirect(url_for('main.dashboard')) @@ -97,6 +93,7 @@ def logout(): @bp.route('/unconfirmed') +@register_breadcrumb(bp, '.unconfirmed', 'Unconfirmed') @login_required def unconfirmed(): if current_user.confirmed: @@ -136,6 +133,7 @@ def confirm(token): @bp.route('/reset_password', methods=['GET', 'POST']) +@register_breadcrumb(bp, '.reset_password_request', 'Password Reset') def reset_password_request(): if current_user.is_authenticated: return redirect(url_for('main.dashboard')) @@ -165,6 +163,7 @@ def reset_password_request(): @bp.route('/reset_password/', methods=['GET', 'POST']) +@register_breadcrumb(bp, '.reset_password', 'Password Reset') def reset_password(token): if current_user.is_authenticated: return redirect(url_for('main.dashboard')) diff --git a/app/contributions/__init__.py b/app/contributions/__init__.py index 5175c0ce..7749a278 100644 --- a/app/contributions/__init__.py +++ b/app/contributions/__init__.py @@ -2,16 +2,4 @@ from flask import Blueprint bp = Blueprint('contributions', __name__) -from . import routes - -from .spacy_nlp_pipeline_models import bp as spacy_nlp_pipeline_models_bp -bp.register_blueprint( - spacy_nlp_pipeline_models_bp, - url_prefix='/spacy-nlp-pipeline-models' -) - -from .tesseract_ocr_pipeline_models import bp as tesseract_ocr_pipeline_models_bp -bp.register_blueprint( - tesseract_ocr_pipeline_models_bp, - url_prefix='/tesseract-ocr-pipeline-models' -) +from . import json_routes, routes diff --git a/app/contributions/forms.py b/app/contributions/forms.py index acec307f..1ef4fdc7 100644 --- a/app/contributions/forms.py +++ b/app/contributions/forms.py @@ -1,11 +1,14 @@ from flask_wtf import FlaskForm +from flask_wtf.file import FileField, FileRequired from wtforms import ( StringField, SubmitField, SelectMultipleField, - IntegerField + IntegerField, + ValidationError ) from wtforms.validators import InputRequired, Length +from app.services import SERVICES class ContributionBaseForm(FlaskForm): @@ -45,3 +48,79 @@ 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 new file mode 100644 index 00000000..c44a4c9c --- /dev/null +++ b/app/contributions/json_routes.py @@ -0,0 +1,107 @@ +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 6d8b9cc3..6da7dc12 100644 --- a/app/contributions/routes.py +++ b/app/contributions/routes.py @@ -1,12 +1,189 @@ -from flask import render_template -from flask_login import login_required +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 SpaCyNLPPipelineModel, TesseractOCRPipelineModel from . import bp +from .forms import ( + CreateSpaCyNLPPipelineModelForm, + EditSpaCyNLPPipelineModelForm, + CreateTesseractOCRPipelineModelForm, + EditTesseractOCRPipelineModelForm +) -@bp.route('/') +@bp.route('') +@register_breadcrumb(bp, '.', '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/', 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 deleted file mode 100644 index 8ff119d0..00000000 --- a/app/contributions/spacy_nlp_pipeline_models/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from flask import Blueprint - - -template_base_dir = 'contributions/spacy_nlp_pipeline_models' - - -bp = Blueprint('spacy_nlp_pipeline_models', __name__) -from . import routes, json_routes diff --git a/app/contributions/spacy_nlp_pipeline_models/forms.py b/app/contributions/spacy_nlp_pipeline_models/forms.py deleted file mode 100644 index 2670c1d1..00000000 --- a/app/contributions/spacy_nlp_pipeline_models/forms.py +++ /dev/null @@ -1,44 +0,0 @@ -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 deleted file mode 100644 index f7a9a254..00000000 --- a/app/contributions/spacy_nlp_pipeline_models/json_routes.py +++ /dev/null @@ -1,54 +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 -from . import bp - - -@bp.route('/', methods=['DELETE']) -@login_required -@content_negotiation(produces='application/json') -def delete_spacy_model(spacy_nlp_pipeline_model_id): - def _delete_spacy_model(app, spacy_nlp_pipeline_model_id): - with app.app_context(): - snpm = SpaCyNLPPipelineModel.query.get(spacy_nlp_pipeline_model_id) - snpm.delete() - db.session.commit() - - snpm = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id) - if not (snpm.user == current_user or current_user.is_administrator()): - abort(403) - thread = Thread( - target=_delete_spacy_model, - args=(current_app._get_current_object(), snpm.id) - ) - thread.start() - resonse_data = { - 'message': \ - f'SpaCy NLP Pipeline Model "{snpm.title}" marked for deletion' - } - return resonse_data, 202 - - -@bp.route('//is_public', methods=['PUT']) -@login_required -@permission_required('CONTRIBUTE') -@content_negotiation(consumes='application/json', produces='application/json') -def update_spacy_nlp_pipeline_model_is_public(spacy_nlp_pipeline_model_id): - is_public = request.json - if not isinstance(is_public, bool): - abort(400) - snpm = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id) - if not (snpm.user == current_user or current_user.is_administrator()): - abort(403) - snpm.is_public = is_public - db.session.commit() - response_data = { - 'message': ( - f'SpaCy NLP Pipeline Model "{snpm.title}"' - f' is now {"public" if is_public else "private"}' - ) - } - return response_data, 200 diff --git a/app/contributions/spacy_nlp_pipeline_models/routes.py b/app/contributions/spacy_nlp_pipeline_models/routes.py deleted file mode 100644 index fd972902..00000000 --- a/app/contributions/spacy_nlp_pipeline_models/routes.py +++ /dev/null @@ -1,76 +0,0 @@ -from flask import abort, flash, redirect, render_template, url_for -from flask_login import current_user, login_required -from app import db -from app.models import SpaCyNLPPipelineModel -from . import bp, template_base_dir -from .forms import ( - CreateSpaCyNLPPipelineModelForm, - EditSpaCyNLPPipelineModelForm -) - - -@bp.route('') -@login_required -def spacy_nlp_pipeline_models(): - return render_template( - f'{template_base_dir}/spacy_nlp_pipeline_models.html.j2', - title='SpaCy NLP Pipeline Models' - ) - - -@bp.route('/create', methods=['GET', 'POST']) -@login_required -def create_spacy_nlp_pipeline_model(): - form_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( - f'{template_base_dir}/create_spacy_nlp_pipeline_model.html.j2', - form=form, - title='Create SpaCy NLP Pipeline Model' - ) - - -@bp.route('/', methods=['GET', 'POST']) -@login_required -def spacy_nlp_pipeline_model(spacy_nlp_pipeline_model_id): - 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( - f'{template_base_dir}/spacy_nlp_pipeline_model.html.j2', - form=form, - spacy_nlp_pipeline_model=snpm, - title=f'{snpm.title} {snpm.version}' - ) diff --git a/app/contributions/tesseract_ocr_pipeline_models/__init__.py b/app/contributions/tesseract_ocr_pipeline_models/__init__.py deleted file mode 100644 index cf44126d..00000000 --- a/app/contributions/tesseract_ocr_pipeline_models/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from flask import Blueprint - - -template_base_dir = 'contributions/tesseract_ocr_pipeline_models' - - -bp = Blueprint('tesseract_ocr_pipeline_models', __name__) -from . import routes, json_routes diff --git a/app/contributions/tesseract_ocr_pipeline_models/forms.py b/app/contributions/tesseract_ocr_pipeline_models/forms.py deleted file mode 100644 index 51f0d76c..00000000 --- a/app/contributions/tesseract_ocr_pipeline_models/forms.py +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index 81aa6598..00000000 --- a/app/contributions/tesseract_ocr_pipeline_models/json_routes.py +++ /dev/null @@ -1,54 +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 TesseractOCRPipelineModel -from . import bp - - -@bp.route('/', methods=['DELETE']) -@login_required -@content_negotiation(produces='application/json') -def delete_tesseract_model(tesseract_ocr_pipeline_model_id): - def _delete_tesseract_ocr_pipeline_model(app, tesseract_ocr_pipeline_model_id): - with app.app_context(): - topm = TesseractOCRPipelineModel.query.get(tesseract_ocr_pipeline_model_id) - topm.delete() - db.session.commit() - - topm = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id) - if not (topm.user == current_user or current_user.is_administrator()): - abort(403) - thread = Thread( - target=_delete_tesseract_ocr_pipeline_model, - args=(current_app._get_current_object(), topm.id) - ) - thread.start() - response_data = { - 'message': \ - f'Tesseract OCR Pipeline Model "{topm.title}" marked for deletion' - } - return response_data, 202 - - -@bp.route('//is_public', methods=['PUT']) -@login_required -@permission_required('CONTRIBUTE') -@content_negotiation(consumes='application/json', produces='application/json') -def update_tesseract_ocr_pipeline_model_is_public(tesseract_ocr_pipeline_model_id): - is_public = request.json - if not isinstance(is_public, bool): - abort(400) - topm = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id) - if not (topm.user == current_user or current_user.is_administrator()): - abort(403) - topm.is_public = is_public - db.session.commit() - response_data = { - 'message': ( - f'Tesseract OCR Pipeline Model "{topm.title}"' - f' is now {"public" if is_public else "private"}' - ) - } - return response_data, 200 diff --git a/app/contributions/tesseract_ocr_pipeline_models/routes.py b/app/contributions/tesseract_ocr_pipeline_models/routes.py deleted file mode 100644 index b888cef7..00000000 --- a/app/contributions/tesseract_ocr_pipeline_models/routes.py +++ /dev/null @@ -1,75 +0,0 @@ -from flask import abort, flash, redirect, render_template, url_for -from flask_login import login_required, current_user -from app import db -from app.models import TesseractOCRPipelineModel -from . import bp, template_base_dir -from .forms import ( - CreateTesseractOCRPipelineModelForm, - EditTesseractOCRPipelineModelForm -) - - -@bp.route('') -@login_required -def tesseract_ocr_pipeline_models(): - return render_template( - f'{template_base_dir}/tesseract_ocr_pipeline_models.html.j2', - title='Tesseract OCR Pipeline Models' - ) - - -@bp.route('/create', methods=['GET', 'POST']) -@login_required -def create_tesseract_ocr_pipeline_model(): - form_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( - f'{template_base_dir}/create_tesseract_ocr_pipeline_model.html.j2', - form=form, - title='Create Tesseract OCR Pipeline Model' - ) - - -@bp.route('/', methods=['GET', 'POST']) -@login_required -def tesseract_ocr_pipeline_model(tesseract_ocr_pipeline_model_id): - 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( - f'{template_base_dir}/tesseract_ocr_pipeline_model.html.j2', - form=form, - tesseract_ocr_pipeline_model=topm, - title=f'{topm.title} {topm.version}' - ) diff --git a/app/corpora/routes.py b/app/corpora/routes.py index 5484fb50..82701cea 100644 --- a/app/corpora/routes.py +++ b/app/corpora/routes.py @@ -1,23 +1,18 @@ -from flask import ( - abort, - flash, - Markup, - redirect, - render_template, - url_for -) +from flask import abort, flash, redirect, render_template, url_for from flask_login import current_user, login_required from .decorators import corpus_follower_permission_required from app import db -from app.models import ( - Corpus, - CorpusFollowerAssociation, - CorpusFollowerRole, -) +from app.models import Corpus, CorpusFollowerAssociation, CorpusFollowerRole from . import bp from .forms import CreateCorpusForm +@bp.route('') +@login_required +def corpora(): + return redirect(url_for('main.dashboard', _anchor='corpora')) + + @bp.route('/create', methods=['GET', 'POST']) @login_required def create_corpus(): diff --git a/app/main/routes.py b/app/main/routes.py index 918f7414..5399b5e9 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -1,4 +1,5 @@ from flask import flash, redirect, render_template, url_for +from flask_breadcrumbs import register_breadcrumb from flask_login import current_user, login_required, login_user from app.auth.forms import LoginForm from app.models import Corpus, User @@ -6,6 +7,7 @@ from . import bp @bp.route('', methods=['GET', 'POST']) +@register_breadcrumb(bp, '.', 'home') def index(): form = LoginForm(prefix='login-form') if form.validate_on_submit(): @@ -20,37 +22,44 @@ def index(): @bp.route('/faq') +@register_breadcrumb(bp, '.faq', 'Frequently Asked Questions') def faq(): return render_template('main/faq.html.j2', title='Frequently Asked Questions') @bp.route('/dashboard') +@register_breadcrumb(bp, '.dashboard', 'Dashboard') @login_required def dashboard(): return render_template('main/dashboard.html.j2', title='Dashboard') @bp.route('/user_manual') +@register_breadcrumb(bp, '.user_manual', 'User manual') def user_manual(): return render_template('main/user_manual.html.j2', title='User manual') @bp.route('/news') +@register_breadcrumb(bp, '.news', 'News') def news(): return render_template('main/news.html.j2', title='News') @bp.route('/privacy_policy') +@register_breadcrumb(bp, '.privacy_policy', 'Private statement (GDPR)') def privacy_policy(): return render_template('main/privacy_policy.html.j2', title='Privacy statement (GDPR)') @bp.route('/terms_of_use') +@register_breadcrumb(bp, '.terms_of_use', 'Terms of Use') def terms_of_use(): return render_template('main/terms_of_use.html.j2', title='Terms of Use') @bp.route('/social-area') +@register_breadcrumb(bp, '.social_area', 'Social Area') def social_area(): users = [ u.to_json_serializeable(relationships=True, filter_by_privacy_settings=True,) for u diff --git a/app/services/routes.py b/app/services/routes.py index 0fb4b168..5d5a69a0 100644 --- a/app/services/routes.py +++ b/app/services/routes.py @@ -1,4 +1,5 @@ -from flask import abort, current_app, flash, make_response, Markup, render_template, request +from flask import abort, current_app, flash, Markup, redirect, render_template, request, url_for +from flask_breadcrumbs import register_breadcrumb from flask_login import current_user, login_required import requests from app import db, hashids @@ -18,7 +19,15 @@ from .forms import ( ) +@bp.route('/services') +@register_breadcrumb(bp, '.', 'Services') +@login_required +def services(): + return redirect(url_for('main.dashboard')) + + @bp.route('/file-setup-pipeline', methods=['GET', 'POST']) +@register_breadcrumb(bp, '.file_setup_pipeline', 'File Setup') @login_required def file_setup_pipeline(): service = 'file-setup-pipeline' @@ -60,6 +69,7 @@ def file_setup_pipeline(): @bp.route('/tesseract-ocr-pipeline', methods=['GET', 'POST']) +@register_breadcrumb(bp, '.tesseract_ocr_pipeline', 'Tesseract OCR Pipeline') @login_required def tesseract_ocr_pipeline(): service_name = 'tesseract-ocr-pipeline' @@ -109,6 +119,7 @@ def tesseract_ocr_pipeline(): @bp.route('/transkribus-htr-pipeline', methods=['GET', 'POST']) +@register_breadcrumb(bp, '.transkribus_htr_pipeline', 'Transkribus HTR Pipeline') @login_required def transkribus_htr_pipeline(): if not current_app.config.get('NOPAQUE_TRANSKRIBUS_ENABLED'): @@ -168,6 +179,7 @@ def transkribus_htr_pipeline(): @bp.route('/spacy-nlp-pipeline', methods=['GET', 'POST']) +@register_breadcrumb(bp, '.spacy_nlp_pipeline', 'SpaCy NLP Pipeline') @login_required def spacy_nlp_pipeline(): service = 'spacy-nlp-pipeline' @@ -213,9 +225,10 @@ def spacy_nlp_pipeline(): @bp.route('/corpus-analysis') +@register_breadcrumb(bp, '.corpus_analysis', 'Corpus Analysis') @login_required def corpus_analysis(): return render_template( 'services/corpus_analysis.html.j2', - title='Corpus analysis' + title='Corpus Analysis' ) diff --git a/app/templates/_navbar.html.j2 b/app/templates/_navbar.html.j2 index d943c0e4..2790ef1a 100644 --- a/app/templates/_navbar.html.j2 +++ b/app/templates/_navbar.html.j2 @@ -14,10 +14,12 @@