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

This commit is contained in:
Inga Kirschnick 2023-03-10 17:03:48 +01:00
commit 7e54d56ed5
35 changed files with 686 additions and 1083 deletions

23
.vscode/settings.json vendored
View File

@ -1 +1,22 @@
{} {
"editor.rulers": [79],
"files.insertFinalNewline": true,
"[css]": {
"editor.tabSize": 2
},
"[scss]": {
"editor.tabSize": 2
},
"[html]": {
"editor.tabSize": 2
},
"[javascript]": {
"editor.tabSize": 2
},
"[jinja-html]": {
"editor.tabSize": 2
},
"[jinja-js]": {
"editor.tabSize": 2
},
}

View File

@ -54,6 +54,9 @@ def create_app(config: Config = Config) -> Flask:
scheduler.init_app(app) scheduler.init_app(app)
socketio.init_app(app, message_queue=app.config['NOPAQUE_SOCKETIO_MESSAGE_QUEUE_URI']) # noqa socketio.init_app(app, message_queue=app.config['NOPAQUE_SOCKETIO_MESSAGE_QUEUE_URI']) # noqa
from .errors import init_app as init_error_handlers
init_error_handlers(app)
from .admin import bp as admin_blueprint from .admin import bp as admin_blueprint
app.register_blueprint(admin_blueprint, url_prefix='/admin') app.register_blueprint(admin_blueprint, url_prefix='/admin')
@ -69,9 +72,6 @@ def create_app(config: Config = Config) -> Flask:
from .corpora import bp as corpora_blueprint from .corpora import bp as corpora_blueprint
app.register_blueprint(corpora_blueprint, url_prefix='/corpora') app.register_blueprint(corpora_blueprint, url_prefix='/corpora')
from .errors import bp as errors_blueprint
app.register_blueprint(errors_blueprint)
from .jobs import bp as jobs_blueprint from .jobs import bp as jobs_blueprint
app.register_blueprint(jobs_blueprint, url_prefix='/jobs') app.register_blueprint(jobs_blueprint, url_prefix='/jobs')

View File

@ -39,8 +39,6 @@ def delete_tesseract_model(tesseract_ocr_pipeline_model_id):
@permission_required('CONTRIBUTE') @permission_required('CONTRIBUTE')
@content_negotiation(consumes='application/json', produces='application/json') @content_negotiation(consumes='application/json', produces='application/json')
def update_tesseract_ocr_pipeline_model_is_public(tesseract_ocr_pipeline_model_id): def update_tesseract_ocr_pipeline_model_is_public(tesseract_ocr_pipeline_model_id):
# body: jsonify({'is_public': True})
# body: jsonify(False)
is_public = request.json is_public = request.json
if not isinstance(is_public, bool): if not isinstance(is_public, bool):
abort(400) abort(400)

View File

@ -2,4 +2,10 @@ from flask import Blueprint
bp = Blueprint('corpora', __name__) bp = Blueprint('corpora', __name__)
from . import cqi_over_socketio, routes # noqa from . import cqi_over_socketio, routes, json_routes # noqa
from .files import bp as files_bp
bp.register_blueprint(files_bp, url_prefix='<hashid:corpus_id>/files')
from .followers import bp as followers_bp
bp.register_blueprint(followers_bp, url_prefix='<hashid:corpus_id>/followers')

View File

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

View File

@ -0,0 +1,54 @@
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileRequired
from wtforms import (
StringField,
SubmitField,
ValidationError,
IntegerField
)
from wtforms.validators import InputRequired, Length
class CorpusFileBaseForm(FlaskForm):
author = StringField(
'Author',
validators=[InputRequired(), Length(max=255)]
)
publishing_year = IntegerField(
'Publishing year',
validators=[InputRequired()]
)
title = StringField(
'Title',
validators=[InputRequired(), Length(max=255)]
)
address = StringField('Adress', validators=[Length(max=255)])
booktitle = StringField('Booktitle', validators=[Length(max=255)])
chapter = StringField('Chapter', validators=[Length(max=255)])
editor = StringField('Editor', validators=[Length(max=255)])
institution = StringField('Institution', validators=[Length(max=255)])
journal = StringField('Journal', validators=[Length(max=255)])
pages = StringField('Pages', validators=[Length(max=255)])
publisher = StringField('Publisher', validators=[Length(max=255)])
school = StringField('School', validators=[Length(max=255)])
submit = SubmitField()
class CreateCorpusFileForm(CorpusFileBaseForm):
vrt = FileField('File', validators=[FileRequired()])
def __init__(self, *args, **kwargs):
if 'prefix' not in kwargs:
kwargs['prefix'] = 'create-corpus-file-form'
super().__init__(*args, **kwargs)
def validate_vrt(self, field):
if not field.data.filename.lower().endswith('.vrt'):
raise ValidationError('VRT files only!')
class UpdateCorpusFileForm(CorpusFileBaseForm):
def __init__(self, *args, **kwargs):
if 'prefix' not in kwargs:
kwargs['prefix'] = 'update-corpus-file-form'
super().__init__(*args, **kwargs)

View File

@ -0,0 +1,42 @@
from flask import current_app, jsonify
from flask_login import login_required
from threading import Thread
from app import db
from app.decorators import content_negotiation
from app.models import CorpusFile
from ..decorators import corpus_follower_permission_required
from . import bp
##############################################################################
# IMPORTANT NOTE: These routes are prefixed by the blueprint #
# Prefix: <hashid:corpus_id>/files #
# This implies that the corpus_id is always in the kwargs of #
# a route that is registered to this blueprint. #
##############################################################################
@bp.route('/<hashid:corpus_file_id>', methods=['DELETE'])
@login_required
@corpus_follower_permission_required('REMOVE_CORPUS_FILE')
@content_negotiation(produces='application/json')
def delete_corpus_file(corpus_id, corpus_file_id):
def _delete_corpus_file(app, corpus_file_id):
with app.app_context():
corpus_file = CorpusFile.query.get(corpus_file_id)
corpus_file.delete()
db.session.commit()
corpus_file = CorpusFile.query.filter_by(corpus_id=corpus_id, id=corpus_file_id).first_or_404()
thread = Thread(
target=_delete_corpus_file,
args=(current_app._get_current_object(), corpus_file.id)
)
thread.start()
response_data = {
'message': f'Corpus File "{corpus_file.title}" marked for deletion',
'category': 'corpus'
}
response = jsonify(response_data)
response.status_code = 202
return response

101
app/corpora/files/routes.py Normal file
View File

