mirror of
https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
synced 2024-11-15 01:05:42 +00:00
Merge branch 'public-corpus' of gitlab.ub.uni-bielefeld.de:sfb1288inf/nopaque into public-corpus
This commit is contained in:
commit
7e54d56ed5
23
.vscode/settings.json
vendored
23
.vscode/settings.json
vendored
@ -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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
@ -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')
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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')
|
||||||
|
8
app/corpora/files/__init__.py
Normal file
8
app/corpora/files/__init__.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
|
||||||
|
TEMPLATE_FOLDER = 'corpora/files'
|
||||||
|
|
||||||
|
|
||||||
|
bp = Blueprint('files', __name__)
|
||||||
|
from . import routes, json_routes
|
54
app/corpora/files/forms.py
Normal file
54
app/corpora/files/forms.py
Normal 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)
|
42
app/corpora/files/json_routes.py
Normal file
42
app/corpora/files/json_routes.py
Normal 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
101
app/corpora/files/routes.py
Normal 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
|
||||||
|
)
|
5
app/corpora/followers/__init__.py
Normal file
5
app/corpora/followers/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
|
||||||
|
bp = Blueprint('followers', __name__)
|
||||||
|
from . import json_routes
|
87
app/corpora/followers/json_routes.py
Normal file
87
app/corpora/followers/json_routes.py
Normal 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
|
@ -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
130
app/corpora/json_routes.py
Normal 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
|
@ -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
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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 = [
|
||||||
|
@ -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);
|
||||||
};
|
};
|
||||||
|
@ -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': {
|
||||||
|
@ -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': {
|
||||||
|
@ -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(
|
||||||
|
@ -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 %}
|
||||||
|
@ -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 %}
|
||||||
|
@ -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 %}
|
|
||||||
|
@ -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 %} #}
|
||||||
|
@ -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 %}
|
|
||||||
|
127
app/templates/corpora/corpus.js.j2
Normal file
127
app/templates/corpora/corpus.js.j2
Normal 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
|
@ -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>
|
@ -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 %}
|
|
@ -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 %}
|
|
||||||
|
11
app/templates/corpora/public_corpus.js.j2
Normal file
11
app/templates/corpora/public_corpus.js.j2
Normal 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 }};
|
||||||
|
});
|
||||||
|
});
|
@ -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> </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 %}
|
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user