Merge branch 'public-corpus' of gitlab.ub.uni-bielefeld.de:sfb1288inf/nopaque into public-corpus

This commit is contained in:
Inga Kirschnick 2023-03-09 13:18:47 +01:00
commit 5f27ce2801
34 changed files with 983 additions and 503 deletions

View File

@ -3,3 +3,15 @@ from flask import Blueprint
bp = Blueprint('contributions', __name__) bp = Blueprint('contributions', __name__)
from . import routes 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'
)

View File

@ -1,16 +1,11 @@
from flask import current_app
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileRequired
from wtforms import ( from wtforms import (
BooleanField,
StringField, StringField,
SubmitField, SubmitField,
SelectMultipleField, SelectMultipleField,
IntegerField, IntegerField
ValidationError
) )
from wtforms.validators import InputRequired, Length from wtforms.validators import InputRequired, Length
from app.services import SERVICES
class ContributionBaseForm(FlaskForm): class ContributionBaseForm(FlaskForm):
@ -48,74 +43,5 @@ class ContributionBaseForm(FlaskForm):
submit = SubmitField() submit = SubmitField()
class CreateTesseractOCRPipelineModelForm(ContributionBaseForm):
tesseract_model_file = FileField(
'File',
validators=[FileRequired()]
)
def validate_tesseract_model_file(self, field):
if not field.data.filename.lower().endswith('.traineddata'):
raise ValidationError('traineddata files only!')
def __init__(self, *args, **kwargs):
service_manifest = SERVICES['tesseract-ocr-pipeline']
super().__init__(*args, **kwargs)
self.compatible_service_versions.choices = [('', 'Choose your option')]
self.compatible_service_versions.choices += [
(x, x) for x in service_manifest['versions'].keys()
]
self.compatible_service_versions.default = ''
class CreateSpaCyNLPPipelineModelForm(ContributionBaseForm):
spacy_model_file = FileField(
'File',
validators=[FileRequired()]
)
pipeline_name = StringField(
'Pipeline name',
validators=[InputRequired(), Length(max=64)]
)
def validate_spacy_model_file(self, field):
if not field.data.filename.lower().endswith('.tar.gz'):
raise ValidationError('.tar.gz files only!')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
service_manifest = SERVICES['spacy-nlp-pipeline']
self.compatible_service_versions.choices = [('', 'Choose your option')]
self.compatible_service_versions.choices += [
(x, x) for x in service_manifest['versions'].keys()
]
self.compatible_service_versions.default = ''
class EditContributionBaseForm(ContributionBaseForm): class EditContributionBaseForm(ContributionBaseForm):
pass pass
class EditTesseractOCRPipelineModelForm(EditContributionBaseForm):
def __init__(self, *args, **kwargs):
service_manifest = SERVICES['tesseract-ocr-pipeline']
super().__init__(*args, **kwargs)
self.compatible_service_versions.choices = [('', 'Choose your option')]
self.compatible_service_versions.choices += [
(x, x) for x in service_manifest['versions'].keys()
]
self.compatible_service_versions.default = ''
class EditSpaCyNLPPipelineModelForm(EditContributionBaseForm):
pipeline_name = StringField(
'Pipeline name',
validators=[InputRequired(), Length(max=64)]
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
service_manifest = SERVICES['spacy-nlp-pipeline']
self.compatible_service_versions.choices = [('', 'Choose your option')]
self.compatible_service_versions.choices += [
(x, x) for x in service_manifest['versions'].keys()
]
self.compatible_service_versions.default = ''

View File

@ -1,233 +1,12 @@
from flask import ( from flask import render_template
abort, from flask_login import login_required
current_app,
flash,
Markup,
redirect,
render_template,
url_for
)
from flask_login import login_required, current_user
from threading import Thread
from app import db
from app.decorators import permission_required
from app.models import (
Permission,
SpaCyNLPPipelineModel,
TesseractOCRPipelineModel
)
from . import bp from . import bp
from .forms import (
CreateSpaCyNLPPipelineModelForm,
CreateTesseractOCRPipelineModelForm,
EditSpaCyNLPPipelineModelForm,
EditTesseractOCRPipelineModelForm
)
@bp.before_request
@login_required
def before_request():
pass
@bp.route('/') @bp.route('/')
@login_required
def contributions(): def contributions():
return render_template( return render_template(
'contributions/contributions.html.j2', 'contributions/contributions.html.j2',
title='Contributions' title='Contributions'
) )
@bp.route('/tesseract-ocr-pipeline-models')
def tesseract_ocr_pipeline_models():
return render_template(
'contributions/tesseract_ocr_pipeline_models.html.j2',
title='Tesseract OCR Pipeline Models'
)
@bp.route('/tesseract-ocr-pipeline-models/<hashid:tesseract_ocr_pipeline_model_id>', methods=['GET', 'POST'])
def tesseract_ocr_pipeline_model(tesseract_ocr_pipeline_model_id):
tesseract_ocr_pipeline_model = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id)
form = EditTesseractOCRPipelineModelForm(
data=tesseract_ocr_pipeline_model.to_json_serializeable(),
prefix='edit-tesseract-ocr-pipeline-model-form'
)
if form.validate_on_submit():
form.populate_obj(tesseract_ocr_pipeline_model)
if db.session.is_modified(tesseract_ocr_pipeline_model):
message = Markup(f'Tesseract OCR Pipeline model "<a href="{tesseract_ocr_pipeline_model.url}">{tesseract_ocr_pipeline_model.title}</a>" updated')
flash(message)
db.session.commit()
return redirect(url_for('.tesseract_ocr_pipeline_models'))
return render_template(
'contributions/tesseract_ocr_pipeline_model.html.j2',
form=form,
tesseract_ocr_pipeline_model=tesseract_ocr_pipeline_model,
title=f'{tesseract_ocr_pipeline_model.title} {tesseract_ocr_pipeline_model.version}'
)
@bp.route('/tesseract-ocr-pipeline-models/<hashid:tesseract_ocr_pipeline_model_id>', methods=['DELETE'])
def delete_tesseract_model(tesseract_ocr_pipeline_model_id):
def _delete_tesseract_ocr_pipeline_model(app, tesseract_ocr_pipeline_model_id):
with app.app_context():
tesseract_ocr_pipeline_model = TesseractOCRPipelineModel.query.get(tesseract_ocr_pipeline_model_id)
tesseract_ocr_pipeline_model.delete()
db.session.commit()
tesseract_ocr_pipeline_model = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id)
if not (tesseract_ocr_pipeline_model.user == current_user or current_user.is_administrator()):
abort(403)
thread = Thread(
target=_delete_tesseract_ocr_pipeline_model,
args=(current_app._get_current_object(), tesseract_ocr_pipeline_model_id)
)
thread.start()
return {}, 202
@bp.route('/tesseract-ocr-pipeline-models/create', methods=['GET', 'POST'])
def create_tesseract_ocr_pipeline_model():
form = CreateTesseractOCRPipelineModelForm(prefix='create-tesseract-ocr-pipeline-model-form')
if form.is_submitted():
if not form.validate():
response = {'errors': form.errors}
return response, 400
try:
tesseract_ocr_pipeline_model = TesseractOCRPipelineModel.create(
form.tesseract_model_file.data,
compatible_service_versions=form.compatible_service_versions.data,
description=form.description.data,
publisher=form.publisher.data,
publisher_url=form.publisher_url.data,
publishing_url=form.publishing_url.data,
publishing_year=form.publishing_year.data,
is_public=False,
title=form.title.data,
version=form.version.data,
user=current_user
)
except OSError:
abort(500)
db.session.commit()
tesseract_ocr_pipeline_model_url = url_for(
'.tesseract_ocr_pipeline_model',
tesseract_ocr_pipeline_model_id=tesseract_ocr_pipeline_model.id
)
message = Markup(f'Tesseract OCR Pipeline model "<a href="{tesseract_ocr_pipeline_model_url}">{tesseract_ocr_pipeline_model.title}</a>" created')
flash(message)
return {}, 201, {'Location': tesseract_ocr_pipeline_model_url}
return render_template(
'contributions/create_tesseract_ocr_pipeline_model.html.j2',
form=form,
title='Create Tesseract OCR Pipeline Model'
)
@bp.route('/tesseract-ocr-pipeline-models/<hashid:tesseract_ocr_pipeline_model_id>/toggle-public-status', methods=['POST'])
@permission_required(Permission.CONTRIBUTE)
def toggle_tesseract_ocr_pipeline_model_public_status(tesseract_ocr_pipeline_model_id):
tesseract_ocr_pipeline_model = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id)
if not (tesseract_ocr_pipeline_model.user == current_user or current_user.is_administrator()):
abort(403)
tesseract_ocr_pipeline_model.is_public = not tesseract_ocr_pipeline_model.is_public
db.session.commit()
return {}, 201
@bp.route('/spacy-nlp-pipeline-models')
def spacy_nlp_pipeline_models():
return render_template(
'contributions/spacy_nlp_pipeline_models.html.j2',
title='SpaCy NLP Pipeline Models'
)
@bp.route('/spacy-nlp-pipeline-models/<hashid:spacy_nlp_pipeline_model_id>', methods=['GET', 'POST'])
def spacy_nlp_pipeline_model(spacy_nlp_pipeline_model_id):
spacy_nlp_pipeline_model = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id)
form = EditSpaCyNLPPipelineModelForm(
data=spacy_nlp_pipeline_model.to_json_serializeable(),
prefix='edit-spacy-nlp-pipeline-model-form'
)
if form.validate_on_submit():
form.populate_obj(spacy_nlp_pipeline_model)
if db.session.is_modified(spacy_nlp_pipeline_model):
message = Markup(f'SpaCy NLP Pipeline model "<a href="{spacy_nlp_pipeline_model.url}">{spacy_nlp_pipeline_model.title}</a>" updated')
flash(message)
db.session.commit()
return redirect(url_for('.spacy_nlp_pipeline_models'))
return render_template(
'contributions/spacy_nlp_pipeline_model.html.j2',
form=form,
spacy_nlp_pipeline_model=spacy_nlp_pipeline_model,
title=f'{spacy_nlp_pipeline_model.title} {spacy_nlp_pipeline_model.version}'
)
@bp.route('/spacy-nlp-pipeline-models/<hashid:spacy_nlp_pipeline_model_id>', methods=['DELETE'])
def delete_spacy_model(spacy_nlp_pipeline_model_id):
def _delete_spacy_model(app, spacy_nlp_pipeline_model_id):
with app.app_context():
spacy_nlp_pipeline_model = SpaCyNLPPipelineModel.query.get(spacy_nlp_pipeline_model_id)
spacy_nlp_pipeline_model.delete()
db.session.commit()
spacy_nlp_pipeline_model = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id)
if not (spacy_nlp_pipeline_model.user == current_user or current_user.is_administrator()):
abort(403)
thread = Thread(
target=_delete_spacy_model,
args=(current_app._get_current_object(), spacy_nlp_pipeline_model_id)
)
thread.start()
return {}, 202
@bp.route('/spacy-nlp-pipeline-models/create', methods=['GET', 'POST'])
def create_spacy_nlp_pipeline_model():
form = CreateSpaCyNLPPipelineModelForm(prefix='create-spacy-nlp-pipeline-model-form')
if form.is_submitted():
if not form.validate():
response = {'errors': form.errors}
return response, 400
try:
spacy_nlp_pipeline_model = SpaCyNLPPipelineModel.create(
form.spacy_model_file.data,
compatible_service_versions=form.compatible_service_versions.data,
description=form.description.data,
pipeline_name=form.pipeline_name.data,
publisher=form.publisher.data,
publisher_url=form.publisher_url.data,
publishing_url=form.publishing_url.data,
publishing_year=form.publishing_year.data,
is_public=False,
title=form.title.data,
version=form.version.data,
user=current_user
)
except OSError:
abort(500)
db.session.commit()
spacy_nlp_pipeline_model_url = url_for(
'.spacy_nlp_pipeline_model',
spacy_nlp_pipeline_model_id=spacy_nlp_pipeline_model.id
)
message = Markup(f'SpaCy NLP Pipeline model "<a href="{spacy_nlp_pipeline_model_url}">{spacy_nlp_pipeline_model.title}</a>" created')
flash(message)
return {}, 201, {'Location': spacy_nlp_pipeline_model_url}
return render_template(
'contributions/create_spacy_nlp_pipeline_model.html.j2',
form=form,
title='Create SpaCy NLP Pipeline Model'
)
@bp.route('/spacy-nlp-pipeline-models/<hashid:spacy_nlp_pipeline_model_id>/toggle-public-status', methods=['POST'])
@permission_required(Permission.CONTRIBUTE)
def toggle_spacy_nlp_pipeline_model_public_status(spacy_nlp_pipeline_model_id):
spacy_nlp_pipeline_model = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id)
if not (spacy_nlp_pipeline_model.user == current_user or current_user.is_administrator()):
abort(403)
spacy_nlp_pipeline_model.is_public = not spacy_nlp_pipeline_model.is_public
db.session.commit()
return {}, 201

