mirror of
				https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
				synced 2025-11-03 20:02:47 +00:00 
			
		
		
		
	Merge branch 'public-corpus' of gitlab.ub.uni-bielefeld.de:sfb1288inf/nopaque into public-corpus
This commit is contained in:
		@@ -54,6 +54,9 @@ def create_app(config: Config = Config) -> Flask:
 | 
			
		||||
    scheduler.init_app(app)
 | 
			
		||||
    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
 | 
			
		||||
    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
 | 
			
		||||
    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
 | 
			
		||||
    app.register_blueprint(jobs_blueprint, url_prefix='/jobs')
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -39,8 +39,6 @@ def delete_tesseract_model(tesseract_ocr_pipeline_model_id):
 | 
			
		||||
@permission_required('CONTRIBUTE')
 | 
			
		||||
@content_negotiation(consumes='application/json', produces='application/json')
 | 
			
		||||
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
 | 
			
		||||
    if not isinstance(is_public, bool):
 | 
			
		||||
        abort(400)
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
from flask import Blueprint
 | 
			
		||||
from werkzeug.exceptions import HTTPException
 | 
			
		||||
from .handlers import generic_error_handler
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
bp = Blueprint('errors', __name__)
 | 
			
		||||
from . import handlers
 | 
			
		||||
def init_app(app):
 | 
			
		||||
    app.register_error_handler(HTTPException, generic_error_handler)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,6 @@
 | 
			
		||||
from flask import render_template, request
 | 
			
		||||
from werkzeug.exceptions import HTTPException
 | 
			
		||||
from . import bp
 | 
			
		||||
from flask import render_template
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@bp.errorhandler(HTTPException)
 | 
			
		||||
def generic_error_handler(e):
 | 
			
		||||
    if (request.accept_mimetypes.accept_json
 | 
			
		||||
            and not request.accept_mimetypes.accept_html):
 | 
			
		||||
        return {'errors': {'message': e.description}}, e.code
 | 
			
		||||
    print('test')
 | 
			
		||||
    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')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@bp.route('/dashboard2')
 | 
			
		||||
@login_required
 | 
			
		||||
def dashboard2():
 | 
			
		||||
    return render_template('main/dashboard2.html.j2', title='Dashboard')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@bp.route('/user_manual')
 | 
			
		||||
def user_manual():
 | 
			
		||||
    return render_template('main/user_manual.html.j2', title='User manual')
 | 
			
		||||
@@ -55,6 +49,7 @@ def privacy_policy():
 | 
			
		||||
def terms_of_use():
 | 
			
		||||
    return render_template('main/terms_of_use.html.j2', title='Terms of Use')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@bp.route('/social-area')
 | 
			
		||||
def social_area():
 | 
			
		||||
    users = [
 | 
			
		||||
 
 | 
			
		||||
@@ -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,118 +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;
 | 
			
		||||
      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) {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      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 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 }};
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -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 }}
 | 
			
		||||
                  <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>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user