mirror of
https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
synced 2025-07-03 03:10:33 +00:00
Compare commits
46 Commits
6aacac2419
...
master
Author | SHA1 | Date | |
---|---|---|---|
41a88fce33 | |||
56844e0898 | |||
c28d534942 | |||
80604bf8de | |||
d4cd313940 | |||
c405061574 | |||
6c1f48eb2f | |||
cda28910f5 | |||
9a805b9d14 | |||
16bf891654 | |||
cb53b27ebf | |||
6684257bc4 | |||
0d1805fb76 | |||
bb60a2ba67 | |||
328f85ba52 | |||
93344c9573 | |||
1372c86609 | |||
713a7645db | |||
0c64c07925 | |||
a6ddf4c980 | |||
cab5f7ea05 | |||
07f09cdbd9 | |||
c97b2a886e | |||
df2bffe0fd | |||
aafb3ca3ec | |||
12a3ac1d5d | |||
a2904caea2 | |||
e325552100 | |||
e269156925 | |||
9c9de242ca | |||
ec54fdc3bb | |||
2263a8d27d | |||
143cdd91f9 | |||
b5f7478e14 | |||
a95b8d979d | |||
18d5ab160e | |||
7439edacef | |||
99d7a8bdfc | |||
54c4295bf7 | |||
1e5c26b8e3 | |||
9f56647cf7 | |||
460257294d | |||
2c43333c94 | |||
fc8b11fa66 | |||
a8ab1bee71 | |||
ee7f64f5be |
16
.vscode/settings.json
vendored
16
.vscode/settings.json
vendored
@ -1,10 +1,24 @@
|
|||||||
{
|
{
|
||||||
"editor.rulers": [79],
|
"editor.rulers": [79],
|
||||||
"editor.tabSize": 4,
|
"editor.tabSize": 4,
|
||||||
|
"emmet.includeLanguages": {
|
||||||
|
"jinja-html": "html"
|
||||||
|
},
|
||||||
|
"files.associations": {
|
||||||
|
".flaskenv": "env",
|
||||||
|
"*.env.tpl": "env",
|
||||||
|
"*.txt.j2": "jinja"
|
||||||
|
},
|
||||||
"files.insertFinalNewline": true,
|
"files.insertFinalNewline": true,
|
||||||
"files.trimFinalNewlines": true,
|
"files.trimFinalNewlines": true,
|
||||||
"files.trimTrailingWhitespace": true,
|
"files.trimTrailingWhitespace": true,
|
||||||
|
"[html]": {
|
||||||
|
"editor.tabSize": 2
|
||||||
|
},
|
||||||
"[javascript]": {
|
"[javascript]": {
|
||||||
"editor.tabSize": 2,
|
"editor.tabSize": 2
|
||||||
|
},
|
||||||
|
"[jinja-html]": {
|
||||||
|
"editor.tabSize": 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ from config import Config
|
|||||||
from docker import DockerClient
|
from docker import DockerClient
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask.logging import default_handler
|
from flask.logging import default_handler
|
||||||
|
from flask_admin import Admin
|
||||||
from flask_apscheduler import APScheduler
|
from flask_apscheduler import APScheduler
|
||||||
from flask_assets import Environment
|
from flask_assets import Environment
|
||||||
from flask_login import LoginManager
|
from flask_login import LoginManager
|
||||||
@ -15,10 +16,12 @@ from flask_sqlalchemy import SQLAlchemy
|
|||||||
from flask_hashids import Hashids
|
from flask_hashids import Hashids
|
||||||
from logging import Formatter, StreamHandler
|
from logging import Formatter, StreamHandler
|
||||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
|
from .extensions.nopaque_flask_admin_views import AdminIndexView, ModelView
|
||||||
|
|
||||||
|
|
||||||
docker_client = DockerClient.from_env()
|
docker_client = DockerClient.from_env()
|
||||||
|
|
||||||
|
admin = Admin()
|
||||||
apifairy = APIFairy()
|
apifairy = APIFairy()
|
||||||
assets = Environment()
|
assets = Environment()
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
@ -74,6 +77,7 @@ def create_app(config: Config = Config) -> Flask:
|
|||||||
|
|
||||||
from .models import AnonymousUser, User
|
from .models import AnonymousUser, User
|
||||||
|
|
||||||
|
admin.init_app(app, index_view=AdminIndexView())
|
||||||
apifairy.init_app(app)
|
apifairy.init_app(app)
|
||||||
assets.init_app(app)
|
assets.init_app(app)
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
@ -92,9 +96,6 @@ def create_app(config: Config = Config) -> Flask:
|
|||||||
# endregion Extensions
|
# endregion Extensions
|
||||||
|
|
||||||
# region Blueprints
|
# region Blueprints
|
||||||
from .blueprints.admin import bp as admin_blueprint
|
|
||||||
app.register_blueprint(admin_blueprint, url_prefix='/admin')
|
|
||||||
|
|
||||||
from .blueprints.api import bp as api_blueprint
|
from .blueprints.api import bp as api_blueprint
|
||||||
app.register_blueprint(api_blueprint, url_prefix='/api')
|
app.register_blueprint(api_blueprint, url_prefix='/api')
|
||||||
|
|
||||||
@ -104,15 +105,6 @@ def create_app(config: Config = Config) -> Flask:
|
|||||||
from .blueprints.contributions import bp as contributions_blueprint
|
from .blueprints.contributions import bp as contributions_blueprint
|
||||||
app.register_blueprint(contributions_blueprint, url_prefix='/contributions')
|
app.register_blueprint(contributions_blueprint, url_prefix='/contributions')
|
||||||
|
|
||||||
from .blueprints.spacy_nlp_pipeline_models import bp as spacy_nlp_pipeline_models_blueprint
|
|
||||||
app.register_blueprint(spacy_nlp_pipeline_models_blueprint, url_prefix='/spacy-nlp-pipeline-models')
|
|
||||||
|
|
||||||
from .blueprints.tesseract_ocr_pipeline_models import bp as tesseract_ocr_pipeline_models_blueprint
|
|
||||||
app.register_blueprint(tesseract_ocr_pipeline_models_blueprint, url_prefix='/tesseract-ocr-pipeline-models')
|
|
||||||
|
|
||||||
from.blueprints.transkribus_htr_pipeline_models import bp as transkribus_htr_pipeline_models_blueprint
|
|
||||||
app.register_blueprint(transkribus_htr_pipeline_models_blueprint, url_prefix='/transkribus-htr-pipeline-models')
|
|
||||||
|
|
||||||
from .blueprints.corpora import bp as corpora_blueprint
|
from .blueprints.corpora import bp as corpora_blueprint
|
||||||
app.register_blueprint(corpora_blueprint, cli_group='corpus', url_prefix='/corpora')
|
app.register_blueprint(corpora_blueprint, cli_group='corpus', url_prefix='/corpora')
|
||||||
|
|
||||||
@ -136,14 +128,15 @@ def create_app(config: Config = Config) -> Flask:
|
|||||||
|
|
||||||
from .blueprints.workshops import bp as workshops_blueprint
|
from .blueprints.workshops import bp as workshops_blueprint
|
||||||
app.register_blueprint(workshops_blueprint, url_prefix='/workshops')
|
app.register_blueprint(workshops_blueprint, url_prefix='/workshops')
|
||||||
|
|
||||||
|
from .models import _models
|
||||||
|
for model in _models:
|
||||||
|
admin.add_view(ModelView(model, db.session, category='Database'))
|
||||||
# endregion Blueprints
|
# endregion Blueprints
|
||||||
|
|
||||||
# region SocketIO Namespaces
|
# region SocketIO Namespaces
|
||||||
from .namespaces.cqi_over_sio import CQiOverSocketIONamespace
|
from .namespaces.cqi_over_sio import CQiOverSocketIONamespace
|
||||||
socketio.on_namespace(CQiOverSocketIONamespace('/cqi_over_sio'))
|
socketio.on_namespace(CQiOverSocketIONamespace('/cqi_over_sio'))
|
||||||
|
|
||||||
from .namespaces.users import UsersNamespace
|
|
||||||
socketio.on_namespace(UsersNamespace('/users'))
|
|
||||||
# endregion SocketIO Namespaces
|
# endregion SocketIO Namespaces
|
||||||
|
|
||||||
# region Database event Listeners
|
# region Database event Listeners
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
from flask import Blueprint
|
|
||||||
from flask_login import login_required
|
|
||||||
from app.decorators import admin_required
|
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint('admin', __name__)
|
|
||||||
|
|
||||||
|
|
||||||
@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,16 +0,0 @@
|
|||||||
from flask_wtf import FlaskForm
|
|
||||||
from wtforms import SelectField, SubmitField
|
|
||||||
from app.models import Role
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateUserForm(FlaskForm):
|
|
||||||
role = SelectField('Role')
|
|
||||||
submit = SubmitField()
|
|
||||||
|
|
||||||
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)
|
|
||||||
self.role.choices = [(x.hashid, x.name) for x in Role.query.all()]
|
|
@ -1,23 +0,0 @@
|
|||||||
from flask import abort, request
|
|
||||||
from app.decorators import content_negotiation
|
|
||||||
from app import db
|
|
||||||
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,136 +0,0 @@
|
|||||||
from flask import abort, flash, redirect, render_template, url_for
|
|
||||||
from app import db, hashids
|
|
||||||
from app.models import Avatar, Corpus, Role, User
|
|
||||||
from app.blueprints.users.settings.forms import (
|
|
||||||
UpdateAvatarForm,
|
|
||||||
UpdatePasswordForm,
|
|
||||||
UpdateNotificationsForm,
|
|
||||||
UpdateAccountInformationForm,
|
|
||||||
UpdateProfileInformationForm
|
|
||||||
)
|
|
||||||
from . import bp
|
|
||||||
from .forms import UpdateUserForm
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('')
|
|
||||||
def admin():
|
|
||||||
return render_template(
|
|
||||||
'admin/admin.html.j2',
|
|
||||||
title='Administration'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/corpora')
|
|
||||||
def corpora():
|
|
||||||
corpora = Corpus.query.all()
|
|
||||||
return render_template(
|
|
||||||
'admin/corpora.html.j2',
|
|
||||||
title='Corpora',
|
|
||||||
corpora=corpora
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/users')
|
|
||||||
def users():
|
|
||||||
users = User.query.all()
|
|
||||||
return render_template(
|
|
||||||
'admin/users.html.j2',
|
|
||||||
title='Users',
|
|
||||||
users=users
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/users/<hashid:user_id>')
|
|
||||||
def user(user_id):
|
|
||||||
user = User.query.get_or_404(user_id)
|
|
||||||
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>/settings', methods=['GET', 'POST'])
|
|
||||||
def user_settings(user_id):
|
|
||||||
user = User.query.get_or_404(user_id)
|
|
||||||
update_account_information_form = UpdateAccountInformationForm(user)
|
|
||||||
update_profile_information_form = UpdateProfileInformationForm(user)
|
|
||||||
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
|
|
||||||
)
|
|
||||||
except (AttributeError, OSError):
|
|
||||||
abort(500)
|
|
||||||
db.session.commit()
|
|
||||||
flash('Your changes have been saved')
|
|
||||||
return redirect(url_for('.user_settings', user_id=user.id))
|
|
||||||
# endregion handle update avatar form
|
|
||||||
|
|
||||||
# region handle update account information form
|
|
||||||
if update_account_information_form.submit.data and update_account_information_form.validate():
|
|
||||||
user.email = update_account_information_form.email.data
|
|
||||||
user.username = update_account_information_form.username.data
|
|
||||||
db.session.commit()
|
|
||||||
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)
|
|
||||||
db.session.commit()
|
|
||||||
flash('Your changes have been saved')
|
|
||||||
return redirect(url_for('.user_settings', user_id=user.id))
|
|
||||||
# endregion handle update user form
|
|
||||||
|
|
||||||
return render_template(
|
|
||||||
'admin/user_settings.html.j2',
|
|
||||||
title='Settings',
|
|
||||||
update_account_information_form=update_account_information_form,
|
|
||||||
update_avatar_form=update_avatar_form,
|
|
||||||
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
|
|
||||||
)
|
|
@ -1,5 +1,27 @@
|
|||||||
from flask import Blueprint
|
from flask import Blueprint, redirect, request, url_for
|
||||||
|
from flask_login import current_user
|
||||||
|
from app import db
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint('auth', __name__)
|
bp = Blueprint('auth', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.before_app_request
|
||||||
|
def before_request():
|
||||||
|
if not current_user.is_authenticated:
|
||||||
|
return
|
||||||
|
|
||||||
|
current_user.ping()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
if (
|
||||||
|
not current_user.confirmed
|
||||||
|
and request.endpoint
|
||||||
|
and request.blueprint != 'auth'
|
||||||
|
and request.endpoint != 'static'
|
||||||
|
and request.endpoint != 'main.accept_terms_of_use'
|
||||||
|
):
|
||||||
|
return redirect(url_for('auth.unconfirmed'))
|
||||||
|
|
||||||
|
|
||||||
from . import routes
|
from . import routes
|
||||||
|
@ -60,7 +60,11 @@ class RegistrationForm(FlaskForm):
|
|||||||
|
|
||||||
def validate_username(self, field):
|
def validate_username(self, field):
|
||||||
if User.query.filter_by(username=field.data).first():
|
if User.query.filter_by(username=field.data).first():
|
||||||
raise ValidationError('Username already in use')
|
raise ValidationError('Username already registered')
|
||||||
|
|
||||||
|
def validate_terms_of_use_accepted(self, field):
|
||||||
|
if not field.data:
|
||||||
|
raise ValidationError('Terms of Use not accepted')
|
||||||
|
|
||||||
|
|
||||||
class LoginForm(FlaskForm):
|
class LoginForm(FlaskForm):
|
||||||
|
@ -12,26 +12,6 @@ from .forms import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.before_app_request
|
|
||||||
def before_request():
|
|
||||||
"""
|
|
||||||
Checks if a user is unconfirmed when visiting specific sites. Redirects to
|
|
||||||
unconfirmed view if user is unconfirmed.
|
|
||||||
"""
|
|
||||||
if not current_user.is_authenticated:
|
|
||||||
return
|
|
||||||
|
|
||||||
current_user.ping()
|
|
||||||
db.session.commit()
|
|
||||||
if (not current_user.confirmed
|
|
||||||
and request.endpoint
|
|
||||||
and request.blueprint != 'auth'
|
|
||||||
and request.endpoint != 'static'):
|
|
||||||
return redirect(url_for('auth.unconfirmed'))
|
|
||||||
if not current_user.terms_of_use_accepted:
|
|
||||||
return redirect(url_for('main.terms_of_use'))
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/register', methods=['GET', 'POST'])
|
@bp.route('/register', methods=['GET', 'POST'])
|
||||||
def register():
|
def register():
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
|
@ -16,3 +16,10 @@ def before_request():
|
|||||||
|
|
||||||
|
|
||||||
from . import routes
|
from . import routes
|
||||||
|
|
||||||
|
|
||||||
|
from .spacy_nlp_pipeline_models import bp as spacy_nlp_pipeline_models_bp
|
||||||
|
bp.register_blueprint(spacy_nlp_pipeline_models_bp, url_prefix='/spacy-nlp-pipeline-models')
|
||||||
|
|
||||||
|
from .tesseract_ocr_pipeline_models import bp as tesseract_ocr_pipeline_models_bp
|
||||||
|
bp.register_blueprint(tesseract_ocr_pipeline_models_bp, url_prefix='/tesseract-ocr-pipeline-models')
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from flask import redirect, url_for
|
from flask import render_template
|
||||||
from . import bp
|
from . import bp
|
||||||
|
|
||||||
|
|
||||||
@bp.route('')
|
@bp.route('')
|
||||||
def index():
|
def index():
|
||||||
return redirect(url_for('main.dashboard', _anchor='contributions'))
|
return render_template('contributions/index.html.j2', title='Contributions')
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
from flask_wtf.file import FileField, FileRequired
|
from flask_wtf.file import FileField, FileRequired
|
||||||
from wtforms import StringField, ValidationError
|
from wtforms import StringField, ValidationError
|
||||||
from wtforms.validators import InputRequired, Length
|
from wtforms.validators import InputRequired, Length
|
||||||
from app.blueprints.contributions.forms import ContributionBaseForm, UpdateContributionBaseForm
|
|
||||||
from app.blueprints.services import SERVICES
|
from app.blueprints.services import SERVICES
|
||||||
|
from ..forms import ContributionBaseForm, UpdateContributionBaseForm
|
||||||
|
|
||||||
|
|
||||||
class CreateSpaCyNLPPipelineModelForm(ContributionBaseForm):
|
class CreateSpaCyNLPPipelineModelForm(ContributionBaseForm):
|
@ -12,10 +12,7 @@ from .forms import (
|
|||||||
@bp.route('/')
|
@bp.route('/')
|
||||||
@login_required
|
@login_required
|
||||||
def index():
|
def index():
|
||||||
return render_template(
|
return redirect(url_for('contributions.index', _anchor='spacy-nlp-pipeline-models'))
|
||||||
'spacy_nlp_pipeline_models/index.html.j2',
|
|
||||||
title='SpaCy NLP Pipeline Models'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/create', methods=['GET', 'POST'])
|
@bp.route('/create', methods=['GET', 'POST'])
|
||||||
@ -46,7 +43,7 @@ def create():
|
|||||||
flash(f'SpaCy NLP Pipeline model "{snpm.title}" created')
|
flash(f'SpaCy NLP Pipeline model "{snpm.title}" created')
|
||||||
return {}, 201, {'Location': url_for('.index')}
|
return {}, 201, {'Location': url_for('.index')}
|
||||||
return render_template(
|
return render_template(
|
||||||
'spacy_nlp_pipeline_models/create.html.j2',
|
'contributions/spacy_nlp_pipeline_models/create.html.j2',
|
||||||
title='Create SpaCy NLP Pipeline Model',
|
title='Create SpaCy NLP Pipeline Model',
|
||||||
form=form
|
form=form
|
||||||
)
|
)
|
||||||
@ -54,7 +51,7 @@ def create():
|
|||||||
|
|
||||||
@bp.route('/<hashid:spacy_nlp_pipeline_model_id>', methods=['GET', 'POST'])
|
@bp.route('/<hashid:spacy_nlp_pipeline_model_id>', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def spacy_nlp_pipeline_model(spacy_nlp_pipeline_model_id):
|
def entity(spacy_nlp_pipeline_model_id):
|
||||||
snpm = SpaCyNLPPipelineModel.query.get_or_404(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):
|
if not (snpm.user == current_user or current_user.is_administrator):
|
||||||
abort(403)
|
abort(403)
|
||||||
@ -66,7 +63,7 @@ def spacy_nlp_pipeline_model(spacy_nlp_pipeline_model_id):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
return redirect(url_for('.index'))
|
return redirect(url_for('.index'))
|
||||||
return render_template(
|
return render_template(
|
||||||
'spacy_nlp_pipeline_models/spacy_nlp_pipeline_model.html.j2',
|
'contributions/spacy_nlp_pipeline_models/entity.html.j2',
|
||||||
title=f'{snpm.title} {snpm.version}',
|
title=f'{snpm.title} {snpm.version}',
|
||||||
form=form,
|
form=form,
|
||||||
spacy_nlp_pipeline_model=snpm
|
spacy_nlp_pipeline_model=snpm
|
@ -1,7 +1,7 @@
|
|||||||
from flask_wtf.file import FileField, FileRequired
|
from flask_wtf.file import FileField, FileRequired
|
||||||
from wtforms import ValidationError
|
from wtforms import ValidationError
|
||||||
from app.blueprints.contributions.forms import ContributionBaseForm, UpdateContributionBaseForm
|
|
||||||
from app.blueprints.services import SERVICES
|
from app.blueprints.services import SERVICES
|
||||||
|
from ..forms import ContributionBaseForm, UpdateContributionBaseForm
|
||||||
|
|
||||||
|
|
||||||
class CreateTesseractOCRPipelineModelForm(ContributionBaseForm):
|
class CreateTesseractOCRPipelineModelForm(ContributionBaseForm):
|
@ -11,10 +11,7 @@ from .forms import (
|
|||||||
|
|
||||||
@bp.route('/')
|
@bp.route('/')
|
||||||
def index():
|
def index():
|
||||||
return render_template(
|
return redirect(url_for('contributions.index', _anchor='tesseract-ocr-pipeline-models'))
|
||||||
'tesseract_ocr_pipeline_models/index.html.j2',
|
|
||||||
title='Tesseract OCR Pipeline Models'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/create', methods=['GET', 'POST'])
|
@bp.route('/create', methods=['GET', 'POST'])
|
||||||
@ -43,14 +40,14 @@ def create():
|
|||||||
flash(f'Tesseract OCR Pipeline model "{topm.title}" created')
|
flash(f'Tesseract OCR Pipeline model "{topm.title}" created')
|
||||||
return {}, 201, {'Location': url_for('.index')}
|
return {}, 201, {'Location': url_for('.index')}
|
||||||
return render_template(
|
return render_template(
|
||||||
'tesseract_ocr_pipeline_models/create.html.j2',
|
'contributions/tesseract_ocr_pipeline_models/create.html.j2',
|
||||||
title='Create Tesseract OCR Pipeline Model',
|
title='Create Tesseract OCR Pipeline Model',
|
||||||
form=form
|
form=form
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:tesseract_ocr_pipeline_model_id>', methods=['GET', 'POST'])
|
@bp.route('/<hashid:tesseract_ocr_pipeline_model_id>', methods=['GET', 'POST'])
|
||||||
def tesseract_ocr_pipeline_model(tesseract_ocr_pipeline_model_id):
|
def entity(tesseract_ocr_pipeline_model_id):
|
||||||
topm = TesseractOCRPipelineModel.query.get_or_404(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):
|
if not (topm.user == current_user or current_user.is_administrator):
|
||||||
abort(403)
|
abort(403)
|
||||||
@ -62,7 +59,7 @@ def tesseract_ocr_pipeline_model(tesseract_ocr_pipeline_model_id):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
return redirect(url_for('.index'))
|
return redirect(url_for('.index'))
|
||||||
return render_template(
|
return render_template(
|
||||||
'tesseract_ocr_pipeline_models/tesseract_ocr_pipeline_model.html.j2',
|
'contributions/tesseract_ocr_pipeline_models/entity.html.j2',
|
||||||
title=f'{topm.title} {topm.version}',
|
title=f'{topm.title} {topm.version}',
|
||||||
form=form,
|
form=form,
|
||||||
tesseract_ocr_pipeline_model=topm
|
tesseract_ocr_pipeline_model=topm
|
@ -16,4 +16,4 @@ def before_request():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
from . import cli, files, followers, routes, json_routes
|
from . import cli, files, followers, routes
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
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'}}
|
|
@ -1,125 +0,0 @@
|
|||||||
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
|
|
||||||
import nltk
|
|
||||||
from string import punctuation
|
|
||||||
|
|
||||||
|
|
||||||
@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('/stopwords')
|
|
||||||
@content_negotiation(produces='application/json')
|
|
||||||
def get_stopwords():
|
|
||||||
nltk.download('stopwords', quiet=True)
|
|
||||||
languages = ["german", "english", "catalan", "greek", "spanish", "french", "italian", "russian", "chinese"]
|
|
||||||
stopwords = {}
|
|
||||||
for language in languages:
|
|
||||||
stopwords[language] = nltk.corpus.stopwords.words(language)
|
|
||||||
stopwords['punctuation'] = list(punctuation) + ['—', '|', '–', '“', '„', '--']
|
|
||||||
stopwords['user_stopwords'] = []
|
|
||||||
response_data = stopwords
|
|
||||||
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,5 +1,19 @@
|
|||||||
from flask import abort, flash, redirect, render_template, url_for
|
from datetime import datetime
|
||||||
|
from flask import (
|
||||||
|
abort,
|
||||||
|
current_app,
|
||||||
|
flash,
|
||||||
|
Flask,
|
||||||
|
jsonify,
|
||||||
|
redirect,
|
||||||
|
request,
|
||||||
|
render_template,
|
||||||
|
url_for
|
||||||
|
)
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
from string import punctuation
|
||||||
|
from threading import Thread
|
||||||
|
import nltk
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import (
|
from app.models import (
|
||||||
Corpus,
|
Corpus,
|
||||||
@ -12,6 +26,21 @@ from .decorators import corpus_follower_permission_required
|
|||||||
from .forms import CreateCorpusForm
|
from .forms import CreateCorpusForm
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_corpus(app: Flask, corpus_id: int):
|
||||||
|
with app.app_context():
|
||||||
|
corpus: Corpus = Corpus.query.get(corpus_id)
|
||||||
|
corpus.delete()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def _build_corpus(app: Flask, corpus_id: int):
|
||||||
|
with app.app_context():
|
||||||
|
corpus = Corpus.query.get(corpus_id)
|
||||||
|
corpus.build()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
@bp.route('')
|
@bp.route('')
|
||||||
def corpora():
|
def corpora():
|
||||||
return redirect(url_for('main.dashboard', _anchor='corpora'))
|
return redirect(url_for('main.dashboard', _anchor='corpora'))
|
||||||
@ -20,6 +49,7 @@ def corpora():
|
|||||||
@bp.route('/create', methods=['GET', 'POST'])
|
@bp.route('/create', methods=['GET', 'POST'])
|
||||||
def create_corpus():
|
def create_corpus():
|
||||||
form = CreateCorpusForm()
|
form = CreateCorpusForm()
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
try:
|
try:
|
||||||
corpus = Corpus.create(
|
corpus = Corpus.create(
|
||||||
@ -30,8 +60,10 @@ def create_corpus():
|
|||||||
except OSError:
|
except OSError:
|
||||||
abort(500)
|
abort(500)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
flash(f'Corpus "{corpus.title}" created', 'corpus')
|
flash(f'Corpus "{corpus.title}" created', 'corpus')
|
||||||
return redirect(corpus.url)
|
return redirect(corpus.url)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'corpora/create.html.j2',
|
'corpora/create.html.j2',
|
||||||
title='Create corpus',
|
title='Create corpus',
|
||||||
@ -40,12 +72,14 @@ def create_corpus():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:corpus_id>')
|
@bp.route('/<hashid:corpus_id>')
|
||||||
def corpus(corpus_id):
|
def corpus(corpus_id: int):
|
||||||
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
|
cfa = CorpusFollowerAssociation.query.filter_by(
|
||||||
users = User.query.filter(User.is_public == True, User.id != current_user.id, User.id != corpus.user.id, User.role_id < 4).all()
|
corpus_id=corpus_id,
|
||||||
cfa = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=current_user.id).first()
|
follower_id=current_user.id
|
||||||
|
).first()
|
||||||
|
|
||||||
if cfa is None:
|
if cfa is None:
|
||||||
if corpus.user == current_user or current_user.is_administrator:
|
if corpus.user == current_user or current_user.is_administrator:
|
||||||
cfr = CorpusFollowerRole.query.filter_by(name='Administrator').first()
|
cfr = CorpusFollowerRole.query.filter_by(name='Administrator').first()
|
||||||
@ -53,7 +87,21 @@ def corpus(corpus_id):
|
|||||||
cfr = CorpusFollowerRole.query.filter_by(name='Anonymous').first()
|
cfr = CorpusFollowerRole.query.filter_by(name='Anonymous').first()
|
||||||
else:
|
else:
|
||||||
cfr = cfa.role
|
cfr = cfa.role
|
||||||
if corpus.user == current_user or current_user.is_administrator:
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
if (
|
||||||
|
corpus.user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
return render_template(
|
return render_template(
|
||||||
'corpora/corpus.html.j2',
|
'corpora/corpus.html.j2',
|
||||||
title=corpus.title,
|
title=corpus.title,
|
||||||
@ -62,8 +110,15 @@ def corpus(corpus_id):
|
|||||||
cfrs=cfrs,
|
cfrs=cfrs,
|
||||||
users=users
|
users=users
|
||||||
)
|
)
|
||||||
if (current_user.is_following_corpus(corpus) or corpus.is_public):
|
|
||||||
cfas = CorpusFollowerAssociation.query.filter(Corpus.id == corpus_id, CorpusFollowerAssociation.follower_id != corpus.user.id).all()
|
if (
|
||||||
|
current_user.is_following_corpus(corpus)
|
||||||
|
or corpus.is_public
|
||||||
|
):
|
||||||
|
cfas = CorpusFollowerAssociation.query.filter(
|
||||||
|
Corpus.id == corpus_id,
|
||||||
|
CorpusFollowerAssociation.follower_id != corpus.user.id
|
||||||
|
).all()
|
||||||
return render_template(
|
return render_template(
|
||||||
'corpora/public_corpus.html.j2',
|
'corpora/public_corpus.html.j2',
|
||||||
title=corpus.title,
|
title=corpus.title,
|
||||||
@ -73,14 +128,110 @@ def corpus(corpus_id):
|
|||||||
cfas=cfas,
|
cfas=cfas,
|
||||||
users=users
|
users=users
|
||||||
)
|
)
|
||||||
|
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<hashid:corpus_id>', methods=['DELETE'])
|
||||||
|
def delete_corpus(corpus_id: int):
|
||||||
|
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 jsonify(f'Corpus "{corpus.title}" marked for deletion.'), 202
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<hashid:corpus_id>/build', methods=['POST'])
|
||||||
|
def build_corpus(corpus_id: int):
|
||||||
|
corpus = Corpus.query.get_or_404(corpus_id)
|
||||||
|
|
||||||
|
cfa = CorpusFollowerAssociation.query.filter_by(
|
||||||
|
corpus_id=corpus_id,
|
||||||
|
follower_id=current_user.id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not (
|
||||||
|
cfa is not None and cfa.role.has_permission('MANAGE_FILES')
|
||||||
|
or corpus.user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
if len(corpus.files.all()) == 0:
|
||||||
|
abort(409)
|
||||||
|
|
||||||
|
thread = Thread(
|
||||||
|
target=_build_corpus,
|
||||||
|
args=(current_app._get_current_object(), corpus.id)
|
||||||
|
)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
return jsonify(f'Corpus "{corpus.title}" marked for building.'), 202
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<hashid:corpus_id>/create-share-link', methods=['POST'])
|
||||||
|
def create_share_link(corpus_id: int):
|
||||||
|
data = request.json
|
||||||
|
|
||||||
|
expiration_date = data['expiration_date']
|
||||||
|
if not isinstance(expiration_date, str):
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
role_name = data['role_name']
|
||||||
|
if not isinstance(role_name, str):
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
corpus = Corpus.query.get_or_404(corpus_id)
|
||||||
|
|
||||||
|
cfa = CorpusFollowerAssociation.query.filter_by(
|
||||||
|
corpus_id=corpus_id,
|
||||||
|
follower_id=current_user.id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not (
|
||||||
|
cfa is not None and cfa.role.has_permission('MANAGE_FOLLOWERS')
|
||||||
|
or corpus.user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
_expiration_date = datetime.strptime(expiration_date, '%b %d, %Y')
|
||||||
|
|
||||||
|
cfr = CorpusFollowerRole.query.filter_by(name=role_name).first()
|
||||||
|
if cfr is None:
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
token = current_user.generate_follow_corpus_token(
|
||||||
|
corpus.hashid,
|
||||||
|
role_name,
|
||||||
|
_expiration_date
|
||||||
|
)
|
||||||
|
|
||||||
|
corpus_share_link = url_for(
|
||||||
|
'corpora.follow_corpus',
|
||||||
|
corpus_id=corpus_id,
|
||||||
|
token=token,
|
||||||
|
_external=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return jsonify(corpus_share_link)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:corpus_id>/analysis')
|
@bp.route('/<hashid:corpus_id>/analysis')
|
||||||
@corpus_follower_permission_required('VIEW')
|
@corpus_follower_permission_required('VIEW')
|
||||||
def analysis(corpus_id):
|
def analysis(corpus_id: int):
|
||||||
corpus = Corpus.query.get_or_404(corpus_id)
|
corpus = Corpus.query.get_or_404(corpus_id)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'corpora/analysis.html.j2',
|
'corpora/analysis.html.j2',
|
||||||
corpus=corpus,
|
corpus=corpus,
|
||||||
@ -88,22 +239,61 @@ def analysis(corpus_id):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<hashid:corpus_id>/analysis/stopwords')
|
||||||
|
def get_stopwords(corpus_id: int):
|
||||||
|
languages = [
|
||||||
|
'german',
|
||||||
|
'english',
|
||||||
|
'catalan',
|
||||||
|
'greek',
|
||||||
|
'spanish',
|
||||||
|
'french',
|
||||||
|
'italian',
|
||||||
|
'russian',
|
||||||
|
'chinese'
|
||||||
|
]
|
||||||
|
|
||||||
|
nltk.download('stopwords', quiet=True)
|
||||||
|
stopwords = {
|
||||||
|
language: nltk.corpus.stopwords.words(language)
|
||||||
|
for language in languages
|
||||||
|
}
|
||||||
|
stopwords['punctuation'] = list(punctuation)
|
||||||
|
stopwords['punctuation'] += ['—', '|', '–', '“', '„', '--']
|
||||||
|
stopwords['user_stopwords'] = []
|
||||||
|
|
||||||
|
return jsonify(stopwords)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:corpus_id>/follow/<token>')
|
@bp.route('/<hashid:corpus_id>/follow/<token>')
|
||||||
def follow_corpus(corpus_id, token):
|
def follow_corpus(corpus_id: int, token: str):
|
||||||
corpus = Corpus.query.get_or_404(corpus_id)
|
corpus = Corpus.query.get_or_404(corpus_id)
|
||||||
if current_user.follow_corpus_by_token(token):
|
|
||||||
db.session.commit()
|
if not current_user.follow_corpus_by_token(token):
|
||||||
flash(f'You are following "{corpus.title}" now', category='corpus')
|
abort(403)
|
||||||
return redirect(url_for('corpora.corpus', corpus_id=corpus_id))
|
|
||||||
abort(403)
|
db.session.commit()
|
||||||
|
|
||||||
|
flash(f'You are following "{corpus.title}" now', category='corpus')
|
||||||
|
return redirect(corpus.url)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/import', methods=['GET', 'POST'])
|
@bp.route('/<hashid:corpus_id>/is-public', methods=['PUT'])
|
||||||
def import_corpus():
|
def update_is_public(corpus_id):
|
||||||
abort(503)
|
new_value = request.json
|
||||||
|
if not isinstance(new_value, bool):
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
corpus = Corpus.query.get_or_404(corpus_id)
|
||||||
|
|
||||||
|
if not (
|
||||||
|
corpus.user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:corpus_id>/export')
|
corpus.is_public = new_value
|
||||||
@corpus_follower_permission_required('VIEW')
|
db.session.commit()
|
||||||
def export_corpus(corpus_id):
|
|
||||||
abort(503)
|
return jsonify(f'Corpus "{corpus.title}" is now {"public" if new_value else "private"}'), 200
|
||||||
|
@ -4,11 +4,17 @@ from . import bp
|
|||||||
|
|
||||||
|
|
||||||
@bp.app_errorhandler(HTTPException)
|
@bp.app_errorhandler(HTTPException)
|
||||||
def handle_http_exception(error):
|
def handle_http_exception(e: HTTPException):
|
||||||
''' Generic HTTP exception handler '''
|
''' Generic HTTP exception handler '''
|
||||||
accept_json = request.accept_mimetypes.accept_json
|
accept_json = request.accept_mimetypes.accept_json
|
||||||
accept_html = request.accept_mimetypes.accept_html
|
accept_html = request.accept_mimetypes.accept_html
|
||||||
|
|
||||||
if accept_json and not accept_html:
|
if accept_json and not accept_html:
|
||||||
response = jsonify(str(error))
|
error = {
|
||||||
return response, error.code
|
'code': e.code,
|
||||||
return render_template('errors/error.html.j2', error=error), error.code
|
'name': e.name,
|
||||||
|
'description': e.description
|
||||||
|
}
|
||||||
|
return jsonify(error), e.code
|
||||||
|
|
||||||
|
return render_template('errors/error.html.j2', error=e), e.code
|
||||||
|
@ -1,18 +1,13 @@
|
|||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
from flask_login import login_required
|
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint('jobs', __name__)
|
bp = Blueprint('jobs', __name__)
|
||||||
|
|
||||||
|
|
||||||
@bp.before_request
|
from . import routes
|
||||||
@login_required
|
|
||||||
def before_request():
|
|
||||||
'''
|
|
||||||
Ensures that the routes in this package can only be visited by users that
|
|
||||||
are logged in.
|
|
||||||
'''
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
from .inputs import bp as inputs_bp
|
||||||
|
bp.register_blueprint(inputs_bp, url_prefix='/<hashid:job_id>/inputs')
|
||||||
|
|
||||||
from . import routes, json_routes
|
from .results import bp as results_bp
|
||||||
|
bp.register_blueprint(results_bp, url_prefix='/<hashid:job_id>/results')
|
||||||
|
7
app/blueprints/jobs/inputs/__init__.py
Normal file
7
app/blueprints/jobs/inputs/__init__.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
|
||||||
|
bp = Blueprint('inputs', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
from . import routes
|
27
app/blueprints/jobs/inputs/routes.py
Normal file
27
app/blueprints/jobs/inputs/routes.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from flask import abort, send_from_directory
|
||||||
|
from flask_login import current_user, login_required
|
||||||
|
from app.models import JobInput
|
||||||
|
from . import bp
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<hashid:job_input_id>/download')
|
||||||
|
@login_required
|
||||||
|
def download_job_input(job_id: int, job_input_id: int):
|
||||||
|
job_input = JobInput.query.filter_by(
|
||||||
|
job_id=job_id,
|
||||||
|
id=job_input_id
|
||||||
|
).first_or_404()
|
||||||
|
|
||||||
|
if not (
|
||||||
|
job_input.job.user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
return send_from_directory(
|
||||||
|
job_input.path.parent,
|
||||||
|
job_input.path.name,
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=job_input.filename,
|
||||||
|
mimetype=job_input.mimetype
|
||||||
|
)
|
@ -1,72 +0,0 @@
|
|||||||
from flask import abort, current_app
|
|
||||||
from flask_login import current_user
|
|
||||||
from threading import Thread
|
|
||||||
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(job.path / 'pipeline_data' / 'logs' / 'pyflow_log.txt') as log_file:
|
|
||||||
log = log_file.read()
|
|
||||||
response_data = {
|
|
||||||
'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
|
|
7
app/blueprints/jobs/results/__init__.py
Normal file
7
app/blueprints/jobs/results/__init__.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
|
||||||
|
bp = Blueprint('results', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
from . import routes
|
27
app/blueprints/jobs/results/routes.py
Normal file
27
app/blueprints/jobs/results/routes.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from flask import abort, send_from_directory
|
||||||
|
from flask_login import current_user, login_required
|
||||||
|
from app.models import JobResult
|
||||||
|
from . import bp
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<hashid:job_result_id>/download')
|
||||||
|
@login_required
|
||||||
|
def download_job_result(job_id: int, job_result_id: int):
|
||||||
|
job_result = JobResult.query.filter_by(
|
||||||
|
job_id=job_id,
|
||||||
|
id=job_result_id
|
||||||
|
).first_or_404()
|
||||||
|
|
||||||
|
if not (
|
||||||
|
job_result.job.user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
return send_from_directory(
|
||||||
|
job_result.path.parent,
|
||||||
|
job_result.path.name,
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=job_result.filename,
|
||||||
|
mimetype=job_result.mimetype
|
||||||
|
)
|
@ -1,25 +1,37 @@
|
|||||||
from flask import (
|
from flask import (
|
||||||
abort,
|
abort,
|
||||||
|
current_app,
|
||||||
|
Flask,
|
||||||
|
jsonify,
|
||||||
redirect,
|
redirect,
|
||||||
render_template,
|
render_template,
|
||||||
send_from_directory,
|
|
||||||
url_for
|
url_for
|
||||||
)
|
)
|
||||||
from flask_login import current_user
|
from flask_login import current_user, login_required
|
||||||
from app.models import Job, JobInput, JobResult
|
from threading import Thread
|
||||||
|
from app import db
|
||||||
|
from app.decorators import admin_required
|
||||||
|
from app.models import Job, JobStatus
|
||||||
from . import bp
|
from . import bp
|
||||||
|
|
||||||
|
|
||||||
@bp.route('')
|
@bp.route('')
|
||||||
def jobs():
|
@login_required
|
||||||
|
def index():
|
||||||
return redirect(url_for('main.dashboard', _anchor='jobs'))
|
return redirect(url_for('main.dashboard', _anchor='jobs'))
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:job_id>')
|
@bp.route('/<hashid:job_id>')
|
||||||
def job(job_id):
|
@login_required
|
||||||
|
def job(job_id: int):
|
||||||
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',
|
||||||
title='Job',
|
title='Job',
|
||||||
@ -27,29 +39,73 @@ def job(job_id):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:job_id>/inputs/<hashid:job_input_id>/download')
|
def _delete_job(app: Flask, job_id: int):
|
||||||
def download_job_input(job_id, job_input_id):
|
with app.app_context():
|
||||||
job_input = JobInput.query.filter_by(job_id=job_id, id=job_input_id).first_or_404()
|
job = Job.query.get(job_id)
|
||||||
if not (job_input.job.user == current_user or current_user.is_administrator):
|
job.delete()
|
||||||
abort(403)
|
db.session.commit()
|
||||||
return send_from_directory(
|
|
||||||
job_input.path.parent,
|
|
||||||
job_input.path.name,
|
|
||||||
as_attachment=True,
|
|
||||||
download_name=job_input.filename,
|
|
||||||
mimetype=job_input.mimetype
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:job_id>/results/<hashid:job_result_id>/download')
|
@bp.route('/<hashid:job_id>', methods=['DELETE'])
|
||||||
def download_job_result(job_id, job_result_id):
|
@login_required
|
||||||
job_result = JobResult.query.filter_by(job_id=job_id, id=job_result_id).first_or_404()
|
def delete_job(job_id: int):
|
||||||
if not (job_result.job.user == current_user or current_user.is_administrator):
|
job = Job.query.get_or_404(job_id)
|
||||||
|
|
||||||
|
if not (
|
||||||
|
job.user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
abort(403)
|
abort(403)
|
||||||
return send_from_directory(
|
|
||||||
job_result.path.parent,
|
thread = Thread(
|
||||||
job_result.path.name,
|
target=_delete_job,
|
||||||
as_attachment=True,
|
args=(current_app._get_current_object(), job.id)
|
||||||
download_name=job_result.filename,
|
|
||||||
mimetype=job_result.mimetype
|
|
||||||
)
|
)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
return jsonify(f'Job "{job.title}" marked for deletion.'), 202
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<hashid:job_id>/log')
|
||||||
|
@admin_required
|
||||||
|
def job_log(job_id: int):
|
||||||
|
job = Job.query.get_or_404(job_id)
|
||||||
|
|
||||||
|
if job.status not in [JobStatus.COMPLETED, JobStatus.FAILED]:
|
||||||
|
abort(409)
|
||||||
|
|
||||||
|
log_file_path = job.path / 'pipeline_data' / 'logs' / 'pyflow_log.txt'
|
||||||
|
with log_file_path.open() as log_file:
|
||||||
|
log = log_file.read()
|
||||||
|
|
||||||
|
return jsonify(log)
|
||||||
|
|
||||||
|
|
||||||
|
def _restart_job(app: Flask, job_id: int):
|
||||||
|
with app.app_context():
|
||||||
|
job = Job.query.get(job_id)
|
||||||
|
job.restart()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<hashid:job_id>/restart', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def restart_job(job_id: int):
|
||||||
|
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:
|
||||||
|
abort(409)
|
||||||
|
|
||||||
|
thread = Thread(
|
||||||
|
target=_restart_job,
|
||||||
|
args=(current_app._get_current_object(), job.id)
|
||||||
|
)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
return jsonify(f'Job "{job.title}" marked for restarting.'), 202
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
from flask import flash, redirect, render_template, url_for
|
from flask import abort, flash, jsonify, redirect, render_template, url_for
|
||||||
from flask_login import current_user, login_required, login_user
|
from flask_login import current_user, login_required, login_user
|
||||||
from app.blueprints.auth.forms import LoginForm
|
from app.blueprints.auth.forms import LoginForm
|
||||||
from app.models import Corpus, User
|
from app.models import Corpus, User
|
||||||
from . import bp
|
from . import bp
|
||||||
|
from app import db
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/', methods=['GET', 'POST'])
|
@bp.route('/', methods=['GET', 'POST'])
|
||||||
@ -56,7 +57,7 @@ def news():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/privacy_policy')
|
@bp.route('/privacy-policy')
|
||||||
def privacy_policy():
|
def privacy_policy():
|
||||||
return render_template(
|
return render_template(
|
||||||
'main/privacy_policy.html.j2',
|
'main/privacy_policy.html.j2',
|
||||||
@ -64,22 +65,32 @@ def privacy_policy():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/terms_of_use')
|
@bp.route('/terms-of-use')
|
||||||
def terms_of_use():
|
def terms_of_use():
|
||||||
return render_template(
|
return render_template(
|
||||||
'main/terms_of_use.html.j2',
|
'main/terms_of_use.html.j2',
|
||||||
title='Terms of Use'
|
title='Terms of use'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/social-area')
|
@bp.route('/accept-terms-of-use', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def social_area():
|
def accept_terms_of_use():
|
||||||
|
current_user.terms_of_use_accepted = True
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify('You accepted the terms of use'), 202
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/social')
|
||||||
|
@login_required
|
||||||
|
def social():
|
||||||
corpora = Corpus.query.filter(Corpus.is_public == True, Corpus.user != current_user).all()
|
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()
|
users = User.query.filter(User.is_public == True, User.id != current_user.id).all()
|
||||||
return render_template(
|
return render_template(
|
||||||
'main/social_area.html.j2',
|
'main/social.html.j2',
|
||||||
title='Social Area',
|
title='Social',
|
||||||
corpora=corpora,
|
corpora=corpora,
|
||||||
users=users
|
users=users
|
||||||
)
|
)
|
||||||
|
@ -1,18 +1,7 @@
|
|||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
from flask_login import login_required
|
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint('settings', __name__)
|
bp = Blueprint('settings', __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
|
from . import routes
|
||||||
|
@ -38,8 +38,8 @@ class UpdateAccountInformationForm(FlaskForm):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
submit = SubmitField()
|
submit = SubmitField()
|
||||||
|
|
||||||
def __init__(self, user, *args, **kwargs):
|
def __init__(self, user: User, *args, **kwargs):
|
||||||
if 'data' not in kwargs:
|
if 'data' not in kwargs:
|
||||||
kwargs['data'] = user.to_json_serializeable()
|
kwargs['data'] = user.to_json_serializeable()
|
||||||
if 'prefix' not in kwargs:
|
if 'prefix' not in kwargs:
|
||||||
@ -64,7 +64,7 @@ class UpdateProfileInformationForm(FlaskForm):
|
|||||||
validators=[Length(max=128)]
|
validators=[Length(max=128)]
|
||||||
)
|
)
|
||||||
about_me = TextAreaField(
|
about_me = TextAreaField(
|
||||||
'About me',
|
'About me',
|
||||||
validators=[
|
validators=[
|
||||||
Length(max=254)
|
Length(max=254)
|
||||||
]
|
]
|
||||||
@ -89,7 +89,7 @@ class UpdateProfileInformationForm(FlaskForm):
|
|||||||
)
|
)
|
||||||
submit = SubmitField()
|
submit = SubmitField()
|
||||||
|
|
||||||
def __init__(self, user, *args, **kwargs):
|
def __init__(self, user: User, *args, **kwargs):
|
||||||
if 'data' not in kwargs:
|
if 'data' not in kwargs:
|
||||||
kwargs['data'] = user.to_json_serializeable()
|
kwargs['data'] = user.to_json_serializeable()
|
||||||
if 'prefix' not in kwargs:
|
if 'prefix' not in kwargs:
|
||||||
@ -130,7 +130,7 @@ class UpdatePasswordForm(FlaskForm):
|
|||||||
)
|
)
|
||||||
submit = SubmitField()
|
submit = SubmitField()
|
||||||
|
|
||||||
def __init__(self, user, *args, **kwargs):
|
def __init__(self, user: User, *args, **kwargs):
|
||||||
if 'prefix' not in kwargs:
|
if 'prefix' not in kwargs:
|
||||||
kwargs['prefix'] = 'update-password-form'
|
kwargs['prefix'] = 'update-password-form'
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@ -152,7 +152,7 @@ class UpdateNotificationsForm(FlaskForm):
|
|||||||
)
|
)
|
||||||
submit = SubmitField()
|
submit = SubmitField()
|
||||||
|
|
||||||
def __init__(self, user, *args, **kwargs):
|
def __init__(self, user: User, *args, **kwargs):
|
||||||
if 'data' not in kwargs:
|
if 'data' not in kwargs:
|
||||||
kwargs['data'] = user.to_json_serializeable()
|
kwargs['data'] = user.to_json_serializeable()
|
||||||
if 'prefix' not in kwargs:
|
if 'prefix' not in kwargs:
|
@ -1,10 +1,158 @@
|
|||||||
from flask import g, url_for
|
from flask import (
|
||||||
from flask_login import current_user
|
abort,
|
||||||
from app.blueprints.users.settings.routes import settings as settings_route
|
flash,
|
||||||
|
jsonify,
|
||||||
|
redirect,
|
||||||
|
render_template,
|
||||||
|
request,
|
||||||
|
url_for
|
||||||
|
)
|
||||||
|
from flask_login import current_user, login_required
|
||||||
|
from app import db
|
||||||
|
from app.models import Avatar
|
||||||
from . import bp
|
from . import bp
|
||||||
|
from .forms import (
|
||||||
|
UpdateAvatarForm,
|
||||||
|
UpdatePasswordForm,
|
||||||
|
UpdateNotificationsForm,
|
||||||
|
UpdateAccountInformationForm,
|
||||||
|
UpdateProfileInformationForm
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/settings', methods=['GET', 'POST'])
|
@bp.route('', methods=['GET', 'POST'])
|
||||||
def settings():
|
@login_required
|
||||||
g._nopaque_redirect_location_on_post = url_for('.settings')
|
def index():
|
||||||
return settings_route(current_user.id)
|
update_account_information_form = UpdateAccountInformationForm(current_user)
|
||||||
|
update_profile_information_form = UpdateProfileInformationForm(current_user)
|
||||||
|
update_avatar_form = UpdateAvatarForm()
|
||||||
|
update_password_form = UpdatePasswordForm(current_user)
|
||||||
|
update_notifications_form = UpdateNotificationsForm(current_user)
|
||||||
|
|
||||||
|
# region handle update profile information form
|
||||||
|
if update_profile_information_form.submit.data and update_profile_information_form.validate():
|
||||||
|
current_user.about_me = update_profile_information_form.about_me.data
|
||||||
|
current_user.location = update_profile_information_form.location.data
|
||||||
|
current_user.organization = update_profile_information_form.organization.data
|
||||||
|
current_user.website = update_profile_information_form.website.data
|
||||||
|
current_user.full_name = update_profile_information_form.full_name.data
|
||||||
|
db.session.commit()
|
||||||
|
flash('Your changes have been saved')
|
||||||
|
return redirect(url_for('.index'))
|
||||||
|
# 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=current_user
|
||||||
|
)
|
||||||
|
except (AttributeError, OSError):
|
||||||
|
abort(500)
|
||||||
|
db.session.commit()
|
||||||
|
flash('Your changes have been saved')
|
||||||
|
return redirect(url_for('.index'))
|
||||||
|
# endregion handle update avatar form
|
||||||
|
|
||||||
|
# region handle update account information form
|
||||||
|
if update_account_information_form.submit.data and update_account_information_form.validate():
|
||||||
|
current_user.email = update_account_information_form.email.data
|
||||||
|
current_user.username = update_account_information_form.username.data
|
||||||
|
db.session.commit()
|
||||||
|
flash('Profile settings updated')
|
||||||
|
return redirect(url_for('.index'))
|
||||||
|
# endregion handle update account information form
|
||||||
|
|
||||||
|
# region handle update password form
|
||||||
|
if update_password_form.submit.data and update_password_form.validate():
|
||||||
|
current_user.password = update_password_form.new_password.data
|
||||||
|
db.session.commit()
|
||||||
|
flash('Your changes have been saved')
|
||||||
|
return redirect(url_for('.index'))
|
||||||
|
# endregion handle update password form
|
||||||
|
|
||||||
|
# region handle update notifications form
|
||||||
|
if update_notifications_form.submit.data and update_notifications_form.validate():
|
||||||
|
current_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('.index'))
|
||||||
|
# endregion handle update notifications form
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'settings/index.html.j2',
|
||||||
|
title='Settings',
|
||||||
|
update_account_information_form=update_account_information_form,
|
||||||
|
update_avatar_form=update_avatar_form,
|
||||||
|
update_notifications_form=update_notifications_form,
|
||||||
|
update_password_form=update_password_form,
|
||||||
|
update_profile_information_form=update_profile_information_form,
|
||||||
|
user=current_user
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/profile-is-public', methods=['PUT'])
|
||||||
|
@login_required
|
||||||
|
def update_profile_is_public():
|
||||||
|
new_value = request.json
|
||||||
|
|
||||||
|
if not isinstance(new_value, bool):
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
current_user.is_public = new_value
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify('Your changes have been saved'), 200
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/profile-show-email', methods=['PUT'])
|
||||||
|
@login_required
|
||||||
|
def update_profile_show_email():
|
||||||
|
new_value = request.json
|
||||||
|
|
||||||
|
if not isinstance(new_value, bool):
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
if new_value:
|
||||||
|
current_user.add_profile_privacy_setting('SHOW_EMAIL')
|
||||||
|
else:
|
||||||
|
current_user.remove_profile_privacy_setting('SHOW_EMAIL')
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify('Your changes have been saved'), 200
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/profile-show-last-seen', methods=['PUT'])
|
||||||
|
@login_required
|
||||||
|
def update_profile_show_last_seen():
|
||||||
|
new_value = request.json
|
||||||
|
|
||||||
|
if not isinstance(new_value, bool):
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
if new_value:
|
||||||
|
current_user.add_profile_privacy_setting('SHOW_LAST_SEEN')
|
||||||
|
else:
|
||||||
|
current_user.remove_profile_privacy_setting('SHOW_LAST_SEEN')
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify('Your changes have been saved'), 200
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/profile-show-member-since', methods=['PUT'])
|
||||||
|
@login_required
|
||||||
|
def update_profile_show_member_since():
|
||||||
|
new_value = request.json
|
||||||
|
|
||||||
|
if not isinstance(new_value, bool):
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
if new_value:
|
||||||
|
current_user.add_profile_privacy_setting('SHOW_MEMBER_SINCE')
|
||||||
|
else:
|
||||||
|
current_user.remove_profile_privacy_setting('SHOW_MEMBER_SINCE')
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify('Your changes have been saved'), 200
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
from flask import Blueprint
|
|
||||||
from flask_login import login_required
|
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint('transkribus_htr_pipeline_models', __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
|
|
@ -1,7 +0,0 @@
|
|||||||
from flask import abort
|
|
||||||
from . import bp
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/transkribus_htr_pipeline_models')
|
|
||||||
def index():
|
|
||||||
return abort(503)
|
|
@ -1,18 +1,7 @@
|
|||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
from flask_login import login_required
|
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint('users', __name__)
|
bp = Blueprint('users', __name__)
|
||||||
|
|
||||||
|
|
||||||
@bp.before_request
|
from . import cli, events, routes
|
||||||
@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, json_routes, routes, settings
|
|
||||||
|
91
app/blueprints/users/events.py
Normal file
91
app/blueprints/users/events.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
from flask_login import current_user
|
||||||
|
from flask_socketio import join_room, leave_room
|
||||||
|
from app import hashids, socketio
|
||||||
|
from app.decorators import socketio_login_required
|
||||||
|
from app.models import User
|
||||||
|
|
||||||
|
|
||||||
|
@socketio.on('SUBSCRIBE User')
|
||||||
|
@socketio_login_required
|
||||||
|
def subscribe(user_hashid: str) -> dict:
|
||||||
|
if not isinstance(user_hashid, str):
|
||||||
|
return {
|
||||||
|
'code': 400,
|
||||||
|
'name': 'Bad Request',
|
||||||
|
'description': 'Invalid User ID.'
|
||||||
|
}
|
||||||
|
|
||||||
|
user_id = hashids.decode(user_hashid)
|
||||||
|
|
||||||
|
if not isinstance(user_id, int):
|
||||||
|
return {
|
||||||
|
'code': 400,
|
||||||
|
'name': 'Bad Request',
|
||||||
|
'description': 'Invalid User ID.'
|
||||||
|
}
|
||||||
|
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
return {
|
||||||
|
'code': 404,
|
||||||
|
'name': 'Not Found',
|
||||||
|
'description': 'User not found.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if not (
|
||||||
|
user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
'code': 403,
|
||||||
|
'name': 'Forbidden',
|
||||||
|
'description': 'Not allowed to subscribe to this user.'
|
||||||
|
}
|
||||||
|
|
||||||
|
join_room(f'/users/{user.hashid}')
|
||||||
|
|
||||||
|
return {'code': 204, 'name': 'No Content'}
|
||||||
|
|
||||||
|
|
||||||
|
@socketio.on('UNSUBSCRIBE User')
|
||||||
|
@socketio_login_required
|
||||||
|
def unsubscribe(user_hashid: str) -> dict:
|
||||||
|
if not isinstance(user_hashid, str):
|
||||||
|
return {
|
||||||
|
'code': 400,
|
||||||
|
'name': 'Bad Request',
|
||||||
|
'description': 'Invalid User ID.'
|
||||||
|
}
|
||||||
|
|
||||||
|
user_id = hashids.decode(user_hashid)
|
||||||
|
|
||||||
|
if not isinstance(user_id, int):
|
||||||
|
return {
|
||||||
|
'code': 400,
|
||||||
|
'name': 'Bad Request',
|
||||||
|
'description': 'Invalid User ID.'
|
||||||
|
}
|
||||||
|
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
return {
|
||||||
|
'code': 404,
|
||||||
|
'name': 'Not Found',
|
||||||
|
'description': 'User not found.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if not (
|
||||||
|
user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
'code': 403,
|
||||||
|
'name': 'Forbidden',
|
||||||
|
'description': 'Not allowed to unsubscribe from this user.'
|
||||||
|
}
|
||||||
|
|
||||||
|
leave_room(f'/users/{user.hashid}')
|
||||||
|
|
||||||
|
return {'code': 204, 'name': 'No Content'}
|
@ -1,69 +0,0 @@
|
|||||||
from flask import abort, current_app
|
|
||||||
from flask_login import current_user, logout_user
|
|
||||||
from threading import Thread
|
|
||||||
from app import db
|
|
||||||
from app.decorators import content_negotiation
|
|
||||||
from app.models import Avatar, User
|
|
||||||
from . import bp
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:user_id>', methods=['DELETE'])
|
|
||||||
@content_negotiation(produces='application/json')
|
|
||||||
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 = User.query.get_or_404(user_id)
|
|
||||||
if not (user == current_user or current_user.is_administrator):
|
|
||||||
abort(403)
|
|
||||||
thread = Thread(
|
|
||||||
target=_delete_user,
|
|
||||||
args=(current_app._get_current_object(), user.id)
|
|
||||||
)
|
|
||||||
if user == current_user:
|
|
||||||
logout_user()
|
|
||||||
thread.start()
|
|
||||||
response_data = {
|
|
||||||
'message': f'User "{user.username}" marked for deletion'
|
|
||||||
}
|
|
||||||
return response_data, 202
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:user_id>/avatar', methods=['DELETE'])
|
|
||||||
@content_negotiation(produces='application/json')
|
|
||||||
def delete_user_avatar(user_id):
|
|
||||||
def _delete_avatar(app, avatar_id):
|
|
||||||
with app.app_context():
|
|
||||||
avatar = Avatar.query.get(avatar_id)
|
|
||||||
avatar.delete()
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
user = User.query.get_or_404(user_id)
|
|
||||||
if user.avatar is None:
|
|
||||||
abort(404)
|
|
||||||
if not (user == current_user or current_user.is_administrator):
|
|
||||||
abort(403)
|
|
||||||
thread = Thread(
|
|
||||||
target=_delete_avatar,
|
|
||||||
args=(current_app._get_current_object(), user.avatar.id)
|
|
||||||
)
|
|
||||||
thread.start()
|
|
||||||
response_data = {
|
|
||||||
'message': f'Avatar marked for deletion'
|
|
||||||
}
|
|
||||||
return response_data, 202
|
|
||||||
|
|
||||||
@bp.route('/accept-terms-of-use', methods=['POST'])
|
|
||||||
@content_negotiation(produces='application/json')
|
|
||||||
def accept_terms_of_use():
|
|
||||||
if not (current_user.is_authenticated or current_user.confirmed):
|
|
||||||
abort(403)
|
|
||||||
current_user.terms_of_use_accepted = True
|
|
||||||
db.session.commit()
|
|
||||||
response_data = {
|
|
||||||
'message': 'You accepted the terms of use',
|
|
||||||
}
|
|
||||||
return response_data, 202
|
|
@ -1,25 +1,48 @@
|
|||||||
from flask import (
|
from flask import (
|
||||||
abort,
|
abort,
|
||||||
redirect,
|
current_app,
|
||||||
render_template,
|
Flask,
|
||||||
|
jsonify,
|
||||||
|
redirect,
|
||||||
|
render_template,
|
||||||
|
request,
|
||||||
send_from_directory,
|
send_from_directory,
|
||||||
url_for
|
url_for
|
||||||
)
|
)
|
||||||
from flask_login import current_user
|
from flask_login import current_user, login_required, logout_user
|
||||||
from app.models import User
|
from threading import Thread
|
||||||
|
from app import db
|
||||||
|
from app.models import Avatar, User
|
||||||
from . import bp
|
from . import bp
|
||||||
|
|
||||||
|
|
||||||
@bp.route('')
|
@bp.route('')
|
||||||
def users():
|
@login_required
|
||||||
|
def index():
|
||||||
return redirect(url_for('main.social_area', _anchor='users'))
|
return redirect(url_for('main.social_area', _anchor='users'))
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:user_id>')
|
@bp.route('/<hashid:user_id>')
|
||||||
def user(user_id):
|
@login_required
|
||||||
|
def user(user_id: int):
|
||||||
user = User.query.get_or_404(user_id)
|
user = User.query.get_or_404(user_id)
|
||||||
if not (user.is_public or user == current_user or current_user.is_administrator):
|
|
||||||
|
if not (
|
||||||
|
user.is_public
|
||||||
|
or user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
accept_json = request.accept_mimetypes.accept_json
|
||||||
|
accept_html = request.accept_mimetypes.accept_html
|
||||||
|
|
||||||
|
if accept_json and not accept_html:
|
||||||
|
return user.to_json_serializeable(
|
||||||
|
backrefs=True,
|
||||||
|
relationships=True
|
||||||
|
)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'users/user.html.j2',
|
'users/user.html.j2',
|
||||||
title=user.username,
|
title=user.username,
|
||||||
@ -27,13 +50,51 @@ def user(user_id):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:user_id>/avatar')
|
def _delete_user(app: Flask, user_id: int):
|
||||||
def user_avatar(user_id):
|
with app.app_context():
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
user.delete()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<hashid:user_id>', methods=['DELETE'])
|
||||||
|
@login_required
|
||||||
|
def delete_user(user_id: int):
|
||||||
user = User.query.get_or_404(user_id)
|
user = User.query.get_or_404(user_id)
|
||||||
if not (user.is_public or user == current_user or current_user.is_administrator):
|
|
||||||
|
if not (
|
||||||
|
user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
if user == current_user:
|
||||||
|
logout_user()
|
||||||
|
|
||||||
|
thread = Thread(
|
||||||
|
target=_delete_user,
|
||||||
|
args=(current_app._get_current_object(), user.id)
|
||||||
|
)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
return jsonify(f'User "{user.username}" marked for deletion'), 202
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<hashid:user_id>/avatar')
|
||||||
|
@login_required
|
||||||
|
def user_avatar(user_id: int):
|
||||||
|
user = User.query.get_or_404(user_id)
|
||||||
|
|
||||||
|
if not (
|
||||||
|
user.is_public
|
||||||
|
or user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
abort(403)
|
||||||
|
|
||||||
if user.avatar is None:
|
if user.avatar is None:
|
||||||
return redirect(url_for('static', filename='images/user_avatar.png'))
|
return redirect(url_for('static', filename='images/user_avatar.png'))
|
||||||
|
|
||||||
return send_from_directory(
|
return send_from_directory(
|
||||||
user.avatar.path.parent,
|
user.avatar.path.parent,
|
||||||
user.avatar.path.name,
|
user.avatar.path.name,
|
||||||
@ -41,3 +102,33 @@ def user_avatar(user_id):
|
|||||||
download_name=user.avatar.filename,
|
download_name=user.avatar.filename,
|
||||||
mimetype=user.avatar.mimetype
|
mimetype=user.avatar.mimetype
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_avatar(app: Flask, avatar_id: int):
|
||||||
|
with app.app_context():
|
||||||
|
avatar = Avatar.query.get(avatar_id)
|
||||||
|
avatar.delete()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<hashid:user_id>/avatar', methods=['DELETE'])
|
||||||
|
@login_required
|
||||||
|
def delete_user_avatar(user_id: int):
|
||||||
|
user = User.query.get_or_404(user_id)
|
||||||
|
|
||||||
|
if user.avatar is None:
|
||||||
|
abort(409)
|
||||||
|
|
||||||
|
if not (
|
||||||
|
user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
thread = Thread(
|
||||||
|
target=_delete_avatar,
|
||||||
|
args=(current_app._get_current_object(), user.avatar.id)
|
||||||
|
)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
return jsonify('Avatar marked for deletion'), 202
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
from .. import bp
|
|
||||||
from . import json_routes, routes
|
|
@ -1,49 +0,0 @@
|
|||||||
from flask import abort, request
|
|
||||||
from flask_login import current_user
|
|
||||||
from app import db
|
|
||||||
from app.decorators import content_negotiation
|
|
||||||
from app.models import User, ProfilePrivacySettings
|
|
||||||
from . import bp
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:user_id>/settings/profile-privacy/is-public', methods=['PUT'])
|
|
||||||
@content_negotiation(consumes='application/json', produces='application/json')
|
|
||||||
def update_user_profile_privacy_setting_is_public(user_id):
|
|
||||||
user = User.query.get_or_404(user_id)
|
|
||||||
if not (user == current_user or current_user.is_administrator):
|
|
||||||
abort(403)
|
|
||||||
enabled = request.json
|
|
||||||
if not isinstance(enabled, bool):
|
|
||||||
abort(400)
|
|
||||||
user.is_public = enabled
|
|
||||||
db.session.commit()
|
|
||||||
response_data = {
|
|
||||||
'message': 'Profile privacy settings updated',
|
|
||||||
'category': 'settings'
|
|
||||||
}
|
|
||||||
return response_data, 200
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:user_id>/settings/profile-privacy/<string:profile_privacy_setting_name>', methods=['PUT'])
|
|
||||||
@content_negotiation(consumes='application/json', produces='application/json')
|
|
||||||
def update_user_profile_privacy_settings(user_id, profile_privacy_setting_name):
|
|
||||||
user = User.query.get_or_404(user_id)
|
|
||||||
try:
|
|
||||||
profile_privacy_setting = ProfilePrivacySettings[profile_privacy_setting_name]
|
|
||||||
except KeyError:
|
|
||||||
abort(404)
|
|
||||||
if not (user == current_user or current_user.is_administrator):
|
|
||||||
abort(403)
|
|
||||||
enabled = request.json
|
|
||||||
if not isinstance(enabled, bool):
|
|
||||||
abort(400)
|
|
||||||
if enabled:
|
|
||||||
user.add_profile_privacy_setting(profile_privacy_setting)
|
|
||||||
else:
|
|
||||||
user.remove_profile_privacy_setting(profile_privacy_setting)
|
|
||||||
db.session.commit()
|
|
||||||
response_data = {
|
|
||||||
'message': 'Profile privacy settings updated',
|
|
||||||
'category': 'settings'
|
|
||||||
}
|
|
||||||
return response_data, 200
|
|
@ -1,93 +0,0 @@
|
|||||||
from flask import abort, flash, g, redirect, render_template, url_for
|
|
||||||
from flask_login import current_user
|
|
||||||
from app import db
|
|
||||||
from app.models import Avatar, User
|
|
||||||
from . import bp
|
|
||||||
from .forms import (
|
|
||||||
UpdateAvatarForm,
|
|
||||||
UpdatePasswordForm,
|
|
||||||
UpdateNotificationsForm,
|
|
||||||
UpdateAccountInformationForm,
|
|
||||||
UpdateProfileInformationForm
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:user_id>/settings', methods=['GET', 'POST'])
|
|
||||||
def settings(user_id):
|
|
||||||
user = User.query.get_or_404(user_id)
|
|
||||||
if not (user == current_user or current_user.is_administrator):
|
|
||||||
abort(403)
|
|
||||||
|
|
||||||
redirect_location_on_post = g.pop(
|
|
||||||
'_nopaque_redirect_location_on_post',
|
|
||||||
url_for('.settings', user_id=user_id)
|
|
||||||
)
|
|
||||||
|
|
||||||
update_account_information_form = UpdateAccountInformationForm(user)
|
|
||||||
update_profile_information_form = UpdateProfileInformationForm(user)
|
|
||||||
update_avatar_form = UpdateAvatarForm()
|
|
||||||
update_password_form = UpdatePasswordForm(user)
|
|
||||||
update_notifications_form = UpdateNotificationsForm(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(redirect_location_on_post)
|
|
||||||
# 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
|
|
||||||
)
|
|
||||||
except (AttributeError, OSError):
|
|
||||||
abort(500)
|
|
||||||
db.session.commit()
|
|
||||||
flash('Your changes have been saved')
|
|
||||||
return redirect(redirect_location_on_post)
|
|
||||||
# endregion handle update avatar form
|
|
||||||
|
|
||||||
# region handle update account information form
|
|
||||||
if update_account_information_form.submit.data and update_account_information_form.validate():
|
|
||||||
user.email = update_account_information_form.email.data
|
|
||||||
user.username = update_account_information_form.username.data
|
|
||||||
db.session.commit()
|
|
||||||
flash('Profile settings updated')
|
|
||||||
return redirect(redirect_location_on_post)
|
|
||||||
# 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(redirect_location_on_post)
|
|
||||||
# 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(redirect_location_on_post)
|
|
||||||
# endregion handle update notifications form
|
|
||||||
|
|
||||||
return render_template(
|
|
||||||
'users/settings/settings.html.j2',
|
|
||||||
title='Settings',
|
|
||||||
update_account_information_form=update_account_information_form,
|
|
||||||
update_avatar_form=update_avatar_form,
|
|
||||||
update_notifications_form=update_notifications_form,
|
|
||||||
update_password_form=update_password_form,
|
|
||||||
update_profile_information_form=update_profile_information_form,
|
|
||||||
user=user
|
|
||||||
)
|
|
@ -26,7 +26,7 @@ def socketio_login_required(f):
|
|||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
return {'code': 401, 'body': 'Unauthorized'}
|
return {'status': 401, 'statusText': 'Unauthorized'}
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ def socketio_permission_required(permission):
|
|||||||
@wraps(f)
|
@wraps(f)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
if not current_user.can(permission):
|
if not current_user.can(permission):
|
||||||
return {'code': 403, 'body': 'Forbidden'}
|
return {'status': 403, 'statusText': 'Forbidden'}
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
return wrapper
|
return wrapper
|
||||||
return decorator
|
return decorator
|
||||||
|
20
app/extensions/nopaque_flask_admin_views.py
Normal file
20
app/extensions/nopaque_flask_admin_views.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from flask import abort
|
||||||
|
from flask_admin import (
|
||||||
|
AdminIndexView as _AdminIndexView,
|
||||||
|
expose
|
||||||
|
)
|
||||||
|
from flask_admin.contrib.sqla import ModelView as _ModelView
|
||||||
|
from flask_login import current_user
|
||||||
|
|
||||||
|
|
||||||
|
class AdminIndexView(_AdminIndexView):
|
||||||
|
@expose('/')
|
||||||
|
def index(self):
|
||||||
|
if not current_user.is_administrator:
|
||||||
|
abort(403)
|
||||||
|
return super().index()
|
||||||
|
|
||||||
|
|
||||||
|
class ModelView(_ModelView):
|
||||||
|
def is_accessible(self):
|
||||||
|
return current_user.is_administrator
|
@ -1,2 +0,0 @@
|
|||||||
from .types import ContainerColumn
|
|
||||||
from .types import IntEnumColumn
|
|
@ -50,7 +50,7 @@ def _create_build_corpus_service(corpus: Corpus):
|
|||||||
''' ## Constraints ## '''
|
''' ## Constraints ## '''
|
||||||
constraints = ['node.role==worker']
|
constraints = ['node.role==worker']
|
||||||
''' ## Image ## '''
|
''' ## Image ## '''
|
||||||
image = f'{current_app.config["NOPAQUE_DOCKER_IMAGE_PREFIX"]}cwb:r1879'
|
image = f'{current_app.config["NOPAQUE_DOCKER_IMAGE_PREFIX"]}cwb:r1887'
|
||||||
''' ## Labels ## '''
|
''' ## Labels ## '''
|
||||||
labels = {
|
labels = {
|
||||||
'nopaque.server_name': current_app.config['SERVER_NAME']
|
'nopaque.server_name': current_app.config['SERVER_NAME']
|
||||||
@ -141,7 +141,7 @@ def _create_cqpserver_container(corpus: Corpus):
|
|||||||
''' ## Entrypoint ## '''
|
''' ## Entrypoint ## '''
|
||||||
entrypoint = ['bash', '-c']
|
entrypoint = ['bash', '-c']
|
||||||
''' ## Image ## '''
|
''' ## Image ## '''
|
||||||
image = f'{current_app.config["NOPAQUE_DOCKER_IMAGE_PREFIX"]}cwb:r1879'
|
image = f'{current_app.config["NOPAQUE_DOCKER_IMAGE_PREFIX"]}cwb:r1887'
|
||||||
''' ## Name ## '''
|
''' ## Name ## '''
|
||||||
name = f'nopaque-cqpserver-{corpus.id}'
|
name = f'nopaque-cqpserver-{corpus.id}'
|
||||||
''' ## Network ## '''
|
''' ## Network ## '''
|
||||||
|
@ -1,14 +1,45 @@
|
|||||||
from .anonymous_user import *
|
from .anonymous_user import AnonymousUser
|
||||||
from .avatar import *
|
from .avatar import Avatar
|
||||||
from .corpus_file import *
|
from .corpus_file import CorpusFile
|
||||||
from .corpus_follower_association import *
|
from .corpus_follower_association import CorpusFollowerAssociation
|
||||||
from .corpus_follower_role import *
|
from .corpus_follower_role import CorpusFollowerPermission, CorpusFollowerRole
|
||||||
from .corpus import *
|
from .corpus import CorpusStatus, Corpus
|
||||||
from .job_input import *
|
from .job_input import JobInput
|
||||||
from .job_result import *
|
from .job_result import JobResult
|
||||||
from .job import *
|
from .job import JobStatus, Job
|
||||||
from .role import *
|
from .role import Permission, Role
|
||||||
from .spacy_nlp_pipeline_model import *
|
from .spacy_nlp_pipeline_model import SpaCyNLPPipelineModel
|
||||||
from .tesseract_ocr_pipeline_model import *
|
from .tesseract_ocr_pipeline_model import TesseractOCRPipelineModel
|
||||||
from .token import *
|
from .token import Token
|
||||||
from .user import *
|
from .user import (
|
||||||
|
ProfilePrivacySettings,
|
||||||
|
UserSettingJobStatusMailNotificationLevel,
|
||||||
|
User
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_models = [
|
||||||
|
Avatar,
|
||||||
|
CorpusFile,
|
||||||
|
CorpusFollowerAssociation,
|
||||||
|
CorpusFollowerRole,
|
||||||
|
Corpus,
|
||||||
|
JobInput,
|
||||||
|
JobResult,
|
||||||
|
Job,
|
||||||
|
Role,
|
||||||
|
SpaCyNLPPipelineModel,
|
||||||
|
TesseractOCRPipelineModel,
|
||||||
|
Token,
|
||||||
|
User
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
_enums = [
|
||||||
|
CorpusFollowerPermission,
|
||||||
|
CorpusStatus,
|
||||||
|
JobStatus,
|
||||||
|
Permission,
|
||||||
|
ProfilePrivacySettings,
|
||||||
|
UserSettingJobStatusMailNotificationLevel
|
||||||
|
]
|
||||||
|
@ -8,7 +8,7 @@ import shutil
|
|||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from app import db
|
from app import db
|
||||||
from app.converters.vrt import normalize_vrt_file
|
from app.converters.vrt import normalize_vrt_file
|
||||||
from app.extensions.nopaque_sqlalchemy_extras import IntEnumColumn
|
from app.extensions.nopaque_sqlalchemy_type_decorators import IntEnumColumn
|
||||||
from .corpus_follower_association import CorpusFollowerAssociation
|
from .corpus_follower_association import CorpusFollowerAssociation
|
||||||
|
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ def resource_after_delete(mapper, connection, resource):
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
room = f'/users/{resource.user_hashid}'
|
room = f'/users/{resource.user_hashid}'
|
||||||
socketio.emit('patch_user', jsonpatch, namespace='/users', room=room)
|
socketio.emit('PATCH', jsonpatch, room=room)
|
||||||
|
|
||||||
|
|
||||||
def cfa_after_delete(mapper, connection, cfa):
|
def cfa_after_delete(mapper, connection, cfa):
|
||||||
@ -55,7 +55,7 @@ def cfa_after_delete(mapper, connection, cfa):
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
room = f'/users/{cfa.corpus.user.hashid}'
|
room = f'/users/{cfa.corpus.user.hashid}'
|
||||||
socketio.emit('patch_user', jsonpatch, namespace='/users', room=room)
|
socketio.emit('PATCH', jsonpatch, room=room)
|
||||||
|
|
||||||
|
|
||||||
def resource_after_insert(mapper, connection, resource):
|
def resource_after_insert(mapper, connection, resource):
|
||||||
@ -70,7 +70,7 @@ def resource_after_insert(mapper, connection, resource):
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
room = f'/users/{resource.user_hashid}'
|
room = f'/users/{resource.user_hashid}'
|
||||||
socketio.emit('patch_user', jsonpatch, namespace='/users', room=room)
|
socketio.emit('PATCH', jsonpatch, room=room)
|
||||||
|
|
||||||
|
|
||||||
def cfa_after_insert(mapper, connection, cfa):
|
def cfa_after_insert(mapper, connection, cfa):
|
||||||
@ -84,7 +84,7 @@ def cfa_after_insert(mapper, connection, cfa):
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
room = f'/users/{cfa.corpus.user.hashid}'
|
room = f'/users/{cfa.corpus.user.hashid}'
|
||||||
socketio.emit('patch_user', jsonpatch, namespace='/users', room=room)
|
socketio.emit('PATCH', jsonpatch, room=room)
|
||||||
|
|
||||||
|
|
||||||
def resource_after_update(mapper, connection, resource):
|
def resource_after_update(mapper, connection, resource):
|
||||||
@ -110,7 +110,7 @@ def resource_after_update(mapper, connection, resource):
|
|||||||
)
|
)
|
||||||
if jsonpatch:
|
if jsonpatch:
|
||||||
room = f'/users/{resource.user_hashid}'
|
room = f'/users/{resource.user_hashid}'
|
||||||
socketio.emit('patch_user', jsonpatch, namespace='/users', room=room)
|
socketio.emit('PATCH', jsonpatch, room=room)
|
||||||
|
|
||||||
|
|
||||||
def job_after_update(mapper, connection, job):
|
def job_after_update(mapper, connection, job):
|
||||||
|
@ -6,7 +6,7 @@ from time import sleep
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
from app import db
|
from app import db
|
||||||
from app.extensions.nopaque_sqlalchemy_extras import ContainerColumn, IntEnumColumn
|
from app.extensions.nopaque_sqlalchemy_type_decorators import ContainerColumn, IntEnumColumn
|
||||||
|
|
||||||
|
|
||||||
class JobStatus(IntEnum):
|
class JobStatus(IntEnum):
|
||||||
|
@ -20,14 +20,6 @@ class JobInput(FileMixin, HashidMixin, db.Model):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<JobInput {self.filename}>'
|
return f'<JobInput {self.filename}>'
|
||||||
|
|
||||||
@property
|
|
||||||
def content_url(self):
|
|
||||||
return url_for(
|
|
||||||
'jobs.download_job_input',
|
|
||||||
job_id=self.job.id,
|
|
||||||
job_input_id=self.id
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def jsonpatch_path(self):
|
def jsonpatch_path(self):
|
||||||
return f'{self.job.jsonpatch_path}/inputs/{self.hashid}'
|
return f'{self.job.jsonpatch_path}/inputs/{self.hashid}'
|
||||||
@ -40,7 +32,7 @@ class JobInput(FileMixin, HashidMixin, db.Model):
|
|||||||
def url(self):
|
def url(self):
|
||||||
return url_for(
|
return url_for(
|
||||||
'jobs.job',
|
'jobs.job',
|
||||||
job_id=self.job_id,
|
job_input_id=self.id,
|
||||||
_anchor=f'job-{self.job.hashid}-input-{self.hashid}'
|
_anchor=f'job-{self.job.hashid}-input-{self.hashid}'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -22,14 +22,6 @@ class JobResult(FileMixin, HashidMixin, db.Model):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<JobResult {self.filename}>'
|
return f'<JobResult {self.filename}>'
|
||||||
|
|
||||||
@property
|
|
||||||
def download_url(self):
|
|
||||||
return url_for(
|
|
||||||
'jobs.download_job_result',
|
|
||||||
job_id=self.job_id,
|
|
||||||
job_result_id=self.id
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def jsonpatch_path(self):
|
def jsonpatch_path(self):
|
||||||
return f'{self.job.jsonpatch_path}/results/{self.hashid}'
|
return f'{self.job.jsonpatch_path}/results/{self.hashid}'
|
||||||
@ -41,8 +33,8 @@ class JobResult(FileMixin, HashidMixin, db.Model):
|
|||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self):
|
||||||
return url_for(
|
return url_for(
|
||||||
'jobs.job',
|
'job_results.job_result',
|
||||||
job_id=self.job_id,
|
job_result_id=self.id,
|
||||||
_anchor=f'job-{self.job.hashid}-result-{self.hashid}'
|
_anchor=f'job-{self.job.hashid}-result-{self.hashid}'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ from pathlib import Path
|
|||||||
import requests
|
import requests
|
||||||
import yaml
|
import yaml
|
||||||
from app import db
|
from app import db
|
||||||
from app.extensions.nopaque_sqlalchemy_extras import ContainerColumn
|
from app.extensions.nopaque_sqlalchemy_type_decorators import ContainerColumn
|
||||||
from .file_mixin import FileMixin
|
from .file_mixin import FileMixin
|
||||||
from .user import User
|
from .user import User
|
||||||
|
|
||||||
@ -41,7 +41,7 @@ class SpaCyNLPPipelineModel(FileMixin, HashidMixin, db.Model):
|
|||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self):
|
||||||
return url_for(
|
return url_for(
|
||||||
'contributions.spacy_nlp_pipeline_model',
|
'contributions.spacy_nlp_pipeline_models.entity',
|
||||||
spacy_nlp_pipeline_model_id=self.id
|
spacy_nlp_pipeline_model_id=self.id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ from pathlib import Path
|
|||||||
import requests
|
import requests
|
||||||
import yaml
|
import yaml
|
||||||
from app import db
|
from app import db
|
||||||
from app.extensions.nopaque_sqlalchemy_extras import ContainerColumn
|
from app.extensions.nopaque_sqlalchemy_type_decorators import ContainerColumn
|
||||||
from .file_mixin import FileMixin
|
from .file_mixin import FileMixin
|
||||||
from .user import User
|
from .user import User
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ class TesseractOCRPipelineModel(FileMixin, HashidMixin, db.Model):
|
|||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self):
|
||||||
return url_for(
|
return url_for(
|
||||||
'contributions.tesseract_ocr_pipeline_model',
|
'contributions.tesseract_ocr_pipeline_models.entity',
|
||||||
tesseract_ocr_pipeline_model_id=self.id
|
tesseract_ocr_pipeline_model_id=self.id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ import re
|
|||||||
import secrets
|
import secrets
|
||||||
import shutil
|
import shutil
|
||||||
from app import db, hashids
|
from app import db, hashids
|
||||||
from app.extensions.nopaque_sqlalchemy_extras import IntEnumColumn
|
from app.extensions.nopaque_sqlalchemy_type_decorators import IntEnumColumn
|
||||||
from .corpus import Corpus
|
from .corpus import Corpus
|
||||||
from .corpus_follower_association import CorpusFollowerAssociation
|
from .corpus_follower_association import CorpusFollowerAssociation
|
||||||
from .corpus_follower_role import CorpusFollowerRole
|
from .corpus_follower_role import CorpusFollowerRole
|
||||||
|
@ -9,8 +9,8 @@ from threading import Lock
|
|||||||
from app import db, docker_client, hashids, socketio
|
from app import db, docker_client, hashids, socketio
|
||||||
from app.decorators import socketio_login_required
|
from app.decorators import socketio_login_required
|
||||||
from app.models import Corpus, CorpusStatus
|
from app.models import Corpus, CorpusStatus
|
||||||
from . import cqi_extensions
|
from . import cqi_extension_functions
|
||||||
from .utils import CQiOverSocketIOSessionManager
|
from .utils import SessionManager
|
||||||
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
@ -85,6 +85,16 @@ CQI_API_FUNCTION_NAMES = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
CQI_EXTENSION_FUNCTION_NAMES = [
|
||||||
|
'ext_corpus_update_db',
|
||||||
|
'ext_corpus_static_data',
|
||||||
|
'ext_corpus_paginate_corpus',
|
||||||
|
'ext_cqp_paginate_subcorpus',
|
||||||
|
'ext_cqp_partial_export_subcorpus',
|
||||||
|
'ext_cqp_export_subcorpus',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class CQiOverSocketIONamespace(Namespace):
|
class CQiOverSocketIONamespace(Namespace):
|
||||||
@socketio_login_required
|
@socketio_login_required
|
||||||
def on_connect(self):
|
def on_connect(self):
|
||||||
@ -135,30 +145,31 @@ class CQiOverSocketIONamespace(Namespace):
|
|||||||
cqi_client = CQiClient(cqpserver_ip_address)
|
cqi_client = CQiClient(cqpserver_ip_address)
|
||||||
cqi_client_lock = Lock()
|
cqi_client_lock = Lock()
|
||||||
|
|
||||||
CQiOverSocketIOSessionManager.setup()
|
SessionManager.setup()
|
||||||
CQiOverSocketIOSessionManager.set_corpus_id(corpus_id)
|
SessionManager.set_corpus_id(corpus_id)
|
||||||
CQiOverSocketIOSessionManager.set_cqi_client(cqi_client)
|
SessionManager.set_cqi_client(cqi_client)
|
||||||
CQiOverSocketIOSessionManager.set_cqi_client_lock(cqi_client_lock)
|
SessionManager.set_cqi_client_lock(cqi_client_lock)
|
||||||
|
|
||||||
return {'code': 200, 'msg': 'OK'}
|
return {'code': 200, 'msg': 'OK'}
|
||||||
|
|
||||||
@socketio_login_required
|
@socketio_login_required
|
||||||
def on_exec(self, fn_name: str, fn_args: dict = {}) -> dict:
|
def on_exec(self, fn_name: str, fn_args: dict = {}) -> dict:
|
||||||
try:
|
try:
|
||||||
cqi_client = CQiOverSocketIOSessionManager.get_cqi_client()
|
cqi_client = SessionManager.get_cqi_client()
|
||||||
cqi_client_lock = CQiOverSocketIOSessionManager.get_cqi_client_lock()
|
cqi_client_lock = SessionManager.get_cqi_client_lock()
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return {'code': 424, 'msg': 'Failed Dependency'}
|
return {'code': 424, 'msg': 'Failed Dependency'}
|
||||||
|
|
||||||
if fn_name in CQI_API_FUNCTION_NAMES:
|
if fn_name in CQI_API_FUNCTION_NAMES:
|
||||||
fn = getattr(cqi_client.api, fn_name)
|
fn = getattr(cqi_client.api, fn_name)
|
||||||
elif fn_name in cqi_extensions.CQI_EXTENSION_FUNCTION_NAMES:
|
elif fn_name in CQI_EXTENSION_FUNCTION_NAMES:
|
||||||
fn = getattr(cqi_extensions, fn_name)
|
fn = getattr(cqi_extension_functions, fn_name)
|
||||||
else:
|
else:
|
||||||
return {'code': 400, 'msg': 'Bad Request'}
|
return {'code': 400, 'msg': 'Bad Request'}
|
||||||
|
|
||||||
for param in signature(fn).parameters.values():
|
for param in signature(fn).parameters.values():
|
||||||
# Check if the parameter is optional or required
|
# Check if the parameter is optional or required
|
||||||
|
# The following is true for required parameters
|
||||||
if param.default is param.empty:
|
if param.default is param.empty:
|
||||||
if param.name not in fn_args:
|
if param.name not in fn_args:
|
||||||
return {'code': 400, 'msg': 'Bad Request'}
|
return {'code': 400, 'msg': 'Bad Request'}
|
||||||
@ -198,10 +209,10 @@ class CQiOverSocketIONamespace(Namespace):
|
|||||||
|
|
||||||
def on_disconnect(self):
|
def on_disconnect(self):
|
||||||
try:
|
try:
|
||||||
corpus_id = CQiOverSocketIOSessionManager.get_corpus_id()
|
corpus_id = SessionManager.get_corpus_id()
|
||||||
cqi_client = CQiOverSocketIOSessionManager.get_cqi_client()
|
cqi_client = SessionManager.get_cqi_client()
|
||||||
cqi_client_lock = CQiOverSocketIOSessionManager.get_cqi_client_lock()
|
cqi_client_lock = SessionManager.get_cqi_client_lock()
|
||||||
CQiOverSocketIOSessionManager.teardown()
|
SessionManager.teardown()
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -8,22 +8,12 @@ import json
|
|||||||
import math
|
import math
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import Corpus
|
from app.models import Corpus
|
||||||
from .utils import CQiOverSocketIOSessionManager
|
from .utils import SessionManager
|
||||||
|
|
||||||
|
|
||||||
CQI_EXTENSION_FUNCTION_NAMES = [
|
|
||||||
'ext_corpus_update_db',
|
|
||||||
'ext_corpus_static_data',
|
|
||||||
'ext_corpus_paginate_corpus',
|
|
||||||
'ext_cqp_paginate_subcorpus',
|
|
||||||
'ext_cqp_partial_export_subcorpus',
|
|
||||||
'ext_cqp_export_subcorpus',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def ext_corpus_update_db(corpus: str) -> CQiStatusOk:
|
def ext_corpus_update_db(corpus: str) -> CQiStatusOk:
|
||||||
corpus_id = CQiOverSocketIOSessionManager.get_corpus_id()
|
corpus_id = SessionManager.get_corpus_id()
|
||||||
cqi_client = CQiOverSocketIOSessionManager.get_cqi_client()
|
cqi_client = SessionManager.get_cqi_client()
|
||||||
db_corpus = Corpus.query.get(corpus_id)
|
db_corpus = Corpus.query.get(corpus_id)
|
||||||
cqi_corpus = cqi_client.corpora.get(corpus)
|
cqi_corpus = cqi_client.corpora.get(corpus)
|
||||||
db_corpus.num_tokens = cqi_corpus.size
|
db_corpus.num_tokens = cqi_corpus.size
|
||||||
@ -32,7 +22,7 @@ def ext_corpus_update_db(corpus: str) -> CQiStatusOk:
|
|||||||
|
|
||||||
|
|
||||||
def ext_corpus_static_data(corpus: str) -> dict:
|
def ext_corpus_static_data(corpus: str) -> dict:
|
||||||
corpus_id = CQiOverSocketIOSessionManager.get_corpus_id()
|
corpus_id = SessionManager.get_corpus_id()
|
||||||
db_corpus = Corpus.query.get(corpus_id)
|
db_corpus = Corpus.query.get(corpus_id)
|
||||||
|
|
||||||
static_data_file_path = db_corpus.path / 'cwb' / 'static.json.gz'
|
static_data_file_path = db_corpus.path / 'cwb' / 'static.json.gz'
|
||||||
@ -40,7 +30,7 @@ def ext_corpus_static_data(corpus: str) -> dict:
|
|||||||
with static_data_file_path.open('rb') as f:
|
with static_data_file_path.open('rb') as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
cqi_client = CQiOverSocketIOSessionManager.get_cqi_client()
|
cqi_client = SessionManager.get_cqi_client()
|
||||||
cqi_corpus = cqi_client.corpora.get(corpus)
|
cqi_corpus = cqi_client.corpora.get(corpus)
|
||||||
cqi_p_attrs = cqi_corpus.positional_attributes.list()
|
cqi_p_attrs = cqi_corpus.positional_attributes.list()
|
||||||
cqi_s_attrs = cqi_corpus.structural_attributes.list()
|
cqi_s_attrs = cqi_corpus.structural_attributes.list()
|
||||||
@ -168,7 +158,7 @@ def ext_corpus_paginate_corpus(
|
|||||||
page: int = 1,
|
page: int = 1,
|
||||||
per_page: int = 20
|
per_page: int = 20
|
||||||
) -> dict:
|
) -> dict:
|
||||||
cqi_client = CQiOverSocketIOSessionManager.get_cqi_client()
|
cqi_client = SessionManager.get_cqi_client()
|
||||||
cqi_corpus = cqi_client.corpora.get(corpus)
|
cqi_corpus = cqi_client.corpora.get(corpus)
|
||||||
# Sanity checks
|
# Sanity checks
|
||||||
if (
|
if (
|
||||||
@ -215,7 +205,7 @@ def ext_cqp_paginate_subcorpus(
|
|||||||
per_page: int = 20
|
per_page: int = 20
|
||||||
) -> dict:
|
) -> dict:
|
||||||
corpus_name, subcorpus_name = subcorpus.split(':', 1)
|
corpus_name, subcorpus_name = subcorpus.split(':', 1)
|
||||||
cqi_client = CQiOverSocketIOSessionManager.get_cqi_client()
|
cqi_client = SessionManager.get_cqi_client()
|
||||||
cqi_corpus = cqi_client.corpora.get(corpus_name)
|
cqi_corpus = cqi_client.corpora.get(corpus_name)
|
||||||
cqi_subcorpus = cqi_corpus.subcorpora.get(subcorpus_name)
|
cqi_subcorpus = cqi_corpus.subcorpora.get(subcorpus_name)
|
||||||
# Sanity checks
|
# Sanity checks
|
||||||
@ -262,7 +252,7 @@ def ext_cqp_partial_export_subcorpus(
|
|||||||
context: int = 50
|
context: int = 50
|
||||||
) -> dict:
|
) -> dict:
|
||||||
corpus_name, subcorpus_name = subcorpus.split(':', 1)
|
corpus_name, subcorpus_name = subcorpus.split(':', 1)
|
||||||
cqi_client = CQiOverSocketIOSessionManager.get_cqi_client()
|
cqi_client = SessionManager.get_cqi_client()
|
||||||
cqi_corpus = cqi_client.corpora.get(corpus_name)
|
cqi_corpus = cqi_client.corpora.get(corpus_name)
|
||||||
cqi_subcorpus = cqi_corpus.subcorpora.get(subcorpus_name)
|
cqi_subcorpus = cqi_corpus.subcorpora.get(subcorpus_name)
|
||||||
cqi_subcorpus_partial_export = _partial_export_subcorpus(cqi_subcorpus, match_id_list, context=context)
|
cqi_subcorpus_partial_export = _partial_export_subcorpus(cqi_subcorpus, match_id_list, context=context)
|
||||||
@ -271,7 +261,7 @@ def ext_cqp_partial_export_subcorpus(
|
|||||||
|
|
||||||
def ext_cqp_export_subcorpus(subcorpus: str, context: int = 50) -> dict:
|
def ext_cqp_export_subcorpus(subcorpus: str, context: int = 50) -> dict:
|
||||||
corpus_name, subcorpus_name = subcorpus.split(':', 1)
|
corpus_name, subcorpus_name = subcorpus.split(':', 1)
|
||||||
cqi_client = CQiOverSocketIOSessionManager.get_cqi_client()
|
cqi_client = SessionManager.get_cqi_client()
|
||||||
cqi_corpus = cqi_client.corpora.get(corpus_name)
|
cqi_corpus = cqi_client.corpora.get(corpus_name)
|
||||||
cqi_subcorpus = cqi_corpus.subcorpora.get(subcorpus_name)
|
cqi_subcorpus = cqi_corpus.subcorpora.get(subcorpus_name)
|
||||||
cqi_subcorpus_export = _export_subcorpus(cqi_subcorpus, context=context)
|
cqi_subcorpus_export = _export_subcorpus(cqi_subcorpus, context=context)
|
@ -3,7 +3,7 @@ from threading import Lock
|
|||||||
from flask import session
|
from flask import session
|
||||||
|
|
||||||
|
|
||||||
class CQiOverSocketIOSessionManager:
|
class SessionManager:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def setup():
|
def setup():
|
||||||
session['cqi_over_sio'] = {}
|
session['cqi_over_sio'] = {}
|
||||||
|
@ -1,78 +0,0 @@
|
|||||||
from flask_login import current_user
|
|
||||||
from flask_socketio import join_room, leave_room, Namespace
|
|
||||||
from app import hashids
|
|
||||||
from app.decorators import socketio_login_required
|
|
||||||
from app.models import User
|
|
||||||
|
|
||||||
|
|
||||||
class UsersNamespace(Namespace):
|
|
||||||
@socketio_login_required
|
|
||||||
def on_get_user(self, user_hashid: str) -> dict:
|
|
||||||
user_id = hashids.decode(user_hashid)
|
|
||||||
|
|
||||||
if not isinstance(user_id, int):
|
|
||||||
return {'status': 400, 'statusText': 'Bad Request'}
|
|
||||||
|
|
||||||
user = User.query.get(user_id)
|
|
||||||
|
|
||||||
if user is None:
|
|
||||||
return {'status': 404, 'statusText': 'Not found'}
|
|
||||||
|
|
||||||
if not (
|
|
||||||
user == current_user
|
|
||||||
or current_user.is_administrator
|
|
||||||
):
|
|
||||||
return {'status': 403, 'statusText': 'Forbidden'}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'body': user.to_json_serializeable(
|
|
||||||
backrefs=True,
|
|
||||||
relationships=True
|
|
||||||
),
|
|
||||||
'status': 200,
|
|
||||||
'statusText': 'OK'
|
|
||||||
}
|
|
||||||
|
|
||||||
@socketio_login_required
|
|
||||||
def on_subscribe_user(self, user_hashid: str) -> dict:
|
|
||||||
user_id = hashids.decode(user_hashid)
|
|
||||||
|
|
||||||
if not isinstance(user_id, int):
|
|
||||||
return {'status': 400, 'statusText': 'Bad Request'}
|
|
||||||
|
|
||||||
user = User.query.get(user_id)
|
|
||||||
|
|
||||||
if user is None:
|
|
||||||
return {'status': 404, 'statusText': 'Not found'}
|
|
||||||
|
|
||||||
if not (
|
|
||||||
user == current_user
|
|
||||||
or current_user.is_administrator
|
|
||||||
):
|
|
||||||
return {'status': 403, 'statusText': 'Forbidden'}
|
|
||||||
|
|
||||||
join_room(f'/users/{user.hashid}')
|
|
||||||
|
|
||||||
return {'status': 200, 'statusText': 'OK'}
|
|
||||||
|
|
||||||
@socketio_login_required
|
|
||||||
def on_unsubscribe_user(self, user_hashid: str) -> dict:
|
|
||||||
user_id = hashids.decode(user_hashid)
|
|
||||||
|
|
||||||
if not isinstance(user_id, int):
|
|
||||||
return {'status': 400, 'statusText': 'Bad Request'}
|
|
||||||
|
|
||||||
user = User.query.get(user_id)
|
|
||||||
|
|
||||||
if user is None:
|
|
||||||
return {'status': 404, 'statusText': 'Not found'}
|
|
||||||
|
|
||||||
if not (
|
|
||||||
user == current_user
|
|
||||||
or current_user.is_administrator
|
|
||||||
):
|
|
||||||
return {'status': 403, 'statusText': 'Forbidden'}
|
|
||||||
|
|
||||||
leave_room(f'/users/{user.hashid}')
|
|
||||||
|
|
||||||
return {'status': 200, 'statusText': 'OK'}
|
|
@ -2,6 +2,10 @@
|
|||||||
--corpus-status-content: "unprepared";
|
--corpus-status-content: "unprepared";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-corpus-status="SUBMITTED"] {
|
||||||
|
--corpus-status-content: "submitted";
|
||||||
|
}
|
||||||
|
|
||||||
[data-corpus-status="QUEUED"] {
|
[data-corpus-status="QUEUED"] {
|
||||||
--corpus-status-content: "queued";
|
--corpus-status-content: "queued";
|
||||||
}
|
}
|
||||||
|
47
app/static/css/height.css
Normal file
47
app/static/css/height.css
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
.h-10 {
|
||||||
|
height: 10% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-20 {
|
||||||
|
height: 20% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-25 {
|
||||||
|
height: 25% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-30 {
|
||||||
|
height: 30% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-40 {
|
||||||
|
height: 40% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-50 {
|
||||||
|
height: 50% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-60 {
|
||||||
|
height: 60% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-70 {
|
||||||
|
height: 70% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-75 {
|
||||||
|
height: 75% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-80 {
|
||||||
|
height: 80% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-90 {
|
||||||
|
height: 90% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-100 {
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
@ -1,36 +1,3 @@
|
|||||||
/* #region sidenav-fixed */
|
|
||||||
/*
|
|
||||||
* The sidenav-fixed class is used which causes the sidenav to be fixed and open
|
|
||||||
* on large screens and hides to the regular functionality on smaller screens.
|
|
||||||
* In order to prevent the sidenav to overlap the content, the content (header, main and footer)
|
|
||||||
* gets an offset equal to the width of the sidenav.
|
|
||||||
*
|
|
||||||
* Read more: https://materializecss.com/sidenav.html#variations
|
|
||||||
*/
|
|
||||||
body[data-sidenav-fixed="true" i] main,
|
|
||||||
body[data-sidenav-fixed="true" i] footer {
|
|
||||||
padding-left: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 992px) {
|
|
||||||
body[data-sidenav-fixed="true" i] main,
|
|
||||||
body[data-sidenav-fixed="true" i] footer {
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body[data-sidenav-fixed="true" i] .navbar-fixed > nav {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (min-width: 993px) {
|
|
||||||
body[data-sidenav-fixed="true" i] .sidenav-fixed {
|
|
||||||
margin-top: 64px;
|
|
||||||
z-index: 996;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* #endregion sidenav-fixed */
|
|
||||||
|
|
||||||
/* #region sticky-footer */
|
/* #region sticky-footer */
|
||||||
/*
|
/*
|
||||||
* Sticky Footer:
|
* Sticky Footer:
|
||||||
@ -43,13 +10,13 @@ body[data-sidenav-fixed="true" i] footer {
|
|||||||
*
|
*
|
||||||
* Read more: https://materializecss.com/footer.html#sticky-footer
|
* Read more: https://materializecss.com/footer.html#sticky-footer
|
||||||
*/
|
*/
|
||||||
body[data-sticky-footer="true" i] {
|
body {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[data-sticky-footer="true" i] main {
|
main {
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
}
|
}
|
||||||
/* #endregion sticky-footer */
|
/* #endregion sticky-footer */
|
||||||
|
47
app/static/css/width.css
Normal file
47
app/static/css/width.css
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
.w-10 {
|
||||||
|
width: 10% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-20 {
|
||||||
|
width: 20% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-25 {
|
||||||
|
width: 25% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-30 {
|
||||||
|
width: 30% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-40 {
|
||||||
|
width: 40% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-50 {
|
||||||
|
width: 50% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-60 {
|
||||||
|
width: 60% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-70 {
|
||||||
|
width: 70% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-75 {
|
||||||
|
width: 75% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-80 {
|
||||||
|
width: 80% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-90 {
|
||||||
|
width: 90% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-100 {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
@ -1,196 +0,0 @@
|
|||||||
nopaque.App = class App {
|
|
||||||
#promises;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.data = {
|
|
||||||
users: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.#promises = {
|
|
||||||
getUser: {},
|
|
||||||
subscribeUser: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.sockets = {
|
|
||||||
users: io('/users', {transports: ['websocket'], upgrade: false})
|
|
||||||
};
|
|
||||||
|
|
||||||
this.sockets.users.on('patch_user', (patch) => {this.onPatch(patch);});
|
|
||||||
}
|
|
||||||
|
|
||||||
getUser(userId) {
|
|
||||||
if (userId in this.#promises.getUser) {
|
|
||||||
return this.#promises.getUser[userId];
|
|
||||||
}
|
|
||||||
|
|
||||||
let socket = this.sockets.users;
|
|
||||||
|
|
||||||
this.#promises.getUser[userId] = new Promise((resolve, reject) => {
|
|
||||||
socket.emit('get_user', userId, (response) => {
|
|
||||||
if (response.status === 200) {
|
|
||||||
this.data.users[userId] = response.body;
|
|
||||||
resolve(this.data.users[userId]);
|
|
||||||
} else {
|
|
||||||
reject(`[${response.status}] ${response.statusText}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.#promises.getUser[userId];
|
|
||||||
}
|
|
||||||
|
|
||||||
subscribeUser(userId) {
|
|
||||||
if (userId in this.#promises.subscribeUser) {
|
|
||||||
return this.#promises.subscribeUser[userId];
|
|
||||||
}
|
|
||||||
|
|
||||||
let socket = this.sockets.users;
|
|
||||||
|
|
||||||
this.#promises.subscribeUser[userId] = new Promise((resolve, reject) => {
|
|
||||||
socket.emit('subscribe_user', userId, (response) => {
|
|
||||||
if (response.status === 200) {
|
|
||||||
resolve(response);
|
|
||||||
} else {
|
|
||||||
reject(response);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.#promises.subscribeUser[userId];
|
|
||||||
}
|
|
||||||
|
|
||||||
flash(message, category) {
|
|
||||||
let iconPrefix = '';
|
|
||||||
switch (category) {
|
|
||||||
case 'corpus': {
|
|
||||||
iconPrefix = '<i class="left material-icons">book</i>';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'error': {
|
|
||||||
iconPrefix = '<i class="error-color-text left material-icons">error</i>';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'job': {
|
|
||||||
iconPrefix = '<i class="left nopaque-icons">J</i>';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'settings': {
|
|
||||||
iconPrefix = '<i class="left material-icons">settings</i>';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
iconPrefix = '<i class="left material-icons">notifications</i>';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let toast = M.toast(
|
|
||||||
{
|
|
||||||
html: `
|
|
||||||
<span>${iconPrefix}${message}</span>
|
|
||||||
<button class="action-button btn-flat toast-action white-text" data-action="close">
|
|
||||||
<i class="material-icons">close</i>
|
|
||||||
</button>
|
|
||||||
`.trim()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
let toastCloseActionElement = toast.el.querySelector('.action-button[data-action="close"]');
|
|
||||||
toastCloseActionElement.addEventListener('click', () => {toast.dismiss();});
|
|
||||||
}
|
|
||||||
|
|
||||||
onPatch(patch) {
|
|
||||||
// Filter Patch to only include operations on users that are initialized
|
|
||||||
let regExp = new RegExp(`^/users/(${Object.keys(this.data.users).join('|')})`);
|
|
||||||
let filteredPatch = patch.filter(operation => regExp.test(operation.path));
|
|
||||||
|
|
||||||
// Handle job status updates
|
|
||||||
let subRegExp = new RegExp(`^/users/([A-Za-z0-9]*)/jobs/([A-Za-z0-9]*)/status$`);
|
|
||||||
let subFilteredPatch = filteredPatch
|
|
||||||
.filter((operation) => {return operation.op === 'replace';})
|
|
||||||
.filter((operation) => {return subRegExp.test(operation.path);});
|
|
||||||
for (let operation of subFilteredPatch) {
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply Patch
|
|
||||||
jsonpatch.applyPatch(this.data, filteredPatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.initUi();
|
|
||||||
}
|
|
||||||
|
|
||||||
initUi() {
|
|
||||||
/* Pre-Initialization fixes */
|
|
||||||
// #region
|
|
||||||
|
|
||||||
// Flask-WTF sets the standard HTML maxlength Attribute on input/textarea
|
|
||||||
// elements to specify their maximum length (in characters). Unfortunatly
|
|
||||||
// Materialize won't recognize the maxlength Attribute, instead it uses
|
|
||||||
// the data-length Attribute. It's conversion time :)
|
|
||||||
for (let elem of document.querySelectorAll('input[maxlength], textarea[maxlength]')) {
|
|
||||||
elem.dataset.length = elem.getAttribute('maxlength');
|
|
||||||
elem.removeAttribute('maxlength');
|
|
||||||
}
|
|
||||||
|
|
||||||
// To work around some limitations with the Form setup of Flask-WTF.
|
|
||||||
// HTML option elements with an empty value are considered as placeholder
|
|
||||||
// elements. The user should not be able to actively select these options.
|
|
||||||
// So they get the disabled attribute.
|
|
||||||
for (let optionElement of document.querySelectorAll('option[value=""]')) {
|
|
||||||
optionElement.disabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Check why we are doing this.
|
|
||||||
for (let optgroupElement of document.querySelectorAll('optgroup[label=""]')) {
|
|
||||||
for (let c of optgroupElement.children) {
|
|
||||||
optgroupElement.parentElement.insertAdjacentElement('afterbegin', c);
|
|
||||||
}
|
|
||||||
optgroupElement.remove();
|
|
||||||
}
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
|
|
||||||
/* Initialize Materialize Components */
|
|
||||||
// #region
|
|
||||||
|
|
||||||
// Automatically initialize Materialize Components that do not require
|
|
||||||
// additional configuration.
|
|
||||||
M.AutoInit();
|
|
||||||
|
|
||||||
// CharacterCounters
|
|
||||||
// Materialize didn't include the CharacterCounter plugin within the
|
|
||||||
// AutoInit method (maybe they forgot it?). Anyway... We do it here. :)
|
|
||||||
M.CharacterCounter.init(document.querySelectorAll('input[data-length]:not(.no-autoinit), textarea[data-length]:not(.no-autoinit)'));
|
|
||||||
|
|
||||||
// Header navigation account Dropdown.
|
|
||||||
M.Dropdown.init(
|
|
||||||
document.querySelector('#nav-account-dropdown-trigger'),
|
|
||||||
{
|
|
||||||
alignment: 'right',
|
|
||||||
constrainWidth: false,
|
|
||||||
coverTrigger: false
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Terms of use modal
|
|
||||||
M.Modal.init(
|
|
||||||
document.querySelector('#terms-of-use-modal'),
|
|
||||||
{
|
|
||||||
dismissible: false,
|
|
||||||
onCloseEnd: (modalElement) => {
|
|
||||||
nopaque.requests.users.entity.acceptTermsOfUse();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
|
|
||||||
/* Initialize nopaque Components */
|
|
||||||
// #region
|
|
||||||
nopaque.resource_displays.AutoInit();
|
|
||||||
nopaque.resource_lists.AutoInit();
|
|
||||||
nopaque.forms.AutoInit();
|
|
||||||
// #endregion
|
|
||||||
}
|
|
||||||
};
|
|
24
app/static/js/app/client.js
Normal file
24
app/static/js/app/client.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
nopaque.app.Client = class Client {
|
||||||
|
constructor() {
|
||||||
|
this.socket = io({transports: ['websocket'], upgrade: false});
|
||||||
|
|
||||||
|
// Endpoints
|
||||||
|
this.corpora = new nopaque.app.endpoints.Corpora(this);
|
||||||
|
this.jobs = new nopaque.app.endpoints.Jobs(this);
|
||||||
|
this.main = new nopaque.app.endpoints.Main(this);
|
||||||
|
this.settings = new nopaque.app.endpoints.Settings(this);
|
||||||
|
this.users = new nopaque.app.endpoints.Users(this);
|
||||||
|
|
||||||
|
// Extensions
|
||||||
|
this.toaster = new nopaque.app.extensions.Toaster(this);
|
||||||
|
this.ui = new nopaque.app.extensions.UI(this);
|
||||||
|
this.userHub = new nopaque.app.extensions.UserHub(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Initialize extensions
|
||||||
|
this.toaster.init();
|
||||||
|
this.ui.init();
|
||||||
|
this.userHub.init();
|
||||||
|
}
|
||||||
|
};
|
93
app/static/js/app/endpoints/corpora.js
Normal file
93
app/static/js/app/endpoints/corpora.js
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
nopaque.app.endpoints.Corpora = class Corpora {
|
||||||
|
constructor(app) {
|
||||||
|
this.app = app;
|
||||||
|
|
||||||
|
this.socket = io('/corpora', {transports: ['websocket'], upgrade: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(corpusId) {
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json'
|
||||||
|
},
|
||||||
|
method: 'DELETE'
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(`/corpora/${corpusId}`, options);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async build(corpusId) {
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json'
|
||||||
|
},
|
||||||
|
method: 'POST'
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(`/corpora/${corpusId}/build`, options);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStopwords(corpusId) {
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(`/corpora/${corpusId}/analysis/stopwords`, options);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createShareLink(corpusId, expirationDate, roleName) {
|
||||||
|
const options = {
|
||||||
|
body: JSON.stringify({
|
||||||
|
'expiration_date': expirationDate,
|
||||||
|
'role_name': roleName
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
method: 'POST'
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(`/corpora/${corpusId}/create-share-link`, options);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateIsPublic(corpusId, newValue) {
|
||||||
|
const options = {
|
||||||
|
body: JSON.stringify(newValue),
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
method: 'PUT',
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(`/corpora/${corpusId}/is-public`, options);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
1
app/static/js/app/endpoints/index.js
Normal file
1
app/static/js/app/endpoints/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
nopaque.app.endpoints = {};
|
52
app/static/js/app/endpoints/jobs.js
Normal file
52
app/static/js/app/endpoints/jobs.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
nopaque.app.endpoints.Jobs = class Jobs {
|
||||||
|
constructor(app) {
|
||||||
|
this.app = app;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(jobId) {
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json'
|
||||||
|
},
|
||||||
|
method: 'DELETE'
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(`/jobs/${jobId}`, options);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async log(jobId) {
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(`/jobs/${jobId}/log`, options);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async restart(jobId) {
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json'
|
||||||
|
},
|
||||||
|
method: 'POST'
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(`/jobs/${jobId}/restart`, options);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
22
app/static/js/app/endpoints/main.js
Normal file
22
app/static/js/app/endpoints/main.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
nopaque.app.endpoints.Main = class Main {
|
||||||
|
constructor(app) {
|
||||||
|
this.app = app;
|
||||||
|
}
|
||||||
|
|
||||||
|
async acceptTermsOfUse() {
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(`/accept-terms-of-use`, options);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
77
app/static/js/app/endpoints/settings.js
Normal file
77
app/static/js/app/endpoints/settings.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
nopaque.app.endpoints.Settings = class Settings {
|
||||||
|
constructor(app) {
|
||||||
|
this.app = app;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateProfileIsPublic(newValue) {
|
||||||
|
const options = {
|
||||||
|
body: JSON.stringify(newValue),
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
method: 'PUT',
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(`/settings/profile-is-public`, options);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateProfileShowEmail(newValue) {
|
||||||
|
const options = {
|
||||||
|
body: JSON.stringify(newValue),
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
method: 'PUT',
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(`/settings/profile-show-email`, options);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateProfileShowLastSeen(newValue) {
|
||||||
|
const options = {
|
||||||
|
body: JSON.stringify(newValue),
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
method: 'PUT',
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(`/settings/profile-show-last-seen`, options);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateProfileShowMemberSince(newValue) {
|
||||||
|
const options = {
|
||||||
|
body: JSON.stringify(newValue),
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
method: 'PUT',
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(`/settings/profile-show-member-since`, options);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
52
app/static/js/app/endpoints/users.js
Normal file
52
app/static/js/app/endpoints/users.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
nopaque.app.endpoints.Users = class Users {
|
||||||
|
constructor(app) {
|
||||||
|
this.app = app;
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(userId) {
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(`/users/${userId}`, options);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async subscribe(userId) {
|
||||||
|
const response = await this.app.socket.emitWithAck('SUBSCRIBE User', userId);
|
||||||
|
|
||||||
|
if (response.code != 204) {
|
||||||
|
throw new Error(`${response.name}: ${response.description}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async unsubscribe(userId) {
|
||||||
|
const response = await this.app.socket.emitWithAck('UNSUBSCRIBE User', userId);
|
||||||
|
|
||||||
|
if (response.status != 204) {
|
||||||
|
throw new Error(`${response.name}: ${response.description}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(userId) {
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json'
|
||||||
|
},
|
||||||
|
method: 'DELETE'
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(`/users/${userId}`, options);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
1
app/static/js/app/extensions/index.js
Normal file
1
app/static/js/app/extensions/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
nopaque.app.extensions = {};
|
56
app/static/js/app/extensions/toaster.js
Normal file
56
app/static/js/app/extensions/toaster.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
nopaque.app.extensions.Toaster = class Toaster {
|
||||||
|
constructor(app) {
|
||||||
|
this.app = app;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.app.userHub.addEventListener('patch', (event) => {this.#onPatch(event.detail);});
|
||||||
|
}
|
||||||
|
|
||||||
|
async #onPatch(patch) {
|
||||||
|
// Handle corpus updates
|
||||||
|
const corpusRegExp = new RegExp(`^/users/([A-Za-z0-9]+)/corpora/([A-Za-z0-9]+)`);
|
||||||
|
const corpusPatch = patch.filter((operation) => {return corpusRegExp.test(operation.path);});
|
||||||
|
|
||||||
|
this.#onCorpusPatch(corpusPatch);
|
||||||
|
|
||||||
|
// Handle job updates
|
||||||
|
const jobRegExp = new RegExp(`^/users/([A-Za-z0-9]+)/jobs/([A-Za-z0-9]+)`);
|
||||||
|
const jobPatch = patch.filter((operation) => {return jobRegExp.test(operation.path);});
|
||||||
|
|
||||||
|
this.#onJobPatch(jobPatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
async #onCorpusPatch(patch) {
|
||||||
|
return;
|
||||||
|
// Handle corpus status updates
|
||||||
|
const corpusStatusRegExp = new RegExp(`^/users/([A-Za-z0-9]+)/corpora/([A-Za-z0-9]+)/status$`);
|
||||||
|
const corpusStatusPatch = patch
|
||||||
|
.filter((operation) => {return corpusStatusRegExp.test(operation.path);})
|
||||||
|
.filter((operation) => {return operation.op === 'replace';});
|
||||||
|
|
||||||
|
for (let operation of corpusStatusPatch) {
|
||||||
|
const [match, userId, corpusId] = operation.path.match(corpusStatusRegExp);
|
||||||
|
const user = await this.app.userHub.get(userId);
|
||||||
|
const corpus = user.corpora[corpusId];
|
||||||
|
|
||||||
|
this.app.ui.flash(`[<a href="/corpora/${corpusId}">${corpus.title}</a>] New status: <span class="corpus-status-text" data-corpus-status="${operation.value}"></span>`, 'corpus');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #onJobPatch(patch) {
|
||||||
|
// Handle job status updates
|
||||||
|
const jobStatusRegExp = new RegExp(`^/users/([A-Za-z0-9]+)/jobs/([A-Za-z0-9]+)/status$`);
|
||||||
|
const jobStatusPatch = patch
|
||||||
|
.filter((operation) => {return jobStatusRegExp.test(operation.path);})
|
||||||
|
.filter((operation) => {return operation.op === 'replace';});
|
||||||
|
|
||||||
|
for (let operation of jobStatusPatch) {
|
||||||
|
const [match, userId, jobId] = operation.path.match(jobStatusRegExp);
|
||||||
|
const user = await this.app.userHub.get(userId);
|
||||||
|
const job = user.jobs[jobId];
|
||||||
|
|
||||||
|
this.app.ui.flash(`[<a href="/jobs/${jobId}">${job.title}</a>] New status: <span class="job-status-text" data-job-status="${operation.value}"></span>`, 'job');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
123
app/static/js/app/extensions/ui.js
Normal file
123
app/static/js/app/extensions/ui.js
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
nopaque.app.extensions.UI = class UI {
|
||||||
|
constructor(app) {
|
||||||
|
this.app = app;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
/* Pre-Initialization fixes */
|
||||||
|
// #region
|
||||||
|
|
||||||
|
// Flask-WTF sets the standard HTML maxlength Attribute on input/textarea
|
||||||
|
// elements to specify their maximum length (in characters). Unfortunatly
|
||||||
|
// Materialize won't recognize the maxlength Attribute, instead it uses
|
||||||
|
// the data-length Attribute. It's conversion time :)
|
||||||
|
for (let elem of document.querySelectorAll('input[maxlength], textarea[maxlength]')) {
|
||||||
|
elem.dataset.length = elem.getAttribute('maxlength');
|
||||||
|
elem.removeAttribute('maxlength');
|
||||||
|
}
|
||||||
|
|
||||||
|
// To work around some limitations with the Form setup of Flask-WTF.
|
||||||
|
// HTML option elements with an empty value are considered as placeholder
|
||||||
|
// elements. The user should not be able to actively select these options.
|
||||||
|
// So they get the disabled attribute.
|
||||||
|
for (let optionElement of document.querySelectorAll('option[value=""]')) {
|
||||||
|
optionElement.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Check why we are doing this.
|
||||||
|
for (let optgroupElement of document.querySelectorAll('optgroup[label=""]')) {
|
||||||
|
for (let c of optgroupElement.children) {
|
||||||
|
optgroupElement.parentElement.insertAdjacentElement('afterbegin', c);
|
||||||
|
}
|
||||||
|
optgroupElement.remove();
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
|
||||||
|
/* Initialize Materialize Components */
|
||||||
|
// #region
|
||||||
|
|
||||||
|
// Automatically initialize Materialize Components that do not require
|
||||||
|
// additional configuration.
|
||||||
|
M.AutoInit();
|
||||||
|
|
||||||
|
// CharacterCounters
|
||||||
|
// Materialize didn't include the CharacterCounter plugin within the
|
||||||
|
// AutoInit method (maybe they forgot it?). Anyway... We do it here. :)
|
||||||
|
M.CharacterCounter.init(document.querySelectorAll('input[data-length]:not(.no-autoinit), textarea[data-length]:not(.no-autoinit)'));
|
||||||
|
|
||||||
|
// Header navigation processes and services Dropdown.
|
||||||
|
M.Dropdown.init(
|
||||||
|
document.querySelector('#navbar-data-processing-and-analysis-dropdown-trigger'),
|
||||||
|
{
|
||||||
|
constrainWidth: false,
|
||||||
|
container: document.querySelector('#dropdowns'),
|
||||||
|
coverTrigger: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Header navigation account Dropdown.
|
||||||
|
M.Dropdown.init(
|
||||||
|
document.querySelector('#navbar-account-dropdown-trigger'),
|
||||||
|
{
|
||||||
|
alignment: 'right',
|
||||||
|
constrainWidth: false,
|
||||||
|
container: document.querySelector('#dropdowns'),
|
||||||
|
coverTrigger: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Terms of use modal
|
||||||
|
M.Modal.init(
|
||||||
|
document.querySelector('#terms-of-use-modal'),
|
||||||
|
{
|
||||||
|
dismissible: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
|
||||||
|
/* Initialize nopaque Components */
|
||||||
|
// #region
|
||||||
|
nopaque.resource_displays.AutoInit();
|
||||||
|
nopaque.resource_lists.AutoInit();
|
||||||
|
nopaque.forms.AutoInit();
|
||||||
|
// #endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
flash(message, category) {
|
||||||
|
let iconPrefix;
|
||||||
|
|
||||||
|
switch (category) {
|
||||||
|
case 'corpus': {
|
||||||
|
iconPrefix = '<i class="material-icons left">book</i>';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'job': {
|
||||||
|
iconPrefix = '<i class="nopaque-icons left">J</i>';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'error': {
|
||||||
|
iconPrefix = '<i class="material-icons left error-color-text">error</i>';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
iconPrefix = '<i class="material-icons left">notifications</i>';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let toast = M.toast(
|
||||||
|
{
|
||||||
|
html: `
|
||||||
|
<span>${iconPrefix}${message}</span>
|
||||||
|
<button class="btn-flat toast-action white-text" data-toast-action="dismiss">
|
||||||
|
<i class="material-icons">close</i>
|
||||||
|
</button>
|
||||||
|
`.trim()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let dismissToastElement = toast.el.querySelector('.toast-action[data-toast-action="dismiss"]');
|
||||||
|
dismissToastElement.addEventListener('click', () => {toast.dismiss();});
|
||||||
|
}
|
||||||
|
}
|
68
app/static/js/app/extensions/user-hub.js
Normal file
68
app/static/js/app/extensions/user-hub.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
nopaque.app.extensions.UserHub = class UserHub extends EventTarget {
|
||||||
|
#data;
|
||||||
|
|
||||||
|
constructor(app) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.app = app;
|
||||||
|
|
||||||
|
this.#data = {
|
||||||
|
users: {},
|
||||||
|
promises: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.app.socket.on('PATCH', (patch) => {this.#onPatch(patch)});
|
||||||
|
}
|
||||||
|
|
||||||
|
add(userId) {
|
||||||
|
if (!(userId in this.#data.promises)) {
|
||||||
|
this.#data.promises[userId] = this.#add(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.#data.promises[userId];
|
||||||
|
}
|
||||||
|
|
||||||
|
async #add(userId) {
|
||||||
|
await this.app.users.subscribe(userId);
|
||||||
|
this.#data.users[userId] = await this.app.users.get(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(userId) {
|
||||||
|
await this.add(userId);
|
||||||
|
return this.#data.users[userId];
|
||||||
|
}
|
||||||
|
|
||||||
|
#onPatch(patch) {
|
||||||
|
// Filter patch to only include operations on users that are initialized
|
||||||
|
const filterRegExp = new RegExp(`^/users/(${Object.keys(this.#data.users).join('|')})`);
|
||||||
|
const filteredPatch = patch.filter(operation => filterRegExp.test(operation.path));
|
||||||
|
|
||||||
|
// Apply patch
|
||||||
|
jsonpatch.applyPatch(this.#data, filteredPatch);
|
||||||
|
|
||||||
|
// Notify event listeners
|
||||||
|
const patchEventa = new CustomEvent('patch', {detail: filteredPatch});
|
||||||
|
this.dispatchEvent(patchEventa);
|
||||||
|
|
||||||
|
// Notify event listeners. Event type: "patch *"
|
||||||
|
const patchEvent = new CustomEvent('patch *', {detail: filteredPatch});
|
||||||
|
this.dispatchEvent(patchEvent);
|
||||||
|
|
||||||
|
// Group patches by user id: {<user-id>: [op, ...], ...}
|
||||||
|
const patches = {};
|
||||||
|
const matchRegExp = new RegExp(`^/users/([A-Za-z0-9]+)`);
|
||||||
|
for (let operation of filteredPatch) {
|
||||||
|
const [match, userId] = operation.path.match(matchRegExp);
|
||||||
|
if (!(userId in patches)) {patches[userId] = [];}
|
||||||
|
patches[userId].push(operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify event listeners. Event type: "patch <user-id>"
|
||||||
|
for (let [userId, patch] of Object.entries(patches)) {
|
||||||
|
const userPatchEvent = new CustomEvent(`patch ${userId}`, {detail: patch});
|
||||||
|
this.dispatchEvent(userPatchEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
app/static/js/app/index.js
Normal file
1
app/static/js/app/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
nopaque.app = {};
|
@ -66,7 +66,7 @@ nopaque.corpus_analysis.ConcordanceExtension = class ConcordanceExtension {
|
|||||||
errorString += `${error.constructor.name}`;
|
errorString += `${error.constructor.name}`;
|
||||||
this.elements.error.innerText = errorString;
|
this.elements.error.innerText = errorString;
|
||||||
this.elements.error.classList.remove('hide');
|
this.elements.error.classList.remove('hide');
|
||||||
app.flash(errorString, 'error');
|
app.ui.flash(errorString, 'error');
|
||||||
this.elements.progress.classList.add('hide');
|
this.elements.progress.classList.add('hide');
|
||||||
}
|
}
|
||||||
this.app.enableActionElements();
|
this.app.enableActionElements();
|
||||||
@ -239,7 +239,7 @@ nopaque.corpus_analysis.ConcordanceExtension = class ConcordanceExtension {
|
|||||||
if (subcorpus.selectedItems.size === 0) {
|
if (subcorpus.selectedItems.size === 0) {
|
||||||
this.elements.progress.classList.add('hide');
|
this.elements.progress.classList.add('hide');
|
||||||
this.app.enableActionElements();
|
this.app.enableActionElements();
|
||||||
app.flash('No matches selected', 'error');
|
app.ui.flash('No matches selected', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
promise = subcorpus.o.partialExport([...subcorpus.selectedItems], 50);
|
promise = subcorpus.o.partialExport([...subcorpus.selectedItems], 50);
|
||||||
@ -298,7 +298,7 @@ nopaque.corpus_analysis.ConcordanceExtension = class ConcordanceExtension {
|
|||||||
let subcorpus = this.data.subcorpora[this.settings.selectedSubcorpus];
|
let subcorpus = this.data.subcorpora[this.settings.selectedSubcorpus];
|
||||||
subcorpus.o.drop().then(
|
subcorpus.o.drop().then(
|
||||||
(cQiStatus) => {
|
(cQiStatus) => {
|
||||||
app.flash(`${subcorpus.o.name} deleted`, 'corpus');
|
app.ui.flash(`${subcorpus.o.name} deleted`, 'corpus');
|
||||||
delete this.data.subcorpora[subcorpus.o.name];
|
delete this.data.subcorpora[subcorpus.o.name];
|
||||||
this.settings.selectedSubcorpus = undefined;
|
this.settings.selectedSubcorpus = undefined;
|
||||||
for (let subcorpusName in this.data.subcorpora) {
|
for (let subcorpusName in this.data.subcorpora) {
|
||||||
@ -320,7 +320,7 @@ nopaque.corpus_analysis.ConcordanceExtension = class ConcordanceExtension {
|
|||||||
},
|
},
|
||||||
(cqiError) => {
|
(cqiError) => {
|
||||||
let errorString = `${cqiError.code}: ${cqiError.constructor.name}`;
|
let errorString = `${cqiError.code}: ${cqiError.constructor.name}`;
|
||||||
app.flash(errorString, 'error');
|
app.ui.flash(errorString, 'error');
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -46,7 +46,7 @@ nopaque.corpus_analysis.ReaderExtension = class ReaderExtension {
|
|||||||
if ('description' in error) {errorString += `: ${error.description}`;}
|
if ('description' in error) {errorString += `: ${error.description}`;}
|
||||||
this.elements.error.innerText = errorString;
|
this.elements.error.innerText = errorString;
|
||||||
this.elements.error.classList.remove('hide');
|
this.elements.error.classList.remove('hide');
|
||||||
app.flash(errorString, 'error');
|
app.ui.flash(errorString, 'error');
|
||||||
this.elements.progress.classList.add('hide');
|
this.elements.progress.classList.add('hide');
|
||||||
}
|
}
|
||||||
this.app.enableActionElements();
|
this.app.enableActionElements();
|
||||||
@ -205,7 +205,7 @@ nopaque.corpus_analysis.ReaderExtension = class ReaderExtension {
|
|||||||
`
|
`
|
||||||
);
|
);
|
||||||
this.elements.corpusPagination.appendChild(pageElement);
|
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();
|
||||||
|
@ -7,7 +7,6 @@ nopaque.corpus_analysis.StaticVisualizationExtension = class StaticVisualization
|
|||||||
stopwords: undefined,
|
stopwords: undefined,
|
||||||
originalStopwords: {},
|
originalStopwords: {},
|
||||||
stopwordCache: {},
|
stopwordCache: {},
|
||||||
promises: {getStopwords: undefined},
|
|
||||||
tokenSet: new Set()
|
tokenSet: new Set()
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -73,22 +72,11 @@ nopaque.corpus_analysis.StaticVisualizationExtension = class StaticVisualization
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getStopwords() {
|
async getStopwords() {
|
||||||
this.data.promises.getStopwords = new Promise((resolve, reject) => {
|
const stopwords = await app.corpora.getStopwords(this.app.corpusId);
|
||||||
nopaque.requests.corpora.entity.getStopwords()
|
this.data.originalStopwords = structuredClone(stopwords);
|
||||||
.then((response) => {
|
this.data.stopwords = structuredClone(stopwords);
|
||||||
response.json()
|
return stopwords;
|
||||||
.then((json) => {
|
|
||||||
this.data.originalStopwords = structuredClone(json);
|
|
||||||
this.data.stopwords = structuredClone(json);
|
|
||||||
resolve(this.data.stopwords);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return this.data.promises.getStopwords;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderGeneralCorpusInfo() {
|
renderGeneralCorpusInfo() {
|
||||||
@ -121,7 +109,7 @@ nopaque.corpus_analysis.StaticVisualizationExtension = class StaticVisualization
|
|||||||
num_unique_pos: Object.entries(corpusData.s_attrs.text.lexicon[i].freqs.pos).length,
|
num_unique_pos: Object.entries(corpusData.s_attrs.text.lexicon[i].freqs.pos).length,
|
||||||
num_unique_simple_pos: Object.entries(corpusData.s_attrs.text.lexicon[i].freqs.simple_pos).length
|
num_unique_simple_pos: Object.entries(corpusData.s_attrs.text.lexicon[i].freqs.simple_pos).length
|
||||||
};
|
};
|
||||||
|
|
||||||
textData.push(resource);
|
textData.push(resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -262,7 +250,7 @@ nopaque.corpus_analysis.StaticVisualizationExtension = class StaticVisualization
|
|||||||
}
|
}
|
||||||
return filteredData;
|
return filteredData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
renderFrequenciesGraphic(tokenSet) {
|
renderFrequenciesGraphic(tokenSet) {
|
||||||
this.data.tokenSet = tokenSet;
|
this.data.tokenSet = tokenSet;
|
||||||
@ -272,7 +260,7 @@ nopaque.corpus_analysis.StaticVisualizationExtension = class StaticVisualization
|
|||||||
let texts = Object.entries(corpusData.s_attrs.text.lexicon);
|
let texts = Object.entries(corpusData.s_attrs.text.lexicon);
|
||||||
let graphtype = document.querySelector('.frequencies-graph-mode-button.disabled').dataset.graphType;
|
let graphtype = document.querySelector('.frequencies-graph-mode-button.disabled').dataset.graphType;
|
||||||
let tokenCategory = frequenciesTokenCategoryDropdownElement.firstChild.textContent.toLowerCase();
|
let tokenCategory = frequenciesTokenCategoryDropdownElement.firstChild.textContent.toLowerCase();
|
||||||
|
|
||||||
let graphData = this.createFrequenciesGraphData(tokenCategory, texts, graphtype, tokenSet);
|
let graphData = this.createFrequenciesGraphData(tokenCategory, texts, graphtype, tokenSet);
|
||||||
let graphLayout = {
|
let graphLayout = {
|
||||||
barmode: graphtype === 'bar' ? 'stack' : '',
|
barmode: graphtype === 'bar' ? 'stack' : '',
|
||||||
@ -328,7 +316,7 @@ nopaque.corpus_analysis.StaticVisualizationExtension = class StaticVisualization
|
|||||||
for (let i = 0; i < texts.length; i++) {
|
for (let i = 0; i < texts.length; i++) {
|
||||||
tokenCountPerText[i] = (tokenCountPerText[i] || 0) + (texts[i][1].freqs[tokenCategory][originalId] || 0);
|
tokenCountPerText[i] = (tokenCountPerText[i] || 0) + (texts[i][1].freqs[tokenCategory][originalId] || 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let data = {
|
let data = {
|
||||||
x: textTitles,
|
x: textTitles,
|
||||||
y: tokenCountPerText,
|
y: tokenCountPerText,
|
||||||
@ -353,7 +341,7 @@ nopaque.corpus_analysis.StaticVisualizationExtension = class StaticVisualization
|
|||||||
stopwordLanguageChipList.innerHTML = '';
|
stopwordLanguageChipList.innerHTML = '';
|
||||||
userStopwordListContainer.innerHTML = '';
|
userStopwordListContainer.innerHTML = '';
|
||||||
stopwordInputField.value = '';
|
stopwordInputField.value = '';
|
||||||
|
|
||||||
// Render stopword language selection. Set english as default language. Filter out user_stopwords.
|
// Render stopword language selection. Set english as default language. Filter out user_stopwords.
|
||||||
if (stopwordLanguageSelection.children.length === 0) {
|
if (stopwordLanguageSelection.children.length === 0) {
|
||||||
Object.keys(stopwords).forEach(language => {
|
Object.keys(stopwords).forEach(language => {
|
||||||
@ -379,7 +367,7 @@ nopaque.corpus_analysis.StaticVisualizationExtension = class StaticVisualization
|
|||||||
// Render english stopwords as default ...
|
// Render english stopwords as default ...
|
||||||
let selectedLanguage = document.querySelector('#stopword-language-selection').value;
|
let selectedLanguage = document.querySelector('#stopword-language-selection').value;
|
||||||
this.renderStopwordLanguageChipList(selectedLanguage, stopwords[selectedLanguage]);
|
this.renderStopwordLanguageChipList(selectedLanguage, stopwords[selectedLanguage]);
|
||||||
|
|
||||||
// ... or render selected language stopwords.
|
// ... or render selected language stopwords.
|
||||||
stopwordLanguageSelection.addEventListener('change', (event) => {
|
stopwordLanguageSelection.addEventListener('change', (event) => {
|
||||||
this.renderStopwordLanguageChipList(event.target.value, stopwords[event.target.value]);
|
this.renderStopwordLanguageChipList(event.target.value, stopwords[event.target.value]);
|
||||||
@ -402,7 +390,7 @@ nopaque.corpus_analysis.StaticVisualizationExtension = class StaticVisualization
|
|||||||
|
|
||||||
// Initialize Materialize components.
|
// Initialize Materialize components.
|
||||||
M.Chips.init(
|
M.Chips.init(
|
||||||
stopwordInputField,
|
stopwordInputField,
|
||||||
{
|
{
|
||||||
placeholder: 'Add stopwords',
|
placeholder: 'Add stopwords',
|
||||||
onChipAdd: (event) => {
|
onChipAdd: (event) => {
|
||||||
@ -428,7 +416,7 @@ nopaque.corpus_analysis.StaticVisualizationExtension = class StaticVisualization
|
|||||||
deleteLanguageStopwordListEntriesButton.classList.toggle('disabled', stopwordLength === 0);
|
deleteLanguageStopwordListEntriesButton.classList.toggle('disabled', stopwordLength === 0);
|
||||||
resetLanguageStopwordListEntriesButton.classList.toggle('disabled', stopwordLength === originalStopwordListLength);
|
resetLanguageStopwordListEntriesButton.classList.toggle('disabled', stopwordLength === originalStopwordListLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderStopwordLanguageChipList(language, stopwords) {
|
renderStopwordLanguageChipList(language, stopwords) {
|
||||||
let stopwordLanguageChipList = document.querySelector('#stopword-language-chip-list');
|
let stopwordLanguageChipList = document.querySelector('#stopword-language-chip-list');
|
||||||
stopwordLanguageChipList.innerHTML = '';
|
stopwordLanguageChipList.innerHTML = '';
|
||||||
|
@ -101,7 +101,7 @@ nopaque.forms.BaseForm = class BaseForm {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (request.status === 500) {
|
if (request.status === 500) {
|
||||||
app.flash('Internal Server Error', 'error');
|
app.ui.flash('Internal Server Error', 'error');
|
||||||
}
|
}
|
||||||
modal.close();
|
modal.close();
|
||||||
});
|
});
|
||||||
|
@ -5,50 +5,6 @@ nopaque.requests.corpora = {};
|
|||||||
|
|
||||||
nopaque.requests.corpora.entity = {};
|
nopaque.requests.corpora.entity = {};
|
||||||
|
|
||||||
nopaque.requests.corpora.entity.delete = (corpusId) => {
|
|
||||||
let input = `/corpora/${corpusId}`;
|
|
||||||
let init = {
|
|
||||||
method: 'DELETE'
|
|
||||||
};
|
|
||||||
return nopaque.requests.JSONfetch(input, init);
|
|
||||||
};
|
|
||||||
|
|
||||||
nopaque.requests.corpora.entity.build = (corpusId) => {
|
|
||||||
let input = `/corpora/${corpusId}/build`;
|
|
||||||
let init = {
|
|
||||||
method: 'POST',
|
|
||||||
};
|
|
||||||
return nopaque.requests.JSONfetch(input, init);
|
|
||||||
};
|
|
||||||
|
|
||||||
nopaque.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 nopaque.requests.JSONfetch(input, init);
|
|
||||||
};
|
|
||||||
|
|
||||||
nopaque.requests.corpora.entity.getStopwords = () => {
|
|
||||||
let input = `/corpora/stopwords`;
|
|
||||||
let init = {
|
|
||||||
method: 'GET'
|
|
||||||
};
|
|
||||||
return nopaque.requests.JSONfetch(input, init);
|
|
||||||
};
|
|
||||||
|
|
||||||
nopaque.requests.corpora.entity.isPublic = {};
|
|
||||||
|
|
||||||
nopaque.requests.corpora.entity.isPublic.update = (corpusId, isPublic) => {
|
|
||||||
let input = `/corpora/${corpusId}/is_public`;
|
|
||||||
let init = {
|
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify(isPublic)
|
|
||||||
};
|
|
||||||
return nopaque.requests.JSONfetch(input, init);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/*****************************************************************************
|
/*****************************************************************************
|
||||||
* Requests for /corpora/<entity>/files routes *
|
* Requests for /corpora/<entity>/files routes *
|
||||||
|
@ -18,23 +18,23 @@ nopaque.requests.JSONfetch = (input, init={}) => {
|
|||||||
}
|
}
|
||||||
if (response.status === 204) {
|
if (response.status === 204) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
response.json()
|
response.json()
|
||||||
.then(
|
.then(
|
||||||
(json) => {
|
(json) => {
|
||||||
let message = json.message;
|
let message = json.message;
|
||||||
let category = json.category || 'message';
|
let category = json.category || 'message';
|
||||||
if (message) {
|
if (message) {
|
||||||
app.flash(message, category);
|
app.ui.flash(message, category);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
app.flash(`[${response.status}]: ${response.statusText}`, 'error');
|
app.ui.flash(`[${response.status}]: ${response.statusText}`, 'error');
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
(response) => {
|
(response) => {
|
||||||
app.flash('Something went wrong', 'error');
|
app.ui.flash('Something went wrong', 'error');
|
||||||
reject(response);
|
reject(response);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
/*****************************************************************************
|
|
||||||
* Requests for /jobs routes *
|
|
||||||
*****************************************************************************/
|
|
||||||
nopaque.requests.jobs = {};
|
|
||||||
|
|
||||||
nopaque.requests.jobs.entity = {};
|
|
||||||
|
|
||||||
nopaque.requests.jobs.entity.delete = (jobId) => {
|
|
||||||
let input = `/jobs/${jobId}`;
|
|
||||||
let init = {
|
|
||||||
method: 'DELETE'
|
|
||||||
};
|
|
||||||
return nopaque.requests.JSONfetch(input, init);
|
|
||||||
};
|
|
||||||
|
|
||||||
nopaque.requests.jobs.entity.log = (jobId) => {
|
|
||||||
let input = `/jobs/${jobId}/log`;
|
|
||||||
let init = {
|
|
||||||
method: 'GET'
|
|
||||||
};
|
|
||||||
return nopaque.requests.JSONfetch(input, init);
|
|
||||||
};
|
|
||||||
|
|
||||||
nopaque.requests.jobs.entity.restart = (jobId) => {
|
|
||||||
let input = `/jobs/${jobId}/restart`;
|
|
||||||
let init = {
|
|
||||||
method: 'POST'
|
|
||||||
};
|
|
||||||
return nopaque.requests.JSONfetch(input, init);
|
|
||||||
};
|
|
@ -7,7 +7,7 @@ nopaque.resource_displays.CorpusDisplay = class CorpusDisplay extends nopaque.re
|
|||||||
this.displayElement
|
this.displayElement
|
||||||
.querySelector('.action-button[data-action="build-request"]')
|
.querySelector('.action-button[data-action="build-request"]')
|
||||||
.addEventListener('click', (event) => {
|
.addEventListener('click', (event) => {
|
||||||
nopaque.requests.corpora.entity.build(this.corpusId);
|
app.corpora.build(this.corpusId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,22 +52,23 @@ nopaque.resource_displays.CorpusDisplay = class CorpusDisplay extends nopaque.re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setTitle(title) {
|
async setTitle(title) {
|
||||||
this.setElements(this.displayElement.querySelectorAll('.corpus-title'), title);
|
const corpusTitleElements = this.displayElement.querySelectorAll('.corpus-title');
|
||||||
|
this.setElements(corpusTitleElements, title);
|
||||||
}
|
}
|
||||||
|
|
||||||
setNumTokens(numTokens) {
|
setNumTokens(numTokens) {
|
||||||
this.setElements(
|
const corpusTokenRatioElements = this.displayElement.querySelectorAll('.corpus-token-ratio');
|
||||||
this.displayElement.querySelectorAll('.corpus-token-ratio'),
|
const maxNumTokens = 2147483647;
|
||||||
`${numTokens}/${app.data.users[this.userId].corpora[this.corpusId].max_num_tokens}`
|
|
||||||
);
|
this.setElements(corpusTokenRatioElements, `${numTokens}/${maxNumTokens}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
setDescription(description) {
|
setDescription(description) {
|
||||||
this.setElements(this.displayElement.querySelectorAll('.corpus-description'), description);
|
this.setElements(this.displayElement.querySelectorAll('.corpus-description'), description);
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatus(status) {
|
async setStatus(status) {
|
||||||
let elements = this.displayElement.querySelectorAll('.action-button[data-action="analyze"]');
|
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)) {
|
||||||
@ -77,8 +78,10 @@ nopaque.resource_displays.CorpusDisplay = class CorpusDisplay extends nopaque.re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
elements = this.displayElement.querySelectorAll('.action-button[data-action="build-request"]');
|
elements = this.displayElement.querySelectorAll('.action-button[data-action="build-request"]');
|
||||||
|
const user = await app.userHub.get(this.userId);
|
||||||
|
const corpusFiles = user.corpora[this.corpusId].files;
|
||||||
for (let element of elements) {
|
for (let element of elements) {
|
||||||
if (['UNPREPARED', 'FAILED'].includes(status) && Object.values(app.data.users[this.userId].corpora[this.corpusId].files).length > 0) {
|
if (['UNPREPARED', 'FAILED'].includes(status) && Object.values(corpusFiles.length > 0)) {
|
||||||
element.classList.remove('disabled');
|
element.classList.remove('disabled');
|
||||||
} else {
|
} else {
|
||||||
element.classList.add('disabled');
|
element.classList.add('disabled');
|
||||||
|
@ -5,19 +5,14 @@ nopaque.resource_displays.ResourceDisplay = class ResourceDisplay {
|
|||||||
this.displayElement = displayElement;
|
this.displayElement = displayElement;
|
||||||
this.userId = this.displayElement.dataset.userId;
|
this.userId = this.displayElement.dataset.userId;
|
||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
if (this.userId) {
|
if (this.userId === undefined) {return;}
|
||||||
app.subscribeUser(this.userId)
|
app.userHub.addEventListener('patch', (event) => {
|
||||||
.then((response) => {
|
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||||
app.sockets.users.on('patch_user', (patch) => {
|
});
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
app.userHub.get(this.userId).then((user) => {
|
||||||
});
|
this.init(user);
|
||||||
});
|
this.isInitialized = true;
|
||||||
app.getUser(this.userId)
|
});
|
||||||
.then((user) => {
|
|
||||||
this.init(user);
|
|
||||||
this.isInitialized = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init(user) {throw 'Not implemented';}
|
init(user) {throw 'Not implemented';}
|
||||||
|
@ -14,12 +14,11 @@ nopaque.resource_lists.CorpusFileList = class CorpusFileList extends nopaque.res
|
|||||||
this.hasPermissionView = listContainerElement.dataset?.hasPermissionView == 'true' || false;
|
this.hasPermissionView = listContainerElement.dataset?.hasPermissionView == 'true' || false;
|
||||||
this.hasPermissionManageFiles = listContainerElement.dataset?.hasPermissionManageFiles == '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.userHub.addEventListener('patch', (event) => {
|
||||||
app.sockets.users.on('patch_user', (patch) => {
|
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
app.getUser(this.userId).then((user) => {
|
app.userHub.get(this.userId).then((user) => {
|
||||||
|
// TODO: Make this better understandable
|
||||||
this.add(Object.values(user.corpora[this.corpusId].files || user.followed_corpora[this.corpusId].files));
|
this.add(Object.values(user.corpora[this.corpusId].files || user.followed_corpora[this.corpusId].files));
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
});
|
});
|
||||||
|
@ -12,15 +12,16 @@ nopaque.resource_lists.CorpusFollowerList = class CorpusFollowerList extends nop
|
|||||||
this.userId = listContainerElement.dataset.userId;
|
this.userId = listContainerElement.dataset.userId;
|
||||||
this.corpusId = listContainerElement.dataset.corpusId;
|
this.corpusId = listContainerElement.dataset.corpusId;
|
||||||
if (this.userId === undefined || this.corpusId === undefined) {return;}
|
if (this.userId === undefined || this.corpusId === undefined) {return;}
|
||||||
app.subscribeUser(this.userId).then((response) => {
|
app.userHub.addEventListener('patch', (event) => {
|
||||||
app.sockets.users.on('patch_user', (patch) => {
|
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
app.getUser(this.userId).then((user) => {
|
app.userHub.get(this.userId).then((user) => {
|
||||||
|
// TODO: Check if the following is better
|
||||||
// let corpusFollowerAssociations = Object.values(user.corpora[this.corpusId].corpus_follower_associations);
|
// let corpusFollowerAssociations = Object.values(user.corpora[this.corpusId].corpus_follower_associations);
|
||||||
// let filteredList = corpusFollowerAssociations.filter(association => association.follower.id != currentUserId);
|
// let filteredList = corpusFollowerAssociations.filter(association => association.follower.id != currentUserId);
|
||||||
// this.add(filteredList);
|
// this.add(filteredList);
|
||||||
|
|
||||||
|
// TODO: Make this better understandable
|
||||||
this.add(Object.values(user.corpora[this.corpusId].corpus_follower_associations));
|
this.add(Object.values(user.corpora[this.corpusId].corpus_follower_associations));
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
});
|
});
|
||||||
|
@ -11,12 +11,10 @@ nopaque.resource_lists.CorpusList = class CorpusList extends nopaque.resource_li
|
|||||||
this.selectedItemIds = new Set();
|
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.userHub.addEventListener('patch', (event) => {
|
||||||
app.sockets.users.on('patch_user', (patch) => {
|
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
app.getUser(this.userId).then((user) => {
|
app.userHub.get(this.userId).then((user) => {
|
||||||
this.add(this.aggregateData(user));
|
this.add(this.aggregateData(user));
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
});
|
});
|
||||||
@ -69,6 +67,7 @@ nopaque.resource_lists.CorpusList = class CorpusList extends nopaque.resource_li
|
|||||||
<span class="disable-on-click"></span>
|
<span class="disable-on-click"></span>
|
||||||
</label>
|
</label>
|
||||||
</td>
|
</td>
|
||||||
|
<td><a class="btn-floating service-color darken" data-service="corpus-analysis"><i class="material-icons">book</i></a></td>
|
||||||
<td>
|
<td>
|
||||||
<b class="title"></b><br>
|
<b class="title"></b><br>
|
||||||
<i class="description"></i>
|
<i class="description"></i>
|
||||||
@ -80,7 +79,7 @@ nopaque.resource_lists.CorpusList = class CorpusList extends nopaque.resource_li
|
|||||||
<td>${values['current-user-is-following'] ? '<span><i class="left material-icons">visibility</i>Following</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 darken 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" data-list-action="view"><i class="material-icons">send</i></a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`.trim();
|
`.trim();
|
||||||
@ -119,6 +118,7 @@ nopaque.resource_lists.CorpusList = class CorpusList extends nopaque.resource_li
|
|||||||
<span></span>
|
<span></span>
|
||||||
</label>
|
</label>
|
||||||
</th>
|
</th>
|
||||||
|
<th></th>
|
||||||
<th>Title and Description</th>
|
<th>Title and Description</th>
|
||||||
<th>Owner</th>
|
<th>Owner</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
@ -180,7 +180,7 @@ nopaque.resource_lists.CorpusList = class CorpusList extends nopaque.resource_li
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
nopaque.requests.corpora.entity.delete(itemId);
|
app.corpora.delete(itemId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
modal.open();
|
modal.open();
|
||||||
@ -276,7 +276,7 @@ nopaque.resource_lists.CorpusList = class CorpusList extends nopaque.resource_li
|
|||||||
let listItem = this.listjs.get('id', selectedItemId)[0].elm;
|
let listItem = this.listjs.get('id', selectedItemId)[0].elm;
|
||||||
let values = this.listjs.get('id', listItem.dataset.id)[0].values();
|
let values = this.listjs.get('id', listItem.dataset.id)[0].values();
|
||||||
if (values['is-owner']) {
|
if (values['is-owner']) {
|
||||||
nopaque.requests.corpora.entity.delete(selectedItemId);
|
app.corpora.delete(selectedItemId);
|
||||||
} else {
|
} else {
|
||||||
nopaque.requests.corpora.entity.followers.entity.delete(selectedItemId, currentUserId);
|
nopaque.requests.corpora.entity.followers.entity.delete(selectedItemId, currentUserId);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -8,8 +8,10 @@ nopaque.resource_lists.JobInputList = class JobInputList extends nopaque.resourc
|
|||||||
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);
|
// app.userHub.addEventListener('patch', (event) => {
|
||||||
app.getUser(this.userId).then((user) => {
|
// if (this.isInitialized) {this.onPatch(event.detail);}
|
||||||
|
// });
|
||||||
|
app.userHub.get(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;
|
||||||
});
|
});
|
||||||
|
@ -12,12 +12,10 @@ nopaque.resource_lists.JobList = class JobList extends nopaque.resource_lists.Re
|
|||||||
this.selectedItemIds = new Set();
|
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.userHub.addEventListener('patch', (event) => {
|
||||||
app.sockets.users.on('patch_user', (patch) => {
|
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
app.getUser(this.userId).then((user) => {
|
app.userHub.get(this.userId).then((user) => {
|
||||||
this.add(Object.values(user.jobs));
|
this.add(Object.values(user.jobs));
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
});
|
});
|
||||||
@ -25,19 +23,19 @@ nopaque.resource_lists.JobList = class JobList extends nopaque.resource_lists.Re
|
|||||||
|
|
||||||
get item() {
|
get item() {
|
||||||
return `
|
return `
|
||||||
<tr class="list-item clickable hoverable">
|
<tr class="list-item clickable hoverable service-color lighten">
|
||||||
<td>
|
<td>
|
||||||
<label class="list-action-trigger" data-list-action="select">
|
<label class="list-action-trigger" data-list-action="select">
|
||||||
<input class="select-checkbox" type="checkbox">
|
<input class="select-checkbox" type="checkbox">
|
||||||
<span class="disable-on-click"></span>
|
<span class="disable-on-click"></span>
|
||||||
</label>
|
</label>
|
||||||
</td>
|
</td>
|
||||||
<td><a class="btn-floating service-color darken" data-service="inherit"><i class="nopaque-icons service-icons" data-service="inherit"></i></a></td>
|
<td><a class="btn-floating service-color darken"><i class="nopaque-icons service-icons"></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 job-status-color job-status-text" data-badge-caption=""></span></td>
|
<td><span class="status badge new job-status-color job-status-text" data-badge-caption=""></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 darken waves-effect waves-light" data-list-action="view"><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"><i class="material-icons">send</i></a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`.trim();
|
`.trim();
|
||||||
@ -138,8 +136,9 @@ nopaque.resource_lists.JobList = class JobList extends nopaque.resource_lists.Re
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]');
|
let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]');
|
||||||
confirmElement.addEventListener('click', (event) => {
|
confirmElement.addEventListener('click', async (event) => {
|
||||||
nopaque.requests.jobs.entity.delete(itemId);
|
const message = await app.jobs.delete(itemId);
|
||||||
|
app.ui.flash(message, 'job');
|
||||||
});
|
});
|
||||||
modal.open();
|
modal.open();
|
||||||
break;
|
break;
|
||||||
@ -223,8 +222,9 @@ nopaque.resource_lists.JobList = class JobList extends nopaque.resource_lists.Re
|
|||||||
);
|
);
|
||||||
let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]');
|
let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]');
|
||||||
confirmElement.addEventListener('click', (event) => {
|
confirmElement.addEventListener('click', (event) => {
|
||||||
this.selectedItemIds.forEach(selectedItemId => {
|
this.selectedItemIds.forEach(async (selectedItemId) => {
|
||||||
nopaque.requests.jobs.entity.delete(selectedItemId);
|
const message = await app.jobs.delete(selectedItemId);
|
||||||
|
app.ui.flash(message, 'job');
|
||||||
});
|
});
|
||||||
this.selectedItemIds.clear();
|
this.selectedItemIds.clear();
|
||||||
this.renderingItemSelection();
|
this.renderingItemSelection();
|
||||||
|
@ -8,12 +8,10 @@ nopaque.resource_lists.JobResultList = class JobResultList extends nopaque.resou
|
|||||||
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.userHub.addEventListener('patch', (event) => {
|
||||||
app.sockets.users.on('patch_user', (patch) => {
|
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
app.getUser(this.userId).then((user) => {
|
app.userHub.get(this.userId).then((user) => {
|
||||||
this.add(Object.values(user.jobs[this.jobId].results));
|
this.add(Object.values(user.jobs[this.jobId].results));
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
});
|
});
|
||||||
|
@ -8,12 +8,10 @@ nopaque.resource_lists.SpaCyNLPPipelineModelList = class SpaCyNLPPipelineModelLi
|
|||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
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.userHub.addEventListener('patch', (event) => {
|
||||||
app.sockets.users.on('patch_user', (patch) => {
|
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
app.getUser(this.userId).then((user) => {
|
app.userHub.get(this.userId).then((user) => {
|
||||||
this.add(Object.values(user.spacy_nlp_pipeline_models));
|
this.add(Object.values(user.spacy_nlp_pipeline_models));
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
});
|
});
|
||||||
|
@ -8,21 +8,11 @@ nopaque.resource_lists.TesseractOCRPipelineModelList = class TesseractOCRPipelin
|
|||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
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.userHub.addEventListener('patch', (event) => {
|
||||||
app.sockets.users.on('patch_user', (patch) => {
|
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
app.getUser(this.userId).then((user) => {
|
app.userHub.get(this.userId).then((user) => {
|
||||||
this.add(Object.values(user.tesseract_ocr_pipeline_models));
|
this.add(Object.values(user.tesseract_ocr_pipeline_models));
|
||||||
for (let uncheckedCheckbox of this.listjs.list.querySelectorAll('input[data-checked="True"]')) {
|
|
||||||
uncheckedCheckbox.setAttribute('checked', '');
|
|
||||||
}
|
|
||||||
if (user.role.name !== ('Administrator' || 'Contributor')) {
|
|
||||||
for (let switchElement of this.listjs.list.querySelectorAll('.is_public')) {
|
|
||||||
switchElement.setAttribute('disabled', '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
<div id="terms-of-use-modal" class="modal modal-fixed-footer">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col s12">
|
|
||||||
<h1 id="title">Terms of use</h1>
|
|
||||||
</div>
|
|
||||||
<div class="col s12">
|
|
||||||
<div class="switch">
|
|
||||||
<label>
|
|
||||||
DE
|
|
||||||
<input type="checkbox" id="terms-of-use-modal-switch">
|
|
||||||
<span class="lever"></span>
|
|
||||||
EN
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
</div>
|
|
||||||
<div class="terms-of-use-modal-content hide">
|
|
||||||
{% include "main/terms_of_use_en.html.j2" %}
|
|
||||||
</div>
|
|
||||||
<div class="terms-of-use-modal-content">
|
|
||||||
{% include "main/terms_of_use_de.html.j2" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<span style="margin-right:20px;">I have taken note of the new GTC and agree to their validity in the context of my further use.</span>
|
|
||||||
<a href="#!" class="modal-close waves-effect waves-green btn">Yes</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,6 +1,79 @@
|
|||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
<ul class="dropdown-content" id="nav-account-dropdown-content">
|
<ul class="dropdown-content" id="navbar-data-processing-and-analysis-dropdown-content">
|
||||||
<li><a href="{{ url_for('settings.settings') }}"><i class="material-icons left">settings</i>Settings</a></li>
|
<li {% if request.path == url_for('services.file_setup_pipeline') %}class="active"{% endif %}>
|
||||||
<li><a href="{{ url_for('auth.logout') }}"><i class="material-icons left">logout</i>Log out</a></li>
|
<a href="{{ url_for('services.file_setup_pipeline') }}">
|
||||||
|
<i class="nopaque-icons service-icons service-color-text text-darken" data-service="file-setup-pipeline"></i>
|
||||||
|
File Setup Pipeline
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li {% if request.path == url_for('services.tesseract_ocr_pipeline') %}class="active"{% endif %}>
|
||||||
|
<a href="{{ url_for('services.tesseract_ocr_pipeline') }}">
|
||||||
|
<i class="nopaque-icons service-icons service-color-text text-darken" data-service="tesseract-ocr-pipeline"></i>
|
||||||
|
Tesseract OCR Pipeline
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% if config.NOPAQUE_TRANSKRIBUS_ENABLED %}
|
||||||
|
<li {% if request.path == url_for('services.transkribus_htr_pipeline') %}class="active"{% endif %}>
|
||||||
|
<a href="{{ url_for('services.transkribus_htr_pipeline') }}">
|
||||||
|
<i class="nopaque-icons service-icons service-color-text text-darken" data-service="transkribus-htr-pipeline"></i>
|
||||||
|
Transkribus HTR Pipeline
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
<li {% if request.path == url_for('services.spacy_nlp_pipeline') %}class="active"{% endif %}>
|
||||||
|
<a href="{{ url_for('services.spacy_nlp_pipeline') }}">
|
||||||
|
<i class="nopaque-icons service-icons service-color-text text-darken" data-service="spacy-nlp-pipeline"></i>
|
||||||
|
SpaCy NLP Pipeline
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="divider" tabindex="-1"></li>
|
||||||
|
<li {% if request.path == url_for('services.corpus_analysis') %}class="active"{% endif %}>
|
||||||
|
<a href="{{ url_for('services.corpus_analysis') }}">
|
||||||
|
<i class="nopaque-icons service-icons service-color-text text-darken" data-service="corpus-analysis"></i>
|
||||||
|
Corpus Analyis
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<ul class="dropdown-content" id="navbar-account-dropdown-content">
|
||||||
|
<li {% if request.path == url_for('users.user', user_id=current_user.id) %}class="active"{% endif %}>
|
||||||
|
<a href="{{ url_for('users.user', user_id=current_user.id) }}">
|
||||||
|
<i class="material-icons">person</i>
|
||||||
|
Your profile
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li {% if request.path == url_for('settings.index') %}class="active"{% endif %}>
|
||||||
|
<a href="{{ url_for('settings.index') }}">
|
||||||
|
<i class="material-icons">settings</i>
|
||||||
|
Settings
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('auth.logout') }}">
|
||||||
|
<i class="material-icons">logout</i>
|
||||||
|
Log out
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% if current_user.can('USE_API') or current_user.is_administrator %}
|
||||||
|
<li class="divider" tabindex="-1"></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if current_user.can('USE_API') %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('apifairy.docs') }}">
|
||||||
|
<i class="material-icons left">api</i>
|
||||||
|
API
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if current_user.is_administrator %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('admin.index') }}">
|
||||||
|
<i class="material-icons left">admin_panel_settings</i>
|
||||||
|
Administration
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,40 +1,45 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col s6 m3">
|
<div class="col s12 l3">
|
||||||
<a href="https://www.dfg.de/">
|
|
||||||
<img class="responsive-img" src="{{ url_for('static', filename='images/logo_-_dfg.gif') }}">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="col s6 m3 offset-m1 center-align">
|
|
||||||
<a href="https://www.uni-bielefeld.de/sfb1288/">
|
|
||||||
<img class="responsive-img" src="{{ url_for('static', filename='images/logo_-_sfb_1288.png') }}">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="col s12 m3 offset-m1">
|
|
||||||
<h5 class="white-text">Legal Notice</h5>
|
<h5 class="white-text">Legal Notice</h5>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a class="grey-text text-lighten-3" href="https://www.uni-bielefeld.de/(en)/impressum/">Legal Notice</a></li>
|
<li><a class="grey-text text-lighten-3" href="https://www.uni-bielefeld.de/(en)/impressum/">Legal Notice</a></li>
|
||||||
<li><a class="grey-text text-lighten-3" href="{{ url_for('main.privacy_policy') }}">Privacy statement (GDPR)</a></li>
|
<li><a class="grey-text text-lighten-3" href="{{ url_for('main.privacy_policy') }}">Privacy statement (GDPR)</a></li>
|
||||||
<li><a class="grey-text text-lighten-3" href="{{ url_for('main.terms_of_use') }}">Terms of use</a></li>
|
<li><a class="grey-text text-lighten-3" href="{{ url_for('main.terms_of_use') }}">Terms of use</a></li>
|
||||||
<li></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
<div class="col s12 l3">
|
||||||
<div class="footer-copyright">
|
<h5 class="white-text">More Resources</h5>
|
||||||
<div class="container">
|
<ul>
|
||||||
<div class="row" style="margin-bottom: 0;">
|
<li><a class="grey-text text-lighten-3" href="{{ url_for('main.faq') }}">Frequently asked questions</a></li>
|
||||||
<div class="col s12 m3">
|
<li><a class="grey-text text-lighten-3" href="mailto:{{ config.NOPAQUE_SERVICE_DESK }}">Report an issue</a></li>
|
||||||
<span>© 2020 Bielefeld University</span>
|
<li><a class="grey-text text-lighten-3" href="https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque">GitLab (source code)</a></li>
|
||||||
</div>
|
</ul>
|
||||||
<div class="col s12 m2">
|
</div>
|
||||||
<span class="right"><b>Version {{ config.NOPAQUE_VERSION }}</b></span>
|
|
||||||
</div>
|
<div class="col s12 l4">
|
||||||
<div class="col s12 m7 right-align">
|
<h5 class="white-text">Who made this?</h5>
|
||||||
<a class="btn-small waves-effect waves-light" href="{{ url_for('main.faq') }}"><i class="left material-icons">info_outline</i>Frequently Asked Questions</a>
|
<p class="grey-text text-lighten-4">
|
||||||
<a class="btn-small waves-effect waves-light" href="mailto:{{ config.NOPAQUE_SERVICE_DESK }}"><i class="left material-icons">mail</i>Report an issue</a>
|
This software is developed by the SFB 1288 INF project at Bielefeld University.
|
||||||
<a class="btn-small waves-effect waves-light" href="https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque" target="_blank"><i class="left material-icons">code</i>GitLab</a>
|
Thanks to all the people who made nopaque possible.
|
||||||
</div>
|
<span class="red-text">♥</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col s12 l2">
|
||||||
|
<br class="hide-on-med-and-down">
|
||||||
|
<br class="hide-on-med-and-down">
|
||||||
|
<a href="https://www.dfg.de/">
|
||||||
|
<img class="responsive-img" src="{{ url_for('static', filename='images/logo_-_dfg.gif') }}">
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-copyright">
|
||||||
|
<div class="container">
|
||||||
|
© 2024 Bielefeld University
|
||||||
|
<a class="grey-text text-lighten-4 right" href="https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque/-/releases/{{ config.NOPAQUE_VERSION }}">Version {{ config.NOPAQUE_VERSION }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user