mirror of
https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
synced 2024-11-14 16:55:42 +00:00
Rework corpora package
This commit is contained in:
parent
3789f61ca4
commit
aaf14a9952
@ -2,4 +2,10 @@ from flask import Blueprint
|
||||
|
||||
|
||||
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.file import FileField, FileRequired
|
||||
from wtforms import (
|
||||
BooleanField,
|
||||
StringField,
|
||||
SubmitField,
|
||||
TextAreaField,
|
||||
ValidationError,
|
||||
IntegerField
|
||||
)
|
||||
from wtforms import StringField, SubmitField, TextAreaField
|
||||
from wtforms.validators import InputRequired, Length
|
||||
|
||||
|
||||
@ -34,50 +26,5 @@ class UpdateCorpusForm(CorpusBaseForm):
|
||||
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):
|
||||
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 (
|
||||
abort,
|
||||
current_app,
|
||||
flash,
|
||||
jsonify,
|
||||
Markup,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
send_from_directory,
|
||||
url_for
|
||||
)
|
||||
from flask_login import current_user, login_required
|
||||
from threading import Thread
|
||||
import os
|
||||
from .decorators import corpus_follower_permission_required, corpus_owner_or_admin_required
|
||||
from app import db, hashids
|
||||
from app.decorators import content_negotiation
|
||||
from .decorators import corpus_follower_permission_required
|
||||
from app import db
|
||||
from app.models import (
|
||||
Corpus,
|
||||
CorpusFile,
|
||||
CorpusFollowerAssociation,
|
||||
CorpusFollowerRole,
|
||||
CorpusStatus,
|
||||
User
|
||||
)
|
||||
from . import bp
|
||||
from .forms import (
|
||||
CreateCorpusFileForm,
|
||||
CreateCorpusForm,
|
||||
UpdateCorpusFileForm
|
||||
)
|
||||
from .forms import CreateCorpusForm
|
||||
|
||||
|
||||
@bp.route('/create', methods=['GET', 'POST'])
|
||||
@ -47,10 +32,7 @@ def create_corpus():
|
||||
except OSError:
|
||||
abort(500)
|
||||
db.session.commit()
|
||||
message = Markup(
|
||||
f'Corpus "<a href="{corpus.url}">{corpus.title}</a>" created'
|
||||
)
|
||||
flash(message, 'corpus')
|
||||
flash(f'Corpus "{corpus.title}" created', 'corpus')
|
||||
return redirect(corpus.url)
|
||||
return render_template(
|
||||
'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>')
|
||||
@login_required
|
||||
def corpus(corpus_id):
|
||||
corpus = Corpus.query.get_or_404(corpus_id)
|
||||
corpus_follower_roles = CorpusFollowerRole.query.all()
|
||||
# TODO: Add URL query option to toggle view
|
||||
if corpus.user == current_user or current_user.is_administrator():
|
||||
return render_template(
|
||||
'corpora/corpus.html.j2',
|
||||
@ -122,7 +87,7 @@ def follow_corpus(corpus_id, token):
|
||||
corpus = Corpus.query.get_or_404(corpus_id)
|
||||
if current_user.follow_corpus_by_token(token):
|
||||
db.session.commit()
|
||||
flash(f'You are following {corpus.title} now', category='corpus')
|
||||
flash(f'You are following "{corpus.title}" now', category='corpus')
|
||||
return redirect(url_for('corpora.corpus', corpus_id=corpus_id))
|
||||
abort(403)
|
||||
|
||||
@ -137,309 +102,3 @@ def import_corpus():
|
||||
@login_required
|
||||
def export_corpus(corpus_id):
|
||||
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
|
||||
|
@ -33,11 +33,11 @@ Requests.corpora.entity.generateShareLink = (corpusId, role, expiration) => {
|
||||
|
||||
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 init = {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(value)
|
||||
body: JSON.stringify(isPublic)
|
||||
};
|
||||
return Requests.JSONfetch(input, init);
|
||||
};
|
||||
|
@ -100,7 +100,37 @@ class CorpusFileList extends ResourceList {
|
||||
let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction;
|
||||
switch (listAction) {
|
||||
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;
|
||||
}
|
||||
case 'download': {
|
||||
|
@ -95,7 +95,37 @@ class CorpusList extends ResourceList {
|
||||
let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction;
|
||||
switch (listAction) {
|
||||
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;
|
||||
}
|
||||
case 'view': {
|
||||
|
@ -69,62 +69,6 @@ class Utils {
|
||||
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;
|
||||
|
@ -14,7 +14,7 @@
|
||||
<li class="tab"><a{%if request.path == url_for('corpora.create_corpus') %} class="active"{% endif %} href="{{ url_for('corpora.create_corpus') }}" target="_self">Create corpus</a></li>
|
||||
<li class="tab disabled"><i class="material-icons">navigate_next</i></li>
|
||||
{% if corpus %}
|
||||
<li class="tab"><a{%if request.path == url_for('corpora.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 %}
|
||||
<li class="tab disabled tooltipped" data-tooltip="Select a corpus first" target="_self"><a>Create corpus file(s)</a></li>
|
||||
{% endif %}
|
||||
|
@ -49,4 +49,8 @@
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
{% 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 %}
|
||||
|
@ -79,37 +79,3 @@
|
||||
</div>
|
||||
</div>
|
||||
{% 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"><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>
|
||||
@ -25,4 +25,4 @@
|
||||
<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>
|
||||
{% 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>
|
||||
<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>
|
||||
@ -215,132 +215,3 @@
|
||||
</div>
|
||||
</div>
|
||||
{% 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 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') }}
|
||||
</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>
|
||||
{% 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 }};
|
||||
});
|
||||
});
|
@ -77,7 +77,7 @@
|
||||
{{ form.model.label }}
|
||||
<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="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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -59,7 +59,7 @@
|
||||
{{ form.model.label }}
|
||||
<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="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>
|
||||
{% for error in form.model.errors %}
|
||||
<span class="helper-text error-color-text">{{ error }}</span>
|
||||
|
Loading…
Reference in New Issue
Block a user