View File

@ -0,0 +1,8 @@
from flask import Blueprint
TEMPLATE_FOLDER = 'contributions/spacy_nlp_pipeline_models'
bp = Blueprint('spacy_nlp_pipeline_models', __name__)
from . import routes, json_routes

View File

@ -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 = ''

View File

@ -0,0 +1,58 @@
from flask import abort, current_app, jsonify, request
from flask_login import login_required, current_user
from threading import Thread
from app import db
from app.decorators import content_negotiation, permission_required
from app.models import SpaCyNLPPipelineModel
from . import bp
@bp.route('/<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'
}
response = jsonify(resonse_data)
response.status_code = 202
return response
@bp.route('/<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"}'
)
}
response = jsonify(response_data)
response.status_code = 200
return response

View File

@ -0,0 +1,87 @@
from flask import abort, flash, Markup, redirect, render_template, url_for
from flask_login import login_required, current_user
from app import db
from app.models import SpaCyNLPPipelineModel
from . import bp, TEMPLATE_FOLDER
from .forms import (
CreateSpaCyNLPPipelineModelForm,
EditSpaCyNLPPipelineModelForm
)
@bp.route('')
@login_required
def spacy_nlp_pipeline_models():
return render_template(
f'{TEMPLATE_FOLDER}/spacy_nlp_pipeline_models.html.j2',
title='SpaCy NLP Pipeline Models'
)
@bp.route('/create', methods=['GET', 'POST'])
@login_required
def create_spacy_nlp_pipeline_model():
form = CreateSpaCyNLPPipelineModelForm(prefix='create-spacy-nlp-pipeline-model-form')
if form.is_submitted():
if not form.validate():
response = {'errors': form.errors}
return response, 400
try:
spacy_nlp_pipeline_model = SpaCyNLPPipelineModel.create(
form.spacy_model_file.data,
compatible_service_versions=form.compatible_service_versions.data,
description=form.description.data,
pipeline_name=form.pipeline_name.data,
publisher=form.publisher.data,
publisher_url=form.publisher_url.data,
publishing_url=form.publishing_url.data,
publishing_year=form.publishing_year.data,
is_public=False,
title=form.title.data,
version=form.version.data,
user=current_user
)
except OSError:
abort(500)
db.session.commit()
spacy_nlp_pipeline_model_url = url_for(
'.spacy_nlp_pipeline_model',
spacy_nlp_pipeline_model_id=spacy_nlp_pipeline_model.id
)
message = Markup(
f'SpaCy NLP Pipeline model "{spacy_nlp_pipeline_model.title}" '
'created'
)
flash(message)
return '', 201, {'Location': spacy_nlp_pipeline_model_url}
return render_template(
f'{TEMPLATE_FOLDER}/create_spacy_nlp_pipeline_model.html.j2',
form=form,
title='Create SpaCy NLP Pipeline Model'
)
@bp.route('/<hashid:spacy_nlp_pipeline_model_id>', methods=['GET', 'POST'])
@login_required
def spacy_nlp_pipeline_model(spacy_nlp_pipeline_model_id):
spacy_nlp_pipeline_model = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id)
form = EditSpaCyNLPPipelineModelForm(
data=spacy_nlp_pipeline_model.to_json_serializeable(),
prefix='edit-spacy-nlp-pipeline-model-form'
)
if form.validate_on_submit():
form.populate_obj(spacy_nlp_pipeline_model)
if db.session.is_modified(spacy_nlp_pipeline_model):
message = Markup(
f'SpaCy NLP Pipeline model "{spacy_nlp_pipeline_model.title}" '
'updated'
)
flash(message)
db.session.commit()
return redirect(url_for('.spacy_nlp_pipeline_models'))
return render_template(
f'{TEMPLATE_FOLDER}/spacy_nlp_pipeline_model.html.j2',
form=form,
spacy_nlp_pipeline_model=spacy_nlp_pipeline_model,
title=f'{spacy_nlp_pipeline_model.title} {spacy_nlp_pipeline_model.version}'
)

