mirror of
https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
synced 2025-07-03 03:10:33 +00:00
Compare commits
41 Commits
460257294d
...
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 |
16
.vscode/settings.json
vendored
16
.vscode/settings.json
vendored
@ -1,10 +1,24 @@
|
||||
{
|
||||
"editor.rulers": [79],
|
||||
"editor.tabSize": 4,
|
||||
"emmet.includeLanguages": {
|
||||
"jinja-html": "html"
|
||||
},
|
||||
"files.associations": {
|
||||
".flaskenv": "env",
|
||||
"*.env.tpl": "env",
|
||||
"*.txt.j2": "jinja"
|
||||
},
|
||||
"files.insertFinalNewline": true,
|
||||
"files.trimFinalNewlines": true,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"[html]": {
|
||||
"editor.tabSize": 2
|
||||
},
|
||||
"[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 flask import Flask
|
||||
from flask.logging import default_handler
|
||||
from flask_admin import Admin
|
||||
from flask_apscheduler import APScheduler
|
||||
from flask_assets import Environment
|
||||
from flask_login import LoginManager
|
||||
@ -15,10 +16,12 @@ from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_hashids import Hashids
|
||||
from logging import Formatter, StreamHandler
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
from .extensions.nopaque_flask_admin_views import AdminIndexView, ModelView
|
||||
|
||||
|
||||
docker_client = DockerClient.from_env()
|
||||
|
||||
admin = Admin()
|
||||
apifairy = APIFairy()
|
||||
assets = Environment()
|
||||
db = SQLAlchemy()
|
||||
@ -74,6 +77,7 @@ def create_app(config: Config = Config) -> Flask:
|
||||
|
||||
from .models import AnonymousUser, User
|
||||
|
||||
admin.init_app(app, index_view=AdminIndexView())
|
||||
apifairy.init_app(app)
|
||||
assets.init_app(app)
|
||||
db.init_app(app)
|
||||
@ -92,9 +96,6 @@ def create_app(config: Config = Config) -> Flask:
|
||||
# endregion Extensions
|
||||
|
||||
# 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
|
||||
app.register_blueprint(api_blueprint, url_prefix='/api')
|
||||
|
||||
@ -127,14 +128,15 @@ def create_app(config: Config = Config) -> Flask:
|
||||
|
||||
from .blueprints.workshops import bp as workshops_blueprint
|
||||
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
|
||||
|
||||
# region SocketIO Namespaces
|
||||
from .namespaces.cqi_over_sio import CQiOverSocketIONamespace
|
||||
socketio.on_namespace(CQiOverSocketIONamespace('/cqi_over_sio'))
|
||||
|
||||
from .namespaces.users import UsersNamespace
|
||||
socketio.on_namespace(UsersNamespace('/users'))
|
||||
# endregion SocketIO Namespaces
|
||||
|
||||
# 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
|
||||
)
|
@ -19,11 +19,9 @@ def before_request():
|
||||
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'))
|
||||
|
||||
if not current_user.terms_of_use_accepted:
|
||||
return redirect(url_for('main.terms_of_use'))
|
||||
|
||||
|
||||
from . import routes
|
||||
|
@ -16,4 +16,4 @@ def before_request():
|
||||
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 string import punctuation
|
||||
from threading import Thread
|
||||
import nltk
|
||||
from app import db
|
||||
from app.models import (
|
||||
Corpus,
|
||||
@ -12,6 +26,21 @@ from .decorators import corpus_follower_permission_required
|
||||
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('')
|
||||
def corpora():
|
||||
return redirect(url_for('main.dashboard', _anchor='corpora'))
|
||||
@ -20,6 +49,7 @@ def corpora():
|
||||
@bp.route('/create', methods=['GET', 'POST'])
|
||||
def create_corpus():
|
||||
form = CreateCorpusForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
corpus = Corpus.create(
|
||||
@ -30,8 +60,10 @@ def create_corpus():
|
||||
except OSError:
|
||||
abort(500)
|
||||
db.session.commit()
|
||||
|
||||
flash(f'Corpus "{corpus.title}" created', 'corpus')
|
||||
return redirect(corpus.url)
|
||||
|
||||
return render_template(
|
||||
'corpora/create.html.j2',
|
||||
title='Create corpus',
|
||||
@ -40,12 +72,14 @@ def create_corpus():
|
||||
|
||||
|
||||
@bp.route('/<hashid:corpus_id>')
|
||||
def corpus(corpus_id):
|
||||
def corpus(corpus_id: int):
|
||||
corpus = Corpus.query.get_or_404(corpus_id)
|
||||
cfrs = CorpusFollowerRole.query.all()
|
||||
# TODO: Better solution for filtering admin
|
||||
users = User.query.filter(User.is_public == True, User.id != current_user.id, User.id != corpus.user.id, User.role_id < 4).all()
|
||||
cfa = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=current_user.id).first()
|
||||
|
||||
cfa = CorpusFollowerAssociation.query.filter_by(
|
||||
corpus_id=corpus_id,
|
||||
follower_id=current_user.id
|
||||
).first()
|
||||
|
||||
if cfa is None:
|
||||
if corpus.user == current_user or current_user.is_administrator:
|
||||
cfr = CorpusFollowerRole.query.filter_by(name='Administrator').first()
|
||||
@ -53,7 +87,21 @@ def corpus(corpus_id):
|
||||
cfr = CorpusFollowerRole.query.filter_by(name='Anonymous').first()
|
||||
else:
|
||||
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(
|
||||
'corpora/corpus.html.j2',
|
||||
title=corpus.title,
|
||||
@ -62,8 +110,15 @@ def corpus(corpus_id):
|
||||
cfrs=cfrs,
|
||||
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(
|
||||
'corpora/public_corpus.html.j2',
|
||||
title=corpus.title,
|
||||
@ -73,14 +128,110 @@ def corpus(corpus_id):
|
||||
cfas=cfas,
|
||||
users=users
|
||||
)
|
||||
|
||||
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')
|
||||
@corpus_follower_permission_required('VIEW')
|
||||
def analysis(corpus_id):
|
||||
def analysis(corpus_id: int):
|
||||
corpus = Corpus.query.get_or_404(corpus_id)
|
||||
|
||||
return render_template(
|
||||
'corpora/analysis.html.j2',
|
||||
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>')
|
||||
def follow_corpus(corpus_id, token):
|
||||
def follow_corpus(corpus_id: int, token: str):
|
||||
corpus = Corpus.query.get_or_404(corpus_id)
|
||||
if current_user.follow_corpus_by_token(token):
|
||||
db.session.commit()
|
||||
flash(f'You are following "{corpus.title}" now', category='corpus')
|
||||
return redirect(url_for('corpora.corpus', corpus_id=corpus_id))
|
||||
abort(403)
|
||||
|
||||
if not current_user.follow_corpus_by_token(token):
|
||||
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'])
|
||||
def import_corpus():
|
||||
abort(503)
|
||||
@bp.route('/<hashid:corpus_id>/is-public', methods=['PUT'])
|
||||
def update_is_public(corpus_id):
|
||||
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_follower_permission_required('VIEW')
|
||||
def export_corpus(corpus_id):
|
||||
abort(503)
|
||||
corpus.is_public = new_value
|
||||
db.session.commit()
|
||||
|
||||
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)
|
||||
def handle_http_exception(error):
|
||||
def handle_http_exception(e: HTTPException):
|
||||
''' Generic HTTP exception handler '''
|
||||
accept_json = request.accept_mimetypes.accept_json
|
||||
accept_html = request.accept_mimetypes.accept_html
|
||||
|
||||
if accept_json and not accept_html:
|
||||
response = jsonify(str(error))
|
||||
return response, error.code
|
||||
return render_template('errors/error.html.j2', error=error), error.code
|
||||
error = {
|
||||
'code': e.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_login import login_required
|
||||
|
||||
|
||||
bp = Blueprint('jobs', __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 .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 (
|
||||
abort,
|
||||
current_app,
|
||||
Flask,
|
||||
jsonify,
|
||||
redirect,
|
||||
render_template,
|
||||
send_from_directory,
|
||||
url_for
|
||||
)
|
||||
from flask_login import current_user
|
||||
from app.models import Job, JobInput, JobResult
|
||||
from flask_login import current_user, login_required
|
||||
from threading import Thread
|
||||
from app import db
|
||||
from app.decorators import admin_required
|
||||
from app.models import Job, JobStatus
|
||||
from . import bp
|
||||
|
||||
|
||||
@bp.route('')
|
||||
def jobs():
|
||||
@login_required
|
||||
def index():
|
||||
return redirect(url_for('main.dashboard', _anchor='jobs'))
|
||||
|
||||
|
||||
@bp.route('/<hashid:job_id>')
|
||||
def job(job_id):
|
||||
@login_required
|
||||
def job(job_id: int):
|
||||
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)
|
||||
|
||||
return render_template(
|
||||
'jobs/job.html.j2',
|
||||
title='Job',
|
||||
@ -27,29 +39,73 @@ def job(job_id):
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/<hashid:job_id>/inputs/<hashid:job_input_id>/download')
|
||||
def download_job_input(job_id, job_input_id):
|
||||
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
|
||||
)
|
||||
def _delete_job(app: Flask, job_id: int):
|
||||
with app.app_context():
|
||||
job = Job.query.get(job_id)
|
||||
job.delete()
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@bp.route('/<hashid:job_id>/results/<hashid:job_result_id>/download')
|
||||
def download_job_result(job_id, job_result_id):
|
||||
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):
|
||||
@bp.route('/<hashid:job_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
def delete_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)
|
||||
return send_from_directory(
|
||||
job_result.path.parent,
|
||||
job_result.path.name,
|
||||
as_attachment=True,
|
||||
download_name=job_result.filename,
|
||||
mimetype=job_result.mimetype
|
||||
|
||||
thread = Thread(
|
||||
target=_delete_job,
|
||||
args=(current_app._get_current_object(), job.id)
|
||||
)
|
||||
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 app.blueprints.auth.forms import LoginForm
|
||||
from app.models import Corpus, User
|
||||
from . import bp
|
||||
from app import db
|
||||
|
||||
|
||||
@bp.route('/', methods=['GET', 'POST'])
|
||||
@ -56,7 +57,7 @@ def news():
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/privacy_policy')
|
||||
@bp.route('/privacy-policy')
|
||||
def privacy_policy():
|
||||
return render_template(
|
||||
'main/privacy_policy.html.j2',
|
||||
@ -64,14 +65,24 @@ def privacy_policy():
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/terms_of_use')
|
||||
@bp.route('/terms-of-use')
|
||||
def terms_of_use():
|
||||
return render_template(
|
||||
'main/terms_of_use.html.j2',
|
||||
title='Terms of Use'
|
||||
title='Terms of use'
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/accept-terms-of-use', methods=['POST'])
|
||||
@login_required
|
||||
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():
|
||||
|
@ -1,18 +1,7 @@
|
||||
from flask import Blueprint
|
||||
from flask_login import login_required
|
||||
|
||||
|
||||
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
|
||||
|
@ -38,8 +38,8 @@ class UpdateAccountInformationForm(FlaskForm):
|
||||
]
|
||||
)
|
||||
submit = SubmitField()
|
||||
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
|
||||
def __init__(self, user: User, *args, **kwargs):
|
||||
if 'data' not in kwargs:
|
||||
kwargs['data'] = user.to_json_serializeable()
|
||||
if 'prefix' not in kwargs:
|
||||
@ -64,7 +64,7 @@ class UpdateProfileInformationForm(FlaskForm):
|
||||
validators=[Length(max=128)]
|
||||
)
|
||||
about_me = TextAreaField(
|
||||
'About me',
|
||||
'About me',
|
||||
validators=[
|
||||
Length(max=254)
|
||||
]
|
||||
@ -89,7 +89,7 @@ class UpdateProfileInformationForm(FlaskForm):
|
||||
)
|
||||
submit = SubmitField()
|
||||
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
def __init__(self, user: User, *args, **kwargs):
|
||||
if 'data' not in kwargs:
|
||||
kwargs['data'] = user.to_json_serializeable()
|
||||
if 'prefix' not in kwargs:
|
||||
@ -130,7 +130,7 @@ class UpdatePasswordForm(FlaskForm):
|
||||
)
|
||||
submit = SubmitField()
|
||||
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
def __init__(self, user: User, *args, **kwargs):
|
||||
if 'prefix' not in kwargs:
|
||||
kwargs['prefix'] = 'update-password-form'
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -152,7 +152,7 @@ class UpdateNotificationsForm(FlaskForm):
|
||||
)
|
||||
submit = SubmitField()
|
||||
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
def __init__(self, user: User, *args, **kwargs):
|
||||
if 'data' not in kwargs:
|
||||
kwargs['data'] = user.to_json_serializeable()
|
||||
if 'prefix' not in kwargs:
|
@ -1,10 +1,158 @@
|
||||
from flask import g, url_for
|
||||
from flask_login import current_user
|
||||
from app.blueprints.users.settings.routes import settings as settings_route
|
||||
from flask import (
|
||||
abort,
|
||||
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 .forms import (
|
||||
UpdateAvatarForm,
|
||||
UpdatePasswordForm,
|
||||
UpdateNotificationsForm,
|
||||
UpdateAccountInformationForm,
|
||||
UpdateProfileInformationForm
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/settings', methods=['GET', 'POST'])
|
||||
def settings():
|
||||
g._nopaque_redirect_location_on_post = url_for('.settings')
|
||||
return settings_route(current_user.id)
|
||||
@bp.route('', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def index():
|
||||
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 +1,7 @@
|
||||
from flask import Blueprint
|
||||
from flask_login import login_required
|
||||
|
||||
|
||||
bp = Blueprint('users', __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 cli, json_routes, routes, settings
|
||||
from . import cli, events, routes
|
||||
|
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 (
|
||||
abort,
|
||||
redirect,
|
||||
render_template,
|
||||
current_app,
|
||||
Flask,
|
||||
jsonify,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
send_from_directory,
|
||||
url_for
|
||||
)
|
||||
from flask_login import current_user
|
||||
from app.models import User
|
||||
from flask_login import current_user, login_required, logout_user
|
||||
from threading import Thread
|
||||
from app import db
|
||||
from app.models import Avatar, User
|
||||
from . import bp
|
||||
|
||||
|
||||
@bp.route('')
|
||||
def users():
|
||||
@login_required
|
||||
def index():
|
||||
return redirect(url_for('main.social_area', _anchor='users'))
|
||||
|
||||
|
||||
@bp.route('/<hashid:user_id>')
|
||||
def user(user_id):
|
||||
@login_required
|
||||
def user(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):
|
||||
|
||||
if not (
|
||||
user.is_public
|
||||
or user == current_user
|
||||
or current_user.is_administrator
|
||||
):
|
||||
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(
|
||||
'users/user.html.j2',
|
||||
title=user.username,
|
||||
@ -27,13 +50,51 @@ def user(user_id):
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/<hashid:user_id>/avatar')
|
||||
def user_avatar(user_id):
|
||||
def _delete_user(app: Flask, user_id: int):
|
||||
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)
|
||||
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)
|
||||
|
||||
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:
|
||||
return redirect(url_for('static', filename='images/user_avatar.png'))
|
||||
|
||||
return send_from_directory(
|
||||
user.avatar.path.parent,
|
||||
user.avatar.path.name,
|
||||
@ -41,3 +102,33 @@ def user_avatar(user_id):
|
||||
download_name=user.avatar.filename,
|
||||
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):
|
||||
if current_user.is_authenticated:
|
||||
return f(*args, **kwargs)
|
||||
return {'code': 401, 'body': 'Unauthorized'}
|
||||
return {'status': 401, 'statusText': 'Unauthorized'}
|
||||
return wrapper
|
||||
|
||||
|
||||
@ -35,7 +35,7 @@ def socketio_permission_required(permission):
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
if not current_user.can(permission):
|
||||
return {'code': 403, 'body': 'Forbidden'}
|
||||
return {'status': 403, 'statusText': 'Forbidden'}
|
||||
return f(*args, **kwargs)
|
||||
return wrapper
|
||||
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 = ['node.role==worker']
|
||||
''' ## 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 = {
|
||||
'nopaque.server_name': current_app.config['SERVER_NAME']
|
||||
@ -141,7 +141,7 @@ def _create_cqpserver_container(corpus: Corpus):
|
||||
''' ## Entrypoint ## '''
|
||||
entrypoint = ['bash', '-c']
|
||||
''' ## 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 = f'nopaque-cqpserver-{corpus.id}'
|
||||
''' ## Network ## '''
|
||||
|
@ -1,14 +1,45 @@
|
||||
from .anonymous_user import *
|
||||
from .avatar import *
|
||||
from .corpus_file import *
|
||||
from .corpus_follower_association import *
|
||||
from .corpus_follower_role import *
|
||||
from .corpus import *
|
||||
from .job_input import *
|
||||
from .job_result import *
|
||||
from .job import *
|
||||
from .role import *
|
||||
from .spacy_nlp_pipeline_model import *
|
||||
from .tesseract_ocr_pipeline_model import *
|
||||
from .token import *
|
||||
from .user import *
|
||||
from .anonymous_user import AnonymousUser
|
||||
from .avatar import Avatar
|
||||
from .corpus_file import CorpusFile
|
||||
from .corpus_follower_association import CorpusFollowerAssociation
|
||||
from .corpus_follower_role import CorpusFollowerPermission, CorpusFollowerRole
|
||||
from .corpus import CorpusStatus, Corpus
|
||||
from .job_input import JobInput
|
||||
from .job_result import JobResult
|
||||
from .job import JobStatus, Job
|
||||
from .role import Permission, Role
|
||||
from .spacy_nlp_pipeline_model import SpaCyNLPPipelineModel
|
||||
from .tesseract_ocr_pipeline_model import TesseractOCRPipelineModel
|
||||
from .token import Token
|
||||
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
|
||||
from app import db
|
||||
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
|
||||
|
||||
|
||||
|
@ -43,7 +43,7 @@ def resource_after_delete(mapper, connection, resource):
|
||||
}
|
||||
]
|
||||
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):
|
||||
@ -55,7 +55,7 @@ def cfa_after_delete(mapper, connection, cfa):
|
||||
}
|
||||
]
|
||||
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):
|
||||
@ -70,7 +70,7 @@ def resource_after_insert(mapper, connection, resource):
|
||||
}
|
||||
]
|
||||
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):
|
||||
@ -84,7 +84,7 @@ def cfa_after_insert(mapper, connection, cfa):
|
||||
}
|
||||
]
|
||||
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):
|
||||
@ -110,7 +110,7 @@ def resource_after_update(mapper, connection, resource):
|
||||
)
|
||||
if jsonpatch:
|
||||
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):
|
||||
|
@ -6,7 +6,7 @@ from time import sleep
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
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):
|
||||
|
@ -20,14 +20,6 @@ class JobInput(FileMixin, HashidMixin, db.Model):
|
||||
def __repr__(self):
|
||||
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
|
||||
def jsonpatch_path(self):
|
||||
return f'{self.job.jsonpatch_path}/inputs/{self.hashid}'
|
||||
@ -40,7 +32,7 @@ class JobInput(FileMixin, HashidMixin, db.Model):
|
||||
def url(self):
|
||||
return url_for(
|
||||
'jobs.job',
|
||||
job_id=self.job_id,
|
||||
job_input_id=self.id,
|
||||
_anchor=f'job-{self.job.hashid}-input-{self.hashid}'
|
||||
)
|
||||
|
||||
|
@ -22,14 +22,6 @@ class JobResult(FileMixin, HashidMixin, db.Model):
|
||||
def __repr__(self):
|
||||
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
|
||||
def jsonpatch_path(self):
|
||||
return f'{self.job.jsonpatch_path}/results/{self.hashid}'
|
||||
@ -41,8 +33,8 @@ class JobResult(FileMixin, HashidMixin, db.Model):
|
||||
@property
|
||||
def url(self):
|
||||
return url_for(
|
||||
'jobs.job',
|
||||
job_id=self.job_id,
|
||||
'job_results.job_result',
|
||||
job_result_id=self.id,
|
||||
_anchor=f'job-{self.job.hashid}-result-{self.hashid}'
|
||||
)
|
||||
|
||||
|
@ -5,7 +5,7 @@ from pathlib import Path
|
||||
import requests
|
||||
import yaml
|
||||
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 .user import User
|
||||
|
||||
|
@ -5,7 +5,7 @@ from pathlib import Path
|
||||
import requests
|
||||
import yaml
|
||||
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 .user import User
|
||||
|
||||
|
@ -11,7 +11,7 @@ import re
|
||||
import secrets
|
||||
import shutil
|
||||
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_follower_association import CorpusFollowerAssociation
|
||||
from .corpus_follower_role import CorpusFollowerRole
|
||||
|
@ -9,8 +9,8 @@ from threading import Lock
|
||||
from app import db, docker_client, hashids, socketio
|
||||
from app.decorators import socketio_login_required
|
||||
from app.models import Corpus, CorpusStatus
|
||||
from . import cqi_extensions
|
||||
from .utils import CQiOverSocketIOSessionManager
|
||||
from . import cqi_extension_functions
|
||||
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):
|
||||
@socketio_login_required
|
||||
def on_connect(self):
|
||||
@ -135,30 +145,31 @@ class CQiOverSocketIONamespace(Namespace):
|
||||
cqi_client = CQiClient(cqpserver_ip_address)
|
||||
cqi_client_lock = Lock()
|
||||
|
||||
CQiOverSocketIOSessionManager.setup()
|
||||
CQiOverSocketIOSessionManager.set_corpus_id(corpus_id)
|
||||
CQiOverSocketIOSessionManager.set_cqi_client(cqi_client)
|
||||
CQiOverSocketIOSessionManager.set_cqi_client_lock(cqi_client_lock)
|
||||
SessionManager.setup()
|
||||
SessionManager.set_corpus_id(corpus_id)
|
||||
SessionManager.set_cqi_client(cqi_client)
|
||||
SessionManager.set_cqi_client_lock(cqi_client_lock)
|
||||
|
||||
return {'code': 200, 'msg': 'OK'}
|
||||
|
||||
@socketio_login_required
|
||||
def on_exec(self, fn_name: str, fn_args: dict = {}) -> dict:
|
||||
try:
|
||||
cqi_client = CQiOverSocketIOSessionManager.get_cqi_client()
|
||||
cqi_client_lock = CQiOverSocketIOSessionManager.get_cqi_client_lock()
|
||||
cqi_client = SessionManager.get_cqi_client()
|
||||
cqi_client_lock = SessionManager.get_cqi_client_lock()
|
||||
except KeyError:
|
||||
return {'code': 424, 'msg': 'Failed Dependency'}
|
||||
|
||||
if fn_name in CQI_API_FUNCTION_NAMES:
|
||||
fn = getattr(cqi_client.api, fn_name)
|
||||
elif fn_name in cqi_extensions.CQI_EXTENSION_FUNCTION_NAMES:
|
||||
fn = getattr(cqi_extensions, fn_name)
|
||||
elif fn_name in CQI_EXTENSION_FUNCTION_NAMES:
|
||||
fn = getattr(cqi_extension_functions, fn_name)
|
||||
else:
|
||||
return {'code': 400, 'msg': 'Bad Request'}
|
||||
|
||||
for param in signature(fn).parameters.values():
|
||||
# Check if the parameter is optional or required
|
||||
# The following is true for required parameters
|
||||
if param.default is param.empty:
|
||||
if param.name not in fn_args:
|
||||
return {'code': 400, 'msg': 'Bad Request'}
|
||||
@ -198,10 +209,10 @@ class CQiOverSocketIONamespace(Namespace):
|
||||
|
||||
def on_disconnect(self):
|
||||
try:
|
||||
corpus_id = CQiOverSocketIOSessionManager.get_corpus_id()
|
||||
cqi_client = CQiOverSocketIOSessionManager.get_cqi_client()
|
||||
cqi_client_lock = CQiOverSocketIOSessionManager.get_cqi_client_lock()
|
||||
CQiOverSocketIOSessionManager.teardown()
|
||||
corpus_id = SessionManager.get_corpus_id()
|
||||
cqi_client = SessionManager.get_cqi_client()
|
||||
cqi_client_lock = SessionManager.get_cqi_client_lock()
|
||||
SessionManager.teardown()
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
|
@ -8,22 +8,12 @@ import json
|
||||
import math
|
||||
from app import db
|
||||
from app.models import Corpus
|
||||
from .utils import CQiOverSocketIOSessionManager
|
||||
|
||||
|
||||
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',
|
||||
]
|
||||
from .utils import SessionManager
|
||||
|
||||
|
||||
def ext_corpus_update_db(corpus: str) -> CQiStatusOk:
|
||||
corpus_id = CQiOverSocketIOSessionManager.get_corpus_id()
|
||||
cqi_client = CQiOverSocketIOSessionManager.get_cqi_client()
|
||||
corpus_id = SessionManager.get_corpus_id()
|
||||
cqi_client = SessionManager.get_cqi_client()
|
||||
db_corpus = Corpus.query.get(corpus_id)
|
||||
cqi_corpus = cqi_client.corpora.get(corpus)
|
||||
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:
|
||||
corpus_id = CQiOverSocketIOSessionManager.get_corpus_id()
|
||||
corpus_id = SessionManager.get_corpus_id()
|
||||
db_corpus = Corpus.query.get(corpus_id)
|
||||
|
||||
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:
|
||||
return f.read()
|
||||
|
||||
cqi_client = CQiOverSocketIOSessionManager.get_cqi_client()
|
||||
cqi_client = SessionManager.get_cqi_client()
|
||||
cqi_corpus = cqi_client.corpora.get(corpus)
|
||||
cqi_p_attrs = cqi_corpus.positional_attributes.list()
|
||||
cqi_s_attrs = cqi_corpus.structural_attributes.list()
|
||||
@ -168,7 +158,7 @@ def ext_corpus_paginate_corpus(
|
||||
page: int = 1,
|
||||
per_page: int = 20
|
||||
) -> dict:
|
||||
cqi_client = CQiOverSocketIOSessionManager.get_cqi_client()
|
||||
cqi_client = SessionManager.get_cqi_client()
|
||||
cqi_corpus = cqi_client.corpora.get(corpus)
|
||||
# Sanity checks
|
||||
if (
|
||||
@ -215,7 +205,7 @@ def ext_cqp_paginate_subcorpus(
|
||||
per_page: int = 20
|
||||
) -> dict:
|
||||
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_subcorpus = cqi_corpus.subcorpora.get(subcorpus_name)
|
||||
# Sanity checks
|
||||
@ -262,7 +252,7 @@ def ext_cqp_partial_export_subcorpus(
|
||||
context: int = 50
|
||||
) -> dict:
|
||||
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_subcorpus = cqi_corpus.subcorpora.get(subcorpus_name)
|
||||
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:
|
||||
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_subcorpus = cqi_corpus.subcorpora.get(subcorpus_name)
|
||||
cqi_subcorpus_export = _export_subcorpus(cqi_subcorpus, context=context)
|
@ -3,7 +3,7 @@ from threading import Lock
|
||||
from flask import session
|
||||
|
||||
|
||||
class CQiOverSocketIOSessionManager:
|
||||
class SessionManager:
|
||||
@staticmethod
|
||||
def setup():
|
||||
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";
|
||||
}
|
||||
|
||||
[data-corpus-status="SUBMITTED"] {
|
||||
--corpus-status-content: "submitted";
|
||||
}
|
||||
|
||||
[data-corpus-status="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,25 +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
|
||||
*/
|
||||
@media only screen and (min-width: 993px) {
|
||||
body[data-sidenav-fixed="true" i] header,
|
||||
body[data-sidenav-fixed="true" i] main,
|
||||
body[data-sidenav-fixed="true" i] footer {
|
||||
padding-left: 300px;
|
||||
}
|
||||
|
||||
body[data-sidenav-fixed="true" i] .navbar-fixed > nav {
|
||||
width: calc(100% - 300px);
|
||||
}
|
||||
}
|
||||
/* #endregion sidenav-fixed */
|
||||
|
||||
/* #region sticky-footer */
|
||||
/*
|
||||
* Sticky Footer:
|
||||
@ -32,13 +10,13 @@
|
||||
*
|
||||
* Read more: https://materializecss.com/footer.html#sticky-footer
|
||||
*/
|
||||
body[data-sticky-footer="true" i] {
|
||||
body {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
body[data-sticky-footer="true" i] main {
|
||||
main {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
/* #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;
|
||||
}
|
@ -1,205 +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 processes and services Dropdown.
|
||||
M.Dropdown.init(
|
||||
document.querySelector('#nav-processes-and-services-dropdown-trigger'),
|
||||
{
|
||||
constrainWidth: false,
|
||||
coverTrigger: false
|
||||
}
|
||||
);
|
||||
|
||||
// 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}`;
|
||||
this.elements.error.innerText = errorString;
|
||||
this.elements.error.classList.remove('hide');
|
||||
app.flash(errorString, 'error');
|
||||
app.ui.flash(errorString, 'error');
|
||||
this.elements.progress.classList.add('hide');
|
||||
}
|
||||
this.app.enableActionElements();
|
||||
@ -239,7 +239,7 @@ nopaque.corpus_analysis.ConcordanceExtension = class ConcordanceExtension {
|
||||
if (subcorpus.selectedItems.size === 0) {
|
||||
this.elements.progress.classList.add('hide');
|
||||
this.app.enableActionElements();
|
||||
app.flash('No matches selected', 'error');
|
||||
app.ui.flash('No matches selected', 'error');
|
||||
return;
|
||||
}
|
||||
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];
|
||||
subcorpus.o.drop().then(
|
||||
(cQiStatus) => {
|
||||
app.flash(`${subcorpus.o.name} deleted`, 'corpus');
|
||||
app.ui.flash(`${subcorpus.o.name} deleted`, 'corpus');
|
||||
delete this.data.subcorpora[subcorpus.o.name];
|
||||
this.settings.selectedSubcorpus = undefined;
|
||||
for (let subcorpusName in this.data.subcorpora) {
|
||||
@ -320,7 +320,7 @@ nopaque.corpus_analysis.ConcordanceExtension = class ConcordanceExtension {
|
||||
},
|
||||
(cqiError) => {
|
||||
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}`;}
|
||||
this.elements.error.innerText = errorString;
|
||||
this.elements.error.classList.remove('hide');
|
||||
app.flash(errorString, 'error');
|
||||
app.ui.flash(errorString, 'error');
|
||||
this.elements.progress.classList.add('hide');
|
||||
}
|
||||
this.app.enableActionElements();
|
||||
@ -205,7 +205,7 @@ nopaque.corpus_analysis.ReaderExtension = class ReaderExtension {
|
||||
`
|
||||
);
|
||||
this.elements.corpusPagination.appendChild(pageElement);
|
||||
|
||||
|
||||
for (let paginateTriggerElement of this.elements.corpusPagination.querySelectorAll('.pagination-trigger[data-target]')) {
|
||||
paginateTriggerElement.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
|
@ -7,7 +7,6 @@ nopaque.corpus_analysis.StaticVisualizationExtension = class StaticVisualization
|
||||
stopwords: undefined,
|
||||
originalStopwords: {},
|
||||
stopwordCache: {},
|
||||
promises: {getStopwords: undefined},
|
||||
tokenSet: new Set()
|
||||
};
|
||||
|
||||
@ -73,22 +72,11 @@ nopaque.corpus_analysis.StaticVisualizationExtension = class StaticVisualization
|
||||
}
|
||||
}
|
||||
|
||||
getStopwords() {
|
||||
this.data.promises.getStopwords = new Promise((resolve, reject) => {
|
||||
nopaque.requests.corpora.entity.getStopwords()
|
||||
.then((response) => {
|
||||
response.json()
|
||||
.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;
|
||||
async getStopwords() {
|
||||
const stopwords = await app.corpora.getStopwords(this.app.corpusId);
|
||||
this.data.originalStopwords = structuredClone(stopwords);
|
||||
this.data.stopwords = structuredClone(stopwords);
|
||||
return stopwords;
|
||||
}
|
||||
|
||||
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_simple_pos: Object.entries(corpusData.s_attrs.text.lexicon[i].freqs.simple_pos).length
|
||||
};
|
||||
|
||||
|
||||
textData.push(resource);
|
||||
}
|
||||
|
||||
@ -262,7 +250,7 @@ nopaque.corpus_analysis.StaticVisualizationExtension = class StaticVisualization
|
||||
}
|
||||
return filteredData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
renderFrequenciesGraphic(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 graphtype = document.querySelector('.frequencies-graph-mode-button.disabled').dataset.graphType;
|
||||
let tokenCategory = frequenciesTokenCategoryDropdownElement.firstChild.textContent.toLowerCase();
|
||||
|
||||
|
||||
let graphData = this.createFrequenciesGraphData(tokenCategory, texts, graphtype, tokenSet);
|
||||
let graphLayout = {
|
||||
barmode: graphtype === 'bar' ? 'stack' : '',
|
||||
@ -328,7 +316,7 @@ nopaque.corpus_analysis.StaticVisualizationExtension = class StaticVisualization
|
||||
for (let i = 0; i < texts.length; i++) {
|
||||
tokenCountPerText[i] = (tokenCountPerText[i] || 0) + (texts[i][1].freqs[tokenCategory][originalId] || 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
let data = {
|
||||
x: textTitles,
|
||||
y: tokenCountPerText,
|
||||
@ -353,7 +341,7 @@ nopaque.corpus_analysis.StaticVisualizationExtension = class StaticVisualization
|
||||
stopwordLanguageChipList.innerHTML = '';
|
||||
userStopwordListContainer.innerHTML = '';
|
||||
stopwordInputField.value = '';
|
||||
|
||||
|
||||
// Render stopword language selection. Set english as default language. Filter out user_stopwords.
|
||||
if (stopwordLanguageSelection.children.length === 0) {
|
||||
Object.keys(stopwords).forEach(language => {
|
||||
@ -379,7 +367,7 @@ nopaque.corpus_analysis.StaticVisualizationExtension = class StaticVisualization
|
||||
// Render english stopwords as default ...
|
||||
let selectedLanguage = document.querySelector('#stopword-language-selection').value;
|
||||
this.renderStopwordLanguageChipList(selectedLanguage, stopwords[selectedLanguage]);
|
||||
|
||||
|
||||
// ... or render selected language stopwords.
|
||||
stopwordLanguageSelection.addEventListener('change', (event) => {
|
||||
this.renderStopwordLanguageChipList(event.target.value, stopwords[event.target.value]);
|
||||
@ -402,7 +390,7 @@ nopaque.corpus_analysis.StaticVisualizationExtension = class StaticVisualization
|
||||
|
||||
// Initialize Materialize components.
|
||||
M.Chips.init(
|
||||
stopwordInputField,
|
||||
stopwordInputField,
|
||||
{
|
||||
placeholder: 'Add stopwords',
|
||||
onChipAdd: (event) => {
|
||||
@ -428,7 +416,7 @@ nopaque.corpus_analysis.StaticVisualizationExtension = class StaticVisualization
|
||||
deleteLanguageStopwordListEntriesButton.classList.toggle('disabled', stopwordLength === 0);
|
||||
resetLanguageStopwordListEntriesButton.classList.toggle('disabled', stopwordLength === originalStopwordListLength);
|
||||
}
|
||||
|
||||
|
||||
renderStopwordLanguageChipList(language, stopwords) {
|
||||
let stopwordLanguageChipList = document.querySelector('#stopword-language-chip-list');
|
||||
stopwordLanguageChipList.innerHTML = '';
|
||||
|
@ -101,7 +101,7 @@ nopaque.forms.BaseForm = class BaseForm {
|
||||
}
|
||||
}
|
||||
if (request.status === 500) {
|
||||
app.flash('Internal Server Error', 'error');
|
||||
app.ui.flash('Internal Server Error', 'error');
|
||||
}
|
||||
modal.close();
|
||||
});
|
||||
|
@ -5,50 +5,6 @@ nopaque.requests.corpora = {};
|
||||
|
||||
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 *
|
||||
|
@ -18,23 +18,23 @@ nopaque.requests.JSONfetch = (input, init={}) => {
|
||||
}
|
||||
if (response.status === 204) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
response.json()
|
||||
.then(
|
||||
(json) => {
|
||||
let message = json.message;
|
||||
let category = json.category || 'message';
|
||||
if (message) {
|
||||
app.flash(message, category);
|
||||
app.ui.flash(message, category);
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
app.flash(`[${response.status}]: ${response.statusText}`, 'error');
|
||||
app.ui.flash(`[${response.status}]: ${response.statusText}`, 'error');
|
||||
}
|
||||
);
|
||||
},
|
||||
(response) => {
|
||||
app.flash('Something went wrong', 'error');
|
||||
app.ui.flash('Something went wrong', 'error');
|
||||
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
|
||||
.querySelector('.action-button[data-action="build-request"]')
|
||||
.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) {
|
||||
this.setElements(this.displayElement.querySelectorAll('.corpus-title'), title);
|
||||
async setTitle(title) {
|
||||
const corpusTitleElements = this.displayElement.querySelectorAll('.corpus-title');
|
||||
this.setElements(corpusTitleElements, title);
|
||||
}
|
||||
|
||||
setNumTokens(numTokens) {
|
||||
this.setElements(
|
||||
this.displayElement.querySelectorAll('.corpus-token-ratio'),
|
||||
`${numTokens}/${app.data.users[this.userId].corpora[this.corpusId].max_num_tokens}`
|
||||
);
|
||||
const corpusTokenRatioElements = this.displayElement.querySelectorAll('.corpus-token-ratio');
|
||||
const maxNumTokens = 2147483647;
|
||||
|
||||
this.setElements(corpusTokenRatioElements, `${numTokens}/${maxNumTokens}`);
|
||||
}
|
||||
|
||||
setDescription(description) {
|
||||
this.setElements(this.displayElement.querySelectorAll('.corpus-description'), description);
|
||||
}
|
||||
|
||||
setStatus(status) {
|
||||
async setStatus(status) {
|
||||
let elements = this.displayElement.querySelectorAll('.action-button[data-action="analyze"]');
|
||||
for (let element of elements) {
|
||||
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"]');
|
||||
const user = await app.userHub.get(this.userId);
|
||||
const corpusFiles = user.corpora[this.corpusId].files;
|
||||
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');
|
||||
} else {
|
||||
element.classList.add('disabled');
|
||||
|
@ -5,19 +5,14 @@ nopaque.resource_displays.ResourceDisplay = class ResourceDisplay {
|
||||
this.displayElement = displayElement;
|
||||
this.userId = this.displayElement.dataset.userId;
|
||||
this.isInitialized = false;
|
||||
if (this.userId) {
|
||||
app.subscribeUser(this.userId)
|
||||
.then((response) => {
|
||||
app.sockets.users.on('patch_user', (patch) => {
|
||||
if (this.isInitialized) {this.onPatch(patch);}
|
||||
});
|
||||
});
|
||||
app.getUser(this.userId)
|
||||
.then((user) => {
|
||||
this.init(user);
|
||||
this.isInitialized = true;
|
||||
});
|
||||
}
|
||||
if (this.userId === undefined) {return;}
|
||||
app.userHub.addEventListener('patch', (event) => {
|
||||
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||
});
|
||||
app.userHub.get(this.userId).then((user) => {
|
||||
this.init(user);
|
||||
this.isInitialized = true;
|
||||
});
|
||||
}
|
||||
|
||||
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.hasPermissionManageFiles = listContainerElement.dataset?.hasPermissionManageFiles == 'true' || false;
|
||||
if (this.userId === undefined || this.corpusId === undefined) {return;}
|
||||
app.subscribeUser(this.userId).then((response) => {
|
||||
app.sockets.users.on('patch_user', (patch) => {
|
||||
if (this.isInitialized) {this.onPatch(patch);}
|
||||
});
|
||||
app.userHub.addEventListener('patch', (event) => {
|
||||
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||
});
|
||||
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.isInitialized = true;
|
||||
});
|
||||
|
@ -12,15 +12,16 @@ nopaque.resource_lists.CorpusFollowerList = class CorpusFollowerList extends nop
|
||||
this.userId = listContainerElement.dataset.userId;
|
||||
this.corpusId = listContainerElement.dataset.corpusId;
|
||||
if (this.userId === undefined || this.corpusId === undefined) {return;}
|
||||
app.subscribeUser(this.userId).then((response) => {
|
||||
app.sockets.users.on('patch_user', (patch) => {
|
||||
if (this.isInitialized) {this.onPatch(patch);}
|
||||
});
|
||||
app.userHub.addEventListener('patch', (event) => {
|
||||
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||
});
|
||||
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 filteredList = corpusFollowerAssociations.filter(association => association.follower.id != currentUserId);
|
||||
// this.add(filteredList);
|
||||
|
||||
// TODO: Make this better understandable
|
||||
this.add(Object.values(user.corpora[this.corpusId].corpus_follower_associations));
|
||||
this.isInitialized = true;
|
||||
});
|
||||
|
@ -11,12 +11,10 @@ nopaque.resource_lists.CorpusList = class CorpusList extends nopaque.resource_li
|
||||
this.selectedItemIds = new Set();
|
||||
this.userId = listContainerElement.dataset.userId;
|
||||
if (this.userId === undefined) {return;}
|
||||
app.subscribeUser(this.userId).then((response) => {
|
||||
app.sockets.users.on('patch_user', (patch) => {
|
||||
if (this.isInitialized) {this.onPatch(patch);}
|
||||
});
|
||||
app.userHub.addEventListener('patch', (event) => {
|
||||
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||
});
|
||||
app.getUser(this.userId).then((user) => {
|
||||
app.userHub.get(this.userId).then((user) => {
|
||||
this.add(this.aggregateData(user));
|
||||
this.isInitialized = true;
|
||||
});
|
||||
@ -69,6 +67,7 @@ nopaque.resource_lists.CorpusList = class CorpusList extends nopaque.resource_li
|
||||
<span class="disable-on-click"></span>
|
||||
</label>
|
||||
</td>
|
||||
<td><a class="btn-floating service-color darken" data-service="corpus-analysis"><i class="material-icons">book</i></a></td>
|
||||
<td>
|
||||
<b class="title"></b><br>
|
||||
<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 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 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>
|
||||
</tr>
|
||||
`.trim();
|
||||
@ -119,6 +118,7 @@ nopaque.resource_lists.CorpusList = class CorpusList extends nopaque.resource_li
|
||||
<span></span>
|
||||
</label>
|
||||
</th>
|
||||
<th></th>
|
||||
<th>Title and Description</th>
|
||||
<th>Owner</th>
|
||||
<th>Status</th>
|
||||
@ -180,7 +180,7 @@ nopaque.resource_lists.CorpusList = class CorpusList extends nopaque.resource_li
|
||||
window.location.reload();
|
||||
});
|
||||
} else {
|
||||
nopaque.requests.corpora.entity.delete(itemId);
|
||||
app.corpora.delete(itemId);
|
||||
}
|
||||
});
|
||||
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 values = this.listjs.get('id', listItem.dataset.id)[0].values();
|
||||
if (values['is-owner']) {
|
||||
nopaque.requests.corpora.entity.delete(selectedItemId);
|
||||
app.corpora.delete(selectedItemId);
|
||||
} else {
|
||||
nopaque.requests.corpora.entity.followers.entity.delete(selectedItemId, currentUserId);
|
||||
setTimeout(() => {
|
||||
|
@ -8,8 +8,10 @@ nopaque.resource_lists.JobInputList = class JobInputList extends nopaque.resourc
|
||||
this.userId = listContainerElement.dataset.userId;
|
||||
this.jobId = listContainerElement.dataset.jobId;
|
||||
if (this.userId === undefined || this.jobId === undefined) {return;}
|
||||
app.subscribeUser(this.userId);
|
||||
app.getUser(this.userId).then((user) => {
|
||||
// app.userHub.addEventListener('patch', (event) => {
|
||||
// if (this.isInitialized) {this.onPatch(event.detail);}
|
||||
// });
|
||||
app.userHub.get(this.userId).then((user) => {
|
||||
this.add(Object.values(user.jobs[this.jobId].inputs));
|
||||
this.isInitialized = true;
|
||||
});
|
||||
|
@ -12,12 +12,10 @@ nopaque.resource_lists.JobList = class JobList extends nopaque.resource_lists.Re
|
||||
this.selectedItemIds = new Set();
|
||||
this.userId = listContainerElement.dataset.userId;
|
||||
if (this.userId === undefined) {return;}
|
||||
app.subscribeUser(this.userId).then((response) => {
|
||||
app.sockets.users.on('patch_user', (patch) => {
|
||||
if (this.isInitialized) {this.onPatch(patch);}
|
||||
});
|
||||
app.userHub.addEventListener('patch', (event) => {
|
||||
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||
});
|
||||
app.getUser(this.userId).then((user) => {
|
||||
app.userHub.get(this.userId).then((user) => {
|
||||
this.add(Object.values(user.jobs));
|
||||
this.isInitialized = true;
|
||||
});
|
||||
@ -25,19 +23,19 @@ nopaque.resource_lists.JobList = class JobList extends nopaque.resource_lists.Re
|
||||
|
||||
get item() {
|
||||
return `
|
||||
<tr class="list-item clickable hoverable">
|
||||
<tr class="list-item clickable hoverable service-color lighten">
|
||||
<td>
|
||||
<label class="list-action-trigger" data-list-action="select">
|
||||
<input class="select-checkbox" type="checkbox">
|
||||
<span class="disable-on-click"></span>
|
||||
</label>
|
||||
</td>
|
||||
<td><a class="btn-floating 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><span class="status badge new job-status-color job-status-text" data-badge-caption=""></span></td>
|
||||
<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 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>
|
||||
</tr>
|
||||
`.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"]');
|
||||
confirmElement.addEventListener('click', (event) => {
|
||||
nopaque.requests.jobs.entity.delete(itemId);
|
||||
confirmElement.addEventListener('click', async (event) => {
|
||||
const message = await app.jobs.delete(itemId);
|
||||
app.ui.flash(message, 'job');
|
||||
});
|
||||
modal.open();
|
||||
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"]');
|
||||
confirmElement.addEventListener('click', (event) => {
|
||||
this.selectedItemIds.forEach(selectedItemId => {
|
||||
nopaque.requests.jobs.entity.delete(selectedItemId);
|
||||
this.selectedItemIds.forEach(async (selectedItemId) => {
|
||||
const message = await app.jobs.delete(selectedItemId);
|
||||
app.ui.flash(message, 'job');
|
||||
});
|
||||
this.selectedItemIds.clear();
|
||||
this.renderingItemSelection();
|
||||
|
@ -8,12 +8,10 @@ nopaque.resource_lists.JobResultList = class JobResultList extends nopaque.resou
|
||||
this.userId = listContainerElement.dataset.userId;
|
||||
this.jobId = listContainerElement.dataset.jobId;
|
||||
if (this.userId === undefined || this.jobId === undefined) {return;}
|
||||
app.subscribeUser(this.userId).then((response) => {
|
||||
app.sockets.users.on('patch_user', (patch) => {
|
||||
if (this.isInitialized) {this.onPatch(patch);}
|
||||
});
|
||||
app.userHub.addEventListener('patch', (event) => {
|
||||
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||
});
|
||||
app.getUser(this.userId).then((user) => {
|
||||
app.userHub.get(this.userId).then((user) => {
|
||||
this.add(Object.values(user.jobs[this.jobId].results));
|
||||
this.isInitialized = true;
|
||||
});
|
||||
|
@ -8,12 +8,10 @@ nopaque.resource_lists.SpaCyNLPPipelineModelList = class SpaCyNLPPipelineModelLi
|
||||
this.isInitialized = false;
|
||||
this.userId = listContainerElement.dataset.userId;
|
||||
if (this.userId === undefined) {return;}
|
||||
app.subscribeUser(this.userId).then((response) => {
|
||||
app.sockets.users.on('patch_user', (patch) => {
|
||||
if (this.isInitialized) {this.onPatch(patch);}
|
||||
});
|
||||
app.userHub.addEventListener('patch', (event) => {
|
||||
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||
});
|
||||
app.getUser(this.userId).then((user) => {
|
||||
app.userHub.get(this.userId).then((user) => {
|
||||
this.add(Object.values(user.spacy_nlp_pipeline_models));
|
||||
this.isInitialized = true;
|
||||
});
|
||||
|
@ -8,21 +8,11 @@ nopaque.resource_lists.TesseractOCRPipelineModelList = class TesseractOCRPipelin
|
||||
this.isInitialized = false;
|
||||
this.userId = listContainerElement.dataset.userId;
|
||||
if (this.userId === undefined) {return;}
|
||||
app.subscribeUser(this.userId).then((response) => {
|
||||
app.sockets.users.on('patch_user', (patch) => {
|
||||
if (this.isInitialized) {this.onPatch(patch);}
|
||||
});
|
||||
app.userHub.addEventListener('patch', (event) => {
|
||||
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||
});
|
||||
app.getUser(this.userId).then((user) => {
|
||||
app.userHub.get(this.userId).then((user) => {
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
@ -1,52 +0,0 @@
|
||||
<div id="data-processing-and-analysis-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="card-panel primary-color white-text">
|
||||
<h4 class="m-3"><i class="material-icons left" style="font-size: inherit; line-height: inherit;">miscellaneous_services</i>Data Processing & Analysis</h4>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="row">
|
||||
<div class="col s12 m6 l3 center-align hoverable" style="position: relative;">
|
||||
<a href="{{ url_for('services.file_setup_pipeline') }}" style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;"></a>
|
||||
<i class="large nopaque-icons service-icons service-color-text text-darken" data-service="file-setup-pipeline"></i>
|
||||
<br>
|
||||
<b class="service-color-text text-darken" data-service="file-setup-pipeline">File Setup</b>
|
||||
<p class="light">Digital copies of text based research data (books, letters, etc.) often comprise various files and formats. nopaque converts and merges those files to facilitate further processing and the application of other services.</p>
|
||||
</div>
|
||||
<div class="col s12 m6 l3 center-align center-align hoverable" style="position: relative;">
|
||||
<a href="{{ url_for('services.tesseract_ocr_pipeline') }}" style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;"></a>
|
||||
<i class="large nopaque-icons service-icons service-color-text text-darken" data-service="tesseract-ocr-pipeline"></i>
|
||||
<br>
|
||||
<b class="service-color-text text-darken" data-service="tesseract-ocr-pipeline">Optical Character Recognition</b>
|
||||
<p class="light">nopaque converts your image data – like photos or scans – into text data through OCR making it machine readable. This step enables you to proceed with further computational analysis of your documents.</p>
|
||||
</div>
|
||||
{% if config.NOPAQUE_TRANSKRIBUS_ENABLED %}
|
||||
<div class="col s12 m6 l3 center-align center-align hoverable" style="position: relative;">
|
||||
<a href="{{ url_for('services.transkribus_htr_pipeline') }}" style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;"></a>
|
||||
<i class="large nopaque-icons service-icons service-color-text text-darken" data-service="transkribus-htr-pipeline"></i>
|
||||
<br>
|
||||
<b class="service-color-text text-darken" data-service="transkribus-htr-pipeline">Transkribus HTR Pipeline</b>
|
||||
<p class="light">nopaque converts your image data – like photos or scans – into text data through HTR making it machine readable. This step enables you to proceed with further computational analysis of your documents.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col s12 m6 l3 center-align center-align hoverable" style="position: relative;">
|
||||
<a href="{{ url_for('services.spacy_nlp_pipeline') }}" style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;"></a>
|
||||
<i class="large nopaque-icons service-icons service-color-text text-darken" data-service="spacy-nlp-pipeline"></i>
|
||||
<br>
|
||||
<b class="service-color-text text-darken" data-service="spacy-nlp-pipeline">Natural Language Processing</b>
|
||||
<p class="light">By means of computational linguistic data processing (tokenization, lemmatization, part-of-speech tagging and named-entity recognition) nopaque extracts additional information from your text.</p>
|
||||
</div>
|
||||
<div class="col s12 m6 l3 center-align center-align hoverable" style="position: relative;">
|
||||
<a href="{{ url_for('services.corpus_analysis') }}" style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;"></a>
|
||||
<i class="large nopaque-icons service-icons service-color-text text-darken" data-service="corpus-analysis"></i>
|
||||
<br>
|
||||
<b class="service-color-text text-darken" data-service="corpus-analysis">Corpus analysis</b>
|
||||
<p class="light">nopaque lets you create and upload as many text corpora as you want. It makes use of CQP Query Language, which allows for complex search requests with the aid of metadata and NLP tags.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a class="btn-flat modal-close">Close</a>
|
||||
</div>
|
||||
</div>
|
@ -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,9 +1,79 @@
|
||||
{% if current_user.is_authenticated %}
|
||||
<ul class="dropdown-content" id="nav-account-dropdown-content">
|
||||
<li><a href="#!">Logged in as {{ current_user.username }}<br><{{ current_user.email }}></a></li>
|
||||
<ul class="dropdown-content" id="navbar-data-processing-and-analysis-dropdown-content">
|
||||
<li {% if request.path == url_for('services.file_setup_pipeline') %}class="active"{% endif %}>
|
||||
<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><a href="{{ url_for('users.user', user_id=current_user.id) }}"><i class="material-icons">person</i>Your profile</a></li>
|
||||
<li><a href="{{ url_for('settings.settings') }}"><i class="material-icons left">settings</i>Settings</a></li>
|
||||
<li><a href="{{ url_for('auth.logout') }}"><i class="material-icons left">logout</i>Log out</a></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>
|
||||
{% endif %}
|
||||
|
@ -1,40 +1,45 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col s6 m3">
|
||||
<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">
|
||||
<div class="col s12 l3">
|
||||
<h5 class="white-text">Legal Notice</h5>
|
||||
<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="{{ 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></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-copyright">
|
||||
<div class="container">
|
||||
<div class="row" style="margin-bottom: 0;">
|
||||
<div class="col s12 m3">
|
||||
<span>© 2020 Bielefeld University</span>
|
||||
</div>
|
||||
<div class="col s12 m2">
|
||||
<span class="right"><b>Version {{ config.NOPAQUE_VERSION }}</b></span>
|
||||
</div>
|
||||
<div class="col s12 m7 right-align">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="col s12 l3">
|
||||
<h5 class="white-text">More Resources</h5>
|
||||
<ul>
|
||||
<li><a class="grey-text text-lighten-3" href="{{ url_for('main.faq') }}">Frequently asked questions</a></li>
|
||||
<li><a class="grey-text text-lighten-3" href="mailto:{{ config.NOPAQUE_SERVICE_DESK }}">Report an issue</a></li>
|
||||
<li><a class="grey-text text-lighten-3" href="https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque">GitLab (source code)</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="col s12 l4">
|
||||
<h5 class="white-text">Who made this?</h5>
|
||||
<p class="grey-text text-lighten-4">
|
||||
This software is developed by the SFB 1288 INF project at Bielefeld University.
|
||||
Thanks to all the people who made nopaque possible.
|
||||
<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 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>
|
||||
|
1
app/templates/_base/icons.html.j2
Normal file
1
app/templates/_base/icons.html.j2
Normal file
@ -0,0 +1 @@
|
||||
<link href="{{ url_for('static', filename='images/nopaque_-_favicon.png') }}" rel="icon">
|
@ -1,5 +1,28 @@
|
||||
{% if current_user.is_authenticated and not current_user.terms_of_use_accepted %}
|
||||
{% include "_base/_modals/terms_of_use.html.j2" %}
|
||||
{% endif %}
|
||||
<div class="modal modal-fixed-footer" id="terms-of-use-modal">
|
||||
<div class="modal-content">
|
||||
<div class="card-panel primary-color white-text">
|
||||
<h4 class="m-3 center-align">Terms of use</h4>
|
||||
</div>
|
||||
|
||||
{% include "_base/_modals/data-processing-and-analysis.html.j2" %}
|
||||
<ul class="tabs tabs-fixed-width z-depth-1">
|
||||
<li class="tab"><a class="active" href="#terms-of-use-modal-content-german">German</a></li>
|
||||
<li class="tab"><a href="#terms-of-use-modal-content-english">English</a></li>
|
||||
</ul>
|
||||
|
||||
<div id="terms-of-use-modal-content-german">
|
||||
{% include 'main/_terms_of_use/german.html.j2' %}
|
||||
</div>
|
||||
|
||||
<div id="terms-of-use-modal-content-english">
|
||||
{% include 'main/_terms_of_use/english.html.j2' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
{% if current_user.is_authenticated and not current_user.terms_of_use_accepted %}
|
||||
<a href="#!" class="btn waves-effect waves-light modal-close" id="terms-of-use-modal-accept-button">Accept</a>
|
||||
{% else %}
|
||||
<a href="#!" class="btn-flat waves-effect waves-light modal-close">Close</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
105
app/templates/_base/navbar.html.j2
Normal file
105
app/templates/_base/navbar.html.j2
Normal file
@ -0,0 +1,105 @@
|
||||
<div class="navbar-fixed">
|
||||
<nav>
|
||||
<div class="nav-wrapper">
|
||||
{# menu icon #}
|
||||
{# small/medium devices #}
|
||||
<a href="#!" class="sidenav-trigger" data-target="sidenav">
|
||||
<i class="material-icons">menu</i>
|
||||
</a>
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
{# nopaque logo #}
|
||||
{# large devices #}
|
||||
<a href="{{ url_for('main.index') }}" class="brand-logo hide-on-med-and-down h-100">
|
||||
<img src="{{ url_for('static', filename='images/nopaque_-_logo.png') }}" alt="" class="mx-3 py-3 h-100">
|
||||
</a>
|
||||
|
||||
{# left aligned navigation items #}
|
||||
{# large devices #}
|
||||
<ul class="hide-on-med-and-down" style="margin-left: calc(57px + 1.5rem);">
|
||||
{# dashboard #}
|
||||
<li {% if request.path == url_for('main.dashboard') %}class="active"{% endif %}>
|
||||
<a href="{{ url_for('main.dashboard') }}">
|
||||
<i class="material-icons left">dashboard</i>
|
||||
Dashboard
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{# data processing & analysis #}
|
||||
<li>
|
||||
<a href="#!" class="dropdown-trigger no-autoinit" data-target="navbar-data-processing-and-analysis-dropdown-content" id="navbar-data-processing-and-analysis-dropdown-trigger">
|
||||
<i class="material-icons left">miscellaneous_services</i>
|
||||
Data Processing & Analysis
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{# contributions #}
|
||||
<li {% if request.path == url_for('contributions.index') %}class="active"{% endif %}>
|
||||
<a href="{{ url_for('contributions.index') }}">
|
||||
<i class="material-icons left">new_label</i>
|
||||
Contributions
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{# social #}
|
||||
<li {% if request.path == url_for('main.social') %}class="active"{% endif %}>
|
||||
<a href="{{ url_for('main.social') }}">
|
||||
<i class="material-icons left">groups</i>
|
||||
Social
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% else %}
|
||||
{# nopaque logo+wordmark+slogan #}
|
||||
{# large devices #}
|
||||
<a href="{{ url_for('main.index') }}" class="brand-logo hide-on-med-and-down h-100">
|
||||
<img src="{{ url_for('static', filename='images/nopaque_-_logo+wordmark+slogan.png') }}" alt="" class="mx-3 py-3 h-100">
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{# nopaque logo+wordmark #}
|
||||
{# small/medium devices #}
|
||||
<a href="{{ url_for('main.index') }}" class="brand-logo center hide-on-large-only h-100">
|
||||
<img src="{{ url_for('static', filename='images/nopaque_-_logo+wordmark.png') }}" alt="" class="py-3 h-100">
|
||||
</a>
|
||||
|
||||
{# right aligned navigation items #}
|
||||
{# large devices #}
|
||||
<ul class="right hide-on-med-and-down h-100">
|
||||
{# manual #}
|
||||
<li class="tooltipped {% if request.path == url_for('main.manual') %}active{% endif %}" data-position="bottom" data-tooltip="Manual">
|
||||
<a href="{{ url_for('main.manual') }}">
|
||||
<i class="material-icons">help_outline</i>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{# news #}
|
||||
<li class="tooltipped {% if request.path == url_for('main.news') %}active{% endif %}" data-position="bottom" data-tooltip="News">
|
||||
<a href="{{ url_for('main.news') }}">
|
||||
<i class="material-icons">newspaper</i>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
{# avatar #}
|
||||
<li class="h-100">
|
||||
<a href="#!" class="dropdown-trigger no-autoinit h-100" data-target="navbar-account-dropdown-content" id="navbar-account-dropdown-trigger">
|
||||
<span class="mr-3">{{ current_user.username }}</span>
|
||||
<img src="{{ url_for('users.user_avatar', user_id=current_user.id) }}" alt="" class="circle py-3 h-100 right">
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
{# log in #}
|
||||
<li {% if request.path == url_for('auth.login') %}class="active"{% endif %}>
|
||||
<a href="{{ url_for('auth.login') }}">Log in</a>
|
||||
</li>
|
||||
|
||||
{# register #}
|
||||
<li {% if request.path == url_for('auth.register') %}class="active"{% endif %}>
|
||||
<a href="{{ url_for('auth.register') }}" class="btn waves-effect waves-light primary-color lighten">Register</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
@ -1,95 +0,0 @@
|
||||
{% if current_user.is_authenticated %}
|
||||
{# menu icon #}
|
||||
{# shown for small/medium devices #}
|
||||
<a href="#!" class="sidenav-trigger" data-target="sidenav"><i class="material-icons">menu</i></a>
|
||||
|
||||
{# nopaque logo #}
|
||||
{# shown for large devices #}
|
||||
<a href="{{ url_for('main.index') }}" class="brand-logo hide-on-med-and-down" style="height: 100%;">
|
||||
<img src="{{ url_for('static', filename='images/nopaque_-_logo.png') }}" alt="" class="mx-3 py-3" style="height: 100%;">
|
||||
</a>
|
||||
|
||||
{# left aligned navigation items #}
|
||||
{# shown for large devices #}
|
||||
<ul class="hide-on-med-and-down" style="margin-left: calc(57px + 1.5rem);">
|
||||
{# dashboard #}
|
||||
<li>
|
||||
<a href="{{ url_for('main.dashboard') }}">
|
||||
<i class="material-icons left">dashboard</i>
|
||||
Dashboard
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{# processes & services #}
|
||||
<li>
|
||||
<a href="#data-processing-and-analysis-modal" class="modal-trigger">
|
||||
<i class="material-icons left">miscellaneous_services</i>
|
||||
Data Processing & Analysis
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{# contributions #}
|
||||
<li>
|
||||
<a href="{{ url_for('contributions.index') }}">
|
||||
<i class="material-icons left">new_label</i>
|
||||
Contributions
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{# social #}
|
||||
<li>
|
||||
<a href="{{ url_for('main.social') }}">
|
||||
<i class="material-icons left">groups</i>
|
||||
Social
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% else %}
|
||||
{# nopaque logo+wordmark+slogan #}
|
||||
{# shown for large devices #}
|
||||
<a href="{{ url_for('main.index') }}" class="brand-logo hide-on-med-and-down" style="height: 100%;">
|
||||
<img src="{{ url_for('static', filename='images/nopaque_-_logo+wordmark+slogan.png') }}" alt="" class="mx-3 py-3" style="height: 100%;">
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{# nopaque logo+wordmark #}
|
||||
{# small/medium devices #}
|
||||
<a href="{{ url_for('main.index') }}" class="brand-logo center hide-on-large-only" style="height: 100%;">
|
||||
<img src="{{ url_for('static', filename='images/nopaque_-_logo+wordmark.png') }}" alt="" class="py-3" style="height: 100%;">
|
||||
</a>
|
||||
|
||||
{# right aligned navigation items #}
|
||||
{# large devices #}
|
||||
<ul class="right hide-on-med-and-down" style="height: 64px;">
|
||||
{# manual #}
|
||||
<li class="tooltipped" data-position="bottom" data-tooltip="Manual">
|
||||
<a href="{{ url_for('main.manual') }}">
|
||||
<i class="material-icons">help_outline</i>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{# news #}
|
||||
<li class="tooltipped" data-position="bottom" data-tooltip="News">
|
||||
<a href="{{ url_for('main.news') }}">
|
||||
<i class="material-icons">email</i>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
{# avatar #}
|
||||
<li style="height: 100%;">
|
||||
<a href="#!" class="dropdown-trigger no-autoinit" data-target="nav-account-dropdown-content" id="nav-account-dropdown-trigger" style="height: 100%;">
|
||||
<img src="{{ url_for('users.user_avatar', user_id=current_user.id) }}" alt="" class="circle py-3" style="height: 100%;">
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
{# log in #}
|
||||
<li {% if request.path == url_for('auth.login') %}class="active"{% endif %}>
|
||||
<a href="{{ url_for('auth.login') }}">Log in</a>
|
||||
</li>
|
||||
{# register #}
|
||||
<li {% if request.path == url_for('auth.register') %}class="active"{% endif %}>
|
||||
<a href="{{ url_for('auth.register') }}" class="waves-effect waves-light btn primary-color lighten">Register</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
@ -5,109 +5,127 @@
|
||||
<script src="{{ url_for('static', filename='external/plotly.js/js/plotly.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='external/socket.io/js/socket.io.min.js') }}"></script>
|
||||
{% assets
|
||||
filters='rjsmin',
|
||||
output='gen/nopaque.%(version)s.js',
|
||||
'js/index.js',
|
||||
'js/app.js',
|
||||
'js/utils.js',
|
||||
filters='rjsmin',
|
||||
output='gen/nopaque.%(version)s.js',
|
||||
'js/index.js',
|
||||
'js/app/index.js',
|
||||
'js/app/client.js',
|
||||
'js/app/endpoints/index.js',
|
||||
'js/app/endpoints/corpora.js',
|
||||
'js/app/endpoints/jobs.js',
|
||||
'js/app/endpoints/main.js',
|
||||
'js/app/endpoints/settings.js',
|
||||
'js/app/endpoints/users.js',
|
||||
'js/app/extensions/index.js',
|
||||
'js/app/extensions/toaster.js',
|
||||
'js/app/extensions/ui.js',
|
||||
'js/app/extensions/user-hub.js',
|
||||
'js/utils.js',
|
||||
|
||||
'js/forms/index.js',
|
||||
'js/forms/base-form.js',
|
||||
'js/forms/create-contribution-form.js',
|
||||
'js/forms/create-corpus-file-form.js',
|
||||
'js/forms/create-job-form.js',
|
||||
'js/forms/index.js',
|
||||
'js/forms/base-form.js',
|
||||
'js/forms/create-contribution-form.js',
|
||||
'js/forms/create-corpus-file-form.js',
|
||||
'js/forms/create-job-form.js',
|
||||
|
||||
'js/resource-displays/index.js',
|
||||
'js/resource-displays/resource-display.js',
|
||||
'js/resource-displays/corpus-display.js',
|
||||
'js/resource-displays/job-display.js',
|
||||
'js/resource-displays/index.js',
|
||||
'js/resource-displays/resource-display.js',
|
||||
'js/resource-displays/corpus-display.js',
|
||||
'js/resource-displays/job-display.js',
|
||||
|
||||
'js/resource-lists/index.js',
|
||||
'js/resource-lists/resource-list.js',
|
||||
'js/resource-lists/admin-user-list.js',
|
||||
'js/resource-lists/corpus-file-list.js',
|
||||
'js/resource-lists/corpus-follower-list.js',
|
||||
'js/resource-lists/corpus-list.js',
|
||||
'js/resource-lists/corpus-text-info-list.js',
|
||||
'js/resource-lists/corpus-token-list.js',
|
||||
'js/resource-lists/detailed-public-corpus-list.js',
|
||||
'js/resource-lists/job-input-list.js',
|
||||
'js/resource-lists/job-list.js',
|
||||
'js/resource-lists/job-result-list.js',
|
||||
'js/resource-lists/public-corpus-list.js',
|
||||
'js/resource-lists/public-user-list.js',
|
||||
'js/resource-lists/spacy-nlp-pipeline-model-list.js',
|
||||
'js/resource-lists/tesseract-ocr-pipeline-model-list.js',
|
||||
'js/resource-lists/index.js',
|
||||
'js/resource-lists/resource-list.js',
|
||||
'js/resource-lists/admin-user-list.js',
|
||||
'js/resource-lists/corpus-file-list.js',
|
||||
'js/resource-lists/corpus-follower-list.js',
|
||||
'js/resource-lists/corpus-list.js',
|
||||
'js/resource-lists/corpus-text-info-list.js',
|
||||
'js/resource-lists/corpus-token-list.js',
|
||||
'js/resource-lists/detailed-public-corpus-list.js',
|
||||
'js/resource-lists/job-input-list.js',
|
||||
'js/resource-lists/job-list.js',
|
||||
'js/resource-lists/job-result-list.js',
|
||||
'js/resource-lists/public-corpus-list.js',
|
||||
'js/resource-lists/public-user-list.js',
|
||||
'js/resource-lists/spacy-nlp-pipeline-model-list.js',
|
||||
'js/resource-lists/tesseract-ocr-pipeline-model-list.js',
|
||||
|
||||
'js/requests/index.js',
|
||||
'js/requests/admin.js',
|
||||
'js/requests/contributions.js',
|
||||
'js/requests/corpora.js',
|
||||
'js/requests/jobs.js',
|
||||
'js/requests/users.js',
|
||||
'js/requests/index.js',
|
||||
'js/requests/admin.js',
|
||||
'js/requests/contributions.js',
|
||||
'js/requests/corpora.js',
|
||||
'js/requests/users.js',
|
||||
|
||||
'js/corpus-analysis/index.js',
|
||||
'js/corpus-analysis/cqi/index.js',
|
||||
'js/corpus-analysis/cqi/constants.js',
|
||||
'js/corpus-analysis/cqi/errors.js',
|
||||
'js/corpus-analysis/cqi/status.js',
|
||||
'js/corpus-analysis/cqi/api/index.js',
|
||||
'js/corpus-analysis/cqi/api/client.js',
|
||||
'js/corpus-analysis/cqi/models/index.js',
|
||||
'js/corpus-analysis/cqi/models/resource.js',
|
||||
'js/corpus-analysis/cqi/models/attributes.js',
|
||||
'js/corpus-analysis/cqi/models/subcorpora.js',
|
||||
'js/corpus-analysis/cqi/models/corpora.js',
|
||||
'js/corpus-analysis/cqi/client.js',
|
||||
'js/corpus-analysis/query-builder/index.js',
|
||||
'js/corpus-analysis/query-builder/element-references.js',
|
||||
'js/corpus-analysis/query-builder/query-builder.js',
|
||||
'js/corpus-analysis/query-builder/structural-attribute-builder-functions.js',
|
||||
'js/corpus-analysis/query-builder/token-attribute-builder-functions.js',
|
||||
'js/corpus-analysis/app.js',
|
||||
'js/corpus-analysis/concordance-extension.js',
|
||||
'js/corpus-analysis/reader-extension.js',
|
||||
'js/corpus-analysis/static-visualization-extension.js'
|
||||
'js/corpus-analysis/index.js',
|
||||
'js/corpus-analysis/cqi/index.js',
|
||||
'js/corpus-analysis/cqi/constants.js',
|
||||
'js/corpus-analysis/cqi/errors.js',
|
||||
'js/corpus-analysis/cqi/status.js',
|
||||
'js/corpus-analysis/cqi/api/index.js',
|
||||
'js/corpus-analysis/cqi/api/client.js',
|
||||
'js/corpus-analysis/cqi/models/index.js',
|
||||
'js/corpus-analysis/cqi/models/resource.js',
|
||||
'js/corpus-analysis/cqi/models/attributes.js',
|
||||
'js/corpus-analysis/cqi/models/subcorpora.js',
|
||||
'js/corpus-analysis/cqi/models/corpora.js',
|
||||
'js/corpus-analysis/cqi/client.js',
|
||||
'js/corpus-analysis/query-builder/index.js',
|
||||
'js/corpus-analysis/query-builder/element-references.js',
|
||||
'js/corpus-analysis/query-builder/query-builder.js',
|
||||
'js/corpus-analysis/query-builder/structural-attribute-builder-functions.js',
|
||||
'js/corpus-analysis/query-builder/token-attribute-builder-functions.js',
|
||||
'js/corpus-analysis/app.js',
|
||||
'js/corpus-analysis/concordance-extension.js',
|
||||
'js/corpus-analysis/reader-extension.js',
|
||||
'js/corpus-analysis/static-visualization-extension.js'
|
||||
-%}
|
||||
<script src="{{ ASSET_URL }}"></script>
|
||||
{% endassets -%}
|
||||
|
||||
{# TODO: Think about implementing the following inside a main.js(.j2) #}
|
||||
<script>
|
||||
// TODO: Implement an app.run method and use this for all of the following
|
||||
const app = new nopaque.App();
|
||||
app.init();
|
||||
var app;
|
||||
var currentUserId;
|
||||
|
||||
{% if current_user.is_authenticated -%}
|
||||
// TODO: Set this as a property of the app object
|
||||
const currentUserId = {{ current_user.hashid|tojson }};
|
||||
async function main() {
|
||||
app = new nopaque.app.Client();
|
||||
app.init();
|
||||
|
||||
// Subscribe to the current user's data events
|
||||
app.subscribeUser(currentUserId)
|
||||
.catch((error) => {throw JSON.stringify(error);});
|
||||
{% if not current_user.is_authenticated %}
|
||||
const currentUserId = null;
|
||||
{% else %}
|
||||
currentUserId = {{ current_user.hashid|tojson }};
|
||||
|
||||
// Get the current user's data
|
||||
app.getUser(currentUserId, true, true)
|
||||
.catch((error) => {throw JSON.stringify(error);});
|
||||
try {
|
||||
await app.userHub.add(currentUserId);
|
||||
} catch (error) {
|
||||
app.ui.flash('Failed to load user data.', 'error');
|
||||
}
|
||||
|
||||
{% if not current_user.terms_of_use_accepted -%}
|
||||
M.Modal.getInstance(document.querySelector('#terms-of-use-modal')).open();
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
|
||||
// Display flashed messages
|
||||
for (let [category, message] of {{ get_flashed_messages(with_categories=True)|tojson }}) {
|
||||
app.flash(message, message);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
let languageModalSwitch = document.querySelector('#terms-of-use-modal-switch');
|
||||
let termsOfUseModalContent = document.querySelectorAll('.terms-of-use-modal-content');
|
||||
if (languageModalSwitch) {
|
||||
languageModalSwitch.addEventListener('change', function() {
|
||||
termsOfUseModalContent.forEach(content => {
|
||||
content.classList.toggle('hide');
|
||||
});
|
||||
{% if not current_user.terms_of_use_accepted %}
|
||||
const termsOfUseAcceptButtonElement = document.querySelector('#terms-of-use-modal-accept-button');
|
||||
termsOfUseAcceptButtonElement.addEventListener('click', async () => {
|
||||
try {
|
||||
await app.main.acceptTermsOfUse();
|
||||
app.ui.flash('Terms of use accepted.');
|
||||
} catch (error) {
|
||||
app.ui.flash('Failed to accept terms of use.', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
const termsOfUseModalElement = document.querySelector('#terms-of-use-modal');
|
||||
const termsOfUseModal = M.Modal.getInstance(termsOfUseModalElement);
|
||||
|
||||
termsOfUseModal.open();
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
const flashedMessages = {{ get_flashed_messages(with_categories=true)|tojson }};
|
||||
|
||||
for (let [category, message] of flashedMessages) {
|
||||
app.ui.flash(message, category);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
</script>
|
||||
|
@ -1,4 +1,5 @@
|
||||
<ul class="sidenav {{ 'sidenav-fixed' if sidenav_fixed else '' }}" id="sidenav">
|
||||
<ul class="sidenav" id="sidenav">
|
||||
{% if current_user.is_authenticated %}
|
||||
{# user view #}
|
||||
<li>
|
||||
<div class="user-view">
|
||||
@ -8,70 +9,117 @@
|
||||
<a><span class="white-text email">{{ current_user.email }}</span></a>
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{# general items #}
|
||||
<li {% if request.path == url_for('main.news') %}class="active"{% endif %}>
|
||||
<a class="waves-effect" href="{{ url_for('main.news') }}"><i class="material-icons">email</i>News</a>
|
||||
</li>
|
||||
{% if current_user.is_authenticated %}
|
||||
{# dashboard #}
|
||||
<li {% if request.path == url_for('main.dashboard') %}class="active"{% endif %}>
|
||||
<a class="waves-effect" href="{{ url_for('main.dashboard') }}"><i class="material-icons">dashboard</i>Dashboard</a>
|
||||
</li>
|
||||
|
||||
{# contributions #}
|
||||
<li {% if request.path == url_for('contributions.index') %}class="active"{% endif %}>
|
||||
<a href="{{ url_for('contributions.index') }}">
|
||||
<i class="material-icons left">new_label</i>
|
||||
Contributions
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{# social #}
|
||||
<li {% if request.path == url_for('main.social') %}class="active"{% endif %}>
|
||||
<a class="waves-effect" href="{{ url_for('main.social') }}"><i class="material-icons">groups</i>Social</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{# news #}
|
||||
<li {% if request.path == url_for('main.news') %}class="active"{% endif %}>
|
||||
<a class="waves-effect" href="{{ url_for('main.news') }}"><i class="material-icons">newspaper</i>News</a>
|
||||
</li>
|
||||
|
||||
{# manual #}
|
||||
<li {% if request.path == url_for('main.manual') %}class="active"{% endif %}>
|
||||
<a class="waves-effect" href="{{ url_for('main.manual') }}"><i class="material-icons">help_outline</i>Manual</a>
|
||||
</li>
|
||||
|
||||
{# processes & services items #}
|
||||
{% if current_user.is_authenticated %}
|
||||
{# data processing & analysis section #}
|
||||
<li><div class="divider"></div></li>
|
||||
<li><a class="subheader">Data Processing & Analysis</a></li>
|
||||
|
||||
{# file setup pipeline #}
|
||||
<li class="service-color service-color-border border-darken" data-service="file-setup-pipeline" style="border-left: 10px solid;">
|
||||
<a class="waves-effect" href="{{ url_for('services.file_setup_pipeline') }}"><i class="nopaque-icons service-icons" data-service="file-setup-pipeline"></i>File setup</a>
|
||||
</li>
|
||||
|
||||
{# tesseract ocr pipeline #}
|
||||
<li class="service-color service-color-border border-darken mt-1" data-service="tesseract-ocr-pipeline" style="border-left: 10px solid;">
|
||||
<a class="waves-effect" href="{{ url_for('services.tesseract_ocr_pipeline') }}"><i class="nopaque-icons service-icons" data-service="tesseract-ocr-pipeline"></i>OCR</a>
|
||||
</li>
|
||||
|
||||
{% if config.NOPAQUE_TRANSKRIBUS_ENABLED %}
|
||||
{# transkribus htr pipeline #}
|
||||
<li class="service-color service-color-border border-darken mt-1" data-service="transkribus-htr-pipeline" style="border-left: 10px solid;">
|
||||
<a class="waves-effect" href="{{ url_for('services.transkribus_htr_pipeline') }}"><i class="nopaque-icons service-icons" data-service="transkribus-htr-pipeline"></i>HTR</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{# spacy nlp pipeline #}
|
||||
<li class="service-color service-color-border border-darken mt-1" data-service="spacy-nlp-pipeline" style="border-left: 10px solid;">
|
||||
<a class="waves-effect" href="{{ url_for('services.spacy_nlp_pipeline') }}"><i class="nopaque-icons service-icons" data-service="spacy-nlp-pipeline"></i>NLP</a>
|
||||
</li>
|
||||
|
||||
{# corpus analysis #}
|
||||
<li class="service-color service-color-border border-darken mt-1" data-service="corpus-analysis" style="border-left: 10px solid;">
|
||||
<a class="waves-effect" href="{{ url_for('services.corpus_analysis') }}"><i class="nopaque-icons service-icons" data-service="corpus-analysis"></i>Corpus Analysis</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{# account items #}
|
||||
<li class="hide-on-large-only"><div class="divider"></div></li>
|
||||
<li class="hide-on-large-only"><a class="subheader">Account</a></li>
|
||||
<li>
|
||||
{# account section #}
|
||||
<li><div class="divider"></div></li>
|
||||
<li><a class="subheader">Account</a></li>
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
{# my profile #}
|
||||
<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>My Profile</a>
|
||||
</li>
|
||||
<li class="hide-on-large-only">
|
||||
<a class="waves-effect" href="{{ url_for('settings.settings') }}"><i class="material-icons">settings</i>Settings</a>
|
||||
</li>
|
||||
<li class="hide-on-large-only">
|
||||
<a class="waves-effect" href="{{ url_for('auth.logout') }}"><i class="material-icons">logout</i>Log out</a>
|
||||
|
||||
{# settings #}
|
||||
<li {% if request.path == url_for('settings.index') %}class="active"{% endif %}>
|
||||
<a class="waves-effect" href="{{ url_for('settings.index') }}"><i class="material-icons">settings</i>Settings</a>
|
||||
</li>
|
||||
|
||||
{# administration items #}
|
||||
{% if current_user.can('ADMINISTRATE') %}
|
||||
<li><div class="divider"></div></li>
|
||||
<li><a class="subheader">Administration</a></li>
|
||||
{# log out #}
|
||||
<li>
|
||||
<a class="waves-effect" href="{{ url_for('admin.corpora') }}"><i class="nopaque-icons">I</i>Corpora</a>
|
||||
<a class="waves-effect" href="{{ url_for('auth.logout') }}"><i class="material-icons">logout</i>Log out</a>
|
||||
</li>
|
||||
{% else %}
|
||||
{# log in #}
|
||||
<li {% if request.path == url_for('auth.login') %}class="active"{% endif %}>
|
||||
<a href="{{ url_for('auth.login') }}">Log in</a>
|
||||
</li>
|
||||
|
||||
{# register #}
|
||||
<li {% if request.path == url_for('auth.register') %}class="active"{% endif %}>
|
||||
<a href="{{ url_for('auth.register') }}" class="btn waves-effect waves-light">Register</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.can('USE_API') or current_user.is_administrator %}
|
||||
<li><div class="divider"></div></li>
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.can('USE_API') %}
|
||||
{# API #}
|
||||
<li>
|
||||
<a class="waves-effect" href="{{ url_for('admin.users') }}"><i class="material-icons">manage_accounts</i>Users</a>
|
||||
<a class="waves-effect" href="{{ url_for('apifairy.docs') }}"><i class="material-icons">api</i>API</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.is_administrator %}
|
||||
{# Administration #}
|
||||
<li>
|
||||
<a class="waves-effect" href="{{ url_for('admin.index') }}"><i class="material-icons">admin_panel_settings</i>Administration</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
@ -1,21 +1,23 @@
|
||||
<link href="{{ url_for('static', filename='external/material-design-icons/css/material-icons.css') }}" rel="stylesheet">
|
||||
{% assets
|
||||
output='gen/nopaque.%(version)s.css',
|
||||
'css/materialize.css',
|
||||
'css/materialize.override.css',
|
||||
'css/nopaque-icons.css',
|
||||
'css/theme-colors.css',
|
||||
'css/corpus-status-colors.css',
|
||||
'css/corpus-status-text.css',
|
||||
'css/job-status-colors.css',
|
||||
'css/job-status-text.css',
|
||||
'css/service-colors.css',
|
||||
'css/pagination.css',
|
||||
'css/service-icons.css',
|
||||
'css/s-attr-colors.css',
|
||||
'css/spacing.css',
|
||||
'css/status-spinner.css',
|
||||
'css/utils.css'
|
||||
-%}
|
||||
output='gen/nopaque.%(version)s.css',
|
||||
'css/materialize.css',
|
||||
'css/materialize.override.css',
|
||||
'css/nopaque-icons.css',
|
||||
'css/theme-colors.css',
|
||||
'css/corpus-status-colors.css',
|
||||
'css/corpus-status-text.css',
|
||||
'css/height.css',
|
||||
'css/job-status-colors.css',
|
||||
'css/job-status-text.css',
|
||||
'css/service-colors.css',
|
||||
'css/pagination.css',
|
||||
'css/service-icons.css',
|
||||
'css/s-attr-colors.css',
|
||||
'css/spacing.css',
|
||||
'css/status-spinner.css',
|
||||
'css/utils.css',
|
||||
'css/width.css'
|
||||
%}
|
||||
<link href="{{ ASSET_URL }}" rel="stylesheet">
|
||||
{% endassets -%}
|
||||
{% endassets %}
|
||||
|
@ -1,43 +0,0 @@
|
||||
{% extends "base.html.j2" %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col s12">
|
||||
<h1 id="title">{{ title }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="col s12 l4">
|
||||
<div class="card hoverable">
|
||||
<a href="{{ url_for('.users') }}" style="position: absolute; width: 100%; height: 100%;"></a>
|
||||
<div class="card-content">
|
||||
<span class="card-title"><i class="material-icons left">group</i>Users</span>
|
||||
<p>Edit the individual user accounts. You have the following options:</p>
|
||||
<ul>
|
||||
<li>- View, edit and delete user accounts</li>
|
||||
<li>- View, edit and delete user corpora</li>
|
||||
<li>- View, edit and delete user jobs</li>
|
||||
<li>- View, edit and delete user added Tesseract models</li>
|
||||
<li>- View, edit and delete user added SpaCy models</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col s12 l4">
|
||||
<div class="card hoverable">
|
||||
<a href="{{ url_for('.corpora') }}" style="position: absolute; width: 100%; height: 100%;"></a>
|
||||
<div class="card-content">
|
||||
<span class="card-title"><i class="nopaque-icons left">I</i>Corpora</span>
|
||||
<p>Edit all Corpora. You have the following options:</p>
|
||||
<ul>
|
||||
<li>- View, edit and delete corpora</li>
|
||||
<li>- View, edit and delete corpus jobs</li>
|
||||
<li>- Edit corpus follower roles and the public status of the corpus</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock page_content %}
|
@ -1,29 +0,0 @@
|
||||
{% extends "base.html.j2" %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="container">
|
||||
<h1 id="title">{{ title }}</h1>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div class="corpus-list no-autoinit" id="corpus-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock page_content %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
let corpusListElement = document.querySelector('#corpus-list');
|
||||
let corpusList = new nopaque.resource_lists.CorpusList(corpusListElement);
|
||||
corpusList.add(
|
||||
[
|
||||
{% for corpus in corpora %}
|
||||
{{ corpus.to_json_serializeable(backrefs=True,relationships=True)|tojson }},
|
||||
{% endfor %}
|
||||
]
|
||||
);
|
||||
</script>
|
||||
{% endblock scripts %}
|
@ -1,104 +0,0 @@
|
||||
{% extends "base.html.j2" %}
|
||||
{% import "wtf.html.j2" as wtf %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col s12">
|
||||
<h1 id="title">{{ title }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="col s12">
|
||||
<form method="POST">
|
||||
{{ edit_profile_settings_form.hidden_tag() }}
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<span class="card-title">General settings</span>
|
||||
{{ wtf.render_field(edit_profile_settings_form.username, material_icon='person') }}
|
||||
{{ wtf.render_field(edit_profile_settings_form.email, material_icon='email') }}
|
||||
</div>
|
||||
<div class="card-action">
|
||||
<div class="right-align">
|
||||
{{ wtf.render_field(edit_profile_settings_form.submit, material_icon='send') }}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="POST">
|
||||
{{ edit_notification_settings_form.hidden_tag() }}
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<span class="card-title">Notification settings</span>
|
||||
{{ wtf.render_field(edit_notification_settings_form.job_status_mail_notification_level, material_icon='notifications') }}
|
||||
</div>
|
||||
<div class="card-action">
|
||||
<div class="right-align">
|
||||
{{ wtf.render_field(edit_notification_settings_form.submit, material_icon='send') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="POST">
|
||||
{{ admin_edit_user_form.hidden_tag() }}
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<span class="card-title">Administrator settings</span>
|
||||
{{ wtf.render_field(admin_edit_user_form.role, material_icon='swap_vert') }}
|
||||
<div class="row">
|
||||
<div class="col s12"><p> </p></div>
|
||||
<div class="col s1">
|
||||
<p><i class="material-icons">check</i></p>
|
||||
</div>
|
||||
<div class="col s8">
|
||||
<p>{{ admin_edit_user_form.confirmed.label.text }}</p>
|
||||
<p class="light">Change confirmation status manually.</p>
|
||||
</div>
|
||||
<div class="col s3 right-align">
|
||||
<div class="switch">
|
||||
<label>
|
||||
{{ admin_edit_user_form.confirmed() }}
|
||||
<span class="lever"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-action right-align">
|
||||
{{ wtf.render_field(admin_edit_user_form.submit, material_icon='send') }}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<span class="card-title">Delete account</span>
|
||||
<p>Deleting an account has the following effects:</p>
|
||||
<ul>
|
||||
<li>All data associated with your corpora and jobs will be permanently deleted.</li>
|
||||
<li>All settings will be permanently deleted.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-action right-align">
|
||||
<a href="#delete-account-modal" class="btn modal-trigger red waves-effect waves-light"><i class="material-icons left">delete</i>Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock page_content %}
|
||||
|
||||
{% block modals %}
|
||||
{{ super() }}
|
||||
<div class="modal" id="delete-account-modal">
|
||||
<div class="modal-content">
|
||||
<h4>Confirm deletion</h4>
|
||||
<p>Do you really want to delete your account and all associated data? All associated corpora, jobs and files will be permanently deleted!</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
|
||||
<a href="{{ url_for('.delete_user', user_id=user.id) }}" class="btn red waves-effect waves-light"><i class="material-icons left">delete</i>Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock modals %}
|
@ -1,131 +0,0 @@
|
||||
{% extends "base.html.j2" %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col s12 l2">
|
||||
<p> </p>
|
||||
{# <img src="{{ url_for('users.user_avatar', user_id=user.id) }}" alt="user-image" class="circle responsive-img"> #}
|
||||
</div>
|
||||
<div class="col s12 l10">
|
||||
<h1 id="title">{{ title }}</h1>
|
||||
<p>
|
||||
<span class="chip hoverable tooltipped no-autoinit" id="user-role-chip">{{ user.role.name }}</span>
|
||||
{% if user.confirmed %}
|
||||
<span class="chip white-text" id="user-confirmed-chip" style="background-color: #4caf50;">confirmed</span>
|
||||
{% else %}
|
||||
<span class="chip white-text" id="user-confirmed-chip" style="background-color: #f44336;">unconfirmed</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if user.about_me %}
|
||||
<p>{{ user.about_me }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col s12 hide-on-med-and-down"> </div>
|
||||
|
||||
<div class="col s12">
|
||||
<ul class="tabs tabs-fixed-width z-depth-1">
|
||||
<li class="tab"><a href="#user-info">User info</a></li>
|
||||
<li class="tab"><a href="#user-corpora">Corpora</a></li>
|
||||
<li class="tab"><a href="#user-jobs">Jobs</a></li>
|
||||
<li class="tab"><a href="#user-tesseract-ocr-pipeline-models">Tesseract OCR Pipeline Models</a></li>
|
||||
<li class="tab"><a href="#user-spacy-nlp-pipeline-models">SpaCy NLP Pipeline Models</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="col s12" id="user-info">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<ul>
|
||||
<li>Username: {{ user.username }}</li>
|
||||
<li>Email: {{ user.email }}</li>
|
||||
<li>Id: {{ user.id }}</li>
|
||||
<li>Hashid: {{ user.hashid }}</li>
|
||||
<li>Member since: {{ user.member_since.strftime('%Y-%m-%d') }}</li>
|
||||
<li>Last seen: {% if user.last_seen %}{{ user.last_seen.strftime('%Y-%m-%d') }}</li>{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-action right-align">
|
||||
<a class="btn waves-effect waves-light" href="{{ url_for('.user_settings', user_id=user.id) }}"><i class="material-icons left">edit</i>Edit</a>
|
||||
<a class="btn red modal-trigger waves-effect waves-light" data-target="delete-user-modal"><i class="material-icons left">delete</i>Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col s12" id="user-corpora">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div class="corpus-list" data-user-id="{{ user.hashid }}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col s12" id="user-jobs">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div class="job-list" data-user-id="{{ user.hashid }}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col s12" id="user-spacy-nlp-pipeline-models">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div class="spacy-nlp-pipeline-model-list" data-user-id="{{ user.hashid }}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col s12" id="user-tesseract-ocr-pipeline-models">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div class="tesseract-ocr-pipeline-model-list" data-user-id="{{ user.hashid }}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock page_content %}
|
||||
|
||||
|
||||
{% block modals %}
|
||||
{{ super() }}
|
||||
<div id="delete-user-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3>Delete user</h3>
|
||||
<p>Do you really want to delete the user {{ user.username }}? All associated data will be permanently deleted!</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a class="btn modal-close waves-effect waves-light">Cancel</a>
|
||||
<a class="btn red modal-close waves-effect waves-light"><i class="material-icons left">delete</i>Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock modals %}
|
||||
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
let userRoleChip = document.querySelector('#user-role-chip');
|
||||
let userRoleChipTooltip = M.Tooltip.init(
|
||||
userRoleChip,
|
||||
{
|
||||
html: `
|
||||
<p>Permissions</p>
|
||||
<p class="left-align">
|
||||
{% for permission in ['ADMINISTRATE', 'CONTRIBUTE', 'USE_API'] %}
|
||||
<label>
|
||||
<input class="filled-in" type="checkbox" {{ 'checked' if user.can(permission) }}>
|
||||
<span>{{ permission|capitalize }}</span>
|
||||
</label>
|
||||
{% if not loop.last %}
|
||||
<br>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
`.trim()
|
||||
}
|
||||
);
|
||||
</script>
|
||||
{% endblock scripts %}
|
@ -1,66 +0,0 @@
|
||||
{% extends "users/settings/settings.html.j2" %}
|
||||
|
||||
{% block admin_settings %}
|
||||
<div class="col s12"></div>
|
||||
|
||||
<div class="col s12 l4">
|
||||
<h4>Administrator Settings</h4>
|
||||
<p>Here the Confirmation Status of the user can be set manually and a special role can be assigned.</p>
|
||||
</div>
|
||||
<div class="col s12 l8">
|
||||
<br>
|
||||
<ul class="collapsible no-autoinit settings-collapsible">
|
||||
<li>
|
||||
<div class="collapsible-header" style="justify-content: space-between;">
|
||||
<span>Confirmation status</span>
|
||||
<i class="caret material-icons">keyboard_arrow_right</i>
|
||||
</div>
|
||||
<div class="collapsible-body">
|
||||
<div style="overflow: auto;">
|
||||
<p class="left"><i class="material-icons">check</i></p>
|
||||
<p class="left" style="margin-left: 10px;">
|
||||
Confirmed<br>
|
||||
<span class="light">Change confirmation status manually.</span>
|
||||
</p>
|
||||
<br class="hide-on-med-and-down">
|
||||
<div class="switch right">
|
||||
<label>
|
||||
<input {% if user.confirmed %}checked{% endif %} id="user-confirmed-switch" type="checkbox">
|
||||
<span class="lever"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="collapsible-header" style="justify-content: space-between;">
|
||||
<span>Role</span>
|
||||
<i class="caret material-icons">keyboard_arrow_right</i>
|
||||
</div>
|
||||
<div class="collapsible-body">
|
||||
<form method="POST">
|
||||
{{ update_user_form.hidden_tag() }}
|
||||
{{ wtf.render_field(update_user_form.role, material_icon='manage_accounts') }}
|
||||
<div class="right-align">
|
||||
{{ wtf.render_field(update_user_form.submit, material_icon='send') }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock admin_settings %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
let userConfirmedSwitchElement = document.querySelector('#user-confirmed-switch');
|
||||
userConfirmedSwitchElement.addEventListener('change', (event) => {
|
||||
let newConfirmed = userConfirmedSwitchElement.checked;
|
||||
nopaque.requests.admin.users.entity.confirmed.update({{ user.hashid|tojson }}, newConfirmed)
|
||||
.catch((response) => {
|
||||
userConfirmedSwitchElement.checked = !userConfirmedSwitchElement;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock scripts %}
|
@ -1,34 +0,0 @@
|
||||
{% extends "base.html.j2" %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col s12">
|
||||
<h1 id="title">{{ title }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="col s12">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div class="admin-user-list no-autoinit" id="admin-user-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock page_content %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
let adminUserListElement = document.querySelector('#admin-user-list');
|
||||
let adminUserList = new nopaque.resource_lists.AdminUserList(adminUserListElement);
|
||||
adminUserList.add(
|
||||
[
|
||||
{% for user in users %}
|
||||
{{ user.to_json_serializeable(backrefs=True)|tojson }},
|
||||
{% endfor %}
|
||||
]
|
||||
);
|
||||
</script>
|
||||
{% endblock scripts %}
|
@ -5,7 +5,7 @@
|
||||
{% block page_content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col s12 m8 offset-m2">
|
||||
<div class="col s12 l8 offset-l2">
|
||||
<h1 id="title">{{ title }}</h1>
|
||||
<p>Want to boost your research and get going? Nopaque is free and no download is needed. <a href="{{ url_for('.register') }}">Register now</a>!</p>
|
||||
|
||||
@ -15,14 +15,14 @@
|
||||
{{ wtf.render_field(form.user, material_icon='person') }}
|
||||
{{ wtf.render_field(form.password, material_icon='vpn_key') }}
|
||||
<div class="row">
|
||||
<div class="col s6 left-align">
|
||||
<a href="{{ url_for('.reset_password_request') }}">Forgot your password?</a>
|
||||
</div>
|
||||
<div class="col s6 right-align">
|
||||
<div class="col s12 l6">
|
||||
{{ wtf.render_field(form.remember_me) }}
|
||||
</div>
|
||||
<div class="col s12 l6 right-align">
|
||||
<a class="mr-3" href="{{ url_for('.reset_password_request') }}">Forgot your password?</a>
|
||||
{{ wtf.render_field(form.submit, material_icon='send') }}
|
||||
</div>
|
||||
</div>
|
||||
{{ wtf.render_field(form.submit, material_icon='send', class_='width-100') }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user