@ -0,0 +1,101 @@
from flask import (
abort,
flash,
Markup,
redirect,
render_template,
send_from_directory
)
from flask_login import current_user, login_required
import os
from app import db
from app.models import Corpus, CorpusFile, CorpusStatus
from ..decorators import corpus_follower_permission_required
from . import bp, TEMPLATE_FOLDER
from .forms import CreateCorpusFileForm, UpdateCorpusFileForm
##############################################################################
# IMPORTANT NOTE: These routes are prefixed by the blueprint #
# Prefix: <hashid:corpus_id>/files #
# This implies that the corpus_id is always in the kwargs of #
# a route that is registered to this blueprint. #
##############################################################################
@bp.route('/create', methods=['GET', 'POST'])
@login_required
@corpus_follower_permission_required('ADD_CORPUS_FILE')
def create_corpus_file(corpus_id):
corpus = Corpus.query.get_or_404(corpus_id)
form = CreateCorpusFileForm()
if form.is_submitted():
if not form.validate():
response = {'errors': form.errors}
return response, 400
try:
corpus_file = CorpusFile.create(
form.vrt.data,
address=form.address.data,
author=form.author.data,
booktitle=form.booktitle.data,
chapter=form.chapter.data,
editor=form.editor.data,
institution=form.institution.data,
journal=form.journal.data,
pages=form.pages.data,
publisher=form.publisher.data,
publishing_year=form.publishing_year.data,
school=form.school.data,
title=form.title.data,
mimetype='application/vrt+xml',
corpus=corpus
)
except (AttributeError, OSError):
abort(500)
corpus.status = CorpusStatus.UNPREPARED
db.session.commit()
flash(f'Corpus File "{corpus_file.filename}" added', category='corpus')
return '', 201, {'Location': corpus.url}
return render_template(
f'{TEMPLATE_FOLDER}/create_corpus_file.html.j2',
corpus=corpus,
form=form,
title='Add corpus file'
)
@bp.route('/<hashid:corpus_file_id>', methods=['GET', 'POST'])
@login_required
@corpus_follower_permission_required('UPDATE_CORPUS_FILE')
def corpus_file(corpus_id, corpus_file_id):
corpus_file = CorpusFile.query.filter_by(corpus_id=corpus_id, id=corpus_file_id).first_or_404()
form = UpdateCorpusFileForm(data=corpus_file.to_json_serializeable())
if form.validate_on_submit():
form.populate_obj(corpus_file)
if db.session.is_modified(corpus_file):
corpus_file.corpus.status = CorpusStatus.UNPREPARED
db.session.commit()
flash(f'Corpus file "{corpus_file.filename}" updated', category='corpus')
return redirect(corpus_file.corpus.url)
return render_template(
f'{TEMPLATE_FOLDER}/corpus_file.html.j2',
corpus=corpus_file.corpus,
corpus_file=corpus_file,
form=form,
title='Edit corpus file'
)
@bp.route('/<hashid:corpus_file_id>/download')
@login_required
@corpus_follower_permission_required('VIEW')
def download_corpus_file(corpus_id, corpus_file_id):
corpus_file = CorpusFile.query.filter_by(corpus_id=corpus_id, id=corpus_file_id).first_or_404()
return send_from_directory(
os.path.dirname(corpus_file.path),
os.path.basename(corpus_file.path),
as_attachment=True,
attachment_filename=corpus_file.filename,
mimetype=corpus_file.mimetype
)

View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('followers', __name__)
from . import json_routes

View File