View File

@ -0,0 +1,8 @@
from flask import Blueprint
TEMPLATE_FOLDER = 'contributions/tesseract_ocr_pipeline_models'
bp = Blueprint('tesseract_ocr_pipeline_models', __name__)
from . import routes, json_routes

View File

@ -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 = ''

View File

@ -0,0 +1,58 @@
from flask import abort, current_app, jsonify, request
from flask_login import login_required, current_user
from threading import Thread
from app import db
from app.decorators import content_negotiation, permission_required
from app.models import TesseractOCRPipelineModel
from . import bp
@bp.route('/<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()
resonse_data = {
'message': \
f'Tesseract OCR Pipeline Model "{topm.title}" marked for deletion'
}
response = jsonify(resonse_data)
response.status_code = 202
return response
@bp.route('/<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"}'
)
}
response = jsonify(response_data)
response.status_code = 200
return response

View File

@ -0,0 +1,80 @@
from flask import abort, flash, Markup, redirect, render_template, url_for
from flask_login import login_required, current_user
from app import db
from app.models import TesseractOCRPipelineModel
from . import bp, TEMPLATE_FOLDER
from .forms import (
CreateTesseractOCRPipelineModelForm,
EditTesseractOCRPipelineModelForm
)
@bp.route('')
@login_required
def tesseract_ocr_pipeline_models():
return render_template(
f'{TEMPLATE_FOLDER}/tesseract_ocr_pipeline_models.html.j2',
title='Tesseract OCR Pipeline Models'
)
@bp.route('/create', methods=['GET', 'POST'])
@login_required
def create_tesseract_ocr_pipeline_model():
form = CreateTesseractOCRPipelineModelForm(prefix='create-tesseract-ocr-pipeline-model-form')
if form.is_submitted():
if not form.validate():
response = {'errors': form.errors}
return response, 400
try:
tesseract_ocr_pipeline_model = TesseractOCRPipelineModel.create(
form.tesseract_model_file.data,
compatible_service_versions=form.compatible_service_versions.data,
description=form.description.data,
publisher=form.publisher.data,
publisher_url=form.publisher_url.data,
publishing_url=form.publishing_url.data,
publishing_year=form.publishing_year.data,
is_public=False,
title=form.title.data,
version=form.version.data,
user=current_user
)
except OSError:
abort(500)
db.session.commit()
tesseract_ocr_pipeline_model_url = url_for(
'.tesseract_ocr_pipeline_model',
tesseract_ocr_pipeline_model_id=tesseract_ocr_pipeline_model.id
)
message = Markup(f'Tesseract OCR Pipeline model "<a href="{tesseract_ocr_pipeline_model_url}">{tesseract_ocr_pipeline_model.title}</a>" created')
flash(message)
return {}, 201, {'Location': tesseract_ocr_pipeline_model_url}
return render_template(
f'{TEMPLATE_FOLDER}/create_tesseract_ocr_pipeline_model.html.j2',
form=form,
title='Create Tesseract OCR Pipeline Model'
)
@bp.route('/<hashid:tesseract_ocr_pipeline_model_id>', methods=['GET', 'POST'])
@login_required
def tesseract_ocr_pipeline_model(tesseract_ocr_pipeline_model_id):
tesseract_ocr_pipeline_model = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id)
form = EditTesseractOCRPipelineModelForm(
data=tesseract_ocr_pipeline_model.to_json_serializeable(),
prefix='edit-tesseract-ocr-pipeline-model-form'
)
if form.validate_on_submit():
form.populate_obj(tesseract_ocr_pipeline_model)
if db.session.is_modified(tesseract_ocr_pipeline_model):
message = Markup(f'Tesseract OCR Pipeline model "<a href="{tesseract_ocr_pipeline_model.url}">{tesseract_ocr_pipeline_model.title}</a>" updated')
flash(message)
db.session.commit()
return redirect(url_for('.tesseract_ocr_pipeline_models'))
return render_template(
f'{TEMPLATE_FOLDER}/tesseract_ocr_pipeline_model.html.j2',
form=form,
tesseract_ocr_pipeline_model=tesseract_ocr_pipeline_model,
title=f'{tesseract_ocr_pipeline_model.title} {tesseract_ocr_pipeline_model.version}'
)

View File

