mirror of
				https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
				synced 2025-11-04 12:22:47 +00:00 
			
		
		
		
	Merge branch 'development'
This commit is contained in:
		
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -6,6 +6,8 @@ logs/
 | 
				
			|||||||
!logs/dummy
 | 
					!logs/dummy
 | 
				
			||||||
*.env
 | 
					*.env
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*.pjentsch-testing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Byte-compiled / optimized / DLL files
 | 
					# Byte-compiled / optimized / DLL files
 | 
				
			||||||
__pycache__/
 | 
					__pycache__/
 | 
				
			||||||
*.py[cod]
 | 
					*.py[cod]
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										24
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										24
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							@@ -1 +1,23 @@
 | 
				
			|||||||
{}
 | 
					{
 | 
				
			||||||
 | 
					    "editor.rulers": [79],
 | 
				
			||||||
 | 
					    "files.insertFinalNewline": true,
 | 
				
			||||||
 | 
					    "python.terminal.activateEnvironment": false,
 | 
				
			||||||
 | 
					    "[css]": {
 | 
				
			||||||
 | 
					        "editor.tabSize": 2
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "[scss]": {
 | 
				
			||||||
 | 
					        "editor.tabSize": 2
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "[html]": {
 | 
				
			||||||
 | 
					        "editor.tabSize": 2
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "[javascript]": {
 | 
				
			||||||
 | 
					        "editor.tabSize": 2
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "[jinja-html]": {
 | 
				
			||||||
 | 
					        "editor.tabSize": 2
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "[jinja-js]": {
 | 
				
			||||||
 | 
					        "editor.tabSize": 2
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@ from docker import DockerClient
 | 
				
			|||||||
from flask import Flask
 | 
					from flask import Flask
 | 
				
			||||||
from flask_apscheduler import APScheduler
 | 
					from flask_apscheduler import APScheduler
 | 
				
			||||||
from flask_assets import Environment
 | 
					from flask_assets import Environment
 | 
				
			||||||
 | 
					from flask_breadcrumbs import Breadcrumbs, default_breadcrumb_root
 | 
				
			||||||
from flask_login import LoginManager
 | 
					from flask_login import LoginManager
 | 
				
			||||||
from flask_mail import Mail
 | 
					from flask_mail import Mail
 | 
				
			||||||
from flask_marshmallow import Marshmallow
 | 
					from flask_marshmallow import Marshmallow
 | 
				
			||||||
@@ -12,10 +13,12 @@ from flask_paranoid import Paranoid
 | 
				
			|||||||
from flask_socketio import SocketIO
 | 
					from flask_socketio import SocketIO
 | 
				
			||||||
from flask_sqlalchemy import SQLAlchemy
 | 
					from flask_sqlalchemy import SQLAlchemy
 | 
				
			||||||
from flask_hashids import Hashids
 | 
					from flask_hashids import Hashids
 | 
				
			||||||
 | 
					from werkzeug.exceptions import HTTPException
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
apifairy = APIFairy()
 | 
					apifairy = APIFairy()
 | 
				
			||||||
assets = Environment()
 | 
					assets = Environment()
 | 
				
			||||||
 | 
					breadcrumbs = Breadcrumbs()
 | 
				
			||||||
db = SQLAlchemy()
 | 
					db = SQLAlchemy()
 | 
				
			||||||
docker_client = DockerClient()
 | 
					docker_client = DockerClient()
 | 
				
			||||||
hashids = Hashids()
 | 
					hashids = Hashids()
 | 
				
			||||||
@@ -33,7 +36,7 @@ socketio = SocketIO()
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
def create_app(config: Config = Config) -> Flask:
 | 
					def create_app(config: Config = Config) -> Flask:
 | 
				
			||||||
    ''' Creates an initialized Flask (WSGI Application) object. '''
 | 
					    ''' Creates an initialized Flask (WSGI Application) object. '''
 | 
				
			||||||
    app: Flask = Flask(__name__)
 | 
					    app = Flask(__name__)
 | 
				
			||||||
    app.config.from_object(config)
 | 
					    app.config.from_object(config)
 | 
				
			||||||
    config.init_app(app)
 | 
					    config.init_app(app)
 | 
				
			||||||
    docker_client.login(
 | 
					    docker_client.login(
 | 
				
			||||||
@@ -44,6 +47,7 @@ def create_app(config: Config = Config) -> Flask:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    apifairy.init_app(app)
 | 
					    apifairy.init_app(app)
 | 
				
			||||||
    assets.init_app(app)
 | 
					    assets.init_app(app)
 | 
				
			||||||
 | 
					    breadcrumbs.init_app(app)
 | 
				
			||||||
    db.init_app(app)
 | 
					    db.init_app(app)
 | 
				
			||||||
    hashids.init_app(app)
 | 
					    hashids.init_app(app)
 | 
				
			||||||
    login.init_app(app)
 | 
					    login.init_app(app)
 | 
				
			||||||
@@ -55,36 +59,45 @@ def create_app(config: Config = Config) -> Flask:
 | 
				
			|||||||
    socketio.init_app(app, message_queue=app.config['NOPAQUE_SOCKETIO_MESSAGE_QUEUE_URI'])  # noqa
 | 
					    socketio.init_app(app, message_queue=app.config['NOPAQUE_SOCKETIO_MESSAGE_QUEUE_URI'])  # noqa
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    from .admin import bp as admin_blueprint
 | 
					    from .admin import bp as admin_blueprint
 | 
				
			||||||
 | 
					    default_breadcrumb_root(admin_blueprint, '.admin')
 | 
				
			||||||
    app.register_blueprint(admin_blueprint, url_prefix='/admin')
 | 
					    app.register_blueprint(admin_blueprint, url_prefix='/admin')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    from .api import bp as api_blueprint
 | 
					    from .api import bp as api_blueprint
 | 
				
			||||||
    app.register_blueprint(api_blueprint, url_prefix='/api')
 | 
					    app.register_blueprint(api_blueprint, url_prefix='/api')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    from .auth import bp as auth_blueprint
 | 
					    from .auth import bp as auth_blueprint
 | 
				
			||||||
    app.register_blueprint(auth_blueprint, url_prefix='/auth')
 | 
					    default_breadcrumb_root(auth_blueprint, '.')
 | 
				
			||||||
 | 
					    app.register_blueprint(auth_blueprint)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    from .contributions import bp as contributions_blueprint
 | 
					    from .contributions import bp as contributions_blueprint
 | 
				
			||||||
 | 
					    default_breadcrumb_root(contributions_blueprint, '.contributions')
 | 
				
			||||||
    app.register_blueprint(contributions_blueprint, url_prefix='/contributions')
 | 
					    app.register_blueprint(contributions_blueprint, url_prefix='/contributions')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    from .corpora import bp as corpora_blueprint
 | 
					    from .corpora import bp as corpora_blueprint
 | 
				
			||||||
    app.register_blueprint(corpora_blueprint, url_prefix='/corpora')
 | 
					    default_breadcrumb_root(corpora_blueprint, '.corpora')
 | 
				
			||||||
 | 
					    app.register_blueprint(corpora_blueprint, cli_group='corpus', url_prefix='/corpora')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    from .errors import bp as errors_blueprint
 | 
					    from .errors import bp as errors_bp
 | 
				
			||||||
    app.register_blueprint(errors_blueprint)
 | 
					    app.register_blueprint(errors_bp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    from .jobs import bp as jobs_blueprint
 | 
					    from .jobs import bp as jobs_blueprint
 | 
				
			||||||
 | 
					    default_breadcrumb_root(jobs_blueprint, '.jobs')
 | 
				
			||||||
    app.register_blueprint(jobs_blueprint, url_prefix='/jobs')
 | 
					    app.register_blueprint(jobs_blueprint, url_prefix='/jobs')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    from .main import bp as main_blueprint
 | 
					    from .main import bp as main_blueprint
 | 
				
			||||||
    app.register_blueprint(main_blueprint, url_prefix='/')
 | 
					    default_breadcrumb_root(main_blueprint, '.')
 | 
				
			||||||
 | 
					    app.register_blueprint(main_blueprint, cli_group=None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    from .services import bp as services_blueprint
 | 
					    from .services import bp as services_blueprint
 | 
				
			||||||
 | 
					    default_breadcrumb_root(services_blueprint, '.services')
 | 
				
			||||||
    app.register_blueprint(services_blueprint, url_prefix='/services')
 | 
					    app.register_blueprint(services_blueprint, url_prefix='/services')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    from .settings import bp as settings_blueprint
 | 
					    from .settings import bp as settings_blueprint
 | 
				
			||||||
 | 
					    default_breadcrumb_root(settings_blueprint, '.settings')
 | 
				
			||||||
    app.register_blueprint(settings_blueprint, url_prefix='/settings')
 | 
					    app.register_blueprint(settings_blueprint, url_prefix='/settings')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    from .users import bp as users_blueprint
 | 
					    from .users import bp as users_blueprint
 | 
				
			||||||
 | 
					    default_breadcrumb_root(users_blueprint, '.users')
 | 
				
			||||||
    app.register_blueprint(users_blueprint, url_prefix='/users')
 | 
					    app.register_blueprint(users_blueprint, url_prefix='/users')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return app
 | 
					    return app
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,20 @@
 | 
				
			|||||||
from flask import Blueprint
 | 
					from flask import Blueprint
 | 
				
			||||||
 | 
					from flask_login import login_required
 | 
				
			||||||
 | 
					from app.decorators import admin_required
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
bp = Blueprint('admin', __name__)
 | 
					bp = Blueprint('admin', __name__)
 | 
				
			||||||
from . import routes
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.before_request
 | 
				
			||||||
 | 
					@login_required
 | 
				
			||||||
 | 
					@admin_required
 | 
				
			||||||
 | 
					def before_request():
 | 
				
			||||||
 | 
					    '''
 | 
				
			||||||
 | 
					    Ensures that the routes in this package can be visited only by users with
 | 
				
			||||||
 | 
					    administrator privileges (login_required and admin_required).
 | 
				
			||||||
 | 
					    '''
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from . import json_routes, routes
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,13 +1,16 @@
 | 
				
			|||||||
from app.models import Role
 | 
					 | 
				
			||||||
from flask_wtf import FlaskForm
 | 
					from flask_wtf import FlaskForm
 | 
				
			||||||
from wtforms import BooleanField, SelectField, SubmitField
 | 
					from wtforms import SelectField, SubmitField
 | 
				
			||||||
 | 
					from app.models import Role
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AdminEditUserForm(FlaskForm):
 | 
					class UpdateUserForm(FlaskForm):
 | 
				
			||||||
    confirmed = BooleanField('Confirmed')
 | 
					 | 
				
			||||||
    role = SelectField('Role')
 | 
					    role = SelectField('Role')
 | 
				
			||||||
    submit = SubmitField('Submit')
 | 
					    submit = SubmitField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, *args, **kwargs):
 | 
					    def __init__(self, user, *args, **kwargs):
 | 
				
			||||||
 | 
					        if 'data' not in kwargs:
 | 
				
			||||||
 | 
					            kwargs['data'] = {'role': user.role.hashid}
 | 
				
			||||||
 | 
					        if 'prefix' not in kwargs:
 | 
				
			||||||
 | 
					            kwargs['prefix'] = 'update-user-form'
 | 
				
			||||||
        super().__init__(*args, **kwargs)
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
        self.role.choices = [(x.hashid, x.name) for x in Role.query.all()]
 | 
					        self.role.choices = [(x.hashid, x.name) for x in Role.query.all()]
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										23
									
								
								app/admin/json_routes.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								app/admin/json_routes.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					from flask import abort, request
 | 
				
			||||||
 | 
					from app import db
 | 
				
			||||||
 | 
					from app.decorators import content_negotiation
 | 
				
			||||||
 | 
					from app.models import User
 | 
				
			||||||
 | 
					from . import bp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/users/<hashid:user_id>/confirmed', methods=['PUT'])
 | 
				
			||||||
 | 
					@content_negotiation(consumes='application/json', produces='application/json')
 | 
				
			||||||
 | 
					def update_user_role(user_id):
 | 
				
			||||||
 | 
					    confirmed = request.json
 | 
				
			||||||
 | 
					    if not isinstance(confirmed, bool):
 | 
				
			||||||
 | 
					        abort(400)
 | 
				
			||||||
 | 
					    user = User.query.get_or_404(user_id)
 | 
				
			||||||
 | 
					    user.confirmed = confirmed
 | 
				
			||||||
 | 
					    db.session.commit()
 | 
				
			||||||
 | 
					    response_data = {
 | 
				
			||||||
 | 
					        'message': (
 | 
				
			||||||
 | 
					            f'User "{user.username}" is now '
 | 
				
			||||||
 | 
					            f'{"confirmed" if confirmed else "unconfirmed"}'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return response_data, 200
 | 
				
			||||||
@@ -1,111 +1,146 @@
 | 
				
			|||||||
from flask import current_app, flash, redirect, render_template, url_for
 | 
					from flask import abort, flash, redirect, render_template, url_for
 | 
				
			||||||
from flask_login import login_required
 | 
					from flask_breadcrumbs import register_breadcrumb
 | 
				
			||||||
from threading import Thread
 | 
					 | 
				
			||||||
from app import db, hashids
 | 
					from app import db, hashids
 | 
				
			||||||
from app.decorators import admin_required
 | 
					from app.models import Avatar, Corpus, Role, User
 | 
				
			||||||
from app.models import Role, User, UserSettingJobStatusMailNotificationLevel
 | 
					from app.users.settings.forms import (
 | 
				
			||||||
from app.settings.forms import (
 | 
					    UpdateAvatarForm,
 | 
				
			||||||
    EditNotificationSettingsForm
 | 
					    UpdatePasswordForm,
 | 
				
			||||||
 | 
					    UpdateNotificationsForm,
 | 
				
			||||||
 | 
					    UpdateAccountInformationForm,
 | 
				
			||||||
 | 
					    UpdateProfileInformationForm
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from app.users.forms import EditProfileSettingsForm
 | 
					 | 
				
			||||||
from . import bp
 | 
					from . import bp
 | 
				
			||||||
from .forms import AdminEditUserForm
 | 
					from .forms import UpdateUserForm
 | 
				
			||||||
 | 
					from app.users.utils import (
 | 
				
			||||||
 | 
					    user_endpoint_arguments_constructor as user_eac,
 | 
				
			||||||
@bp.before_request
 | 
					    user_dynamic_list_constructor as user_dlc
 | 
				
			||||||
@login_required
 | 
					)
 | 
				
			||||||
@admin_required
 | 
					 | 
				
			||||||
def before_request():
 | 
					 | 
				
			||||||
    '''
 | 
					 | 
				
			||||||
    Ensures that the routes in this package can be visited only by users with
 | 
					 | 
				
			||||||
    administrator privileges (login_required and admin_required).
 | 
					 | 
				
			||||||
    '''
 | 
					 | 
				
			||||||
    pass
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('')
 | 
					@bp.route('')
 | 
				
			||||||
def index():
 | 
					@register_breadcrumb(bp, '.', '<i class="material-icons left">admin_panel_settings</i>Administration')
 | 
				
			||||||
    return redirect(url_for('.users'))
 | 
					def admin():
 | 
				
			||||||
 | 
					    return render_template(
 | 
				
			||||||
 | 
					        'admin/admin.html.j2',
 | 
				
			||||||
 | 
					        title='Administration'
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/corpora')
 | 
				
			||||||
 | 
					@register_breadcrumb(bp, '.corpora', 'Corpora')
 | 
				
			||||||
 | 
					def corpora():
 | 
				
			||||||
 | 
					    corpora = Corpus.query.all()
 | 
				
			||||||
 | 
					    return render_template(
 | 
				
			||||||
 | 
					        'admin/corpora.html.j2',
 | 
				
			||||||
 | 
					        title='Corpora',
 | 
				
			||||||
 | 
					        corpora=corpora
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('/users')
 | 
					@bp.route('/users')
 | 
				
			||||||
 | 
					@register_breadcrumb(bp, '.users', '<i class="material-icons left">group</i>Users')
 | 
				
			||||||
def users():
 | 
					def users():
 | 
				
			||||||
    users = [x.to_json_serializeable(backrefs=True) for x in User.query.all()]
 | 
					    users = User.query.all()
 | 
				
			||||||
    return render_template(
 | 
					    return render_template(
 | 
				
			||||||
        'admin/users.html.j2',
 | 
					        'admin/users.html.j2',
 | 
				
			||||||
        users=users,
 | 
					        title='Users',
 | 
				
			||||||
        title='Users'
 | 
					        users=users
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('/users/<hashid:user_id>')
 | 
					@bp.route('/users/<hashid:user_id>')
 | 
				
			||||||
 | 
					@register_breadcrumb(bp, '.users.entity', '', dynamic_list_constructor=user_dlc)
 | 
				
			||||||
def user(user_id):
 | 
					def user(user_id):
 | 
				
			||||||
    user = User.query.get_or_404(user_id)
 | 
					    user = User.query.get_or_404(user_id)
 | 
				
			||||||
    return render_template('admin/user.html.j2', title='User', user=user)
 | 
					    corpora = Corpus.query.filter(Corpus.user == user).all()
 | 
				
			||||||
 | 
					    return render_template(
 | 
				
			||||||
 | 
					        'admin/user.html.j2',
 | 
				
			||||||
 | 
					        title=user.username,
 | 
				
			||||||
 | 
					        user=user,
 | 
				
			||||||
 | 
					        corpora=corpora
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('/users/<hashid:user_id>/edit', methods=['GET', 'POST'])
 | 
					@bp.route('/users/<hashid:user_id>/settings', methods=['GET', 'POST'])
 | 
				
			||||||
def edit_user(user_id):
 | 
					@register_breadcrumb(bp, '.users.entity.settings', '<i class="material-icons left">settings</i>Settings')
 | 
				
			||||||
 | 
					def user_settings(user_id):
 | 
				
			||||||
    user = User.query.get_or_404(user_id)
 | 
					    user = User.query.get_or_404(user_id)
 | 
				
			||||||
    admin_edit_user_form = AdminEditUserForm(
 | 
					    update_account_information_form = UpdateAccountInformationForm(user)
 | 
				
			||||||
        data={'confirmed': user.confirmed, 'role': user.role.hashid},
 | 
					    update_profile_information_form = UpdateProfileInformationForm(user)
 | 
				
			||||||
        prefix='admin-edit-user-form'
 | 
					    update_avatar_form = UpdateAvatarForm()
 | 
				
			||||||
 | 
					    update_password_form = UpdatePasswordForm(user)
 | 
				
			||||||
 | 
					    update_notifications_form = UpdateNotificationsForm(user)
 | 
				
			||||||
 | 
					    update_user_form = UpdateUserForm(user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # region handle update profile information form
 | 
				
			||||||
 | 
					    if update_profile_information_form.submit.data and update_profile_information_form.validate():
 | 
				
			||||||
 | 
					        user.about_me = update_profile_information_form.about_me.data
 | 
				
			||||||
 | 
					        user.location = update_profile_information_form.location.data
 | 
				
			||||||
 | 
					        user.organization = update_profile_information_form.organization.data
 | 
				
			||||||
 | 
					        user.website = update_profile_information_form.website.data
 | 
				
			||||||
 | 
					        user.full_name = update_profile_information_form.full_name.data
 | 
				
			||||||
 | 
					        db.session.commit()
 | 
				
			||||||
 | 
					        flash('Your changes have been saved')
 | 
				
			||||||
 | 
					        return redirect(url_for('.user_settings', user_id=user.id))
 | 
				
			||||||
 | 
					    # endregion handle update profile information form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # region handle update avatar form
 | 
				
			||||||
 | 
					    if update_avatar_form.submit.data and update_avatar_form.validate():
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            Avatar.create(
 | 
				
			||||||
 | 
					                update_avatar_form.avatar.data,
 | 
				
			||||||
 | 
					                user=user
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
    edit_profile_settings_form = EditProfileSettingsForm(
 | 
					        except (AttributeError, OSError):
 | 
				
			||||||
        user,
 | 
					            abort(500)
 | 
				
			||||||
        data=user.to_json_serializeable(),
 | 
					        db.session.commit()
 | 
				
			||||||
        prefix='edit-profile-settings-form'
 | 
					        flash('Your changes have been saved')
 | 
				
			||||||
    )
 | 
					        return redirect(url_for('.user_settings', user_id=user.id))
 | 
				
			||||||
    edit_notification_settings_form = EditNotificationSettingsForm(
 | 
					    # endregion handle update avatar form
 | 
				
			||||||
        data=user.to_json_serializeable(),
 | 
					
 | 
				
			||||||
        prefix='edit-notification-settings-form'
 | 
					    # region handle update account information form
 | 
				
			||||||
    )
 | 
					    if update_account_information_form.submit.data and update_account_information_form.validate():
 | 
				
			||||||
    if (admin_edit_user_form.submit.data
 | 
					        user.email = update_account_information_form.email.data
 | 
				
			||||||
            and admin_edit_user_form.validate()):
 | 
					        user.username = update_account_information_form.username.data
 | 
				
			||||||
        user.confirmed = admin_edit_user_form.confirmed.data
 | 
					        db.session.commit()
 | 
				
			||||||
        role_id = hashids.decode(admin_edit_user_form.role.data)
 | 
					        flash('Profile settings updated')
 | 
				
			||||||
 | 
					        return redirect(url_for('.user_settings', user_id=user.id))
 | 
				
			||||||
 | 
					    # endregion handle update account information form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # region handle update password form
 | 
				
			||||||
 | 
					    if update_password_form.submit.data and update_password_form.validate():
 | 
				
			||||||
 | 
					        user.password = update_password_form.new_password.data
 | 
				
			||||||
 | 
					        db.session.commit()
 | 
				
			||||||
 | 
					        flash('Your changes have been saved')
 | 
				
			||||||
 | 
					        return redirect(url_for('.user_settings', user_id=user.id))
 | 
				
			||||||
 | 
					    # endregion handle update password form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # region handle update notifications form
 | 
				
			||||||
 | 
					    if update_notifications_form.submit.data and update_notifications_form.validate():
 | 
				
			||||||
 | 
					        user.setting_job_status_mail_notification_level = \
 | 
				
			||||||
 | 
					            update_notifications_form.job_status_mail_notification_level.data
 | 
				
			||||||
 | 
					        db.session.commit()
 | 
				
			||||||
 | 
					        flash('Your changes have been saved')
 | 
				
			||||||
 | 
					        return redirect(url_for('.user_settings', user_id=user.id))
 | 
				
			||||||
 | 
					    # endregion handle update notifications form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # region handle update user form
 | 
				
			||||||
 | 
					    if update_user_form.submit.data and update_user_form.validate():
 | 
				
			||||||
 | 
					        role_id = hashids.decode(update_user_form.role.data)
 | 
				
			||||||
        user.role = Role.query.get(role_id)
 | 
					        user.role = Role.query.get(role_id)
 | 
				
			||||||
        db.session.commit()
 | 
					        db.session.commit()
 | 
				
			||||||
        flash('Your changes have been saved')
 | 
					        flash('Your changes have been saved')
 | 
				
			||||||
        return redirect(url_for('.edit_user', user_id=user.id))
 | 
					        return redirect(url_for('.user_settings', user_id=user.id))
 | 
				
			||||||
    if (edit_profile_settings_form.submit.data
 | 
					    # endregion handle update user form
 | 
				
			||||||
            and edit_profile_settings_form.validate()):
 | 
					
 | 
				
			||||||
        user.email = edit_profile_settings_form.email.data
 | 
					 | 
				
			||||||
        user.username = edit_profile_settings_form.username.data
 | 
					 | 
				
			||||||
        db.session.commit()
 | 
					 | 
				
			||||||
        flash('Your changes have been saved')
 | 
					 | 
				
			||||||
        return redirect(url_for('.edit_user', user_id=user.id))
 | 
					 | 
				
			||||||
    if (edit_notification_settings_form.submit.data
 | 
					 | 
				
			||||||
            and edit_notification_settings_form.validate()):
 | 
					 | 
				
			||||||
        user.setting_job_status_mail_notification_level = \
 | 
					 | 
				
			||||||
            UserSettingJobStatusMailNotificationLevel[
 | 
					 | 
				
			||||||
                edit_notification_settings_form.job_status_mail_notification_level.data  # noqa
 | 
					 | 
				
			||||||
            ]
 | 
					 | 
				
			||||||
        db.session.commit()
 | 
					 | 
				
			||||||
        flash('Your changes have been saved')
 | 
					 | 
				
			||||||
        return redirect(url_for('.edit_user', user_id=user.id))
 | 
					 | 
				
			||||||
    return render_template(
 | 
					    return render_template(
 | 
				
			||||||
        'admin/edit_user.html.j2',
 | 
					        'admin/user_settings.html.j2',
 | 
				
			||||||
        admin_edit_user_form=admin_edit_user_form,
 | 
					        title='Settings',
 | 
				
			||||||
        edit_profile_settings_form=edit_profile_settings_form,
 | 
					        update_account_information_form=update_account_information_form,
 | 
				
			||||||
        edit_notification_settings_form=edit_notification_settings_form,
 | 
					        update_avatar_form=update_avatar_form,
 | 
				
			||||||
        title='Edit user',
 | 
					        update_notifications_form=update_notifications_form,
 | 
				
			||||||
 | 
					        update_password_form=update_password_form,
 | 
				
			||||||
 | 
					        update_profile_information_form=update_profile_information_form,
 | 
				
			||||||
 | 
					        update_user_form=update_user_form,
 | 
				
			||||||
        user=user
 | 
					        user=user
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.route('/users/<hashid:user_id>/delete', methods=['DELETE'])
 | 
					 | 
				
			||||||
def delete_user(user_id):
 | 
					 | 
				
			||||||
    def _delete_user(app, user_id):
 | 
					 | 
				
			||||||
        with app.app_context():
 | 
					 | 
				
			||||||
            user = User.query.get(user_id)
 | 
					 | 
				
			||||||
            user.delete()
 | 
					 | 
				
			||||||
            db.session.commit()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    User.query.get_or_404(user_id)
 | 
					 | 
				
			||||||
    thread = Thread(
 | 
					 | 
				
			||||||
        target=_delete_user,
 | 
					 | 
				
			||||||
        args=(current_app._get_current_object(), user_id)
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    thread.start()
 | 
					 | 
				
			||||||
    return {}, 202
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,6 @@ from apifairy.fields import FileField
 | 
				
			|||||||
from marshmallow import validate, validates, ValidationError
 | 
					from marshmallow import validate, validates, ValidationError
 | 
				
			||||||
from marshmallow.decorators import post_dump
 | 
					from marshmallow.decorators import post_dump
 | 
				
			||||||
from app import ma
 | 
					from app import ma
 | 
				
			||||||
from app.auth import USERNAME_REGEX
 | 
					 | 
				
			||||||
from app.models import (
 | 
					from app.models import (
 | 
				
			||||||
    Job,
 | 
					    Job,
 | 
				
			||||||
    JobStatus,
 | 
					    JobStatus,
 | 
				
			||||||
@@ -142,7 +141,10 @@ class UserSchema(ma.SQLAlchemySchema):
 | 
				
			|||||||
    username = ma.auto_field(
 | 
					    username = ma.auto_field(
 | 
				
			||||||
        validate=[
 | 
					        validate=[
 | 
				
			||||||
            validate.Length(min=1, max=64),
 | 
					            validate.Length(min=1, max=64),
 | 
				
			||||||
            validate.Regexp(USERNAME_REGEX, error='Usernames must have only letters, numbers, dots or underscores')
 | 
					            validate.Regexp(
 | 
				
			||||||
 | 
					                User.username_pattern,
 | 
				
			||||||
 | 
					                error='Usernames must have only letters, numbers, dots or underscores'
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    email = ma.auto_field(validate=validate.Email())
 | 
					    email = ma.auto_field(validate=validate.Email())
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,5 @@
 | 
				
			|||||||
from flask import Blueprint
 | 
					from flask import Blueprint
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
USERNAME_REGEX = '^[A-Za-zÄÖÜäöüß0-9_.]*$'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
bp = Blueprint('auth', __name__)
 | 
					bp = Blueprint('auth', __name__)
 | 
				
			||||||
from . import routes
 | 
					from . import routes
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,6 @@ from wtforms import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
from wtforms.validators import InputRequired, Email, EqualTo, Length, Regexp
 | 
					from wtforms.validators import InputRequired, Email, EqualTo, Length, Regexp
 | 
				
			||||||
from app.models import User
 | 
					from app.models import User
 | 
				
			||||||
from . import USERNAME_REGEX
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class RegistrationForm(FlaskForm):
 | 
					class RegistrationForm(FlaskForm):
 | 
				
			||||||
@@ -22,7 +21,7 @@ class RegistrationForm(FlaskForm):
 | 
				
			|||||||
            InputRequired(),
 | 
					            InputRequired(),
 | 
				
			||||||
            Length(max=64),
 | 
					            Length(max=64),
 | 
				
			||||||
            Regexp(
 | 
					            Regexp(
 | 
				
			||||||
                USERNAME_REGEX,
 | 
					                User.username_pattern,
 | 
				
			||||||
                message=(
 | 
					                message=(
 | 
				
			||||||
                    'Usernames must have only letters, numbers, dots or '
 | 
					                    'Usernames must have only letters, numbers, dots or '
 | 
				
			||||||
                    'underscores'
 | 
					                    'underscores'
 | 
				
			||||||
@@ -44,8 +43,17 @@ class RegistrationForm(FlaskForm):
 | 
				
			|||||||
            EqualTo('password', message='Passwords must match')
 | 
					            EqualTo('password', message='Passwords must match')
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					    terms_of_use_accepted = BooleanField(
 | 
				
			||||||
 | 
					        'I have read and accept the terms of use',
 | 
				
			||||||
 | 
					        validators=[InputRequired()]
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    submit = SubmitField()
 | 
					    submit = SubmitField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        if 'prefix' not in kwargs:
 | 
				
			||||||
 | 
					            kwargs['prefix'] = 'registration-form'
 | 
				
			||||||
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def validate_email(self, field):
 | 
					    def validate_email(self, field):
 | 
				
			||||||
        if User.query.filter_by(email=field.data.lower()).first():
 | 
					        if User.query.filter_by(email=field.data.lower()).first():
 | 
				
			||||||
            raise ValidationError('Email already registered')
 | 
					            raise ValidationError('Email already registered')
 | 
				
			||||||
@@ -61,11 +69,21 @@ class LoginForm(FlaskForm):
 | 
				
			|||||||
    remember_me = BooleanField('Keep me logged in')
 | 
					    remember_me = BooleanField('Keep me logged in')
 | 
				
			||||||
    submit = SubmitField()
 | 
					    submit = SubmitField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        if 'prefix' not in kwargs:
 | 
				
			||||||
 | 
					            kwargs['prefix'] = 'login-form'
 | 
				
			||||||
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ResetPasswordRequestForm(FlaskForm):
 | 
					class ResetPasswordRequestForm(FlaskForm):
 | 
				
			||||||
    email = StringField('Email', validators=[InputRequired(), Email()])
 | 
					    email = StringField('Email', validators=[InputRequired(), Email()])
 | 
				
			||||||
    submit = SubmitField()
 | 
					    submit = SubmitField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        if 'prefix' not in kwargs:
 | 
				
			||||||
 | 
					            kwargs['prefix'] = 'reset-password-request-form'
 | 
				
			||||||
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ResetPasswordForm(FlaskForm):
 | 
					class ResetPasswordForm(FlaskForm):
 | 
				
			||||||
    password = PasswordField(
 | 
					    password = PasswordField(
 | 
				
			||||||
@@ -83,3 +101,8 @@ class ResetPasswordForm(FlaskForm):
 | 
				
			|||||||
        ]
 | 
					        ]
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    submit = SubmitField()
 | 
					    submit = SubmitField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        if 'prefix' not in kwargs:
 | 
				
			||||||
 | 
					            kwargs['prefix'] = 'reset-password-form'
 | 
				
			||||||
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,5 @@
 | 
				
			|||||||
from flask import (
 | 
					from flask import abort, flash, redirect, render_template, request, url_for
 | 
				
			||||||
    abort,
 | 
					from flask_breadcrumbs import register_breadcrumb
 | 
				
			||||||
    flash,
 | 
					 | 
				
			||||||
    redirect,
 | 
					 | 
				
			||||||
    render_template,
 | 
					 | 
				
			||||||
    request,
 | 
					 | 
				
			||||||
    url_for
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
from flask_login import current_user, login_user, login_required, logout_user
 | 
					from flask_login import current_user, login_user, login_required, logout_user
 | 
				
			||||||
from app import db
 | 
					from app import db
 | 
				
			||||||
from app.email import create_message, send
 | 
					from app.email import create_message, send
 | 
				
			||||||
@@ -36,16 +30,18 @@ def before_request():
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('/register', methods=['GET', 'POST'])
 | 
					@bp.route('/register', methods=['GET', 'POST'])
 | 
				
			||||||
 | 
					@register_breadcrumb(bp, '.register', 'Register')
 | 
				
			||||||
def register():
 | 
					def register():
 | 
				
			||||||
    if current_user.is_authenticated:
 | 
					    if current_user.is_authenticated:
 | 
				
			||||||
        return redirect(url_for('main.dashboard'))
 | 
					        return redirect(url_for('main.dashboard'))
 | 
				
			||||||
    form = RegistrationForm(prefix='registration-form')
 | 
					    form = RegistrationForm()
 | 
				
			||||||
    if form.validate_on_submit():
 | 
					    if form.validate_on_submit():
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            user = User.create(
 | 
					            user = User.create(
 | 
				
			||||||
                email=form.email.data.lower(),
 | 
					                email=form.email.data.lower(),
 | 
				
			||||||
                password=form.password.data,
 | 
					                password=form.password.data,
 | 
				
			||||||
                username=form.username.data
 | 
					                username=form.username.data,
 | 
				
			||||||
 | 
					                terms_of_use_accepted=form.terms_of_use_accepted.data
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        except OSError:
 | 
					        except OSError:
 | 
				
			||||||
            flash('Internal Server Error', category='error')
 | 
					            flash('Internal Server Error', category='error')
 | 
				
			||||||
@@ -65,16 +61,17 @@ def register():
 | 
				
			|||||||
        return redirect(url_for('.login'))
 | 
					        return redirect(url_for('.login'))
 | 
				
			||||||
    return render_template(
 | 
					    return render_template(
 | 
				
			||||||
        'auth/register.html.j2',
 | 
					        'auth/register.html.j2',
 | 
				
			||||||
        form=form,
 | 
					        title='Register',
 | 
				
			||||||
        title='Register'
 | 
					        form=form
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('/login', methods=['GET', 'POST'])
 | 
					@bp.route('/login', methods=['GET', 'POST'])
 | 
				
			||||||
 | 
					@register_breadcrumb(bp, '.login', 'Login')
 | 
				
			||||||
def login():
 | 
					def login():
 | 
				
			||||||
    if current_user.is_authenticated:
 | 
					    if current_user.is_authenticated:
 | 
				
			||||||
        return redirect(url_for('main.dashboard'))
 | 
					        return redirect(url_for('main.dashboard'))
 | 
				
			||||||
    form = LoginForm(prefix='login-form')
 | 
					    form = LoginForm()
 | 
				
			||||||
    if form.validate_on_submit():
 | 
					    if form.validate_on_submit():
 | 
				
			||||||
        user = User.query.filter((User.email == form.user.data.lower()) | (User.username == form.user.data)).first()
 | 
					        user = User.query.filter((User.email == form.user.data.lower()) | (User.username == form.user.data)).first()
 | 
				
			||||||
        if user and user.verify_password(form.password.data):
 | 
					        if user and user.verify_password(form.password.data):
 | 
				
			||||||
@@ -85,7 +82,11 @@ def login():
 | 
				
			|||||||
            flash('You have been logged in')
 | 
					            flash('You have been logged in')
 | 
				
			||||||
            return redirect(next)
 | 
					            return redirect(next)
 | 
				
			||||||
        flash('Invalid email/username or password', category='error')
 | 
					        flash('Invalid email/username or password', category='error')
 | 
				
			||||||
    return render_template('auth/login.html.j2', form=form, title='Log in')
 | 
					    return render_template(
 | 
				
			||||||
 | 
					        'auth/login.html.j2',
 | 
				
			||||||
 | 
					        title='Log in',
 | 
				
			||||||
 | 
					        form=form
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('/logout')
 | 
					@bp.route('/logout')
 | 
				
			||||||
@@ -97,14 +98,18 @@ def logout():
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('/unconfirmed')
 | 
					@bp.route('/unconfirmed')
 | 
				
			||||||
 | 
					@register_breadcrumb(bp, '.unconfirmed', 'Unconfirmed')
 | 
				
			||||||
@login_required
 | 
					@login_required
 | 
				
			||||||
def unconfirmed():
 | 
					def unconfirmed():
 | 
				
			||||||
    if current_user.confirmed:
 | 
					    if current_user.confirmed:
 | 
				
			||||||
        return redirect(url_for('main.dashboard'))
 | 
					        return redirect(url_for('main.dashboard'))
 | 
				
			||||||
    return render_template('auth/unconfirmed.html.j2', title='Unconfirmed')
 | 
					    return render_template(
 | 
				
			||||||
 | 
					        'auth/unconfirmed.html.j2',
 | 
				
			||||||
 | 
					        title='Unconfirmed'
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('/confirm')
 | 
					@bp.route('/confirm-request')
 | 
				
			||||||
@login_required
 | 
					@login_required
 | 
				
			||||||
def confirm_request():
 | 
					def confirm_request():
 | 
				
			||||||
    if current_user.confirmed:
 | 
					    if current_user.confirmed:
 | 
				
			||||||
@@ -135,11 +140,12 @@ def confirm(token):
 | 
				
			|||||||
    return redirect(url_for('.unconfirmed'))
 | 
					    return redirect(url_for('.unconfirmed'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('/reset_password', methods=['GET', 'POST'])
 | 
					@bp.route('/reset-password-request', methods=['GET', 'POST'])
 | 
				
			||||||
 | 
					@register_breadcrumb(bp, '.reset_password_request', 'Password Reset')
 | 
				
			||||||
def reset_password_request():
 | 
					def reset_password_request():
 | 
				
			||||||
    if current_user.is_authenticated:
 | 
					    if current_user.is_authenticated:
 | 
				
			||||||
        return redirect(url_for('main.dashboard'))
 | 
					        return redirect(url_for('main.dashboard'))
 | 
				
			||||||
    form = ResetPasswordRequestForm(prefix='reset-password-request-form')
 | 
					    form = ResetPasswordRequestForm()
 | 
				
			||||||
    if form.validate_on_submit():
 | 
					    if form.validate_on_submit():
 | 
				
			||||||
        user = User.query.filter_by(email=form.email.data.lower()).first()
 | 
					        user = User.query.filter_by(email=form.email.data.lower()).first()
 | 
				
			||||||
        if user is not None:
 | 
					        if user is not None:
 | 
				
			||||||
@@ -159,16 +165,17 @@ def reset_password_request():
 | 
				
			|||||||
        return redirect(url_for('.login'))
 | 
					        return redirect(url_for('.login'))
 | 
				
			||||||
    return render_template(
 | 
					    return render_template(
 | 
				
			||||||
        'auth/reset_password_request.html.j2',
 | 
					        'auth/reset_password_request.html.j2',
 | 
				
			||||||
        form=form,
 | 
					        title='Password Reset',
 | 
				
			||||||
        title='Password Reset'
 | 
					        form=form
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('/reset_password/<token>', methods=['GET', 'POST'])
 | 
					@bp.route('/reset-password/<token>', methods=['GET', 'POST'])
 | 
				
			||||||
 | 
					@register_breadcrumb(bp, '.reset_password', 'Password Reset')
 | 
				
			||||||
def reset_password(token):
 | 
					def reset_password(token):
 | 
				
			||||||
    if current_user.is_authenticated:
 | 
					    if current_user.is_authenticated:
 | 
				
			||||||
        return redirect(url_for('main.dashboard'))
 | 
					        return redirect(url_for('main.dashboard'))
 | 
				
			||||||
    form = ResetPasswordForm(prefix='reset-password-form')
 | 
					    form = ResetPasswordForm()
 | 
				
			||||||
    if form.validate_on_submit():
 | 
					    if form.validate_on_submit():
 | 
				
			||||||
        if User.reset_password(token, form.password.data):
 | 
					        if User.reset_password(token, form.password.data):
 | 
				
			||||||
            db.session.commit()
 | 
					            db.session.commit()
 | 
				
			||||||
@@ -177,7 +184,7 @@ def reset_password(token):
 | 
				
			|||||||
        return redirect(url_for('main.index'))
 | 
					        return redirect(url_for('main.index'))
 | 
				
			||||||
    return render_template(
 | 
					    return render_template(
 | 
				
			||||||
        'auth/reset_password.html.j2',
 | 
					        'auth/reset_password.html.j2',
 | 
				
			||||||
        form=form,
 | 
					 | 
				
			||||||
        title='Password Reset',
 | 
					        title='Password Reset',
 | 
				
			||||||
 | 
					        form=form,
 | 
				
			||||||
        token=token
 | 
					        token=token
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										72
									
								
								app/cli.py
									
									
									
									
									
								
							
							
						
						
									
										72
									
								
								app/cli.py
									
									
									
									
									
								
							@@ -1,72 +0,0 @@
 | 
				
			|||||||
from flask import current_app
 | 
					 | 
				
			||||||
from flask_migrate import upgrade
 | 
					 | 
				
			||||||
import click
 | 
					 | 
				
			||||||
import os
 | 
					 | 
				
			||||||
from app.models import (
 | 
					 | 
				
			||||||
    Role,
 | 
					 | 
				
			||||||
    User,
 | 
					 | 
				
			||||||
    TesseractOCRPipelineModel,
 | 
					 | 
				
			||||||
    SpaCyNLPPipelineModel
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def _make_default_dirs():
 | 
					 | 
				
			||||||
    base_dir = current_app.config['NOPAQUE_DATA_DIR']
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    default_directories = [
 | 
					 | 
				
			||||||
        os.path.join(base_dir, 'tmp'),
 | 
					 | 
				
			||||||
        os.path.join(base_dir, 'users')
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
    for directory in default_directories:
 | 
					 | 
				
			||||||
        if os.path.exists(directory):
 | 
					 | 
				
			||||||
            if not os.path.isdir(directory):
 | 
					 | 
				
			||||||
                raise NotADirectoryError(f'{directory} is not a directory')
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            os.mkdir(directory)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def register(app):
 | 
					 | 
				
			||||||
    @app.cli.command()
 | 
					 | 
				
			||||||
    def deploy():
 | 
					 | 
				
			||||||
        ''' Run deployment tasks. '''
 | 
					 | 
				
			||||||
        # Make default directories
 | 
					 | 
				
			||||||
        _make_default_dirs()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # migrate database to latest revision
 | 
					 | 
				
			||||||
        upgrade()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Insert/Update default database values
 | 
					 | 
				
			||||||
        current_app.logger.info('Insert/Update default roles')
 | 
					 | 
				
			||||||
        Role.insert_defaults()
 | 
					 | 
				
			||||||
        current_app.logger.info('Insert/Update default users')
 | 
					 | 
				
			||||||
        User.insert_defaults()
 | 
					 | 
				
			||||||
        current_app.logger.info('Insert/Update default SpaCyNLPPipelineModels')
 | 
					 | 
				
			||||||
        SpaCyNLPPipelineModel.insert_defaults()
 | 
					 | 
				
			||||||
        current_app.logger.info('Insert/Update default TesseractOCRPipelineModels')
 | 
					 | 
				
			||||||
        TesseractOCRPipelineModel.insert_defaults()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @app.cli.group()
 | 
					 | 
				
			||||||
    def converter():
 | 
					 | 
				
			||||||
        ''' Converter commands. '''
 | 
					 | 
				
			||||||
        pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @converter.command()
 | 
					 | 
				
			||||||
    @click.argument('json_db')
 | 
					 | 
				
			||||||
    @click.argument('data_dir')
 | 
					 | 
				
			||||||
    def sandpaper(json_db, data_dir):
 | 
					 | 
				
			||||||
        ''' Sandpaper converter '''
 | 
					 | 
				
			||||||
        from app.converters.sandpaper import convert
 | 
					 | 
				
			||||||
        convert(json_db, data_dir)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @app.cli.group()
 | 
					 | 
				
			||||||
    def test():
 | 
					 | 
				
			||||||
        ''' Test commands. '''
 | 
					 | 
				
			||||||
        pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @test.command('run')
 | 
					 | 
				
			||||||
    def run_test():
 | 
					 | 
				
			||||||
        ''' Run unit tests. '''
 | 
					 | 
				
			||||||
        from unittest import TestLoader, TextTestRunner
 | 
					 | 
				
			||||||
        from unittest.suite import TestSuite
 | 
					 | 
				
			||||||
        tests: TestSuite = TestLoader().discover('tests')
 | 
					 | 
				
			||||||
        TextTestRunner(verbosity=2).run(tests)
 | 
					 | 
				
			||||||
@@ -1,5 +1,23 @@
 | 
				
			|||||||
from flask import Blueprint
 | 
					from flask import Blueprint
 | 
				
			||||||
 | 
					from flask_login import login_required
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
bp = Blueprint('contributions', __name__)
 | 
					bp = Blueprint('contributions', __name__)
 | 
				
			||||||
from . import routes
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.before_request
 | 
				
			||||||
 | 
					@login_required
 | 
				
			||||||
 | 
					def before_request():
 | 
				
			||||||
 | 
					    '''
 | 
				
			||||||
 | 
					    Ensures that the routes in this package can only be visited by users that
 | 
				
			||||||
 | 
					    are logged in.
 | 
				
			||||||
 | 
					    '''
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from . import (
 | 
				
			||||||
 | 
					    routes,
 | 
				
			||||||
 | 
					    spacy_nlp_pipeline_models,
 | 
				
			||||||
 | 
					    tesseract_ocr_pipeline_models,
 | 
				
			||||||
 | 
					    transkribus_htr_pipeline_models
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,16 +1,11 @@
 | 
				
			|||||||
from flask import current_app
 | 
					 | 
				
			||||||
from flask_wtf import FlaskForm
 | 
					from flask_wtf import FlaskForm
 | 
				
			||||||
from flask_wtf.file import FileField, FileRequired
 | 
					 | 
				
			||||||
from wtforms import (
 | 
					from wtforms import (
 | 
				
			||||||
    BooleanField,
 | 
					 | 
				
			||||||
    StringField,
 | 
					    StringField,
 | 
				
			||||||
    SubmitField,
 | 
					    SubmitField,
 | 
				
			||||||
    SelectMultipleField,
 | 
					    SelectMultipleField,
 | 
				
			||||||
    IntegerField,
 | 
					    IntegerField
 | 
				
			||||||
    ValidationError
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from wtforms.validators import InputRequired, Length
 | 
					from wtforms.validators import InputRequired, Length
 | 
				
			||||||
from app.services import SERVICES
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ContributionBaseForm(FlaskForm):
 | 
					class ContributionBaseForm(FlaskForm):
 | 
				
			||||||
@@ -48,74 +43,5 @@ class ContributionBaseForm(FlaskForm):
 | 
				
			|||||||
    submit = SubmitField()
 | 
					    submit = SubmitField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CreateTesseractOCRPipelineModelForm(ContributionBaseForm):
 | 
					class UpdateContributionBaseForm(ContributionBaseForm):
 | 
				
			||||||
    tesseract_model_file = FileField(
 | 
					 | 
				
			||||||
        'File',
 | 
					 | 
				
			||||||
        validators=[FileRequired()]
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    def validate_tesseract_model_file(self, field):
 | 
					 | 
				
			||||||
        if not field.data.filename.lower().endswith('.traineddata'):
 | 
					 | 
				
			||||||
            raise ValidationError('traineddata files only!')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __init__(self, *args, **kwargs):
 | 
					 | 
				
			||||||
        service_manifest = SERVICES['tesseract-ocr-pipeline']
 | 
					 | 
				
			||||||
        super().__init__(*args, **kwargs)
 | 
					 | 
				
			||||||
        self.compatible_service_versions.choices = [('', 'Choose your option')]
 | 
					 | 
				
			||||||
        self.compatible_service_versions.choices += [
 | 
					 | 
				
			||||||
            (x, x) for x in service_manifest['versions'].keys()
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
        self.compatible_service_versions.default = ''
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class CreateSpaCyNLPPipelineModelForm(ContributionBaseForm):
 | 
					 | 
				
			||||||
    spacy_model_file = FileField(
 | 
					 | 
				
			||||||
        'File',
 | 
					 | 
				
			||||||
        validators=[FileRequired()]
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    pipeline_name = StringField(
 | 
					 | 
				
			||||||
        'Pipeline name',
 | 
					 | 
				
			||||||
        validators=[InputRequired(), Length(max=64)]
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def validate_spacy_model_file(self, field):
 | 
					 | 
				
			||||||
        if not field.data.filename.lower().endswith('.tar.gz'):
 | 
					 | 
				
			||||||
            raise ValidationError('.tar.gz files only!')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __init__(self, *args, **kwargs):
 | 
					 | 
				
			||||||
        super().__init__(*args, **kwargs)
 | 
					 | 
				
			||||||
        service_manifest = SERVICES['spacy-nlp-pipeline']
 | 
					 | 
				
			||||||
        self.compatible_service_versions.choices = [('', 'Choose your option')]
 | 
					 | 
				
			||||||
        self.compatible_service_versions.choices += [
 | 
					 | 
				
			||||||
            (x, x) for x in service_manifest['versions'].keys()
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
        self.compatible_service_versions.default = ''
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class EditContributionBaseForm(ContributionBaseForm):
 | 
					 | 
				
			||||||
    pass
 | 
					    pass
 | 
				
			||||||
 | 
					 | 
				
			||||||
class EditTesseractOCRPipelineModelForm(EditContributionBaseForm):
 | 
					 | 
				
			||||||
    def __init__(self, *args, **kwargs):
 | 
					 | 
				
			||||||
        service_manifest = SERVICES['tesseract-ocr-pipeline']
 | 
					 | 
				
			||||||
        super().__init__(*args, **kwargs)
 | 
					 | 
				
			||||||
        self.compatible_service_versions.choices = [('', 'Choose your option')]
 | 
					 | 
				
			||||||
        self.compatible_service_versions.choices += [
 | 
					 | 
				
			||||||
            (x, x) for x in service_manifest['versions'].keys()
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
        self.compatible_service_versions.default = ''
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class EditSpaCyNLPPipelineModelForm(EditContributionBaseForm):
 | 
					 | 
				
			||||||
    pipeline_name = StringField(
 | 
					 | 
				
			||||||
        'Pipeline name',
 | 
					 | 
				
			||||||
        validators=[InputRequired(), Length(max=64)]
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    def __init__(self, *args, **kwargs):
 | 
					 | 
				
			||||||
        super().__init__(*args, **kwargs)
 | 
					 | 
				
			||||||
        service_manifest = SERVICES['spacy-nlp-pipeline']
 | 
					 | 
				
			||||||
        self.compatible_service_versions.choices = [('', 'Choose your option')]
 | 
					 | 
				
			||||||
        self.compatible_service_versions.choices += [
 | 
					 | 
				
			||||||
            (x, x) for x in service_manifest['versions'].keys()
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
        self.compatible_service_versions.default = ''
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,233 +1,9 @@
 | 
				
			|||||||
from flask import (
 | 
					from flask import redirect, url_for
 | 
				
			||||||
    abort,
 | 
					from flask_breadcrumbs import register_breadcrumb
 | 
				
			||||||
    current_app,
 | 
					 | 
				
			||||||
    flash,
 | 
					 | 
				
			||||||
    Markup,
 | 
					 | 
				
			||||||
    redirect,
 | 
					 | 
				
			||||||
    render_template,
 | 
					 | 
				
			||||||
    url_for
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
from flask_login import login_required, current_user
 | 
					 | 
				
			||||||
from threading import Thread
 | 
					 | 
				
			||||||
from app import db
 | 
					 | 
				
			||||||
from app.decorators import permission_required 
 | 
					 | 
				
			||||||
from app.models import (
 | 
					 | 
				
			||||||
    Permission,
 | 
					 | 
				
			||||||
    SpaCyNLPPipelineModel,
 | 
					 | 
				
			||||||
    TesseractOCRPipelineModel
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
from . import bp
 | 
					from . import bp
 | 
				
			||||||
from .forms import (
 | 
					 | 
				
			||||||
    CreateSpaCyNLPPipelineModelForm,
 | 
					 | 
				
			||||||
    CreateTesseractOCRPipelineModelForm,
 | 
					 | 
				
			||||||
    EditSpaCyNLPPipelineModelForm,
 | 
					 | 
				
			||||||
    EditTesseractOCRPipelineModelForm
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.before_request
 | 
					@bp.route('')
 | 
				
			||||||
@login_required
 | 
					@register_breadcrumb(bp, '.', '<i class="material-icons left">new_label</i>My Contributions')
 | 
				
			||||||
def before_request():
 | 
					 | 
				
			||||||
    pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.route('/')
 | 
					 | 
				
			||||||
def contributions():
 | 
					def contributions():
 | 
				
			||||||
    return render_template(
 | 
					    return redirect(url_for('main.dashboard', _anchor='contributions'))
 | 
				
			||||||
        'contributions/contributions.html.j2',
 | 
					 | 
				
			||||||
        title='Contributions'
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.route('/tesseract-ocr-pipeline-models')
 | 
					 | 
				
			||||||
def tesseract_ocr_pipeline_models():
 | 
					 | 
				
			||||||
    return render_template(
 | 
					 | 
				
			||||||
        'contributions/tesseract_ocr_pipeline_models.html.j2',
 | 
					 | 
				
			||||||
        title='Tesseract OCR Pipeline Models'
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.route('/tesseract-ocr-pipeline-models/<hashid:tesseract_ocr_pipeline_model_id>', methods=['GET', 'POST'])
 | 
					 | 
				
			||||||
def tesseract_ocr_pipeline_model(tesseract_ocr_pipeline_model_id):
 | 
					 | 
				
			||||||
    tesseract_ocr_pipeline_model = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id)
 | 
					 | 
				
			||||||
    form = EditTesseractOCRPipelineModelForm(
 | 
					 | 
				
			||||||
        data=tesseract_ocr_pipeline_model.to_json_serializeable(),
 | 
					 | 
				
			||||||
        prefix='edit-tesseract-ocr-pipeline-model-form'
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    if form.validate_on_submit():
 | 
					 | 
				
			||||||
        form.populate_obj(tesseract_ocr_pipeline_model)
 | 
					 | 
				
			||||||
        if db.session.is_modified(tesseract_ocr_pipeline_model):
 | 
					 | 
				
			||||||
            message = Markup(f'Tesseract OCR Pipeline model "<a href="{tesseract_ocr_pipeline_model.url}">{tesseract_ocr_pipeline_model.title}</a>" updated')
 | 
					 | 
				
			||||||
            flash(message)
 | 
					 | 
				
			||||||
            db.session.commit()
 | 
					 | 
				
			||||||
        return redirect(url_for('.tesseract_ocr_pipeline_models'))
 | 
					 | 
				
			||||||
    return render_template(
 | 
					 | 
				
			||||||
        'contributions/tesseract_ocr_pipeline_model.html.j2',
 | 
					 | 
				
			||||||
        form=form,
 | 
					 | 
				
			||||||
        tesseract_ocr_pipeline_model=tesseract_ocr_pipeline_model,
 | 
					 | 
				
			||||||
        title=f'{tesseract_ocr_pipeline_model.title} {tesseract_ocr_pipeline_model.version}'
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.route('/tesseract-ocr-pipeline-models/<hashid:tesseract_ocr_pipeline_model_id>', methods=['DELETE'])
 | 
					 | 
				
			||||||
def delete_tesseract_model(tesseract_ocr_pipeline_model_id):
 | 
					 | 
				
			||||||
    def _delete_tesseract_ocr_pipeline_model(app, tesseract_ocr_pipeline_model_id):
 | 
					 | 
				
			||||||
        with app.app_context():
 | 
					 | 
				
			||||||
            tesseract_ocr_pipeline_model = TesseractOCRPipelineModel.query.get(tesseract_ocr_pipeline_model_id)
 | 
					 | 
				
			||||||
            tesseract_ocr_pipeline_model.delete()
 | 
					 | 
				
			||||||
            db.session.commit()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    tesseract_ocr_pipeline_model = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id)
 | 
					 | 
				
			||||||
    if not (tesseract_ocr_pipeline_model.user == current_user or current_user.is_administrator()):
 | 
					 | 
				
			||||||
        abort(403)
 | 
					 | 
				
			||||||
    thread = Thread(
 | 
					 | 
				
			||||||
        target=_delete_tesseract_ocr_pipeline_model,
 | 
					 | 
				
			||||||
        args=(current_app._get_current_object(), tesseract_ocr_pipeline_model_id)
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    thread.start()
 | 
					 | 
				
			||||||
    return {}, 202
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.route('/tesseract-ocr-pipeline-models/create', methods=['GET', 'POST'])
 | 
					 | 
				
			||||||
def create_tesseract_ocr_pipeline_model():
 | 
					 | 
				
			||||||
    form = CreateTesseractOCRPipelineModelForm(prefix='create-tesseract-ocr-pipeline-model-form')
 | 
					 | 
				
			||||||
    if form.is_submitted():
 | 
					 | 
				
			||||||
        if not form.validate():
 | 
					 | 
				
			||||||
            response = {'errors': form.errors}
 | 
					 | 
				
			||||||
            return response, 400
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            tesseract_ocr_pipeline_model = TesseractOCRPipelineModel.create(
 | 
					 | 
				
			||||||
                form.tesseract_model_file.data,
 | 
					 | 
				
			||||||
                compatible_service_versions=form.compatible_service_versions.data,
 | 
					 | 
				
			||||||
                description=form.description.data,
 | 
					 | 
				
			||||||
                publisher=form.publisher.data,
 | 
					 | 
				
			||||||
                publisher_url=form.publisher_url.data,
 | 
					 | 
				
			||||||
                publishing_url=form.publishing_url.data,
 | 
					 | 
				
			||||||
                publishing_year=form.publishing_year.data,
 | 
					 | 
				
			||||||
                is_public=False,
 | 
					 | 
				
			||||||
                title=form.title.data,
 | 
					 | 
				
			||||||
                version=form.version.data,
 | 
					 | 
				
			||||||
                user=current_user
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        except OSError:
 | 
					 | 
				
			||||||
            abort(500)
 | 
					 | 
				
			||||||
        db.session.commit()
 | 
					 | 
				
			||||||
        tesseract_ocr_pipeline_model_url = url_for(
 | 
					 | 
				
			||||||
            '.tesseract_ocr_pipeline_model',
 | 
					 | 
				
			||||||
            tesseract_ocr_pipeline_model_id=tesseract_ocr_pipeline_model.id
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        message = Markup(f'Tesseract OCR Pipeline model "<a href="{tesseract_ocr_pipeline_model_url}">{tesseract_ocr_pipeline_model.title}</a>" created')
 | 
					 | 
				
			||||||
        flash(message)
 | 
					 | 
				
			||||||
        return {}, 201, {'Location': tesseract_ocr_pipeline_model_url}
 | 
					 | 
				
			||||||
    return render_template(
 | 
					 | 
				
			||||||
        'contributions/create_tesseract_ocr_pipeline_model.html.j2',
 | 
					 | 
				
			||||||
        form=form,
 | 
					 | 
				
			||||||
        title='Create Tesseract OCR Pipeline Model'
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.route('/tesseract-ocr-pipeline-models/<hashid:tesseract_ocr_pipeline_model_id>/toggle-public-status', methods=['POST'])
 | 
					 | 
				
			||||||
@permission_required(Permission.CONTRIBUTE)
 | 
					 | 
				
			||||||
def toggle_tesseract_ocr_pipeline_model_public_status(tesseract_ocr_pipeline_model_id):
 | 
					 | 
				
			||||||
    tesseract_ocr_pipeline_model = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id)
 | 
					 | 
				
			||||||
    if not (tesseract_ocr_pipeline_model.user == current_user or current_user.is_administrator()):
 | 
					 | 
				
			||||||
        abort(403)
 | 
					 | 
				
			||||||
    tesseract_ocr_pipeline_model.is_public = not tesseract_ocr_pipeline_model.is_public
 | 
					 | 
				
			||||||
    db.session.commit()
 | 
					 | 
				
			||||||
    return {}, 201
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.route('/spacy-nlp-pipeline-models')
 | 
					 | 
				
			||||||
def spacy_nlp_pipeline_models():
 | 
					 | 
				
			||||||
    return render_template(
 | 
					 | 
				
			||||||
        'contributions/spacy_nlp_pipeline_models.html.j2',
 | 
					 | 
				
			||||||
        title='SpaCy NLP Pipeline Models'
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.route('/spacy-nlp-pipeline-models/<hashid:spacy_nlp_pipeline_model_id>', methods=['GET', 'POST'])
 | 
					 | 
				
			||||||
def spacy_nlp_pipeline_model(spacy_nlp_pipeline_model_id):
 | 
					 | 
				
			||||||
    spacy_nlp_pipeline_model = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id)
 | 
					 | 
				
			||||||
    form = EditSpaCyNLPPipelineModelForm(
 | 
					 | 
				
			||||||
        data=spacy_nlp_pipeline_model.to_json_serializeable(),
 | 
					 | 
				
			||||||
        prefix='edit-spacy-nlp-pipeline-model-form'
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    if form.validate_on_submit():
 | 
					 | 
				
			||||||
        form.populate_obj(spacy_nlp_pipeline_model)
 | 
					 | 
				
			||||||
        if db.session.is_modified(spacy_nlp_pipeline_model):
 | 
					 | 
				
			||||||
            message = Markup(f'SpaCy NLP Pipeline model "<a href="{spacy_nlp_pipeline_model.url}">{spacy_nlp_pipeline_model.title}</a>" updated')
 | 
					 | 
				
			||||||
            flash(message)
 | 
					 | 
				
			||||||
            db.session.commit()
 | 
					 | 
				
			||||||
        return redirect(url_for('.spacy_nlp_pipeline_models'))
 | 
					 | 
				
			||||||
    return render_template(
 | 
					 | 
				
			||||||
        'contributions/spacy_nlp_pipeline_model.html.j2',
 | 
					 | 
				
			||||||
        form=form,
 | 
					 | 
				
			||||||
        spacy_nlp_pipeline_model=spacy_nlp_pipeline_model,
 | 
					 | 
				
			||||||
        title=f'{spacy_nlp_pipeline_model.title} {spacy_nlp_pipeline_model.version}'
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.route('/spacy-nlp-pipeline-models/<hashid:spacy_nlp_pipeline_model_id>', methods=['DELETE'])
 | 
					 | 
				
			||||||
def delete_spacy_model(spacy_nlp_pipeline_model_id):
 | 
					 | 
				
			||||||
    def _delete_spacy_model(app, spacy_nlp_pipeline_model_id):
 | 
					 | 
				
			||||||
        with app.app_context():
 | 
					 | 
				
			||||||
            spacy_nlp_pipeline_model = SpaCyNLPPipelineModel.query.get(spacy_nlp_pipeline_model_id)
 | 
					 | 
				
			||||||
            spacy_nlp_pipeline_model.delete()
 | 
					 | 
				
			||||||
            db.session.commit()
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    spacy_nlp_pipeline_model = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id)
 | 
					 | 
				
			||||||
    if not (spacy_nlp_pipeline_model.user == current_user or current_user.is_administrator()):
 | 
					 | 
				
			||||||
        abort(403)
 | 
					 | 
				
			||||||
    thread = Thread(
 | 
					 | 
				
			||||||
        target=_delete_spacy_model,
 | 
					 | 
				
			||||||
        args=(current_app._get_current_object(), spacy_nlp_pipeline_model_id)
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    thread.start()
 | 
					 | 
				
			||||||
    return {}, 202
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.route('/spacy-nlp-pipeline-models/create', methods=['GET', 'POST'])
 | 
					 | 
				
			||||||
def create_spacy_nlp_pipeline_model():
 | 
					 | 
				
			||||||
    form = CreateSpaCyNLPPipelineModelForm(prefix='create-spacy-nlp-pipeline-model-form')
 | 
					 | 
				
			||||||
    if form.is_submitted():
 | 
					 | 
				
			||||||
        if not form.validate():
 | 
					 | 
				
			||||||
            response = {'errors': form.errors}
 | 
					 | 
				
			||||||
            return response, 400
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            spacy_nlp_pipeline_model = SpaCyNLPPipelineModel.create(
 | 
					 | 
				
			||||||
                form.spacy_model_file.data,
 | 
					 | 
				
			||||||
                compatible_service_versions=form.compatible_service_versions.data,
 | 
					 | 
				
			||||||
                description=form.description.data,
 | 
					 | 
				
			||||||
                pipeline_name=form.pipeline_name.data,
 | 
					 | 
				
			||||||
                publisher=form.publisher.data,
 | 
					 | 
				
			||||||
                publisher_url=form.publisher_url.data,
 | 
					 | 
				
			||||||
                publishing_url=form.publishing_url.data,
 | 
					 | 
				
			||||||
                publishing_year=form.publishing_year.data,
 | 
					 | 
				
			||||||
                is_public=False,
 | 
					 | 
				
			||||||
                title=form.title.data,
 | 
					 | 
				
			||||||
                version=form.version.data,
 | 
					 | 
				
			||||||
                user=current_user
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        except OSError:
 | 
					 | 
				
			||||||
            abort(500)
 | 
					 | 
				
			||||||
        db.session.commit()
 | 
					 | 
				
			||||||
        spacy_nlp_pipeline_model_url = url_for(
 | 
					 | 
				
			||||||
            '.spacy_nlp_pipeline_model',
 | 
					 | 
				
			||||||
            spacy_nlp_pipeline_model_id=spacy_nlp_pipeline_model.id
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        message = Markup(f'SpaCy NLP Pipeline model "<a href="{spacy_nlp_pipeline_model_url}">{spacy_nlp_pipeline_model.title}</a>" created')
 | 
					 | 
				
			||||||
        flash(message)
 | 
					 | 
				
			||||||
        return {}, 201, {'Location': spacy_nlp_pipeline_model_url}
 | 
					 | 
				
			||||||
    return render_template(
 | 
					 | 
				
			||||||
        'contributions/create_spacy_nlp_pipeline_model.html.j2',
 | 
					 | 
				
			||||||
        form=form,
 | 
					 | 
				
			||||||
        title='Create SpaCy NLP Pipeline Model'
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.route('/spacy-nlp-pipeline-models/<hashid:spacy_nlp_pipeline_model_id>/toggle-public-status', methods=['POST'])
 | 
					 | 
				
			||||||
@permission_required(Permission.CONTRIBUTE)
 | 
					 | 
				
			||||||
def toggle_spacy_nlp_pipeline_model_public_status(spacy_nlp_pipeline_model_id):
 | 
					 | 
				
			||||||
    spacy_nlp_pipeline_model = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id)
 | 
					 | 
				
			||||||
    if not (spacy_nlp_pipeline_model.user == current_user or current_user.is_administrator()):
 | 
					 | 
				
			||||||
        abort(403)
 | 
					 | 
				
			||||||
    spacy_nlp_pipeline_model.is_public = not spacy_nlp_pipeline_model.is_public
 | 
					 | 
				
			||||||
    db.session.commit()
 | 
					 | 
				
			||||||
    return {}, 201
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								app/contributions/spacy_nlp_pipeline_models/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								app/contributions/spacy_nlp_pipeline_models/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					from .. import bp
 | 
				
			||||||
 | 
					from . import json_routes, routes
 | 
				
			||||||
							
								
								
									
										48
									
								
								app/contributions/spacy_nlp_pipeline_models/forms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								app/contributions/spacy_nlp_pipeline_models/forms.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
				
			|||||||
 | 
					from flask_wtf.file import FileField, FileRequired
 | 
				
			||||||
 | 
					from wtforms import StringField, ValidationError
 | 
				
			||||||
 | 
					from wtforms.validators import InputRequired, Length
 | 
				
			||||||
 | 
					from app.services import SERVICES
 | 
				
			||||||
 | 
					from ..forms import ContributionBaseForm, UpdateContributionBaseForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CreateSpaCyNLPPipelineModelForm(ContributionBaseForm):
 | 
				
			||||||
 | 
					    spacy_model_file = FileField(
 | 
				
			||||||
 | 
					        'File',
 | 
				
			||||||
 | 
					        validators=[FileRequired()]
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    pipeline_name = StringField(
 | 
				
			||||||
 | 
					        'Pipeline name',
 | 
				
			||||||
 | 
					        validators=[InputRequired(), Length(max=64)]
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def validate_spacy_model_file(self, field):
 | 
				
			||||||
 | 
					        if not field.data.filename.lower().endswith('.tar.gz'):
 | 
				
			||||||
 | 
					            raise ValidationError('.tar.gz files only!')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        if 'prefix' not in kwargs:
 | 
				
			||||||
 | 
					            kwargs['prefix'] = 'create-spacy-nlp-pipeline-model-form'
 | 
				
			||||||
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
 | 
					        service_manifest = SERVICES['spacy-nlp-pipeline']
 | 
				
			||||||
 | 
					        self.compatible_service_versions.choices = [('', 'Choose your option')]
 | 
				
			||||||
 | 
					        self.compatible_service_versions.choices += [
 | 
				
			||||||
 | 
					            (x, x) for x in service_manifest['versions'].keys()
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					        self.compatible_service_versions.default = ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class UpdateSpaCyNLPPipelineModelForm(UpdateContributionBaseForm):
 | 
				
			||||||
 | 
					    pipeline_name = StringField(
 | 
				
			||||||
 | 
					        'Pipeline name',
 | 
				
			||||||
 | 
					        validators=[InputRequired(), Length(max=64)]
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        if 'prefix' not in kwargs:
 | 
				
			||||||
 | 
					            kwargs['prefix'] = 'edit-spacy-nlp-pipeline-model-form'
 | 
				
			||||||
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
 | 
					        service_manifest = SERVICES['spacy-nlp-pipeline']
 | 
				
			||||||
 | 
					        self.compatible_service_versions.choices = [('', 'Choose your option')]
 | 
				
			||||||
 | 
					        self.compatible_service_versions.choices += [
 | 
				
			||||||
 | 
					            (x, x) for x in service_manifest['versions'].keys()
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					        self.compatible_service_versions.default = ''
 | 
				
			||||||
							
								
								
									
										52
									
								
								app/contributions/spacy_nlp_pipeline_models/json_routes.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								app/contributions/spacy_nlp_pipeline_models/json_routes.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
				
			|||||||
 | 
					from flask import abort, current_app, request
 | 
				
			||||||
 | 
					from flask_login import current_user
 | 
				
			||||||
 | 
					from threading import Thread
 | 
				
			||||||
 | 
					from app import db
 | 
				
			||||||
 | 
					from app.decorators import content_negotiation, permission_required
 | 
				
			||||||
 | 
					from app.models import SpaCyNLPPipelineModel
 | 
				
			||||||
 | 
					from .. import bp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/spacy-nlp-pipeline-models/<hashid:spacy_nlp_pipeline_model_id>', methods=['DELETE'])
 | 
				
			||||||
 | 
					@content_negotiation(produces='application/json')
 | 
				
			||||||
 | 
					def delete_spacy_model(spacy_nlp_pipeline_model_id):
 | 
				
			||||||
 | 
					    def _delete_spacy_model(app, spacy_nlp_pipeline_model_id):
 | 
				
			||||||
 | 
					        with app.app_context():
 | 
				
			||||||
 | 
					            snpm = SpaCyNLPPipelineModel.query.get(spacy_nlp_pipeline_model_id)
 | 
				
			||||||
 | 
					            snpm.delete()
 | 
				
			||||||
 | 
					            db.session.commit()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    snpm = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id)
 | 
				
			||||||
 | 
					    if not (snpm.user == current_user or current_user.is_administrator()):
 | 
				
			||||||
 | 
					        abort(403)
 | 
				
			||||||
 | 
					    thread = Thread(
 | 
				
			||||||
 | 
					        target=_delete_spacy_model,
 | 
				
			||||||
 | 
					        args=(current_app._get_current_object(), snpm.id)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    thread.start()
 | 
				
			||||||
 | 
					    response_data = {
 | 
				
			||||||
 | 
					        'message': \
 | 
				
			||||||
 | 
					            f'SpaCy NLP Pipeline Model "{snpm.title}" marked for deletion'
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return response_data, 202
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/spacy-nlp-pipeline-models/<hashid:spacy_nlp_pipeline_model_id>/is_public', methods=['PUT'])
 | 
				
			||||||
 | 
					@permission_required('CONTRIBUTE')
 | 
				
			||||||
 | 
					@content_negotiation(consumes='application/json', produces='application/json')
 | 
				
			||||||
 | 
					def update_spacy_nlp_pipeline_model_is_public(spacy_nlp_pipeline_model_id):
 | 
				
			||||||
 | 
					    is_public = request.json
 | 
				
			||||||
 | 
					    if not isinstance(is_public, bool):
 | 
				
			||||||
 | 
					        abort(400)
 | 
				
			||||||
 | 
					    snpm = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id)
 | 
				
			||||||
 | 
					    if not (snpm.user == current_user or current_user.is_administrator()):
 | 
				
			||||||
 | 
					        abort(403)
 | 
				
			||||||
 | 
					    snpm.is_public = is_public
 | 
				
			||||||
 | 
					    db.session.commit()
 | 
				
			||||||
 | 
					    response_data = {
 | 
				
			||||||
 | 
					        'message': (
 | 
				
			||||||
 | 
					            f'SpaCy NLP Pipeline Model "{snpm.title}"'
 | 
				
			||||||
 | 
					            f' is now {"public" if is_public else "private"}'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return response_data, 200
 | 
				
			||||||
							
								
								
									
										77
									
								
								app/contributions/spacy_nlp_pipeline_models/routes.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								app/contributions/spacy_nlp_pipeline_models/routes.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,77 @@
 | 
				
			|||||||
 | 
					from flask import abort, flash, redirect, render_template, url_for
 | 
				
			||||||
 | 
					from flask_breadcrumbs import register_breadcrumb
 | 
				
			||||||
 | 
					from flask_login import current_user
 | 
				
			||||||
 | 
					from app import db
 | 
				
			||||||
 | 
					from app.models import SpaCyNLPPipelineModel
 | 
				
			||||||
 | 
					from . import bp
 | 
				
			||||||
 | 
					from .forms import (
 | 
				
			||||||
 | 
					    CreateSpaCyNLPPipelineModelForm,
 | 
				
			||||||
 | 
					    UpdateSpaCyNLPPipelineModelForm
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					from .utils import (
 | 
				
			||||||
 | 
					    spacy_nlp_pipeline_model_dlc as spacy_nlp_pipeline_model_dlc
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/spacy-nlp-pipeline-models')
 | 
				
			||||||
 | 
					@register_breadcrumb(bp, '.spacy_nlp_pipeline_models', 'SpaCy NLP Pipeline Models')
 | 
				
			||||||
 | 
					def spacy_nlp_pipeline_models():
 | 
				
			||||||
 | 
					    return render_template(
 | 
				
			||||||
 | 
					        'contributions/spacy_nlp_pipeline_models/spacy_nlp_pipeline_models.html.j2',
 | 
				
			||||||
 | 
					        title='SpaCy NLP Pipeline Models'
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/spacy-nlp-pipeline-models/create', methods=['GET', 'POST'])
 | 
				
			||||||
 | 
					@register_breadcrumb(bp, '.spacy_nlp_pipeline_models.create', 'Create')
 | 
				
			||||||
 | 
					def create_spacy_nlp_pipeline_model():
 | 
				
			||||||
 | 
					    form = CreateSpaCyNLPPipelineModelForm()
 | 
				
			||||||
 | 
					    if form.is_submitted():
 | 
				
			||||||
 | 
					        if not form.validate():
 | 
				
			||||||
 | 
					            return {'errors': form.errors}, 400
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            snpm = SpaCyNLPPipelineModel.create(
 | 
				
			||||||
 | 
					                form.spacy_model_file.data,
 | 
				
			||||||
 | 
					                compatible_service_versions=form.compatible_service_versions.data,
 | 
				
			||||||
 | 
					                description=form.description.data,
 | 
				
			||||||
 | 
					                pipeline_name=form.pipeline_name.data,
 | 
				
			||||||
 | 
					                publisher=form.publisher.data,
 | 
				
			||||||
 | 
					                publisher_url=form.publisher_url.data,
 | 
				
			||||||
 | 
					                publishing_url=form.publishing_url.data,
 | 
				
			||||||
 | 
					                publishing_year=form.publishing_year.data,
 | 
				
			||||||
 | 
					                is_public=False,
 | 
				
			||||||
 | 
					                title=form.title.data,
 | 
				
			||||||
 | 
					                version=form.version.data,
 | 
				
			||||||
 | 
					                user=current_user
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        except OSError:
 | 
				
			||||||
 | 
					            abort(500)
 | 
				
			||||||
 | 
					        db.session.commit()
 | 
				
			||||||
 | 
					        flash(f'SpaCy NLP Pipeline model "{snpm.title}" created')
 | 
				
			||||||
 | 
					        return {}, 201, {'Location': url_for('.spacy_nlp_pipeline_models')}
 | 
				
			||||||
 | 
					    return render_template(
 | 
				
			||||||
 | 
					        'contributions/spacy_nlp_pipeline_models/create.html.j2',
 | 
				
			||||||
 | 
					        title='Create SpaCy NLP Pipeline Model',
 | 
				
			||||||
 | 
					        form=form
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/spacy-nlp-pipeline-models/<hashid:spacy_nlp_pipeline_model_id>', methods=['GET', 'POST'])
 | 
				
			||||||
 | 
					@register_breadcrumb(bp, '.spacy_nlp_pipeline_models.entity', '', dynamic_list_constructor=spacy_nlp_pipeline_model_dlc)
 | 
				
			||||||
 | 
					def spacy_nlp_pipeline_model(spacy_nlp_pipeline_model_id):
 | 
				
			||||||
 | 
					    snpm = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id)
 | 
				
			||||||
 | 
					    if not (snpm.user == current_user or current_user.is_administrator()):
 | 
				
			||||||
 | 
					        abort(403)
 | 
				
			||||||
 | 
					    form = UpdateSpaCyNLPPipelineModelForm(data=snpm.to_json_serializeable())
 | 
				
			||||||
 | 
					    if form.validate_on_submit():
 | 
				
			||||||
 | 
					        form.populate_obj(snpm)
 | 
				
			||||||
 | 
					        if db.session.is_modified(snpm):
 | 
				
			||||||
 | 
					            flash(f'SpaCy NLP Pipeline model "{snpm.title}" updated')
 | 
				
			||||||
 | 
					            db.session.commit()
 | 
				
			||||||
 | 
					        return redirect(url_for('.spacy_nlp_pipeline_models'))
 | 
				
			||||||
 | 
					    return render_template(
 | 
				
			||||||
 | 
					        'contributions/spacy_nlp_pipeline_models/spacy_nlp_pipeline_model.html.j2',
 | 
				
			||||||
 | 
					        title=f'{snpm.title} {snpm.version}',
 | 
				
			||||||
 | 
					        form=form,
 | 
				
			||||||
 | 
					        spacy_nlp_pipeline_model=snpm
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
							
								
								
									
										13
									
								
								app/contributions/spacy_nlp_pipeline_models/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								app/contributions/spacy_nlp_pipeline_models/utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					from flask import request, url_for
 | 
				
			||||||
 | 
					from app.models import SpaCyNLPPipelineModel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def spacy_nlp_pipeline_model_dlc():
 | 
				
			||||||
 | 
					    snpm_id = request.view_args['spacy_nlp_pipeline_model_id']
 | 
				
			||||||
 | 
					    snpm = SpaCyNLPPipelineModel.query.get_or_404(snpm_id)
 | 
				
			||||||
 | 
					    return [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            'text': f'{snpm.title} {snpm.version}',
 | 
				
			||||||
 | 
					            'url': url_for('.spacy_nlp_pipeline_model', spacy_nlp_pipeline_model_id=snpm_id)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					from .. import bp
 | 
				
			||||||
 | 
					from . import json_routes, routes
 | 
				
			||||||
							
								
								
									
										39
									
								
								app/contributions/tesseract_ocr_pipeline_models/forms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								app/contributions/tesseract_ocr_pipeline_models/forms.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
				
			|||||||
 | 
					from flask_wtf.file import FileField, FileRequired
 | 
				
			||||||
 | 
					from wtforms import ValidationError
 | 
				
			||||||
 | 
					from app.services import SERVICES
 | 
				
			||||||
 | 
					from ..forms import ContributionBaseForm, UpdateContributionBaseForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CreateTesseractOCRPipelineModelForm(ContributionBaseForm):
 | 
				
			||||||
 | 
					    tesseract_model_file = FileField(
 | 
				
			||||||
 | 
					        'File',
 | 
				
			||||||
 | 
					        validators=[FileRequired()]
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def validate_tesseract_model_file(self, field):
 | 
				
			||||||
 | 
					        if not field.data.filename.lower().endswith('.traineddata'):
 | 
				
			||||||
 | 
					            raise ValidationError('traineddata files only!')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        if 'prefix' not in kwargs:
 | 
				
			||||||
 | 
					            kwargs['prefix'] = 'create-tesseract-ocr-pipeline-model-form'
 | 
				
			||||||
 | 
					        service_manifest = SERVICES['tesseract-ocr-pipeline']
 | 
				
			||||||
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
 | 
					        self.compatible_service_versions.choices = [('', 'Choose your option')]
 | 
				
			||||||
 | 
					        self.compatible_service_versions.choices += [
 | 
				
			||||||
 | 
					            (x, x) for x in service_manifest['versions'].keys()
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					        self.compatible_service_versions.default = ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class UpdateTesseractOCRPipelineModelForm(UpdateContributionBaseForm):
 | 
				
			||||||
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        if 'prefix' not in kwargs:
 | 
				
			||||||
 | 
					            kwargs['prefix'] = 'edit-tesseract-ocr-pipeline-model-form'
 | 
				
			||||||
 | 
					        service_manifest = SERVICES['tesseract-ocr-pipeline']
 | 
				
			||||||
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
 | 
					        self.compatible_service_versions.choices = [('', 'Choose your option')]
 | 
				
			||||||
 | 
					        self.compatible_service_versions.choices += [
 | 
				
			||||||
 | 
					            (x, x) for x in service_manifest['versions'].keys()
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					        self.compatible_service_versions.default = ''
 | 
				
			||||||
@@ -0,0 +1,52 @@
 | 
				
			|||||||
 | 
					from flask import abort, current_app, request
 | 
				
			||||||
 | 
					from flask_login import current_user
 | 
				
			||||||
 | 
					from threading import Thread
 | 
				
			||||||
 | 
					from app import db
 | 
				
			||||||
 | 
					from app.decorators import content_negotiation, permission_required
 | 
				
			||||||
 | 
					from app.models import TesseractOCRPipelineModel
 | 
				
			||||||
 | 
					from . import bp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/tesseract-ocr-pipeline-models/<hashid:tesseract_ocr_pipeline_model_id>', methods=['DELETE'])
 | 
				
			||||||
 | 
					@content_negotiation(produces='application/json')
 | 
				
			||||||
 | 
					def delete_tesseract_model(tesseract_ocr_pipeline_model_id):
 | 
				
			||||||
 | 
					    def _delete_tesseract_ocr_pipeline_model(app, tesseract_ocr_pipeline_model_id):
 | 
				
			||||||
 | 
					        with app.app_context():
 | 
				
			||||||
 | 
					            topm = TesseractOCRPipelineModel.query.get(tesseract_ocr_pipeline_model_id)
 | 
				
			||||||
 | 
					            topm.delete()
 | 
				
			||||||
 | 
					            db.session.commit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    topm = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id)
 | 
				
			||||||
 | 
					    if not (topm.user == current_user or current_user.is_administrator()):
 | 
				
			||||||
 | 
					        abort(403)
 | 
				
			||||||
 | 
					    thread = Thread(
 | 
				
			||||||
 | 
					        target=_delete_tesseract_ocr_pipeline_model,
 | 
				
			||||||
 | 
					        args=(current_app._get_current_object(), topm.id)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    thread.start()
 | 
				
			||||||
 | 
					    response_data = {
 | 
				
			||||||
 | 
					        'message': \
 | 
				
			||||||
 | 
					            f'Tesseract OCR Pipeline Model "{topm.title}" marked for deletion'
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return response_data, 202
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/tesseract-ocr-pipeline-models/<hashid:tesseract_ocr_pipeline_model_id>/is_public', methods=['PUT'])
 | 
				
			||||||
 | 
					@permission_required('CONTRIBUTE')
 | 
				
			||||||
 | 
					@content_negotiation(consumes='application/json', produces='application/json')
 | 
				
			||||||
 | 
					def update_tesseract_ocr_pipeline_model_is_public(tesseract_ocr_pipeline_model_id):
 | 
				
			||||||
 | 
					    is_public = request.json
 | 
				
			||||||
 | 
					    if not isinstance(is_public, bool):
 | 
				
			||||||
 | 
					        abort(400)
 | 
				
			||||||
 | 
					    topm = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id)
 | 
				
			||||||
 | 
					    if not (topm.user == current_user or current_user.is_administrator()):
 | 
				
			||||||
 | 
					        abort(403)
 | 
				
			||||||
 | 
					    topm.is_public = is_public
 | 
				
			||||||
 | 
					    db.session.commit()
 | 
				
			||||||
 | 
					    response_data = {
 | 
				
			||||||
 | 
					        'message': (
 | 
				
			||||||
 | 
					            f'Tesseract OCR Pipeline Model "{topm.title}"'
 | 
				
			||||||
 | 
					            f' is now {"public" if is_public else "private"}'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return response_data, 200
 | 
				
			||||||
							
								
								
									
										76
									
								
								app/contributions/tesseract_ocr_pipeline_models/routes.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								app/contributions/tesseract_ocr_pipeline_models/routes.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,76 @@
 | 
				
			|||||||
 | 
					from flask import abort, flash, redirect, render_template, url_for
 | 
				
			||||||
 | 
					from flask_breadcrumbs import register_breadcrumb
 | 
				
			||||||
 | 
					from flask_login import current_user
 | 
				
			||||||
 | 
					from app import db
 | 
				
			||||||
 | 
					from app.models import TesseractOCRPipelineModel
 | 
				
			||||||
 | 
					from . import bp
 | 
				
			||||||
 | 
					from .forms import (
 | 
				
			||||||
 | 
					    CreateTesseractOCRPipelineModelForm,
 | 
				
			||||||
 | 
					    UpdateTesseractOCRPipelineModelForm
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					from .utils import (
 | 
				
			||||||
 | 
					    tesseract_ocr_pipeline_model_dlc as tesseract_ocr_pipeline_model_dlc
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/tesseract-ocr-pipeline-models')
 | 
				
			||||||
 | 
					@register_breadcrumb(bp, '.tesseract_ocr_pipeline_models', 'Tesseract OCR Pipeline Models')
 | 
				
			||||||
 | 
					def tesseract_ocr_pipeline_models():
 | 
				
			||||||
 | 
					    return render_template(
 | 
				
			||||||
 | 
					        'contributions/tesseract_ocr_pipeline_models/tesseract_ocr_pipeline_models.html.j2',
 | 
				
			||||||
 | 
					        title='Tesseract OCR Pipeline Models'
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/tesseract-ocr-pipeline-models/create', methods=['GET', 'POST'])
 | 
				
			||||||
 | 
					@register_breadcrumb(bp, '.tesseract_ocr_pipeline_models.create', 'Create')
 | 
				
			||||||
 | 
					def create_tesseract_ocr_pipeline_model():
 | 
				
			||||||
 | 
					    form = CreateTesseractOCRPipelineModelForm()
 | 
				
			||||||
 | 
					    if form.is_submitted():
 | 
				
			||||||
 | 
					        if not form.validate():
 | 
				
			||||||
 | 
					            return {'errors': form.errors}, 400
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            topm = TesseractOCRPipelineModel.create(
 | 
				
			||||||
 | 
					                form.tesseract_model_file.data,
 | 
				
			||||||
 | 
					                compatible_service_versions=form.compatible_service_versions.data,
 | 
				
			||||||
 | 
					                description=form.description.data,
 | 
				
			||||||
 | 
					                publisher=form.publisher.data,
 | 
				
			||||||
 | 
					                publisher_url=form.publisher_url.data,
 | 
				
			||||||
 | 
					                publishing_url=form.publishing_url.data,
 | 
				
			||||||
 | 
					                publishing_year=form.publishing_year.data,
 | 
				
			||||||
 | 
					                is_public=False,
 | 
				
			||||||
 | 
					                title=form.title.data,
 | 
				
			||||||
 | 
					                version=form.version.data,
 | 
				
			||||||
 | 
					                user=current_user
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        except OSError:
 | 
				
			||||||
 | 
					            abort(500)
 | 
				
			||||||
 | 
					        db.session.commit()
 | 
				
			||||||
 | 
					        flash(f'Tesseract OCR Pipeline model "{topm.title}" created')
 | 
				
			||||||
 | 
					        return {}, 201, {'Location': url_for('.tesseract_ocr_pipeline_models')}
 | 
				
			||||||
 | 
					    return render_template(
 | 
				
			||||||
 | 
					        'contributions/tesseract_ocr_pipeline_models/create.html.j2',
 | 
				
			||||||
 | 
					        title='Create Tesseract OCR Pipeline Model',
 | 
				
			||||||
 | 
					        form=form
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/tesseract-ocr-pipeline-models/<hashid:tesseract_ocr_pipeline_model_id>', methods=['GET', 'POST'])
 | 
				
			||||||
 | 
					@register_breadcrumb(bp, '.tesseract_ocr_pipeline_models.entity', '', dynamic_list_constructor=tesseract_ocr_pipeline_model_dlc)
 | 
				
			||||||
 | 
					def tesseract_ocr_pipeline_model(tesseract_ocr_pipeline_model_id):
 | 
				
			||||||
 | 
					    topm = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id)
 | 
				
			||||||
 | 
					    if not (topm.user == current_user or current_user.is_administrator()):
 | 
				
			||||||
 | 
					        abort(403)
 | 
				
			||||||
 | 
					    form = UpdateTesseractOCRPipelineModelForm(data=topm.to_json_serializeable())
 | 
				
			||||||
 | 
					    if form.validate_on_submit():
 | 
				
			||||||
 | 
					        form.populate_obj(topm)
 | 
				
			||||||
 | 
					        if db.session.is_modified(topm):
 | 
				
			||||||
 | 
					            flash(f'Tesseract OCR Pipeline model "{topm.title}" updated')
 | 
				
			||||||
 | 
					            db.session.commit()
 | 
				
			||||||
 | 
					        return redirect(url_for('.tesseract_ocr_pipeline_models'))
 | 
				
			||||||
 | 
					    return render_template(
 | 
				
			||||||
 | 
					        'contributions/tesseract_ocr_pipeline_models/tesseract_ocr_pipeline_model.html.j2',
 | 
				
			||||||
 | 
					        title=f'{topm.title} {topm.version}',
 | 
				
			||||||
 | 
					        form=form,
 | 
				
			||||||
 | 
					        tesseract_ocr_pipeline_model=topm
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
							
								
								
									
										13
									
								
								app/contributions/tesseract_ocr_pipeline_models/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								app/contributions/tesseract_ocr_pipeline_models/utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					from flask import request, url_for
 | 
				
			||||||
 | 
					from app.models import TesseractOCRPipelineModel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def tesseract_ocr_pipeline_model_dlc():
 | 
				
			||||||
 | 
					    topm_id = request.view_args['tesseract_ocr_pipeline_model_id']
 | 
				
			||||||
 | 
					    topm = TesseractOCRPipelineModel.query.get_or_404(topm_id)
 | 
				
			||||||
 | 
					    return [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            'text': f'{topm.title} {topm.version}',
 | 
				
			||||||
 | 
					            'url': url_for('.tesseract_ocr_pipeline_model', tesseract_ocr_pipeline_model_id=topm_id)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					from .. import bp
 | 
				
			||||||
 | 
					from . import routes
 | 
				
			||||||
@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					from flask import abort
 | 
				
			||||||
 | 
					from . import bp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/transkribus_htr_pipeline_models')
 | 
				
			||||||
 | 
					def transkribus_htr_pipeline_models():
 | 
				
			||||||
 | 
					    return abort(503)
 | 
				
			||||||
							
								
								
									
										22
									
								
								app/converters/cli.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								app/converters/cli.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					import click
 | 
				
			||||||
 | 
					from . import bp
 | 
				
			||||||
 | 
					from .sandpaper import SandpaperConverter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.cli.group('converter')
 | 
				
			||||||
 | 
					def converter():
 | 
				
			||||||
 | 
					    ''' Converter commands. '''
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@converter.group('sandpaper')
 | 
				
			||||||
 | 
					def sandpaper_converter():
 | 
				
			||||||
 | 
					    ''' Sandpaper converter commands. '''
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@sandpaper_converter.command('run')
 | 
				
			||||||
 | 
					@click.argument('json_db_file')
 | 
				
			||||||
 | 
					@click.argument('data_dir')
 | 
				
			||||||
 | 
					def run_sandpaper_converter(json_db_file, data_dir):
 | 
				
			||||||
 | 
					    ''' Run the sandpaper converter. '''
 | 
				
			||||||
 | 
					    sandpaper_converter = SandpaperConverter(json_db_file, data_dir)
 | 
				
			||||||
 | 
					    sandpaper_converter.run()
 | 
				
			||||||
@@ -7,20 +7,25 @@ import os
 | 
				
			|||||||
import shutil
 | 
					import shutil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def convert(json_db_file, data_dir):
 | 
					class SandpaperConverter:
 | 
				
			||||||
    with open(json_db_file, 'r') as f:
 | 
					    def __init__(self, json_db_file, data_dir):
 | 
				
			||||||
 | 
					        self.json_db_file = json_db_file
 | 
				
			||||||
 | 
					        self.data_dir = data_dir
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def run(self):
 | 
				
			||||||
 | 
					        with open(self.json_db_file, 'r') as f:
 | 
				
			||||||
            json_db = json.loads(f.read())
 | 
					            json_db = json.loads(f.read())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for json_user in json_db:
 | 
					        for json_user in json_db:
 | 
				
			||||||
            if not json_user['confirmed']:
 | 
					            if not json_user['confirmed']:
 | 
				
			||||||
                current_app.logger.info(f'Skip unconfirmed user {json_user["username"]}')
 | 
					                current_app.logger.info(f'Skip unconfirmed user {json_user["username"]}')
 | 
				
			||||||
                continue
 | 
					                continue
 | 
				
			||||||
        user_dir = os.path.join(data_dir, str(json_user['id']))
 | 
					            user_dir = os.path.join(self.data_dir, str(json_user['id']))
 | 
				
			||||||
        convert_user(json_user, user_dir)
 | 
					            self.convert_user(json_user, user_dir)
 | 
				
			||||||
            db.session.commit()
 | 
					            db.session.commit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def convert_user(json_user, user_dir):
 | 
					    def convert_user(self, json_user, user_dir):
 | 
				
			||||||
        current_app.logger.info(f'Create User {json_user["username"]}...')
 | 
					        current_app.logger.info(f'Create User {json_user["username"]}...')
 | 
				
			||||||
        user = User(
 | 
					        user = User(
 | 
				
			||||||
            confirmed=json_user['confirmed'],
 | 
					            confirmed=json_user['confirmed'],
 | 
				
			||||||
@@ -44,11 +49,11 @@ def convert_user(json_user, user_dir):
 | 
				
			|||||||
                current_app.logger.info(f'Skip empty corpus {json_corpus["title"]}')
 | 
					                current_app.logger.info(f'Skip empty corpus {json_corpus["title"]}')
 | 
				
			||||||
                continue
 | 
					                continue
 | 
				
			||||||
            corpus_dir = os.path.join(user_dir, 'corpora', str(json_corpus['id']))
 | 
					            corpus_dir = os.path.join(user_dir, 'corpora', str(json_corpus['id']))
 | 
				
			||||||
        convert_corpus(json_corpus, user, corpus_dir)
 | 
					            self.convert_corpus(json_corpus, user, corpus_dir)
 | 
				
			||||||
        current_app.logger.info('Done')
 | 
					        current_app.logger.info('Done')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def convert_corpus(json_corpus, user, corpus_dir):
 | 
					    def convert_corpus(self, json_corpus, user, corpus_dir):
 | 
				
			||||||
        current_app.logger.info(f'Create Corpus {json_corpus["title"]}...')
 | 
					        current_app.logger.info(f'Create Corpus {json_corpus["title"]}...')
 | 
				
			||||||
        corpus = Corpus(
 | 
					        corpus = Corpus(
 | 
				
			||||||
            user=user,
 | 
					            user=user,
 | 
				
			||||||
@@ -66,11 +71,11 @@ def convert_corpus(json_corpus, user, corpus_dir):
 | 
				
			|||||||
            db.session.rollback()
 | 
					            db.session.rollback()
 | 
				
			||||||
            raise Exception('Internal Server Error')
 | 
					            raise Exception('Internal Server Error')
 | 
				
			||||||
        for json_corpus_file in json_corpus['files'].values():
 | 
					        for json_corpus_file in json_corpus['files'].values():
 | 
				
			||||||
        convert_corpus_file(json_corpus_file, corpus, corpus_dir)
 | 
					            self.convert_corpus_file(json_corpus_file, corpus, corpus_dir)
 | 
				
			||||||
        current_app.logger.info('Done')
 | 
					        current_app.logger.info('Done')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def convert_corpus_file(json_corpus_file, corpus, corpus_dir):
 | 
					    def convert_corpus_file(self, json_corpus_file, corpus, corpus_dir):
 | 
				
			||||||
        current_app.logger.info(f'Create CorpusFile {json_corpus_file["title"]}...')
 | 
					        current_app.logger.info(f'Create CorpusFile {json_corpus_file["title"]}...')
 | 
				
			||||||
        corpus_file = CorpusFile(
 | 
					        corpus_file = CorpusFile(
 | 
				
			||||||
            corpus=corpus,
 | 
					            corpus=corpus,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,19 @@
 | 
				
			|||||||
from flask import Blueprint
 | 
					from flask import Blueprint
 | 
				
			||||||
 | 
					from flask_login import login_required
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
bp = Blueprint('corpora', __name__)
 | 
					bp = Blueprint('corpora', __name__)
 | 
				
			||||||
from . import cqi_over_socketio, routes  # noqa
 | 
					bp.cli.short_help = 'Corpus commands.'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.before_request
 | 
				
			||||||
 | 
					@login_required
 | 
				
			||||||
 | 
					def before_request():
 | 
				
			||||||
 | 
					    '''
 | 
				
			||||||
 | 
					    Ensures that the routes in this package can only be visited by users that
 | 
				
			||||||
 | 
					    are logged in.
 | 
				
			||||||
 | 
					    '''
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from . import cli, cqi_over_socketio, files, followers, routes, json_routes
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										24
									
								
								app/corpora/cli.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								app/corpora/cli.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					from app.models import Corpus, CorpusStatus
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import shutil
 | 
				
			||||||
 | 
					from app import db
 | 
				
			||||||
 | 
					from . import bp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.cli.command('reset')
 | 
				
			||||||
 | 
					def reset():
 | 
				
			||||||
 | 
					    ''' Reset built corpora. '''
 | 
				
			||||||
 | 
					    status = [
 | 
				
			||||||
 | 
					        CorpusStatus.QUEUED,
 | 
				
			||||||
 | 
					        CorpusStatus.BUILDING,
 | 
				
			||||||
 | 
					        CorpusStatus.BUILT,
 | 
				
			||||||
 | 
					        CorpusStatus.STARTING_ANALYSIS_SESSION,
 | 
				
			||||||
 | 
					        CorpusStatus.RUNNING_ANALYSIS_SESSION,
 | 
				
			||||||
 | 
					        CorpusStatus.CANCELING_ANALYSIS_SESSION
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					    for corpus in [x for x in Corpus.query.all() if x.status in status]:
 | 
				
			||||||
 | 
					        print(f'Resetting corpus {corpus}')
 | 
				
			||||||
 | 
					        shutil.rmtree(os.path.join(corpus.path, 'cwb'), ignore_errors=True)
 | 
				
			||||||
 | 
					        corpus.status = CorpusStatus.UNPREPARED
 | 
				
			||||||
 | 
					        corpus.num_analysis_sessions = 0
 | 
				
			||||||
 | 
					    db.session.commit()
 | 
				
			||||||
@@ -38,7 +38,7 @@ def cqi_corpora_corpus_query(cqi_client: cqi.CQiClient, corpus_name: str, subcor
 | 
				
			|||||||
@cqi_over_socketio
 | 
					@cqi_over_socketio
 | 
				
			||||||
def cqi_corpora_corpus_update_db(cqi_client: cqi.CQiClient, corpus_name: str):
 | 
					def cqi_corpora_corpus_update_db(cqi_client: cqi.CQiClient, corpus_name: str):
 | 
				
			||||||
    corpus = Corpus.query.get(session['d']['corpus_id'])
 | 
					    corpus = Corpus.query.get(session['d']['corpus_id'])
 | 
				
			||||||
    corpus.num_tokens = cqi_client.corpora.get('CORPUS').attrs['size']
 | 
					    corpus.num_tokens = cqi_client.corpora.get(corpus_name).attrs['size']
 | 
				
			||||||
    db.session.commit()
 | 
					    db.session.commit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										33
									
								
								app/corpora/decorators.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								app/corpora/decorators.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
				
			|||||||
 | 
					from flask import abort
 | 
				
			||||||
 | 
					from flask_login import current_user
 | 
				
			||||||
 | 
					from functools import wraps
 | 
				
			||||||
 | 
					from app.models import Corpus, CorpusFollowerAssociation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def corpus_follower_permission_required(*permissions):
 | 
				
			||||||
 | 
					    def decorator(f):
 | 
				
			||||||
 | 
					        @wraps(f)
 | 
				
			||||||
 | 
					        def decorated_function(*args, **kwargs):
 | 
				
			||||||
 | 
					            corpus_id = kwargs.get('corpus_id')
 | 
				
			||||||
 | 
					            corpus = Corpus.query.get_or_404(corpus_id)
 | 
				
			||||||
 | 
					            if not (corpus.user == current_user or current_user.is_administrator()):
 | 
				
			||||||
 | 
					                cfa = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=current_user.id).first()
 | 
				
			||||||
 | 
					                if cfa is None:
 | 
				
			||||||
 | 
					                    abort(403)
 | 
				
			||||||
 | 
					                if not all([cfa.role.has_permission(p) for p in permissions]):
 | 
				
			||||||
 | 
					                    abort(403)
 | 
				
			||||||
 | 
					            return f(*args, **kwargs)
 | 
				
			||||||
 | 
					        return decorated_function
 | 
				
			||||||
 | 
					    return decorator
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def corpus_owner_or_admin_required(f):
 | 
				
			||||||
 | 
					    @wraps(f)
 | 
				
			||||||
 | 
					    def decorated_function(*args, **kwargs):
 | 
				
			||||||
 | 
					        corpus_id = kwargs.get('corpus_id')
 | 
				
			||||||
 | 
					        corpus = Corpus.query.get_or_404(corpus_id)
 | 
				
			||||||
 | 
					        if not (corpus.user == current_user or current_user.is_administrator()):
 | 
				
			||||||
 | 
					            abort(403)
 | 
				
			||||||
 | 
					        return f(*args, **kwargs)
 | 
				
			||||||
 | 
					    return decorated_function
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										45
									
								
								app/corpora/events.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								app/corpora/events.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					from flask_login import current_user
 | 
				
			||||||
 | 
					from flask_socketio import join_room
 | 
				
			||||||
 | 
					from app import hashids, socketio
 | 
				
			||||||
 | 
					from app.decorators import socketio_login_required
 | 
				
			||||||
 | 
					from app.models import Corpus
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@socketio.on('GET /corpora/<corpus_id>')
 | 
				
			||||||
 | 
					@socketio_login_required
 | 
				
			||||||
 | 
					def get_corpus(corpus_hashid):
 | 
				
			||||||
 | 
					    corpus_id = hashids.decode(corpus_hashid)
 | 
				
			||||||
 | 
					    corpus = Corpus.query.get(corpus_id)
 | 
				
			||||||
 | 
					    if corpus is None:
 | 
				
			||||||
 | 
					        return {'options': {'status': 404, 'statusText': 'Not found'}}
 | 
				
			||||||
 | 
					    if not (
 | 
				
			||||||
 | 
					        corpus.is_public
 | 
				
			||||||
 | 
					        or corpus.user == current_user
 | 
				
			||||||
 | 
					        or current_user.is_administrator()
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
 | 
					        return {'options': {'status': 403, 'statusText': 'Forbidden'}}
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        'body': corpus.to_json_serializable(),
 | 
				
			||||||
 | 
					        'options': {
 | 
				
			||||||
 | 
					            'status': 200,
 | 
				
			||||||
 | 
					            'statusText': 'OK',
 | 
				
			||||||
 | 
					            'headers': {'Content-Type: application/json'}
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@socketio.on('SUBSCRIBE /corpora/<corpus_id>')
 | 
				
			||||||
 | 
					@socketio_login_required
 | 
				
			||||||
 | 
					def subscribe_corpus(corpus_hashid):
 | 
				
			||||||
 | 
					    corpus_id = hashids.decode(corpus_hashid)
 | 
				
			||||||
 | 
					    corpus = Corpus.query.get(corpus_id)
 | 
				
			||||||
 | 
					    if corpus is None:
 | 
				
			||||||
 | 
					        return {'options': {'status': 404, 'statusText': 'Not found'}}
 | 
				
			||||||
 | 
					    if not (
 | 
				
			||||||
 | 
					        corpus.is_public
 | 
				
			||||||
 | 
					        or corpus.user == current_user
 | 
				
			||||||
 | 
					        or current_user.is_administrator()
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
 | 
					        return {'options': {'status': 403, 'statusText': 'Forbidden'}}
 | 
				
			||||||
 | 
					    join_room(f'/corpora/{corpus.hashid}')
 | 
				
			||||||
 | 
					    return {'options': {'status': 200, 'statusText': 'OK'}}
 | 
				
			||||||
							
								
								
									
										2
									
								
								app/corpora/files/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								app/corpora/files/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					from .. import bp
 | 
				
			||||||
 | 
					from . import json_routes, 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 validate_vrt(self, field):
 | 
				
			||||||
 | 
					        if not field.data.filename.lower().endswith('.vrt'):
 | 
				
			||||||
 | 
					            raise ValidationError('VRT files only!')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        if 'prefix' not in kwargs:
 | 
				
			||||||
 | 
					            kwargs['prefix'] = 'create-corpus-file-form'
 | 
				
			||||||
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class UpdateCorpusFileForm(CorpusFileBaseForm):
 | 
				
			||||||
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        if 'prefix' not in kwargs:
 | 
				
			||||||
 | 
					            kwargs['prefix'] = 'update-corpus-file-form'
 | 
				
			||||||
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
							
								
								
									
										30
									
								
								app/corpora/files/json_routes.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								app/corpora/files/json_routes.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					from flask import abort, current_app
 | 
				
			||||||
 | 
					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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>', methods=['DELETE'])
 | 
				
			||||||
 | 
					@corpus_follower_permission_required('MANAGE_FILES')
 | 
				
			||||||
 | 
					@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'
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return response_data, 202
 | 
				
			||||||
							
								
								
									
										100
									
								
								app/corpora/files/routes.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								app/corpora/files/routes.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,100 @@
 | 
				
			|||||||
 | 
					from flask import (
 | 
				
			||||||
 | 
					    abort,
 | 
				
			||||||
 | 
					    flash,
 | 
				
			||||||
 | 
					    redirect,
 | 
				
			||||||
 | 
					    render_template,
 | 
				
			||||||
 | 
					    send_from_directory,
 | 
				
			||||||
 | 
					    url_for
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					from flask_breadcrumbs import register_breadcrumb
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					from app import db
 | 
				
			||||||
 | 
					from app.models import Corpus, CorpusFile, CorpusStatus
 | 
				
			||||||
 | 
					from ..decorators import corpus_follower_permission_required
 | 
				
			||||||
 | 
					from ..utils import corpus_endpoint_arguments_constructor as corpus_eac
 | 
				
			||||||
 | 
					from . import bp
 | 
				
			||||||
 | 
					from .forms import CreateCorpusFileForm, UpdateCorpusFileForm
 | 
				
			||||||
 | 
					from .utils import corpus_file_dynamic_list_constructor as corpus_file_dlc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/<hashid:corpus_id>/files')
 | 
				
			||||||
 | 
					@register_breadcrumb(bp, '.entity.files', 'Files', endpoint_arguments_constructor=corpus_eac)
 | 
				
			||||||
 | 
					def corpus_files(corpus_id):
 | 
				
			||||||
 | 
					    return redirect(url_for('.corpus', _anchor='files', corpus_id=corpus_id))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/<hashid:corpus_id>/files/create', methods=['GET', 'POST'])
 | 
				
			||||||
 | 
					@register_breadcrumb(bp, '.entity.files.create', 'Create', endpoint_arguments_constructor=corpus_eac)
 | 
				
			||||||
 | 
					@corpus_follower_permission_required('MANAGE_FILES')
 | 
				
			||||||
 | 
					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(
 | 
				
			||||||
 | 
					        'corpora/files/create.html.j2',
 | 
				
			||||||
 | 
					        title='Add corpus file',
 | 
				
			||||||
 | 
					        form=form,
 | 
				
			||||||
 | 
					        corpus=corpus
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>', methods=['GET', 'POST'])
 | 
				
			||||||
 | 
					@register_breadcrumb(bp, '.entity.files.entity', '', dynamic_list_constructor=corpus_file_dlc)
 | 
				
			||||||
 | 
					@corpus_follower_permission_required('MANAGE_FILES')
 | 
				
			||||||
 | 
					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(
 | 
				
			||||||
 | 
					        'corpora/files/corpus_file.html.j2',
 | 
				
			||||||
 | 
					        title='Edit corpus file',
 | 
				
			||||||
 | 
					        form=form,
 | 
				
			||||||
 | 
					        corpus=corpus_file.corpus,
 | 
				
			||||||
 | 
					        corpus_file=corpus_file
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>/download')
 | 
				
			||||||
 | 
					@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
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
							
								
								
									
										15
									
								
								app/corpora/files/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								app/corpora/files/utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					from flask import request, url_for
 | 
				
			||||||
 | 
					from app.models import CorpusFile
 | 
				
			||||||
 | 
					from ..utils import corpus_endpoint_arguments_constructor as corpus_eac
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def corpus_file_dynamic_list_constructor():
 | 
				
			||||||
 | 
					    corpus_id = request.view_args['corpus_id']
 | 
				
			||||||
 | 
					    corpus_file_id = request.view_args['corpus_file_id']
 | 
				
			||||||
 | 
					    corpus_file = CorpusFile.query.filter_by(corpus_id=corpus_id, id=corpus_file_id).first_or_404()
 | 
				
			||||||
 | 
					    return [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            'text': f'{corpus_file.author}: {corpus_file.title} ({corpus_file.publishing_year})',
 | 
				
			||||||
 | 
					            'url': url_for('.corpus_file', corpus_id=corpus_id, corpus_file_id=corpus_file_id)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										2
									
								
								app/corpora/followers/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								app/corpora/followers/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					from .. import bp
 | 
				
			||||||
 | 
					from . import json_routes
 | 
				
			||||||
							
								
								
									
										76
									
								
								app/corpora/followers/json_routes.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								app/corpora/followers/json_routes.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,76 @@
 | 
				
			|||||||
 | 
					from flask import abort, flash, jsonify, make_response, request
 | 
				
			||||||
 | 
					from flask_login import current_user
 | 
				
			||||||
 | 
					from app import db
 | 
				
			||||||
 | 
					from app.decorators import content_negotiation
 | 
				
			||||||
 | 
					from app.models import (
 | 
				
			||||||
 | 
					    Corpus,
 | 
				
			||||||
 | 
					    CorpusFollowerAssociation,
 | 
				
			||||||
 | 
					    CorpusFollowerRole,
 | 
				
			||||||
 | 
					    User
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					from ..decorators import corpus_follower_permission_required
 | 
				
			||||||
 | 
					from . import bp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# @bp.route('/<hashid:corpus_id>/followers', methods=['POST'])
 | 
				
			||||||
 | 
					# @corpus_follower_permission_required('MANAGE_FOLLOWERS')
 | 
				
			||||||
 | 
					# @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()
 | 
				
			||||||
 | 
					#     response_data = {
 | 
				
			||||||
 | 
					#         'message': f'Users are now following "{corpus.title}"',
 | 
				
			||||||
 | 
					#         'category': 'corpus'
 | 
				
			||||||
 | 
					#     }
 | 
				
			||||||
 | 
					#     return response_data, 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# @bp.route('/<hashid:corpus_id>/followers/<hashid:follower_id>/role', methods=['PUT'])
 | 
				
			||||||
 | 
					# @corpus_follower_permission_required('MANAGE_FOLLOWERS')
 | 
				
			||||||
 | 
					# @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()
 | 
				
			||||||
 | 
					#     response_data = {
 | 
				
			||||||
 | 
					#         'message': f'User "{cfa.follower.username}" is now {cfa.role.name}',
 | 
				
			||||||
 | 
					#         'category': 'corpus'
 | 
				
			||||||
 | 
					#     }
 | 
				
			||||||
 | 
					#     return response_data, 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# @bp.route('/<hashid:corpus_id>/followers/<hashid:follower_id>', methods=['DELETE'])
 | 
				
			||||||
 | 
					# def delete_corpus_follower(corpus_id, follower_id):
 | 
				
			||||||
 | 
					#     cfa = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=follower_id).first_or_404()
 | 
				
			||||||
 | 
					#     if not (
 | 
				
			||||||
 | 
					#         current_user.id == follower_id
 | 
				
			||||||
 | 
					#         or current_user == cfa.corpus.user 
 | 
				
			||||||
 | 
					#         or CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=current_user.id).first().role.has_permission('MANAGE_FOLLOWERS')
 | 
				
			||||||
 | 
					#         or current_user.is_administrator()):
 | 
				
			||||||
 | 
					#         abort(403)
 | 
				
			||||||
 | 
					#     if current_user.id == follower_id:
 | 
				
			||||||
 | 
					#         flash(f'You are no longer following "{cfa.corpus.title}"', 'corpus')
 | 
				
			||||||
 | 
					#         response = make_response()
 | 
				
			||||||
 | 
					#         response.status_code = 204
 | 
				
			||||||
 | 
					#     else:
 | 
				
			||||||
 | 
					#         response_data = {
 | 
				
			||||||
 | 
					#             'message': f'"{cfa.follower.username}" is not following "{cfa.corpus.title}" anymore',
 | 
				
			||||||
 | 
					#             'category': 'corpus'
 | 
				
			||||||
 | 
					#         }
 | 
				
			||||||
 | 
					#         response = jsonify(response_data)
 | 
				
			||||||
 | 
					#         response.status_code = 200
 | 
				
			||||||
 | 
					#     cfa.follower.unfollow_corpus(cfa.corpus)
 | 
				
			||||||
 | 
					#     db.session.commit()
 | 
				
			||||||
 | 
					#     return response
 | 
				
			||||||
@@ -1,13 +1,5 @@
 | 
				
			|||||||
from flask_wtf import FlaskForm
 | 
					from flask_wtf import FlaskForm
 | 
				
			||||||
from flask_wtf.file import FileField, FileRequired
 | 
					from wtforms import StringField, SubmitField, TextAreaField
 | 
				
			||||||
from wtforms import (
 | 
					 | 
				
			||||||
    BooleanField,
 | 
					 | 
				
			||||||
    StringField,
 | 
					 | 
				
			||||||
    SubmitField,
 | 
					 | 
				
			||||||
    TextAreaField,
 | 
					 | 
				
			||||||
    ValidationError,
 | 
					 | 
				
			||||||
    IntegerField
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
from wtforms.validators import InputRequired, Length
 | 
					from wtforms.validators import InputRequired, Length
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -34,50 +26,8 @@ class UpdateCorpusForm(CorpusBaseForm):
 | 
				
			|||||||
        super().__init__(*args, **kwargs)
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CorpusFileBaseForm(FlaskForm):
 | 
					 | 
				
			||||||
    author = StringField(
 | 
					 | 
				
			||||||
        'Author',
 | 
					 | 
				
			||||||
        validators=[InputRequired(), Length(max=255)]
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    publishing_year = IntegerField(
 | 
					 | 
				
			||||||
        'Publishing year',
 | 
					 | 
				
			||||||
        validators=[InputRequired()]
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    title = StringField(
 | 
					 | 
				
			||||||
        'Title',
 | 
					 | 
				
			||||||
        validators=[InputRequired(), Length(max=255)]
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    address = StringField('Adress', validators=[Length(max=255)])
 | 
					 | 
				
			||||||
    booktitle = StringField('Booktitle', validators=[Length(max=255)])
 | 
					 | 
				
			||||||
    chapter = StringField('Chapter', validators=[Length(max=255)])
 | 
					 | 
				
			||||||
    editor = StringField('Editor', validators=[Length(max=255)])
 | 
					 | 
				
			||||||
    institution = StringField('Institution', validators=[Length(max=255)])
 | 
					 | 
				
			||||||
    journal = StringField('Journal', validators=[Length(max=255)])
 | 
					 | 
				
			||||||
    pages = StringField('Pages', validators=[Length(max=255)])
 | 
					 | 
				
			||||||
    publisher = StringField('Publisher', validators=[Length(max=255)])
 | 
					 | 
				
			||||||
    school = StringField('School', validators=[Length(max=255)])
 | 
					 | 
				
			||||||
    submit = SubmitField()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class CreateCorpusFileForm(CorpusFileBaseForm):
 | 
					 | 
				
			||||||
    vrt = FileField('File', validators=[FileRequired()])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __init__(self, *args, **kwargs):
 | 
					 | 
				
			||||||
        if 'prefix' not in kwargs:
 | 
					 | 
				
			||||||
            kwargs['prefix'] = 'create-corpus-file-form'
 | 
					 | 
				
			||||||
        super().__init__(*args, **kwargs)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def validate_vrt(self, field):
 | 
					 | 
				
			||||||
        if not field.data.filename.lower().endswith('.vrt'):
 | 
					 | 
				
			||||||
            raise ValidationError('VRT files only!')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class UpdateCorpusFileForm(CorpusFileBaseForm):
 | 
					 | 
				
			||||||
    def __init__(self, *args, **kwargs):
 | 
					 | 
				
			||||||
        if 'prefix' not in kwargs:
 | 
					 | 
				
			||||||
            kwargs['prefix'] = 'update-corpus-file-form'
 | 
					 | 
				
			||||||
        super().__init__(*args, **kwargs)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class ImportCorpusForm(FlaskForm):
 | 
					class ImportCorpusForm(FlaskForm):
 | 
				
			||||||
    pass
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        if 'prefix' not in kwargs:
 | 
				
			||||||
 | 
					            kwargs['prefix'] = 'import-corpus-form'
 | 
				
			||||||
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										111
									
								
								app/corpora/json_routes.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								app/corpora/json_routes.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,111 @@
 | 
				
			|||||||
 | 
					from datetime import datetime
 | 
				
			||||||
 | 
					from flask import abort, current_app, request, url_for
 | 
				
			||||||
 | 
					from flask_login import current_user
 | 
				
			||||||
 | 
					from threading import Thread
 | 
				
			||||||
 | 
					from app import db
 | 
				
			||||||
 | 
					from app.decorators import content_negotiation
 | 
				
			||||||
 | 
					from app.models import Corpus, CorpusFollowerRole
 | 
				
			||||||
 | 
					from . import bp
 | 
				
			||||||
 | 
					from .decorators import corpus_follower_permission_required, corpus_owner_or_admin_required
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/<hashid:corpus_id>', methods=['DELETE'])
 | 
				
			||||||
 | 
					@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'
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return response_data, 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/<hashid:corpus_id>/build', methods=['POST'])
 | 
				
			||||||
 | 
					@corpus_follower_permission_required('MANAGE_FILES')
 | 
				
			||||||
 | 
					@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 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'
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return response_data, 202
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# @bp.route('/<hashid:corpus_id>/generate-share-link', methods=['POST'])
 | 
				
			||||||
 | 
					# @corpus_follower_permission_required('MANAGE_FOLLOWERS')
 | 
				
			||||||
 | 
					# @content_negotiation(consumes='application/json', produces='application/json')
 | 
				
			||||||
 | 
					# def generate_corpus_share_link(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)
 | 
				
			||||||
 | 
					#     corpus = Corpus.query.get_or_404(corpus_id)
 | 
				
			||||||
 | 
					#     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
 | 
				
			||||||
 | 
					#     }
 | 
				
			||||||
 | 
					#     return response_data, 200
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# @bp.route('/<hashid:corpus_id>/is_public', methods=['PUT'])
 | 
				
			||||||
 | 
					# @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'
 | 
				
			||||||
 | 
					#     }
 | 
				
			||||||
 | 
					#     return response_data, 200
 | 
				
			||||||
@@ -1,139 +1,30 @@
 | 
				
			|||||||
from datetime import datetime
 | 
					from flask import abort, flash, redirect, render_template, url_for
 | 
				
			||||||
from flask import (
 | 
					from flask_breadcrumbs import register_breadcrumb
 | 
				
			||||||
    abort,
 | 
					from flask_login import current_user
 | 
				
			||||||
    current_app,
 | 
					from app import db
 | 
				
			||||||
    flash,
 | 
					from app.models import (
 | 
				
			||||||
    Markup,
 | 
					    Corpus,
 | 
				
			||||||
    redirect,
 | 
					    CorpusFollowerAssociation,
 | 
				
			||||||
    render_template,
 | 
					    CorpusFollowerRole,
 | 
				
			||||||
    request,
 | 
					    User
 | 
				
			||||||
    send_from_directory,
 | 
					 | 
				
			||||||
    url_for
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from flask_login import current_user, login_required
 | 
					 | 
				
			||||||
from threading import Thread
 | 
					 | 
				
			||||||
import jwt
 | 
					 | 
				
			||||||
import os
 | 
					 | 
				
			||||||
from app import db, hashids
 | 
					 | 
				
			||||||
from app.models import Corpus, CorpusFile, CorpusStatus, User
 | 
					 | 
				
			||||||
from . import bp
 | 
					from . import bp
 | 
				
			||||||
from .forms import (
 | 
					from .decorators import corpus_follower_permission_required
 | 
				
			||||||
    CreateCorpusFileForm,
 | 
					from .forms import CreateCorpusForm
 | 
				
			||||||
    CreateCorpusForm,
 | 
					from .utils import (
 | 
				
			||||||
    UpdateCorpusFileForm
 | 
					    corpus_endpoint_arguments_constructor as corpus_eac,
 | 
				
			||||||
 | 
					    corpus_dynamic_list_constructor as corpus_dlc
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# @bp.route('/share/<token>', methods=['GET', 'POST'])
 | 
					@bp.route('')
 | 
				
			||||||
# def share_corpus(token):
 | 
					@register_breadcrumb(bp, '.', '<i class="nopaque-icons left">I</i>My Corpora')
 | 
				
			||||||
#     try:
 | 
					def corpora():
 | 
				
			||||||
#         payload = jwt.decode(
 | 
					    return redirect(url_for('main.dashboard', _anchor='corpora'))
 | 
				
			||||||
#             token,
 | 
					 | 
				
			||||||
#             current_app.config['SECRET_KEY'],
 | 
					 | 
				
			||||||
#             algorithms=['HS256'],
 | 
					 | 
				
			||||||
#             issuer=current_app.config['SERVER_NAME'],
 | 
					 | 
				
			||||||
#             options={'require': ['iat', 'iss', 'sub']}
 | 
					 | 
				
			||||||
#         )
 | 
					 | 
				
			||||||
#     except jwt.PyJWTError:
 | 
					 | 
				
			||||||
#         return False
 | 
					 | 
				
			||||||
#     corpus_hashid = payload.get('sub')
 | 
					 | 
				
			||||||
#     corpus_id = hashids.decode(corpus_hashid)
 | 
					 | 
				
			||||||
#     return redirect(url_for('.corpus', corpus_id=corpus_id))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.route('/<hashid:corpus_id>/enable_is_public', methods=['POST'])
 | 
					 | 
				
			||||||
@login_required
 | 
					 | 
				
			||||||
def enable_corpus_is_public(corpus_id):
 | 
					 | 
				
			||||||
    corpus = Corpus.query.get_or_404(corpus_id)
 | 
					 | 
				
			||||||
    if not (corpus.user == current_user or current_user.is_administrator()):
 | 
					 | 
				
			||||||
        abort(403)
 | 
					 | 
				
			||||||
    corpus.is_public = True
 | 
					 | 
				
			||||||
    db.session.commit()
 | 
					 | 
				
			||||||
    return '', 204
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.route('/<hashid:corpus_id>/disable_is_public', methods=['POST'])
 | 
					 | 
				
			||||||
@login_required
 | 
					 | 
				
			||||||
def disable_corpus_is_public(corpus_id):
 | 
					 | 
				
			||||||
    corpus = Corpus.query.get_or_404(corpus_id)
 | 
					 | 
				
			||||||
    if not (corpus.user == current_user or current_user.is_administrator()):
 | 
					 | 
				
			||||||
        abort(403)
 | 
					 | 
				
			||||||
    corpus.is_public = False
 | 
					 | 
				
			||||||
    db.session.commit()
 | 
					 | 
				
			||||||
    return '', 204
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# @bp.route('/<hashid:corpus_id>/follow', methods=['GET', 'POST'])
 | 
					 | 
				
			||||||
# @login_required
 | 
					 | 
				
			||||||
# def follow_corpus(corpus_id):
 | 
					 | 
				
			||||||
#     corpus = Corpus.query.get_or_404(corpus_id)
 | 
					 | 
				
			||||||
#     user_hashid = request.args.get('user_id')
 | 
					 | 
				
			||||||
#     if user_hashid is None:
 | 
					 | 
				
			||||||
#         user = current_user
 | 
					 | 
				
			||||||
#     else:
 | 
					 | 
				
			||||||
#         if not current_user.is_administrator():
 | 
					 | 
				
			||||||
#             abort(403)
 | 
					 | 
				
			||||||
#         else:
 | 
					 | 
				
			||||||
#             user_id = hashids.decode(user_hashid)
 | 
					 | 
				
			||||||
#             user = User.query.get_or_404(user_id)
 | 
					 | 
				
			||||||
#     if not user.is_following_corpus(corpus):
 | 
					 | 
				
			||||||
#         user.follow_corpus(corpus)
 | 
					 | 
				
			||||||
#     db.session.commit()
 | 
					 | 
				
			||||||
#     flash(f'You are following {corpus.title} now', category='corpus')
 | 
					 | 
				
			||||||
#     return {}, 202
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.route('/<hashid:corpus_id>/unfollow', methods=['GET', 'POST'])
 | 
					 | 
				
			||||||
@login_required
 | 
					 | 
				
			||||||
def unfollow_corpus(corpus_id):
 | 
					 | 
				
			||||||
    corpus = Corpus.query.get_or_404(corpus_id)
 | 
					 | 
				
			||||||
    user_hashid = request.args.get('user_id')
 | 
					 | 
				
			||||||
    if user_hashid is None:
 | 
					 | 
				
			||||||
        user = current_user
 | 
					 | 
				
			||||||
    elif current_user.is_administrator():
 | 
					 | 
				
			||||||
        user_id = hashids.decode(user_hashid)
 | 
					 | 
				
			||||||
        user = User.query.get_or_404(user_id)
 | 
					 | 
				
			||||||
    else:
 | 
					 | 
				
			||||||
        abort(403)
 | 
					 | 
				
			||||||
    if user.is_following_corpus(corpus):
 | 
					 | 
				
			||||||
        user.unfollow_corpus(corpus)
 | 
					 | 
				
			||||||
    db.session.commit()
 | 
					 | 
				
			||||||
    flash(f'You are not following {corpus.title} anymore', category='corpus')
 | 
					 | 
				
			||||||
    return {}, 202
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# @bp.route('/add_permission/<hashid:corpus_id>/<hashid:user_id>/<int:permission>')
 | 
					 | 
				
			||||||
# def add_permission(corpus_id, user_id, permission):
 | 
					 | 
				
			||||||
#     a = CorpusFollowerAssociation.query.filter_by(followed_corpus_id=corpus_id, following_user_id=user_id).first_or_404()
 | 
					 | 
				
			||||||
#     a.add_permission(permission)
 | 
					 | 
				
			||||||
#     db.session.commit()
 | 
					 | 
				
			||||||
#     return 'ok'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# @bp.route('/remove_permission/<hashid:corpus_id>/<hashid:user_id>/<int:permission>')
 | 
					 | 
				
			||||||
# def remove_permission(corpus_id, user_id, permission):
 | 
					 | 
				
			||||||
#     a = CorpusFollowerAssociation.query.filter_by(followed_corpus_id=corpus_id, following_user_id=user_id).first_or_404()
 | 
					 | 
				
			||||||
#     a.remove_permission(permission)
 | 
					 | 
				
			||||||
#     db.session.commit()
 | 
					 | 
				
			||||||
#     return 'ok'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.route('/public')
 | 
					 | 
				
			||||||
@login_required
 | 
					 | 
				
			||||||
def public_corpora():
 | 
					 | 
				
			||||||
    corpora = [
 | 
					 | 
				
			||||||
        c.to_json_serializeable()
 | 
					 | 
				
			||||||
        for c in Corpus.query.filter(Corpus.is_public == True).all()
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
    return render_template(
 | 
					 | 
				
			||||||
        'corpora/public_corpora.html.j2',
 | 
					 | 
				
			||||||
        corpora=corpora,
 | 
					 | 
				
			||||||
        title='Corpora'
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('/create', methods=['GET', 'POST'])
 | 
					@bp.route('/create', methods=['GET', 'POST'])
 | 
				
			||||||
@login_required
 | 
					@register_breadcrumb(bp, '.create', 'Create')
 | 
				
			||||||
def create_corpus():
 | 
					def create_corpus():
 | 
				
			||||||
    form = CreateCorpusForm()
 | 
					    form = CreateCorpusForm()
 | 
				
			||||||
    if form.validate_on_submit():
 | 
					    if form.validate_on_submit():
 | 
				
			||||||
@@ -146,224 +37,85 @@ def create_corpus():
 | 
				
			|||||||
        except OSError:
 | 
					        except OSError:
 | 
				
			||||||
            abort(500)
 | 
					            abort(500)
 | 
				
			||||||
        db.session.commit()
 | 
					        db.session.commit()
 | 
				
			||||||
        message = Markup(
 | 
					        flash(f'Corpus "{corpus.title}" created', 'corpus')
 | 
				
			||||||
            f'Corpus "<a href="{corpus.url}">{corpus.title}</a>" created'
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        flash(message, 'corpus')
 | 
					 | 
				
			||||||
        return redirect(corpus.url)
 | 
					        return redirect(corpus.url)
 | 
				
			||||||
    return render_template(
 | 
					    return render_template(
 | 
				
			||||||
        'corpora/create_corpus.html.j2',
 | 
					        'corpora/create.html.j2',
 | 
				
			||||||
        form=form,
 | 
					        title='Create corpus',
 | 
				
			||||||
        title='Create corpus'
 | 
					        form=form
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('/<hashid:corpus_id>', methods=['GET', 'POST'])
 | 
					@bp.route('/<hashid:corpus_id>')
 | 
				
			||||||
@login_required
 | 
					@register_breadcrumb(bp, '.entity', '', dynamic_list_constructor=corpus_dlc)
 | 
				
			||||||
def corpus(corpus_id):
 | 
					def corpus(corpus_id):
 | 
				
			||||||
    corpus = Corpus.query.get_or_404(corpus_id)
 | 
					    corpus = Corpus.query.get_or_404(corpus_id)
 | 
				
			||||||
 | 
					    cfrs = CorpusFollowerRole.query.all()
 | 
				
			||||||
 | 
					    # TODO: Better solution for filtering admin
 | 
				
			||||||
 | 
					    users = User.query.filter(User.is_public == True, User.id != current_user.id, User.id != corpus.user.id, User.role_id < 4).all()
 | 
				
			||||||
 | 
					    cfa = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=current_user.id).first()
 | 
				
			||||||
 | 
					    if cfa is None:
 | 
				
			||||||
 | 
					        if corpus.user == current_user or current_user.is_administrator():
 | 
				
			||||||
 | 
					            cfr = CorpusFollowerRole.query.filter_by(name='Administrator').first()
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            cfr = CorpusFollowerRole.query.filter_by(name='Anonymous').first()
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        cfr = cfa.role
 | 
				
			||||||
    if corpus.user == current_user or current_user.is_administrator():
 | 
					    if corpus.user == current_user or current_user.is_administrator():
 | 
				
			||||||
        # now = datetime.utcnow()
 | 
					 | 
				
			||||||
        # payload = {
 | 
					 | 
				
			||||||
        #         'exp': now + timedelta(weeks=1),
 | 
					 | 
				
			||||||
        #         'iat': now,
 | 
					 | 
				
			||||||
        #         'iss': current_app.config['SERVER_NAME'],
 | 
					 | 
				
			||||||
        #         'sub': corpus.hashid
 | 
					 | 
				
			||||||
        #     }
 | 
					 | 
				
			||||||
        # token = jwt.encode(
 | 
					 | 
				
			||||||
        #         payload,
 | 
					 | 
				
			||||||
        #         current_app.config['SECRET_KEY'],
 | 
					 | 
				
			||||||
        #         algorithm='HS256'
 | 
					 | 
				
			||||||
        #     )
 | 
					 | 
				
			||||||
        return render_template(
 | 
					        return render_template(
 | 
				
			||||||
            'corpora/corpus.html.j2',
 | 
					            'corpora/corpus.html.j2',
 | 
				
			||||||
 | 
					            title=corpus.title,
 | 
				
			||||||
            corpus=corpus,
 | 
					            corpus=corpus,
 | 
				
			||||||
            # token=token,
 | 
					            cfr=cfr,
 | 
				
			||||||
            title='Corpus'
 | 
					            cfrs=cfrs,
 | 
				
			||||||
        )
 | 
					            users = users
 | 
				
			||||||
    if current_user.is_following_corpus(corpus) or corpus.is_public:
 | 
					 | 
				
			||||||
        corpus_files = [x.to_json_serializeable() for x in corpus.files]
 | 
					 | 
				
			||||||
        return render_template(
 | 
					 | 
				
			||||||
            'corpora/public_corpus.html.j2',
 | 
					 | 
				
			||||||
            corpus=corpus,
 | 
					 | 
				
			||||||
            corpus_files=corpus_files,
 | 
					 | 
				
			||||||
            title='Corpus'
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					    if (current_user.is_following_corpus(corpus) or corpus.is_public):
 | 
				
			||||||
 | 
					        abort(404)
 | 
				
			||||||
 | 
					        # cfas = CorpusFollowerAssociation.query.filter(Corpus.id == corpus_id, CorpusFollowerAssociation.follower_id != corpus.user.id).all()
 | 
				
			||||||
 | 
					        # return render_template(
 | 
				
			||||||
 | 
					        #     'corpora/public_corpus.html.j2',
 | 
				
			||||||
 | 
					        #     title=corpus.title,
 | 
				
			||||||
 | 
					        #     corpus=corpus,
 | 
				
			||||||
 | 
					        #     cfrs=cfrs,
 | 
				
			||||||
 | 
					        #     cfr=cfr,
 | 
				
			||||||
 | 
					        #     cfas=cfas,
 | 
				
			||||||
 | 
					        #     users = users
 | 
				
			||||||
 | 
					        # )
 | 
				
			||||||
    abort(403)
 | 
					    abort(403)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('/<hashid:corpus_id>', methods=['DELETE'])
 | 
					 | 
				
			||||||
@login_required
 | 
					 | 
				
			||||||
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()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/<hashid:corpus_id>/analysis')
 | 
				
			||||||
 | 
					@corpus_follower_permission_required('VIEW')
 | 
				
			||||||
 | 
					@register_breadcrumb(bp, '.entity.analysis', 'Analysis', endpoint_arguments_constructor=corpus_eac)
 | 
				
			||||||
 | 
					def analysis(corpus_id):
 | 
				
			||||||
    corpus = Corpus.query.get_or_404(corpus_id)
 | 
					    corpus = Corpus.query.get_or_404(corpus_id)
 | 
				
			||||||
    if not (corpus.user == current_user or current_user.is_administrator()):
 | 
					 | 
				
			||||||
        abort(403)
 | 
					 | 
				
			||||||
    thread = Thread(
 | 
					 | 
				
			||||||
        target=_delete_corpus,
 | 
					 | 
				
			||||||
        args=(current_app._get_current_object(), corpus_id)
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    thread.start()
 | 
					 | 
				
			||||||
    return {}, 202
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.route('/<hashid:corpus_id>/analyse')
 | 
					 | 
				
			||||||
@login_required
 | 
					 | 
				
			||||||
def analyse_corpus(corpus_id):
 | 
					 | 
				
			||||||
    corpus = Corpus.query.get_or_404(corpus_id)
 | 
					 | 
				
			||||||
    if not (corpus.user == current_user
 | 
					 | 
				
			||||||
            or current_user.is_administrator()
 | 
					 | 
				
			||||||
            or current_user.is_following_corpus(corpus)):
 | 
					 | 
				
			||||||
        abort(403)
 | 
					 | 
				
			||||||
    return render_template(
 | 
					    return render_template(
 | 
				
			||||||
        'corpora/analyse_corpus.html.j2',
 | 
					        'corpora/analysis.html.j2',
 | 
				
			||||||
        corpus=corpus,
 | 
					        corpus=corpus,
 | 
				
			||||||
        title=f'Analyse Corpus {corpus.title}'
 | 
					        title=f'Analyse Corpus {corpus.title}'
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('/<hashid:corpus_id>/build', methods=['POST'])
 | 
					# @bp.route('/<hashid:corpus_id>/follow/<token>')
 | 
				
			||||||
@login_required
 | 
					# def follow_corpus(corpus_id, token):
 | 
				
			||||||
def build_corpus(corpus_id):
 | 
					#     corpus = Corpus.query.get_or_404(corpus_id)
 | 
				
			||||||
    def _build_corpus(app, corpus_id):
 | 
					#     if current_user.follow_corpus_by_token(token):
 | 
				
			||||||
        with app.app_context():
 | 
					#         db.session.commit()
 | 
				
			||||||
            corpus = Corpus.query.get(corpus_id)
 | 
					#         flash(f'You are following "{corpus.title}" now', category='corpus')
 | 
				
			||||||
            corpus.build()
 | 
					#         return redirect(url_for('corpora.corpus', corpus_id=corpus_id))
 | 
				
			||||||
            db.session.commit()
 | 
					#     abort(403)
 | 
				
			||||||
 | 
					 | 
				
			||||||
    corpus = Corpus.query.get_or_404(corpus_id)
 | 
					 | 
				
			||||||
    if not (corpus.user == current_user or current_user.is_administrator()):
 | 
					 | 
				
			||||||
        abort(403)
 | 
					 | 
				
			||||||
    # Check if the corpus has corpus files
 | 
					 | 
				
			||||||
    if not corpus.files.all():
 | 
					 | 
				
			||||||
        response = {'errors': {'message': 'Corpus file(s) required'}}
 | 
					 | 
				
			||||||
        return response, 409
 | 
					 | 
				
			||||||
    thread = Thread(
 | 
					 | 
				
			||||||
        target=_build_corpus,
 | 
					 | 
				
			||||||
        args=(current_app._get_current_object(), corpus_id)
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    thread.start()
 | 
					 | 
				
			||||||
    return {}, 202
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.route('/<hashid:corpus_id>/files/create', methods=['GET', 'POST'])
 | 
					 | 
				
			||||||
@login_required
 | 
					 | 
				
			||||||
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
 | 
					 | 
				
			||||||
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()
 | 
					 | 
				
			||||||
    if not (corpus_file.corpus.user == current_user or current_user.is_administrator()):
 | 
					 | 
				
			||||||
        abort(403)
 | 
					 | 
				
			||||||
    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>', methods=['DELETE'])
 | 
					 | 
				
			||||||
@login_required
 | 
					 | 
				
			||||||
def delete_corpus_file(corpus_id, corpus_file_id):
 | 
					 | 
				
			||||||
    def _delete_corpus_file(app, corpus_file_id):
 | 
					 | 
				
			||||||
        with app.app_context():
 | 
					 | 
				
			||||||
            corpus_file = CorpusFile.query.get(corpus_file_id)
 | 
					 | 
				
			||||||
            corpus_file.delete()
 | 
					 | 
				
			||||||
            db.session.commit()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    corpus_file = CorpusFile.query.filter_by(corpus_id = corpus_id, id=corpus_file_id).first_or_404()
 | 
					 | 
				
			||||||
    if not (corpus_file.corpus.user == current_user or current_user.is_administrator()):
 | 
					 | 
				
			||||||
        abort(403)
 | 
					 | 
				
			||||||
    thread = Thread(
 | 
					 | 
				
			||||||
        target=_delete_corpus_file,
 | 
					 | 
				
			||||||
        args=(current_app._get_current_object(), corpus_file_id)
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    thread.start()
 | 
					 | 
				
			||||||
    return {}, 202
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>/download')
 | 
					 | 
				
			||||||
@login_required
 | 
					 | 
				
			||||||
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
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('/import', methods=['GET', 'POST'])
 | 
					@bp.route('/import', methods=['GET', 'POST'])
 | 
				
			||||||
@login_required
 | 
					@register_breadcrumb(bp, '.import', 'Import')
 | 
				
			||||||
def import_corpus():
 | 
					def import_corpus():
 | 
				
			||||||
    abort(503)
 | 
					    abort(503)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('/<hashid:corpus_id>/export')
 | 
					@bp.route('/<hashid:corpus_id>/export')
 | 
				
			||||||
@login_required
 | 
					@corpus_follower_permission_required('VIEW')
 | 
				
			||||||
 | 
					@register_breadcrumb(bp, '.entity.export', 'Export', endpoint_arguments_constructor=corpus_eac)
 | 
				
			||||||
def export_corpus(corpus_id):
 | 
					def export_corpus(corpus_id):
 | 
				
			||||||
    abort(503)
 | 
					    abort(503)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										17
									
								
								app/corpora/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/corpora/utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					from flask import request, url_for
 | 
				
			||||||
 | 
					from app.models import Corpus
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def corpus_endpoint_arguments_constructor():
 | 
				
			||||||
 | 
					    return {'corpus_id': request.view_args['corpus_id']}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def corpus_dynamic_list_constructor():
 | 
				
			||||||
 | 
					    corpus_id = request.view_args['corpus_id']
 | 
				
			||||||
 | 
					    corpus = Corpus.query.get_or_404(corpus_id)
 | 
				
			||||||
 | 
					    return [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            'text': f'<i class="material-icons left">book</i>{corpus.title}',
 | 
				
			||||||
 | 
					            'url': url_for('.corpus', corpus_id=corpus_id)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -1,7 +1,9 @@
 | 
				
			|||||||
from flask import abort, current_app
 | 
					from flask import abort, current_app, request
 | 
				
			||||||
from flask_login import current_user
 | 
					from flask_login import current_user
 | 
				
			||||||
from functools import wraps
 | 
					from functools import wraps
 | 
				
			||||||
from threading import Thread
 | 
					from threading import Thread
 | 
				
			||||||
 | 
					from typing import List, Union
 | 
				
			||||||
 | 
					from werkzeug.exceptions import NotAcceptable
 | 
				
			||||||
from app.models import Permission
 | 
					from app.models import Permission
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -61,3 +63,37 @@ def background(f):
 | 
				
			|||||||
        thread.start()
 | 
					        thread.start()
 | 
				
			||||||
        return thread
 | 
					        return thread
 | 
				
			||||||
    return wrapped
 | 
					    return wrapped
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def content_negotiation(
 | 
				
			||||||
 | 
					    produces: Union[str, List[str], None] = None,
 | 
				
			||||||
 | 
					    consumes: Union[str, List[str], None] = None
 | 
				
			||||||
 | 
					):
 | 
				
			||||||
 | 
					    def decorator(f):
 | 
				
			||||||
 | 
					        @wraps(f)
 | 
				
			||||||
 | 
					        def decorated_function(*args, **kwargs):
 | 
				
			||||||
 | 
					            provided = request.mimetype
 | 
				
			||||||
 | 
					            if consumes is None:
 | 
				
			||||||
 | 
					                consumeables = None
 | 
				
			||||||
 | 
					            elif isinstance(consumes, str):
 | 
				
			||||||
 | 
					                consumeables = {consumes}
 | 
				
			||||||
 | 
					            elif isinstance(consumes, list) and all(isinstance(x, str) for x in consumes):
 | 
				
			||||||
 | 
					                consumeables = {*consumes}
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                raise TypeError()
 | 
				
			||||||
 | 
					            accepted = {*request.accept_mimetypes.values()}
 | 
				
			||||||
 | 
					            if produces is None:
 | 
				
			||||||
 | 
					                produceables = None
 | 
				
			||||||
 | 
					            elif isinstance(produces, str):
 | 
				
			||||||
 | 
					                produceables = {produces}
 | 
				
			||||||
 | 
					            elif isinstance(produces, list) and all(isinstance(x, str) for x in produces):
 | 
				
			||||||
 | 
					                produceables = {*produces}
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                raise TypeError()
 | 
				
			||||||
 | 
					            if produceables is not None and len(produceables & accepted) == 0:
 | 
				
			||||||
 | 
					                raise NotAcceptable()
 | 
				
			||||||
 | 
					            if consumeables is not None and provided not in consumeables:
 | 
				
			||||||
 | 
					                raise NotAcceptable()
 | 
				
			||||||
 | 
					            return f(*args, **kwargs)
 | 
				
			||||||
 | 
					        return decorated_function
 | 
				
			||||||
 | 
					    return decorator
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,14 @@
 | 
				
			|||||||
from flask import render_template, request
 | 
					from flask import jsonify, render_template, request
 | 
				
			||||||
from werkzeug.exceptions import HTTPException
 | 
					from werkzeug.exceptions import HTTPException
 | 
				
			||||||
from . import bp
 | 
					from . import bp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.errorhandler(HTTPException)
 | 
					@bp.app_errorhandler(HTTPException)
 | 
				
			||||||
def generic_error_handler(e):
 | 
					def handle_http_exception(error):
 | 
				
			||||||
    if (request.accept_mimetypes.accept_json
 | 
					    ''' Generic HTTP exception handler '''
 | 
				
			||||||
            and not request.accept_mimetypes.accept_html):
 | 
					    accept_json = request.accept_mimetypes.accept_json
 | 
				
			||||||
        return {'errors': {'message': e.description}}, e.code
 | 
					    accept_html = request.accept_mimetypes.accept_html
 | 
				
			||||||
    return render_template('errors/error.html.j2', error=e), e.code
 | 
					    if accept_json and not accept_html:
 | 
				
			||||||
 | 
					        response = jsonify(str(error))
 | 
				
			||||||
 | 
					        return response, error.code
 | 
				
			||||||
 | 
					    return render_template('errors/error.html.j2', error=error), error.code
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,18 @@
 | 
				
			|||||||
from flask import Blueprint
 | 
					from flask import Blueprint
 | 
				
			||||||
 | 
					from flask_login import login_required
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
bp = Blueprint('jobs', __name__)
 | 
					bp = Blueprint('jobs', __name__)
 | 
				
			||||||
from . import routes
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.before_request
 | 
				
			||||||
 | 
					@login_required
 | 
				
			||||||
 | 
					def before_request():
 | 
				
			||||||
 | 
					    '''
 | 
				
			||||||
 | 
					    Ensures that the routes in this package can only be visited by users that
 | 
				
			||||||
 | 
					    are logged in.
 | 
				
			||||||
 | 
					    '''
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from . import routes, json_routes
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										74
									
								
								app/jobs/json_routes.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								app/jobs/json_routes.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,74 @@
 | 
				
			|||||||
 | 
					from flask import abort, current_app
 | 
				
			||||||
 | 
					from flask_login import current_user
 | 
				
			||||||
 | 
					from threading import Thread
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					from app import db
 | 
				
			||||||
 | 
					from app.decorators import admin_required, content_negotiation
 | 
				
			||||||
 | 
					from app.models import Job, JobStatus
 | 
				
			||||||
 | 
					from . import bp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/<hashid:job_id>', methods=['DELETE'])
 | 
				
			||||||
 | 
					@content_negotiation(produces='application/json')
 | 
				
			||||||
 | 
					def delete_job(job_id):
 | 
				
			||||||
 | 
					    def _delete_job(app, job_id):
 | 
				
			||||||
 | 
					        with app.app_context():
 | 
				
			||||||
 | 
					            job = Job.query.get(job_id)
 | 
				
			||||||
 | 
					            job.delete()
 | 
				
			||||||
 | 
					            db.session.commit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    job = Job.query.get_or_404(job_id)
 | 
				
			||||||
 | 
					    if not (job.user == current_user or current_user.is_administrator()):
 | 
				
			||||||
 | 
					        abort(403)
 | 
				
			||||||
 | 
					    thread = Thread(
 | 
				
			||||||
 | 
					        target=_delete_job,
 | 
				
			||||||
 | 
					        args=(current_app._get_current_object(), job_id)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    thread.start()
 | 
				
			||||||
 | 
					    response_data = {
 | 
				
			||||||
 | 
					        'message': f'Job "{job.title}" marked for deletion'
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return response_data, 202
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/<hashid:job_id>/log')
 | 
				
			||||||
 | 
					@admin_required
 | 
				
			||||||
 | 
					@content_negotiation(produces='application/json')
 | 
				
			||||||
 | 
					def job_log(job_id):
 | 
				
			||||||
 | 
					    job = Job.query.get_or_404(job_id)
 | 
				
			||||||
 | 
					    if job.status not in [JobStatus.COMPLETED, JobStatus.FAILED]:
 | 
				
			||||||
 | 
					        response = {'errors': {'message': 'Job status is not completed or failed'}}
 | 
				
			||||||
 | 
					        return response, 409
 | 
				
			||||||
 | 
					    with open(os.path.join(job.path, 'pipeline_data', 'logs', 'pyflow_log.txt')) as log_file:
 | 
				
			||||||
 | 
					        log = log_file.read()
 | 
				
			||||||
 | 
					    response_data = {
 | 
				
			||||||
 | 
					        'message': '',
 | 
				
			||||||
 | 
					        'jobLog': log
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return response_data, 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/<hashid:job_id>/restart', methods=['POST'])
 | 
				
			||||||
 | 
					@content_negotiation(produces='application/json')
 | 
				
			||||||
 | 
					def restart_job(job_id):
 | 
				
			||||||
 | 
					    def _restart_job(app, job_id):
 | 
				
			||||||
 | 
					        with app.app_context():
 | 
				
			||||||
 | 
					            job = Job.query.get(job_id)
 | 
				
			||||||
 | 
					            job.restart()
 | 
				
			||||||
 | 
					            db.session.commit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    job = Job.query.get_or_404(job_id)
 | 
				
			||||||
 | 
					    if not (job.user == current_user or current_user.is_administrator()):
 | 
				
			||||||
 | 
					        abort(403)
 | 
				
			||||||
 | 
					    if job.status == JobStatus.FAILED:
 | 
				
			||||||
 | 
					        response = {'errors': {'message': 'Job status is not "failed"'}}
 | 
				
			||||||
 | 
					        return response, 409
 | 
				
			||||||
 | 
					    thread = Thread(
 | 
				
			||||||
 | 
					        target=_restart_job,
 | 
				
			||||||
 | 
					        args=(current_app._get_current_object(), job_id)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    thread.start()
 | 
				
			||||||
 | 
					    response_data = {
 | 
				
			||||||
 | 
					        'message': f'Job "{job.title}" marked for restarting'
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return response_data, 202
 | 
				
			||||||
@@ -1,93 +1,40 @@
 | 
				
			|||||||
from flask import (
 | 
					from flask import (
 | 
				
			||||||
    abort,
 | 
					    abort,
 | 
				
			||||||
    current_app,
 | 
					    redirect,
 | 
				
			||||||
    render_template,
 | 
					    render_template,
 | 
				
			||||||
    send_from_directory
 | 
					    send_from_directory,
 | 
				
			||||||
 | 
					    url_for
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from flask_login import current_user, login_required
 | 
					from flask_breadcrumbs import register_breadcrumb
 | 
				
			||||||
from threading import Thread
 | 
					from flask_login import current_user
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
from app import db
 | 
					from app.models import Job, JobInput, JobResult
 | 
				
			||||||
from app.decorators import admin_required
 | 
					 | 
				
			||||||
from app.models import Job, JobInput, JobResult, JobStatus
 | 
					 | 
				
			||||||
from . import bp
 | 
					from . import bp
 | 
				
			||||||
 | 
					from .utils import job_dynamic_list_constructor as job_dlc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('')
 | 
				
			||||||
 | 
					@register_breadcrumb(bp, '.', '<i class="nopaque-icons left">J</i>My Jobs')
 | 
				
			||||||
 | 
					def corpora():
 | 
				
			||||||
 | 
					    return redirect(url_for('main.dashboard', _anchor='jobs'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('/<hashid:job_id>')
 | 
					@bp.route('/<hashid:job_id>')
 | 
				
			||||||
@login_required
 | 
					@register_breadcrumb(bp, '.entity', '', dynamic_list_constructor=job_dlc)
 | 
				
			||||||
def job(job_id):
 | 
					def job(job_id):
 | 
				
			||||||
    job = Job.query.get_or_404(job_id)
 | 
					    job = Job.query.get_or_404(job_id)
 | 
				
			||||||
    if not (job.user == current_user or current_user.is_administrator()):
 | 
					    if not (job.user == current_user or current_user.is_administrator()):
 | 
				
			||||||
        abort(403)
 | 
					        abort(403)
 | 
				
			||||||
    return render_template(
 | 
					    return render_template(
 | 
				
			||||||
        'jobs/job.html.j2',
 | 
					        'jobs/job.html.j2',
 | 
				
			||||||
        job=job,
 | 
					        title='Job',
 | 
				
			||||||
        title='Job'
 | 
					        job=job
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('/<hashid:job_id>', methods=['DELETE'])
 | 
					 | 
				
			||||||
@login_required
 | 
					 | 
				
			||||||
def delete_job(job_id):
 | 
					 | 
				
			||||||
    def _delete_job(app, job_id):
 | 
					 | 
				
			||||||
        with app.app_context():
 | 
					 | 
				
			||||||
            job = Job.query.get(job_id)
 | 
					 | 
				
			||||||
            job.delete()
 | 
					 | 
				
			||||||
            db.session.commit()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    job = Job.query.get_or_404(job_id)
 | 
					 | 
				
			||||||
    if not (job.user == current_user or current_user.is_administrator()):
 | 
					 | 
				
			||||||
        abort(403)
 | 
					 | 
				
			||||||
    thread = Thread(
 | 
					 | 
				
			||||||
        target=_delete_job,
 | 
					 | 
				
			||||||
        args=(current_app._get_current_object(), job_id)
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    thread.start()
 | 
					 | 
				
			||||||
    return {}, 202
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.route('/<hashid:job_id>/log')
 | 
					 | 
				
			||||||
@login_required
 | 
					 | 
				
			||||||
@admin_required
 | 
					 | 
				
			||||||
def job_log(job_id):
 | 
					 | 
				
			||||||
    job = Job.query.get_or_404(job_id)
 | 
					 | 
				
			||||||
    if job.status not in [JobStatus.COMPLETED, JobStatus.FAILED]:
 | 
					 | 
				
			||||||
        response = {'errors': {'message': 'Job status is not completed or failed'}}
 | 
					 | 
				
			||||||
        return response, 409
 | 
					 | 
				
			||||||
    with open(os.path.join(job.path, 'pipeline_data', 'logs', 'pyflow_log.txt')) as log_file:
 | 
					 | 
				
			||||||
        log = log_file.read()
 | 
					 | 
				
			||||||
    return log, 200, {'Content-Type': 'text/plain; charset=utf-8'}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.route('/<hashid:job_id>/restart', methods=['POST'])
 | 
					 | 
				
			||||||
@login_required
 | 
					 | 
				
			||||||
def restart_job(job_id):
 | 
					 | 
				
			||||||
    def _restart_job(app, job_id):
 | 
					 | 
				
			||||||
        with app.app_context():
 | 
					 | 
				
			||||||
            job = Job.query.get(job_id)
 | 
					 | 
				
			||||||
            job.restart()
 | 
					 | 
				
			||||||
            db.session.commit()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    job = Job.query.get_or_404(job_id)
 | 
					 | 
				
			||||||
    if not (job.user == current_user or current_user.is_administrator()):
 | 
					 | 
				
			||||||
        abort(403)
 | 
					 | 
				
			||||||
    if job.status == JobStatus.FAILED:
 | 
					 | 
				
			||||||
        response = {'errors': {'message': 'Job status is not "failed"'}}
 | 
					 | 
				
			||||||
        return response, 409
 | 
					 | 
				
			||||||
    thread = Thread(
 | 
					 | 
				
			||||||
        target=_restart_job,
 | 
					 | 
				
			||||||
        args=(current_app._get_current_object(), job_id)
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    thread.start()
 | 
					 | 
				
			||||||
    return {}, 202
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.route('/<hashid:job_id>/inputs/<hashid:job_input_id>/download')
 | 
					@bp.route('/<hashid:job_id>/inputs/<hashid:job_input_id>/download')
 | 
				
			||||||
@login_required
 | 
					 | 
				
			||||||
def download_job_input(job_id, job_input_id):
 | 
					def download_job_input(job_id, job_input_id):
 | 
				
			||||||
    job_input = JobInput.query.get_or_404(job_input_id)
 | 
					    job_input = JobInput.query.filter_by(job_id=job_id, id=job_input_id).first_or_404()
 | 
				
			||||||
    if job_input.job.id != job_id:
 | 
					 | 
				
			||||||
        abort(404)
 | 
					 | 
				
			||||||
    if not (job_input.job.user == current_user or current_user.is_administrator()):
 | 
					    if not (job_input.job.user == current_user or current_user.is_administrator()):
 | 
				
			||||||
        abort(403)
 | 
					        abort(403)
 | 
				
			||||||
    return send_from_directory(
 | 
					    return send_from_directory(
 | 
				
			||||||
@@ -100,11 +47,8 @@ def download_job_input(job_id, job_input_id):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('/<hashid:job_id>/results/<hashid:job_result_id>/download')
 | 
					@bp.route('/<hashid:job_id>/results/<hashid:job_result_id>/download')
 | 
				
			||||||
@login_required
 | 
					 | 
				
			||||||
def download_job_result(job_id, job_result_id):
 | 
					def download_job_result(job_id, job_result_id):
 | 
				
			||||||
    job_result = JobResult.query.get_or_404(job_result_id)
 | 
					    job_result = JobResult.query.filter_by(job_id=job_id, id=job_result_id).first_or_404()
 | 
				
			||||||
    if job_result.job.id != job_id:
 | 
					 | 
				
			||||||
        abort(404)
 | 
					 | 
				
			||||||
    if not (job_result.job.user == current_user or current_user.is_administrator()):
 | 
					    if not (job_result.job.user == current_user or current_user.is_administrator()):
 | 
				
			||||||
        abort(403)
 | 
					        abort(403)
 | 
				
			||||||
    return send_from_directory(
 | 
					    return send_from_directory(
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										13
									
								
								app/jobs/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								app/jobs/utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					from flask import request, url_for
 | 
				
			||||||
 | 
					from app.models import Job
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def job_dynamic_list_constructor():
 | 
				
			||||||
 | 
					    job_id = request.view_args['job_id']
 | 
				
			||||||
 | 
					    job = Job.query.get_or_404(job_id)
 | 
				
			||||||
 | 
					    return [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            'text': f'<i class="nopaque-icons left service-icons" data-service="{job.service}"></i>{job.title}',
 | 
				
			||||||
 | 
					            'url': url_for('.job', job_id=job_id)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
from flask import Blueprint
 | 
					from flask import Blueprint
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
bp = Blueprint('main', __name__)
 | 
					bp = Blueprint('main', __name__, cli_group=None)
 | 
				
			||||||
from . import routes
 | 
					from . import cli, routes
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										45
									
								
								app/main/cli.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								app/main/cli.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					from flask import current_app
 | 
				
			||||||
 | 
					from flask_migrate import upgrade
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					from app.models import (
 | 
				
			||||||
 | 
					    CorpusFollowerRole,
 | 
				
			||||||
 | 
					    Role,
 | 
				
			||||||
 | 
					    SpaCyNLPPipelineModel,
 | 
				
			||||||
 | 
					    TesseractOCRPipelineModel,
 | 
				
			||||||
 | 
					    User
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					from . import bp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.cli.command('deploy')
 | 
				
			||||||
 | 
					def deploy():
 | 
				
			||||||
 | 
					    ''' Run deployment tasks. '''
 | 
				
			||||||
 | 
					    # Make default directories
 | 
				
			||||||
 | 
					    print('Make default directories')
 | 
				
			||||||
 | 
					    base_dir = current_app.config['NOPAQUE_DATA_DIR']
 | 
				
			||||||
 | 
					    default_dirs = [
 | 
				
			||||||
 | 
					        os.path.join(base_dir, 'tmp'),
 | 
				
			||||||
 | 
					        os.path.join(base_dir, 'users')
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					    for dir in default_dirs:
 | 
				
			||||||
 | 
					        if os.path.exists(dir):
 | 
				
			||||||
 | 
					            if not os.path.isdir(dir):
 | 
				
			||||||
 | 
					                raise NotADirectoryError(f'{dir} is not a directory')
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            os.mkdir(dir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # migrate database to latest revision
 | 
				
			||||||
 | 
					    print('Migrate database to latest revision')
 | 
				
			||||||
 | 
					    upgrade()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Insert/Update default database values
 | 
				
			||||||
 | 
					    print('Insert/Update default Roles')
 | 
				
			||||||
 | 
					    Role.insert_defaults()
 | 
				
			||||||
 | 
					    print('Insert/Update default Users')
 | 
				
			||||||
 | 
					    User.insert_defaults()
 | 
				
			||||||
 | 
					    print('Insert/Update default CorpusFollowerRoles')
 | 
				
			||||||
 | 
					    CorpusFollowerRole.insert_defaults()
 | 
				
			||||||
 | 
					    print('Insert/Update default SpaCyNLPPipelineModels')
 | 
				
			||||||
 | 
					    SpaCyNLPPipelineModel.insert_defaults()
 | 
				
			||||||
 | 
					    print('Insert/Update default TesseractOCRPipelineModels')
 | 
				
			||||||
 | 
					    TesseractOCRPipelineModel.insert_defaults()
 | 
				
			||||||
@@ -1,13 +1,16 @@
 | 
				
			|||||||
from flask import flash, redirect, render_template, url_for
 | 
					from flask import flash, redirect, render_template, url_for
 | 
				
			||||||
 | 
					from flask_breadcrumbs import register_breadcrumb
 | 
				
			||||||
from flask_login import current_user, login_required, login_user
 | 
					from flask_login import current_user, login_required, login_user
 | 
				
			||||||
from app.auth.forms import LoginForm
 | 
					from app.auth.forms import LoginForm
 | 
				
			||||||
from app.models import Corpus, User
 | 
					from app.models import Corpus, User
 | 
				
			||||||
 | 
					from sqlalchemy import or_
 | 
				
			||||||
from . import bp
 | 
					from . import bp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('', methods=['GET', 'POST'])
 | 
					@bp.route('/', methods=['GET', 'POST'])
 | 
				
			||||||
 | 
					@register_breadcrumb(bp, '.', '<i class="material-icons">home</i>')
 | 
				
			||||||
def index():
 | 
					def index():
 | 
				
			||||||
    form = LoginForm(prefix='login-form')
 | 
					    form = LoginForm()
 | 
				
			||||||
    if form.validate_on_submit():
 | 
					    if form.validate_on_submit():
 | 
				
			||||||
        user = User.query.filter((User.email == form.user.data.lower()) | (User.username == form.user.data)).first()
 | 
					        user = User.query.filter((User.email == form.user.data.lower()) | (User.username == form.user.data)).first()
 | 
				
			||||||
        if user and user.verify_password(form.password.data):
 | 
					        if user and user.verify_password(form.password.data):
 | 
				
			||||||
@@ -16,54 +19,74 @@ def index():
 | 
				
			|||||||
            return redirect(url_for('.dashboard'))
 | 
					            return redirect(url_for('.dashboard'))
 | 
				
			||||||
        flash('Invalid email/username or password', category='error')
 | 
					        flash('Invalid email/username or password', category='error')
 | 
				
			||||||
        redirect(url_for('.index'))
 | 
					        redirect(url_for('.index'))
 | 
				
			||||||
    return render_template('main/index.html.j2', form=form, title='nopaque')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.route('/faq')
 | 
					 | 
				
			||||||
def faq():
 | 
					 | 
				
			||||||
    return render_template('main/faq.html.j2', title='Frequently Asked Questions')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@bp.route('/dashboard')
 | 
					 | 
				
			||||||
@login_required
 | 
					 | 
				
			||||||
def dashboard():
 | 
					 | 
				
			||||||
    # users = [
 | 
					 | 
				
			||||||
    #     u.to_json_serializeable(filter_by_privacy_settings=True) for u
 | 
					 | 
				
			||||||
    #     in User.query.filter(User.is_public == True, User.id != current_user.id).all()
 | 
					 | 
				
			||||||
    # ]
 | 
					 | 
				
			||||||
    # corpora = [
 | 
					 | 
				
			||||||
    #     c.to_json_serializeable() for c
 | 
					 | 
				
			||||||
    #     in Corpus.query.filter(Corpus.is_public == True, Corpus.user != current_user).all()
 | 
					 | 
				
			||||||
    # ]
 | 
					 | 
				
			||||||
    return render_template(
 | 
					    return render_template(
 | 
				
			||||||
        'main/dashboard.html.j2',
 | 
					        'main/index.html.j2',
 | 
				
			||||||
        title='Dashboard',
 | 
					        title='nopaque',
 | 
				
			||||||
        # users=users,
 | 
					        form=form
 | 
				
			||||||
        # corpora=corpora
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('/dashboard2')
 | 
					@bp.route('/faq')
 | 
				
			||||||
 | 
					@register_breadcrumb(bp, '.faq', 'Frequently Asked Questions')
 | 
				
			||||||
 | 
					def faq():
 | 
				
			||||||
 | 
					    return render_template(
 | 
				
			||||||
 | 
					        'main/faq.html.j2',
 | 
				
			||||||
 | 
					        title='Frequently Asked Questions'
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/dashboard')
 | 
				
			||||||
 | 
					@register_breadcrumb(bp, '.dashboard', '<i class="material-icons left">dashboard</i>Dashboard')
 | 
				
			||||||
@login_required
 | 
					@login_required
 | 
				
			||||||
def dashboard2():
 | 
					def dashboard():
 | 
				
			||||||
    return render_template('main/dashboard2.html.j2', title='Dashboard')
 | 
					    return render_template(
 | 
				
			||||||
 | 
					        'main/dashboard.html.j2',
 | 
				
			||||||
 | 
					        title='Dashboard'
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('/user_manual')
 | 
					# @bp.route('/user_manual')
 | 
				
			||||||
def user_manual():
 | 
					# @register_breadcrumb(bp, '.user_manual', '<i class="material-icons left">help</i>User manual')
 | 
				
			||||||
    return render_template('main/user_manual.html.j2', title='User manual')
 | 
					# def user_manual():
 | 
				
			||||||
 | 
					#     return render_template('main/user_manual.html.j2', title='User manual')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('/news')
 | 
					@bp.route('/news')
 | 
				
			||||||
 | 
					@register_breadcrumb(bp, '.news', '<i class="material-icons left">email</i>News')
 | 
				
			||||||
def news():
 | 
					def news():
 | 
				
			||||||
    return render_template('main/news.html.j2', title='News')
 | 
					    return render_template(
 | 
				
			||||||
 | 
					        'main/news.html.j2',
 | 
				
			||||||
 | 
					        title='News'
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('/privacy_policy')
 | 
					@bp.route('/privacy_policy')
 | 
				
			||||||
 | 
					@register_breadcrumb(bp, '.privacy_policy', 'Private statement (GDPR)')
 | 
				
			||||||
def privacy_policy():
 | 
					def privacy_policy():
 | 
				
			||||||
    return render_template('main/privacy_policy.html.j2', title='Privacy statement (GDPR)')
 | 
					    return render_template(
 | 
				
			||||||
 | 
					        'main/privacy_policy.html.j2',
 | 
				
			||||||
 | 
					        title='Privacy statement (GDPR)'
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('/terms_of_use')
 | 
					@bp.route('/terms_of_use')
 | 
				
			||||||
 | 
					@register_breadcrumb(bp, '.terms_of_use', 'Terms of Use')
 | 
				
			||||||
def terms_of_use():
 | 
					def terms_of_use():
 | 
				
			||||||
    return render_template('main/terms_of_use.html.j2', title='Terms of Use')
 | 
					    return render_template(
 | 
				
			||||||
 | 
					        'main/terms_of_use.html.j2',
 | 
				
			||||||
 | 
					        title='Terms of Use'
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# @bp.route('/social-area')
 | 
				
			||||||
 | 
					# @register_breadcrumb(bp, '.social_area', '<i class="material-icons left">group</i>Social Area')
 | 
				
			||||||
 | 
					# @login_required
 | 
				
			||||||
 | 
					# def social_area():
 | 
				
			||||||
 | 
					#     corpora = Corpus.query.filter(Corpus.is_public == True, Corpus.user != current_user).all()
 | 
				
			||||||
 | 
					#     users = User.query.filter(User.is_public == True, User.id != current_user.id).all()
 | 
				
			||||||
 | 
					#     return render_template(
 | 
				
			||||||
 | 
					#         'main/social_area.html.j2',
 | 
				
			||||||
 | 
					#         title='Social Area',
 | 
				
			||||||
 | 
					#         corpora=corpora,
 | 
				
			||||||
 | 
					#         users=users
 | 
				
			||||||
 | 
					#     )
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										482
									
								
								app/models.py
									
									
									
									
									
								
							
							
						
						
									
										482
									
								
								app/models.py
									
									
									
									
									
								
							@@ -1,16 +1,18 @@
 | 
				
			|||||||
from datetime import datetime, timedelta
 | 
					from datetime import datetime, timedelta
 | 
				
			||||||
from enum import Enum, IntEnum
 | 
					from enum import Enum, IntEnum
 | 
				
			||||||
from flask import current_app, url_for
 | 
					from flask import abort, current_app, url_for
 | 
				
			||||||
from flask_hashids import HashidMixin
 | 
					from flask_hashids import HashidMixin
 | 
				
			||||||
from flask_login import UserMixin
 | 
					from flask_login import UserMixin
 | 
				
			||||||
from sqlalchemy.ext.associationproxy import association_proxy
 | 
					from sqlalchemy.ext.associationproxy import association_proxy
 | 
				
			||||||
from time import sleep
 | 
					from time import sleep
 | 
				
			||||||
from tqdm import tqdm
 | 
					from tqdm import tqdm
 | 
				
			||||||
 | 
					from typing import Union
 | 
				
			||||||
from werkzeug.security import generate_password_hash, check_password_hash
 | 
					from werkzeug.security import generate_password_hash, check_password_hash
 | 
				
			||||||
from werkzeug.utils import secure_filename
 | 
					from werkzeug.utils import secure_filename
 | 
				
			||||||
import json
 | 
					import json
 | 
				
			||||||
import jwt
 | 
					import jwt
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
 | 
					import re
 | 
				
			||||||
import requests
 | 
					import requests
 | 
				
			||||||
import secrets
 | 
					import secrets
 | 
				
			||||||
import shutil
 | 
					import shutil
 | 
				
			||||||
@@ -36,6 +38,16 @@ class CorpusStatus(IntEnum):
 | 
				
			|||||||
    RUNNING_ANALYSIS_SESSION = 8
 | 
					    RUNNING_ANALYSIS_SESSION = 8
 | 
				
			||||||
    CANCELING_ANALYSIS_SESSION = 9
 | 
					    CANCELING_ANALYSIS_SESSION = 9
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def get(corpus_status: Union['CorpusStatus', int, str]) -> 'CorpusStatus':
 | 
				
			||||||
 | 
					        if isinstance(corpus_status, CorpusStatus):
 | 
				
			||||||
 | 
					            return corpus_status
 | 
				
			||||||
 | 
					        if isinstance(corpus_status, int):
 | 
				
			||||||
 | 
					            return CorpusStatus(corpus_status)
 | 
				
			||||||
 | 
					        if isinstance(corpus_status, str):
 | 
				
			||||||
 | 
					            return CorpusStatus[corpus_status]
 | 
				
			||||||
 | 
					        raise TypeError('corpus_status must be CorpusStatus, int, or str')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class JobStatus(IntEnum):
 | 
					class JobStatus(IntEnum):
 | 
				
			||||||
    INITIALIZING = 1
 | 
					    INITIALIZING = 1
 | 
				
			||||||
@@ -47,6 +59,16 @@ class JobStatus(IntEnum):
 | 
				
			|||||||
    COMPLETED = 7
 | 
					    COMPLETED = 7
 | 
				
			||||||
    FAILED = 8
 | 
					    FAILED = 8
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def get(job_status: Union['JobStatus', int, str]) -> 'JobStatus':
 | 
				
			||||||
 | 
					        if isinstance(job_status, JobStatus):
 | 
				
			||||||
 | 
					            return job_status
 | 
				
			||||||
 | 
					        if isinstance(job_status, int):
 | 
				
			||||||
 | 
					            return JobStatus(job_status)
 | 
				
			||||||
 | 
					        if isinstance(job_status, str):
 | 
				
			||||||
 | 
					            return JobStatus[job_status]
 | 
				
			||||||
 | 
					        raise TypeError('job_status must be JobStatus, int, or str')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Permission(IntEnum):
 | 
					class Permission(IntEnum):
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
@@ -57,6 +79,16 @@ class Permission(IntEnum):
 | 
				
			|||||||
    CONTRIBUTE = 2
 | 
					    CONTRIBUTE = 2
 | 
				
			||||||
    USE_API = 4
 | 
					    USE_API = 4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def get(permission: Union['Permission', int, str]) -> 'Permission':
 | 
				
			||||||
 | 
					        if isinstance(permission, Permission):
 | 
				
			||||||
 | 
					            return permission
 | 
				
			||||||
 | 
					        if isinstance(permission, int):
 | 
				
			||||||
 | 
					            return Permission(permission)
 | 
				
			||||||
 | 
					        if isinstance(permission, str):
 | 
				
			||||||
 | 
					            return Permission[permission]
 | 
				
			||||||
 | 
					        raise TypeError('permission must be Permission, int, or str')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserSettingJobStatusMailNotificationLevel(IntEnum):
 | 
					class UserSettingJobStatusMailNotificationLevel(IntEnum):
 | 
				
			||||||
    NONE = 1
 | 
					    NONE = 1
 | 
				
			||||||
@@ -69,10 +101,31 @@ class ProfilePrivacySettings(IntEnum):
 | 
				
			|||||||
    SHOW_LAST_SEEN = 2
 | 
					    SHOW_LAST_SEEN = 2
 | 
				
			||||||
    SHOW_MEMBER_SINCE = 4
 | 
					    SHOW_MEMBER_SINCE = 4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CorpusFollowPermission(IntEnum):
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def get(profile_privacy_setting: Union['ProfilePrivacySettings', int, str]) -> 'ProfilePrivacySettings':
 | 
				
			||||||
 | 
					        if isinstance(profile_privacy_setting, ProfilePrivacySettings):
 | 
				
			||||||
 | 
					            return profile_privacy_setting
 | 
				
			||||||
 | 
					        if isinstance(profile_privacy_setting, int):
 | 
				
			||||||
 | 
					            return ProfilePrivacySettings(profile_privacy_setting)
 | 
				
			||||||
 | 
					        if isinstance(profile_privacy_setting, str):
 | 
				
			||||||
 | 
					            return ProfilePrivacySettings[profile_privacy_setting]
 | 
				
			||||||
 | 
					        raise TypeError('profile_privacy_setting must be ProfilePrivacySettings, int, or str')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CorpusFollowerPermission(IntEnum):
 | 
				
			||||||
    VIEW = 1
 | 
					    VIEW = 1
 | 
				
			||||||
    CONTRIBUTE = 2
 | 
					    MANAGE_FILES = 2
 | 
				
			||||||
    ADMINISTRATE = 4
 | 
					    MANAGE_FOLLOWERS = 4
 | 
				
			||||||
 | 
					    MANAGE_CORPUS = 8
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def get(corpus_follower_permission: Union['CorpusFollowerPermission', int, str]) -> 'CorpusFollowerPermission':
 | 
				
			||||||
 | 
					        if isinstance(corpus_follower_permission, CorpusFollowerPermission):
 | 
				
			||||||
 | 
					            return corpus_follower_permission
 | 
				
			||||||
 | 
					        if isinstance(corpus_follower_permission, int):
 | 
				
			||||||
 | 
					            return CorpusFollowerPermission(corpus_follower_permission)
 | 
				
			||||||
 | 
					        if isinstance(corpus_follower_permission, str):
 | 
				
			||||||
 | 
					            return CorpusFollowerPermission[corpus_follower_permission]
 | 
				
			||||||
 | 
					        raise TypeError('corpus_follower_permission must be CorpusFollowerPermission, int, or str')
 | 
				
			||||||
# endregion enums
 | 
					# endregion enums
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -180,16 +233,19 @@ class Role(HashidMixin, db.Model):
 | 
				
			|||||||
    def __repr__(self):
 | 
					    def __repr__(self):
 | 
				
			||||||
        return f'<Role {self.name}>'
 | 
					        return f'<Role {self.name}>'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def add_permission(self, permission):
 | 
					    def has_permission(self, permission: Union[Permission, int, str]):
 | 
				
			||||||
        if not self.has_permission(permission):
 | 
					        p = Permission.get(permission)
 | 
				
			||||||
            self.permissions += permission
 | 
					        return self.permissions & p.value == p.value
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    def has_permission(self, permission):
 | 
					    def add_permission(self, permission: Union[Permission, int, str]):
 | 
				
			||||||
        return self.permissions & permission == permission
 | 
					        p = Permission.get(permission)
 | 
				
			||||||
 | 
					        if not self.has_permission(p):
 | 
				
			||||||
 | 
					            self.permissions += p.value
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    def remove_permission(self, permission):
 | 
					    def remove_permission(self, permission: Union[Permission, int, str]):
 | 
				
			||||||
        if self.has_permission(permission):
 | 
					        p = Permission.get(permission)
 | 
				
			||||||
            self.permissions -= permission
 | 
					        if self.has_permission(p):
 | 
				
			||||||
 | 
					            self.permissions -= p.value
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def reset_permissions(self):
 | 
					    def reset_permissions(self):
 | 
				
			||||||
        self.permissions = 0
 | 
					        self.permissions = 0
 | 
				
			||||||
@@ -199,8 +255,13 @@ class Role(HashidMixin, db.Model):
 | 
				
			|||||||
            'id': self.hashid,
 | 
					            'id': self.hashid,
 | 
				
			||||||
            'default': self.default,
 | 
					            'default': self.default,
 | 
				
			||||||
            'name': self.name,
 | 
					            'name': self.name,
 | 
				
			||||||
            'permissions': self.permissions
 | 
					            'permissions': [
 | 
				
			||||||
 | 
					                x.name for x in Permission
 | 
				
			||||||
 | 
					                if self.has_permission(x.value)
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        if backrefs:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
        if relationships:
 | 
					        if relationships:
 | 
				
			||||||
            json_serializeable['users'] = {
 | 
					            json_serializeable['users'] = {
 | 
				
			||||||
                x.hashid: x.to_json_serializeable(relationships=True)
 | 
					                x.hashid: x.to_json_serializeable(relationships=True)
 | 
				
			||||||
@@ -252,6 +313,27 @@ class Token(db.Model):
 | 
				
			|||||||
        self.access_expiration = datetime.utcnow()
 | 
					        self.access_expiration = datetime.utcnow()
 | 
				
			||||||
        self.refresh_expiration = datetime.utcnow()
 | 
					        self.refresh_expiration = datetime.utcnow()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def to_json_serializeable(self, backrefs=False, relationships=False):
 | 
				
			||||||
 | 
					        json_serializeable = {
 | 
				
			||||||
 | 
					            'id': self.hashid,
 | 
				
			||||||
 | 
					            'access_token': self.access_token,
 | 
				
			||||||
 | 
					            'access_expiration': (
 | 
				
			||||||
 | 
					                None if self.access_expiration is None
 | 
				
			||||||
 | 
					                else f'{self.access_expiration.isoformat()}Z'
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            'refresh_token': self.refresh_token,
 | 
				
			||||||
 | 
					            'refresh_expiration': (
 | 
				
			||||||
 | 
					                None if self.refresh_expiration is None
 | 
				
			||||||
 | 
					                else f'{self.refresh_expiration.isoformat()}Z'
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if backrefs:
 | 
				
			||||||
 | 
					            json_serializeable['user'] = \
 | 
				
			||||||
 | 
					                self.user.to_json_serializeable(backrefs=True)
 | 
				
			||||||
 | 
					        if relationships:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					        return json_serializeable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					    @staticmethod
 | 
				
			||||||
    def clean():
 | 
					    def clean():
 | 
				
			||||||
        """Remove any tokens that have been expired for more than a day."""
 | 
					        """Remove any tokens that have been expired for more than a day."""
 | 
				
			||||||
@@ -284,35 +366,143 @@ class Avatar(HashidMixin, FileMixin, db.Model):
 | 
				
			|||||||
            'id': self.hashid,
 | 
					            'id': self.hashid,
 | 
				
			||||||
            **self.file_mixin_to_json_serializeable()
 | 
					            **self.file_mixin_to_json_serializeable()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        if backrefs:
 | 
				
			||||||
 | 
					            json_serializeable['user'] = \
 | 
				
			||||||
 | 
					                self.user.to_json_serializeable(backrefs=True)
 | 
				
			||||||
 | 
					        if relationships:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
        return json_serializeable
 | 
					        return json_serializeable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CorpusFollowerAssociation(db.Model):
 | 
					class CorpusFollowerRole(HashidMixin, db.Model):
 | 
				
			||||||
 | 
					    __tablename__ = 'corpus_follower_roles'
 | 
				
			||||||
 | 
					    # Primary key
 | 
				
			||||||
 | 
					    id = db.Column(db.Integer, primary_key=True)
 | 
				
			||||||
 | 
					    # Fields
 | 
				
			||||||
 | 
					    name = db.Column(db.String(64), unique=True)
 | 
				
			||||||
 | 
					    default = db.Column(db.Boolean, default=False, index=True)
 | 
				
			||||||
 | 
					    permissions = db.Column(db.Integer, default=0)
 | 
				
			||||||
 | 
					    # Relationships
 | 
				
			||||||
 | 
					    corpus_follower_associations = db.relationship(
 | 
				
			||||||
 | 
					        'CorpusFollowerAssociation',
 | 
				
			||||||
 | 
					        back_populates='role'
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __repr__(self):
 | 
				
			||||||
 | 
					        return f'<CorpusFollowerRole {self.name}>'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def has_permission(self, permission: Union[CorpusFollowerPermission, int, str]):
 | 
				
			||||||
 | 
					        perm = CorpusFollowerPermission.get(permission)
 | 
				
			||||||
 | 
					        return self.permissions & perm.value == perm.value
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def add_permission(self, permission: Union[CorpusFollowerPermission, int, str]):
 | 
				
			||||||
 | 
					        perm = CorpusFollowerPermission.get(permission)
 | 
				
			||||||
 | 
					        if not self.has_permission(perm):
 | 
				
			||||||
 | 
					            self.permissions += perm.value
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def remove_permission(self, permission: Union[CorpusFollowerPermission, int, str]):
 | 
				
			||||||
 | 
					        perm = CorpusFollowerPermission.get(permission)
 | 
				
			||||||
 | 
					        if self.has_permission(perm):
 | 
				
			||||||
 | 
					            self.permissions -= perm.value
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def reset_permissions(self):
 | 
				
			||||||
 | 
					        self.permissions = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def to_json_serializeable(self, backrefs=False, relationships=False):
 | 
				
			||||||
 | 
					        json_serializeable = {
 | 
				
			||||||
 | 
					            'id': self.hashid,
 | 
				
			||||||
 | 
					            'default': self.default,
 | 
				
			||||||
 | 
					            'name': self.name,
 | 
				
			||||||
 | 
					            'permissions': [
 | 
				
			||||||
 | 
					                x.name
 | 
				
			||||||
 | 
					                for x in CorpusFollowerPermission
 | 
				
			||||||
 | 
					                if self.has_permission(x)
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if backrefs:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					        if relationships:
 | 
				
			||||||
 | 
					            json_serializeable['corpus_follower_association'] = {
 | 
				
			||||||
 | 
					                x.hashid: x.to_json_serializeable(relationships=True)
 | 
				
			||||||
 | 
					                for x in self.corpus_follower_association
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        return json_serializeable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def insert_defaults():
 | 
				
			||||||
 | 
					        roles = {
 | 
				
			||||||
 | 
					            'Anonymous': [],
 | 
				
			||||||
 | 
					            'Viewer': [
 | 
				
			||||||
 | 
					                CorpusFollowerPermission.VIEW
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            'Contributor': [
 | 
				
			||||||
 | 
					                CorpusFollowerPermission.VIEW,
 | 
				
			||||||
 | 
					                CorpusFollowerPermission.MANAGE_FILES
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            'Administrator': [
 | 
				
			||||||
 | 
					                CorpusFollowerPermission.VIEW,
 | 
				
			||||||
 | 
					                CorpusFollowerPermission.MANAGE_FILES,
 | 
				
			||||||
 | 
					                CorpusFollowerPermission.MANAGE_FOLLOWERS,
 | 
				
			||||||
 | 
					                CorpusFollowerPermission.MANAGE_CORPUS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        default_role_name = 'Viewer'
 | 
				
			||||||
 | 
					        for role_name, permissions in roles.items():
 | 
				
			||||||
 | 
					            role = CorpusFollowerRole.query.filter_by(name=role_name).first()
 | 
				
			||||||
 | 
					            if role is None:
 | 
				
			||||||
 | 
					                role = CorpusFollowerRole(name=role_name)
 | 
				
			||||||
 | 
					            role.reset_permissions()
 | 
				
			||||||
 | 
					            for permission in permissions:
 | 
				
			||||||
 | 
					                role.add_permission(permission)
 | 
				
			||||||
 | 
					            role.default = role.name == default_role_name
 | 
				
			||||||
 | 
					            db.session.add(role)
 | 
				
			||||||
 | 
					        db.session.commit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CorpusFollowerAssociation(HashidMixin, db.Model):
 | 
				
			||||||
    __tablename__ = 'corpus_follower_associations'
 | 
					    __tablename__ = 'corpus_follower_associations'
 | 
				
			||||||
    # Primary key
 | 
					    # Primary key
 | 
				
			||||||
    id = db.Column(db.Integer, primary_key=True)
 | 
					    id = db.Column(db.Integer, primary_key=True)
 | 
				
			||||||
    # Foreign keys
 | 
					    # Foreign keys
 | 
				
			||||||
    following_user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
 | 
					    corpus_id = db.Column(db.Integer, db.ForeignKey('corpora.id'))
 | 
				
			||||||
    followed_corpus_id = db.Column(db.Integer, db.ForeignKey('corpora.id'))
 | 
					    follower_id = db.Column(db.Integer, db.ForeignKey('users.id'))
 | 
				
			||||||
    # Fields
 | 
					    role_id = db.Column(db.Integer, db.ForeignKey('corpus_follower_roles.id'))
 | 
				
			||||||
    permissions = db.Column(db.Integer, default=0, nullable=False)
 | 
					 | 
				
			||||||
    # Relationships
 | 
					    # Relationships
 | 
				
			||||||
    followed_corpus = db.relationship('Corpus', back_populates='following_user_associations')
 | 
					    corpus = db.relationship(
 | 
				
			||||||
    following_user = db.relationship('User', back_populates='followed_corpus_associations')
 | 
					        'Corpus',
 | 
				
			||||||
 | 
					        back_populates='corpus_follower_associations'
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    follower = db.relationship(
 | 
				
			||||||
 | 
					        'User',
 | 
				
			||||||
 | 
					        back_populates='corpus_follower_associations'
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    role = db.relationship(
 | 
				
			||||||
 | 
					        'CorpusFollowerRole',
 | 
				
			||||||
 | 
					        back_populates='corpus_follower_associations'
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, **kwargs):
 | 
				
			||||||
 | 
					        if 'role' not in kwargs:
 | 
				
			||||||
 | 
					            kwargs['role'] = CorpusFollowerRole.query.filter_by(default=True).first()
 | 
				
			||||||
 | 
					        super().__init__(**kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __repr__(self):
 | 
					    def __repr__(self):
 | 
				
			||||||
        return f'<CorpusFollowerAssociation {self.following_user.__repr__()} ~ {self.followed_corpus.__repr__()}>'
 | 
					        return f'<CorpusFollowerAssociation {self.follower.__repr__()} ~ {self.role.__repr__()} ~ {self.corpus.__repr__()}>'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def has_permission(self, permission):
 | 
					    def to_json_serializeable(self, backrefs=False, relationships=False):
 | 
				
			||||||
        return self.permissions & permission == permission
 | 
					        json_serializeable = {
 | 
				
			||||||
 | 
					            'id': self.hashid,
 | 
				
			||||||
 | 
					            'corpus': self.corpus.to_json_serializeable(backrefs=True),
 | 
				
			||||||
 | 
					            'follower': self.follower.to_json_serializeable(),
 | 
				
			||||||
 | 
					            'role': self.role.to_json_serializeable()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if backrefs:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					        if relationships:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					        return json_serializeable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def add_permission(self, permission):
 | 
					 | 
				
			||||||
        if not self.has_permission(permission):
 | 
					 | 
				
			||||||
            self.permissions += permission
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    def remove_permission(self, permission):
 | 
					 | 
				
			||||||
        if self.has_permission(permission):
 | 
					 | 
				
			||||||
            self.permissions -= permission
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class User(HashidMixin, UserMixin, db.Model):
 | 
					class User(HashidMixin, UserMixin, db.Model):
 | 
				
			||||||
    __tablename__ = 'users'
 | 
					    __tablename__ = 'users'
 | 
				
			||||||
@@ -323,8 +513,10 @@ class User(HashidMixin, UserMixin, db.Model):
 | 
				
			|||||||
    # Fields
 | 
					    # Fields
 | 
				
			||||||
    email = db.Column(db.String(254), index=True, unique=True)
 | 
					    email = db.Column(db.String(254), index=True, unique=True)
 | 
				
			||||||
    username = db.Column(db.String(64), index=True, unique=True)
 | 
					    username = db.Column(db.String(64), index=True, unique=True)
 | 
				
			||||||
 | 
					    username_pattern = re.compile(r'^[A-Za-zÄÖÜäöüß0-9_.]*$')
 | 
				
			||||||
    password_hash = db.Column(db.String(128))
 | 
					    password_hash = db.Column(db.String(128))
 | 
				
			||||||
    confirmed = db.Column(db.Boolean, default=False)
 | 
					    confirmed = db.Column(db.Boolean, default=False)
 | 
				
			||||||
 | 
					    terms_of_use_accepted = db.Column(db.Boolean, default=False)
 | 
				
			||||||
    member_since = db.Column(db.DateTime(), default=datetime.utcnow)
 | 
					    member_since = db.Column(db.DateTime(), default=datetime.utcnow)
 | 
				
			||||||
    setting_job_status_mail_notification_level = db.Column(
 | 
					    setting_job_status_mail_notification_level = db.Column(
 | 
				
			||||||
        IntEnumColumn(UserSettingJobStatusMailNotificationLevel),
 | 
					        IntEnumColumn(UserSettingJobStatusMailNotificationLevel),
 | 
				
			||||||
@@ -351,14 +543,15 @@ class User(HashidMixin, UserMixin, db.Model):
 | 
				
			|||||||
        cascade='all, delete-orphan',
 | 
					        cascade='all, delete-orphan',
 | 
				
			||||||
        lazy='dynamic'
 | 
					        lazy='dynamic'
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    followed_corpus_associations = db.relationship(
 | 
					    corpus_follower_associations = db.relationship(
 | 
				
			||||||
        'CorpusFollowerAssociation',
 | 
					        'CorpusFollowerAssociation',
 | 
				
			||||||
        back_populates='following_user'
 | 
					        back_populates='follower',
 | 
				
			||||||
 | 
					        cascade='all, delete-orphan'
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    followed_corpora = association_proxy(
 | 
					    followed_corpora = association_proxy(
 | 
				
			||||||
        'followed_corpus_associations',
 | 
					        'corpus_follower_associations',
 | 
				
			||||||
        'followed_corpus',
 | 
					        'corpus',
 | 
				
			||||||
        creator=lambda c: CorpusFollowerAssociation(followed_corpus=c)
 | 
					        creator=lambda c: CorpusFollowerAssociation(corpus=c)
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    jobs = db.relationship(
 | 
					    jobs = db.relationship(
 | 
				
			||||||
        'Job',
 | 
					        'Job',
 | 
				
			||||||
@@ -390,13 +583,13 @@ class User(HashidMixin, UserMixin, db.Model):
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, **kwargs):
 | 
					    def __init__(self, **kwargs):
 | 
				
			||||||
 | 
					        if 'role' not in kwargs:
 | 
				
			||||||
 | 
					            kwargs['role'] = (
 | 
				
			||||||
 | 
					                Role.query.filter_by(name='Administrator').first()
 | 
				
			||||||
 | 
					                if kwargs['email'] == current_app.config['NOPAQUE_ADMIN']
 | 
				
			||||||
 | 
					                else Role.query.filter_by(default=True).first()
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
        super().__init__(**kwargs)
 | 
					        super().__init__(**kwargs)
 | 
				
			||||||
        if self.role is not None:
 | 
					 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
        if self.email == current_app.config['NOPAQUE_ADMIN']:
 | 
					 | 
				
			||||||
            self.role = Role.query.filter_by(name='Administrator').first()
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            self.role = Role.query.filter_by(default=True).first()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __repr__(self):
 | 
					    def __repr__(self):
 | 
				
			||||||
        return f'<User {self.username}>'
 | 
					        return f'<User {self.username}>'
 | 
				
			||||||
@@ -495,7 +688,7 @@ class User(HashidMixin, UserMixin, db.Model):
 | 
				
			|||||||
            db.session.commit()
 | 
					            db.session.commit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def can(self, permission):
 | 
					    def can(self, permission):
 | 
				
			||||||
        return self.role.has_permission(permission)
 | 
					        return self.role is not None and self.role.has_permission(permission)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def confirm(self, confirmation_token):
 | 
					    def confirm(self, confirmation_token):
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
@@ -506,7 +699,6 @@ class User(HashidMixin, UserMixin, db.Model):
 | 
				
			|||||||
                issuer=current_app.config['SERVER_NAME'],
 | 
					                issuer=current_app.config['SERVER_NAME'],
 | 
				
			||||||
                options={'require': ['exp', 'iat', 'iss', 'purpose', 'sub']}
 | 
					                options={'require': ['exp', 'iat', 'iss', 'purpose', 'sub']}
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            current_app.logger.warning(payload)
 | 
					 | 
				
			||||||
        except jwt.PyJWTError:
 | 
					        except jwt.PyJWTError:
 | 
				
			||||||
            return False
 | 
					            return False
 | 
				
			||||||
        if payload.get('purpose') != 'user.confirm':
 | 
					        if payload.get('purpose') != 'user.confirm':
 | 
				
			||||||
@@ -577,42 +769,97 @@ class User(HashidMixin, UserMixin, db.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    #region Profile Privacy settings
 | 
					    #region Profile Privacy settings
 | 
				
			||||||
    def has_profile_privacy_setting(self, setting):
 | 
					    def has_profile_privacy_setting(self, setting):
 | 
				
			||||||
        return self.profile_privacy_settings & setting == setting
 | 
					        s = ProfilePrivacySettings.get(setting)
 | 
				
			||||||
 | 
					        return self.profile_privacy_settings & s.value == s.value
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    def add_profile_privacy_setting(self, setting):
 | 
					    def add_profile_privacy_setting(self, setting):
 | 
				
			||||||
        if not self.has_profile_privacy_setting(setting):
 | 
					        s = ProfilePrivacySettings.get(setting)
 | 
				
			||||||
            self.profile_privacy_settings += setting
 | 
					        if not self.has_profile_privacy_setting(s):
 | 
				
			||||||
 | 
					            self.profile_privacy_settings += s.value
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def remove_profile_privacy_setting(self, setting):
 | 
					    def remove_profile_privacy_setting(self, setting):
 | 
				
			||||||
        if self.has_profile_privacy_setting(setting):
 | 
					        s = ProfilePrivacySettings.get(setting)
 | 
				
			||||||
            self.profile_privacy_settings -= setting
 | 
					        if self.has_profile_privacy_setting(s):
 | 
				
			||||||
 | 
					            self.profile_privacy_settings -= s.value
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def reset_profile_privacy_settings(self):
 | 
					    def reset_profile_privacy_settings(self):
 | 
				
			||||||
        self.profile_privacy_settings = 0
 | 
					        self.profile_privacy_settings = 0
 | 
				
			||||||
    #endregion Profile Privacy settings
 | 
					    #endregion Profile Privacy settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def follow_corpus(self, corpus):
 | 
					    def follow_corpus(self, corpus, role=None):
 | 
				
			||||||
        if not self.is_following_corpus(corpus):
 | 
					        if role is None:
 | 
				
			||||||
            self.followed_corpora.append(corpus)
 | 
					            cfr = CorpusFollowerRole.query.filter_by(default=True).first()
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            cfr = role
 | 
				
			||||||
 | 
					        if self.is_following_corpus(corpus):
 | 
				
			||||||
 | 
					            cfa = CorpusFollowerAssociation.query.filter_by(corpus=corpus, follower=self).first()
 | 
				
			||||||
 | 
					            if cfa.role != cfr:
 | 
				
			||||||
 | 
					                cfa.role = cfr
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            cfa = CorpusFollowerAssociation(corpus=corpus, role=cfr, follower=self)
 | 
				
			||||||
 | 
					            db.session.add(cfa)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def unfollow_corpus(self, corpus):
 | 
					    def unfollow_corpus(self, corpus):
 | 
				
			||||||
        if self.is_following_corpus(corpus):
 | 
					        if not self.is_following_corpus(corpus):
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
        self.followed_corpora.remove(corpus)
 | 
					        self.followed_corpora.remove(corpus)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_following_corpus(self, corpus):
 | 
					    def is_following_corpus(self, corpus):
 | 
				
			||||||
        return corpus in self.followed_corpora
 | 
					        return corpus in self.followed_corpora
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    def generate_follow_corpus_token(self, corpus_hashid, role_name, expiration=7):
 | 
				
			||||||
 | 
					        now = datetime.utcnow()
 | 
				
			||||||
 | 
					        payload = {
 | 
				
			||||||
 | 
					            'exp': expiration,
 | 
				
			||||||
 | 
					            'iat': now,
 | 
				
			||||||
 | 
					            'iss': current_app.config['SERVER_NAME'],
 | 
				
			||||||
 | 
					            'purpose': 'User.follow_corpus',
 | 
				
			||||||
 | 
					            'role_name': role_name,
 | 
				
			||||||
 | 
					            'sub': corpus_hashid
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return jwt.encode(
 | 
				
			||||||
 | 
					            payload,
 | 
				
			||||||
 | 
					            current_app.config['SECRET_KEY'],
 | 
				
			||||||
 | 
					            algorithm='HS256'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def follow_corpus_by_token(self, token):
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            payload = jwt.decode(
 | 
				
			||||||
 | 
					                token,
 | 
				
			||||||
 | 
					                current_app.config['SECRET_KEY'],
 | 
				
			||||||
 | 
					                algorithms=['HS256'],
 | 
				
			||||||
 | 
					                issuer=current_app.config['SERVER_NAME'],
 | 
				
			||||||
 | 
					                options={'require': ['exp', 'iat', 'iss', 'purpose', 'role_name', 'sub']}
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        except jwt.PyJWTError:
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					        if payload.get('purpose') != 'User.follow_corpus':
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					        corpus_hashid = payload.get('sub')
 | 
				
			||||||
 | 
					        corpus_id = hashids.decode(corpus_hashid)
 | 
				
			||||||
 | 
					        corpus = Corpus.query.get_or_404(corpus_id)
 | 
				
			||||||
 | 
					        if corpus is None:
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					        role_name = payload.get('role_name')
 | 
				
			||||||
 | 
					        role = CorpusFollowerRole.query.filter_by(name=role_name).first()
 | 
				
			||||||
 | 
					        if role is None:
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					        self.follow_corpus(corpus, role)
 | 
				
			||||||
 | 
					        # db.session.add(self)
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def to_json_serializeable(self, backrefs=False, relationships=False, filter_by_privacy_settings=False):
 | 
					    def to_json_serializeable(self, backrefs=False, relationships=False, filter_by_privacy_settings=False):
 | 
				
			||||||
        json_serializeable = {
 | 
					        json_serializeable = {
 | 
				
			||||||
            'id': self.hashid,
 | 
					            'id': self.hashid,
 | 
				
			||||||
            'confirmed': self.confirmed,
 | 
					            'confirmed': self.confirmed,
 | 
				
			||||||
 | 
					            # 'avatar': url_for('users.user_avatar', user_id=self.id),
 | 
				
			||||||
            'email': self.email,
 | 
					            'email': self.email,
 | 
				
			||||||
            'last_seen': (
 | 
					            'last_seen': (
 | 
				
			||||||
                None if self.last_seen is None
 | 
					                None if self.last_seen is None
 | 
				
			||||||
                else self.last_seen.strftime('%Y-%m-%d %H:%M')
 | 
					                else f'{self.last_seen.isoformat()}Z'
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            'member_since': self.member_since.strftime('%Y-%m-%d'),
 | 
					            'member_since': f'{self.member_since.isoformat()}Z',
 | 
				
			||||||
            'username': self.username,
 | 
					            'username': self.username,
 | 
				
			||||||
            'full_name': self.full_name,
 | 
					            'full_name': self.full_name,
 | 
				
			||||||
            'about_me': self.about_me,
 | 
					            'about_me': self.about_me,
 | 
				
			||||||
@@ -621,19 +868,21 @@ class User(HashidMixin, UserMixin, db.Model):
 | 
				
			|||||||
            'organization': self.organization,
 | 
					            'organization': self.organization,
 | 
				
			||||||
            'job_status_mail_notification_level': \
 | 
					            'job_status_mail_notification_level': \
 | 
				
			||||||
                    self.setting_job_status_mail_notification_level.name,
 | 
					                    self.setting_job_status_mail_notification_level.name,
 | 
				
			||||||
 | 
					            'profile_privacy_settings': {
 | 
				
			||||||
                'is_public': self.is_public,
 | 
					                'is_public': self.is_public,
 | 
				
			||||||
                'show_email': self.has_profile_privacy_setting(ProfilePrivacySettings.SHOW_EMAIL),
 | 
					                'show_email': self.has_profile_privacy_setting(ProfilePrivacySettings.SHOW_EMAIL),
 | 
				
			||||||
                'show_last_seen': self.has_profile_privacy_setting(ProfilePrivacySettings.SHOW_LAST_SEEN),
 | 
					                'show_last_seen': self.has_profile_privacy_setting(ProfilePrivacySettings.SHOW_LAST_SEEN),
 | 
				
			||||||
                'show_member_since': self.has_profile_privacy_setting(ProfilePrivacySettings.SHOW_MEMBER_SINCE)
 | 
					                'show_member_since': self.has_profile_privacy_setting(ProfilePrivacySettings.SHOW_MEMBER_SINCE)
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        json_serializeable['avatar'] = (
 | 
					        }
 | 
				
			||||||
            None if self.avatar is None
 | 
					 | 
				
			||||||
            else self.avatar.to_json_serializeable(relationships=True)
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        if backrefs:
 | 
					        if backrefs:
 | 
				
			||||||
            json_serializeable['role'] = \
 | 
					            json_serializeable['role'] = \
 | 
				
			||||||
                self.role.to_json_serializeable(backrefs=True)
 | 
					                self.role.to_json_serializeable(backrefs=True)
 | 
				
			||||||
        if relationships:
 | 
					        if relationships:
 | 
				
			||||||
 | 
					            json_serializeable['corpus_follower_associations'] = {
 | 
				
			||||||
 | 
					                x.hashid: x.to_json_serializeable()
 | 
				
			||||||
 | 
					                for x in self.corpus_follower_associations
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            json_serializeable['corpora'] = {
 | 
					            json_serializeable['corpora'] = {
 | 
				
			||||||
                x.hashid: x.to_json_serializeable(relationships=True)
 | 
					                x.hashid: x.to_json_serializeable(relationships=True)
 | 
				
			||||||
                for x in self.corpora
 | 
					                for x in self.corpora
 | 
				
			||||||
@@ -650,10 +899,6 @@ class User(HashidMixin, UserMixin, db.Model):
 | 
				
			|||||||
                x.hashid: x.to_json_serializeable(relationships=True)
 | 
					                x.hashid: x.to_json_serializeable(relationships=True)
 | 
				
			||||||
                for x in self.spacy_nlp_pipeline_models
 | 
					                for x in self.spacy_nlp_pipeline_models
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            json_serializeable['followed_corpora'] = {
 | 
					 | 
				
			||||||
                x.hashid: x.to_json_serializeable(relationships=True)
 | 
					 | 
				
			||||||
                for x in self.followed_corpora
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if filter_by_privacy_settings:
 | 
					        if filter_by_privacy_settings:
 | 
				
			||||||
            if not self.has_profile_privacy_setting(ProfilePrivacySettings.SHOW_EMAIL):
 | 
					            if not self.has_profile_privacy_setting(ProfilePrivacySettings.SHOW_EMAIL):
 | 
				
			||||||
@@ -786,6 +1031,8 @@ class TesseractOCRPipelineModel(FileMixin, HashidMixin, db.Model):
 | 
				
			|||||||
        if backrefs:
 | 
					        if backrefs:
 | 
				
			||||||
            json_serializeable['user'] = \
 | 
					            json_serializeable['user'] = \
 | 
				
			||||||
                self.user.to_json_serializeable(backrefs=True)
 | 
					                self.user.to_json_serializeable(backrefs=True)
 | 
				
			||||||
 | 
					        if relationships:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
        return json_serializeable
 | 
					        return json_serializeable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -912,7 +1159,10 @@ class SpaCyNLPPipelineModel(FileMixin, HashidMixin, db.Model):
 | 
				
			|||||||
            **self.file_mixin_to_json_serializeable()
 | 
					            **self.file_mixin_to_json_serializeable()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if backrefs:
 | 
					        if backrefs:
 | 
				
			||||||
            json_serializeable['user'] = self.user.to_json_serializeable(backrefs=True)
 | 
					            json_serializeable['user'] = \
 | 
				
			||||||
 | 
					                self.user.to_json_serializeable(backrefs=True)
 | 
				
			||||||
 | 
					        if relationships:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
        return json_serializeable
 | 
					        return json_serializeable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -971,6 +1221,8 @@ class JobInput(FileMixin, HashidMixin, db.Model):
 | 
				
			|||||||
        if backrefs:
 | 
					        if backrefs:
 | 
				
			||||||
            json_serializeable['job'] = \
 | 
					            json_serializeable['job'] = \
 | 
				
			||||||
                self.job.to_json_serializeable(backrefs=True)
 | 
					                self.job.to_json_serializeable(backrefs=True)
 | 
				
			||||||
 | 
					        if relationships:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
        return json_serializeable
 | 
					        return json_serializeable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1035,6 +1287,8 @@ class JobResult(FileMixin, HashidMixin, db.Model):
 | 
				
			|||||||
        if backrefs:
 | 
					        if backrefs:
 | 
				
			||||||
            json_serializeable['job'] = \
 | 
					            json_serializeable['job'] = \
 | 
				
			||||||
                self.job.to_json_serializeable(backrefs=True)
 | 
					                self.job.to_json_serializeable(backrefs=True)
 | 
				
			||||||
 | 
					        if relationships:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
        return json_serializeable
 | 
					        return json_serializeable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1114,7 +1368,6 @@ class Job(HashidMixin, db.Model):
 | 
				
			|||||||
            raise e
 | 
					            raise e
 | 
				
			||||||
        return job
 | 
					        return job
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
    def delete(self):
 | 
					    def delete(self):
 | 
				
			||||||
        ''' Delete the job and its inputs and results from the database. '''
 | 
					        ''' Delete the job and its inputs and results from the database. '''
 | 
				
			||||||
        if self.status not in [JobStatus.COMPLETED, JobStatus.FAILED]:  # noqa
 | 
					        if self.status not in [JobStatus.COMPLETED, JobStatus.FAILED]:  # noqa
 | 
				
			||||||
@@ -1159,8 +1412,7 @@ class Job(HashidMixin, db.Model):
 | 
				
			|||||||
            'service_args': self.service_args,
 | 
					            'service_args': self.service_args,
 | 
				
			||||||
            'service_version': self.service_version,
 | 
					            'service_version': self.service_version,
 | 
				
			||||||
            'status': self.status.name,
 | 
					            'status': self.status.name,
 | 
				
			||||||
            'title': self.title,
 | 
					            'title': self.title
 | 
				
			||||||
            'url': self.url
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if backrefs:
 | 
					        if backrefs:
 | 
				
			||||||
            json_serializeable['user'] = \
 | 
					            json_serializeable['user'] = \
 | 
				
			||||||
@@ -1246,9 +1498,9 @@ class CorpusFile(FileMixin, HashidMixin, db.Model):
 | 
				
			|||||||
    def to_json_serializeable(self, backrefs=False, relationships=False):
 | 
					    def to_json_serializeable(self, backrefs=False, relationships=False):
 | 
				
			||||||
        json_serializeable = {
 | 
					        json_serializeable = {
 | 
				
			||||||
            'id': self.hashid,
 | 
					            'id': self.hashid,
 | 
				
			||||||
            'url': self.url,
 | 
					 | 
				
			||||||
            'address': self.address,
 | 
					            'address': self.address,
 | 
				
			||||||
            'author': self.author,
 | 
					            'author': self.author,
 | 
				
			||||||
 | 
					            'description': self.description,
 | 
				
			||||||
            'booktitle': self.booktitle,
 | 
					            'booktitle': self.booktitle,
 | 
				
			||||||
            'chapter': self.chapter,
 | 
					            'chapter': self.chapter,
 | 
				
			||||||
            'editor': self.editor,
 | 
					            'editor': self.editor,
 | 
				
			||||||
@@ -1267,6 +1519,8 @@ class CorpusFile(FileMixin, HashidMixin, db.Model):
 | 
				
			|||||||
        if backrefs:
 | 
					        if backrefs:
 | 
				
			||||||
            json_serializeable['corpus'] = \
 | 
					            json_serializeable['corpus'] = \
 | 
				
			||||||
                self.corpus.to_json_serializeable(backrefs=True)
 | 
					                self.corpus.to_json_serializeable(backrefs=True)
 | 
				
			||||||
 | 
					        if relationships:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
        return json_serializeable
 | 
					        return json_serializeable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1297,14 +1551,15 @@ class Corpus(HashidMixin, db.Model):
 | 
				
			|||||||
        lazy='dynamic',
 | 
					        lazy='dynamic',
 | 
				
			||||||
        cascade='all, delete-orphan'
 | 
					        cascade='all, delete-orphan'
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    following_user_associations = db.relationship(
 | 
					    corpus_follower_associations = db.relationship(
 | 
				
			||||||
        'CorpusFollowerAssociation',
 | 
					        'CorpusFollowerAssociation',
 | 
				
			||||||
        back_populates='followed_corpus'
 | 
					        back_populates='corpus',
 | 
				
			||||||
 | 
					        cascade='all, delete-orphan'
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    following_users = association_proxy(
 | 
					    followers = association_proxy(
 | 
				
			||||||
        'following_user_associations',
 | 
					        'corpus_follower_associations',
 | 
				
			||||||
        'following_user',
 | 
					        'follower',
 | 
				
			||||||
        creator=lambda u: CorpusFollowerAssociation(following_user=u)
 | 
					        creator=lambda u: CorpusFollowerAssociation(follower=u)
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    user = db.relationship('User', back_populates='corpora')
 | 
					    user = db.relationship('User', back_populates='corpora')
 | 
				
			||||||
    # "static" attributes
 | 
					    # "static" attributes
 | 
				
			||||||
@@ -1315,7 +1570,7 @@ class Corpus(HashidMixin, db.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def analysis_url(self):
 | 
					    def analysis_url(self):
 | 
				
			||||||
        return url_for('corpora.analyse_corpus', corpus_id=self.id)
 | 
					        return url_for('corpora.analysis', corpus_id=self.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def jsonpatch_path(self):
 | 
					    def jsonpatch_path(self):
 | 
				
			||||||
@@ -1403,8 +1658,13 @@ class Corpus(HashidMixin, db.Model):
 | 
				
			|||||||
            'is_public': self.is_public
 | 
					            'is_public': self.is_public
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if backrefs:
 | 
					        if backrefs:
 | 
				
			||||||
            json_serializeable['user'] = self.user.to_json_serializeable(backrefs=True)
 | 
					            json_serializeable['user'] = \
 | 
				
			||||||
 | 
					                self.user.to_json_serializeable(backrefs=True)
 | 
				
			||||||
        if relationships:
 | 
					        if relationships:
 | 
				
			||||||
 | 
					            json_serializeable['corpus_follower_associations'] = {
 | 
				
			||||||
 | 
					                x.hashid: x.to_json_serializeable()
 | 
				
			||||||
 | 
					                for x in self.corpus_follower_associations
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            json_serializeable['files'] = {
 | 
					            json_serializeable['files'] = {
 | 
				
			||||||
                x.hashid: x.to_json_serializeable(relationships=True)
 | 
					                x.hashid: x.to_json_serializeable(relationships=True)
 | 
				
			||||||
                for x in self.files
 | 
					                for x in self.files
 | 
				
			||||||
@@ -1424,11 +1684,27 @@ class Corpus(HashidMixin, db.Model):
 | 
				
			|||||||
@db.event.listens_for(JobResult, 'after_delete')
 | 
					@db.event.listens_for(JobResult, 'after_delete')
 | 
				
			||||||
@db.event.listens_for(SpaCyNLPPipelineModel, 'after_delete')
 | 
					@db.event.listens_for(SpaCyNLPPipelineModel, 'after_delete')
 | 
				
			||||||
@db.event.listens_for(TesseractOCRPipelineModel, 'after_delete')
 | 
					@db.event.listens_for(TesseractOCRPipelineModel, 'after_delete')
 | 
				
			||||||
def ressource_after_delete(mapper, connection, ressource):
 | 
					def resource_after_delete(mapper, connection, resource):
 | 
				
			||||||
    jsonpatch = [{'op': 'remove', 'path': ressource.jsonpatch_path}]
 | 
					    jsonpatch = [
 | 
				
			||||||
    room = f'users.{ressource.user_hashid}'
 | 
					        {
 | 
				
			||||||
    socketio.emit('users.patch', jsonpatch, room=room)
 | 
					            'op': 'remove',
 | 
				
			||||||
    room = f'/users/{ressource.user_hashid}'
 | 
					            'path': resource.jsonpatch_path
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					    room = f'/users/{resource.user_hashid}'
 | 
				
			||||||
 | 
					    socketio.emit('PATCH', jsonpatch, room=room)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@db.event.listens_for(CorpusFollowerAssociation, 'after_delete')
 | 
				
			||||||
 | 
					def cfa_after_delete_handler(mapper, connection, cfa):
 | 
				
			||||||
 | 
					    jsonpatch_path = f'/users/{cfa.corpus.user.hashid}/corpora/{cfa.corpus.hashid}/corpus_follower_associations/{cfa.hashid}'
 | 
				
			||||||
 | 
					    jsonpatch = [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            'op': 'remove',
 | 
				
			||||||
 | 
					            'path': jsonpatch_path
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					    room = f'/users/{cfa.corpus.user.hashid}'
 | 
				
			||||||
    socketio.emit('PATCH', jsonpatch, room=room)
 | 
					    socketio.emit('PATCH', jsonpatch, room=room)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1439,14 +1715,33 @@ def ressource_after_delete(mapper, connection, ressource):
 | 
				
			|||||||
@db.event.listens_for(JobResult, 'after_insert')
 | 
					@db.event.listens_for(JobResult, 'after_insert')
 | 
				
			||||||
@db.event.listens_for(SpaCyNLPPipelineModel, 'after_insert')
 | 
					@db.event.listens_for(SpaCyNLPPipelineModel, 'after_insert')
 | 
				
			||||||
@db.event.listens_for(TesseractOCRPipelineModel, 'after_insert')
 | 
					@db.event.listens_for(TesseractOCRPipelineModel, 'after_insert')
 | 
				
			||||||
def ressource_after_insert_handler(mapper, connection, ressource):
 | 
					def resource_after_insert_handler(mapper, connection, resource):
 | 
				
			||||||
    value = ressource.to_json_serializeable()
 | 
					    jsonpatch_value = resource.to_json_serializeable()
 | 
				
			||||||
    for attr in mapper.relationships:
 | 
					    for attr in mapper.relationships:
 | 
				
			||||||
        value[attr.key] = {}
 | 
					        jsonpatch_value[attr.key] = {}
 | 
				
			||||||
    jsonpatch = [
 | 
					    jsonpatch = [
 | 
				
			||||||
        {'op': 'add', 'path': ressource.jsonpatch_path, 'value': value}
 | 
					        {
 | 
				
			||||||
 | 
					            'op': 'add',
 | 
				
			||||||
 | 
					            'path': resource.jsonpatch_path,
 | 
				
			||||||
 | 
					            'value': jsonpatch_value
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
    room = f'/users/{ressource.user_hashid}'
 | 
					    room = f'/users/{resource.user_hashid}'
 | 
				
			||||||
 | 
					    socketio.emit('PATCH', jsonpatch, room=room)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@db.event.listens_for(CorpusFollowerAssociation, 'after_insert')
 | 
				
			||||||
 | 
					def cfa_after_insert_handler(mapper, connection, cfa):
 | 
				
			||||||
 | 
					    jsonpatch_value = cfa.to_json_serializeable()
 | 
				
			||||||
 | 
					    jsonpatch_path = f'/users/{cfa.corpus.user.hashid}/corpora/{cfa.corpus.hashid}/corpus_follower_associations/{cfa.hashid}'
 | 
				
			||||||
 | 
					    jsonpatch = [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            'op': 'add',
 | 
				
			||||||
 | 
					            'path': jsonpatch_path,
 | 
				
			||||||
 | 
					            'value': jsonpatch_value
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					    room = f'/users/{cfa.corpus.user.hashid}'
 | 
				
			||||||
    socketio.emit('PATCH', jsonpatch, room=room)
 | 
					    socketio.emit('PATCH', jsonpatch, room=room)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1457,28 +1752,29 @@ def ressource_after_insert_handler(mapper, connection, ressource):
 | 
				
			|||||||
@db.event.listens_for(JobResult, 'after_update')
 | 
					@db.event.listens_for(JobResult, 'after_update')
 | 
				
			||||||
@db.event.listens_for(SpaCyNLPPipelineModel, 'after_update')
 | 
					@db.event.listens_for(SpaCyNLPPipelineModel, 'after_update')
 | 
				
			||||||
@db.event.listens_for(TesseractOCRPipelineModel, 'after_update')
 | 
					@db.event.listens_for(TesseractOCRPipelineModel, 'after_update')
 | 
				
			||||||
def ressource_after_update_handler(mapper, connection, ressource):
 | 
					def resource_after_update_handler(mapper, connection, resource):
 | 
				
			||||||
    jsonpatch = []
 | 
					    jsonpatch = []
 | 
				
			||||||
    for attr in db.inspect(ressource).attrs:
 | 
					    for attr in db.inspect(resource).attrs:
 | 
				
			||||||
        if attr.key in mapper.relationships:
 | 
					        if attr.key in mapper.relationships:
 | 
				
			||||||
            continue
 | 
					            continue
 | 
				
			||||||
        if not attr.load_history().has_changes():
 | 
					        if not attr.load_history().has_changes():
 | 
				
			||||||
            continue
 | 
					            continue
 | 
				
			||||||
 | 
					        jsonpatch_path = f'{resource.jsonpatch_path}/{attr.key}'
 | 
				
			||||||
        if isinstance(attr.value, datetime):
 | 
					        if isinstance(attr.value, datetime):
 | 
				
			||||||
            value = f'{attr.value.isoformat()}Z'
 | 
					            jsonpatch_value = f'{attr.value.isoformat()}Z'
 | 
				
			||||||
        elif isinstance(attr.value, Enum):
 | 
					        elif isinstance(attr.value, Enum):
 | 
				
			||||||
            value = attr.value.name
 | 
					            jsonpatch_value = attr.value.name
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            value = attr.value
 | 
					            jsonpatch_value = attr.value
 | 
				
			||||||
        jsonpatch.append(
 | 
					        jsonpatch.append(
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                'op': 'replace',
 | 
					                'op': 'replace',
 | 
				
			||||||
                'path': f'{ressource.jsonpatch_path}/{attr.key}',
 | 
					                'path': jsonpatch_path,
 | 
				
			||||||
                'value': value
 | 
					                'value': jsonpatch_value
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    if jsonpatch:
 | 
					    if jsonpatch:
 | 
				
			||||||
        room = f'/users/{ressource.user_hashid}'
 | 
					        room = f'/users/{resource.user_hashid}'
 | 
				
			||||||
        socketio.emit('PATCH', jsonpatch, room=room)
 | 
					        socketio.emit('PATCH', jsonpatch, room=room)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
from flask import Blueprint
 | 
					from flask import Blueprint
 | 
				
			||||||
 | 
					from flask_login import login_required
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import yaml
 | 
					import yaml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -9,4 +10,16 @@ with open(services_file, 'r') as f:
 | 
				
			|||||||
    SERVICES = yaml.safe_load(f)
 | 
					    SERVICES = yaml.safe_load(f)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
bp = Blueprint('services', __name__)
 | 
					bp = Blueprint('services', __name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.before_request
 | 
				
			||||||
 | 
					@login_required
 | 
				
			||||||
 | 
					def before_request():
 | 
				
			||||||
 | 
					    '''
 | 
				
			||||||
 | 
					    Ensures that the routes in this package can only be visited by users that
 | 
				
			||||||
 | 
					    are logged in.
 | 
				
			||||||
 | 
					    '''
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from . import routes  # noqa
 | 
					from . import routes  # noqa
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,12 +1,17 @@
 | 
				
			|||||||
from flask_login import current_user
 | 
					 | 
				
			||||||
from flask_wtf import FlaskForm
 | 
					from flask_wtf import FlaskForm
 | 
				
			||||||
 | 
					from flask_login import current_user
 | 
				
			||||||
from flask_wtf.file import FileField, FileRequired
 | 
					from flask_wtf.file import FileField, FileRequired
 | 
				
			||||||
from wtforms import (BooleanField, DecimalRangeField, MultipleFileField,
 | 
					from wtforms import (
 | 
				
			||||||
                     SelectField, StringField, SubmitField, ValidationError)
 | 
					    BooleanField,
 | 
				
			||||||
 | 
					    DecimalRangeField,
 | 
				
			||||||
 | 
					    MultipleFileField,
 | 
				
			||||||
 | 
					    SelectField,
 | 
				
			||||||
 | 
					    StringField,
 | 
				
			||||||
 | 
					    SubmitField,
 | 
				
			||||||
 | 
					    ValidationError
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
from wtforms.validators import InputRequired, Length
 | 
					from wtforms.validators import InputRequired, Length
 | 
				
			||||||
 | 
					 | 
				
			||||||
from app.models import SpaCyNLPPipelineModel, TesseractOCRPipelineModel
 | 
					from app.models import SpaCyNLPPipelineModel, TesseractOCRPipelineModel
 | 
				
			||||||
 | 
					 | 
				
			||||||
from . import SERVICES
 | 
					from . import SERVICES
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -33,6 +38,8 @@ class CreateFileSetupPipelineJobForm(CreateJobBaseForm):
 | 
				
			|||||||
                raise ValidationError('JPEG, PNG and TIFF files only!')
 | 
					                raise ValidationError('JPEG, PNG and TIFF files only!')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, *args, **kwargs):
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        if 'prefix' not in kwargs:
 | 
				
			||||||
 | 
					            kwargs['prefix'] = 'create-file-setup-pipeline-job-form'
 | 
				
			||||||
        service_manifest = SERVICES['file-setup-pipeline']
 | 
					        service_manifest = SERVICES['file-setup-pipeline']
 | 
				
			||||||
        version = kwargs.pop('version', service_manifest['latest_version'])
 | 
					        version = kwargs.pop('version', service_manifest['latest_version'])
 | 
				
			||||||
        super().__init__(*args, **kwargs)
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
@@ -60,6 +67,8 @@ class CreateTesseractOCRPipelineJobForm(CreateJobBaseForm):
 | 
				
			|||||||
            raise ValidationError('PDF files only!')
 | 
					            raise ValidationError('PDF files only!')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, *args, **kwargs):
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        if 'prefix' not in kwargs:
 | 
				
			||||||
 | 
					            kwargs['prefix'] = 'create-tesseract-ocr-pipeline-job-form'
 | 
				
			||||||
        service_manifest = SERVICES['tesseract-ocr-pipeline']
 | 
					        service_manifest = SERVICES['tesseract-ocr-pipeline']
 | 
				
			||||||
        version = kwargs.pop('version', service_manifest['latest_version'])
 | 
					        version = kwargs.pop('version', service_manifest['latest_version'])
 | 
				
			||||||
        super().__init__(*args, **kwargs)
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
@@ -75,12 +84,18 @@ class CreateTesseractOCRPipelineJobForm(CreateJobBaseForm):
 | 
				
			|||||||
                del self.binarization.render_kw['disabled']
 | 
					                del self.binarization.render_kw['disabled']
 | 
				
			||||||
                if 'ocropus_nlbin_threshold' in service_info['methods']:
 | 
					                if 'ocropus_nlbin_threshold' in service_info['methods']:
 | 
				
			||||||
                    del self.ocropus_nlbin_threshold.render_kw['disabled']
 | 
					                    del self.ocropus_nlbin_threshold.render_kw['disabled']
 | 
				
			||||||
 | 
					        user_models = [
 | 
				
			||||||
 | 
					            x for x in current_user.tesseract_ocr_pipeline_models.order_by(TesseractOCRPipelineModel.title).all()
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
        models = [
 | 
					        models = [
 | 
				
			||||||
            x for x in TesseractOCRPipelineModel.query.order_by(TesseractOCRPipelineModel.title).all()
 | 
					            x for x in TesseractOCRPipelineModel.query.order_by(TesseractOCRPipelineModel.title).all()
 | 
				
			||||||
            if version in x.compatible_service_versions and (x.is_public == True or x.user == current_user)
 | 
					            if version in x.compatible_service_versions and (x.is_public == True or x.user == current_user)
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
        self.model.choices = [('', 'Choose your option')]
 | 
					        self.model.choices = {
 | 
				
			||||||
        self.model.choices += [(x.hashid, f'{x.title} [{x.version}]') for x in models]
 | 
					            '': [('', 'Choose your option')],
 | 
				
			||||||
 | 
					            'Your models': [(x.hashid, f'{x.title} [{x.version}]') for x in user_models] if user_models else [(0, 'Nothing here yet...')],
 | 
				
			||||||
 | 
					            'Public models': [(x.hashid, f'{x.title} [{x.version}]') for x in models]
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        self.model.default = ''
 | 
					        self.model.default = ''
 | 
				
			||||||
        self.version.choices = [(x, x) for x in service_manifest['versions']]
 | 
					        self.version.choices = [(x, x) for x in service_manifest['versions']]
 | 
				
			||||||
        self.version.data = version
 | 
					        self.version.data = version
 | 
				
			||||||
@@ -106,6 +121,8 @@ class CreateTranskribusHTRPipelineJobForm(CreateJobBaseForm):
 | 
				
			|||||||
            raise ValidationError('PDF files only!')
 | 
					            raise ValidationError('PDF files only!')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, *args, **kwargs):
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        if 'prefix' not in kwargs:
 | 
				
			||||||
 | 
					            kwargs['prefix'] = 'create-transkribus-htr-pipeline-job-form'
 | 
				
			||||||
        transkribus_htr_pipeline_models = kwargs.pop('transkribus_htr_pipeline_models', [])
 | 
					        transkribus_htr_pipeline_models = kwargs.pop('transkribus_htr_pipeline_models', [])
 | 
				
			||||||
        service_manifest = SERVICES['transkribus-htr-pipeline']
 | 
					        service_manifest = SERVICES['transkribus-htr-pipeline']
 | 
				
			||||||
        version = kwargs.pop('version', service_manifest['latest_version'])
 | 
					        version = kwargs.pop('version', service_manifest['latest_version'])
 | 
				
			||||||
@@ -144,6 +161,8 @@ class CreateSpacyNLPPipelineJobForm(CreateJobBaseForm):
 | 
				
			|||||||
            raise ValidationError('Plain text files only!')
 | 
					            raise ValidationError('Plain text files only!')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, *args, **kwargs):
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        if 'prefix' not in kwargs:
 | 
				
			||||||
 | 
					            kwargs['prefix'] = 'create-spacy-nlp-pipeline-job-form'
 | 
				
			||||||
        service_manifest = SERVICES['spacy-nlp-pipeline']
 | 
					        service_manifest = SERVICES['spacy-nlp-pipeline']
 | 
				
			||||||
        version = kwargs.pop('version', service_manifest['latest_version'])
 | 
					        version = kwargs.pop('version', service_manifest['latest_version'])
 | 
				
			||||||
        super().__init__(*args, **kwargs)
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
@@ -155,12 +174,18 @@ class CreateSpacyNLPPipelineJobForm(CreateJobBaseForm):
 | 
				
			|||||||
        if 'methods' in service_info:
 | 
					        if 'methods' in service_info:
 | 
				
			||||||
            if 'encoding_detection' in service_info['methods']:
 | 
					            if 'encoding_detection' in service_info['methods']:
 | 
				
			||||||
                del self.encoding_detection.render_kw['disabled']
 | 
					                del self.encoding_detection.render_kw['disabled']
 | 
				
			||||||
        models = [
 | 
					        user_models = [
 | 
				
			||||||
            x for x in SpaCyNLPPipelineModel.query.order_by(SpaCyNLPPipelineModel.title).all()
 | 
					            x for x in current_user.spacy_nlp_pipeline_models.order_by(SpaCyNLPPipelineModel.title).all()
 | 
				
			||||||
            if version in x.compatible_service_versions and (x.is_public == True or x.user == current_user)
 | 
					 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
        self.model.choices = [('', 'Choose your option')]
 | 
					        models = [
 | 
				
			||||||
        self.model.choices += [(x.hashid, f'{x.title} [{x.version}]') for x in models]
 | 
					            x for x in SpaCyNLPPipelineModel.query.filter(SpaCyNLPPipelineModel.user != current_user, SpaCyNLPPipelineModel.is_public == True).order_by(SpaCyNLPPipelineModel.title).all()
 | 
				
			||||||
 | 
					            if version in x.compatible_service_versions
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					        self.model.choices = {
 | 
				
			||||||
 | 
					            '': [('', 'Choose your option')],
 | 
				
			||||||
 | 
					            'Your models': [(x.hashid, f'{x.title} [{x.version}]') for x in user_models] if user_models else [(0, 'Nothing here yet...')],
 | 
				
			||||||
 | 
					            'Public models': [(x.hashid, f'{x.title} [{x.version}]') for x in models]
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        self.model.default = ''
 | 
					        self.model.default = ''
 | 
				
			||||||
        self.version.choices = [(x, x) for x in service_manifest['versions']]
 | 
					        self.version.choices = [(x, x) for x in service_manifest['versions']]
 | 
				
			||||||
        self.version.data = version
 | 
					        self.version.data = version
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
from flask import abort, current_app, flash, make_response, Markup, render_template, request
 | 
					from flask import abort, current_app, flash, Markup, redirect, render_template, request, url_for
 | 
				
			||||||
from flask_login import current_user, login_required
 | 
					from flask_breadcrumbs import register_breadcrumb
 | 
				
			||||||
 | 
					from flask_login import current_user
 | 
				
			||||||
import requests
 | 
					import requests
 | 
				
			||||||
from app import db, hashids
 | 
					from app import db, hashids
 | 
				
			||||||
from app.models import (
 | 
					from app.models import (
 | 
				
			||||||
@@ -18,8 +19,14 @@ from .forms import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/services')
 | 
				
			||||||
 | 
					@register_breadcrumb(bp, '.', 'Services')
 | 
				
			||||||
 | 
					def services():
 | 
				
			||||||
 | 
					    return redirect(url_for('main.dashboard'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('/file-setup-pipeline', methods=['GET', 'POST'])
 | 
					@bp.route('/file-setup-pipeline', methods=['GET', 'POST'])
 | 
				
			||||||
@login_required
 | 
					@register_breadcrumb(bp, '.file_setup_pipeline', '<i class="nopaque-icons service-icons left" data-service="file-setup-pipeline"></i>File Setup')
 | 
				
			||||||
def file_setup_pipeline():
 | 
					def file_setup_pipeline():
 | 
				
			||||||
    service = 'file-setup-pipeline'
 | 
					    service = 'file-setup-pipeline'
 | 
				
			||||||
    service_manifest = SERVICES[service]
 | 
					    service_manifest = SERVICES[service]
 | 
				
			||||||
@@ -54,13 +61,13 @@ def file_setup_pipeline():
 | 
				
			|||||||
        return {}, 201, {'Location': job.url}
 | 
					        return {}, 201, {'Location': job.url}
 | 
				
			||||||
    return render_template(
 | 
					    return render_template(
 | 
				
			||||||
        'services/file_setup_pipeline.html.j2',
 | 
					        'services/file_setup_pipeline.html.j2',
 | 
				
			||||||
        form=form,
 | 
					        title=service_manifest['name'],
 | 
				
			||||||
        title=service_manifest['name']
 | 
					        form=form
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('/tesseract-ocr-pipeline', methods=['GET', 'POST'])
 | 
					@bp.route('/tesseract-ocr-pipeline', methods=['GET', 'POST'])
 | 
				
			||||||
@login_required
 | 
					@register_breadcrumb(bp, '.tesseract_ocr_pipeline', '<i class="nopaque-icons service-icons left" data-service="tesseract-ocr-pipeline"></i>Tesseract OCR Pipeline')
 | 
				
			||||||
def tesseract_ocr_pipeline():
 | 
					def tesseract_ocr_pipeline():
 | 
				
			||||||
    service_name = 'tesseract-ocr-pipeline'
 | 
					    service_name = 'tesseract-ocr-pipeline'
 | 
				
			||||||
    service_manifest = SERVICES[service_name]
 | 
					    service_manifest = SERVICES[service_name]
 | 
				
			||||||
@@ -100,16 +107,18 @@ def tesseract_ocr_pipeline():
 | 
				
			|||||||
        x for x in TesseractOCRPipelineModel.query.all()
 | 
					        x for x in TesseractOCRPipelineModel.query.all()
 | 
				
			||||||
        if version in x.compatible_service_versions and (x.is_public == True or x.user == current_user)
 | 
					        if version in x.compatible_service_versions and (x.is_public == True or x.user == current_user)
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					    user_tesseract_ocr_pipeline_models_count = len(current_user.tesseract_ocr_pipeline_models.all())
 | 
				
			||||||
    return render_template(
 | 
					    return render_template(
 | 
				
			||||||
        'services/tesseract_ocr_pipeline.html.j2',
 | 
					        'services/tesseract_ocr_pipeline.html.j2',
 | 
				
			||||||
 | 
					        title=service_manifest['name'],
 | 
				
			||||||
        form=form,
 | 
					        form=form,
 | 
				
			||||||
        tesseract_ocr_pipeline_models=tesseract_ocr_pipeline_models,
 | 
					        tesseract_ocr_pipeline_models=tesseract_ocr_pipeline_models,
 | 
				
			||||||
        title=service_manifest['name']
 | 
					        user_tesseract_ocr_pipeline_models_count=user_tesseract_ocr_pipeline_models_count
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('/transkribus-htr-pipeline', methods=['GET', 'POST'])
 | 
					@bp.route('/transkribus-htr-pipeline', methods=['GET', 'POST'])
 | 
				
			||||||
@login_required
 | 
					@register_breadcrumb(bp, '.transkribus_htr_pipeline', '<i class="nopaque-icons service-icons left" data-service="transkribus-htr-pipeline"></i>Transkribus HTR Pipeline')
 | 
				
			||||||
def transkribus_htr_pipeline():
 | 
					def transkribus_htr_pipeline():
 | 
				
			||||||
    if not current_app.config.get('NOPAQUE_TRANSKRIBUS_ENABLED'):
 | 
					    if not current_app.config.get('NOPAQUE_TRANSKRIBUS_ENABLED'):
 | 
				
			||||||
        abort(404)
 | 
					        abort(404)
 | 
				
			||||||
@@ -126,10 +135,9 @@ def transkribus_htr_pipeline():
 | 
				
			|||||||
        abort(500)
 | 
					        abort(500)
 | 
				
			||||||
    transkribus_htr_pipeline_models = r.json()['trpModelMetadata']
 | 
					    transkribus_htr_pipeline_models = r.json()['trpModelMetadata']
 | 
				
			||||||
    transkribus_htr_pipeline_models.append({'modelId': 48513, 'name': 'Caroline Minuscle', 'language': 'lat', 'isoLanguages': ['lat']})
 | 
					    transkribus_htr_pipeline_models.append({'modelId': 48513, 'name': 'Caroline Minuscle', 'language': 'lat', 'isoLanguages': ['lat']})
 | 
				
			||||||
    print(transkribus_htr_pipeline_models[len(transkribus_htr_pipeline_models)-1])
 | 
					 | 
				
			||||||
    form = CreateTranskribusHTRPipelineJobForm(
 | 
					    form = CreateTranskribusHTRPipelineJobForm(
 | 
				
			||||||
        transkribus_htr_pipeline_models=transkribus_htr_pipeline_models,
 | 
					 | 
				
			||||||
        prefix='create-job-form',
 | 
					        prefix='create-job-form',
 | 
				
			||||||
 | 
					        transkribus_htr_pipeline_models=transkribus_htr_pipeline_models,
 | 
				
			||||||
        version=version
 | 
					        version=version
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    if form.is_submitted():
 | 
					    if form.is_submitted():
 | 
				
			||||||
@@ -161,14 +169,14 @@ def transkribus_htr_pipeline():
 | 
				
			|||||||
        return {}, 201, {'Location': job.url}
 | 
					        return {}, 201, {'Location': job.url}
 | 
				
			||||||
    return render_template(
 | 
					    return render_template(
 | 
				
			||||||
        'services/transkribus_htr_pipeline.html.j2',
 | 
					        'services/transkribus_htr_pipeline.html.j2',
 | 
				
			||||||
        form=form,
 | 
					 | 
				
			||||||
        title=service_manifest['name'],
 | 
					        title=service_manifest['name'],
 | 
				
			||||||
 | 
					        form=form,
 | 
				
			||||||
        transkribus_htr_pipeline_models=transkribus_htr_pipeline_models
 | 
					        transkribus_htr_pipeline_models=transkribus_htr_pipeline_models
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('/spacy-nlp-pipeline', methods=['GET', 'POST'])
 | 
					@bp.route('/spacy-nlp-pipeline', methods=['GET', 'POST'])
 | 
				
			||||||
@login_required
 | 
					@register_breadcrumb(bp, '.spacy_nlp_pipeline', '<i class="nopaque-icons service-icons left" data-service="spacy-nlp-pipeline"></i>SpaCy NLP Pipeline')
 | 
				
			||||||
def spacy_nlp_pipeline():
 | 
					def spacy_nlp_pipeline():
 | 
				
			||||||
    service = 'spacy-nlp-pipeline'
 | 
					    service = 'spacy-nlp-pipeline'
 | 
				
			||||||
    service_manifest = SERVICES[service]
 | 
					    service_manifest = SERVICES[service]
 | 
				
			||||||
@@ -177,6 +185,7 @@ def spacy_nlp_pipeline():
 | 
				
			|||||||
        abort(404)
 | 
					        abort(404)
 | 
				
			||||||
    form = CreateSpacyNLPPipelineJobForm(prefix='create-job-form', version=version)
 | 
					    form = CreateSpacyNLPPipelineJobForm(prefix='create-job-form', version=version)
 | 
				
			||||||
    spacy_nlp_pipeline_models = SpaCyNLPPipelineModel.query.all()
 | 
					    spacy_nlp_pipeline_models = SpaCyNLPPipelineModel.query.all()
 | 
				
			||||||
 | 
					    user_spacy_nlp_pipeline_models_count = len(current_user.spacy_nlp_pipeline_models.all())
 | 
				
			||||||
    if form.is_submitted():
 | 
					    if form.is_submitted():
 | 
				
			||||||
        if not form.validate():
 | 
					        if not form.validate():
 | 
				
			||||||
            response = {'errors': form.errors}
 | 
					            response = {'errors': form.errors}
 | 
				
			||||||
@@ -206,16 +215,17 @@ def spacy_nlp_pipeline():
 | 
				
			|||||||
        return {}, 201, {'Location': job.url}
 | 
					        return {}, 201, {'Location': job.url}
 | 
				
			||||||
    return render_template(
 | 
					    return render_template(
 | 
				
			||||||
        'services/spacy_nlp_pipeline.html.j2',
 | 
					        'services/spacy_nlp_pipeline.html.j2',
 | 
				
			||||||
 | 
					        title=service_manifest['name'],
 | 
				
			||||||
        form=form,
 | 
					        form=form,
 | 
				
			||||||
        spacy_nlp_pipeline_models=spacy_nlp_pipeline_models,
 | 
					        spacy_nlp_pipeline_models=spacy_nlp_pipeline_models,
 | 
				
			||||||
        title=service_manifest['name']
 | 
					        user_spacy_nlp_pipeline_models_count=user_spacy_nlp_pipeline_models_count
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('/corpus-analysis')
 | 
					@bp.route('/corpus-analysis')
 | 
				
			||||||
@login_required
 | 
					@register_breadcrumb(bp, '.corpus_analysis', '<i class="nopaque-icons service-icons left" data-service="corpus-analysis"></i>Corpus Analysis')
 | 
				
			||||||
def corpus_analysis():
 | 
					def corpus_analysis():
 | 
				
			||||||
    return render_template(
 | 
					    return render_template(
 | 
				
			||||||
        'services/corpus_analysis.html.j2',
 | 
					        'services/corpus_analysis.html.j2',
 | 
				
			||||||
        title='Corpus analysis'
 | 
					        title='Corpus Analysis'
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,18 @@
 | 
				
			|||||||
from flask import Blueprint
 | 
					from flask import Blueprint
 | 
				
			||||||
 | 
					from flask_login import login_required
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
bp = Blueprint('settings', __name__)
 | 
					bp = Blueprint('settings', __name__)
 | 
				
			||||||
from . import routes  # noqa
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.before_request
 | 
				
			||||||
 | 
					@login_required
 | 
				
			||||||
 | 
					def before_request():
 | 
				
			||||||
 | 
					    '''
 | 
				
			||||||
 | 
					    Ensures that the routes in this package can only be visited by users that
 | 
				
			||||||
 | 
					    are logged in.
 | 
				
			||||||
 | 
					    '''
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from . import routes
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,43 +0,0 @@
 | 
				
			|||||||
from flask_wtf import FlaskForm
 | 
					 | 
				
			||||||
from wtforms import PasswordField, SelectField, SubmitField, ValidationError
 | 
					 | 
				
			||||||
from wtforms.validators import DataRequired, EqualTo
 | 
					 | 
				
			||||||
from app.models import UserSettingJobStatusMailNotificationLevel
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class ChangePasswordForm(FlaskForm):
 | 
					 | 
				
			||||||
    password = PasswordField('Old password', validators=[DataRequired()])
 | 
					 | 
				
			||||||
    new_password = PasswordField(
 | 
					 | 
				
			||||||
        'New password',
 | 
					 | 
				
			||||||
        validators=[
 | 
					 | 
				
			||||||
            DataRequired(),
 | 
					 | 
				
			||||||
            EqualTo('new_password_2', message='Passwords must match')
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    new_password_2 = PasswordField(
 | 
					 | 
				
			||||||
        'New password confirmation',
 | 
					 | 
				
			||||||
        validators=[
 | 
					 | 
				
			||||||
            DataRequired(),
 | 
					 | 
				
			||||||
            EqualTo('new_password', message='Passwords must match')
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    submit = SubmitField()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __init__(self, user, *args, **kwargs):
 | 
					 | 
				
			||||||
        super().__init__(*args, **kwargs)
 | 
					 | 
				
			||||||
        self.user = user
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def validate_current_password(self, field):
 | 
					 | 
				
			||||||
        if not self.user.verify_password(field.data):
 | 
					 | 
				
			||||||
            raise ValidationError('Invalid password')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class EditNotificationSettingsForm(FlaskForm):
 | 
					 | 
				
			||||||
    job_status_mail_notification_level = SelectField(
 | 
					 | 
				
			||||||
        'Job status mail notification level',
 | 
					 | 
				
			||||||
        choices=[
 | 
					 | 
				
			||||||
            (x.name, x.name.capitalize())
 | 
					 | 
				
			||||||
            for x in UserSettingJobStatusMailNotificationLevel
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
        validators=[DataRequired()]
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    submit = SubmitField()
 | 
					 | 
				
			||||||
@@ -1,39 +1,12 @@
 | 
				
			|||||||
from flask import flash, redirect, render_template, url_for
 | 
					from flask import g, url_for
 | 
				
			||||||
from flask_login import current_user, login_required
 | 
					from flask_breadcrumbs import register_breadcrumb
 | 
				
			||||||
from app import db
 | 
					from flask_login import current_user
 | 
				
			||||||
from app.models import UserSettingJobStatusMailNotificationLevel
 | 
					from app.users.settings.routes import settings as settings_route
 | 
				
			||||||
from . import bp
 | 
					from . import bp
 | 
				
			||||||
from .forms import ChangePasswordForm, EditNotificationSettingsForm
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route('', methods=['GET', 'POST'])
 | 
					@bp.route('/settings', methods=['GET', 'POST'])
 | 
				
			||||||
@login_required
 | 
					@register_breadcrumb(bp, '.', '<i class="material-icons left">settings</i>Settings')
 | 
				
			||||||
def settings():
 | 
					def settings():
 | 
				
			||||||
    change_password_form = ChangePasswordForm(
 | 
					    g._nopaque_redirect_location_on_post = url_for('.settings')
 | 
				
			||||||
        current_user,
 | 
					    return settings_route(current_user.id)
 | 
				
			||||||
        prefix='change-password-form'
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    edit_notification_settings_form = EditNotificationSettingsForm(
 | 
					 | 
				
			||||||
        data=current_user.to_json_serializeable(),
 | 
					 | 
				
			||||||
        prefix='edit-notification-settings-form'
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    # region handle change_password_form POST
 | 
					 | 
				
			||||||
    if change_password_form.submit.data and change_password_form.validate():
 | 
					 | 
				
			||||||
        current_user.password = change_password_form.new_password.data
 | 
					 | 
				
			||||||
        db.session.commit()
 | 
					 | 
				
			||||||
        flash('Your changes have been saved')
 | 
					 | 
				
			||||||
        return redirect(url_for('.settings'))
 | 
					 | 
				
			||||||
    # endregion handle change_password_form POST
 | 
					 | 
				
			||||||
    # region handle edit_notification_settings_form POST
 | 
					 | 
				
			||||||
    if edit_notification_settings_form.submit and edit_notification_settings_form.validate():
 | 
					 | 
				
			||||||
        current_user.setting_job_status_mail_notification_level = edit_notification_settings_form.job_status_mail_notification_level.data
 | 
					 | 
				
			||||||
        db.session.commit()
 | 
					 | 
				
			||||||
        flash('Your changes have been saved')
 | 
					 | 
				
			||||||
        return redirect(url_for('.settings'))
 | 
					 | 
				
			||||||
    # endregion handle edit_notification_settings_form POST
 | 
					 | 
				
			||||||
    return render_template(
 | 
					 | 
				
			||||||
        'settings/settings.html.j2',
 | 
					 | 
				
			||||||
        change_password_form=change_password_form,
 | 
					 | 
				
			||||||
        edit_notification_settings_form=edit_notification_settings_form,
 | 
					 | 
				
			||||||
        title='Settings'
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,6 +22,11 @@ $color: (
 | 
				
			|||||||
    "surface": #ffffff,
 | 
					    "surface": #ffffff,
 | 
				
			||||||
    "error": #b00020
 | 
					    "error": #b00020
 | 
				
			||||||
  ),
 | 
					  ),
 | 
				
			||||||
 | 
					  "social-area": (
 | 
				
			||||||
 | 
					    "base": #d6ae86,
 | 
				
			||||||
 | 
					    "darken": #C98536,
 | 
				
			||||||
 | 
					    "lighten": #EAE2DB
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
  "service": (
 | 
					  "service": (
 | 
				
			||||||
    "corpus-analysis": (
 | 
					    "corpus-analysis": (
 | 
				
			||||||
      "base": #aa9cc9,
 | 
					      "base": #aa9cc9,
 | 
				
			||||||
@@ -108,6 +113,16 @@ $color: (
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@each $key, $color-code in map-get($color, "social-area") {
 | 
				
			||||||
 | 
					  .social-area-color-#{$key} {
 | 
				
			||||||
 | 
					    background-color: $color-code !important;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .social-area-color-border-#{$key} {
 | 
				
			||||||
 | 
					    border-color: $color-code !important;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@each $service-name, $color-palette in map-get($color, "service") {
 | 
					@each $service-name, $color-palette in map-get($color, "service") {
 | 
				
			||||||
  .service-color[data-service="#{$service-name}"] {
 | 
					  .service-color[data-service="#{$service-name}"] {
 | 
				
			||||||
    background-color: map-get($color-palette, "base") !important;
 | 
					    background-color: map-get($color-palette, "base") !important;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,8 @@
 | 
				
			|||||||
.parallax-container .parallax {
 | 
					.parallax-container .parallax {
 | 
				
			||||||
  z-index: 0;
 | 
					  z-index: 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.autocomplete-content {
 | 
				
			||||||
 | 
					  width: 100% !important;
 | 
				
			||||||
 | 
					  left: 0 !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,6 +19,10 @@
 | 
				
			|||||||
  height: 30px !important;
 | 
					  height: 30px !important;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#manual-modal .manual-chapter-title {
 | 
				
			||||||
 | 
					  display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.show-if-only-child:not(:only-child) {
 | 
					.show-if-only-child:not(:only-child) {
 | 
				
			||||||
  display: none !important;
 | 
					  display: none !important;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 34 KiB  | 
@@ -60,6 +60,10 @@ class App {
 | 
				
			|||||||
        iconPrefix = '<i class="left nopaque-icons">J</i>';
 | 
					        iconPrefix = '<i class="left nopaque-icons">J</i>';
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      case 'settings': {
 | 
				
			||||||
 | 
					        iconPrefix = '<i class="left material-icons">settings</i>';
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      default: {
 | 
					      default: {
 | 
				
			||||||
        iconPrefix = '<i class="left material-icons">notifications</i>';
 | 
					        iconPrefix = '<i class="left material-icons">notifications</i>';
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
@@ -91,7 +95,7 @@ class App {
 | 
				
			|||||||
      .filter((operation) => {return subRegExp.test(operation.path);});
 | 
					      .filter((operation) => {return subRegExp.test(operation.path);});
 | 
				
			||||||
    for (let operation of subFilteredPatch) {
 | 
					    for (let operation of subFilteredPatch) {
 | 
				
			||||||
      let [match, userId, jobId] = operation.path.match(subRegExp);
 | 
					      let [match, userId, jobId] = operation.path.match(subRegExp);
 | 
				
			||||||
      this.flash(`[<a href="/jobs/${jobId}">${this.data.users[userId].jobs[jobId].title}</a>] New status: <span class="job-status-text" data-job-status="${operation.value}"></span>`, 'job');
 | 
					      this.flash(`[<a href="/jobs/${jobId}">${this.data.users[userId].jobs[jobId].title}</a>] New status: <span class="job-status-text" data-status="${operation.value}"></span>`, 'job');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Apply Patch
 | 
					    // Apply Patch
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,8 +7,6 @@ class CorpusAnalysisApp {
 | 
				
			|||||||
      container: document.querySelector('#corpus-analysis-app-container'),
 | 
					      container: document.querySelector('#corpus-analysis-app-container'),
 | 
				
			||||||
      extensionTabs: document.querySelector('#corpus-analysis-app-extension-tabs'),
 | 
					      extensionTabs: document.querySelector('#corpus-analysis-app-extension-tabs'),
 | 
				
			||||||
      initModal: document.querySelector('#corpus-analysis-app-init-modal'),
 | 
					      initModal: document.querySelector('#corpus-analysis-app-init-modal'),
 | 
				
			||||||
      initError: document.querySelector('#corpus-analysis-app-init-error'),
 | 
					 | 
				
			||||||
      initProgress: document.querySelector('#corpus-analysis-app-init-progress'),
 | 
					 | 
				
			||||||
      overview: document.querySelector('#corpus-analysis-app-overview')
 | 
					      overview: document.querySelector('#corpus-analysis-app-overview')
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    // Materialize elements
 | 
					    // Materialize elements
 | 
				
			||||||
@@ -27,6 +25,7 @@ class CorpusAnalysisApp {
 | 
				
			|||||||
  init() {
 | 
					  init() {
 | 
				
			||||||
    this.disableActionElements();
 | 
					    this.disableActionElements();
 | 
				
			||||||
    this.elements.m.initModal.open();
 | 
					    this.elements.m.initModal.open();
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
    // Init data
 | 
					    // Init data
 | 
				
			||||||
    this.data.cQiClient = new CQiClient(this.settings.corpusId);
 | 
					    this.data.cQiClient = new CQiClient(this.settings.corpusId);
 | 
				
			||||||
    this.data.cQiClient.connect()
 | 
					    this.data.cQiClient.connect()
 | 
				
			||||||
@@ -43,14 +42,17 @@ class CorpusAnalysisApp {
 | 
				
			|||||||
          this.elements.m.initModal.close();
 | 
					          this.elements.m.initModal.close();
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        cQiError => {
 | 
					        cQiError => {
 | 
				
			||||||
          this.elements.initError.innerText = JSON.stringify(cQiError);
 | 
					          let errorsElement = this.elements.initModal.querySelector('.errors');
 | 
				
			||||||
          this.elements.initError.classList.remove('hide');
 | 
					          let progressElement = this.elements.initModal.querySelector('.progress');
 | 
				
			||||||
          this.elements.initProgress.classList.add('hide');
 | 
					          errorsElement.innerText = JSON.stringify(cQiError);
 | 
				
			||||||
 | 
					          errorsElement.classList.remove('hide');
 | 
				
			||||||
 | 
					          progressElement.classList.add('hide');
 | 
				
			||||||
          if ('payload' in cQiError && 'code' in cQiError.payload && 'msg' in cQiError.payload) {
 | 
					          if ('payload' in cQiError && 'code' in cQiError.payload && 'msg' in cQiError.payload) {
 | 
				
			||||||
            app.flash(`${cQiError.payload.code}: ${cQiError.payload.msg}`, 'error');
 | 
					            app.flash(`${cQiError.payload.code}: ${cQiError.payload.msg}`, 'error');
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
    // Add event listeners
 | 
					    // Add event listeners
 | 
				
			||||||
    for (let extensionSelectorElement of this.elements.overview.querySelectorAll('.extension-selector')) {
 | 
					    for (let extensionSelectorElement of this.elements.overview.querySelectorAll('.extension-selector')) {
 | 
				
			||||||
      extensionSelectorElement.addEventListener('click', () => {
 | 
					      extensionSelectorElement.addEventListener('click', () => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -106,41 +106,102 @@ class CorpusAnalysisReader {
 | 
				
			|||||||
  renderCorpusPagination() {
 | 
					  renderCorpusPagination() {
 | 
				
			||||||
    this.clearCorpusPagination();
 | 
					    this.clearCorpusPagination();
 | 
				
			||||||
    if (this.data.corpus.p.pages === 0) {return;}
 | 
					    if (this.data.corpus.p.pages === 0) {return;}
 | 
				
			||||||
    this.elements.corpusPagination.innerHTML += `
 | 
					    let pageElement;
 | 
				
			||||||
 | 
					    // First page button. Disables first page button if on first page
 | 
				
			||||||
 | 
					    pageElement = Utils.HTMLToElement(
 | 
				
			||||||
 | 
					      `
 | 
				
			||||||
        <li class="${this.data.corpus.p.page === 1 ? 'disabled' : 'waves-effect'}">
 | 
					        <li class="${this.data.corpus.p.page === 1 ? 'disabled' : 'waves-effect'}">
 | 
				
			||||||
          <a class="corpus-analysis-action pagination-trigger" ${this.data.corpus.p.page === 1 ? '' : 'data-target="1"'}>
 | 
					          <a class="corpus-analysis-action pagination-trigger" ${this.data.corpus.p.page === 1 ? '' : 'data-target="1"'}>
 | 
				
			||||||
            <i class="material-icons">first_page</i>
 | 
					            <i class="material-icons">first_page</i>
 | 
				
			||||||
          </a>
 | 
					          </a>
 | 
				
			||||||
        </li>
 | 
					        </li>
 | 
				
			||||||
    `.trim();
 | 
					      `
 | 
				
			||||||
    this.elements.corpusPagination.innerHTML += `
 | 
					    );
 | 
				
			||||||
 | 
					    this.elements.corpusPagination.appendChild(pageElement);
 | 
				
			||||||
 | 
					    // Previous page button. Disables previous page button if on first page
 | 
				
			||||||
 | 
					    pageElement = Utils.HTMLToElement(
 | 
				
			||||||
 | 
					      `
 | 
				
			||||||
        <li class="${this.data.corpus.p.has_prev ? 'waves-effect' : 'disabled'}">
 | 
					        <li class="${this.data.corpus.p.has_prev ? 'waves-effect' : 'disabled'}">
 | 
				
			||||||
          <a class="corpus-analysis-action pagination-trigger" ${this.data.corpus.p.has_prev ? 'data-target="' + this.data.corpus.p.prev_num + '"' : ''}>
 | 
					          <a class="corpus-analysis-action pagination-trigger" ${this.data.corpus.p.has_prev ? 'data-target="' + this.data.corpus.p.prev_num + '"' : ''}>
 | 
				
			||||||
            <i class="material-icons">chevron_left</i>
 | 
					            <i class="material-icons">chevron_left</i>
 | 
				
			||||||
          </a>
 | 
					          </a>
 | 
				
			||||||
        </li>
 | 
					        </li>
 | 
				
			||||||
    `.trim();
 | 
					      `
 | 
				
			||||||
    for (let i = 1; i <= this.data.corpus.p.pages; i++) {
 | 
					    );
 | 
				
			||||||
      this.elements.corpusPagination.innerHTML += `
 | 
					    this.elements.corpusPagination.appendChild(pageElement);
 | 
				
			||||||
 | 
					    // First page as number. Hides first page button if on first page
 | 
				
			||||||
 | 
					    if (this.data.corpus.p.page > 6) {
 | 
				
			||||||
 | 
					      pageElement = Utils.HTMLToElement(
 | 
				
			||||||
 | 
					        `
 | 
				
			||||||
 | 
					          <li class="waves-effect">
 | 
				
			||||||
 | 
					            <a class="corpus-analysis-action pagination-trigger" data-target="1">1</a>
 | 
				
			||||||
 | 
					          </li>
 | 
				
			||||||
 | 
					        `
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      this.elements.corpusPagination.appendChild(pageElement);
 | 
				
			||||||
 | 
					      pageElement = Utils.HTMLToElement("<li style='margin-top: 5px;'>…</li>");
 | 
				
			||||||
 | 
					      this.elements.corpusPagination.appendChild(pageElement);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // render page buttons (5 before and 5 after current page)
 | 
				
			||||||
 | 
					    for (let i = this.data.corpus.p.page -5; i <= this.data.corpus.p.page; i++) {
 | 
				
			||||||
 | 
					      if (i <= 0) {continue;}
 | 
				
			||||||
 | 
					      pageElement = Utils.HTMLToElement(
 | 
				
			||||||
 | 
					        `
 | 
				
			||||||
          <li class="${i === this.data.corpus.p.page ? 'active' : 'waves-effect'}">
 | 
					          <li class="${i === this.data.corpus.p.page ? 'active' : 'waves-effect'}">
 | 
				
			||||||
          <a class="corpus-analysis-action pagination-trigger" ${i === this.data.corpus.p.page ? '' : 'data-target="' + i + '"'}>${i}</a>
 | 
					          <a class="corpus-analysis-action pagination-trigger" ${i === this.data.corpus.p.page ? '' : 'data-target="' + i + '"'}>${i}</a>
 | 
				
			||||||
          </li>
 | 
					          </li>
 | 
				
			||||||
      `.trim();
 | 
					        `
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      this.elements.corpusPagination.appendChild(pageElement);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    for (let i = this.data.corpus.p.page +1; i <= this.data.corpus.p.page +5; i++) {
 | 
				
			||||||
 | 
					      if (i > this.data.corpus.p.pages) {break;}
 | 
				
			||||||
 | 
					      pageElement = Utils.HTMLToElement(
 | 
				
			||||||
 | 
					        `
 | 
				
			||||||
 | 
					          <li class="${i === this.data.corpus.p.page ? 'active' : 'waves-effect'}">
 | 
				
			||||||
 | 
					          <a class="corpus-analysis-action pagination-trigger" ${i === this.data.corpus.p.page ? '' : 'data-target="' + i + '"'}>${i}</a>
 | 
				
			||||||
 | 
					          </li>
 | 
				
			||||||
 | 
					        `
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      this.elements.corpusPagination.appendChild(pageElement);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    // Last page as number. Hides last page button if on last page
 | 
				
			||||||
 | 
					    if (this.data.corpus.p.page < this.data.corpus.p.pages - 6) {
 | 
				
			||||||
 | 
					      pageElement = Utils.HTMLToElement("<li style='margin-top: 5px;'>…</li>");
 | 
				
			||||||
 | 
					      this.elements.corpusPagination.appendChild(pageElement);
 | 
				
			||||||
 | 
					      pageElement = Utils.HTMLToElement(
 | 
				
			||||||
 | 
					        `
 | 
				
			||||||
 | 
					          <li class="waves-effect">
 | 
				
			||||||
 | 
					            <a class="corpus-analysis-action pagination-trigger" data-target="${this.data.corpus.p.pages}">${this.data.corpus.p.pages}</a>
 | 
				
			||||||
 | 
					          </li>
 | 
				
			||||||
 | 
					        `
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      this.elements.corpusPagination.appendChild(pageElement);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    this.elements.corpusPagination.innerHTML += `
 | 
					    // Next page button. Disables next page button if on last page
 | 
				
			||||||
 | 
					    pageElement = Utils.HTMLToElement(
 | 
				
			||||||
 | 
					      `
 | 
				
			||||||
        <li class="${this.data.corpus.p.has_next ? 'waves-effect' : 'disabled'}">
 | 
					        <li class="${this.data.corpus.p.has_next ? 'waves-effect' : 'disabled'}">
 | 
				
			||||||
          <a class="corpus-analysis-action pagination-trigger" ${this.data.corpus.p.has_next ? 'data-target="' + this.data.corpus.p.next_num + '"' : ''}>
 | 
					          <a class="corpus-analysis-action pagination-trigger" ${this.data.corpus.p.has_next ? 'data-target="' + this.data.corpus.p.next_num + '"' : ''}>
 | 
				
			||||||
            <i class="material-icons">chevron_right</i>
 | 
					            <i class="material-icons">chevron_right</i>
 | 
				
			||||||
          </a>
 | 
					          </a>
 | 
				
			||||||
        </li>
 | 
					        </li>
 | 
				
			||||||
    `.trim();
 | 
					      `
 | 
				
			||||||
    this.elements.corpusPagination.innerHTML += `
 | 
					    );
 | 
				
			||||||
 | 
					    this.elements.corpusPagination.appendChild(pageElement);
 | 
				
			||||||
 | 
					    // Last page button. Disables last page button if on last page
 | 
				
			||||||
 | 
					    pageElement = Utils.HTMLToElement(
 | 
				
			||||||
 | 
					      `
 | 
				
			||||||
        <li class="${this.data.corpus.p.page === this.data.corpus.p.pages ? 'disabled' : 'waves-effect'}">
 | 
					        <li class="${this.data.corpus.p.page === this.data.corpus.p.pages ? 'disabled' : 'waves-effect'}">
 | 
				
			||||||
          <a class="corpus-analysis-action pagination-trigger" ${this.data.corpus.p.page === this.data.corpus.p.pages ? '' : 'data-target="' + this.data.corpus.p.pages + '"'}>
 | 
					          <a class="corpus-analysis-action pagination-trigger" ${this.data.corpus.p.page === this.data.corpus.p.pages ? '' : 'data-target="' + this.data.corpus.p.pages + '"'}>
 | 
				
			||||||
            <i class="material-icons">last_page</i>
 | 
					            <i class="material-icons">last_page</i>
 | 
				
			||||||
          </a>
 | 
					          </a>
 | 
				
			||||||
        </li>
 | 
					        </li>
 | 
				
			||||||
    `.trim();
 | 
					      `
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    this.elements.corpusPagination.appendChild(pageElement);
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    for (let paginateTriggerElement of this.elements.corpusPagination.querySelectorAll('.pagination-trigger[data-target]')) {
 | 
					    for (let paginateTriggerElement of this.elements.corpusPagination.querySelectorAll('.pagination-trigger[data-target]')) {
 | 
				
			||||||
      paginateTriggerElement.addEventListener('click', event => {
 | 
					      paginateTriggerElement.addEventListener('click', event => {
 | 
				
			||||||
        event.preventDefault();
 | 
					        event.preventDefault();
 | 
				
			||||||
@@ -182,6 +243,7 @@ class CorpusAnalysisReader {
 | 
				
			|||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    this.app.disableActionElements();
 | 
					    this.app.disableActionElements();
 | 
				
			||||||
 | 
					    window.scrollTo(top);
 | 
				
			||||||
    this.elements.progress.classList.remove('hide');
 | 
					    this.elements.progress.classList.remove('hide');
 | 
				
			||||||
    this.data.corpus.o.paginate(pageNum, this.settings.perPage)
 | 
					    this.data.corpus.o.paginate(pageNum, this.settings.perPage)
 | 
				
			||||||
      .then(
 | 
					      .then(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -561,7 +561,6 @@ class ConcordanceQueryBuilder {
 | 
				
			|||||||
      if (tokenIsEmpty === false) {
 | 
					      if (tokenIsEmpty === false) {
 | 
				
			||||||
        tokenQueryText = '[' + tokenQueryText + ']';
 | 
					        tokenQueryText = '[' + tokenQueryText + ']';
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      console.log(tokenQueryText);
 | 
					 | 
				
			||||||
      this.queryChipFactory('token', tokenQueryContent, tokenQueryText);
 | 
					      this.queryChipFactory('token', tokenQueryContent, tokenQueryText);
 | 
				
			||||||
      this.hideEverything();
 | 
					      this.hideEverything();
 | 
				
			||||||
      this.elements.positionalAttrArea.classList.add('hide');
 | 
					      this.elements.positionalAttrArea.classList.add('hide');
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -92,7 +92,6 @@ class Form {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
      if (request.status === 400) {
 | 
					      if (request.status === 400) {
 | 
				
			||||||
        let responseJson = JSON.parse(request.responseText);
 | 
					        let responseJson = JSON.parse(request.responseText);
 | 
				
			||||||
        console.log(responseJson);
 | 
					 | 
				
			||||||
        for (let [inputName, inputErrors] of Object.entries(responseJson.errors)) {
 | 
					        for (let [inputName, inputErrors] of Object.entries(responseJson.errors)) {
 | 
				
			||||||
          let inputFieldElement = this.formElement
 | 
					          let inputFieldElement = this.formElement
 | 
				
			||||||
            .querySelector(`input[name$="${inputName}"], select[name$="${inputName}"]`)
 | 
					            .querySelector(`input[name$="${inputName}"], select[name$="${inputName}"]`)
 | 
				
			||||||
@@ -122,10 +121,11 @@ class Form {
 | 
				
			|||||||
    request.setRequestHeader('Accept', 'application/json');
 | 
					    request.setRequestHeader('Accept', 'application/json');
 | 
				
			||||||
    let formData = new FormData(this.formElement);
 | 
					    let formData = new FormData(this.formElement);
 | 
				
			||||||
    switch (this.formElement.enctype) {
 | 
					    switch (this.formElement.enctype) {
 | 
				
			||||||
      case 'application/x-www-form-urlencoded':
 | 
					      case 'application/x-www-form-urlencoded': {
 | 
				
			||||||
        let urlSearchParams = new URLSearchParams(formData);
 | 
					        let urlSearchParams = new URLSearchParams(formData);
 | 
				
			||||||
        request.send(urlSearchParams);
 | 
					        request.send(urlSearchParams);
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      case 'multipart/form-data': {
 | 
					      case 'multipart/form-data': {
 | 
				
			||||||
        request.send(formData);
 | 
					        request.send(formData);
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										40
									
								
								app/static/js/Requests/Requests.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								app/static/js/Requests/Requests.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
				
			|||||||
 | 
					let Requests = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.JSONfetch = (input, init={}) => {
 | 
				
			||||||
 | 
					  return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					    let fixedInit = {};
 | 
				
			||||||
 | 
					    fixedInit.headers = {};
 | 
				
			||||||
 | 
					    fixedInit.headers['Accept'] = 'application/json';
 | 
				
			||||||
 | 
					    if (init.hasOwnProperty('body')) {
 | 
				
			||||||
 | 
					      fixedInit.headers['Content-Type'] = 'application/json';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    fetch(input, Utils.mergeObjectsDeep(init, fixedInit))
 | 
				
			||||||
 | 
					      .then(
 | 
				
			||||||
 | 
					        (response) => {
 | 
				
			||||||
 | 
					          if (response.ok) {
 | 
				
			||||||
 | 
					            resolve(response.clone());
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            reject(response);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          if (response.status === 204) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					          } 
 | 
				
			||||||
 | 
					          response.json()
 | 
				
			||||||
 | 
					            .then(
 | 
				
			||||||
 | 
					              (json) => {
 | 
				
			||||||
 | 
					                let message = json.message || json;
 | 
				
			||||||
 | 
					                let category = json.category || 'message';
 | 
				
			||||||
 | 
					                app.flash(message, category);
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              (error) => {
 | 
				
			||||||
 | 
					                app.flash(`[${response.status}]: ${response.statusText}`, 'error');
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        (response) => {
 | 
				
			||||||
 | 
					          app.flash('Something went wrong', 'error');
 | 
				
			||||||
 | 
					          reject(response);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										20
									
								
								app/static/js/Requests/admin/admin.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								app/static/js/Requests/admin/admin.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					/*****************************************************************************
 | 
				
			||||||
 | 
					* Admin                                                                      *
 | 
				
			||||||
 | 
					* Fetch requests for /admin routes                                           *
 | 
				
			||||||
 | 
					*****************************************************************************/
 | 
				
			||||||
 | 
					Requests.admin = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.admin.users = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.admin.users.entity = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.admin.users.entity.confirmed = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.admin.users.entity.confirmed.update = (userId, value) => {
 | 
				
			||||||
 | 
					  let input = `/admin/users/${userId}/confirmed`;
 | 
				
			||||||
 | 
					  let init = {
 | 
				
			||||||
 | 
					    method: 'PUT',
 | 
				
			||||||
 | 
					    body: JSON.stringify(value)
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  return Requests.JSONfetch(input, init);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										5
									
								
								app/static/js/Requests/contributions/contributions.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/static/js/Requests/contributions/contributions.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					/*****************************************************************************
 | 
				
			||||||
 | 
					* Contributions                                                              *
 | 
				
			||||||
 | 
					* Fetch requests for /contributions routes                                   *
 | 
				
			||||||
 | 
					*****************************************************************************/
 | 
				
			||||||
 | 
					Requests.contributions = {};
 | 
				
			||||||
@@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					/*****************************************************************************
 | 
				
			||||||
 | 
					* SpaCy NLP Pipeline Models                                                  *
 | 
				
			||||||
 | 
					* Fetch requests for /contributions/spacy-nlp-pipeline-models routes         *
 | 
				
			||||||
 | 
					*****************************************************************************/
 | 
				
			||||||
 | 
					Requests.contributions.spacy_nlp_pipeline_models = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.contributions.spacy_nlp_pipeline_models.entity = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.contributions.spacy_nlp_pipeline_models.entity.delete = (spacyNlpPipelineModelId) => {
 | 
				
			||||||
 | 
					  let input = `/contributions/spacy-nlp-pipeline-models/${spacyNlpPipelineModelId}`;
 | 
				
			||||||
 | 
					  let init = {
 | 
				
			||||||
 | 
					    method: 'DELETE'
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  return Requests.JSONfetch(input, init);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.contributions.spacy_nlp_pipeline_models.entity.isPublic = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.contributions.spacy_nlp_pipeline_models.entity.isPublic.update = (spacyNlpPipelineModelId, value) => {
 | 
				
			||||||
 | 
					  let input = `/contributions/spacy-nlp-pipeline-models/${spacyNlpPipelineModelId}/is_public`;
 | 
				
			||||||
 | 
					  let init = {
 | 
				
			||||||
 | 
					    method: 'PUT',
 | 
				
			||||||
 | 
					    body: JSON.stringify(value)
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  return Requests.JSONfetch(input, init);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					/*****************************************************************************
 | 
				
			||||||
 | 
					* Tesseract OCR Pipeline Models                                              *
 | 
				
			||||||
 | 
					* Fetch requests for /contributions/tesseract-ocr-pipeline-models routes     *
 | 
				
			||||||
 | 
					*****************************************************************************/
 | 
				
			||||||
 | 
					Requests.contributions.tesseract_ocr_pipeline_models = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.contributions.tesseract_ocr_pipeline_models.entity = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.contributions.tesseract_ocr_pipeline_models.entity.delete = (tesseractOcrPipelineModelId) => {
 | 
				
			||||||
 | 
					  let input = `/contributions/tesseract-ocr-pipeline-models/${tesseractOcrPipelineModelId}`;
 | 
				
			||||||
 | 
					  let init = {
 | 
				
			||||||
 | 
					    method: 'DELETE'
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  return Requests.JSONfetch(input, init);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.contributions.tesseract_ocr_pipeline_models.entity.isPublic = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.contributions.tesseract_ocr_pipeline_models.entity.isPublic.update = (tesseractOcrPipelineModelId, value) => {
 | 
				
			||||||
 | 
					  let input = `/contributions/tesseract-ocr-pipeline-models/${tesseractOcrPipelineModelId}/is_public`;
 | 
				
			||||||
 | 
					  let init = {
 | 
				
			||||||
 | 
					    method: 'PUT',
 | 
				
			||||||
 | 
					    body: JSON.stringify(value)
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  return Requests.JSONfetch(input, init);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										46
									
								
								app/static/js/Requests/corpora/corpora.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								app/static/js/Requests/corpora/corpora.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					/*****************************************************************************
 | 
				
			||||||
 | 
					* Corpora                                                                    *
 | 
				
			||||||
 | 
					* Fetch requests for /corpora routes                                         *
 | 
				
			||||||
 | 
					*****************************************************************************/
 | 
				
			||||||
 | 
					Requests.corpora = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.corpora.entity = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.corpora.entity.delete = (corpusId) => {
 | 
				
			||||||
 | 
					  let input = `/corpora/${corpusId}`;
 | 
				
			||||||
 | 
					  let init = {
 | 
				
			||||||
 | 
					    method: 'DELETE'
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  return Requests.JSONfetch(input, init);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.corpora.entity.build = (corpusId) => {
 | 
				
			||||||
 | 
					  let input = `/corpora/${corpusId}/build`;
 | 
				
			||||||
 | 
					  let init = {
 | 
				
			||||||
 | 
					    method: 'POST',
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  return Requests.JSONfetch(input, init);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.corpora.entity.generateShareLink = (corpusId, role, expiration) => {
 | 
				
			||||||
 | 
					  let input = `/corpora/${corpusId}/generate-share-link`;
 | 
				
			||||||
 | 
					  let init = {
 | 
				
			||||||
 | 
					    method: 'POST',
 | 
				
			||||||
 | 
					    body: JSON.stringify({role: role, expiration: expiration})
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  return Requests.JSONfetch(input, init);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.corpora.entity.isPublic = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.corpora.entity.isPublic.update = (corpusId, isPublic) => {
 | 
				
			||||||
 | 
					  let input = `/corpora/${corpusId}/is_public`;
 | 
				
			||||||
 | 
					  let init = {
 | 
				
			||||||
 | 
					    method: 'PUT',
 | 
				
			||||||
 | 
					    body: JSON.stringify(isPublic)
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  return Requests.JSONfetch(input, init);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										15
									
								
								app/static/js/Requests/corpora/files.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								app/static/js/Requests/corpora/files.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					/*****************************************************************************
 | 
				
			||||||
 | 
					* Corpora                                                                    *
 | 
				
			||||||
 | 
					* Fetch requests for /corpora/<entity>/files routes                          *
 | 
				
			||||||
 | 
					*****************************************************************************/
 | 
				
			||||||
 | 
					Requests.corpora.entity.files = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.corpora.entity.files.ent = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.corpora.entity.files.ent.delete = (corpusId, corpusFileId) => {
 | 
				
			||||||
 | 
					  let input = `/corpora/${corpusId}/files/${corpusFileId}`;
 | 
				
			||||||
 | 
					  let init = {
 | 
				
			||||||
 | 
					    method: 'DELETE',
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  return Requests.JSONfetch(input, init);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										35
									
								
								app/static/js/Requests/corpora/followers.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								app/static/js/Requests/corpora/followers.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					/*****************************************************************************
 | 
				
			||||||
 | 
					* Corpora                                                                    *
 | 
				
			||||||
 | 
					* Fetch requests for /corpora/<entity>/followers routes                      *
 | 
				
			||||||
 | 
					*****************************************************************************/
 | 
				
			||||||
 | 
					Requests.corpora.entity.followers = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.corpora.entity.followers.add = (corpusId, usernames) => {
 | 
				
			||||||
 | 
					  let input = `/corpora/${corpusId}/followers`;
 | 
				
			||||||
 | 
					  let init = {
 | 
				
			||||||
 | 
					    method: 'POST',
 | 
				
			||||||
 | 
					    body: JSON.stringify(usernames)
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  return Requests.JSONfetch(input, init);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.corpora.entity.followers.entity = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.corpora.entity.followers.entity.delete = (corpusId, followerId) => {
 | 
				
			||||||
 | 
					  let input = `/corpora/${corpusId}/followers/${followerId}`;
 | 
				
			||||||
 | 
					  let init = {
 | 
				
			||||||
 | 
					    method: 'DELETE',
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  return Requests.JSONfetch(input, init);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.corpora.entity.followers.entity.role = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.corpora.entity.followers.entity.role.update = (corpusId, followerId, value) => {
 | 
				
			||||||
 | 
					  let input = `/corpora/${corpusId}/followers/${followerId}/role`;
 | 
				
			||||||
 | 
					  let init = {
 | 
				
			||||||
 | 
					    method: 'PUT',
 | 
				
			||||||
 | 
					    body: JSON.stringify(value)
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  return Requests.JSONfetch(input, init);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										31
									
								
								app/static/js/Requests/jobs/jobs.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								app/static/js/Requests/jobs/jobs.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					/*****************************************************************************
 | 
				
			||||||
 | 
					* Jobs                                                              *
 | 
				
			||||||
 | 
					* Fetch requests for /jobs routes                                   *
 | 
				
			||||||
 | 
					*****************************************************************************/
 | 
				
			||||||
 | 
					Requests.jobs = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.jobs.entity = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.jobs.entity.delete = (jobId) => {
 | 
				
			||||||
 | 
					  let input = `/jobs/${jobId}`;
 | 
				
			||||||
 | 
					  let init = {
 | 
				
			||||||
 | 
					    method: 'DELETE'
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  return Requests.JSONfetch(input, init);
 | 
				
			||||||
 | 
					} 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.jobs.entity.log = (jobId) => {
 | 
				
			||||||
 | 
					  let input = `/jobs/${jobId}/log`;
 | 
				
			||||||
 | 
					  let init = {
 | 
				
			||||||
 | 
					    method: 'GET'
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  return Requests.JSONfetch(input, init);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.jobs.entity.restart = (jobId) => {
 | 
				
			||||||
 | 
					  let input = `/jobs/${jobId}/restart`;
 | 
				
			||||||
 | 
					  let init = {
 | 
				
			||||||
 | 
					    method: 'POST'
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  return Requests.JSONfetch(input, init);
 | 
				
			||||||
 | 
					} 
 | 
				
			||||||
							
								
								
									
										17
									
								
								app/static/js/Requests/users/settings.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/static/js/Requests/users/settings.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					/*****************************************************************************
 | 
				
			||||||
 | 
					* Settings                                                                   *
 | 
				
			||||||
 | 
					* Fetch requests for /users/<entity>/settings routes                         *
 | 
				
			||||||
 | 
					*****************************************************************************/
 | 
				
			||||||
 | 
					Requests.users.entity.settings = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.users.entity.settings.profilePrivacy = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.users.entity.settings.profilePrivacy.update = (userId, profilePrivacySetting, enabled) => {
 | 
				
			||||||
 | 
					  let input = `/users/${userId}/settings/profile-privacy/${profilePrivacySetting}`;
 | 
				
			||||||
 | 
					  let init = {
 | 
				
			||||||
 | 
					    method: 'PUT',
 | 
				
			||||||
 | 
					    body: JSON.stringify(enabled)
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  return Requests.JSONfetch(input, init);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										35
									
								
								app/static/js/Requests/users/users.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								app/static/js/Requests/users/users.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					/*****************************************************************************
 | 
				
			||||||
 | 
					* Users                                                                      *
 | 
				
			||||||
 | 
					* Fetch requests for /users routes                                           *
 | 
				
			||||||
 | 
					*****************************************************************************/
 | 
				
			||||||
 | 
					Requests.users = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.users.entity = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.users.entity.delete = (userId) => {
 | 
				
			||||||
 | 
					  let input = `/users/${userId}`;
 | 
				
			||||||
 | 
					  let init = {
 | 
				
			||||||
 | 
					    method: 'DELETE'
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  return Requests.JSONfetch(input, init);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.users.entity.acceptTermsOfUse = () => {
 | 
				
			||||||
 | 
					  let input = `/users/accept-terms-of-use`;
 | 
				
			||||||
 | 
					  let init = {
 | 
				
			||||||
 | 
					    method: 'POST'
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  return Requests.JSONfetch(input, init);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.users.entity.avatar = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requests.users.entity.avatar.delete = (userId) => {
 | 
				
			||||||
 | 
					  let input = `/users/${userId}/avatar`;
 | 
				
			||||||
 | 
					  let init = {
 | 
				
			||||||
 | 
					    method: 'DELETE'
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  return Requests.JSONfetch(input, init);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1,26 +1,11 @@
 | 
				
			|||||||
class CorpusDisplay extends RessourceDisplay {
 | 
					class CorpusDisplay extends ResourceDisplay {
 | 
				
			||||||
  constructor(displayElement) {
 | 
					  constructor(displayElement) {
 | 
				
			||||||
    super(displayElement);
 | 
					    super(displayElement);
 | 
				
			||||||
    this.corpusId = displayElement.dataset.corpusId;
 | 
					    this.corpusId = displayElement.dataset.corpusId;
 | 
				
			||||||
    this.displayElement
 | 
					    this.displayElement
 | 
				
			||||||
      .querySelector('.action-button[data-action="build-request"]')
 | 
					      .querySelector('.action-button[data-action="build-request"]')
 | 
				
			||||||
      .addEventListener('click', (event) => {
 | 
					      .addEventListener('click', (event) => {
 | 
				
			||||||
        Utils.buildCorpusRequest(this.userId, this.corpusId);
 | 
					        Requests.corpora.entity.build(this.corpusId);
 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    this.displayElement
 | 
					 | 
				
			||||||
      .querySelector('.action-button[data-action="delete-request"]')
 | 
					 | 
				
			||||||
      .addEventListener('click', (event) => {
 | 
					 | 
				
			||||||
        Utils.deleteCorpusRequest(this.userId, this.corpusId);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    this.displayElement
 | 
					 | 
				
			||||||
      .querySelector('.action-switch[data-action="toggle-is-public"]')
 | 
					 | 
				
			||||||
      .addEventListener('click', (event) => {
 | 
					 | 
				
			||||||
        if (event.target.tagName !== 'INPUT') {return;}
 | 
					 | 
				
			||||||
        if (event.target.checked) {
 | 
					 | 
				
			||||||
          Utils.enableCorpusIsPublicRequest(this.userId, this.corpusId);
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
          Utils.disableCorpusIsPublicRequest(this.userId, this.corpusId);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -81,7 +66,7 @@ class CorpusDisplay extends RessourceDisplay {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  setStatus(status) {
 | 
					  setStatus(status) {
 | 
				
			||||||
    let elements = this.displayElement.querySelectorAll('.corpus-analyse-trigger')
 | 
					    let elements = this.displayElement.querySelectorAll('.action-button[data-action="analyze"]');
 | 
				
			||||||
    for (let element of elements) {
 | 
					    for (let element of elements) {
 | 
				
			||||||
      if (['BUILT', 'STARTING_ANALYSIS_SESSION', 'RUNNING_ANALYSIS_SESSION', 'CANCELING_ANALYSIS_SESSION'].includes(status)) {
 | 
					      if (['BUILT', 'STARTING_ANALYSIS_SESSION', 'RUNNING_ANALYSIS_SESSION', 'CANCELING_ANALYSIS_SESSION'].includes(status)) {
 | 
				
			||||||
        element.classList.remove('disabled');
 | 
					        element.classList.remove('disabled');
 | 
				
			||||||
@@ -1,22 +1,7 @@
 | 
				
			|||||||
class JobDisplay extends RessourceDisplay {
 | 
					class JobDisplay extends ResourceDisplay {
 | 
				
			||||||
  constructor(displayElement) {
 | 
					  constructor(displayElement) {
 | 
				
			||||||
    super(displayElement);
 | 
					    super(displayElement);
 | 
				
			||||||
    this.jobId = this.displayElement.dataset.jobId;
 | 
					    this.jobId = this.displayElement.dataset.jobId;
 | 
				
			||||||
    this.displayElement
 | 
					 | 
				
			||||||
      .querySelector('.action-button[data-action="delete-request"]')
 | 
					 | 
				
			||||||
      .addEventListener('click', (event) => {
 | 
					 | 
				
			||||||
        Utils.deleteJobRequest(this.userId, this.jobId);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    this.displayElement
 | 
					 | 
				
			||||||
      .querySelector('.action-button[data-action="get-log-request"]')
 | 
					 | 
				
			||||||
      .addEventListener('click', (event) => {
 | 
					 | 
				
			||||||
        Utils.getJobLogRequest(this.userId, this.jobId);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    this.displayElement
 | 
					 | 
				
			||||||
      .querySelector('.action-button[data-action="restart-request"]')
 | 
					 | 
				
			||||||
      .addEventListener('click', (event) => {
 | 
					 | 
				
			||||||
        Utils.restartJobRequest(this.userId, this.jobId);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  init(user) {
 | 
					  init(user) {
 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
class RessourceDisplay {
 | 
					class ResourceDisplay {
 | 
				
			||||||
  constructor(displayElement) {
 | 
					  constructor(displayElement) {
 | 
				
			||||||
    this.displayElement = displayElement;
 | 
					    this.displayElement = displayElement;
 | 
				
			||||||
    this.userId = this.displayElement.dataset.userId;
 | 
					    this.userId = this.displayElement.dataset.userId;
 | 
				
			||||||
@@ -8,9 +8,15 @@ class CorpusFileList extends ResourceList {
 | 
				
			|||||||
  constructor(listContainerElement, options = {}) {
 | 
					  constructor(listContainerElement, options = {}) {
 | 
				
			||||||
    super(listContainerElement, options);
 | 
					    super(listContainerElement, options);
 | 
				
			||||||
    this.listjs.list.addEventListener('click', (event) => {this.onClick(event)});
 | 
					    this.listjs.list.addEventListener('click', (event) => {this.onClick(event)});
 | 
				
			||||||
 | 
					    document.querySelectorAll('.selection-action-trigger[data-selection-action]').forEach((element) => {
 | 
				
			||||||
 | 
					      element.addEventListener('click', (event) => {this.onSelectionAction(event)});
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
    this.isInitialized = false;
 | 
					    this.isInitialized = false;
 | 
				
			||||||
 | 
					    this.selectedItemIds = new Set();
 | 
				
			||||||
    this.userId = listContainerElement.dataset.userId;
 | 
					    this.userId = listContainerElement.dataset.userId;
 | 
				
			||||||
    this.corpusId = listContainerElement.dataset.corpusId;
 | 
					    this.corpusId = listContainerElement.dataset.corpusId;
 | 
				
			||||||
 | 
					    this.hasPermissionView =  listContainerElement.dataset?.hasPermissionView == 'true' || false;
 | 
				
			||||||
 | 
					    this.hasPermissionManageFiles =  listContainerElement.dataset?.hasPermissionManageFiles == 'true' || false;
 | 
				
			||||||
    if (this.userId === undefined || this.corpusId === undefined) {return;}
 | 
					    if (this.userId === undefined || this.corpusId === undefined) {return;}
 | 
				
			||||||
    app.subscribeUser(this.userId).then((response) => {
 | 
					    app.subscribeUser(this.userId).then((response) => {
 | 
				
			||||||
      app.socket.on('PATCH', (patch) => {
 | 
					      app.socket.on('PATCH', (patch) => {
 | 
				
			||||||
@@ -24,20 +30,28 @@ class CorpusFileList extends ResourceList {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get item() {
 | 
					  get item() {
 | 
				
			||||||
 | 
					    return (values) => {
 | 
				
			||||||
      return `
 | 
					      return `
 | 
				
			||||||
        <tr class="list-item clickable hoverable">
 | 
					        <tr class="list-item clickable hoverable">
 | 
				
			||||||
 | 
					          <td>
 | 
				
			||||||
 | 
					            <label class="list-action-trigger ${this.hasPermissionView ? '' : 'hide'}" data-list-action="select">
 | 
				
			||||||
 | 
					              <input class="select-checkbox" type="checkbox">
 | 
				
			||||||
 | 
					              <span class="disable-on-click"></span>
 | 
				
			||||||
 | 
					            </label>
 | 
				
			||||||
 | 
					          </td>
 | 
				
			||||||
          <td><span class="filename"></span></td>
 | 
					          <td><span class="filename"></span></td>
 | 
				
			||||||
          <td><span class="author"></span></td>
 | 
					          <td><span class="author"></span></td>
 | 
				
			||||||
          <td><span class="title"></span></td>
 | 
					          <td><span class="title"></span></td>
 | 
				
			||||||
          <td><span class="publishing-year"></span></td>
 | 
					          <td><span class="publishing-year"></span></td>
 | 
				
			||||||
          <td class="right-align">
 | 
					          <td class="right-align">
 | 
				
			||||||
          <a class="list-action-trigger btn-floating red waves-effect waves-light" data-list-action="delete"><i class="material-icons">delete</i></a>
 | 
					            <a class="list-action-trigger btn-floating red waves-effect waves-light ${this.hasPermissionManageFiles ? '' : 'hide'}" data-list-action="delete"><i class="material-icons">delete</i></a>
 | 
				
			||||||
          <a class="list-action-trigger btn-floating service-color darken waves-effect waves-light" data-list-action="download" data-service="corpus-analysis"><i class="material-icons">file_download</i></a>
 | 
					            <a class="list-action-trigger btn-floating service-color darken waves-effect waves-light ${this.hasPermissionView ? '' : 'hide'}" data-list-action="download" data-service="corpus-analysis"><i class="material-icons">file_download</i></a>
 | 
				
			||||||
          <a class="list-action-trigger btn-floating service-color darken waves-effect waves-light" data-list-action="view" data-service="corpus-analysis"><i class="material-icons">send</i></a>
 | 
					            <a class="list-action-trigger btn-floating service-color darken waves-effect waves-light ${this.hasPermissionManageFiles ? '' : 'hide'}" data-list-action="view" data-service="corpus-analysis"><i class="material-icons">send</i></a>
 | 
				
			||||||
          </td>
 | 
					          </td>
 | 
				
			||||||
        </tr>
 | 
					        </tr>
 | 
				
			||||||
      `.trim();
 | 
					      `.trim();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get valueNames() {
 | 
					  get valueNames() {
 | 
				
			||||||
    return [
 | 
					    return [
 | 
				
			||||||
@@ -64,11 +78,20 @@ class CorpusFileList extends ResourceList {
 | 
				
			|||||||
      <table>
 | 
					      <table>
 | 
				
			||||||
        <thead>
 | 
					        <thead>
 | 
				
			||||||
          <tr>
 | 
					          <tr>
 | 
				
			||||||
 | 
					            <th>
 | 
				
			||||||
 | 
					              <label class="disable-on-click selection-action-trigger ${this.listContainerElement.dataset?.hasPermissionView == 'true' ? '' : 'hide'}" data-selection-action="select-all">
 | 
				
			||||||
 | 
					                <input class="select-all-checkbox" type="checkbox">
 | 
				
			||||||
 | 
					                <span class="disable-on-click"></span>
 | 
				
			||||||
 | 
					              </label>
 | 
				
			||||||
 | 
					            </th>
 | 
				
			||||||
            <th>Filename</th>
 | 
					            <th>Filename</th>
 | 
				
			||||||
            <th>Author</th>
 | 
					            <th>Author</th>
 | 
				
			||||||
            <th>Title</th>
 | 
					            <th>Title</th>
 | 
				
			||||||
            <th>Publishing year</th>
 | 
					            <th>Publishing year</th>
 | 
				
			||||||
            <th></th>
 | 
					            <th class="right-align">
 | 
				
			||||||
 | 
					              <a class="selection-action-trigger btn-floating red waves-effect waves-light hide" data-selection-action="delete"><i class="material-icons">delete</i></a>
 | 
				
			||||||
 | 
					              <a class="selection-action-trigger btn-floating service-color darken waves-effect waves-light hide" data-selection-action="download" data-service="corpus-analysis"><i class="material-icons">file_download</i></a>
 | 
				
			||||||
 | 
					            </th>
 | 
				
			||||||
          </tr>
 | 
					          </tr>
 | 
				
			||||||
        </thead>
 | 
					        </thead>
 | 
				
			||||||
        <tbody class="list"></tbody>
 | 
					        <tbody class="list"></tbody>
 | 
				
			||||||
@@ -93,6 +116,7 @@ class CorpusFileList extends ResourceList {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onClick(event) {
 | 
					  onClick(event) {
 | 
				
			||||||
 | 
					    if (event.target.closest('.disable-on-click') !== null) {return;}
 | 
				
			||||||
    let listItemElement = event.target.closest('.list-item[data-id]');
 | 
					    let listItemElement = event.target.closest('.list-item[data-id]');
 | 
				
			||||||
    if (listItemElement === null) {return;}
 | 
					    if (listItemElement === null) {return;}
 | 
				
			||||||
    let itemId = listItemElement.dataset.id;
 | 
					    let itemId = listItemElement.dataset.id;
 | 
				
			||||||
@@ -100,7 +124,44 @@ class CorpusFileList extends ResourceList {
 | 
				
			|||||||
    let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction;
 | 
					    let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction;
 | 
				
			||||||
    switch (listAction) {
 | 
					    switch (listAction) {
 | 
				
			||||||
      case 'delete': {
 | 
					      case 'delete': {
 | 
				
			||||||
        Utils.deleteCorpusFileRequest(this.userId, this.corpusId, itemId);
 | 
					        let values = this.listjs.get('id', itemId)[0].values();
 | 
				
			||||||
 | 
					        let modalElement = Utils.HTMLToElement(
 | 
				
			||||||
 | 
					          `
 | 
				
			||||||
 | 
					            <div class="modal">
 | 
				
			||||||
 | 
					              <div class="modal-content">
 | 
				
			||||||
 | 
					                <h4>Confirm Corpus File deletion</h4>
 | 
				
			||||||
 | 
					                <p>Do you really want to delete the Corpus File <b>${values.title}</b>? All files will be permanently deleted!</p>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <div class="modal-footer">
 | 
				
			||||||
 | 
					                <a class="btn modal-close waves-effect waves-light">Cancel</a>
 | 
				
			||||||
 | 
					                <a class="action-button btn modal-close red waves-effect waves-light" data-action="confirm">Delete</a>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          `
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        document.querySelector('#modals').appendChild(modalElement);
 | 
				
			||||||
 | 
					        let modal = M.Modal.init(
 | 
				
			||||||
 | 
					          modalElement,
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            dismissible: false,
 | 
				
			||||||
 | 
					            onCloseEnd: () => {
 | 
				
			||||||
 | 
					              modal.destroy();
 | 
				
			||||||
 | 
					              modalElement.remove();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]');
 | 
				
			||||||
 | 
					        confirmElement.addEventListener('click', (event) => {
 | 
				
			||||||
 | 
					          if (currentUserId != this.userId) {
 | 
				
			||||||
 | 
					            Requests.corpora.entity.files.ent.delete(this.corpusId, itemId)
 | 
				
			||||||
 | 
					            .then(() => {
 | 
				
			||||||
 | 
					              window.location.reload();
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            Requests.corpora.entity.files.ent.delete(this.corpusId, itemId)
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        modal.open();
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      case 'download': {
 | 
					      case 'download': {
 | 
				
			||||||
@@ -111,12 +172,171 @@ class CorpusFileList extends ResourceList {
 | 
				
			|||||||
        window.location.href = `/corpora/${this.corpusId}/files/${itemId}`;
 | 
					        window.location.href = `/corpora/${this.corpusId}/files/${itemId}`;
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      case 'select': {
 | 
				
			||||||
 | 
					        if (event.target.checked) {
 | 
				
			||||||
 | 
					          this.selectedItemIds.add(itemId);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.selectedItemIds.delete(itemId);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.renderingItemSelection();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      default: {
 | 
					      default: {
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onSelectionAction(event) {
 | 
				
			||||||
 | 
					    let selectionActionElement = event.target.closest('.selection-action-trigger[data-selection-action]');
 | 
				
			||||||
 | 
					    let selectionAction = selectionActionElement.dataset.selectionAction;
 | 
				
			||||||
 | 
					    let items = this.listjs.items;
 | 
				
			||||||
 | 
					    let selectableItems = Array.from(items)
 | 
				
			||||||
 | 
					      .filter(item => item.elm)
 | 
				
			||||||
 | 
					      .map(item => item.elm.querySelector('.select-checkbox[type="checkbox"]'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    switch (selectionAction) {
 | 
				
			||||||
 | 
					      case 'select-all': {
 | 
				
			||||||
 | 
					        let selectedIds = new Set(Array.from(items)
 | 
				
			||||||
 | 
					          .map(item => item.values().id))
 | 
				
			||||||
 | 
					        if (event.target.checked !== undefined) {
 | 
				
			||||||
 | 
					          if (event.target.checked) {
 | 
				
			||||||
 | 
					            selectableItems.forEach(selectableItem => selectableItem.checked = true);
 | 
				
			||||||
 | 
					            this.selectedItemIds = selectedIds;
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            selectableItems.forEach(checkbox => checkbox.checked = false);
 | 
				
			||||||
 | 
					            this.selectedItemIds = new Set([...this.selectedItemIds].filter(id => !selectedIds.has(id)));
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          this.renderingItemSelection();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      case 'delete': {
 | 
				
			||||||
 | 
					        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 Files?</p>
 | 
				
			||||||
 | 
					                  <ul id="selected-items-list"></ul>
 | 
				
			||||||
 | 
					                <p>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 itemList = document.querySelector('#selected-items-list');
 | 
				
			||||||
 | 
					        this.selectedItemIds.forEach(selectedItemId => {
 | 
				
			||||||
 | 
					          let listItem = this.listjs.get('id', selectedItemId)[0].elm;
 | 
				
			||||||
 | 
					          let values = this.listjs.get('id', listItem.dataset.id)[0].values();
 | 
				
			||||||
 | 
					          let itemElement = Utils.HTMLToElement(`<li> - ${values.title}</li>`);
 | 
				
			||||||
 | 
					          itemList.appendChild(itemElement);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        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) => {
 | 
				
			||||||
 | 
					          this.selectedItemIds.forEach(selectedItemId => {
 | 
				
			||||||
 | 
					            if (currentUserId != this.userId) {
 | 
				
			||||||
 | 
					              Requests.corpora.entity.files.ent.delete(this.corpusId, selectedItemId)
 | 
				
			||||||
 | 
					              .then(() => {
 | 
				
			||||||
 | 
					                window.location.reload();
 | 
				
			||||||
 | 
					              });
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              Requests.corpora.entity.files.ent.delete(this.corpusId, selectedItemId);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          this.selectedItemIds.clear();
 | 
				
			||||||
 | 
					          this.renderingItemSelection();
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        modal.open();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      case 'download': {
 | 
				
			||||||
 | 
					        this.selectedItemIds.forEach(selectedItemId => {
 | 
				
			||||||
 | 
					          let downloadLink = document.createElement('a');
 | 
				
			||||||
 | 
					          downloadLink.href = `/corpora/${this.corpusId}/files/${selectedItemId}/download`;
 | 
				
			||||||
 | 
					          downloadLink.download = '';
 | 
				
			||||||
 | 
					          downloadLink.click();
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        selectableItems.forEach(checkbox => checkbox.checked = false);
 | 
				
			||||||
 | 
					        this.selectedItemIds.clear();
 | 
				
			||||||
 | 
					        this.renderingItemSelection();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }  
 | 
				
			||||||
 | 
					      default: {
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  renderingItemSelection() {
 | 
				
			||||||
 | 
					    let selectionActionButtons;
 | 
				
			||||||
 | 
					    if (this.hasPermissionManageFiles) {
 | 
				
			||||||
 | 
					      selectionActionButtons = document.querySelectorAll('.selection-action-trigger:not([data-selection-action="select-all"])');
 | 
				
			||||||
 | 
					    } else if (this.hasPermissionView) {
 | 
				
			||||||
 | 
					      selectionActionButtons = document.querySelectorAll('.selection-action-trigger:not([data-selection-action="select-all"]):not([data-selection-action="delete"])');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    let selectableItems = this.listjs.items;
 | 
				
			||||||
 | 
					    let actionButtons = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Object.values(selectableItems).forEach(selectableItem => {
 | 
				
			||||||
 | 
					      if (selectableItem.elm) {
 | 
				
			||||||
 | 
					        let checkbox = selectableItem.elm.querySelector('.select-checkbox[type="checkbox"]');
 | 
				
			||||||
 | 
					        if (checkbox.checked) {
 | 
				
			||||||
 | 
					          selectableItem.elm.classList.add('grey', 'lighten-3');
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          selectableItem.elm.classList.remove('grey', 'lighten-3');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        let itemActionButtons = [];
 | 
				
			||||||
 | 
					        if (this.hasPermissionManageFiles) {
 | 
				
			||||||
 | 
					          itemActionButtons = selectableItem.elm.querySelectorAll('.list-action-trigger:not([data-list-action="select"])');
 | 
				
			||||||
 | 
					        } else if (this.hasPermissionView) {
 | 
				
			||||||
 | 
					          itemActionButtons = selectableItem.elm.querySelectorAll('.list-action-trigger:not([data-list-action="select"]):not([data-list-action="delete"]):not([data-list-action="view"])');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        itemActionButtons.forEach(itemActionButton => {
 | 
				
			||||||
 | 
					          actionButtons.push(itemActionButton);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    // Hide item action buttons if > 0 item is selected and show selection action buttons
 | 
				
			||||||
 | 
					    if (this.selectedItemIds.size > 0) {
 | 
				
			||||||
 | 
					      selectionActionButtons.forEach(selectionActionButton => {
 | 
				
			||||||
 | 
					        selectionActionButton.classList.remove('hide');
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      actionButtons.forEach(actionButton => {
 | 
				
			||||||
 | 
					        actionButton.classList.add('hide');
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      selectionActionButtons.forEach(selectionActionButton => {
 | 
				
			||||||
 | 
					        selectionActionButton.classList.add('hide');
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      actionButtons.forEach(actionButton => {
 | 
				
			||||||
 | 
					        actionButton.classList.remove('hide');
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Check select all checkbox if all items are selected
 | 
				
			||||||
 | 
					    let selectAllCheckbox = document.querySelector('.select-all-checkbox[type="checkbox"]');
 | 
				
			||||||
 | 
					    if (selectableItems.length === this.selectedItemIds.size && selectAllCheckbox.checked === false) {
 | 
				
			||||||
 | 
					      selectAllCheckbox.checked = true;
 | 
				
			||||||
 | 
					    } else if (selectableItems.length !== this.selectedItemIds.size && selectAllCheckbox.checked === true) {
 | 
				
			||||||
 | 
					      selectAllCheckbox.checked = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onPatch(patch) {
 | 
					  onPatch(patch) {
 | 
				
			||||||
    let re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/files/([A-Za-z0-9]*)`);
 | 
					    let re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/files/([A-Za-z0-9]*)`);
 | 
				
			||||||
    let filteredPatch = patch.filter(operation => re.test(operation.path));
 | 
					    let filteredPatch = patch.filter(operation => re.test(operation.path));
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										199
									
								
								app/static/js/ResourceLists/CorpusFollowerList.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										199
									
								
								app/static/js/ResourceLists/CorpusFollowerList.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,199 @@
 | 
				
			|||||||
 | 
					class CorpusFollowerList extends ResourceList {
 | 
				
			||||||
 | 
					  static autoInit() {
 | 
				
			||||||
 | 
					    for (let corpusFollowerListElement of document.querySelectorAll('.corpus-follower-list:not(.no-autoinit)')) {
 | 
				
			||||||
 | 
					      new CorpusFollowerList(corpusFollowerListElement);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(listContainerElement, options = {}) {
 | 
				
			||||||
 | 
					    super(listContainerElement, options);
 | 
				
			||||||
 | 
					    this.listjs.on('updated', () => {
 | 
				
			||||||
 | 
					      M.FormSelect.init(this.listjs.list.querySelectorAll('.list-item select'));
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    this.listjs.list.addEventListener('change', (event) => {this.onChange(event)});
 | 
				
			||||||
 | 
					    this.listjs.list.addEventListener('click', (event) => {this.onClick(event)});
 | 
				
			||||||
 | 
					    this.isInitialized = false;
 | 
				
			||||||
 | 
					    this.userId = listContainerElement.dataset.userId;
 | 
				
			||||||
 | 
					    this.corpusId = listContainerElement.dataset.corpusId;
 | 
				
			||||||
 | 
					    if (this.userId === undefined || this.corpusId === undefined) {return;}
 | 
				
			||||||
 | 
					    app.subscribeUser(this.userId).then((response) => {
 | 
				
			||||||
 | 
					      app.socket.on('PATCH', (patch) => {
 | 
				
			||||||
 | 
					        if (this.isInitialized) {this.onPatch(patch);}
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    app.getUser(this.userId).then((user) => {
 | 
				
			||||||
 | 
					      let corpusFollowerAssociations = Object.values(user.corpora[this.corpusId].corpus_follower_associations);
 | 
				
			||||||
 | 
					      // let filteredList = corpusFollowerAssociations.filter(association => association.follower.id != currentUserId);
 | 
				
			||||||
 | 
					      // this.add(filteredList);
 | 
				
			||||||
 | 
					      this.add(Object.values(user.corpora[this.corpusId].corpus_follower_associations));
 | 
				
			||||||
 | 
					      this.isInitialized = true;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get item() {
 | 
				
			||||||
 | 
					    return (values) => {
 | 
				
			||||||
 | 
					      return `
 | 
				
			||||||
 | 
					        <tr class="list-item clickable hoverable">
 | 
				
			||||||
 | 
					          <td><img alt="follower-avatar" class="circle responsive-img follower-avatar" style="width:50%"></td>
 | 
				
			||||||
 | 
					          <td><b class="follower-username"><b></td>
 | 
				
			||||||
 | 
					          <td>
 | 
				
			||||||
 | 
					            <span class="follower-full-name"></span>
 | 
				
			||||||
 | 
					            <br>
 | 
				
			||||||
 | 
					            <i class="follower-about-me"></i>
 | 
				
			||||||
 | 
					          </td>
 | 
				
			||||||
 | 
					          <td>
 | 
				
			||||||
 | 
					            <div class="input-field disable-on-click list-action-trigger" data-list-action="update-role">
 | 
				
			||||||
 | 
					              <select ${values['follower-id'] === currentUserId ? 'disabled' : ''}>
 | 
				
			||||||
 | 
					                <option value="Viewer" ${values['role-name'] === 'Viewer' ? 'selected' : ''}>Viewer</option>
 | 
				
			||||||
 | 
					                <option value="Contributor" ${values['role-name'] === 'Contributor' ? 'selected' : ''}>Contributor</option>
 | 
				
			||||||
 | 
					                <option value="Administrator" ${values['role-name'] === 'Administrator' ? 'selected' : ''}>Administrator</option>
 | 
				
			||||||
 | 
					              </select>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </td>
 | 
				
			||||||
 | 
					          <td class="right-align">
 | 
				
			||||||
 | 
					            <a class="list-action-trigger btn-floating red waves-effect waves-light" data-list-action="unfollow-request"><i class="material-icons">delete</i></a>
 | 
				
			||||||
 | 
					            <a class="list-action-trigger btn-floating darken waves-effect waves-light" data-list-action="view"><i class="material-icons">send</i></a>
 | 
				
			||||||
 | 
					          </td>
 | 
				
			||||||
 | 
					        </tr>
 | 
				
			||||||
 | 
					      `.trim();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get valueNames() {
 | 
				
			||||||
 | 
					    return [
 | 
				
			||||||
 | 
					      {data: ['id']},
 | 
				
			||||||
 | 
					      {data: ['follower-id']},
 | 
				
			||||||
 | 
					      {name: 'follower-avatar', attr: 'src'},
 | 
				
			||||||
 | 
					      'follower-username',
 | 
				
			||||||
 | 
					      'follower-about-me',
 | 
				
			||||||
 | 
					      'follower-full-name'
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  initListContainerElement() {
 | 
				
			||||||
 | 
					    if (!this.listContainerElement.hasAttribute('id')) {
 | 
				
			||||||
 | 
					      this.listContainerElement.id = Utils.generateElementId('corpus-follower-list-');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    let listSearchElementId = Utils.generateElementId(`${this.listContainerElement.id}-search-`);
 | 
				
			||||||
 | 
					    this.listContainerElement.innerHTML = `
 | 
				
			||||||
 | 
					      <div class="input-field">
 | 
				
			||||||
 | 
					        <i class="material-icons prefix">search</i>
 | 
				
			||||||
 | 
					        <input id="${listSearchElementId}" class="search" type="text"></input>
 | 
				
			||||||
 | 
					        <label for="${listSearchElementId}">Search corpus follower</label>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <table>
 | 
				
			||||||
 | 
					        <thead>
 | 
				
			||||||
 | 
					          <tr>
 | 
				
			||||||
 | 
					            <th style="width:15%;"></th>
 | 
				
			||||||
 | 
					            <th>Username</th>
 | 
				
			||||||
 | 
					            <th>User details</th>
 | 
				
			||||||
 | 
					            <th>Role</th>
 | 
				
			||||||
 | 
					            <th></th>
 | 
				
			||||||
 | 
					          </tr>
 | 
				
			||||||
 | 
					        </thead>
 | 
				
			||||||
 | 
					        <tbody class="list"></tbody>
 | 
				
			||||||
 | 
					      </table>
 | 
				
			||||||
 | 
					      <ul class="pagination"></ul>
 | 
				
			||||||
 | 
					    `.trim();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  mapResourceToValue(corpusFollowerAssociation) {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      'id': corpusFollowerAssociation.id,
 | 
				
			||||||
 | 
					      'follower-id': corpusFollowerAssociation.follower.id,
 | 
				
			||||||
 | 
					      'follower-avatar': corpusFollowerAssociation.follower.avatar ? `/users/${corpusFollowerAssociation.follower.id}/avatar` : '/static/images/user_avatar.png',
 | 
				
			||||||
 | 
					      'follower-username': corpusFollowerAssociation.follower.username,
 | 
				
			||||||
 | 
					      'follower-full-name': corpusFollowerAssociation.follower.full_name ? corpusFollowerAssociation.follower.full_name : '',
 | 
				
			||||||
 | 
					      'follower-about-me': corpusFollowerAssociation.follower.about_me ? corpusFollowerAssociation.follower.about_me : '',
 | 
				
			||||||
 | 
					      'role-name': corpusFollowerAssociation.role.name
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  sort() {
 | 
				
			||||||
 | 
					    this.listjs.sort('username', {order: 'desc'});
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onChange(event) {
 | 
				
			||||||
 | 
					    let listItemElement = event.target.closest('.list-item[data-id]');
 | 
				
			||||||
 | 
					    if (listItemElement === null) {return;}
 | 
				
			||||||
 | 
					    let itemId = listItemElement.dataset.id;
 | 
				
			||||||
 | 
					    let listActionElement = event.target.closest('.list-action-trigger[data-list-action]');
 | 
				
			||||||
 | 
					    if (listActionElement === null) {return;}
 | 
				
			||||||
 | 
					    let listAction = listActionElement.dataset.listAction;
 | 
				
			||||||
 | 
					    switch (listAction) {
 | 
				
			||||||
 | 
					      case 'update-role': {
 | 
				
			||||||
 | 
					        let followerId = listItemElement.dataset.followerId;
 | 
				
			||||||
 | 
					        let roleName = event.target.value;
 | 
				
			||||||
 | 
					        Requests.corpora.entity.followers.entity.role.update(this.corpusId, followerId, roleName);
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      default: {
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  onClick(event) {
 | 
				
			||||||
 | 
					    if (event.target.closest('.disable-on-click') !== null) {return;}
 | 
				
			||||||
 | 
					    let listItemElement = event.target.closest('.list-item[data-id]');
 | 
				
			||||||
 | 
					    if (listItemElement === null) {return;}
 | 
				
			||||||
 | 
					    let itemId = listItemElement.dataset.id;
 | 
				
			||||||
 | 
					    let listActionElement = event.target.closest('.list-action-trigger[data-list-action]');
 | 
				
			||||||
 | 
					    let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction;
 | 
				
			||||||
 | 
					    switch (listAction) {
 | 
				
			||||||
 | 
					      case 'unfollow-request': {
 | 
				
			||||||
 | 
					        let followerId = listItemElement.dataset.followerId;
 | 
				
			||||||
 | 
					        if (currentUserId != this.userId) {
 | 
				
			||||||
 | 
					          Requests.corpora.entity.followers.entity.delete(this.corpusId, followerId)
 | 
				
			||||||
 | 
					            .then(() => {
 | 
				
			||||||
 | 
					              window.location.reload();
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          Requests.corpora.entity.followers.entity.delete(this.corpusId, followerId);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      case 'view': {
 | 
				
			||||||
 | 
					        let followerId = listItemElement.dataset.followerId;
 | 
				
			||||||
 | 
					        window.location.href = `/users/${followerId}`;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      default: {
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onPatch(patch) {
 | 
				
			||||||
 | 
					    let re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/corpus_follower_associations/([A-Za-z0-9]*)`);
 | 
				
			||||||
 | 
					    let filteredPatch = patch.filter(operation => re.test(operation.path));
 | 
				
			||||||
 | 
					    for (let operation of filteredPatch) {
 | 
				
			||||||
 | 
					      switch(operation.op) {
 | 
				
			||||||
 | 
					        case 'add': {
 | 
				
			||||||
 | 
					          let re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/corpus_follower_associations/([A-Za-z0-9]*)$`);
 | 
				
			||||||
 | 
					          if (re.test(operation.path)) {this.add(operation.value);}
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        case 'remove': {
 | 
				
			||||||
 | 
					          let re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/corpus_follower_associations/([A-Za-z0-9]*)$`);
 | 
				
			||||||
 | 
					          if (re.test(operation.path)) {
 | 
				
			||||||
 | 
					            let [match, jobId] = operation.path.match(re);
 | 
				
			||||||
 | 
					            this.remove(jobId);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        case 'replace': {
 | 
				
			||||||
 | 
					          let re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/corpus_follower_associations/([A-Za-z0-9]*)/role$`);
 | 
				
			||||||
 | 
					          if (re.test(operation.path)) {
 | 
				
			||||||
 | 
					            let [match, jobId, valueName] = operation.path.match(re);
 | 
				
			||||||
 | 
					            this.replace(jobId, valueName, operation.value);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        default: {
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -8,7 +8,11 @@ class CorpusList extends ResourceList {
 | 
				
			|||||||
  constructor(listContainerElement, options = {}) {
 | 
					  constructor(listContainerElement, options = {}) {
 | 
				
			||||||
    super(listContainerElement, options);
 | 
					    super(listContainerElement, options);
 | 
				
			||||||
    this.listjs.list.addEventListener('click', (event) => {this.onClick(event)});
 | 
					    this.listjs.list.addEventListener('click', (event) => {this.onClick(event)});
 | 
				
			||||||
 | 
					    document.querySelectorAll('.corpus-list-selection-action-trigger[data-selection-action]').forEach((element) => {
 | 
				
			||||||
 | 
					      element.addEventListener('click', (event) => {this.onSelectionAction(event)});
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
    this.isInitialized = false
 | 
					    this.isInitialized = false
 | 
				
			||||||
 | 
					    this.selectedItemIds = new Set();
 | 
				
			||||||
    this.userId = listContainerElement.dataset.userId;
 | 
					    this.userId = listContainerElement.dataset.userId;
 | 
				
			||||||
    if (this.userId === undefined) {return;}
 | 
					    if (this.userId === undefined) {return;}
 | 
				
			||||||
    app.subscribeUser(this.userId).then((response) => {
 | 
					    app.subscribeUser(this.userId).then((response) => {
 | 
				
			||||||
@@ -17,24 +21,66 @@ class CorpusList extends ResourceList {
 | 
				
			|||||||
      });
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    app.getUser(this.userId).then((user) => {
 | 
					    app.getUser(this.userId).then((user) => {
 | 
				
			||||||
      this.add(Object.values(user.corpora));
 | 
					      this.add(this.aggregateData(user));
 | 
				
			||||||
      this.isInitialized = true;
 | 
					      this.isInitialized = true;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  aggregateData(user) {
 | 
				
			||||||
 | 
					    const aggregatedData = [];
 | 
				
			||||||
 | 
					    for (let corpus of Object.values(user.corpora)) {
 | 
				
			||||||
 | 
					      aggregatedData.push(
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          'id': corpus.id,
 | 
				
			||||||
 | 
					          'creation-date': corpus.creation_date,
 | 
				
			||||||
 | 
					          'description': corpus.description,
 | 
				
			||||||
 | 
					          'status': corpus.status,
 | 
				
			||||||
 | 
					          'title': corpus.title,
 | 
				
			||||||
 | 
					          'owner': user.username,
 | 
				
			||||||
 | 
					          'is-owner': true,
 | 
				
			||||||
 | 
					          'current-user-is-following': false
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    for (let cfa of Object.values(user.corpus_follower_associations)) {
 | 
				
			||||||
 | 
					      aggregatedData.push(
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          'id': cfa.corpus.id,
 | 
				
			||||||
 | 
					          'creation-date': cfa.corpus.creation_date,
 | 
				
			||||||
 | 
					          'description': cfa.corpus.description,
 | 
				
			||||||
 | 
					          'status': cfa.corpus.status,
 | 
				
			||||||
 | 
					          'title': cfa.corpus.title,
 | 
				
			||||||
 | 
					          'owner': cfa.corpus.user.username,
 | 
				
			||||||
 | 
					          'is-owner': false,
 | 
				
			||||||
 | 
					          'current-user-is-following': true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return aggregatedData;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // #region Mandatory getters and methods to implement
 | 
					  // #region Mandatory getters and methods to implement
 | 
				
			||||||
  get item() {
 | 
					  get item() {
 | 
				
			||||||
 | 
					    return (values) => {
 | 
				
			||||||
      return `
 | 
					      return `
 | 
				
			||||||
        <tr class="list-item clickable hoverable">
 | 
					        <tr class="list-item clickable hoverable">
 | 
				
			||||||
        <td><a class="btn-floating disabled"><i class="material-icons service-color darken" data-service="corpus-analysis">book</i></a></td>
 | 
					          <td>
 | 
				
			||||||
 | 
					            <label class="list-action-trigger" data-list-action="select">
 | 
				
			||||||
 | 
					              <input class="select-checkbox" type="checkbox">
 | 
				
			||||||
 | 
					              <span class="disable-on-click"></span>
 | 
				
			||||||
 | 
					            </label>
 | 
				
			||||||
 | 
					          </td>
 | 
				
			||||||
          <td><b class="title"></b><br><i class="description"></i></td>
 | 
					          <td><b class="title"></b><br><i class="description"></i></td>
 | 
				
			||||||
 | 
					          <td><span class="owner"></span></td>
 | 
				
			||||||
          <td><span class="status badge new corpus-status-color corpus-status-text" data-badge-caption=""></span></td>
 | 
					          <td><span class="status badge new corpus-status-color corpus-status-text" data-badge-caption=""></span></td>
 | 
				
			||||||
 | 
					          <td>${values['current-user-is-following'] ? '<span><i class="left material-icons">visibility</i>Following</span>' : ''}</td>
 | 
				
			||||||
          <td class="right-align">
 | 
					          <td class="right-align">
 | 
				
			||||||
            <a class="list-action-trigger btn-floating red waves-effect waves-light" data-list-action="delete-request"><i class="material-icons">delete</i></a>
 | 
					            <a class="list-action-trigger btn-floating red waves-effect waves-light" data-list-action="delete-request"><i class="material-icons">delete</i></a>
 | 
				
			||||||
            <a class="list-action-trigger btn-floating service-color darken waves-effect waves-light" data-list-action="view" data-service="corpus-analysis"><i class="material-icons">send</i></a>
 | 
					            <a class="list-action-trigger btn-floating service-color darken waves-effect waves-light" data-list-action="view" data-service="corpus-analysis"><i class="material-icons">send</i></a>
 | 
				
			||||||
          </td>
 | 
					          </td>
 | 
				
			||||||
        </tr>
 | 
					        </tr>
 | 
				
			||||||
      `.trim();
 | 
					      `.trim();
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get valueNames() {
 | 
					  get valueNames() {
 | 
				
			||||||
@@ -43,7 +89,9 @@ class CorpusList extends ResourceList {
 | 
				
			|||||||
      {data: ['creation-date']},
 | 
					      {data: ['creation-date']},
 | 
				
			||||||
      {name: 'status', attr: 'data-status'},
 | 
					      {name: 'status', attr: 'data-status'},
 | 
				
			||||||
      'description',
 | 
					      'description',
 | 
				
			||||||
      'title'
 | 
					      'title',
 | 
				
			||||||
 | 
					      'owner',
 | 
				
			||||||
 | 
					      'current-user-is-following'
 | 
				
			||||||
    ];
 | 
					    ];
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -56,15 +104,24 @@ class CorpusList extends ResourceList {
 | 
				
			|||||||
      <div class="input-field">
 | 
					      <div class="input-field">
 | 
				
			||||||
        <i class="material-icons prefix">search</i>
 | 
					        <i class="material-icons prefix">search</i>
 | 
				
			||||||
        <input id="${listSearchElementId}" class="search" type="text"></input>
 | 
					        <input id="${listSearchElementId}" class="search" type="text"></input>
 | 
				
			||||||
        <label for="${listSearchElementId}">Search corpus</label>
 | 
					        <label for="${listSearchElementId}">Search Corpus</label>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <table>
 | 
					      <table>
 | 
				
			||||||
        <thead>
 | 
					        <thead>
 | 
				
			||||||
          <tr>
 | 
					          <tr>
 | 
				
			||||||
            <th></th>
 | 
					            <th>
 | 
				
			||||||
 | 
					              <label class="corpus-list-selection-action-trigger" data-selection-action="select-all">
 | 
				
			||||||
 | 
					                <input class="corpus-list-select-all-checkbox" type="checkbox">
 | 
				
			||||||
 | 
					                <span></span>
 | 
				
			||||||
 | 
					              </label>
 | 
				
			||||||
 | 
					            </th>
 | 
				
			||||||
            <th>Title and Description</th>
 | 
					            <th>Title and Description</th>
 | 
				
			||||||
 | 
					            <th>Owner</th>
 | 
				
			||||||
            <th>Status</th>
 | 
					            <th>Status</th>
 | 
				
			||||||
            <th></th>
 | 
					            <th></th>
 | 
				
			||||||
 | 
					            <th class="right-align">
 | 
				
			||||||
 | 
					              <a class="corpus-list-selection-action-trigger btn-floating red waves-effect waves-light hide" data-selection-action="delete"><i class="material-icons">delete</i></a>
 | 
				
			||||||
 | 
					            </th>
 | 
				
			||||||
          </tr>
 | 
					          </tr>
 | 
				
			||||||
        </thead>
 | 
					        </thead>
 | 
				
			||||||
        <tbody class="list"></tbody>
 | 
					        <tbody class="list"></tbody>
 | 
				
			||||||
@@ -73,16 +130,6 @@ class CorpusList extends ResourceList {
 | 
				
			|||||||
    `.trim();
 | 
					    `.trim();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  mapResourceToValue(corpus) {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      'id': corpus.id,
 | 
					 | 
				
			||||||
      'creation-date': corpus.creation_date,
 | 
					 | 
				
			||||||
      'description': corpus.description,
 | 
					 | 
				
			||||||
      'status': corpus.status,
 | 
					 | 
				
			||||||
      'title': corpus.title
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  sort() {
 | 
					  sort() {
 | 
				
			||||||
    this.listjs.sort('creation-date', {order: 'desc'});
 | 
					    this.listjs.sort('creation-date', {order: 'desc'});
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -95,19 +142,202 @@ class CorpusList extends ResourceList {
 | 
				
			|||||||
    let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction;
 | 
					    let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction;
 | 
				
			||||||
    switch (listAction) {
 | 
					    switch (listAction) {
 | 
				
			||||||
      case 'delete-request': {
 | 
					      case 'delete-request': {
 | 
				
			||||||
        Utils.deleteCorpusRequest(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 ${values['is-owner'] ? 'delete' : 'unfollow'} the Corpus <b>${values.title}</b>? ${values['is-owner'] ? '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) => {
 | 
				
			||||||
 | 
					          if (!values['is-owner']) {
 | 
				
			||||||
 | 
					            Requests.corpora.entity.followers.entity.delete(itemId, currentUserId)
 | 
				
			||||||
 | 
					              .then((response) => {
 | 
				
			||||||
 | 
					                window.location.reload();
 | 
				
			||||||
 | 
					              });
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            Requests.corpora.entity.delete(itemId);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        modal.open();
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      case 'view': {
 | 
					      case 'view': {
 | 
				
			||||||
        window.location.href = `/corpora/${itemId}`;
 | 
					        window.location.href = `/corpora/${itemId}`;
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      case 'select': {
 | 
				
			||||||
 | 
					        if (event.target.checked) {
 | 
				
			||||||
 | 
					          this.selectedItemIds.add(itemId);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.selectedItemIds.delete(itemId);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.renderingItemSelection();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      default: {
 | 
					      default: {
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onSelectionAction(event) {
 | 
				
			||||||
 | 
					    let selectionActionElement = event.target.closest('.corpus-list-selection-action-trigger[data-selection-action]');
 | 
				
			||||||
 | 
					    let selectionAction = selectionActionElement.dataset.selectionAction;
 | 
				
			||||||
 | 
					    let items = Array.from(this.listjs.items);
 | 
				
			||||||
 | 
					    let selectableItems = Array.from(items)
 | 
				
			||||||
 | 
					      .filter(item => item.elm)
 | 
				
			||||||
 | 
					      .map(item => item.elm.querySelector('.select-checkbox[type="checkbox"]'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    switch (selectionAction) {
 | 
				
			||||||
 | 
					      case 'select-all': {
 | 
				
			||||||
 | 
					        let selectedIds = new Set(Array.from(items)
 | 
				
			||||||
 | 
					          .map(item => item.values().id))
 | 
				
			||||||
 | 
					        if (event.target.checked !== undefined) {
 | 
				
			||||||
 | 
					          if (event.target.checked) {
 | 
				
			||||||
 | 
					            selectableItems.forEach(selectableItem => selectableItem.checked = true);
 | 
				
			||||||
 | 
					            this.selectedItemIds = selectedIds;
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            selectableItems.forEach(checkbox => checkbox.checked = false);
 | 
				
			||||||
 | 
					            this.selectedItemIds = new Set([...this.selectedItemIds].filter(id => !selectedIds.has(id)));
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          this.renderingItemSelection();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      case 'delete': {
 | 
				
			||||||
 | 
					        // Saved for future use:
 | 
				
			||||||
 | 
					        // <p class="hide">Do you really want to unfollow this Corpora?</p>
 | 
				
			||||||
 | 
					        // <ul id="selected-unfollow-items-list"></ul>
 | 
				
			||||||
 | 
					        let modalElement = Utils.HTMLToElement(
 | 
				
			||||||
 | 
					          `
 | 
				
			||||||
 | 
					            <div class="modal">
 | 
				
			||||||
 | 
					              <div class="modal-content">
 | 
				
			||||||
 | 
					                <h4>Confirm Corpus deletion</h4>
 | 
				
			||||||
 | 
					                <p>Do you really want to delete this Corpora? <i>All corpora will be permanently deleted!</i></p>
 | 
				
			||||||
 | 
					                <ul id="selected-deletion-items-list"></ul>
 | 
				
			||||||
 | 
					              </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 itemDeletionList = document.querySelector('#selected-deletion-items-list');
 | 
				
			||||||
 | 
					        // let itemUnfollowList = document.querySelector('#selected-unfollow-items-list');
 | 
				
			||||||
 | 
					        this.selectedItemIds.forEach(selectedItemId => {
 | 
				
			||||||
 | 
					          let listItem = this.listjs.get('id', selectedItemId)[0].elm;
 | 
				
			||||||
 | 
					          let values = this.listjs.get('id', listItem.dataset.id)[0].values();
 | 
				
			||||||
 | 
					          let itemElement = Utils.HTMLToElement(`<li> - ${values.title}</li>`);
 | 
				
			||||||
 | 
					          // if (!values['is-owner']) { 
 | 
				
			||||||
 | 
					          //   itemUnfollowList.appendChild(itemElement);
 | 
				
			||||||
 | 
					          // } else {
 | 
				
			||||||
 | 
					          itemDeletionList.appendChild(itemElement);
 | 
				
			||||||
 | 
					          // }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        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) => {
 | 
				
			||||||
 | 
					          this.selectedItemIds.forEach(selectedItemId => {
 | 
				
			||||||
 | 
					            let listItem = this.listjs.get('id', selectedItemId)[0].elm;
 | 
				
			||||||
 | 
					            let values = this.listjs.get('id', listItem.dataset.id)[0].values();
 | 
				
			||||||
 | 
					            if (values['is-owner']) {
 | 
				
			||||||
 | 
					              Requests.corpora.entity.delete(selectedItemId);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              Requests.corpora.entity.followers.entity.delete(selectedItemId, currentUserId);
 | 
				
			||||||
 | 
					              setTimeout(() => {
 | 
				
			||||||
 | 
					                window.location.reload();
 | 
				
			||||||
 | 
					              }, 1000);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          this.selectedItemIds.clear();
 | 
				
			||||||
 | 
					          this.renderingItemSelection();
 | 
				
			||||||
 | 
					   
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        modal.open();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      default: {
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  renderingItemSelection() {
 | 
				
			||||||
 | 
					    let selectionActionButtons = document.querySelectorAll('.corpus-list-selection-action-trigger:not([data-selection-action="select-all"])');
 | 
				
			||||||
 | 
					    let selectableItems = this.listjs.items;
 | 
				
			||||||
 | 
					    let actionButtons = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Object.values(selectableItems).forEach(selectableItem => {
 | 
				
			||||||
 | 
					      if (selectableItem.elm) {
 | 
				
			||||||
 | 
					        let checkbox = selectableItem.elm.querySelector('.select-checkbox[type="checkbox"]');
 | 
				
			||||||
 | 
					        if (checkbox.checked) {
 | 
				
			||||||
 | 
					          selectableItem.elm.classList.add('grey', 'lighten-3');
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          selectableItem.elm.classList.remove('grey', 'lighten-3');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        let itemActionButtons = selectableItem.elm.querySelectorAll('.list-action-trigger:not([data-list-action="select"])');
 | 
				
			||||||
 | 
					        itemActionButtons.forEach(itemActionButton => {
 | 
				
			||||||
 | 
					          actionButtons.push(itemActionButton);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    // Hide item action buttons if > 0 item is selected and show selection action buttons
 | 
				
			||||||
 | 
					    if (this.selectedItemIds.size > 0) {
 | 
				
			||||||
 | 
					      selectionActionButtons.forEach(selectionActionButton => {
 | 
				
			||||||
 | 
					        selectionActionButton.classList.remove('hide');
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      actionButtons.forEach(actionButton => {
 | 
				
			||||||
 | 
					        actionButton.classList.add('hide');
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      selectionActionButtons.forEach(selectionActionButton => {
 | 
				
			||||||
 | 
					        selectionActionButton.classList.add('hide');
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      actionButtons.forEach(actionButton => {
 | 
				
			||||||
 | 
					        actionButton.classList.remove('hide');
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Check select all checkbox if all items are selected
 | 
				
			||||||
 | 
					    let selectAllCheckbox = document.querySelector('.corpus-list-select-all-checkbox[type="checkbox"]');
 | 
				
			||||||
 | 
					    if (selectableItems.length === this.selectedItemIds.size && selectAllCheckbox.checked === false) {
 | 
				
			||||||
 | 
					      selectAllCheckbox.checked = true;
 | 
				
			||||||
 | 
					    } else if (selectableItems.length !== this.selectedItemIds.size && selectAllCheckbox.checked === true) {
 | 
				
			||||||
 | 
					      selectAllCheckbox.checked = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onPatch(patch) {
 | 
					  onPatch(patch) {
 | 
				
			||||||
    let re = new RegExp(`^/users/${this.userId}/corpora/([A-Za-z0-9]*)`);
 | 
					    let re = new RegExp(`^/users/${this.userId}/corpora/([A-Za-z0-9]*)`);
 | 
				
			||||||
    let filteredPatch = patch.filter(operation => re.test(operation.path));
 | 
					    let filteredPatch = patch.filter(operation => re.test(operation.path));
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										71
									
								
								app/static/js/ResourceLists/DetailledPublicCorpusList.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								app/static/js/ResourceLists/DetailledPublicCorpusList.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,71 @@
 | 
				
			|||||||
 | 
					class DetailledPublicCorpusList extends CorpusList {
 | 
				
			||||||
 | 
					  get item() {
 | 
				
			||||||
 | 
					    return (values) => {
 | 
				
			||||||
 | 
					      return `
 | 
				
			||||||
 | 
					        <tr class="list-item clickable hoverable">
 | 
				
			||||||
 | 
					          <td></td>
 | 
				
			||||||
 | 
					          <td><b class="title"></b><br><i class="description"></i></td>
 | 
				
			||||||
 | 
					          <td><span class="owner"></span></td>
 | 
				
			||||||
 | 
					          <td><span class="status badge new corpus-status-color corpus-status-text" data-badge-caption=""></span></td>
 | 
				
			||||||
 | 
					          <td>${values['current-user-is-following'] ? '<span><i class="left material-icons">visibility</i>Following</span>' : ''}</td>
 | 
				
			||||||
 | 
					          <td class="right-align">
 | 
				
			||||||
 | 
					            <a class="list-action-trigger btn-floating service-color darken waves-effect waves-light" data-list-action="view" data-service="corpus-analysis"><i class="material-icons">send</i></a>
 | 
				
			||||||
 | 
					          </td>
 | 
				
			||||||
 | 
					        </tr>
 | 
				
			||||||
 | 
					      `.trim();
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get valueNames() {
 | 
				
			||||||
 | 
					    return [
 | 
				
			||||||
 | 
					      {data: ['id']},
 | 
				
			||||||
 | 
					      {data: ['creation-date']},
 | 
				
			||||||
 | 
					      {name: 'status', attr: 'data-status'},
 | 
				
			||||||
 | 
					      'description',
 | 
				
			||||||
 | 
					      'title',
 | 
				
			||||||
 | 
					      'owner',
 | 
				
			||||||
 | 
					      'current-user-is-following'
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  initListContainerElement() {
 | 
				
			||||||
 | 
					    if (!this.listContainerElement.hasAttribute('id')) {
 | 
				
			||||||
 | 
					      this.listContainerElement.id = Utils.generateElementId('corpus-list-');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    let listSearchElementId = Utils.generateElementId(`${this.listContainerElement.id}-search-`);
 | 
				
			||||||
 | 
					    this.listContainerElement.innerHTML = `
 | 
				
			||||||
 | 
					      <div class="input-field">
 | 
				
			||||||
 | 
					        <i class="material-icons prefix">search</i>
 | 
				
			||||||
 | 
					        <input id="${listSearchElementId}" class="search" type="text"></input>
 | 
				
			||||||
 | 
					        <label for="${listSearchElementId}">Search Corpus</label>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <table>
 | 
				
			||||||
 | 
					        <thead>
 | 
				
			||||||
 | 
					          <tr>
 | 
				
			||||||
 | 
					            <th></th>
 | 
				
			||||||
 | 
					            <th>Title and Description</th>
 | 
				
			||||||
 | 
					            <th>Owner</th>
 | 
				
			||||||
 | 
					            <th>Status</th>
 | 
				
			||||||
 | 
					            <th></th>
 | 
				
			||||||
 | 
					            <th></th>
 | 
				
			||||||
 | 
					          </tr>
 | 
				
			||||||
 | 
					        </thead>
 | 
				
			||||||
 | 
					        <tbody class="list"></tbody>
 | 
				
			||||||
 | 
					      </table>
 | 
				
			||||||
 | 
					      <ul class="pagination"></ul>
 | 
				
			||||||
 | 
					    `.trim();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  mapResourceToValue(corpus) {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      'id': corpus.id,
 | 
				
			||||||
 | 
					      'creation-date': corpus.creation_date,
 | 
				
			||||||
 | 
					      'description': corpus.description,
 | 
				
			||||||
 | 
					      'status': corpus.status,
 | 
				
			||||||
 | 
					      'title': corpus.title,
 | 
				
			||||||
 | 
					      'owner': corpus.user.username,
 | 
				
			||||||
 | 
					      'is-owner': corpus.user.id === this.userId,
 | 
				
			||||||
 | 
					      'current-user-is-following': Object.values(corpus.corpus_follower_associations).some(association => association.follower.id === currentUserId)
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -12,11 +12,7 @@ class JobInputList extends ResourceList {
 | 
				
			|||||||
    this.userId = listContainerElement.dataset.userId;
 | 
					    this.userId = listContainerElement.dataset.userId;
 | 
				
			||||||
    this.jobId = listContainerElement.dataset.jobId;
 | 
					    this.jobId = listContainerElement.dataset.jobId;
 | 
				
			||||||
    if (this.userId === undefined || this.jobId === undefined) {return;}
 | 
					    if (this.userId === undefined || this.jobId === undefined) {return;}
 | 
				
			||||||
    app.subscribeUser(this.userId).then((response) => {
 | 
					    app.subscribeUser(this.userId);
 | 
				
			||||||
      app.socket.on('PATCH', (patch) => {
 | 
					 | 
				
			||||||
        if (this.isInitialized) {this.onPatch(patch);}
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    app.getUser(this.userId).then((user) => {
 | 
					    app.getUser(this.userId).then((user) => {
 | 
				
			||||||
      this.add(Object.values(user.jobs[this.jobId].inputs));
 | 
					      this.add(Object.values(user.jobs[this.jobId].inputs));
 | 
				
			||||||
      this.isInitialized = true;
 | 
					      this.isInitialized = true;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,8 +7,13 @@ class JobList extends ResourceList {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  constructor(listContainerElement, options = {}) {
 | 
					  constructor(listContainerElement, options = {}) {
 | 
				
			||||||
    super(listContainerElement, options);
 | 
					    super(listContainerElement, options);
 | 
				
			||||||
 | 
					    this.documentJobArea = document.querySelector('#jobs');
 | 
				
			||||||
    this.listjs.list.addEventListener('click', (event) => {this.onClick(event)});
 | 
					    this.listjs.list.addEventListener('click', (event) => {this.onClick(event)});
 | 
				
			||||||
 | 
					    document.querySelectorAll('.job-list-selection-action-trigger[data-selection-action]').forEach((element) => {
 | 
				
			||||||
 | 
					      element.addEventListener('click', (event) => {this.onSelectionAction(event)});
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
    this.isInitialized = false;
 | 
					    this.isInitialized = false;
 | 
				
			||||||
 | 
					    this.selectedItemIds = new Set();
 | 
				
			||||||
    this.userId = listContainerElement.dataset.userId;
 | 
					    this.userId = listContainerElement.dataset.userId;
 | 
				
			||||||
    if (this.userId === undefined) {return;}
 | 
					    if (this.userId === undefined) {return;}
 | 
				
			||||||
    app.subscribeUser(this.userId).then((response) => {
 | 
					    app.subscribeUser(this.userId).then((response) => {
 | 
				
			||||||
@@ -24,7 +29,13 @@ class JobList extends ResourceList {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  get item() {
 | 
					  get item() {
 | 
				
			||||||
    return `
 | 
					    return `
 | 
				
			||||||
      <tr class="list-item clickable hoverable service-scheme">
 | 
					      <tr class="list-item service-scheme">
 | 
				
			||||||
 | 
					        <td>
 | 
				
			||||||
 | 
					          <label class="list-action-trigger" data-list-action="select">
 | 
				
			||||||
 | 
					            <input class="select-checkbox" type="checkbox">
 | 
				
			||||||
 | 
					            <span class="disable-on-click"></span>
 | 
				
			||||||
 | 
					          </label>
 | 
				
			||||||
 | 
					        </td>
 | 
				
			||||||
        <td><a class="btn-floating"><i class="nopaque-icons service-icons" data-service="inherit"></i></a></td>
 | 
					        <td><a class="btn-floating"><i class="nopaque-icons service-icons" data-service="inherit"></i></a></td>
 | 
				
			||||||
        <td><b class="title"></b><br><i class="description"></i></td>
 | 
					        <td><b class="title"></b><br><i class="description"></i></td>
 | 
				
			||||||
        <td><span class="badge new job-status-color job-status-text status" data-badge-caption=""></span></td>
 | 
					        <td><span class="badge new job-status-color job-status-text status" data-badge-caption=""></span></td>
 | 
				
			||||||
@@ -56,15 +67,23 @@ class JobList extends ResourceList {
 | 
				
			|||||||
      <div class="input-field">
 | 
					      <div class="input-field">
 | 
				
			||||||
        <i class="material-icons prefix">search</i>
 | 
					        <i class="material-icons prefix">search</i>
 | 
				
			||||||
        <input id="${listSearchElementId}" class="search" type="text"></input>
 | 
					        <input id="${listSearchElementId}" class="search" type="text"></input>
 | 
				
			||||||
        <label for="${listSearchElementId}">Search job</label>
 | 
					        <label for="${listSearchElementId}">Search Job</label>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <table>
 | 
					      <table>
 | 
				
			||||||
        <thead>
 | 
					        <thead>
 | 
				
			||||||
          <tr>
 | 
					          <tr>
 | 
				
			||||||
 | 
					            <th>
 | 
				
			||||||
 | 
					              <label class="job-list-selection-action-trigger" data-selection-action="select-all">
 | 
				
			||||||
 | 
					                <input class="job-list-select-all-checkbox" type="checkbox">
 | 
				
			||||||
 | 
					                <span class="disable-on-click"></span>
 | 
				
			||||||
 | 
					              </label>
 | 
				
			||||||
 | 
					            </th>
 | 
				
			||||||
            <th>Service</th>
 | 
					            <th>Service</th>
 | 
				
			||||||
            <th>Title and Description</th>
 | 
					            <th>Title and Description</th>
 | 
				
			||||||
            <th>Status</th>
 | 
					            <th>Status</th>
 | 
				
			||||||
            <th></th>
 | 
					            <th class="right-align">
 | 
				
			||||||
 | 
					              <a class="job-list-selection-action-trigger btn-floating red waves-effect waves-light hide" data-selection-action="delete"><i class="material-icons">delete</i></a>
 | 
				
			||||||
 | 
					            </th>
 | 
				
			||||||
          </tr>
 | 
					          </tr>
 | 
				
			||||||
        </thead>
 | 
					        </thead>
 | 
				
			||||||
        <tbody class="list"></tbody>
 | 
					        <tbody class="list"></tbody>
 | 
				
			||||||
@@ -93,22 +112,185 @@ class JobList extends ResourceList {
 | 
				
			|||||||
    if (listItemElement === null) {return;}
 | 
					    if (listItemElement === null) {return;}
 | 
				
			||||||
    let itemId = listItemElement.dataset.id;
 | 
					    let itemId = listItemElement.dataset.id;
 | 
				
			||||||
    let listActionElement = event.target.closest('.list-action-trigger[data-list-action]');
 | 
					    let listActionElement = event.target.closest('.list-action-trigger[data-list-action]');
 | 
				
			||||||
    let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction;
 | 
					    let listAction = listActionElement === null ? '' : listActionElement.dataset.listAction;
 | 
				
			||||||
    switch (listAction) {
 | 
					    switch (listAction) {
 | 
				
			||||||
      case 'delete-request': {
 | 
					      case 'delete-request': {
 | 
				
			||||||
        Utils.deleteJobRequest(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 Job deletion</h4>
 | 
				
			||||||
 | 
					                <p>Do you really want to delete the Job <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.jobs.entity.delete(itemId);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        modal.open();
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      case 'view': {
 | 
					      case 'view': {
 | 
				
			||||||
        window.location.href = `/jobs/${itemId}`;
 | 
					        window.location.href = `/jobs/${itemId}`;
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      case 'select': {
 | 
				
			||||||
 | 
					        if (event.target.checked) {
 | 
				
			||||||
 | 
					          this.selectedItemIds.add(itemId);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.selectedItemIds.delete(itemId);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.renderingItemSelection();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      default: {
 | 
					      default: {
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onSelectionAction(event) {
 | 
				
			||||||
 | 
					    let selectionActionElement = event.target.closest('.job-list-selection-action-trigger[data-selection-action]');
 | 
				
			||||||
 | 
					    let selectionAction = selectionActionElement.dataset.selectionAction;
 | 
				
			||||||
 | 
					    let items = this.listjs.items;
 | 
				
			||||||
 | 
					    let selectableItems = Array.from(items)
 | 
				
			||||||
 | 
					      .filter(item => item.elm)
 | 
				
			||||||
 | 
					      .map(item => item.elm.querySelector('.select-checkbox[type="checkbox"]'));
 | 
				
			||||||
 | 
					    switch (selectionAction) {
 | 
				
			||||||
 | 
					      case 'select-all': {
 | 
				
			||||||
 | 
					        let selectedIds = new Set(Array.from(items)
 | 
				
			||||||
 | 
					          .map(item => item.values().id))
 | 
				
			||||||
 | 
					        if (event.target.checked !== undefined) {
 | 
				
			||||||
 | 
					          if (event.target.checked) {
 | 
				
			||||||
 | 
					            selectableItems.forEach(selectableItem => selectableItem.checked = true);
 | 
				
			||||||
 | 
					            this.selectedItemIds = selectedIds;
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            selectableItems.forEach(checkbox => checkbox.checked = false);
 | 
				
			||||||
 | 
					            this.selectedItemIds = new Set([...this.selectedItemIds].filter(id => !selectedIds.has(id)));
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          this.renderingItemSelection();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      case 'delete': {
 | 
				
			||||||
 | 
					        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 Jobs?</p>
 | 
				
			||||||
 | 
					                  <ul id="selected-items-list"></ul>
 | 
				
			||||||
 | 
					                <p>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 itemList = document.querySelector('#selected-items-list');
 | 
				
			||||||
 | 
					        this.selectedItemIds.forEach(selectedItemId => {
 | 
				
			||||||
 | 
					          let listItem = this.listjs.get('id', selectedItemId)[0].elm;
 | 
				
			||||||
 | 
					          let values = this.listjs.get('id', listItem.dataset.id)[0].values();
 | 
				
			||||||
 | 
					          let itemElement = Utils.HTMLToElement(`<li> - ${values.title}</li>`);
 | 
				
			||||||
 | 
					          itemList.appendChild(itemElement);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        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) => {
 | 
				
			||||||
 | 
					          this.selectedItemIds.forEach(selectedItemId => {
 | 
				
			||||||
 | 
					            Requests.jobs.entity.delete(selectedItemId);
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          this.selectedItemIds.clear();
 | 
				
			||||||
 | 
					          this.renderingItemSelection();
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        modal.open();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      default: {
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  renderingItemSelection() {
 | 
				
			||||||
 | 
					    let selectionActionButtons = document.querySelectorAll('.job-list-selection-action-trigger:not([data-selection-action="select-all"])');
 | 
				
			||||||
 | 
					    let selectableItems = this.listjs.items;
 | 
				
			||||||
 | 
					    let actionButtons = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Object.values(selectableItems).forEach(selectableItem => {
 | 
				
			||||||
 | 
					      if (selectableItem.elm) {
 | 
				
			||||||
 | 
					        let checkbox = selectableItem.elm.querySelector('.select-checkbox[type="checkbox"]');
 | 
				
			||||||
 | 
					        if (checkbox.checked) {
 | 
				
			||||||
 | 
					          selectableItem.elm.classList.add('grey', 'lighten-3');
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          selectableItem.elm.classList.remove('grey', 'lighten-3');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        let itemActionButtons = selectableItem.elm.querySelectorAll('.list-action-trigger:not([data-list-action="select"])');
 | 
				
			||||||
 | 
					        itemActionButtons.forEach(itemActionButton => {
 | 
				
			||||||
 | 
					          actionButtons.push(itemActionButton);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Hide item action buttons if > 0 item is selected and show selection action buttons
 | 
				
			||||||
 | 
					    if (this.selectedItemIds.size > 0) {
 | 
				
			||||||
 | 
					      selectionActionButtons.forEach(selectionActionButton => {
 | 
				
			||||||
 | 
					        selectionActionButton.classList.remove('hide');
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      actionButtons.forEach(actionButton => {
 | 
				
			||||||
 | 
					        actionButton.classList.add('hide');
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      selectionActionButtons.forEach(selectionActionButton => {
 | 
				
			||||||
 | 
					        selectionActionButton.classList.add('hide');
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      actionButtons.forEach(actionButton => {
 | 
				
			||||||
 | 
					        actionButton.classList.remove('hide');
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Check select all checkbox if all items are selected
 | 
				
			||||||
 | 
					    let selectAllCheckbox = document.querySelector('.job-list-select-all-checkbox[type="checkbox"]');
 | 
				
			||||||
 | 
					    if (selectableItems.length === this.selectedItemIds.size && selectAllCheckbox.checked === false) {
 | 
				
			||||||
 | 
					      selectAllCheckbox.checked = true;
 | 
				
			||||||
 | 
					    } else if (selectableItems.length !== this.selectedItemIds.size && selectAllCheckbox.checked === true) {
 | 
				
			||||||
 | 
					      selectAllCheckbox.checked = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onPatch(patch) {
 | 
					  onPatch(patch) {
 | 
				
			||||||
    let re = new RegExp(`^/users/${this.userId}/jobs/([A-Za-z0-9]*)`);
 | 
					    let re = new RegExp(`^/users/${this.userId}/jobs/([A-Za-z0-9]*)`);
 | 
				
			||||||
    let filteredPatch = patch.filter(operation => re.test(operation.path));
 | 
					    let filteredPatch = patch.filter(operation => re.test(operation.path));
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,15 +0,0 @@
 | 
				
			|||||||
class PublicCorpusFileList extends CorpusFileList {
 | 
					 | 
				
			||||||
  get item() {
 | 
					 | 
				
			||||||
    return `
 | 
					 | 
				
			||||||
      <tr class="list-item clickable hoverable">
 | 
					 | 
				
			||||||
        <td><span class="filename"></span></td>
 | 
					 | 
				
			||||||
        <td><span class="author"></span></td>
 | 
					 | 
				
			||||||
        <td><span class="title"></span></td>
 | 
					 | 
				
			||||||
        <td><span class="publishing-year"></span></td>
 | 
					 | 
				
			||||||
        <td class="right-align">
 | 
					 | 
				
			||||||
          <a class="list-action-trigger btn-floating service-color darken waves-effect waves-light" data-list-action="view" data-service="corpus-analysis"><i class="material-icons">send</i></a>
 | 
					 | 
				
			||||||
        </td>
 | 
					 | 
				
			||||||
      </tr>
 | 
					 | 
				
			||||||
    `.trim();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,14 +1,55 @@
 | 
				
			|||||||
class PublicCorpusList extends CorpusList {
 | 
					class PublicCorpusList extends CorpusList {
 | 
				
			||||||
  get item() {
 | 
					  get item() {
 | 
				
			||||||
 | 
					    return (values) => {
 | 
				
			||||||
      return `
 | 
					      return `
 | 
				
			||||||
        <tr class="list-item clickable hoverable">
 | 
					        <tr class="list-item 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><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><span class="owner"></span></td>
 | 
				
			||||||
 | 
					          <td>${values['current-user-is-following'] ? '<span><i class="left material-icons">visibility</i></span>' : ''}</td>
 | 
				
			||||||
          <td class="right-align">
 | 
					          <td class="right-align">
 | 
				
			||||||
            <a class="list-action-trigger btn-floating service-color darken waves-effect waves-light" data-list-action="view" data-service="corpus-analysis"><i class="material-icons">send</i></a>
 | 
					            <a class="list-action-trigger btn-floating service-color darken waves-effect waves-light" data-list-action="view" data-service="corpus-analysis"><i class="material-icons">send</i></a>
 | 
				
			||||||
          </td>
 | 
					          </td>
 | 
				
			||||||
        </tr>
 | 
					        </tr>
 | 
				
			||||||
      `.trim();
 | 
					      `.trim();
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  mapResourceToValue(corpus) {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      'id': corpus.id,
 | 
				
			||||||
 | 
					      'creation-date': corpus.creation_date,
 | 
				
			||||||
 | 
					      'description': corpus.description,
 | 
				
			||||||
 | 
					      'status': corpus.status,
 | 
				
			||||||
 | 
					      'title': corpus.title,
 | 
				
			||||||
 | 
					      'owner': corpus.user.username,
 | 
				
			||||||
 | 
					      'is-owner': corpus.user.id === this.userId ? true : false,
 | 
				
			||||||
 | 
					      'current-user-is-following': Object.values(corpus.corpus_follower_associations).some(association => association.follower.id === currentUserId)
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  initListContainerElement() {
 | 
				
			||||||
 | 
					    if (!this.listContainerElement.hasAttribute('id')) {
 | 
				
			||||||
 | 
					      this.listContainerElement.id = Utils.generateElementId('corpus-list-');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    let listSearchElementId = Utils.generateElementId(`${this.listContainerElement.id}-search-`);
 | 
				
			||||||
 | 
					    this.listContainerElement.innerHTML = `
 | 
				
			||||||
 | 
					      <div class="input-field">
 | 
				
			||||||
 | 
					        <i class="material-icons prefix">search</i>
 | 
				
			||||||
 | 
					        <input id="${listSearchElementId}" class="search" type="text"></input>
 | 
				
			||||||
 | 
					        <label for="${listSearchElementId}">Search Corpus</label>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <table>
 | 
				
			||||||
 | 
					        <thead>
 | 
				
			||||||
 | 
					          <tr>
 | 
				
			||||||
 | 
					            <th>Title and Description</th>
 | 
				
			||||||
 | 
					            <th>Owner</th>
 | 
				
			||||||
 | 
					            <th></th>
 | 
				
			||||||
 | 
					            <th></th>
 | 
				
			||||||
 | 
					          </tr>
 | 
				
			||||||
 | 
					        </thead>
 | 
				
			||||||
 | 
					        <tbody class="list"></tbody>
 | 
				
			||||||
 | 
					      </table>
 | 
				
			||||||
 | 
					      <ul class="pagination"></ul>
 | 
				
			||||||
 | 
					    `.trim();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,6 +14,7 @@ class ResourceList {
 | 
				
			|||||||
    TesseractOCRPipelineModelList.autoInit();
 | 
					    TesseractOCRPipelineModelList.autoInit();
 | 
				
			||||||
    UserList.autoInit();
 | 
					    UserList.autoInit();
 | 
				
			||||||
    AdminUserList.autoInit();
 | 
					    AdminUserList.autoInit();
 | 
				
			||||||
 | 
					    CorpusFollowerList.autoInit();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static defaultOptions = {
 | 
					  static defaultOptions = {
 | 
				
			||||||
@@ -42,7 +43,8 @@ class ResourceList {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  add(resources, callback) {
 | 
					  add(resources, callback) {
 | 
				
			||||||
    let values = resources.map((resource) => {
 | 
					    let tmp = Array.isArray(resources) ? resources : [resources];
 | 
				
			||||||
 | 
					    let values = tmp.map((resource) => {
 | 
				
			||||||
      return this.mapResourceToValue(resource);
 | 
					      return this.mapResourceToValue(resource);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    this.listjs.add(values, (items) => {
 | 
					    this.listjs.add(values, (items) => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,14 +30,12 @@ class SpaCyNLPPipelineModelList extends ResourceList {
 | 
				
			|||||||
          <td><b><span class="title"></span> <span class="version"></span></b><br><i><span class="description"></span></i></td>
 | 
					          <td><b><span class="title"></span> <span class="version"></span></b><br><i><span class="description"></span></i></td>
 | 
				
			||||||
          <td><a class="publisher-url"><span class="publisher"></span></a> (<span class="publishing-year"></span>)<br><a class="publishing-url publishing-url-2"></a></td>
 | 
					          <td><a class="publisher-url"><span class="publisher"></span></a> (<span class="publishing-year"></span>)<br><a class="publishing-url publishing-url-2"></a></td>
 | 
				
			||||||
          <td>
 | 
					          <td>
 | 
				
			||||||
            <div class="list-action-trigger switch center-align" data-list-action="share-request">
 | 
					            <span class="disable-on-click">
 | 
				
			||||||
              <span class="share"></span>
 | 
					 | 
				
			||||||
              <label>
 | 
					              <label>
 | 
				
			||||||
                <input class="is-public" ${values['is-public'] ? 'checked' : ''} type="checkbox">
 | 
					                <input ${values['is-public'] ? 'checked' : ''} class="is-public list-action-trigger" data-list-action="toggle-is-public" type="checkbox">
 | 
				
			||||||
                <span class="lever"></span>
 | 
					                <span>Public</span>
 | 
				
			||||||
                public
 | 
					 | 
				
			||||||
              </label>
 | 
					              </label>
 | 
				
			||||||
            </div>
 | 
					            </span>
 | 
				
			||||||
          </td>
 | 
					          </td>
 | 
				
			||||||
          <td class="right-align">
 | 
					          <td class="right-align">
 | 
				
			||||||
            <a class="list-action-trigger btn-floating red waves-effect waves-light" data-list-action="delete-request"><i class="material-icons">delete</i></a>
 | 
					            <a class="list-action-trigger btn-floating red waves-effect waves-light" data-list-action="delete-request"><i class="material-icons">delete</i></a>
 | 
				
			||||||
@@ -80,6 +78,7 @@ class SpaCyNLPPipelineModelList extends ResourceList {
 | 
				
			|||||||
          <tr>
 | 
					          <tr>
 | 
				
			||||||
            <th>Title and Description</th>
 | 
					            <th>Title and Description</th>
 | 
				
			||||||
            <th>Publisher</th>
 | 
					            <th>Publisher</th>
 | 
				
			||||||
 | 
					            <th>Availability</th>
 | 
				
			||||||
            <th></th>
 | 
					            <th></th>
 | 
				
			||||||
          </tr>
 | 
					          </tr>
 | 
				
			||||||
        </thead>
 | 
					        </thead>
 | 
				
			||||||
@@ -111,6 +110,7 @@ class SpaCyNLPPipelineModelList extends ResourceList {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onChange(event) {
 | 
					  onChange(event) {
 | 
				
			||||||
 | 
					    if (event.target.tagName !== 'INPUT') {return;}
 | 
				
			||||||
    let listItemElement = event.target.closest('.list-item[data-id]');
 | 
					    let listItemElement = event.target.closest('.list-item[data-id]');
 | 
				
			||||||
    if (listItemElement === null) {return;}
 | 
					    if (listItemElement === null) {return;}
 | 
				
			||||||
    let itemId = listItemElement.dataset.id;
 | 
					    let itemId = listItemElement.dataset.id;
 | 
				
			||||||
@@ -118,8 +118,12 @@ class SpaCyNLPPipelineModelList extends ResourceList {
 | 
				
			|||||||
    if (listActionElement === null) {return;}
 | 
					    if (listActionElement === null) {return;}
 | 
				
			||||||
    let listAction = listActionElement.dataset.listAction;
 | 
					    let listAction = listActionElement.dataset.listAction;
 | 
				
			||||||
    switch (listAction) {
 | 
					    switch (listAction) {
 | 
				
			||||||
      case 'share-request': {
 | 
					      case 'toggle-is-public': {
 | 
				
			||||||
        Utils.spaCyNLPPipelineModelToggleIsPublicRequest(this.userId, itemId);
 | 
					        let newIsPublicValue = listActionElement.checked;
 | 
				
			||||||
 | 
					        Requests.contributions.spacy_nlp_pipeline_models.entity.isPublic.update(itemId, newIsPublicValue)
 | 
				
			||||||
 | 
					          .catch((response) => {
 | 
				
			||||||
 | 
					            listActionElement.checked = !newIsPublicValue;
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      default: {
 | 
					      default: {
 | 
				
			||||||
@@ -129,19 +133,45 @@ class SpaCyNLPPipelineModelList extends ResourceList {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onClick(event) {
 | 
					  onClick(event) {
 | 
				
			||||||
 | 
					    if (event.target.closest('.disable-on-click') !== null) {return;}
 | 
				
			||||||
    let listItemElement = event.target.closest('.list-item[data-id]');
 | 
					    let listItemElement = event.target.closest('.list-item[data-id]');
 | 
				
			||||||
    if (listItemElement === null) {return;}
 | 
					    if (listItemElement === null) {return;}
 | 
				
			||||||
    let itemId = listItemElement.dataset.id;
 | 
					    let itemId = listItemElement.dataset.id;
 | 
				
			||||||
    let listActionElement = event.target.closest('.list-action-trigger[data-list-action]');
 | 
					    let listActionElement = event.target.closest('.list-action-trigger[data-list-action]');
 | 
				
			||||||
    // ignore switch clicks, handle them by the onChange method instead
 | 
					 | 
				
			||||||
    if (listActionElement.classList.contains('switch')) {
 | 
					 | 
				
			||||||
      event.preventDefault();
 | 
					 | 
				
			||||||
      this.onChange(event);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction;
 | 
					    let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction;
 | 
				
			||||||
    switch (listAction) {
 | 
					    switch (listAction) {
 | 
				
			||||||
      case 'delete-request': {
 | 
					      case 'delete-request': {
 | 
				
			||||||
        Utils.deleteSpaCyNLPPipelineModelRequest(this.userId, itemId);
 | 
					        let values = this.listjs.get('id', itemId)[0].values();
 | 
				
			||||||
 | 
					        let modalElement = Utils.HTMLToElement(
 | 
				
			||||||
 | 
					          `
 | 
				
			||||||
 | 
					            <div class="modal">
 | 
				
			||||||
 | 
					              <div class="modal-content">
 | 
				
			||||||
 | 
					                <h4>Confirm SpaCy NLP Pipeline Model deletion</h4>
 | 
				
			||||||
 | 
					                <p>Do you really want to delete the SpaCy NLP Pipeline Model <b>${values.title}</b>? All files will be permanently deleted!</p>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <div class="modal-footer">
 | 
				
			||||||
 | 
					                <a class="btn modal-close waves-effect waves-light">Cancel</a>
 | 
				
			||||||
 | 
					                <a class="action-button btn modal-close red waves-effect waves-light" data-action="confirm">Delete</a>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          `
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        document.querySelector('#modals').appendChild(modalElement);
 | 
				
			||||||
 | 
					        let modal = M.Modal.init(
 | 
				
			||||||
 | 
					          modalElement,
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            dismissible: false,
 | 
				
			||||||
 | 
					            onCloseEnd: () => {
 | 
				
			||||||
 | 
					              modal.destroy();
 | 
				
			||||||
 | 
					              modalElement.remove();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]');
 | 
				
			||||||
 | 
					        confirmElement.addEventListener('click', (event) => {
 | 
				
			||||||
 | 
					          Requests.contributions.spacy_nlp_pipeline_models.entity.delete(itemId);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        modal.open();
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      case 'view': {
 | 
					      case 'view': {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -38,14 +38,12 @@ class TesseractOCRPipelineModelList extends ResourceList {
 | 
				
			|||||||
          <td><b><span class="title"></span> <span class="version"></span></b><br><i><span class="description"></span></i></td>
 | 
					          <td><b><span class="title"></span> <span class="version"></span></b><br><i><span class="description"></span></i></td>
 | 
				
			||||||
          <td><a class="publisher-url"><span class="publisher"></span></a> (<span class="publishing-year"></span>)<br><a class="publishing-url"><span class="publishing-url-2"></span></a></td>
 | 
					          <td><a class="publisher-url"><span class="publisher"></span></a> (<span class="publishing-year"></span>)<br><a class="publishing-url"><span class="publishing-url-2"></span></a></td>
 | 
				
			||||||
          <td>
 | 
					          <td>
 | 
				
			||||||
            <div class="list-action-trigger switch center-align" data-list-action="share-request">
 | 
					            <span class="disable-on-click">
 | 
				
			||||||
              <span class="share"></span>
 | 
					 | 
				
			||||||
              <label>
 | 
					              <label>
 | 
				
			||||||
                <input ${values['is-public'] ? 'checked' : ''} class="is-public" type="checkbox">
 | 
					                <input ${values['is-public'] ? 'checked' : ''} class="is-public list-action-trigger" data-list-action="toggle-is-public" type="checkbox">
 | 
				
			||||||
                <span class="lever"></span>
 | 
					                <span>Public</span>
 | 
				
			||||||
                public
 | 
					 | 
				
			||||||
              </label>
 | 
					              </label>
 | 
				
			||||||
            </div>
 | 
					            </span>
 | 
				
			||||||
          </td>
 | 
					          </td>
 | 
				
			||||||
          <td class="right-align">
 | 
					          <td class="right-align">
 | 
				
			||||||
            <a class="list-action-trigger btn-floating red waves-effect waves-light" data-list-action="delete-request"><i class="material-icons">delete</i></a>
 | 
					            <a class="list-action-trigger btn-floating red waves-effect waves-light" data-list-action="delete-request"><i class="material-icons">delete</i></a>
 | 
				
			||||||
@@ -89,6 +87,7 @@ class TesseractOCRPipelineModelList extends ResourceList {
 | 
				
			|||||||
          <tr>
 | 
					          <tr>
 | 
				
			||||||
            <th>Title and Description</th>
 | 
					            <th>Title and Description</th>
 | 
				
			||||||
            <th>Publisher</th>
 | 
					            <th>Publisher</th>
 | 
				
			||||||
 | 
					            <th>Availability</th>
 | 
				
			||||||
            <th></th>
 | 
					            <th></th>
 | 
				
			||||||
          </tr>
 | 
					          </tr>
 | 
				
			||||||
        </thead>
 | 
					        </thead>
 | 
				
			||||||
@@ -120,6 +119,7 @@ class TesseractOCRPipelineModelList extends ResourceList {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onChange(event) {
 | 
					  onChange(event) {
 | 
				
			||||||
 | 
					    if (event.target.tagName !== 'INPUT') {return;}
 | 
				
			||||||
    let listItemElement = event.target.closest('.list-item[data-id]');
 | 
					    let listItemElement = event.target.closest('.list-item[data-id]');
 | 
				
			||||||
    if (listItemElement === null) {return;}
 | 
					    if (listItemElement === null) {return;}
 | 
				
			||||||
    let itemId = listItemElement.dataset.id;
 | 
					    let itemId = listItemElement.dataset.id;
 | 
				
			||||||
@@ -127,8 +127,12 @@ class TesseractOCRPipelineModelList extends ResourceList {
 | 
				
			|||||||
    if (listActionElement === null) {return;}
 | 
					    if (listActionElement === null) {return;}
 | 
				
			||||||
    let listAction = listActionElement.dataset.listAction;
 | 
					    let listAction = listActionElement.dataset.listAction;
 | 
				
			||||||
    switch (listAction) {
 | 
					    switch (listAction) {
 | 
				
			||||||
      case 'share-request': {
 | 
					      case 'toggle-is-public': {
 | 
				
			||||||
        Utils.tesseractOCRPipelineModelToggleIsPublicRequest(this.userId, itemId);
 | 
					        let newIsPublicValue = listActionElement.checked;
 | 
				
			||||||
 | 
					        Requests.contributions.tesseract_ocr_pipeline_models.entity.isPublic.update(itemId, newIsPublicValue)
 | 
				
			||||||
 | 
					          .catch((response) => {
 | 
				
			||||||
 | 
					            listActionElement.checked = !newIsPublicValue;
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      default: {
 | 
					      default: {
 | 
				
			||||||
@@ -138,19 +142,45 @@ class TesseractOCRPipelineModelList extends ResourceList {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onClick(event) {
 | 
					  onClick(event) {
 | 
				
			||||||
 | 
					    if (event.target.closest('.disable-on-click') !== null) {return;}
 | 
				
			||||||
    let listItemElement = event.target.closest('.list-item[data-id]');
 | 
					    let listItemElement = event.target.closest('.list-item[data-id]');
 | 
				
			||||||
    if (listItemElement === null) {return;}
 | 
					    if (listItemElement === null) {return;}
 | 
				
			||||||
    let itemId = listItemElement.dataset.id;
 | 
					    let itemId = listItemElement.dataset.id;
 | 
				
			||||||
    let listActionElement = event.target.closest('.list-action-trigger[data-list-action]');
 | 
					    let listActionElement = event.target.closest('.list-action-trigger[data-list-action]');
 | 
				
			||||||
    // ignore switch clicks, handle them by the onChange method instead
 | 
					 | 
				
			||||||
    if (listActionElement.classList.contains('switch')) {
 | 
					 | 
				
			||||||
      event.preventDefault();
 | 
					 | 
				
			||||||
      this.onChange(event);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction;
 | 
					    let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction;
 | 
				
			||||||
    switch (listAction) {
 | 
					    switch (listAction) {
 | 
				
			||||||
      case 'delete-request': {
 | 
					      case 'delete-request': {
 | 
				
			||||||
        Utils.deleteTesseractOCRPipelineModelRequest(this.userId, itemId);
 | 
					        let values = this.listjs.get('id', itemId)[0].values();
 | 
				
			||||||
 | 
					        let modalElement = Utils.HTMLToElement(
 | 
				
			||||||
 | 
					          `
 | 
				
			||||||
 | 
					            <div class="modal">
 | 
				
			||||||
 | 
					              <div class="modal-content">
 | 
				
			||||||
 | 
					                <h4>Confirm Tesseract OCR Pipeline Model deletion</h4>
 | 
				
			||||||
 | 
					                <p>Do you really want to delete the Tesseract OCR Pipeline Model <b>${values.title}</b>? All files will be permanently deleted!</p>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <div class="modal-footer">
 | 
				
			||||||
 | 
					                <a class="btn modal-close waves-effect waves-light">Cancel</a>
 | 
				
			||||||
 | 
					                <a class="action-button btn modal-close red waves-effect waves-light" data-action="confirm">Delete</a>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          `
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        document.querySelector('#modals').appendChild(modalElement);
 | 
				
			||||||
 | 
					        let modal = M.Modal.init(
 | 
				
			||||||
 | 
					          modalElement,
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            dismissible: false,
 | 
				
			||||||
 | 
					            onCloseEnd: () => {
 | 
				
			||||||
 | 
					              modal.destroy();
 | 
				
			||||||
 | 
					              modalElement.remove();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]');
 | 
				
			||||||
 | 
					        confirmElement.addEventListener('click', (event) => {
 | 
				
			||||||
 | 
					          Requests.contributions.tesseract_ocr_pipeline_models.entity.delete(itemId);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        modal.open();
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      case 'view': {
 | 
					      case 'view': {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,14 +13,14 @@ class UserList extends ResourceList {
 | 
				
			|||||||
  get item() {
 | 
					  get item() {
 | 
				
			||||||
    return `
 | 
					    return `
 | 
				
			||||||
      <tr class="list-item clickable hoverable">
 | 
					      <tr class="list-item clickable hoverable">
 | 
				
			||||||
        <td><img alt="user-image" class="circle responsive-img avatar" style="width:50%"></td>
 | 
					        <td><img alt="user-image" class="circle responsive-img avatar" style="width:25%"></td>
 | 
				
			||||||
        <td><b><span class="username"></span><b></td>
 | 
					        <td><b><span class="username"></span><b></td>
 | 
				
			||||||
        <td><span class="full-name"></span></td>
 | 
					        <td><span class="full-name"></span></td>
 | 
				
			||||||
        <td><span class="location"></span></td>
 | 
					        <td><span class="location"></span></td>
 | 
				
			||||||
        <td><span class="organization"></span></td>
 | 
					        <td><span class="organization"></span></td>
 | 
				
			||||||
        <td><span class="corpora-online"></span></td>
 | 
					        <td><span class="corpora-online"></span></td>
 | 
				
			||||||
        <td class="right-align">
 | 
					        <td class="right-align">
 | 
				
			||||||
          <a class="list-action-trigger btn-floating waves-effect waves-light" data-list-action="view"><i class="material-icons">send</i></a>
 | 
					          <a class="list-action-trigger btn-floating waves-effect waves-light social-area-color-darken" data-list-action="view"><i class="material-icons">send</i></a>
 | 
				
			||||||
        </td>
 | 
					        </td>
 | 
				
			||||||
      </tr>
 | 
					      </tr>
 | 
				
			||||||
    `.trim();
 | 
					    `.trim();
 | 
				
			||||||
@@ -72,12 +72,12 @@ class UserList extends ResourceList {
 | 
				
			|||||||
    return {
 | 
					    return {
 | 
				
			||||||
      'id': user.id,
 | 
					      'id': user.id,
 | 
				
			||||||
      'member-since': user.member_since,
 | 
					      'member-since': user.member_since,
 | 
				
			||||||
      'avatar': user.avatar ? `/users/${user.id}/avatar` : '/static/images/user_avatar.png',
 | 
					      'avatar': user.avatar,
 | 
				
			||||||
      'username': user.username,
 | 
					      'username': user.username,
 | 
				
			||||||
      'full-name': user.full_name ? user.full_name : '',
 | 
					      'full-name': user.full_name ? user.full_name : '',
 | 
				
			||||||
      'location': user.location ? user.location : '',
 | 
					      'location': user.location ? user.location : '',
 | 
				
			||||||
      'organization': user.organization ? user.organization : '',
 | 
					      'organization': user.organization ? user.organization : '',
 | 
				
			||||||
      'corpora-online': '-'
 | 
					      'corpora-online': Object.values(user.corpora).filter((corpus) => corpus.is_public).length
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -69,648 +69,4 @@ class Utils {
 | 
				
			|||||||
    return Utils.mergeObjectsDeep(mergedObject, ...objects.slice(2));
 | 
					    return Utils.mergeObjectsDeep(mergedObject, ...objects.slice(2));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static enableCorpusIsPublicRequest(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>Hier könnte eine Warnung stehen</h4>
 | 
					 | 
				
			||||||
              <p></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">Confirm</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}/enable_is_public`, {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);}
 | 
					 | 
				
			||||||
              app.flash(`Corpus "${corpusTitle}" is public now`, 'corpus');
 | 
					 | 
				
			||||||
              resolve(response);
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            (response) => {
 | 
					 | 
				
			||||||
              app.flash('Something went wrong', 'error');
 | 
					 | 
				
			||||||
              reject(response);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      modal.open();
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static disableCorpusIsPublicRequest(userId, corpusId) {
 | 
					 | 
				
			||||||
    return new Promise((resolve, reject) => {
 | 
					 | 
				
			||||||
      let corpus;
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        corpus = app.data.users[userId].corpora[corpusId];
 | 
					 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        corpus = {};
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      let corpusTitle = corpus?.title;
 | 
					 | 
				
			||||||
      fetch(`/corpora/${corpusId}/disable_is_public`, {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);}
 | 
					 | 
				
			||||||
            app.flash(`Corpus "${corpusTitle}" is private now`, 'corpus');
 | 
					 | 
				
			||||||
            resolve(response);
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          (response) => {
 | 
					 | 
				
			||||||
            app.flash('Something went wrong', 'error');
 | 
					 | 
				
			||||||
            reject(response);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static buildCorpusRequest(userId, corpusId) {
 | 
					 | 
				
			||||||
    return new Promise((resolve, reject) => {
 | 
					 | 
				
			||||||
      let corpus;
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        corpus = app.data.users[userId].corpora[corpusId];
 | 
					 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        corpus = {};
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      fetch(`/corpora/${corpusId}/build`, {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);}
 | 
					 | 
				
			||||||
            if (response.status === 409) {app.flash('Conflict', 'error'); reject(response);}
 | 
					 | 
				
			||||||
            app.flash(`Corpus "${corpus?.title}" marked for building`, 'corpus');
 | 
					 | 
				
			||||||
            resolve(response);
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          (response) => {
 | 
					 | 
				
			||||||
            app.flash('Something went wrong', 'error');
 | 
					 | 
				
			||||||
            reject(response);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  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 deleteSpaCyNLPPipelineModelRequest(userId, spaCyNLPPipelineModelId) {
 | 
					 | 
				
			||||||
    return new Promise((resolve, reject) => {
 | 
					 | 
				
			||||||
      let spaCyNLPPipelineModel;
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        spaCyNLPPipelineModel = app.data.users[userId].spacy_nlp_pipeline_models[spaCyNLPPipelineModelId];
 | 
					 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        spaCyNLPPipelineModel = {};
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      let modalElement = Utils.HTMLToElement(
 | 
					 | 
				
			||||||
        `
 | 
					 | 
				
			||||||
          <div class="modal">
 | 
					 | 
				
			||||||
            <div class="modal-content">
 | 
					 | 
				
			||||||
              <h4>Confirm SpaCy NLP Pipeline Model deletion</h4>
 | 
					 | 
				
			||||||
              <p>Do you really want to delete the SpaCy NLP Pipeline Model <b>${spaCyNLPPipelineModel?.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 spaCyNLPPipelineModelTitle = spaCyNLPPipelineModel?.title;
 | 
					 | 
				
			||||||
        fetch(`/contributions/spacy-nlp-pipeline-models/${spaCyNLPPipelineModelId}`, {method: 'DELETE'})
 | 
					 | 
				
			||||||
          .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(`SpaCy NLP Pipeline Model "${spaCyNLPPipelineModelTitle}" marked for deletion`);
 | 
					 | 
				
			||||||
              resolve(response);
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            (response) => {
 | 
					 | 
				
			||||||
              app.flash('Something went wrong', 'error');
 | 
					 | 
				
			||||||
              reject(response);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      modal.open();
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static deleteTesseractOCRPipelineModelRequest(userId, tesseractOCRPipelineModelId) {
 | 
					 | 
				
			||||||
    return new Promise((resolve, reject) => {
 | 
					 | 
				
			||||||
      let tesseractOCRPipelineModel;
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        tesseractOCRPipelineModel = app.data.users[userId].tesseract_ocr_pipeline_models[tesseractOCRPipelineModelId];
 | 
					 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        tesseractOCRPipelineModel = {};
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      let modalElement = Utils.HTMLToElement(
 | 
					 | 
				
			||||||
        `
 | 
					 | 
				
			||||||
          <div class="modal">
 | 
					 | 
				
			||||||
            <div class="modal-content">
 | 
					 | 
				
			||||||
              <h4>Confirm Tesseract OCR Pipeline Model deletion</h4>
 | 
					 | 
				
			||||||
              <p>Do you really want to delete the Tesseract OCR Pipeline Model <b>${tesseractOCRPipelineModel?.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 tesseractOCRPipelineModelTitle = tesseractOCRPipelineModel?.title;
 | 
					 | 
				
			||||||
        fetch(`/contributions/tesseract-ocr-pipeline-models/${tesseractOCRPipelineModelId}`, {method: 'DELETE'})
 | 
					 | 
				
			||||||
          .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(`Tesseract OCR Pipeline Model "${tesseractOCRPipelineModelTitle}" marked for deletion`);
 | 
					 | 
				
			||||||
              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(
 | 
					 | 
				
			||||||
        `
 | 
					 | 
				
			||||||
          <div class="modal">
 | 
					 | 
				
			||||||
            <div class="modal-content">
 | 
					 | 
				
			||||||
              <h4>Confirm Avatar deletion</h4>
 | 
					 | 
				
			||||||
              <p>Do you really want to delete your Avatar?</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) => {
 | 
					 | 
				
			||||||
        fetch(`/users/${userId}/avatar`, {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(`Avatar marked for deletion`);
 | 
					 | 
				
			||||||
              resolve(response);
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            (response) => {
 | 
					 | 
				
			||||||
              app.flash('Something went wrong', 'error');
 | 
					 | 
				
			||||||
              reject(response);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      modal.open();
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static deleteJobRequest(userId, jobId) {
 | 
					 | 
				
			||||||
    return new Promise((resolve, reject) => {
 | 
					 | 
				
			||||||
      let job;
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        job = app.data.users[userId].jobs[jobId];
 | 
					 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        job = {};
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      let modalElement = Utils.HTMLToElement(
 | 
					 | 
				
			||||||
        `
 | 
					 | 
				
			||||||
          <div class="modal">
 | 
					 | 
				
			||||||
            <div class="modal-content">
 | 
					 | 
				
			||||||
              <h4>Confirm Job deletion</h4>
 | 
					 | 
				
			||||||
              <p>Do you really want to delete the Job <b>${job?.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 jobTitle = job?.title;
 | 
					 | 
				
			||||||
        fetch(`/jobs/${jobId}`, {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(`Job "${jobTitle}" marked for deletion`, 'job');
 | 
					 | 
				
			||||||
              resolve(response);
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            (response) => {
 | 
					 | 
				
			||||||
              app.flash('Something went wrong', 'error');
 | 
					 | 
				
			||||||
              reject(response);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      modal.open();
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static getJobLogRequest(userId, jobId) {
 | 
					 | 
				
			||||||
    return new Promise((resolve, reject) => {
 | 
					 | 
				
			||||||
      fetch(`/jobs/${jobId}/log`, {method: 'GET', headers: {Accept: 'application/json, text/plain'}})
 | 
					 | 
				
			||||||
        .then(
 | 
					 | 
				
			||||||
          (response) => {
 | 
					 | 
				
			||||||
            if (response.status === 403) {app.flash('Forbidden', 'error'); reject(response);}
 | 
					 | 
				
			||||||
            if (response.status === 404) {app.flash('Not Found', 'error'); reject(response);}
 | 
					 | 
				
			||||||
            return response.text();
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          (response) => {
 | 
					 | 
				
			||||||
            app.flash('Something went wrong', 'error');
 | 
					 | 
				
			||||||
            reject(response);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .then(
 | 
					 | 
				
			||||||
          (text) => {
 | 
					 | 
				
			||||||
            let modalElement = Utils.HTMLToElement(
 | 
					 | 
				
			||||||
              `
 | 
					 | 
				
			||||||
                <div class="modal">
 | 
					 | 
				
			||||||
                  <div class="modal-content">
 | 
					 | 
				
			||||||
                    <h4>Job logs</h4>
 | 
					 | 
				
			||||||
                    <pre><code>${text}</code></pre>
 | 
					 | 
				
			||||||
                  </div>
 | 
					 | 
				
			||||||
                  <div class="modal-footer">
 | 
					 | 
				
			||||||
                    <a class="btn modal-close waves-effect waves-light">Close</a>
 | 
					 | 
				
			||||||
                  </div>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
              `
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
            document.querySelector('#modals').appendChild(modalElement);
 | 
					 | 
				
			||||||
            let modal = M.Modal.init(
 | 
					 | 
				
			||||||
              modalElement,
 | 
					 | 
				
			||||||
              {
 | 
					 | 
				
			||||||
                onCloseEnd: () => {
 | 
					 | 
				
			||||||
                  modal.destroy();
 | 
					 | 
				
			||||||
                  modalElement.remove();
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
            modal.open();
 | 
					 | 
				
			||||||
            resolve(text);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static restartJobRequest(userId, jobId) {
 | 
					 | 
				
			||||||
    return new Promise((resolve, reject) => {
 | 
					 | 
				
			||||||
      let job;
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        job = app.data.users[userId].jobs[jobId];
 | 
					 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        job = {};
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      let modalElement = Utils.HTMLToElement(
 | 
					 | 
				
			||||||
        `
 | 
					 | 
				
			||||||
          <div class="modal">
 | 
					 | 
				
			||||||
            <div class="modal-content">
 | 
					 | 
				
			||||||
              <h4>Confirm Job restart</h4>
 | 
					 | 
				
			||||||
              <p>Do you really want to restart the Job <b>${job?.title}</b>? All Job Results 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">Restart</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 jobTitle = job?.title;
 | 
					 | 
				
			||||||
        fetch(`/jobs/${jobId}/restart`, {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);}
 | 
					 | 
				
			||||||
              if (response.status === 409) {app.flash('Conflict', 'error'); reject(response);}
 | 
					 | 
				
			||||||
              app.flash(`Job "${jobTitle}" restarted.`, 'job');
 | 
					 | 
				
			||||||
              resolve(response);
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            (response) => {
 | 
					 | 
				
			||||||
              app.flash('Something went wrong', 'error');
 | 
					 | 
				
			||||||
              reject(response);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      modal.open();
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static deleteUserRequest(userId) {
 | 
					 | 
				
			||||||
    return new Promise((resolve, reject) => {
 | 
					 | 
				
			||||||
      let user;
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        user = app.data.users[userId];
 | 
					 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        user = {};
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      let modalElement = Utils.HTMLToElement(
 | 
					 | 
				
			||||||
        `
 | 
					 | 
				
			||||||
          <div class="modal">
 | 
					 | 
				
			||||||
            <div class="modal-content">
 | 
					 | 
				
			||||||
              <h4>Confirm User deletion</h4>
 | 
					 | 
				
			||||||
              <p>Do you really want to delete the User <b>${user?.username}</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 userName = user?.username;
 | 
					 | 
				
			||||||
        fetch(`/users/${userId}`, {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(`User "${userName}" marked for deletion`);
 | 
					 | 
				
			||||||
              resolve(response);
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            (response) => {
 | 
					 | 
				
			||||||
              app.flash('Something went wrong', 'error');
 | 
					 | 
				
			||||||
              reject(response);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      modal.open();
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static tesseractOCRPipelineModelToggleIsPublicRequest(userId, tesseractOCRPipelineModelId, is_public) {
 | 
					 | 
				
			||||||
    return new Promise((resolve, reject) => {
 | 
					 | 
				
			||||||
      let tesseractOCRPipelineModel;
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        tesseractOCRPipelineModel = app.data.users[userId].tesseract_ocr_pipeline_models[tesseractOCRPipelineModelId];
 | 
					 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        tesseractOCRPipelineModel = {};
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
      fetch(`/contributions/tesseract-ocr-pipeline-models/${tesseractOCRPipelineModelId}/toggle-public-status`, {method: 'POST', headers: {Accept: 'application/json'}})
 | 
					 | 
				
			||||||
        .then(
 | 
					 | 
				
			||||||
          (response) => {
 | 
					 | 
				
			||||||
            if (response.status === 403) {
 | 
					 | 
				
			||||||
              app.flash('Forbidden', 'error');
 | 
					 | 
				
			||||||
              reject(response);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            resolve(response);
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          (response) => {
 | 
					 | 
				
			||||||
            app.flash('Something went wrong', 'error');
 | 
					 | 
				
			||||||
            reject(response);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static spaCyNLPPipelineModelToggleIsPublicRequest(userId, spaCyNLPPipelineModelId) {
 | 
					 | 
				
			||||||
    return new Promise((resolve, reject) => {
 | 
					 | 
				
			||||||
      let spaCyNLPPipelineModel;
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        spaCyNLPPipelineModel = app.data.users[userId].spacy_nlp_pipeline_models[spaCyNLPPipelineModelId];
 | 
					 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        spaCyNLPPipelineModel = {};
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      fetch(`/contributions/spacy-nlp-pipeline-models/${spaCyNLPPipelineModelId}/toggle-public-status`, {method: 'POST', headers: {Accept: 'application/json'}})
 | 
					 | 
				
			||||||
        .then(
 | 
					 | 
				
			||||||
          (response) => {
 | 
					 | 
				
			||||||
            if (response.status === 403) {
 | 
					 | 
				
			||||||
              app.flash('Forbidden', 'error');
 | 
					 | 
				
			||||||
              reject(response);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            resolve(response);
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          (response) => {
 | 
					 | 
				
			||||||
            app.flash('Something went wrong', 'error');
 | 
					 | 
				
			||||||
            reject(response);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,29 +8,32 @@
 | 
				
			|||||||
        <img class="hide-on-small-only" src="{{ url_for('static', filename='images/nopaque_-_logo_name_slogan.svg') }}" style="height: 128px; margin-top: -32px; margin-left: -32px;">
 | 
					        <img class="hide-on-small-only" src="{{ url_for('static', filename='images/nopaque_-_logo_name_slogan.svg') }}" style="height: 128px; margin-top: -32px; margin-left: -32px;">
 | 
				
			||||||
        <img class="hide-on-med-and-up" src="{{ url_for('static', filename='images/nopaque_-_logo.svg') }}" style="height: 128px; margin-top: -32px; margin-left: -32px;">
 | 
					        <img class="hide-on-med-and-up" src="{{ url_for('static', filename='images/nopaque_-_logo.svg') }}" style="height: 128px; margin-top: -32px; margin-left: -32px;">
 | 
				
			||||||
      </a>
 | 
					      </a>
 | 
				
			||||||
      <ul class="right">
 | 
					      <ul class="right hide-on-med-and-down">
 | 
				
			||||||
 | 
					        <li><a href="{{ url_for('main.news') }}"><i class="material-icons left">email</i>News</a></li>
 | 
				
			||||||
        <li><a class="dropdown-trigger no-autoinit" data-target="nav-more-dropdown" href="#!" id="nav-more-dropdown-trigger"><i class="material-icons">more_vert</i></a></li>
 | 
					        <li><a class="dropdown-trigger no-autoinit" data-target="nav-more-dropdown" href="#!" id="nav-more-dropdown-trigger"><i class="material-icons">more_vert</i></a></li>
 | 
				
			||||||
      </ul>
 | 
					      </ul>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="nav-content primary-variant-color">
 | 
					    <div class="nav-content primary-variant-color">
 | 
				
			||||||
      <ul class="tabs tabs-transparent">
 | 
					      <ul class="tabs tabs-transparent">
 | 
				
			||||||
        <li class="tab"><a href="{{ url_for('main.index') }}" target="_self"><i class="material-icons">home</i></a></li>
 | 
					        {%- for breadcrumb in breadcrumbs -%}
 | 
				
			||||||
        {% if breadcrumbs is defined %}
 | 
					        <li class="tab"><a {{ 'class="active"' if loop.last }} href="{{ breadcrumb.url }}" target="_self">{{ breadcrumb.text }}</a></li>
 | 
				
			||||||
        {{ breadcrumbs }}
 | 
					        {% if not loop.last %}
 | 
				
			||||||
 | 
					        <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
 | 
				
			||||||
        {% endif %}
 | 
					        {% endif %}
 | 
				
			||||||
 | 
					        {%- endfor -%}
 | 
				
			||||||
      </ul>
 | 
					      </ul>
 | 
				
			||||||
      {% if current_user.is_authenticated %}
 | 
					      {# {% if current_user.is_authenticated %}
 | 
				
			||||||
      <a class="btn-floating btn-large halfway-fab modal-trigger pink tooltipped waves-effect waves-light" data-tooltip="Roadmap" href="#roadmap-modal"><i class="material-icons">explore</i></a>
 | 
					      <a class="btn-floating btn-large halfway-fab modal-trigger pink tooltipped waves-effect waves-light" data-tooltip="Roadmap" href="#roadmap-modal"><i class="material-icons">explore</i></a>
 | 
				
			||||||
      {% endif %}
 | 
					      {% endif %} #}
 | 
				
			||||||
 | 
					      <a class="btn-floating btn-large halfway-fab modal-trigger pink tooltipped waves-effect waves-light" data-tooltip="Manual" href="#manual-modal"><i class="material-icons">help</i></a>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </nav>
 | 
					  </nav>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<ul class="dropdown-content" id="nav-more-dropdown">
 | 
					<ul class="dropdown-content" id="nav-more-dropdown">
 | 
				
			||||||
  <li><a href="{{ url_for('main.user_manual') }}"><i class="material-icons left">help</i>Manual</a></li>
 | 
					  {# <li><a href="{{ url_for('main.user_manual') }}"><i class="material-icons left">help</i>Manual</a></li> #}
 | 
				
			||||||
  {% if current_user.is_authenticated %}
 | 
					  {% if current_user.is_authenticated %}
 | 
				
			||||||
  <li><a href="{{ url_for('settings.settings') }}"><i class="material-icons left">settings</i>General settings</a></li>
 | 
					  <li><a href="{{ url_for('settings.settings') }}"><i class="material-icons left">settings</i>Settings</a></li>
 | 
				
			||||||
  <li><a href="{{ url_for('users.edit_profile', user_id=current_user.id) }}"><i class="material-icons left">contact_page</i>Profile settings</a></li>
 | 
					 | 
				
			||||||
  <li class="divider" tabindex="-1"></li>
 | 
					  <li class="divider" tabindex="-1"></li>
 | 
				
			||||||
  <li><a href="{{ url_for('auth.logout') }}">Log out</a></li>
 | 
					  <li><a href="{{ url_for('auth.logout') }}">Log out</a></li>
 | 
				
			||||||
  {% else %}
 | 
					  {% else %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,7 +21,7 @@
 | 
				
			|||||||
      <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
 | 
					      <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
 | 
				
			||||||
      {% if corpus %}
 | 
					      {% if corpus %}
 | 
				
			||||||
      {% if corpus.files.all() %}
 | 
					      {% if corpus.files.all() %}
 | 
				
			||||||
      <li class="tab"><a{%if request.path == url_for('corpora.analyse_corpus', corpus_id=corpus.id) %} class="active"{% endif %} href="{{ url_for('corpora.analyse_corpus', corpus_id=corpus.id) }}" target="_self">Corpus analysis</a></li>
 | 
					      <li class="tab"><a{%if request.path == url_for('corpora.analysis', corpus_id=corpus.id) %} class="active"{% endif %} href="{{ url_for('corpora.analysis', corpus_id=corpus.id) }}" target="_self">Corpus analysis</a></li>
 | 
				
			||||||
      {% else %}
 | 
					      {% else %}
 | 
				
			||||||
      <li class="tab disabled tooltipped" data-tooltip="Create at least one corpus file first"><a>Corpus analysis</a></li>
 | 
					      <li class="tab disabled tooltipped" data-tooltip="Create at least one corpus file first"><a>Corpus analysis</a></li>
 | 
				
			||||||
      {% endif %}
 | 
					      {% endif %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,21 +6,39 @@
 | 
				
			|||||||
  output='gen/app.%(version)s.js',
 | 
					  output='gen/app.%(version)s.js',
 | 
				
			||||||
  'js/App.js',
 | 
					  'js/App.js',
 | 
				
			||||||
  'js/Utils.js',
 | 
					  'js/Utils.js',
 | 
				
			||||||
  'js/Forms/Form.js',
 | 
					 | 
				
			||||||
  'js/Forms/CreateCorpusFileForm.js',
 | 
					 | 
				
			||||||
  'js/Forms/CreateJobForm.js',
 | 
					 | 
				
			||||||
  'js/Forms/CreateContributionForm.js',
 | 
					 | 
				
			||||||
  'js/CorpusAnalysis/CQiClient.js',
 | 
					  'js/CorpusAnalysis/CQiClient.js',
 | 
				
			||||||
  'js/CorpusAnalysis/CorpusAnalysisApp.js',
 | 
					  'js/CorpusAnalysis/CorpusAnalysisApp.js',
 | 
				
			||||||
  'js/CorpusAnalysis/CorpusAnalysisConcordance.js',
 | 
					  'js/CorpusAnalysis/CorpusAnalysisConcordance.js',
 | 
				
			||||||
  'js/CorpusAnalysis/CorpusAnalysisReader.js',
 | 
					  'js/CorpusAnalysis/CorpusAnalysisReader.js',
 | 
				
			||||||
  'js/CorpusAnalysis/QueryBuilder.js',
 | 
					  'js/CorpusAnalysis/QueryBuilder.js',
 | 
				
			||||||
  'js/RessourceDisplays/RessourceDisplay.js',
 | 
					  'js/XMLtoObject.js'
 | 
				
			||||||
  'js/RessourceDisplays/CorpusDisplay.js',
 | 
					%}
 | 
				
			||||||
  'js/RessourceDisplays/JobDisplay.js',
 | 
					<script src="{{ ASSET_URL }}"></script>
 | 
				
			||||||
 | 
					{%- endassets %}
 | 
				
			||||||
 | 
					{%- assets
 | 
				
			||||||
 | 
					  filters='rjsmin',
 | 
				
			||||||
 | 
					  output='gen/Forms.%(version)s.js',
 | 
				
			||||||
 | 
					  'js/Forms/Form.js',
 | 
				
			||||||
 | 
					  'js/Forms/CreateCorpusFileForm.js',
 | 
				
			||||||
 | 
					  'js/Forms/CreateJobForm.js',
 | 
				
			||||||
 | 
					  'js/Forms/CreateContributionForm.js'
 | 
				
			||||||
 | 
					%}
 | 
				
			||||||
 | 
					<script src="{{ ASSET_URL }}"></script>
 | 
				
			||||||
 | 
					{%- endassets %}
 | 
				
			||||||
 | 
					{%- assets
 | 
				
			||||||
 | 
					  filters='rjsmin',
 | 
				
			||||||
 | 
					  output='gen/ResourceDisplays.%(version)s.js',
 | 
				
			||||||
 | 
					  'js/ResourceDisplays/ResourceDisplay.js',
 | 
				
			||||||
 | 
					  'js/ResourceDisplays/CorpusDisplay.js',
 | 
				
			||||||
 | 
					  'js/ResourceDisplays/JobDisplay.js'
 | 
				
			||||||
 | 
					%}
 | 
				
			||||||
 | 
					<script src="{{ ASSET_URL }}"></script>
 | 
				
			||||||
 | 
					{%- endassets %}
 | 
				
			||||||
 | 
					{%- assets
 | 
				
			||||||
 | 
					  filters='rjsmin',
 | 
				
			||||||
 | 
					  output='gen/ResourceLists.%(version)s.js',
 | 
				
			||||||
  'js/ResourceLists/ResourceList.js',
 | 
					  'js/ResourceLists/ResourceList.js',
 | 
				
			||||||
  'js/ResourceLists/CorpusFileList.js',
 | 
					  'js/ResourceLists/CorpusFileList.js',
 | 
				
			||||||
  'js/ResourceLists/PublicCorpusFileList.js',
 | 
					 | 
				
			||||||
  'js/ResourceLists/CorpusList.js',
 | 
					  'js/ResourceLists/CorpusList.js',
 | 
				
			||||||
  'js/ResourceLists/PublicCorpusList.js',
 | 
					  'js/ResourceLists/PublicCorpusList.js',
 | 
				
			||||||
  'js/ResourceLists/JobList.js',
 | 
					  'js/ResourceLists/JobList.js',
 | 
				
			||||||
@@ -30,7 +48,25 @@
 | 
				
			|||||||
  'js/ResourceLists/TesseractOCRPipelineModelList.js',
 | 
					  'js/ResourceLists/TesseractOCRPipelineModelList.js',
 | 
				
			||||||
  'js/ResourceLists/UserList.js',
 | 
					  'js/ResourceLists/UserList.js',
 | 
				
			||||||
  'js/ResourceLists/AdminUserList.js',
 | 
					  'js/ResourceLists/AdminUserList.js',
 | 
				
			||||||
  'js/XMLtoObject.js'
 | 
					  'js/ResourceLists/CorpusFollowerList.js',
 | 
				
			||||||
 | 
					  'js/ResourceLists/DetailledPublicCorpusList.js'
 | 
				
			||||||
 | 
					%}
 | 
				
			||||||
 | 
					<script src="{{ ASSET_URL }}"></script>
 | 
				
			||||||
 | 
					{%- endassets %}
 | 
				
			||||||
 | 
					{%- assets
 | 
				
			||||||
 | 
					  filters='rjsmin',
 | 
				
			||||||
 | 
					  output='gen/Requests.%(version)s.js',
 | 
				
			||||||
 | 
					  'js/Requests/Requests.js',
 | 
				
			||||||
 | 
					  'js/Requests/admin/admin.js',
 | 
				
			||||||
 | 
					  'js/Requests/contributions/contributions.js',
 | 
				
			||||||
 | 
					  'js/Requests/contributions/spacy_nlp_pipeline_models.js',
 | 
				
			||||||
 | 
					  'js/Requests/contributions/tesseract_ocr_pipeline_models.js',
 | 
				
			||||||
 | 
					  'js/Requests/corpora/corpora.js',
 | 
				
			||||||
 | 
					  'js/Requests/corpora/files.js',
 | 
				
			||||||
 | 
					  'js/Requests/corpora/followers.js',
 | 
				
			||||||
 | 
					  'js/Requests/jobs/jobs.js',
 | 
				
			||||||
 | 
					  'js/Requests/users/users.js',
 | 
				
			||||||
 | 
					  'js/Requests/users/settings.js'
 | 
				
			||||||
%}
 | 
					%}
 | 
				
			||||||
<script src="{{ ASSET_URL }}"></script>
 | 
					<script src="{{ ASSET_URL }}"></script>
 | 
				
			||||||
{%- endassets %}
 | 
					{%- endassets %}
 | 
				
			||||||
@@ -50,7 +86,13 @@
 | 
				
			|||||||
  for (let optionElement of document.querySelectorAll('option[value=""]')) {
 | 
					  for (let optionElement of document.querySelectorAll('option[value=""]')) {
 | 
				
			||||||
    optionElement.disabled = true;
 | 
					    optionElement.disabled = true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  for (let optgroupElement of document.querySelectorAll('optgroup[label=""]')) {
 | 
				
			||||||
 | 
					    for (let c of optgroupElement.children) {
 | 
				
			||||||
 | 
					      optgroupElement.parentElement.insertAdjacentElement('afterbegin', c);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    optgroupElement.remove();
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
  // Set the data-length attribute on textareas/inputs with the maxlength attribute
 | 
					  // Set the data-length attribute on textareas/inputs with the maxlength attribute
 | 
				
			||||||
  for (let inputElement of document.querySelectorAll('textarea[maxlength], input[maxlength]')) {
 | 
					  for (let inputElement of document.querySelectorAll('textarea[maxlength], input[maxlength]')) {
 | 
				
			||||||
    inputElement.dataset.length = inputElement.getAttribute('maxlength');
 | 
					    inputElement.dataset.length = inputElement.getAttribute('maxlength');
 | 
				
			||||||
@@ -70,4 +112,35 @@
 | 
				
			|||||||
  for (let [category, message] of {{ get_flashed_messages(with_categories=True)|tojson }}) {
 | 
					  for (let [category, message] of {{ get_flashed_messages(with_categories=True)|tojson }}) {
 | 
				
			||||||
    app.flash(message, message);
 | 
					    app.flash(message, message);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Initialize manual modal
 | 
				
			||||||
 | 
					  let manualModalTableOfContentsElement = document.querySelector('#manual-modal-table-of-contents');
 | 
				
			||||||
 | 
					  let manualModalTableOfContents = M.Tabs.init(manualModalTableOfContentsElement);
 | 
				
			||||||
 | 
					  let manualModalElement = document.querySelector('#manual-modal');
 | 
				
			||||||
 | 
					  let manualModal = M.Modal.init(
 | 
				
			||||||
 | 
					    manualModalElement,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      onOpenStart: (manualModalElement, modalTriggerElement) => {
 | 
				
			||||||
 | 
					        if ('manualModalChapter' in modalTriggerElement.dataset) {
 | 
				
			||||||
 | 
					          manualModalTableOfContents.select(modalTriggerElement.dataset.manualModalChapter);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Initialize terms of use modal
 | 
				
			||||||
 | 
					  const termsOfUseModal = document.getElementById('terms-of-use-modal');
 | 
				
			||||||
 | 
					  M.Modal.init(
 | 
				
			||||||
 | 
					    termsOfUseModal, 
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      dismissible: false, 
 | 
				
			||||||
 | 
					      onCloseEnd: () => {
 | 
				
			||||||
 | 
					        Requests.users.entity.acceptTermsOfUse();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  {% if current_user.is_authenticated and not current_user.terms_of_use_accepted %}
 | 
				
			||||||
 | 
					      termsOfUseModal.M_Modal.open();
 | 
				
			||||||
 | 
					  {% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 
 | 
				
			|||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user