@ -0,0 +1,87 @@
from flask import abort, jsonify, request
from flask_login import current_user, login_required
from app import db
from app.decorators import content_negotiation
from app.models import (
Corpus,
CorpusFollowerAssociation,
CorpusFollowerRole,
User
)
from ..decorators import corpus_owner_or_admin_required
from . import bp
##############################################################################
# IMPORTANT NOTE: These routes are prefixed by the blueprint #
# Prefix: <hashid:corpus_id>/followers #
# This implies that the corpus_id is always in the kwargs of #
# a route that is registered to this blueprint. #
##############################################################################
@bp.route('', methods=['POST'])
@login_required
@corpus_owner_or_admin_required
@content_negotiation(consumes='application/json', produces='application/json')
def create_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:follower_id>/role', methods=['PUT'])
@login_required
@corpus_owner_or_admin_required
@content_negotiation(consumes='application/json', produces='application/json')
def update_corpus_follower_role(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()
if cfr is None:
abort(400)
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
@bp.route('/<hashid:follower_id>', methods=['DELETE'])
@login_required
@content_negotiation(produces='application/json')
def delete_corpus_follower(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)
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

View File

@ -1,13 +1,5 @@
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileRequired from wtforms import StringField, SubmitField, TextAreaField
from wtforms import (
BooleanField,
StringField,
SubmitField,
TextAreaField,
ValidationError,
IntegerField
)
from wtforms.validators import InputRequired, Length from wtforms.validators import InputRequired, Length
@ -34,50 +26,5 @@ class UpdateCorpusForm(CorpusBaseForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
class CorpusFileBaseForm(FlaskForm):
author = StringField(
'Author',
validators=[InputRequired(), Length(max=255)]
)
publishing_year = IntegerField(
'Publishing year',
validators=[InputRequired()]
)
title = StringField(
'Title',
validators=[InputRequired(), Length(max=255)]
)
address = StringField('Adress', validators=[Length(max=255)])
booktitle = StringField('Booktitle', validators=[Length(max=255)])
chapter = StringField('Chapter', validators=[Length(max=255)])
editor = StringField('Editor', validators=[Length(max=255)])
institution = StringField('Institution', validators=[Length(max=255)])
journal = StringField('Journal', validators=[Length(max=255)])
pages = StringField('Pages', validators=[Length(max=255)])
publisher = StringField('Publisher', validators=[Length(max=255)])
school = StringField('School', validators=[Length(max=255)])
submit = SubmitField()
class CreateCorpusFileForm(CorpusFileBaseForm):
vrt = FileField('File', validators=[FileRequired()])
def __init__(self, *args, **kwargs):
if 'prefix' not in kwargs:
kwargs['prefix'] = 'create-corpus-file-form'
super().__init__(*args, **kwargs)
def validate_vrt(self, field):
if not field.data.filename.lower().endswith('.vrt'):
raise ValidationError('VRT files only!')
class UpdateCorpusFileForm(CorpusFileBaseForm):
def __init__(self, *args, **kwargs):
if 'prefix' not in kwargs:
kwargs['prefix'] = 'update-corpus-file-form'
super().__init__(*args, **kwargs)
class ImportCorpusForm(FlaskForm): class ImportCorpusForm(FlaskForm):
pass pass

130
app/corpora/json_routes.py Normal file
View File

@ -0,0 +1,130 @@
from datetime import datetime
from flask import (
abort,
current_app,
jsonify,
request,
url_for
)
from flask_login import current_user, login_required
from threading import Thread
from .decorators import corpus_follower_permission_required, corpus_owner_or_admin_required
from app import db, hashids
from app.decorators import content_negotiation
from app.models import Corpus, CorpusFollowerRole
from . import bp
@bp.route('/<hashid:corpus_id>', methods=['DELETE'])
@login_required
@corpus_owner_or_admin_required
@content_negotiation(produces='application/json')
def delete_corpus(corpus_id):
def _delete_corpus(app, corpus_id):
with app.app_context():
corpus = Corpus.query.get(corpus_id)
corpus.delete()
db.session.commit()
corpus = Corpus.query.get_or_404(corpus_id)
thread = Thread(
target=_delete_corpus,
args=(current_app._get_current_object(), corpus.id)
)
thread.start()
response_data = {
'message': f'Corpus "{corpus.title}" marked for deletion',
'category': 'corpus'
}
response = jsonify(response_data)
response.status_code = 200
return response
@bp.route('/<hashid:corpus_id>/build', methods=['POST'])
@login_required
@corpus_owner_or_admin_required
@content_negotiation(produces='application/json')
def build_corpus(corpus_id):
def _build_corpus(app, corpus_id):
with app.app_context():
corpus = Corpus.query.get(corpus_id)
corpus.build()
db.session.commit()
print(corpus_id)
corpus = Corpus.query.get_or_404(corpus_id)
if len(corpus.files.all()) == 0:
abort(409)
thread = Thread(
target=_build_corpus,
args=(current_app._get_current_object(), corpus_id)
)
thread.start()
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>/generate-share-link', methods=['POST'])
@login_required
@corpus_follower_permission_required('GENERATE_SHARE_LINK')
@content_negotiation(consumes='application/json', produces='application/json')
def generate_corpus_share_link(corpus_id):
corpus_hashid = hashids.encode(corpus_id)
data = request.json
if not isinstance(data, dict):
abort(400)
expiration = data.get('expiration')
if not isinstance(expiration, str):
abort(400)
role_name = data.get('role')
if not isinstance(role_name, str):
abort(400)
expiration_date = datetime.strptime(expiration, '%b %d, %Y')
cfr = CorpusFollowerRole.query.filter_by(name=role_name).first()
if cfr is None:
abort(400)
token = current_user.generate_follow_corpus_token(corpus_hashid, role_name, expiration_date)
corpus_share_link = url_for(
'corpora.follow_corpus',
corpus_id=corpus_id,
token=token,
_external=True
)
response_data = {
'message': 'Corpus share link generated',
'category': 'corpus',
'corpusShareLink': corpus_share_link
}
response = jsonify(response_data)
response.status_code = 200
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

View File

@ -1,36 +1,21 @@
from datetime import datetime
from flask import ( from flask import (
abort, abort,
current_app,
flash, flash,
jsonify,
Markup, Markup,
redirect, redirect,
render_template, render_template,
request,
send_from_directory,
url_for url_for
) )
from flask_login import current_user, login_required from flask_login import current_user, login_required
from threading import Thread from .decorators import corpus_follower_permission_required
import os from app import db
from .decorators import corpus_follower_permission_required, corpus_owner_or_admin_required
from app import db, hashids
from app.decorators import content_negotiation
from app.models import ( from app.models import (
Corpus, Corpus,
CorpusFile,
CorpusFollowerAssociation, CorpusFollowerAssociation,
CorpusFollowerRole, CorpusFollowerRole,
CorpusStatus,
User
) )
from . import bp from . import bp
from .forms import ( from .forms import CreateCorpusForm
CreateCorpusFileForm,
CreateCorpusForm,
UpdateCorpusFileForm
)
@bp.route('/create', methods=['GET', 'POST']) @bp.route('/create', methods=['GET', 'POST'])
@ -47,10 +32,7 @@ def create_corpus():
except OSError: except OSError:
abort(500) abort(500)
db.session.commit() db.session.commit()
message = Markup( flash(f'Corpus "{corpus.title}" created', 'corpus')
f'Corpus "<a href="{corpus.url}">{corpus.title}</a>" created'
)
flash(message, 'corpus')
return redirect(corpus.url) return redirect(corpus.url)
return render_template( return render_template(
'corpora/create_corpus.html.j2', 'corpora/create_corpus.html.j2',
@ -59,29 +41,12 @@ 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):
corpus = Corpus.query.get_or_404(corpus_id) corpus = Corpus.query.get_or_404(corpus_id)
corpus_follower_roles = CorpusFollowerRole.query.all() corpus_follower_roles = CorpusFollowerRole.query.all()
# TODO: Add URL query option to toggle view
if corpus.user == current_user or current_user.is_administrator(): if corpus.user == current_user or current_user.is_administrator():
return render_template( return render_template(
'corpora/corpus.html.j2', 'corpora/corpus.html.j2',
@ -122,7 +87,7 @@ def follow_corpus(corpus_id, token):
corpus = Corpus.query.get_or_404(corpus_id) corpus = Corpus.query.get_or_404(corpus_id)
if current_user.follow_corpus_by_token(token): if current_user.follow_corpus_by_token(token):
db.session.commit() db.session.commit()
flash(f'You are following {corpus.title} now', category='corpus') flash(f'You are following "{corpus.title}" now', category='corpus')
return redirect(url_for('corpora.corpus', corpus_id=corpus_id)) return redirect(url_for('corpora.corpus', corpus_id=corpus_id))
abort(403) abort(403)
@ -137,309 +102,3 @@ def import_corpus():
@login_required @login_required
def export_corpus(corpus_id): def export_corpus(corpus_id):
abort(503) abort(503)
#region json-routes
@bp.route('/<hashid:corpus_id>', methods=['DELETE'])
@login_required
@corpus_owner_or_admin_required
@content_negotiation(produces='application/json')
def delete_corpus(corpus_id):
def _delete_corpus(app, corpus_id):
with app.app_context():
corpus = Corpus.query.get(corpus_id)
corpus.delete()
db.session.commit()
corpus = Corpus.query.get_or_404(corpus_id)
thread = Thread(
target=_delete_corpus,
args=(current_app._get_current_object(), corpus.id)
)
thread.start()
response_data = {
'message': f'Corpus "{corpus.title}" marked for deletion',
'category': 'corpus'
}
response = jsonify(response_data)
response.status_code = 200
return response
@bp.route('/<hashid:corpus_id>/build', methods=['POST'])
@login_required
@corpus_owner_or_admin_required
@content_negotiation(produces='application/json')
def build_corpus(corpus_id):
def _build_corpus(app, corpus_id):
with app.app_context():
corpus = Corpus.query.get(corpus_id)
corpus.build()
db.session.commit()
corpus = Corpus.query.get_or_404(corpus_id)
if not (corpus.user == current_user or current_user.is_administrator()):
abort(403)
if len(corpus.files.all()) == 0:
abort(409)
thread = Thread(
target=_build_corpus,
args=(current_app._get_current_object(), corpus_id)
)
thread.start()
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>/generate-corpus-share-link', methods=['POST'])
@login_required
@corpus_follower_permission_required('GENERATE_SHARE_LINK')
@content_negotiation(consumes='application/json', produces='application/json')
def generate_corpus_share_link(corpus_id):
corpus_hashid = hashids.encode(corpus_id)
data = request.json
if not isinstance(data, dict):
abort(400)
expiration = data.get('expiration')
if not isinstance(expiration, str):
abort(400)
role_name = data.get('role')
if not isinstance(role_name, str):
abort(400)
expiration_date = datetime.strptime(expiration, '%b %d, %Y')
cfr = CorpusFollowerRole.query.filter_by(name=role_name).first()
if cfr is None:
abort(400)
token = current_user.generate_follow_corpus_token(corpus_hashid, role_name, expiration_date)
corpus_share_link = url_for(
'corpora.follow_corpus',
corpus_id=corpus_id,
token=token,
_external=True
)
response_data = {
'message': 'Corpus share link generated',
'category': 'corpus',
'corpusShareLink': corpus_share_link
}
response = jsonify(response_data)
response.status_code = 200
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'])
@login_required
@corpus_follower_permission_required('ADD_CORPUS_FILE')
def create_corpus_file(corpus_id):
corpus = Corpus.query.get_or_404(corpus_id)
if not (corpus.user == current_user or current_user.is_administrator()):
abort(403)
form = CreateCorpusFileForm()
if form.is_submitted():
if not form.validate():
response = {'errors': form.errors}
return response, 400
try:
corpus_file = CorpusFile.create(
form.vrt.data,
address=form.address.data,
author=form.author.data,
booktitle=form.booktitle.data,
chapter=form.chapter.data,
editor=form.editor.data,
institution=form.institution.data,
journal=form.journal.data,
pages=form.pages.data,
publisher=form.publisher.data,
publishing_year=form.publishing_year.data,
school=form.school.data,
title=form.title.data,
mimetype='application/vrt+xml',
corpus=corpus
)
except (AttributeError, OSError):
abort(500)
corpus.status = CorpusStatus.UNPREPARED
db.session.commit()
message = Markup(
'Corpus file'
f'"<a href="{corpus_file.url}">{corpus_file.filename}</a>" added'
)
flash(message, category='corpus')
return {}, 201, {'Location': corpus.url}
return render_template(
'corpora/create_corpus_file.html.j2',
corpus=corpus,
form=form,
title='Add corpus file'
)
@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>', methods=['GET', 'POST'])
@login_required
@corpus_follower_permission_required('UPDATE_CORPUS_FILE')
def corpus_file(corpus_id, corpus_file_id):
corpus_file = CorpusFile.query.filter_by(corpus_id = corpus_id, id=corpus_file_id).first_or_404()
form = UpdateCorpusFileForm(data=corpus_file.to_json_serializeable())
if form.validate_on_submit():
form.populate_obj(corpus_file)
if db.session.is_modified(corpus_file):
corpus_file.corpus.status = CorpusStatus.UNPREPARED
db.session.commit()
message = Markup(f'Corpus file "<a href="{corpus_file.url}">{corpus_file.filename}</a>" updated')
flash(message, category='corpus')
return redirect(corpus_file.corpus.url)
return render_template(
'corpora/corpus_file.html.j2',
corpus=corpus_file.corpus,
corpus_file=corpus_file,
form=form,
title='Edit corpus file'
)
@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>/download')
@login_required
@corpus_follower_permission_required('VIEW')
def download_corpus_file(corpus_id, corpus_file_id):
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)
return send_from_directory(
os.path.dirname(corpus_file.path),
os.path.basename(corpus_file.path),
as_attachment=True,
attachment_filename=corpus_file.filename,
mimetype=corpus_file.mimetype
)
#region json-routes
@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>', methods=['DELETE'])
@login_required
@corpus_follower_permission_required('REMOVE_CORPUS_FILE')
@content_negotiation(produces='application/json')
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>/followers/<hashid:follower_id>', methods=['DELETE'])
@login_required
@content_negotiation(produces='application/json')
def unfollow_corpus(corpus_id, follower_id):
corpus = Corpus.query.get_or_404(corpus_id)
follower = User.query.get_or_404(follower_id)
if not (corpus.user == current_user or follower == current_user or current_user.is_administrator()):
abort(403)
if not follower.is_following_corpus(corpus):
abort(409) # 'User is not following the corpus'
follower.unfollow_corpus(corpus)
db.session.commit()
response_data = {
'message': \
f'"{follower.username}" is not following "{corpus.title}" anymore',
'category': 'corpus'
}
response = jsonify(response_data)
response.status_code = 200
return response
@bp.route('/<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()
if cfr is None:
abort(400)
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,5 +1,6 @@
from flask import Blueprint from werkzeug.exceptions import HTTPException
from .handlers import generic_error_handler
bp = Blueprint('errors', __name__) def init_app(app):
from . import handlers app.register_error_handler(HTTPException, generic_error_handler)

View File

@ -1,11 +1,6 @@
from flask import render_template, request from flask import render_template
from werkzeug.exceptions import HTTPException
from . import bp
@bp.errorhandler(HTTPException)
def generic_error_handler(e): def generic_error_handler(e):
if (request.accept_mimetypes.accept_json print('test')
and not request.accept_mimetypes.accept_html):
return {'errors': {'message': e.description}}, e.code
return render_template('errors/error.html.j2', error=e), e.code return render_template('errors/error.html.j2', error=e), e.code

View File

@ -30,12 +30,6 @@ def dashboard():
return render_template('main/dashboard.html.j2', title='Dashboard') return render_template('main/dashboard.html.j2', title='Dashboard')
@bp.route('/dashboard2')
@login_required
def dashboard2():
return render_template('main/dashboard2.html.j2', title='Dashboard')
@bp.route('/user_manual') @bp.route('/user_manual')
def user_manual(): def user_manual():
return render_template('main/user_manual.html.j2', title='User manual') return render_template('main/user_manual.html.j2', title='User manual')
@ -55,6 +49,7 @@ def privacy_policy():
def terms_of_use(): def terms_of_use():
return render_template('main/terms_of_use.html.j2', title='Terms of Use') return render_template('main/terms_of_use.html.j2', title='Terms of Use')
@bp.route('/social-area') @bp.route('/social-area')
def social_area(): def social_area():
users = [ users = [

View File

@ -33,11 +33,11 @@ Requests.corpora.entity.generateShareLink = (corpusId, role, expiration) => {
Requests.corpora.entity.isPublic = {}; Requests.corpora.entity.isPublic = {};
Requests.corpora.entity.isPublic.update = (corpusId, value) => { Requests.corpora.entity.isPublic.update = (corpusId, isPublic) => {
let input = `/corpora/${corpusId}/is_public`; let input = `/corpora/${corpusId}/is_public`;
let init = { let init = {
method: 'PUT', method: 'PUT',
body: JSON.stringify(value) body: JSON.stringify(isPublic)
}; };
return Requests.JSONfetch(input, init); return Requests.JSONfetch(input, init);
}; };

View File

@ -100,7 +100,37 @@ class CorpusFileList extends ResourceList {
let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction; let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction;
switch (listAction) { switch (listAction) {
case 'delete': { case 'delete': {
Utils.deleteCorpusFileRequest(this.userId, this.corpusId, itemId); let values = this.listjs.get('id', itemId)[0].values();
let modalElement = Utils.HTMLToElement(
`
<div class="modal">
<div class="modal-content">
<h4>Confirm Corpus File deletion</h4>
<p>Do you really want to delete the Corpus File <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.corpora.entity.files.ent.delete(this.corpusId, itemId);
});
modal.open();
break; break;
} }
case 'download': { case 'download': {

View File

@ -95,7 +95,37 @@ class CorpusList 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': {
Requests.corpora.entity.delete(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 Corpus deletion</h4>
<p>Do you really want to delete the Corpus <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.corpora.entity.delete(itemId);
});
modal.open();
break; break;
} }
case 'view': { case 'view': {

View File

@ -69,118 +69,6 @@ class Utils {
return Utils.mergeObjectsDeep(mergedObject, ...objects.slice(2)); return Utils.mergeObjectsDeep(mergedObject, ...objects.slice(2));
} }
static deleteCorpusRequest(userId, corpusId) {
return new Promise((resolve, reject) => {
let corpus;
try {
corpus = app.data.users[userId].corpora[corpusId];
} catch (error) {
corpus = {};
}
let modalElement = Utils.HTMLToElement(
`
<div class="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="action-button btn modal-close waves-effect waves-light" data-action="cancel">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) => {
let corpusTitle = corpus?.title;
fetch(`/corpora/${corpusId}`, {method: 'DELETE', headers: {Accept: 'application/json'}})
.then(
(response) => {
if (response.status === 403) {app.flash('Forbidden', 'error'); reject(response);}
if (response.status === 404) {app.flash('Not Found', 'error'); reject(response);}
app.flash(`Corpus "${corpusTitle}" marked for deletion`, 'corpus');
resolve(response);
},
(response) => {
app.flash('Something went wrong', 'error');
reject(response);
}
);
});
modal.open();
});
}
static deleteCorpusFileRequest(userId, corpusId, corpusFileId) {
return new Promise((resolve, reject) => {
let corpusFile;
try {
corpusFile = app.data.users[userId].corpora[corpusId].files[corpusFileId];
} catch (error) {
corpusFile = {};
}
let modalElement = Utils.HTMLToElement(
`
<div class="modal">
<div class="modal-content">
<h4>Confirm Corpus File deletion</h4>
<p>Do you really want to delete the Corpus File <b>${corpusFile?.title}</b>? All files will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a class="action-button btn modal-close waves-effect waves-light" data-action="cancel">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) => {
let corpusFileTitle = corpusFile?.title;
fetch(`/corpora/${corpusId}/files/${corpusFileId}`, {method: 'DELETE', headers: {Accept: 'application/json'}})
.then(
(response) => {
if (response.status === 403) {app.flash('Forbidden', 'error'); reject(response);}
if (response.status === 404) {app.flash('Not Found', 'error'); reject(response);}
app.flash(`Corpus File "${corpusFileTitle}" deleted`, 'corpus');
resolve(response);
},
(response) => {
app.flash('Something went wrong', 'error');
reject(response);
}
);
});
modal.open();
});
}
static deleteProfileAvatarRequest(userId) { static deleteProfileAvatarRequest(userId) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let modalElement = Utils.HTMLToElement( let modalElement = Utils.HTMLToElement(

View File

@ -14,7 +14,7 @@
<li class="tab"><a{%if request.path == url_for('corpora.create_corpus') %} class="active"{% endif %} href="{{ url_for('corpora.create_corpus') }}" target="_self">Create corpus</a></li> <li class="tab"><a{%if request.path == url_for('corpora.create_corpus') %} class="active"{% endif %} href="{{ url_for('corpora.create_corpus') }}" target="_self">Create corpus</a></li>
<li class="tab disabled"><i class="material-icons">navigate_next</i></li> <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
{% if corpus %} {% if corpus %}
<li class="tab"><a{%if request.path == url_for('corpora.create_corpus_file', corpus_id=corpus.id) %} class="active"{% endif %} href="{{ url_for('corpora.create_corpus_file', corpus_id=corpus.id) }}" target="_self">Create corpus file(s)</a></li> <li class="tab"><a{%if request.path == url_for('corpora.files.create_corpus_file', corpus_id=corpus.id) %} class="active"{% endif %} href="{{ url_for('corpora.files.create_corpus_file', corpus_id=corpus.id) }}" target="_self">Create corpus file(s)</a></li>
{% else %} {% else %}
<li class="tab disabled tooltipped" data-tooltip="Select a corpus first" target="_self"><a>Create corpus file(s)</a></li> <li class="tab disabled tooltipped" data-tooltip="Select a corpus first" target="_self"><a>Create corpus file(s)</a></li>
{% endif %} {% endif %}

View File

@ -49,4 +49,8 @@
{% block scripts %} {% block scripts %}
{{ super() }} {{ super() }}
{% include "_scripts.html.j2" %} {% include "_scripts.html.j2" %}
{% set page_script = self._TemplateReference__context.name|replace('.html.j2', '.js.j2') %}
<script>
{% include page_script ignore missing %}
</script>
{% endblock scripts %} {% endblock scripts %}

View File

@ -79,37 +79,3 @@
</div> </div>
</div> </div>
{% endblock page_content %} {% endblock page_content %}
{% block modals %}
{{ super() }}
<div id="models-modal" class="modal">
<div class="modal-content">
<h4>Tesseract OCR Pipeline models</h4>
<table>
<thead>
<tr>
<th>Title</th>
<th>Description</th>
<th>Biblio</th>
</tr>
</thead>
<tbody>
{% for m in tesseract_ocr_pipeline_models %}
<tr id="tesseract-ocr-pipeline-model-{{ m.hashid }}">
<td>{{ m.title }}</td>
{% if m.description == '' %}
<td>Description is not available.</td>
{% else %}
<td>{{ m.description }}</td>
{% endif %}
<td><a href="{{ m.publisher_url }}">{{ m.publisher }}</a> ({{ m.publishing_year }}), {{ m.title }} {{ m.version}}, <a href="{{ m.publishing_url }}">{{ m.publishing_url }}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="modal-footer">
<a href="#!" class="modal-close waves-effect waves-light btn">Close</a>
</div>
</div>
{% endblock modals %}

View File

@ -1,4 +1,4 @@
{% set breadcrumbs %} {# {% set breadcrumbs %}
<li class="tab disabled"><i class="material-icons">navigate_next</i></li> <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
<li class="tab"><a href="{{ url_for('main.dashboard', _anchor='jobs') }}" target="_self">My corpora</a></li> <li class="tab"><a href="{{ url_for('main.dashboard', _anchor='jobs') }}" target="_self">My corpora</a></li>
<li class="tab disabled"><i class="material-icons">navigate_next</i></li> <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
@ -25,4 +25,4 @@
<li class="tab disabled"><i class="material-icons">navigate_next</i></li> <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
<li class="tab"><a class="active" href="{{ url_for('.corpus_file', corpus_file_id=corpus_file.id, corpus_id=corpus.id) }}" target="_self">{{ corpus_file.author }}: {{ corpus_file.title }} ({{ corpus_file.publishing_year }})</a></li> <li class="tab"><a class="active" href="{{ url_for('.corpus_file', corpus_file_id=corpus_file.id, corpus_id=corpus.id) }}" target="_self">{{ corpus_file.author }}: {{ corpus_file.title }} ({{ corpus_file.publishing_year }})</a></li>
{% endif %} {% endif %}
{% endset %} {% endset %} #}

View File

@ -91,7 +91,7 @@
<div class="corpus-file-list" data-user-id="{{ corpus.user.hashid }}" data-corpus-id="{{ corpus.hashid }}"></div> <div class="corpus-file-list" data-user-id="{{ corpus.user.hashid }}" data-corpus-id="{{ corpus.hashid }}"></div>
</div> </div>
<div class="card-action right-align"> <div class="card-action right-align">
<a href="{{ url_for('corpora.create_corpus_file', corpus_id=corpus.id) }}" class="btn waves-effect waves-light"><i class="material-icons left">add</i>Add corpus file</a> <a href="{{ url_for('corpora.files.create_corpus_file', corpus_id=corpus.id) }}" class="btn waves-effect waves-light"><i class="material-icons left">add</i>Add corpus file</a>
</div> </div>
</div> </div>
</div> </div>
@ -215,132 +215,3 @@
</div> </div>
</div> </div>
{% endblock modals %} {% endblock modals %}
{% block scripts %}
{{ super() }}
<script>
let corpusId = {{ corpus.hashid|tojson }};
let corpusDisplay = new CorpusDisplay(document.querySelector('#corpus-display'));
// #region publishing_modal_js
let publishingModalIsPublicSwitchElement = document.querySelector('#publishing-modal-is-public-switch');
publishingModalIsPublicSwitchElement.addEventListener('change', (event) => {
let newIsPublic = publishingModalIsPublicSwitchElement.checked;
Requests.corpora.entity.isPublic.update(corpusId, newIsPublic)
.catch((response) => {
publishingModalIsPublicSwitchElement.checked = !newIsPublic;
});
});
// #endregion publishing_modal_js
// #region delete_modal_js
let deleteModalDeleteButtonElement = document.querySelector('#delete-modal-delete-button');
deleteModalDeleteButtonElement.addEventListener('click', (event) => {
Requests.corpora.entity.delete(corpusId)
.then((response) => {window.location.href = '/dashboard';});
});
// #endregion delete_modal_js
// #region invite_user_modal_js
let inviteUserModalElement = document.querySelector('#invite-user-modal');
let inviteUserModalSearchElement = document.querySelector('#invite-user-modal-search');
let inviteUserModalInviteButtonElement = document.querySelector('#invite-user-modal-invite-button');
let inviteUserModalSearch = M.Chips.init(
inviteUserModalSearchElement,
{
autocompleteOptions: {
data: {
'nopaque': '/users/3V8Aqpg74JvxOd9o/avatar',
'pjentsch': '/users/3V8Aqpg74JvxOd9o/avatar',
'pjentsch2': '/users/3V8Aqpg74JvxOd9o/avatar'
}
},
limit: 3,
onChipAdd: (a, chipElement) => {
if (!(chipElement.firstChild.data in inviteUserModalSearch.autocomplete.options.data)) {
chipElement.firstElementChild.click();
}
},
placeholder: 'Enter a username',
secondaryPlaceholder: 'Add more users'
}
);
M.Modal.init(
inviteUserModalElement,
{
onOpenStart: (modalElement, modalTriggerElement) => {
while (inviteUserModalSearch.chipsData.length > 0) {
inviteUserModalSearch.deleteChip(0);
}
}
}
)
inviteUserModalInviteButtonElement.addEventListener('click', (event) => {
let usernames = inviteUserModalSearch.chipsData.map((chipData) => chipData.tag);
Requests.corpora.entity.followers.add(corpusId, usernames);
});
// #endregion invite_user_modal_js
// #region share_link_modal_js
let shareLinkModalElement = document.querySelector('#share-link-modal');
let shareLinkModalCorpusFollowerRoleSelectElement = document.querySelector('#share-link-modal-corpus-follower-role-select');
let shareLinkModalExpirationDateDatepickerElement = document.querySelector('#share-link-modal-expiration-date-datepicker');
let shareLinkModalCreateButtonElement = document.querySelector('#share-link-modal-create-button');
let shareLinkModalOutputContainerElement = document.querySelector('#share-link-modal-output-container');
let shareLinkModalOutputFieldElement = document.querySelector('#share-link-modal-output-field');
let shareLinkModalOutputCopyButtonElement = document.querySelector('#share-link-modal-output-copy-button');
let today = new Date();
let tomorrow = new Date();
tomorrow.setDate(today.getDate() + 1);
let oneWeekLater = new Date();
oneWeekLater.setDate(today.getDate() + 7);
let fourWeeksLater = new Date();
fourWeeksLater.setDate(today.getDate() + 28);
M.Datepicker.init(
shareLinkModalExpirationDateDatepickerElement,
{
container: document.querySelector('main'),
defaultDate: oneWeekLater,
setDefaultDate: true,
minDate: tomorrow,
maxDate: fourWeeksLater
}
);
M.Modal.init(
shareLinkModalElement,
{
onOpenStart: (modalElement, modalTriggerElement) => {
shareLinkModalOutputFieldElement.value = '';
shareLinkModalOutputContainerElement.classList.add('hide');
}
}
)
shareLinkModalCreateButtonElement.addEventListener('click', (event) => {
Requests.corpora.entity.generateShareLink(corpusId, shareLinkModalCorpusFollowerRoleSelectElement.value, shareLinkModalExpirationDateDatepickerElement.value)
.then((response) => {
response.json()
.then((json) => {
shareLinkModalOutputContainerElement.classList.remove('hide');
shareLinkModalOutputFieldElement.value = json.corpusShareLink;
});
});
});
shareLinkModalOutputCopyButtonElement.addEventListener('click', (event) => {
navigator.clipboard.writeText(shareLinkModalOutputFieldElement.value)
.then(
() => {app.flash('Copied!');},
() => {app.flash('Could not copy to clipboard. Please copy manually.', 'error');}
);
});
// #endregion share_link_modal_js
</script>
{% endblock scripts %}

View File

@ -0,0 +1,127 @@
let corpusId = {{ corpus.hashid|tojson }};
let corpusDisplay = new CorpusDisplay(document.querySelector('#corpus-display'));
// #region Publishing
let publishingModalIsPublicSwitchElement = document.querySelector('#publishing-modal-is-public-switch');
publishingModalIsPublicSwitchElement.addEventListener('change', (event) => {
let newIsPublic = publishingModalIsPublicSwitchElement.checked;
Requests.corpora.entity.isPublic.update(corpusId, newIsPublic)
.catch((response) => {
publishingModalIsPublicSwitchElement.checked = !newIsPublic;
});
});
// #endregion Publishing
// #region Delete
let deleteModalDeleteButtonElement = document.querySelector('#delete-modal-delete-button');
deleteModalDeleteButtonElement.addEventListener('click', (event) => {
Requests.corpora.entity.delete(corpusId)
.then((response) => {
window.location.href = {{ url_for('main.dashboard')|tojson }};
});
});
// #endregion Delete
// #region Invite users
let inviteUserModalElement = document.querySelector('#invite-user-modal');
let inviteUserModalSearchElement = document.querySelector('#invite-user-modal-search');
let inviteUserModalInviteButtonElement = document.querySelector('#invite-user-modal-invite-button');
let inviteUserModalSearch = M.Chips.init(
inviteUserModalSearchElement,
{
autocompleteOptions: {
data: {
'nopaque': '/users/3V8Aqpg74JvxOd9o/avatar',
'pjentsch': '/users/3V8Aqpg74JvxOd9o/avatar',
'pjentsch2': '/users/3V8Aqpg74JvxOd9o/avatar'
}
},
limit: 3,
onChipAdd: (a, chipElement) => {
if (!(chipElement.firstChild.data in inviteUserModalSearch.autocomplete.options.data)) {
chipElement.firstElementChild.click();
}
},
placeholder: 'Enter a username',
secondaryPlaceholder: 'Add more users'
}
);
M.Modal.init(
inviteUserModalElement,
{
onOpenStart: (modalElement, modalTriggerElement) => {
while (inviteUserModalSearch.chipsData.length > 0) {
inviteUserModalSearch.deleteChip(0);
}
}
}
)
inviteUserModalInviteButtonElement.addEventListener('click', (event) => {
let usernames = inviteUserModalSearch.chipsData.map((chipData) => chipData.tag);
Requests.corpora.entity.followers.add(corpusId, usernames);
});
// #endregion Invite users
// #region Share link
let shareLinkModalElement = document.querySelector('#share-link-modal');
let shareLinkModalCorpusFollowerRoleSelectElement = document.querySelector('#share-link-modal-corpus-follower-role-select');
let shareLinkModalExpirationDateDatepickerElement = document.querySelector('#share-link-modal-expiration-date-datepicker');
let shareLinkModalCreateButtonElement = document.querySelector('#share-link-modal-create-button');
let shareLinkModalOutputContainerElement = document.querySelector('#share-link-modal-output-container');
let shareLinkModalOutputFieldElement = document.querySelector('#share-link-modal-output-field');
let shareLinkModalOutputCopyButtonElement = document.querySelector('#share-link-modal-output-copy-button');
let today = new Date();
let tomorrow = new Date();
tomorrow.setDate(today.getDate() + 1);
let oneWeekLater = new Date();
oneWeekLater.setDate(today.getDate() + 7);
let fourWeeksLater = new Date();
fourWeeksLater.setDate(today.getDate() + 28);
M.Datepicker.init(
shareLinkModalExpirationDateDatepickerElement,
{
container: document.querySelector('main'),
defaultDate: oneWeekLater,
setDefaultDate: true,
minDate: tomorrow,
maxDate: fourWeeksLater
}
);
M.Modal.init(
shareLinkModalElement,
{
onOpenStart: (modalElement, modalTriggerElement) => {
shareLinkModalOutputFieldElement.value = '';
shareLinkModalOutputContainerElement.classList.add('hide');
}
}
)
shareLinkModalCreateButtonElement.addEventListener('click', (event) => {
let role = shareLinkModalCorpusFollowerRoleSelectElement.value;
let expiration = shareLinkModalExpirationDateDatepickerElement.value
Requests.corpora.entity.generateShareLink(corpusId, role, expiration)
.then((response) => {
response.json()
.then((json) => {
shareLinkModalOutputContainerElement.classList.remove('hide');
shareLinkModalOutputFieldElement.value = json.corpusShareLink;
});
});
});
shareLinkModalOutputCopyButtonElement.addEventListener('click', (event) => {
navigator.clipboard.writeText(shareLinkModalOutputFieldElement.value)
.then(
() => {app.flash('Copied!');},
() => {app.flash('Could not copy to clipboard. Please copy manually.', 'error');}
);
});
// #endregion Share link

View File

@ -36,7 +36,7 @@
</div> </div>
</div> </div>
<div class="card-action right-align"> <div class="card-action right-align">
<a class="waves-effect waves-light btn red" href="{{ url_for('.corpus', corpus_id=corpus.id) }}"><i class="material-icons left">close</i>Cancel</a> <a class="waves-effect waves-light btn red" href="{{ url_for('corpora.corpus', corpus_id=corpus.id) }}"><i class="material-icons left">close</i>Cancel</a>
{{ wtf.render_field(form.submit, material_icon='send') }} {{ wtf.render_field(form.submit, material_icon='send') }}
</div> </div>
</div> </div>

View File

@ -1,72 +0,0 @@
{% extends "base.html.j2" %}
{% block main_attribs %} class="service-scheme" data-service="corpus-analysis"{% endblock main_attribs %}
{% block page_content %}
<div class="corpus-list no-autoinit" id="corpus-list">
<div class="parallax-container">
<div class="parallax"><img src="{{ url_for('static', filename='images/parallax_hq/canvas.png') }}"></div>
<div style="position: absolute; bottom: 0; width: 100%;">
<div class="container">
<div class="white-text">
<h1 id="title"><i class="nopaque-icons" style="font-size: inherit;">I</i>Corpora</h1>
</div>
<div class="white" style="padding: 1px 35px 0 10px; border-radius: 35px;">
<div class="input-field">
<i class="material-icons prefix">search</i>
<input class="search" id="corpus-list-search" type="text">
<label for="corpus-list-search">Search corpus</label>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col s12" id="corpora">
<div class="card">
<div class="card-content">
<div>
<table>
<thead>
<tr>
<th></th>
<th>Title and Description</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody class="list"></tbody>
</table>
<ul class="pagination"></ul>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock page_content %}
{% block scripts %}
{{ super() }}
<script>
let corpusListElement = document.querySelector('#corpus-list');
let corpusListOptions = {
initialHtmlGenerator: null,
item: `
<tr class="clickable hoverable">
<td><a class="btn-floating disabled"><i class="material-icons service-color darken" data-service="corpus-analysis">book</i></a></td>
<td><b class="title"></b><br><i class="description"></i></td>
<td><span class="status badge new corpus-status-color corpus-status-text" data-badge-caption=""></span></td>
<td class="right-align">
<a class="action-button btn-floating service-color darken waves-effect waves-light" data-action="view" data-service="corpus-analysis"><i class="material-icons">send</i></a>
</td>
</tr>
`.trim(),
};
let corpusList = new CorpusList(corpusListElement, corpusListOptions);
corpusList._init({{ corpora|tojson }});
</script>
{% endblock scripts %}

View File

@ -101,30 +101,3 @@
</div> </div>
</div> </div>
{% endblock page_content %} {% endblock page_content %}
{% block scripts %}
{{ super() }}
<script>
let corpusFileList = new PublicCorpusFileList(document.querySelector('.corpus-file-list'));
corpusFileList.add({{ corpus_files|tojson }});
let unfollowRequestElement = document.querySelector('.action-button[data-action="unfollow-request"]');
unfollowRequestElement.addEventListener('click', () => {
return new Promise((resolve, reject) => {
fetch('{{ url_for("corpora.unfollow_corpus", corpus_id=corpus.id, follower_id=current_user.id) }}', {method: 'POST', headers: {Accept: 'application/json'}})
.then(
(response) => {
if (response.status === 403) {app.flash('Forbidden', 'error'); reject(response);}
if (response.status === 404) {app.flash('Not Found', 'error'); reject(response);}
resolve(response);
window.location.href = '{{ url_for("main.dashboard") }}';
},
(response) => {
app.flash('Something went wrong', 'error');
reject(response);
}
);
});
});
</script>
{% endblock scripts %}

View File

@ -0,0 +1,11 @@
let corpusId = {{ corpus.hashid|tojson }};
let corpusFileList = new PublicCorpusFileList(document.querySelector('.corpus-file-list'));
corpusFileList.add({{ corpus_files|tojson }});
let unfollowRequestElement = document.querySelector('.action-button[data-action="unfollow-request"]');
unfollowRequestElement.addEventListener('click', () => {
Requests.corpora.entity.followers.entity.delete(corpusId, currentUserId)
.then((response) => {
window.location.href = {{ url_for('main.dashboard')|tojson }};
});
});

View File

@ -1,274 +0,0 @@
{% extends "base.html.j2" %}
{% from "main/_breadcrumbs.html.j2" import breadcrumbs with context %}
{% block page_content %}
<div class="section scrollspy" id="dashboard">
<div class="row">
<div class="col s1"></div>
<div class="col s11">
<h1 id="title">Dashboard</h1>
</div>
<div class="col s1"></div>
<div class="col s3">
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.</p>
</div>
<div class="col s8">
<a class="btn waves-effect waves-light" href="#my-corpora"><i class="nopaque-icons left">I</i>My Corpora</a>
<a class="btn waves-effect waves-light" href="#my-jobs"><i class="nopaque-icons left">J</i>My Jobs</a>
<a class="btn waves-effect waves-light" href="#my-groups"><i class="material-icons left">groups</i>My Groups</a>
</div>
</div>
</div>
<div class="corpus-list no-autoinit" id="corpus-list" data-user-id="{{ current_user.hashid }}">
<div class="parallax-container">
<div class="parallax"><img src="{{ url_for('static', filename='images/parallax_hq/canvas.png') }}"></div>
<div style="position: absolute; bottom: 0; width: 100%;">
<div class="container">
<div class="white" style="padding: 1px 35px 0 10px; border-radius: 35px;">
<div class="input-field">
<i class="material-icons prefix">search</i>
<input class="search" id="corpus-list-search" type="text">
<label for="corpus-list-search">Search Corpus</label>
</div>
</div>
</div>
</div>
</div>
<div class="section scrollspy" id="my-corpora">
<div class="row">
<div class="col s1"></div>
<div class="col s2">
<h2>My Corpora</h2>
<p>Create a corpus to interactively perform linguistic analysis.</p>
<p>Or browse our users public corpora.<span class="new badge"></span></p>
</div>
<div class="col s6">
<div class="card">
<div class="card-content">
<div>
<table>
<thead>
<tr>
<th></th>
<th>Title and Description</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody class="list"></tbody>
</table>
<ul class="pagination"></ul>
</div>
</div>
<div class="card-action right-align">
<a class="btn disabled waves-effect waves-light" href="{{ url_for('corpora.import_corpus') }}">Import Corpus<i class="material-icons right">import_export</i></a>
<a class="btn waves-effect waves-light" href="{{ url_for('corpora.create_corpus') }}">Create corpus<i class="material-icons right">add</i></a>
</div>
</div>
</div>
<div class="col s1"></div>
<div class="col s2">
<ul class="section table-of-contents">
<li><a href="#dashboard">Dashboard</a></li>
<li><a href="#my-corpora">My Corpora</a></li>
<li><a href="#my-jobs">My Jobs</a></li>
<li><a href="#my-groups">My Groups</a></li>
</ul>
</div>
</div>
</div>
</div>
<div class="job-list no-autoinit" id="job-list" data-user-id="{{ current_user.hashid }}">
<div class="parallax-container">
<div class="parallax"><img src="{{ url_for('static', filename='images/parallax_hq/canvas.png') }}"></div>
<div style="position: absolute; bottom: 0; width: 100%;">
<div class="container">
<div class="white" style="padding: 1px 35px 0 10px; border-radius: 35px;">
<div class="input-field">
<i class="material-icons prefix">search</i>
<input class="search" id="job-list-search" type="text">
<label for="job-list-search">Search Job</label>
</div>
</div>
</div>
</div>
</div>
<div class="section scrollspy" id="my-jobs">
<div class="row">
<div class="col s1"></div>
<div class="col s2">
<h2>My Jobs</h2>
<p>
A job is the execution of a service provided by nopaque. You can
create any number of jobs and let them be processed simultaneously. We
<b>strongly recommend</b> that you create a folder on your computer where you
save the various files that nopaque provides you with after each
pre-processing step. You will need the result of each step for the
next step.
</p>
<p><b>Where is my Job data?</b> Don't worry, please read <a href="{{ url_for('main.news', _anchor='april-2022-update') }}">this news</a> entry</p>
</div>
<div class="col s6">
<div class="card">
<div class="card-content">
<div>
<table>
<thead>
<tr>
<th></th>
<th>Title and Description</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody class="list"></tbody>
</table>
<ul class="pagination"></ul>
</div>
</div>
<div class="card-action right-align">
<a class="btn modal-trigger waves-effect waves-light" data-target="create-job-modal"><i class="material-icons left">add</i>Create job</a>
</div>
</div>
</div>
<div class="col s1"></div>
<div class="col s2">
<ul class="section table-of-contents">
<li><a href="#dashboard">Dashboard</a></li>
<li><a href="#my-corpora">My Corpora</a></li>
<li><a href="#my-jobs">My Jobs</a></li>
<li><a href="#my-groups">My Groups</a></li>
</ul>
</div>
</div>
</div>
</div>
<div class="group-list no-autoinit" id="group-list" data-user-id="{{ current_user.hashid }}">
<div class="parallax-container">
<div class="parallax"><img src="{{ url_for('static', filename='images/parallax_hq/canvas.png') }}"></div>
<div style="position: absolute; bottom: 0; width: 100%;">
<div class="container">
<div class="white" style="padding: 1px 35px 0 10px; border-radius: 35px;">
<div class="input-field">
<i class="material-icons prefix">search</i>
<input class="search" id="group-list-search" type="text">
<label for="group-list-search">Search Group</label>
</div>
</div>
</div>
</div>
</div>
<div class="section scrollspy" id="my-groups">
<div class="row">
<div class="col s1"></div>
<div class="col s2">
<h2>My Groups</h2>
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.</p>
</div>
<div class="col s6">
<div class="card">
<div class="card-content">
<div>
<table>
<thead>
<tr>
<th></th>
<th>Title and Description</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody class="list"></tbody>
</table>
<ul class="pagination"></ul>
</div>
</div>
<div class="card-action right-align">
<a class="btn waves-effect waves-light"><i class="material-icons left">add</i>Create group</a>
</div>
</div>
</div>
<div class="col s1"></div>
<div class="col s2">
<ul class="section table-of-contents">
<li><a href="#dashboard">Dashboard</a></li>
<li><a href="#my-corpora">My Corpora</a></li>
<li><a href="#my-jobs">My Jobs</a></li>
<li><a href="#my-groups">My Groups</a></li>
</ul>
</div>
</div>
</div>
</div>
{% endblock page_content %}
{% block modals %}
{{ super() }}
<div id="create-job-modal" class="modal">
<div class="modal-content">
<h4>Select a service</h4>
<p>&nbsp;</p>
<div class="row">
<div class="col s12 m4">
<div class="card-panel center-align hoverable">
<br>
<a href="{{ url_for('services.file_setup_pipeline') }}" class="btn-floating btn-large waves-effect waves-light" style="transform: scale(2);">
<i class="nopaque-icons service-color darken service-icons" data-service="file-setup-pipeline"></i>
</a>
<br><br>
<p class="service-color-text darken" data-service="file-setup-pipeline"><b>File setup</b></p>
<p class="light">Digital copies of text based research data (books, letters, etc.) often comprise various files and formats. nopaque converts and merges those files to facilitate further processing.</p>
<a href="{{ url_for('services.file_setup_pipeline') }}" class="waves-effect waves-light btn service-color darken" data-service="file-setup-pipeline">Create Job</a>
</div>
</div>
<div class="col s12 m4">
<div class="card-panel center-align hoverable">
<br>
<a href="{{ url_for('services.tesseract_ocr_pipeline') }}" class="btn-floating btn-large waves-effect waves-light" style="transform: scale(2);">
<i class="nopaque-icons service-color darken service-icons" data-service="tesseract-ocr-pipeline" style="font-size: 2.5rem;"></i>
</a>
<br><br>
<p class="service-color-text darken" data-service="tesseract-ocr-pipeline"><b>Optical Character Recognition</b></p>
<p class="light">nopaque converts your image data like photos or scans into text data through a process called OCR. This step enables you to proceed with further computational analysis of your documents.</p>
<a href="{{ url_for('services.tesseract_ocr_pipeline') }}" class="waves-effect waves-light btn service-color darken" data-service="tesseract-ocr-pipeline">Create Job</a>
</div>
</div>
<div class="col s12 m4">
<div class="card-panel center-align hoverable">
<br>
<a href="{{ url_for('services.spacy_nlp_pipeline') }}" class="btn-floating btn-large waves-effect waves-light" style="transform: scale(2);">
<i class="nopaque-icons service-color darken service-icons" data-service="spacy-nlp-pipeline" style="font-size: 2.5rem;"></i>
</a>
<br><br>
<p class="service-color-text darken" data-service="spacy-nlp-pipeline"><b>Natural Language Processing</b></p>
<p class="light">By means of computational linguistic data processing (tokenization, lemmatization, part-of-speech tagging and named-entity recognition) nopaque extracts additional information from your text.</p>
<a href="{{ url_for('services.spacy_nlp_pipeline') }}" class="waves-effect waves-light btn service-color darken" data-service="spacy-nlp-pipeline">Create Job</a>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<a class="btn-flat modal-close waves-effect waves-light">Close</a>
</div>
</div>
{% endblock modals %}
{% block scripts %}
{{ super() }}
<script>
let corpusListElement = document.querySelector('#corpus-list');
let corpusListOptions = {initialHtmlGenerator: null};
let corpusList = new CorpusList(corpusListElement, corpusListOptions);
let jobListElement = document.querySelector('#job-list');
let jobListOptions = {initialHtmlGenerator: null};
let jobList = new JobList(jobListElement, jobListOptions);
</script>
{% endblock scripts %}

View File

@ -77,7 +77,7 @@
{{ form.model.label }} {{ form.model.label }}
<span class="helper-text"> <span class="helper-text">
<a class="modal-trigger tooltipped" href="#models-modal" data-position="bottom" data-tooltip="See more information about models"><i class="material-icons" style="color:#0064A3;">help_outline</i></a> <a class="modal-trigger tooltipped" href="#models-modal" data-position="bottom" data-tooltip="See more information about models"><i class="material-icons" style="color:#0064A3;">help_outline</i></a>
<a class="tooltipped" href="{{ url_for('contributions.create_spacy_nlp_pipeline_model') }}" data-position="bottom" data-tooltip="Add your own spaCy NLP models"><i class="material-icons" style="color:#0064A3">new_label</i></a> <a class="tooltipped" href="{{ url_for('contributions.spacy_nlp_pipeline_models.create_spacy_nlp_pipeline_model') }}" data-position="bottom" data-tooltip="Add your own spaCy NLP models"><i class="material-icons" style="color:#0064A3">new_label</i></a>
</span> </span>
</div> </div>
</div> </div>

View File

@ -59,7 +59,7 @@
{{ form.model.label }} {{ form.model.label }}
<span class="helper-text"> <span class="helper-text">
<a class="modal-trigger tooltipped" href="#models-modal" data-position="bottom" data-tooltip="See more information about models"><i class="material-icons" style="color:#00A58B;">help_outline</i></a> <a class="modal-trigger tooltipped" href="#models-modal" data-position="bottom" data-tooltip="See more information about models"><i class="material-icons" style="color:#00A58B;">help_outline</i></a>
<a class="tooltipped" href="{{ url_for('contributions.create_tesseract_ocr_pipeline_model') }}" data-position="bottom" data-tooltip="Add your own Tesseract OCR models"><i class="material-icons" style="color:#00A58B">new_label</i></a> <a class="tooltipped" href="{{ url_for('contributions.tesseract_ocr_pipeline_models.create_tesseract_ocr_pipeline_model') }}" data-position="bottom" data-tooltip="Add your own Tesseract OCR models"><i class="material-icons" style="color:#00A58B">new_label</i></a>
</span> </span>
{% for error in form.model.errors %} {% for error in form.model.errors %}
<span class="helper-text error-color-text">{{ error }}</span> <span class="helper-text error-color-text">{{ error }}</span>