@ -16,6 +16,7 @@ from threading import Thread
import os import os
from .decorators import corpus_follower_permission_required, corpus_owner_or_admin_required from .decorators import corpus_follower_permission_required, corpus_owner_or_admin_required
from app import db, hashids from app import db, hashids
from app.decorators import content_negotiation
from app.models import ( from app.models import (
Corpus, Corpus,
CorpusFile, CorpusFile,
@ -31,102 +32,6 @@ from .forms import (
UpdateCorpusFileForm UpdateCorpusFileForm
) )
@bp.route('/fake-add')
@login_required
def fake_add():
pjentsch = User.query.filter_by(username='pjentsch').first()
alice = Corpus.query.filter_by(title='Alice in Wonderland').first()
pjentsch.follow_corpus(alice)
db.session.commit()
return ''
@bp.route('/<hashid:corpus_id>/is_public', methods=['POST'])
@login_required
@corpus_owner_or_admin_required
def update_corpus_is_public(corpus_id):
is_public = request.json
if not isinstance(is_public, bool):
response = jsonify('The request body must be a boolean')
response.status_code = 400
abort(response)
corpus = Corpus.query.get_or_404(corpus_id)
corpus.is_public = is_public
db.session.commit()
return '', 204
@bp.route('/<hashid:corpus_id>/followers/add', methods=['POST'])
@login_required
@corpus_owner_or_admin_required
def add_corpus_followers(corpus_id):
usernames = request.json
if not (isinstance(usernames, list) or all(isinstance(u, str) for u in usernames)):
response = jsonify('The request body must be a list of strings')
response.status_code = 400
abort(response)
corpus = Corpus.query.get_or_404(corpus_id)
for username in usernames:
user = User.query.filter_by(username=username, is_public=True).first_or_404()
user.follow_corpus(corpus)
db.session.commit()
return '', 204
@bp.route('/<hashid:corpus_id>/follow/<token>')
@login_required
def follow_corpus(corpus_id, token):
corpus = Corpus.query.get_or_404(corpus_id)
if current_user.follow_corpus_by_token(token):
db.session.commit()
flash(f'You are following {corpus.title} now', category='corpus')
return redirect(url_for('corpora.corpus', corpus_id=corpus_id))
abort(403)
@bp.route('/<hashid:corpus_id>/followers/<hashid:follower_id>/unfollow', methods=['POST'])
@login_required
def unfollow_corpus(corpus_id, follower_id):
corpus = Corpus.query.get_or_404(corpus_id)
follower = User.query.get_or_404(follower_id)
if not (corpus.user == current_user or follower == current_user or current_user.is_administrator()):
abort(403)
if not follower.is_following_corpus(corpus):
abort(409) # 'User is not following the corpus'
follower.unfollow_corpus(corpus)
db.session.commit()
flash(f'{follower.username} is not following {corpus.title} anymore', category='corpus')
return '', 204
@bp.route('/<hashid:corpus_id>/followers/<hashid:follower_id>/role', methods=['POST'])
@corpus_follower_permission_required('UPDATE_FOLLOWER')
def add_permission(corpus_id, follower_id):
corpus_follower_association = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=follower_id).first_or_404()
if not (corpus_follower_association.corpus.user == current_user or current_user.is_administrator()):
abort(403)
role_name = request.json.get('role')
if role_name is None:
abort(400)
corpus_follower_role = CorpusFollowerRole.query.filter_by(name=role_name).first_or_404()
corpus_follower_association.role = corpus_follower_role
db.session.commit()
return '', 204
@bp.route('/public')
@login_required
def public_corpora():
corpora = [
c.to_json_serializeable()
for c in Corpus.query.filter(Corpus.is_public == True).all()
]
return render_template(
'corpora/public_corpora.html.j2',
corpora=corpora,
title='Corpora'
)
@bp.route('/create', methods=['GET', 'POST']) @bp.route('/create', methods=['GET', 'POST'])
@login_required @login_required
@ -154,6 +59,24 @@ def create_corpus():
) )
@bp.route('/public')
@login_required
def public_corpora():
corpora = [
c.to_json_serializeable()
for c in Corpus.query.filter(Corpus.is_public == True).all()
]
return render_template(
'corpora/public_corpora.html.j2',
corpora=corpora,
title='Corpora'
)
##############################################################################
# Corpus #
##############################################################################
#region corpus
@bp.route('/<hashid:corpus_id>') @bp.route('/<hashid:corpus_id>')
@login_required @login_required
def corpus(corpus_id): def corpus(corpus_id):
@ -181,6 +104,18 @@ def corpus(corpus_id):
abort(403) abort(403)
@bp.route('/<hashid:corpus_id>/analyse')
@login_required
@corpus_follower_permission_required('VIEW')
def analyse_corpus(corpus_id):
corpus = Corpus.query.get_or_404(corpus_id)
return render_template(
'corpora/analyse_corpus.html.j2',
corpus=corpus,
title=f'Analyse Corpus {corpus.title}'
)
@bp.route('/<hashid:corpus_id>/generate-corpus-share-link', methods=['GET', 'POST']) @bp.route('/<hashid:corpus_id>/generate-corpus-share-link', methods=['GET', 'POST'])
@login_required @login_required
@corpus_follower_permission_required('GENERATE_SHARE_LINK') @corpus_follower_permission_required('GENERATE_SHARE_LINK')
@ -195,9 +130,34 @@ def generate_corpus_share_link(corpus_id):
return link return link
@bp.route('/<hashid:corpus_id>/follow/<token>')
@login_required
def follow_corpus(corpus_id, token):
corpus = Corpus.query.get_or_404(corpus_id)
if current_user.follow_corpus_by_token(token):
db.session.commit()
flash(f'You are following {corpus.title} now', category='corpus')
return redirect(url_for('corpora.corpus', corpus_id=corpus_id))
abort(403)
@bp.route('/import', methods=['GET', 'POST'])
@login_required
def import_corpus():
abort(503)
@bp.route('/<hashid:corpus_id>/export')
@login_required
def export_corpus(corpus_id):
abort(503)
#region json-routes
@bp.route('/<hashid:corpus_id>', methods=['DELETE']) @bp.route('/<hashid:corpus_id>', methods=['DELETE'])
@login_required @login_required
@corpus_owner_or_admin_required @corpus_owner_or_admin_required
@content_negotiation(produces='application/json')
def delete_corpus(corpus_id): def delete_corpus(corpus_id):
def _delete_corpus(app, corpus_id): def _delete_corpus(app, corpus_id):
with app.app_context(): with app.app_context():
@ -208,27 +168,22 @@ def delete_corpus(corpus_id):
corpus = Corpus.query.get_or_404(corpus_id) corpus = Corpus.query.get_or_404(corpus_id)
thread = Thread( thread = Thread(
target=_delete_corpus, target=_delete_corpus,
args=(current_app._get_current_object(), corpus_id) args=(current_app._get_current_object(), corpus.id)
) )
thread.start() thread.start()
return {}, 202 response_data = {
'message': f'Corpus "{corpus.title}" marked for deletion',
'category': 'corpus'
@bp.route('/<hashid:corpus_id>/analyse') }
@login_required response = jsonify(response_data)
@corpus_follower_permission_required('VIEW') response.status_code = 200
def analyse_corpus(corpus_id): return response
corpus = Corpus.query.get_or_404(corpus_id)
return render_template(
'corpora/analyse_corpus.html.j2',
corpus=corpus,
title=f'Analyse Corpus {corpus.title}'
)
@bp.route('/<hashid:corpus_id>/build', methods=['POST']) @bp.route('/<hashid:corpus_id>/build', methods=['POST'])
@login_required @login_required
@corpus_owner_or_admin_required @corpus_owner_or_admin_required
@content_negotiation(produces='application/json')
def build_corpus(corpus_id): def build_corpus(corpus_id):
def _build_corpus(app, corpus_id): def _build_corpus(app, corpus_id):
with app.app_context(): with app.app_context():
@ -239,18 +194,51 @@ def build_corpus(corpus_id):
corpus = Corpus.query.get_or_404(corpus_id) corpus = Corpus.query.get_or_404(corpus_id)
if not (corpus.user == current_user or current_user.is_administrator()): if not (corpus.user == current_user or current_user.is_administrator()):
abort(403) abort(403)
# Check if the corpus has corpus files if len(corpus.files.all()) == 0:
if not corpus.files.all(): abort(409)
response = {'errors': {'message': 'Corpus file(s) required'}}
return response, 409
thread = Thread( thread = Thread(
target=_build_corpus, target=_build_corpus,
args=(current_app._get_current_object(), corpus_id) args=(current_app._get_current_object(), corpus_id)
) )
thread.start() thread.start()
return {}, 202 response_data = {
'message': f'Corpus "{corpus.title}" marked for building',
'category': 'corpus'
}
response = jsonify(response_data)
response.status_code = 202
return response
@bp.route('/<hashid:corpus_id>/is_public', methods=['PUT'])
@login_required
@corpus_owner_or_admin_required
@content_negotiation(consumes='application/json', produces='application/json')
def update_corpus_is_public(corpus_id):
is_public = request.json
if not isinstance(is_public, bool):
abort(400)
corpus = Corpus.query.get_or_404(corpus_id)
corpus.is_public = is_public
db.session.commit()
response_data = {
'message': (
f'Corpus "{corpus.title}" is now'
f' {"public" if is_public else "private"}'
),
'category': 'corpus'
}
response = jsonify(response_data)
response.status_code = 200
return response
#endregion json-routes
#endregion corpus
##############################################################################
# Corpus/Files #
##############################################################################
#region files
@bp.route('/<hashid:corpus_id>/files/create', methods=['GET', 'POST']) @bp.route('/<hashid:corpus_id>/files/create', methods=['GET', 'POST'])
@login_required @login_required
@corpus_follower_permission_required('ADD_CORPUS_FILE') @corpus_follower_permission_required('ADD_CORPUS_FILE')
@ -301,7 +289,7 @@ def create_corpus_file(corpus_id):
@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>', methods=['GET', 'POST']) @bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>', methods=['GET', 'POST'])
@login_required @login_required
@corpus_follower_permission_required('ADD_CORPUS_FILE', 'UPDATE_CORPUS_FILE', 'REMOVE_CORPUS_FILE') @corpus_follower_permission_required('UPDATE_CORPUS_FILE')
def corpus_file(corpus_id, corpus_file_id): def corpus_file(corpus_id, corpus_file_id):
corpus_file = CorpusFile.query.filter_by(corpus_id = corpus_id, id=corpus_file_id).first_or_404() corpus_file = CorpusFile.query.filter_by(corpus_id = corpus_id, id=corpus_file_id).first_or_404()
form = UpdateCorpusFileForm(data=corpus_file.to_json_serializeable()) form = UpdateCorpusFileForm(data=corpus_file.to_json_serializeable())
@ -322,27 +310,6 @@ def corpus_file(corpus_id, corpus_file_id):
) )
@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>', methods=['DELETE'])
@login_required
@corpus_follower_permission_required('REMOVE_CORPUS_FILE')
def delete_corpus_file(corpus_id, corpus_file_id):
def _delete_corpus_file(app, corpus_file_id):
with app.app_context():
corpus_file = CorpusFile.query.get(corpus_file_id)
corpus_file.delete()
db.session.commit()
corpus_file = CorpusFile.query.filter_by(corpus_id = corpus_id, id=corpus_file_id).first_or_404()
if not (corpus_file.corpus.user == current_user or current_user.is_administrator()):
abort(403)
thread = Thread(
target=_delete_corpus_file,
args=(current_app._get_current_object(), corpus_file_id)
)
thread.start()
return {}, 202
@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>/download') @bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>/download')
@login_required @login_required
@corpus_follower_permission_required('VIEW') @corpus_follower_permission_required('VIEW')
@ -359,13 +326,95 @@ def download_corpus_file(corpus_id, corpus_file_id):
) )
@bp.route('/import', methods=['GET', 'POST']) #region json-routes
@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>', methods=['DELETE'])
@login_required @login_required
def import_corpus(): @corpus_follower_permission_required('REMOVE_CORPUS_FILE')
abort(503) @content_negotiation(produces='application/json')
def delete_corpus_file(corpus_id, corpus_file_id):
def _delete_corpus_file(app, corpus_file_id):
with app.app_context():
corpus_file = CorpusFile.query.get(corpus_file_id)
corpus_file.delete()
db.session.commit()
corpus_file = CorpusFile.query.filter_by(corpus_id=corpus_id, id=corpus_file_id).first_or_404()
thread = Thread(
target=_delete_corpus_file,
args=(current_app._get_current_object(), corpus_file.id)
)
thread.start()
return {}, 202
#endregion json-routes
#endregion files
##############################################################################
# Corpus/Followers #
##############################################################################
#region followers
#region json-routes
@bp.route('/<hashid:corpus_id>/followers', methods=['POST'])
@login_required
@corpus_owner_or_admin_required
@content_negotiation(consumes='application/json', produces='application/json')
def add_corpus_followers(corpus_id):
usernames = request.json
if not (isinstance(usernames, list) or all(isinstance(u, str) for u in usernames)):
abort(400)
corpus = Corpus.query.get_or_404(corpus_id)
for username in usernames:
user = User.query.filter_by(username=username, is_public=True).first_or_404()
user.follow_corpus(corpus)
db.session.commit()
resonse_data = {
'message': f'Users are now following "{corpus.title}"',
'category': 'corpus'
}
response = jsonify(resonse_data)
response.status_code = 200
return response
@bp.route('/<hashid:corpus_id>/export') @bp.route('/<hashid:corpus_id>/followers/<hashid:follower_id>', methods=['DELETE'])
@login_required @login_required
def export_corpus(corpus_id): @content_negotiation(produces='application/json')
abort(503) def unfollow_corpus(corpus_id, follower_id):
corpus = Corpus.query.get_or_404(corpus_id)
follower = User.query.get_or_404(follower_id)
if not (corpus.user == current_user or follower == current_user or current_user.is_administrator()):
abort(403)
if not follower.is_following_corpus(corpus):
abort(409) # 'User is not following the corpus'
follower.unfollow_corpus(corpus)
db.session.commit()
response_data = {
'message': \
f'"{follower.username}" is not following "{corpus.title}" anymore',
'category': 'corpus'
}
response = jsonify(response_data)
response.status_code = 200
return response
@bp.route('/<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')
def add_permission(corpus_id, follower_id):
role_name = request.json
if not isinstance(role_name, str):
abort(400)
cfr = CorpusFollowerRole.query.filter_by(name=role_name).first_or_404()
cfa = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=follower_id).first_or_404()
cfa.role = cfr
db.session.commit()
resonse_data = {
'message': f'User "{cfa.follower.username}" is now {cfa.role.name}',
'category': 'corpus'
}
response = jsonify(resonse_data)
response.status_code = 200
return response
#endregion json-routes
#endregion followers

View File

@ -1,7 +1,9 @@
from flask import abort, current_app from flask import abort, current_app, request
from flask_login import current_user from flask_login import current_user
from functools import wraps from functools import wraps
from threading import Thread from threading import Thread
from typing import List, Union
from werkzeug.exceptions import NotAcceptable
from app.models import Permission from app.models import Permission
@ -61,3 +63,37 @@ def background(f):
thread.start() thread.start()
return thread return thread
return wrapped return wrapped
def content_negotiation(
produces: Union[str, List[str], None] = None,
consumes: Union[str, List[str], None] = None
):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
provided = request.mimetype
if consumes is None:
consumeables = None
elif isinstance(consumes, str):
consumeables = {consumes}
elif isinstance(consumes, list) and all(isinstance(x, str) for x in consumes):
consumeables = {*consumes}
else:
raise TypeError()
accepted = {*request.accept_mimetypes.values()}
if produces is None:
produceables = None
elif isinstance(produces, str):
produceables = {produces}
elif isinstance(produces, list) and all(isinstance(x, str) for x in produces):
produceables = {*produces}
else:
raise TypeError()
if produceables is not None and len(produceables & accepted) == 0:
raise NotAcceptable()
if consumeables is not None and provided not in consumeables:
raise NotAcceptable()
return f(*args, **kwargs)
return decorated_function
return decorator

View File

View File

@ -0,0 +1,37 @@
Requests = {};
Requests.JSONfetch = (input, init={}) => {
return new Promise((resolve, reject) => {
let fixedInit = {};
fixedInit.headers = {};
fixedInit.headers['Accept'] = 'application/json';
if (init.hasOwnProperty('body')) {
fixedInit.headers['Content-Type'] = 'application/json';
}
fetch(input, Utils.mergeObjectsDeep(init, fixedInit))
.then(
(response) => {
response.json()
.then(
(json) => {
let message = json.message || json;
let category = json.category || 'message';
app.flash(message, category);
},
(error) => {
app.flash(`[${response.status}]: ${response.statusText}`, 'error');
}
);
if (response.ok) {
resolve(response);
} else {
reject(response);
}
},
(response) => {
app.flash('Something went wrong', 'error');
reject(response);
}
);
});
};

View File

@ -0,0 +1,5 @@
/*****************************************************************************
* Contributions *
* Fetch requests for /contributions routes *
*****************************************************************************/
Requests.contributions = {};

View File

@ -0,0 +1,26 @@
/*****************************************************************************
* SpaCy NLP Pipeline Models *
* Fetch requests for /contributions/spacy-nlp-pipeline-models routes *
*****************************************************************************/
Requests.contributions.spacy_nlp_pipeline_models = {};
Requests.contributions.spacy_nlp_pipeline_models.entity = {};
Requests.contributions.spacy_nlp_pipeline_models.entity.delete = (spacyNlpPipelineModelId) => {
let input = `/contributions/spacy-nlp-pipeline-models/${spacyNlpPipelineModelId}`;
let init = {
method: 'DELETE'
};
return Requests.JSONfetch(input, init);
};
Requests.contributions.spacy_nlp_pipeline_models.entity.isPublic = {};
Requests.contributions.spacy_nlp_pipeline_models.entity.isPublic.update = (spacyNlpPipelineModelId, value) => {
let input = `/contributions/spacy-nlp-pipeline-models/${spacyNlpPipelineModelId}/is_public`;
let init = {
method: 'PUT',
body: JSON.stringify(value)
};
return Requests.JSONfetch(input, init);
};

View File

@ -0,0 +1,26 @@
/*****************************************************************************
* Tesseract OCR Pipeline Models *
* Fetch requests for /contributions/tesseract-ocr-pipeline-models routes *
*****************************************************************************/
Requests.contributions.tesseract_ocr_pipeline_models = {};
Requests.contributions.tesseract_ocr_pipeline_models.entity = {};
Requests.contributions.tesseract_ocr_pipeline_models.entity.delete = (tesseractOcrPipelineModelId) => {
let input = `/contributions/tesseract-ocr-pipeline-models/${tesseractOcrPipelineModelId}`;
let init = {
method: 'DELETE'
};
return Requests.JSONfetch(input, init);
};
Requests.contributions.tesseract_ocr_pipeline_models.entity.isPublic = {};
Requests.contributions.tesseract_ocr_pipeline_models.entity.isPublic.update = (tesseractOcrPipelineModelId, value) => {
let input = `/contributions/tesseract-ocr-pipeline-models/${tesseractOcrPipelineModelId}/is_public`;
let init = {
method: 'PUT',
body: JSON.stringify(value)
};
return Requests.JSONfetch(input, init);
};

View File

@ -0,0 +1,78 @@
/*****************************************************************************
* Corpora *
* Fetch requests for /corpora routes *
*****************************************************************************/
Requests.corpora = {};
Requests.corpora.ent = {};
Requests.corpora.ent.delete = (corpusId) => {
let input = `/corpora/${corpusId}`;
let init = {
method: 'DELETE'
};
return Requests.JSONfetch(input, init);
};
Requests.corpora.ent.build = (corpusId) => {
let input = `/corpora/${corpusId}/build`;
let init = {
method: 'POST',
};
return Requests.JSONfetch(input, init);
};
Requests.corpora.ent.isPublic = {};
Requests.corpora.ent.isPublic.update = (corpusId, value) => {
let input = `/corpora/${corpusId}/is_public`;
let init = {
method: 'PUT',
body: JSON.stringify(value)
};
return Requests.JSONfetch(input, init);
};
Requests.corpora.ent.files = {};
Requests.corpora.ent.files.ent = {};
Requests.corpora.ent.files.ent.delete = (corpusId, corpusFileId) => {
let input = `/corpora/${corpusId}/files/${corpusFileId}`;
let init = {
method: 'DELETE',
};
return Requests.JSONfetch(input, init);
};
Requests.corpora.ent.followers = {};
Requests.corpora.ent.followers.add = (corpusId, usernames) => {
let input = `/corpora/${corpusId}/followers`;
let init = {
method: 'POST',
body: JSON.stringify(usernames)
};
return Requests.JSONfetch(input, init);
};
Requests.corpora.ent.followers.ent = {};
Requests.corpora.ent.followers.ent.delete = (corpusId, followerId) => {
let input = `/corpora/${corpusId}/followers/${followerId}`;
let init = {
method: 'DELETE',
};
return Requests.JSONfetch(input, init);
};
Requests.corpora.ent.followers.ent.role = {};
Requests.corpora.ent.followers.ent.role.update = (corpusId, followerId, value) => {
let input = `/corpora/${corpusId}/followers/${followerId}/role`;
let init = {
method: 'PUT',
body: JSON.stringify(value)
};
return Requests.JSONfetch(input, init);
};

View File

@ -1,16 +1,11 @@
class CorpusDisplay extends RessourceDisplay { class CorpusDisplay extends ResourceDisplay {
constructor(displayElement) { constructor(displayElement) {
super(displayElement); super(displayElement);
this.corpusId = displayElement.dataset.corpusId; this.corpusId = displayElement.dataset.corpusId;
this.displayElement this.displayElement
.querySelector('.action-button[data-action="build-request"]') .querySelector('.action-button[data-action="build-request"]')
.addEventListener('click', (event) => { .addEventListener('click', (event) => {
Utils.buildCorpusRequest(this.userId, this.corpusId); Requests.corpora.corpus.build(this.corpusId);
});
this.displayElement
.querySelector('.action-button[data-action="delete-request"]')
.addEventListener('click', (event) => {
Utils.deleteCorpusRequest(this.userId, this.corpusId);
}); });
} }

View File

@ -1,4 +1,4 @@
class JobDisplay extends RessourceDisplay { class JobDisplay extends ResourceDisplay {
constructor(displayElement) { constructor(displayElement) {
super(displayElement); super(displayElement);
this.jobId = this.displayElement.dataset.jobId; this.jobId = this.displayElement.dataset.jobId;

View File

@ -1,4 +1,4 @@
class RessourceDisplay { class ResourceDisplay {
constructor(displayElement) { constructor(displayElement) {
this.displayElement = displayElement; this.displayElement = displayElement;
this.userId = this.displayElement.dataset.userId; this.userId = this.displayElement.dataset.userId;

View File

@ -119,7 +119,11 @@ class SpaCyNLPPipelineModelList extends ResourceList {
let listAction = listActionElement.dataset.listAction; let listAction = listActionElement.dataset.listAction;
switch (listAction) { switch (listAction) {
case 'toggle-is-public': { case 'toggle-is-public': {
Utils.spaCyNLPPipelineModelToggleIsPublicRequest(this.userId, itemId); let newIsPublicValue = listActionElement.checked;
Requests.contributions.spacy_nlp_pipeline_models.entity.isPublic.update(itemId, newIsPublicValue)
.catch((response) => {
listActionElement.checked = !newIsPublicValue;
});
break; break;
} }
default: { default: {
@ -137,7 +141,37 @@ class SpaCyNLPPipelineModelList extends ResourceList {
let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction; let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction;
switch (listAction) { switch (listAction) {
case 'delete-request': { case 'delete-request': {
Utils.deleteSpaCyNLPPipelineModelRequest(this.userId, itemId); let values = this.listjs.get('id', itemId)[0].values();
let modalElement = Utils.HTMLToElement(
`
<div class="modal">
<div class="modal-content">
<h4>Confirm SpaCy NLP Pipeline Model deletion</h4>
<p>Do you really want to delete the SpaCy NLP Pipeline Model <b>${values.title}</b>? All files will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a class="btn modal-close waves-effect waves-light">Cancel</a>
<a class="action-button btn modal-close red waves-effect waves-light" data-action="confirm">Delete</a>
</div>
</div>
`
);
document.querySelector('#modals').appendChild(modalElement);
let modal = M.Modal.init(
modalElement,
{
dismissible: false,
onCloseEnd: () => {
modal.destroy();
modalElement.remove();
}
}
);
let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]');
confirmElement.addEventListener('click', (event) => {
Requests.contributions.spacy_nlp_pipeline_models.entity.delete(itemId);
});
modal.open();
break; break;
} }
case 'view': { case 'view': {

View File

@ -128,7 +128,11 @@ class TesseractOCRPipelineModelList extends ResourceList {
let listAction = listActionElement.dataset.listAction; let listAction = listActionElement.dataset.listAction;
switch (listAction) { switch (listAction) {
case 'toggle-is-public': { case 'toggle-is-public': {
Utils.tesseractOCRPipelineModelToggleIsPublicRequest(this.userId, itemId); let newIsPublicValue = listActionElement.checked;
Requests.contributions.tesseract_ocr_pipeline_models.entity.isPublic.update(itemId, newIsPublicValue)
.catch((response) => {
listActionElement.checked = !newIsPublicValue;
});
break; break;
} }
default: { default: {
@ -151,7 +155,37 @@ class TesseractOCRPipelineModelList extends ResourceList {
let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction; let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction;
switch (listAction) { switch (listAction) {
case 'delete-request': { case 'delete-request': {
Utils.deleteTesseractOCRPipelineModelRequest(this.userId, itemId); let values = this.listjs.get('id', itemId)[0].values();
let modalElement = Utils.HTMLToElement(
`
<div class="modal">
<div class="modal-content">
<h4>Confirm Tesseract OCR Pipeline Model deletion</h4>
<p>Do you really want to delete the Tesseract OCR Pipeline Model <b>${values.title}</b>? All files will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a class="btn modal-close waves-effect waves-light">Cancel</a>
<a class="action-button btn modal-close red waves-effect waves-light" data-action="confirm">Delete</a>
</div>
</div>
`
);
document.querySelector('#modals').appendChild(modalElement);
let modal = M.Modal.init(
modalElement,
{
dismissible: false,
onCloseEnd: () => {
modal.destroy();
modalElement.remove();
}
}
);
let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]');
confirmElement.addEventListener('click', (event) => {
Requests.contributions.tesseract_ocr_pipeline_models.entity.delete(itemId);
});
modal.open();
break; break;
} }
case 'view': { case 'view': {

View File

@ -101,13 +101,21 @@ class Utils {
static updateCorpusFollowerRole(corpusId, followerId, roleName) { static updateCorpusFollowerRole(corpusId, followerId, roleName) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fetch(`/corpora/${corpusId}/followers/${followerId}/role`, {method: 'POST', headers: {Accept: 'application/json', 'Content-Type': 'application/json'}, body: JSON.stringify({role: roleName})}) let fetchRessource = `/corpora/${corpusId}/followers/${followerId}/role`;
let fetchOptions = {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({role: roleName})
};
fetch(fetchRessource, fetchOptions)
.then( .then(
(response) => { (response) => {
if (response.ok) { if (response.ok) {
app.flash('Role updated', 'corpus'); app.flash('Role updated', 'corpus');
resolve(response); resolve(response);
return;
} else { } else {
app.flash(`${response.statusText}`, 'error'); app.flash(`${response.statusText}`, 'error');
reject(response); reject(response);
@ -179,7 +187,15 @@ class Utils {
static unfollowCorpusRequest(corpusId, followerId) { static unfollowCorpusRequest(corpusId, followerId) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fetch(`/corpora/${corpusId}/followers/${followerId}/unfollow`, {method: 'POST', headers: {Accept: 'application/json'}}) let fetchRessource = `/corpora/${corpusId}/followers/${followerId}/unfollow`;
let fetchOptions = {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
};
fetch(fetchRessource, fetchOptions)
.then( .then(
(response) => { (response) => {
if (response.ok) { if (response.ok) {
@ -683,23 +699,27 @@ class Utils {
}); });
} }
static tesseractOCRPipelineModelToggleIsPublicRequest(userId, tesseractOCRPipelineModelId, is_public) { static updateTesseractOCRPipelineModelIsPublicRequest(tesseractOCRPipelineModelId, newIsPublicValue) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let tesseractOCRPipelineModel; let fetchRessource = `/contributions/tesseract-ocr-pipeline-models/${tesseractOCRPipelineModelId}/is_public`;
try { let fetchOptions = {
tesseractOCRPipelineModel = app.data.users[userId].tesseract_ocr_pipeline_models[tesseractOCRPipelineModelId]; method: 'PUT',
} catch (error) { headers: {
tesseractOCRPipelineModel = {}; 'Accept': 'application/json',
} 'Content-Type': 'application/json'
},
fetch(`/contributions/tesseract-ocr-pipeline-models/${tesseractOCRPipelineModelId}/toggle-public-status`, {method: 'POST', headers: {Accept: 'application/json'}}) body: JSON.stringify(newIsPublicValue)
};
fetch(fetchRessource, fetchOptions)
.then( .then(
(response) => { (response) => {
if (response.status === 403) { if (response.ok) {
app.flash('Forbidden', 'error'); response.json().then((data) => {app.flash(data);});
resolve(response);
} else {
app.flash(`${response.statusText}`, 'error');
reject(response); reject(response);
} }
resolve(response);
}, },
(response) => { (response) => {
app.flash('Something went wrong', 'error'); app.flash('Something went wrong', 'error');
@ -709,23 +729,27 @@ class Utils {
}); });
} }
static spaCyNLPPipelineModelToggleIsPublicRequest(userId, spaCyNLPPipelineModelId) { static updateSpaCyNLPPipelineModelIsPublicRequest(SpaCyNLPPipelineModelId, newIsPublicValue) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let spaCyNLPPipelineModel; let fetchRessource = `/contributions/spacy-nlp-pipeline-models/${SpaCyNLPPipelineModelId}/is_public`;
try { let fetchOptions = {
spaCyNLPPipelineModel = app.data.users[userId].spacy_nlp_pipeline_models[spaCyNLPPipelineModelId]; method: 'PUT',
} catch (error) { headers: {
spaCyNLPPipelineModel = {}; 'Accept': 'application/json',
} 'Content-Type': 'application/json'
},
fetch(`/contributions/spacy-nlp-pipeline-models/${spaCyNLPPipelineModelId}/toggle-public-status`, {method: 'POST', headers: {Accept: 'application/json'}}) body: JSON.stringify(newIsPublicValue)
};
fetch(fetchRessource, fetchOptions)
.then( .then(
(response) => { (response) => {
if (response.status === 403) { if (response.ok) {
app.flash('Forbidden', 'error'); response.json().then((data) => {app.flash(data);});
resolve(response);
} else {
app.flash(`${response.statusText}`, 'error');
reject(response); reject(response);
} }
resolve(response);
}, },
(response) => { (response) => {
app.flash('Something went wrong', 'error'); app.flash('Something went wrong', 'error');

View File

@ -6,18 +6,37 @@
output='gen/app.%(version)s.js', output='gen/app.%(version)s.js',
'js/App.js', 'js/App.js',
'js/Utils.js', 'js/Utils.js',
'js/Forms/Form.js',
'js/Forms/CreateCorpusFileForm.js',
'js/Forms/CreateJobForm.js',
'js/Forms/CreateContributionForm.js',
'js/CorpusAnalysis/CQiClient.js', 'js/CorpusAnalysis/CQiClient.js',
'js/CorpusAnalysis/CorpusAnalysisApp.js', 'js/CorpusAnalysis/CorpusAnalysisApp.js',
'js/CorpusAnalysis/CorpusAnalysisConcordance.js', 'js/CorpusAnalysis/CorpusAnalysisConcordance.js',
'js/CorpusAnalysis/CorpusAnalysisReader.js', 'js/CorpusAnalysis/CorpusAnalysisReader.js',
'js/CorpusAnalysis/QueryBuilder.js', 'js/CorpusAnalysis/QueryBuilder.js',
'js/RessourceDisplays/RessourceDisplay.js', 'js/XMLtoObject.js'
'js/RessourceDisplays/CorpusDisplay.js', %}
'js/RessourceDisplays/JobDisplay.js', <script src="{{ ASSET_URL }}"></script>
{%- endassets %}
{%- assets
filters='rjsmin',
output='gen/Forms.%(version)s.js',
'js/Forms/Form.js',
'js/Forms/CreateCorpusFileForm.js',
'js/Forms/CreateJobForm.js',
'js/Forms/CreateContributionForm.js'
%}
<script src="{{ ASSET_URL }}"></script>
{%- endassets %}
{%- assets
filters='rjsmin',
output='gen/ResourceDisplays.%(version)s.js',
'js/ResourceDisplays/ResourceDisplay.js',
'js/ResourceDisplays/CorpusDisplay.js',
'js/ResourceDisplays/JobDisplay.js'
%}
<script src="{{ ASSET_URL }}"></script>
{%- endassets %}
{%- assets
filters='rjsmin',
output='gen/ResourceLists.%(version)s.js',
'js/ResourceLists/ResourceList.js', 'js/ResourceLists/ResourceList.js',
'js/ResourceLists/CorpusFileList.js', 'js/ResourceLists/CorpusFileList.js',
'js/ResourceLists/PublicCorpusFileList.js', 'js/ResourceLists/PublicCorpusFileList.js',
@ -31,8 +50,18 @@
'js/ResourceLists/TesseractOCRPipelineModelList.js', 'js/ResourceLists/TesseractOCRPipelineModelList.js',
'js/ResourceLists/UserList.js', 'js/ResourceLists/UserList.js',
'js/ResourceLists/AdminUserList.js', 'js/ResourceLists/AdminUserList.js',
'js/ResourceLists/CorpusFollowerList.js', 'js/ResourceLists/CorpusFollowerList.js'
'js/XMLtoObject.js' %}
<script src="{{ ASSET_URL }}"></script>
{%- endassets %}
{%- assets
filters='rjsmin',
output='gen/Requests.%(version)s.js',
'js/Requests/Requests.js',
'js/Requests/contributions/contributions.js',
'js/Requests/contributions/spacy_nlp_pipeline_models.js',
'js/Requests/contributions/tesseract_ocr_pipeline_models.js',
'js/Requests/Corpora.js'
%} %}
<script src="{{ ASSET_URL }}"></script> <script src="{{ ASSET_URL }}"></script>
{%- endassets %} {%- endassets %}

View File

@ -1,6 +1,5 @@
{% extends "base.html.j2" %} {% extends "base.html.j2" %}
{% import "materialize/wtf.html.j2" as wtf %} {% import "materialize/wtf.html.j2" as wtf %}
{% from "contributions/_breadcrumbs.html.j2" import breadcrumbs with context %}
{% block page_content %} {% block page_content %}
<div class="container"> <div class="container">
@ -11,7 +10,7 @@
<div class="col s4"> <div class="col s4">
<div class="card extension-selector hoverable service-color" data-service="tesseract-ocr-pipeline"> <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> <a href="{{ url_for('.tesseract_ocr_pipeline_models.tesseract_ocr_pipeline_models') }}" style="position: absolute; width: 100%; height: 100%;"></a>
<div class="card-content"> <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"><i class="nopaque-icons service-icons" data-service="tesseract-ocr-pipeline"></i>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> <p>Here you can see and edit the models that you have created. You can also create new models.</p>
@ -21,7 +20,7 @@
<div class="col s4"> <div class="col s4">
<div class="card extension-selector hoverable service-color" data-service="spacy-nlp-pipeline"> <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> <a href="{{ url_for('.spacy_nlp_pipeline_models.spacy_nlp_pipeline_models') }}" style="position: absolute; width: 100%; height: 100%;"></a>
<div class="card-content"> <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"><i class="nopaque-icons service-icons" data-service="spacy-nlp-pipeline"></i>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> <p>Here you can see and edit the models that you have created. You can also create new models.</p>

View File

@ -1,6 +1,5 @@
{% extends "base.html.j2" %} {% extends "base.html.j2" %}
{% import "materialize/wtf.html.j2" as wtf %} {% import "materialize/wtf.html.j2" as wtf %}
{% from "contributions/_breadcrumbs.html.j2" import breadcrumbs with context %}
{% block main_attribs %} class="service-scheme" data-service="spacy-nlp-pipeline"{% endblock main_attribs %} {% block main_attribs %} class="service-scheme" data-service="spacy-nlp-pipeline"{% endblock main_attribs %}

View File

@ -1,6 +1,5 @@
{% extends "base.html.j2" %} {% extends "base.html.j2" %}
{% import "materialize/wtf.html.j2" as wtf %} {% import "materialize/wtf.html.j2" as wtf %}
{% from "contributions/_breadcrumbs.html.j2" import breadcrumbs with context %}
{% block main_attribs %} class="service-scheme" data-service="spacy-nlp-pipeline"{% endblock main_attribs %} {% block main_attribs %} class="service-scheme" data-service="spacy-nlp-pipeline"{% endblock main_attribs %}

View File

@ -1,6 +1,5 @@
{% extends "base.html.j2" %} {% extends "base.html.j2" %}
{% import "materialize/wtf.html.j2" as wtf %} {% import "materialize/wtf.html.j2" as wtf %}
{% from "contributions/_breadcrumbs.html.j2" import breadcrumbs with context %}
{% block page_content %} {% block page_content %}
<div class="container"> <div class="container">

View File

@ -1,6 +1,5 @@
{% extends "base.html.j2" %} {% extends "base.html.j2" %}
{% import "materialize/wtf.html.j2" as wtf %} {% import "materialize/wtf.html.j2" as wtf %}
{% from "contributions/_breadcrumbs.html.j2" import breadcrumbs with context %}
{% block main_attribs %} class="service-scheme" data-service="tesseract-ocr-pipeline"{% endblock main_attribs %} {% block main_attribs %} class="service-scheme" data-service="tesseract-ocr-pipeline"{% endblock main_attribs %}

View File

@ -1,6 +1,5 @@
{% extends "base.html.j2" %} {% extends "base.html.j2" %}
{% import "materialize/wtf.html.j2" as wtf %} {% import "materialize/wtf.html.j2" as wtf %}
{% from "contributions/_breadcrumbs.html.j2" import breadcrumbs with context %}
{% block main_attribs %} class="service-scheme" data-service="tesseract-ocr-pipeline"{% endblock main_attribs %} {% block main_attribs %} class="service-scheme" data-service="tesseract-ocr-pipeline"{% endblock main_attribs %}

View File

@ -1,6 +1,5 @@
{% extends "base.html.j2" %} {% extends "base.html.j2" %}
{% import "materialize/wtf.html.j2" as wtf %} {% import "materialize/wtf.html.j2" as wtf %}
{% from "contributions/_breadcrumbs.html.j2" import breadcrumbs with context %}
{% block page_content %} {% block page_content %}
<div class="container"> <div class="container">

View File

@ -68,7 +68,7 @@
<a class="btn waves-effect waves-light modal-trigger" href="#publishing-modal" style="width: 100%;"><i class="material-icons left">publish</i>Publishing</a> <a class="btn waves-effect waves-light modal-trigger" href="#publishing-modal" style="width: 100%;"><i class="material-icons left">publish</i>Publishing</a>
</div> </div>
<div class="col s12 l6" style="padding: 5px 2.5px 0 2.5px;"> <div class="col s12 l6" style="padding: 5px 2.5px 0 2.5px;">
<a class="action-button btn red waves-effect waves-light" data-action="delete-request" style="width: 100%;"><i class="material-icons left">delete</i>Delete</a> <a class="btn red waves-effect waves-light modal-trigger" href="#delete-modal" style="width: 100%;"><i class="material-icons left">delete</i>Delete</a>
</div> </div>
</div> </div>
<span class="card-title">Social</span> <span class="card-title">Social</span>
@ -131,6 +131,17 @@
</div> </div>
</div> </div>
<div class="modal" id="delete-modal">
<div class="modal-content">
<h4>Confirm Corpus deletion</h4>
<p>Do you really want to delete the Corpus <b>{{ corpus.title }}</b>? All files will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a class="btn modal-close waves-effect waves-light">Cancel</a>
<a class="btn modal-close red waves-effect waves-light" id="delete-modal-delete-button">Delete</a>
</div>
</div>
<div class="modal no-autoinit" id="invite-user-modal"> <div class="modal no-autoinit" id="invite-user-modal">
<div class="modal-content"> <div class="modal-content">
<h4>Invite a nopaque user by username</h4> <h4>Invite a nopaque user by username</h4>
@ -215,13 +226,21 @@
let publishingModalIsPublicSwitchElement = document.querySelector('#publishing-modal-is-public-switch'); let publishingModalIsPublicSwitchElement = document.querySelector('#publishing-modal-is-public-switch');
publishingModalIsPublicSwitchElement.addEventListener('change', (event) => { publishingModalIsPublicSwitchElement.addEventListener('change', (event) => {
let newIsPublic = publishingModalIsPublicSwitchElement.checked; let newIsPublic = publishingModalIsPublicSwitchElement.checked;
Utils.updateCorpusIsPublicRequest(corpusId, newIsPublic) Requests.corpora.corpus.isPublic.update(corpusId, newIsPublic)
.catch((response) => { .catch((response) => {
publishingModalIsPublicSwitchElement.checked = !newIsPublic; publishingModalIsPublicSwitchElement.checked = !newIsPublic;
}); });
}); });
// #endregion publishing_modal_js // #endregion publishing_modal_js
// #region delete_modal_js
let deleteModalDeleteButtonElement = document.querySelector('#delete-modal-delete-button');
deleteModalDeleteButtonElement.addEventListener('click', (event) => {
Requests.corpora.corpus.delete(corpusId)
.then((response) => {window.location.href = '/dashboard';});
});
// #endregion delete_modal_js
// #region invite_user_modal_js // #region invite_user_modal_js
let inviteUserModalElement = document.querySelector('#invite-user-modal'); let inviteUserModalElement = document.querySelector('#invite-user-modal');
let inviteUserModalSearchElement = document.querySelector('#invite-user-modal-search'); let inviteUserModalSearchElement = document.querySelector('#invite-user-modal-search');