mirror of
https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
synced 2025-07-01 10:20:34 +00:00
Compare commits
22 Commits
df2bffe0fd
...
developmen
Author | SHA1 | Date | |
---|---|---|---|
56844e0898 | |||
c28d534942 | |||
80604bf8de | |||
d4cd313940 | |||
c405061574 | |||
6c1f48eb2f | |||
cda28910f5 | |||
9a805b9d14 | |||
16bf891654 | |||
cb53b27ebf | |||
6684257bc4 | |||
0d1805fb76 | |||
bb60a2ba67 | |||
328f85ba52 | |||
93344c9573 | |||
1372c86609 | |||
713a7645db | |||
0c64c07925 | |||
a6ddf4c980 | |||
cab5f7ea05 | |||
07f09cdbd9 | |||
c97b2a886e |
@ -3,6 +3,7 @@ from config import Config
|
|||||||
from docker import DockerClient
|
from docker import DockerClient
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask.logging import default_handler
|
from flask.logging import default_handler
|
||||||
|
from flask_admin import Admin
|
||||||
from flask_apscheduler import APScheduler
|
from flask_apscheduler import APScheduler
|
||||||
from flask_assets import Environment
|
from flask_assets import Environment
|
||||||
from flask_login import LoginManager
|
from flask_login import LoginManager
|
||||||
@ -15,10 +16,12 @@ from flask_sqlalchemy import SQLAlchemy
|
|||||||
from flask_hashids import Hashids
|
from flask_hashids import Hashids
|
||||||
from logging import Formatter, StreamHandler
|
from logging import Formatter, StreamHandler
|
||||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
|
from .extensions.nopaque_flask_admin_views import AdminIndexView, ModelView
|
||||||
|
|
||||||
|
|
||||||
docker_client = DockerClient.from_env()
|
docker_client = DockerClient.from_env()
|
||||||
|
|
||||||
|
admin = Admin()
|
||||||
apifairy = APIFairy()
|
apifairy = APIFairy()
|
||||||
assets = Environment()
|
assets = Environment()
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
@ -74,6 +77,7 @@ def create_app(config: Config = Config) -> Flask:
|
|||||||
|
|
||||||
from .models import AnonymousUser, User
|
from .models import AnonymousUser, User
|
||||||
|
|
||||||
|
admin.init_app(app, index_view=AdminIndexView())
|
||||||
apifairy.init_app(app)
|
apifairy.init_app(app)
|
||||||
assets.init_app(app)
|
assets.init_app(app)
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
@ -92,9 +96,6 @@ def create_app(config: Config = Config) -> Flask:
|
|||||||
# endregion Extensions
|
# endregion Extensions
|
||||||
|
|
||||||
# region Blueprints
|
# region Blueprints
|
||||||
from .blueprints.admin import bp as admin_blueprint
|
|
||||||
app.register_blueprint(admin_blueprint, url_prefix='/admin')
|
|
||||||
|
|
||||||
from .blueprints.api import bp as api_blueprint
|
from .blueprints.api import bp as api_blueprint
|
||||||
app.register_blueprint(api_blueprint, url_prefix='/api')
|
app.register_blueprint(api_blueprint, url_prefix='/api')
|
||||||
|
|
||||||
@ -127,14 +128,15 @@ def create_app(config: Config = Config) -> Flask:
|
|||||||
|
|
||||||
from .blueprints.workshops import bp as workshops_blueprint
|
from .blueprints.workshops import bp as workshops_blueprint
|
||||||
app.register_blueprint(workshops_blueprint, url_prefix='/workshops')
|
app.register_blueprint(workshops_blueprint, url_prefix='/workshops')
|
||||||
|
|
||||||
|
from .models import _models
|
||||||
|
for model in _models:
|
||||||
|
admin.add_view(ModelView(model, db.session, category='Database'))
|
||||||
# endregion Blueprints
|
# endregion Blueprints
|
||||||
|
|
||||||
# region SocketIO Namespaces
|
# region SocketIO Namespaces
|
||||||
from .namespaces.cqi_over_sio import CQiOverSocketIONamespace
|
from .namespaces.cqi_over_sio import CQiOverSocketIONamespace
|
||||||
socketio.on_namespace(CQiOverSocketIONamespace('/cqi_over_sio'))
|
socketio.on_namespace(CQiOverSocketIONamespace('/cqi_over_sio'))
|
||||||
|
|
||||||
from .namespaces.users import UsersNamespace
|
|
||||||
socketio.on_namespace(UsersNamespace('/users'))
|
|
||||||
# endregion SocketIO Namespaces
|
# endregion SocketIO Namespaces
|
||||||
|
|
||||||
# region Database event Listeners
|
# region Database event Listeners
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
from flask import Blueprint
|
|
||||||
from flask_login import login_required
|
|
||||||
from app.decorators import admin_required
|
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint('admin', __name__)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.before_request
|
|
||||||
@login_required
|
|
||||||
@admin_required
|
|
||||||
def before_request():
|
|
||||||
'''
|
|
||||||
Ensures that the routes in this package can be visited only by users with
|
|
||||||
administrator privileges (login_required and admin_required).
|
|
||||||
'''
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
from . import json_routes, routes
|
|
@ -1,16 +0,0 @@
|
|||||||
from flask_wtf import FlaskForm
|
|
||||||
from wtforms import SelectField, SubmitField
|
|
||||||
from app.models import Role
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateUserForm(FlaskForm):
|
|
||||||
role = SelectField('Role')
|
|
||||||
submit = SubmitField()
|
|
||||||
|
|
||||||
def __init__(self, user, *args, **kwargs):
|
|
||||||
if 'data' not in kwargs:
|
|
||||||
kwargs['data'] = {'role': user.role.hashid}
|
|
||||||
if 'prefix' not in kwargs:
|
|
||||||
kwargs['prefix'] = 'update-user-form'
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.role.choices = [(x.hashid, x.name) for x in Role.query.all()]
|
|
@ -1,23 +0,0 @@
|
|||||||
from flask import abort, request
|
|
||||||
from app.decorators import content_negotiation
|
|
||||||
from app import db
|
|
||||||
from app.models import User
|
|
||||||
from . import bp
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/users/<hashid:user_id>/confirmed', methods=['PUT'])
|
|
||||||
@content_negotiation(consumes='application/json', produces='application/json')
|
|
||||||
def update_user_role(user_id):
|
|
||||||
confirmed = request.json
|
|
||||||
if not isinstance(confirmed, bool):
|
|
||||||
abort(400)
|
|
||||||
user = User.query.get_or_404(user_id)
|
|
||||||
user.confirmed = confirmed
|
|
||||||
db.session.commit()
|
|
||||||
response_data = {
|
|
||||||
'message': (
|
|
||||||
f'User "{user.username}" is now '
|
|
||||||
f'{"confirmed" if confirmed else "unconfirmed"}'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return response_data, 200
|
|
@ -1,136 +0,0 @@
|
|||||||
from flask import abort, flash, redirect, render_template, url_for
|
|
||||||
from app import db, hashids
|
|
||||||
from app.models import Avatar, Corpus, Role, User
|
|
||||||
from app.blueprints.users.settings.forms import (
|
|
||||||
UpdateAvatarForm,
|
|
||||||
UpdatePasswordForm,
|
|
||||||
UpdateNotificationsForm,
|
|
||||||
UpdateAccountInformationForm,
|
|
||||||
UpdateProfileInformationForm
|
|
||||||
)
|
|
||||||
from . import bp
|
|
||||||
from .forms import UpdateUserForm
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('')
|
|
||||||
def admin():
|
|
||||||
return render_template(
|
|
||||||
'admin/admin.html.j2',
|
|
||||||
title='Administration'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/corpora')
|
|
||||||
def corpora():
|
|
||||||
corpora = Corpus.query.all()
|
|
||||||
return render_template(
|
|
||||||
'admin/corpora.html.j2',
|
|
||||||
title='Corpora',
|
|
||||||
corpora=corpora
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/users')
|
|
||||||
def users():
|
|
||||||
users = User.query.all()
|
|
||||||
return render_template(
|
|
||||||
'admin/users.html.j2',
|
|
||||||
title='Users',
|
|
||||||
users=users
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/users/<hashid:user_id>')
|
|
||||||
def user(user_id):
|
|
||||||
user = User.query.get_or_404(user_id)
|
|
||||||
corpora = Corpus.query.filter(Corpus.user == user).all()
|
|
||||||
return render_template(
|
|
||||||
'admin/user.html.j2',
|
|
||||||
title=user.username,
|
|
||||||
user=user,
|
|
||||||
corpora=corpora
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/users/<hashid:user_id>/settings', methods=['GET', 'POST'])
|
|
||||||
def user_settings(user_id):
|
|
||||||
user = User.query.get_or_404(user_id)
|
|
||||||
update_account_information_form = UpdateAccountInformationForm(user)
|
|
||||||
update_profile_information_form = UpdateProfileInformationForm(user)
|
|
||||||
update_avatar_form = UpdateAvatarForm()
|
|
||||||
update_password_form = UpdatePasswordForm(user)
|
|
||||||
update_notifications_form = UpdateNotificationsForm(user)
|
|
||||||
update_user_form = UpdateUserForm(user)
|
|
||||||
|
|
||||||
# region handle update profile information form
|
|
||||||
if update_profile_information_form.submit.data and update_profile_information_form.validate():
|
|
||||||
user.about_me = update_profile_information_form.about_me.data
|
|
||||||
user.location = update_profile_information_form.location.data
|
|
||||||
user.organization = update_profile_information_form.organization.data
|
|
||||||
user.website = update_profile_information_form.website.data
|
|
||||||
user.full_name = update_profile_information_form.full_name.data
|
|
||||||
db.session.commit()
|
|
||||||
flash('Your changes have been saved')
|
|
||||||
return redirect(url_for('.user_settings', user_id=user.id))
|
|
||||||
# endregion handle update profile information form
|
|
||||||
|
|
||||||
# region handle update avatar form
|
|
||||||
if update_avatar_form.submit.data and update_avatar_form.validate():
|
|
||||||
try:
|
|
||||||
Avatar.create(
|
|
||||||
update_avatar_form.avatar.data,
|
|
||||||
user=user
|
|
||||||
)
|
|
||||||
except (AttributeError, OSError):
|
|
||||||
abort(500)
|
|
||||||
db.session.commit()
|
|
||||||
flash('Your changes have been saved')
|
|
||||||
return redirect(url_for('.user_settings', user_id=user.id))
|
|
||||||
# endregion handle update avatar form
|
|
||||||
|
|
||||||
# region handle update account information form
|
|
||||||
if update_account_information_form.submit.data and update_account_information_form.validate():
|
|
||||||
user.email = update_account_information_form.email.data
|
|
||||||
user.username = update_account_information_form.username.data
|
|
||||||
db.session.commit()
|
|
||||||
flash('Profile settings updated')
|
|
||||||
return redirect(url_for('.user_settings', user_id=user.id))
|
|
||||||
# endregion handle update account information form
|
|
||||||
|
|
||||||
# region handle update password form
|
|
||||||
if update_password_form.submit.data and update_password_form.validate():
|
|
||||||
user.password = update_password_form.new_password.data
|
|
||||||
db.session.commit()
|
|
||||||
flash('Your changes have been saved')
|
|
||||||
return redirect(url_for('.user_settings', user_id=user.id))
|
|
||||||
# endregion handle update password form
|
|
||||||
|
|
||||||
# region handle update notifications form
|
|
||||||
if update_notifications_form.submit.data and update_notifications_form.validate():
|
|
||||||
user.setting_job_status_mail_notification_level = \
|
|
||||||
update_notifications_form.job_status_mail_notification_level.data
|
|
||||||
db.session.commit()
|
|
||||||
flash('Your changes have been saved')
|
|
||||||
return redirect(url_for('.user_settings', user_id=user.id))
|
|
||||||
# endregion handle update notifications form
|
|
||||||
|
|
||||||
# region handle update user form
|
|
||||||
if update_user_form.submit.data and update_user_form.validate():
|
|
||||||
role_id = hashids.decode(update_user_form.role.data)
|
|
||||||
user.role = Role.query.get(role_id)
|
|
||||||
db.session.commit()
|
|
||||||
flash('Your changes have been saved')
|
|
||||||
return redirect(url_for('.user_settings', user_id=user.id))
|
|
||||||
# endregion handle update user form
|
|
||||||
|
|
||||||
return render_template(
|
|
||||||
'admin/user_settings.html.j2',
|
|
||||||
title='Settings',
|
|
||||||
update_account_information_form=update_account_information_form,
|
|
||||||
update_avatar_form=update_avatar_form,
|
|
||||||
update_notifications_form=update_notifications_form,
|
|
||||||
update_password_form=update_password_form,
|
|
||||||
update_profile_information_form=update_profile_information_form,
|
|
||||||
update_user_form=update_user_form,
|
|
||||||
user=user
|
|
||||||
)
|
|
@ -19,11 +19,9 @@ def before_request():
|
|||||||
and request.endpoint
|
and request.endpoint
|
||||||
and request.blueprint != 'auth'
|
and request.blueprint != 'auth'
|
||||||
and request.endpoint != 'static'
|
and request.endpoint != 'static'
|
||||||
|
and request.endpoint != 'main.accept_terms_of_use'
|
||||||
):
|
):
|
||||||
return redirect(url_for('auth.unconfirmed'))
|
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
|
from . import routes
|
||||||
|
@ -16,4 +16,4 @@ def before_request():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
from . import cli, files, followers, routes, json_routes
|
from . import cli, files, followers, routes
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
from flask_login import current_user
|
|
||||||
from flask_socketio import join_room
|
|
||||||
from app import hashids, socketio
|
|
||||||
from app.decorators import socketio_login_required
|
|
||||||
from app.models import Corpus
|
|
||||||
|
|
||||||
|
|
||||||
@socketio.on('GET /corpora/<corpus_id>')
|
|
||||||
@socketio_login_required
|
|
||||||
def get_corpus(corpus_hashid):
|
|
||||||
corpus_id = hashids.decode(corpus_hashid)
|
|
||||||
corpus = Corpus.query.get(corpus_id)
|
|
||||||
if corpus is None:
|
|
||||||
return {'options': {'status': 404, 'statusText': 'Not found'}}
|
|
||||||
if not (
|
|
||||||
corpus.is_public
|
|
||||||
or corpus.user == current_user
|
|
||||||
or current_user.is_administrator
|
|
||||||
):
|
|
||||||
return {'options': {'status': 403, 'statusText': 'Forbidden'}}
|
|
||||||
return {
|
|
||||||
'body': corpus.to_json_serializable(),
|
|
||||||
'options': {
|
|
||||||
'status': 200,
|
|
||||||
'statusText': 'OK',
|
|
||||||
'headers': {'Content-Type: application/json'}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@socketio.on('SUBSCRIBE /corpora/<corpus_id>')
|
|
||||||
@socketio_login_required
|
|
||||||
def subscribe_corpus(corpus_hashid):
|
|
||||||
corpus_id = hashids.decode(corpus_hashid)
|
|
||||||
corpus = Corpus.query.get(corpus_id)
|
|
||||||
if corpus is None:
|
|
||||||
return {'options': {'status': 404, 'statusText': 'Not found'}}
|
|
||||||
if not (
|
|
||||||
corpus.is_public
|
|
||||||
or corpus.user == current_user
|
|
||||||
or current_user.is_administrator
|
|
||||||
):
|
|
||||||
return {'options': {'status': 403, 'statusText': 'Forbidden'}}
|
|
||||||
join_room(f'/corpora/{corpus.hashid}')
|
|
||||||
return {'options': {'status': 200, 'statusText': 'OK'}}
|
|
@ -1,125 +0,0 @@
|
|||||||
from datetime import datetime
|
|
||||||
from flask import abort, current_app, request, url_for
|
|
||||||
from flask_login import current_user
|
|
||||||
from threading import Thread
|
|
||||||
from app import db
|
|
||||||
from app.decorators import content_negotiation
|
|
||||||
from app.models import Corpus, CorpusFollowerRole
|
|
||||||
from . import bp
|
|
||||||
from .decorators import corpus_follower_permission_required, corpus_owner_or_admin_required
|
|
||||||
import nltk
|
|
||||||
from string import punctuation
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:corpus_id>', methods=['DELETE'])
|
|
||||||
@corpus_owner_or_admin_required
|
|
||||||
@content_negotiation(produces='application/json')
|
|
||||||
def delete_corpus(corpus_id):
|
|
||||||
def _delete_corpus(app, corpus_id):
|
|
||||||
with app.app_context():
|
|
||||||
corpus = Corpus.query.get(corpus_id)
|
|
||||||
corpus.delete()
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
corpus = Corpus.query.get_or_404(corpus_id)
|
|
||||||
thread = Thread(
|
|
||||||
target=_delete_corpus,
|
|
||||||
args=(current_app._get_current_object(), corpus.id)
|
|
||||||
)
|
|
||||||
thread.start()
|
|
||||||
response_data = {
|
|
||||||
'message': f'Corpus "{corpus.title}" marked for deletion',
|
|
||||||
'category': 'corpus'
|
|
||||||
}
|
|
||||||
return response_data, 200
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:corpus_id>/build', methods=['POST'])
|
|
||||||
@corpus_follower_permission_required('MANAGE_FILES')
|
|
||||||
@content_negotiation(produces='application/json')
|
|
||||||
def build_corpus(corpus_id):
|
|
||||||
def _build_corpus(app, corpus_id):
|
|
||||||
with app.app_context():
|
|
||||||
corpus = Corpus.query.get(corpus_id)
|
|
||||||
corpus.build()
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
corpus = Corpus.query.get_or_404(corpus_id)
|
|
||||||
if len(corpus.files.all()) == 0:
|
|
||||||
abort(409)
|
|
||||||
thread = Thread(
|
|
||||||
target=_build_corpus,
|
|
||||||
args=(current_app._get_current_object(), corpus_id)
|
|
||||||
)
|
|
||||||
thread.start()
|
|
||||||
response_data = {
|
|
||||||
'message': f'Corpus "{corpus.title}" marked for building',
|
|
||||||
'category': 'corpus'
|
|
||||||
}
|
|
||||||
return response_data, 202
|
|
||||||
|
|
||||||
@bp.route('/stopwords')
|
|
||||||
@content_negotiation(produces='application/json')
|
|
||||||
def get_stopwords():
|
|
||||||
nltk.download('stopwords', quiet=True)
|
|
||||||
languages = ["german", "english", "catalan", "greek", "spanish", "french", "italian", "russian", "chinese"]
|
|
||||||
stopwords = {}
|
|
||||||
for language in languages:
|
|
||||||
stopwords[language] = nltk.corpus.stopwords.words(language)
|
|
||||||
stopwords['punctuation'] = list(punctuation) + ['—', '|', '–', '“', '„', '--']
|
|
||||||
stopwords['user_stopwords'] = []
|
|
||||||
response_data = stopwords
|
|
||||||
return response_data, 202
|
|
||||||
|
|
||||||
@bp.route('/<hashid:corpus_id>/generate-share-link', methods=['POST'])
|
|
||||||
@corpus_follower_permission_required('MANAGE_FOLLOWERS')
|
|
||||||
@content_negotiation(consumes='application/json', produces='application/json')
|
|
||||||
def generate_corpus_share_link(corpus_id):
|
|
||||||
data = request.json
|
|
||||||
if not isinstance(data, dict):
|
|
||||||
abort(400)
|
|
||||||
expiration = data.get('expiration')
|
|
||||||
if not isinstance(expiration, str):
|
|
||||||
abort(400)
|
|
||||||
role_name = data.get('role')
|
|
||||||
if not isinstance(role_name, str):
|
|
||||||
abort(400)
|
|
||||||
expiration_date = datetime.strptime(expiration, '%b %d, %Y')
|
|
||||||
cfr = CorpusFollowerRole.query.filter_by(name=role_name).first()
|
|
||||||
if cfr is None:
|
|
||||||
abort(400)
|
|
||||||
corpus = Corpus.query.get_or_404(corpus_id)
|
|
||||||
token = current_user.generate_follow_corpus_token(corpus.hashid, role_name, expiration_date)
|
|
||||||
corpus_share_link = url_for(
|
|
||||||
'corpora.follow_corpus',
|
|
||||||
corpus_id=corpus_id,
|
|
||||||
token=token,
|
|
||||||
_external=True
|
|
||||||
)
|
|
||||||
response_data = {
|
|
||||||
'message': 'Corpus share link generated',
|
|
||||||
'category': 'corpus',
|
|
||||||
'corpusShareLink': corpus_share_link
|
|
||||||
}
|
|
||||||
return response_data, 200
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:corpus_id>/is_public', methods=['PUT'])
|
|
||||||
@corpus_owner_or_admin_required
|
|
||||||
@content_negotiation(consumes='application/json', produces='application/json')
|
|
||||||
def update_corpus_is_public(corpus_id):
|
|
||||||
is_public = request.json
|
|
||||||
if not isinstance(is_public, bool):
|
|
||||||
abort(400)
|
|
||||||
corpus = Corpus.query.get_or_404(corpus_id)
|
|
||||||
corpus.is_public = is_public
|
|
||||||
db.session.commit()
|
|
||||||
response_data = {
|
|
||||||
'message': (
|
|
||||||
f'Corpus "{corpus.title}" is now'
|
|
||||||
f' {"public" if is_public else "private"}'
|
|
||||||
),
|
|
||||||
'category': 'corpus'
|
|
||||||
}
|
|
||||||
return response_data, 200
|
|
@ -1,5 +1,19 @@
|
|||||||
from flask import abort, flash, redirect, render_template, url_for
|
from datetime import datetime
|
||||||
|
from flask import (
|
||||||
|
abort,
|
||||||
|
current_app,
|
||||||
|
flash,
|
||||||
|
Flask,
|
||||||
|
jsonify,
|
||||||
|
redirect,
|
||||||
|
request,
|
||||||
|
render_template,
|
||||||
|
url_for
|
||||||
|
)
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
from string import punctuation
|
||||||
|
from threading import Thread
|
||||||
|
import nltk
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import (
|
from app.models import (
|
||||||
Corpus,
|
Corpus,
|
||||||
@ -12,6 +26,21 @@ from .decorators import corpus_follower_permission_required
|
|||||||
from .forms import CreateCorpusForm
|
from .forms import CreateCorpusForm
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_corpus(app: Flask, corpus_id: int):
|
||||||
|
with app.app_context():
|
||||||
|
corpus: Corpus = Corpus.query.get(corpus_id)
|
||||||
|
corpus.delete()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def _build_corpus(app: Flask, corpus_id: int):
|
||||||
|
with app.app_context():
|
||||||
|
corpus = Corpus.query.get(corpus_id)
|
||||||
|
corpus.build()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
@bp.route('')
|
@bp.route('')
|
||||||
def corpora():
|
def corpora():
|
||||||
return redirect(url_for('main.dashboard', _anchor='corpora'))
|
return redirect(url_for('main.dashboard', _anchor='corpora'))
|
||||||
@ -20,6 +49,7 @@ def corpora():
|
|||||||
@bp.route('/create', methods=['GET', 'POST'])
|
@bp.route('/create', methods=['GET', 'POST'])
|
||||||
def create_corpus():
|
def create_corpus():
|
||||||
form = CreateCorpusForm()
|
form = CreateCorpusForm()
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
try:
|
try:
|
||||||
corpus = Corpus.create(
|
corpus = Corpus.create(
|
||||||
@ -30,8 +60,10 @@ def create_corpus():
|
|||||||
except OSError:
|
except OSError:
|
||||||
abort(500)
|
abort(500)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
flash(f'Corpus "{corpus.title}" created', 'corpus')
|
flash(f'Corpus "{corpus.title}" created', 'corpus')
|
||||||
return redirect(corpus.url)
|
return redirect(corpus.url)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'corpora/create.html.j2',
|
'corpora/create.html.j2',
|
||||||
title='Create corpus',
|
title='Create corpus',
|
||||||
@ -40,12 +72,14 @@ def create_corpus():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:corpus_id>')
|
@bp.route('/<hashid:corpus_id>')
|
||||||
def corpus(corpus_id):
|
def corpus(corpus_id: int):
|
||||||
corpus = Corpus.query.get_or_404(corpus_id)
|
corpus = Corpus.query.get_or_404(corpus_id)
|
||||||
cfrs = CorpusFollowerRole.query.all()
|
|
||||||
# TODO: Better solution for filtering admin
|
cfa = CorpusFollowerAssociation.query.filter_by(
|
||||||
users = User.query.filter(User.is_public == True, User.id != current_user.id, User.id != corpus.user.id, User.role_id < 4).all()
|
corpus_id=corpus_id,
|
||||||
cfa = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=current_user.id).first()
|
follower_id=current_user.id
|
||||||
|
).first()
|
||||||
|
|
||||||
if cfa is None:
|
if cfa is None:
|
||||||
if corpus.user == current_user or current_user.is_administrator:
|
if corpus.user == current_user or current_user.is_administrator:
|
||||||
cfr = CorpusFollowerRole.query.filter_by(name='Administrator').first()
|
cfr = CorpusFollowerRole.query.filter_by(name='Administrator').first()
|
||||||
@ -53,7 +87,21 @@ def corpus(corpus_id):
|
|||||||
cfr = CorpusFollowerRole.query.filter_by(name='Anonymous').first()
|
cfr = CorpusFollowerRole.query.filter_by(name='Anonymous').first()
|
||||||
else:
|
else:
|
||||||
cfr = cfa.role
|
cfr = cfa.role
|
||||||
if corpus.user == current_user or current_user.is_administrator:
|
|
||||||
|
cfrs = CorpusFollowerRole.query.all()
|
||||||
|
|
||||||
|
# TODO: Better solution for filtering admin
|
||||||
|
users = User.query.filter(
|
||||||
|
User.is_public == True,
|
||||||
|
User.id != current_user.id,
|
||||||
|
User.id != corpus.user.id,
|
||||||
|
User.role_id < 4
|
||||||
|
).all()
|
||||||
|
|
||||||
|
if (
|
||||||
|
corpus.user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
return render_template(
|
return render_template(
|
||||||
'corpora/corpus.html.j2',
|
'corpora/corpus.html.j2',
|
||||||
title=corpus.title,
|
title=corpus.title,
|
||||||
@ -62,8 +110,15 @@ def corpus(corpus_id):
|
|||||||
cfrs=cfrs,
|
cfrs=cfrs,
|
||||||
users=users
|
users=users
|
||||||
)
|
)
|
||||||
if (current_user.is_following_corpus(corpus) or corpus.is_public):
|
|
||||||
cfas = CorpusFollowerAssociation.query.filter(Corpus.id == corpus_id, CorpusFollowerAssociation.follower_id != corpus.user.id).all()
|
if (
|
||||||
|
current_user.is_following_corpus(corpus)
|
||||||
|
or corpus.is_public
|
||||||
|
):
|
||||||
|
cfas = CorpusFollowerAssociation.query.filter(
|
||||||
|
Corpus.id == corpus_id,
|
||||||
|
CorpusFollowerAssociation.follower_id != corpus.user.id
|
||||||
|
).all()
|
||||||
return render_template(
|
return render_template(
|
||||||
'corpora/public_corpus.html.j2',
|
'corpora/public_corpus.html.j2',
|
||||||
title=corpus.title,
|
title=corpus.title,
|
||||||
@ -73,14 +128,110 @@ def corpus(corpus_id):
|
|||||||
cfas=cfas,
|
cfas=cfas,
|
||||||
users=users
|
users=users
|
||||||
)
|
)
|
||||||
|
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<hashid:corpus_id>', methods=['DELETE'])
|
||||||
|
def delete_corpus(corpus_id: int):
|
||||||
|
corpus = Corpus.query.get_or_404(corpus_id)
|
||||||
|
|
||||||
|
if not (
|
||||||
|
corpus.user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
thread = Thread(
|
||||||
|
target=_delete_corpus,
|
||||||
|
args=(current_app._get_current_object(), corpus.id)
|
||||||
|
)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
return jsonify(f'Corpus "{corpus.title}" marked for deletion.'), 202
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<hashid:corpus_id>/build', methods=['POST'])
|
||||||
|
def build_corpus(corpus_id: int):
|
||||||
|
corpus = Corpus.query.get_or_404(corpus_id)
|
||||||
|
|
||||||
|
cfa = CorpusFollowerAssociation.query.filter_by(
|
||||||
|
corpus_id=corpus_id,
|
||||||
|
follower_id=current_user.id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not (
|
||||||
|
cfa is not None and cfa.role.has_permission('MANAGE_FILES')
|
||||||
|
or corpus.user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
if len(corpus.files.all()) == 0:
|
||||||
|
abort(409)
|
||||||
|
|
||||||
|
thread = Thread(
|
||||||
|
target=_build_corpus,
|
||||||
|
args=(current_app._get_current_object(), corpus.id)
|
||||||
|
)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
return jsonify(f'Corpus "{corpus.title}" marked for building.'), 202
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<hashid:corpus_id>/create-share-link', methods=['POST'])
|
||||||
|
def create_share_link(corpus_id: int):
|
||||||
|
data = request.json
|
||||||
|
|
||||||
|
expiration_date = data['expiration_date']
|
||||||
|
if not isinstance(expiration_date, str):
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
role_name = data['role_name']
|
||||||
|
if not isinstance(role_name, str):
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
corpus = Corpus.query.get_or_404(corpus_id)
|
||||||
|
|
||||||
|
cfa = CorpusFollowerAssociation.query.filter_by(
|
||||||
|
corpus_id=corpus_id,
|
||||||
|
follower_id=current_user.id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not (
|
||||||
|
cfa is not None and cfa.role.has_permission('MANAGE_FOLLOWERS')
|
||||||
|
or corpus.user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
_expiration_date = datetime.strptime(expiration_date, '%b %d, %Y')
|
||||||
|
|
||||||
|
cfr = CorpusFollowerRole.query.filter_by(name=role_name).first()
|
||||||
|
if cfr is None:
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
token = current_user.generate_follow_corpus_token(
|
||||||
|
corpus.hashid,
|
||||||
|
role_name,
|
||||||
|
_expiration_date
|
||||||
|
)
|
||||||
|
|
||||||
|
corpus_share_link = url_for(
|
||||||
|
'corpora.follow_corpus',
|
||||||
|
corpus_id=corpus_id,
|
||||||
|
token=token,
|
||||||
|
_external=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return jsonify(corpus_share_link)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:corpus_id>/analysis')
|
@bp.route('/<hashid:corpus_id>/analysis')
|
||||||
@corpus_follower_permission_required('VIEW')
|
@corpus_follower_permission_required('VIEW')
|
||||||
def analysis(corpus_id):
|
def analysis(corpus_id: int):
|
||||||
corpus = Corpus.query.get_or_404(corpus_id)
|
corpus = Corpus.query.get_or_404(corpus_id)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'corpora/analysis.html.j2',
|
'corpora/analysis.html.j2',
|
||||||
corpus=corpus,
|
corpus=corpus,
|
||||||
@ -88,22 +239,61 @@ def analysis(corpus_id):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<hashid:corpus_id>/analysis/stopwords')
|
||||||
|
def get_stopwords(corpus_id: int):
|
||||||
|
languages = [
|
||||||
|
'german',
|
||||||
|
'english',
|
||||||
|
'catalan',
|
||||||
|
'greek',
|
||||||
|
'spanish',
|
||||||
|
'french',
|
||||||
|
'italian',
|
||||||
|
'russian',
|
||||||
|
'chinese'
|
||||||
|
]
|
||||||
|
|
||||||
|
nltk.download('stopwords', quiet=True)
|
||||||
|
stopwords = {
|
||||||
|
language: nltk.corpus.stopwords.words(language)
|
||||||
|
for language in languages
|
||||||
|
}
|
||||||
|
stopwords['punctuation'] = list(punctuation)
|
||||||
|
stopwords['punctuation'] += ['—', '|', '–', '“', '„', '--']
|
||||||
|
stopwords['user_stopwords'] = []
|
||||||
|
|
||||||
|
return jsonify(stopwords)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:corpus_id>/follow/<token>')
|
@bp.route('/<hashid:corpus_id>/follow/<token>')
|
||||||
def follow_corpus(corpus_id, token):
|
def follow_corpus(corpus_id: int, token: str):
|
||||||
corpus = Corpus.query.get_or_404(corpus_id)
|
corpus = Corpus.query.get_or_404(corpus_id)
|
||||||
if current_user.follow_corpus_by_token(token):
|
|
||||||
db.session.commit()
|
if not current_user.follow_corpus_by_token(token):
|
||||||
flash(f'You are following "{corpus.title}" now', category='corpus')
|
abort(403)
|
||||||
return redirect(url_for('corpora.corpus', corpus_id=corpus_id))
|
|
||||||
abort(403)
|
db.session.commit()
|
||||||
|
|
||||||
|
flash(f'You are following "{corpus.title}" now', category='corpus')
|
||||||
|
return redirect(corpus.url)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/import', methods=['GET', 'POST'])
|
@bp.route('/<hashid:corpus_id>/is-public', methods=['PUT'])
|
||||||
def import_corpus():
|
def update_is_public(corpus_id):
|
||||||
abort(503)
|
new_value = request.json
|
||||||
|
if not isinstance(new_value, bool):
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
corpus = Corpus.query.get_or_404(corpus_id)
|
||||||
|
|
||||||
|
if not (
|
||||||
|
corpus.user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:corpus_id>/export')
|
corpus.is_public = new_value
|
||||||
@corpus_follower_permission_required('VIEW')
|
db.session.commit()
|
||||||
def export_corpus(corpus_id):
|
|
||||||
abort(503)
|
return jsonify(f'Corpus "{corpus.title}" is now {"public" if new_value else "private"}'), 200
|
||||||
|
@ -4,11 +4,17 @@ from . import bp
|
|||||||
|
|
||||||
|
|
||||||
@bp.app_errorhandler(HTTPException)
|
@bp.app_errorhandler(HTTPException)
|
||||||
def handle_http_exception(error):
|
def handle_http_exception(e: HTTPException):
|
||||||
''' Generic HTTP exception handler '''
|
''' Generic HTTP exception handler '''
|
||||||
accept_json = request.accept_mimetypes.accept_json
|
accept_json = request.accept_mimetypes.accept_json
|
||||||
accept_html = request.accept_mimetypes.accept_html
|
accept_html = request.accept_mimetypes.accept_html
|
||||||
|
|
||||||
if accept_json and not accept_html:
|
if accept_json and not accept_html:
|
||||||
response = jsonify(str(error))
|
error = {
|
||||||
return response, error.code
|
'code': e.code,
|
||||||
return render_template('errors/error.html.j2', error=error), error.code
|
'name': e.name,
|
||||||
|
'description': e.description
|
||||||
|
}
|
||||||
|
return jsonify(error), e.code
|
||||||
|
|
||||||
|
return render_template('errors/error.html.j2', error=e), e.code
|
||||||
|
@ -1,18 +1,13 @@
|
|||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
from flask_login import login_required
|
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint('jobs', __name__)
|
bp = Blueprint('jobs', __name__)
|
||||||
|
|
||||||
|
|
||||||
@bp.before_request
|
from . import routes
|
||||||
@login_required
|
|
||||||
def before_request():
|
|
||||||
'''
|
|
||||||
Ensures that the routes in this package can only be visited by users that
|
|
||||||
are logged in.
|
|
||||||
'''
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
from .inputs import bp as inputs_bp
|
||||||
|
bp.register_blueprint(inputs_bp, url_prefix='/<hashid:job_id>/inputs')
|
||||||
|
|
||||||
from . import routes, json_routes
|
from .results import bp as results_bp
|
||||||
|
bp.register_blueprint(results_bp, url_prefix='/<hashid:job_id>/results')
|
||||||
|
7
app/blueprints/jobs/inputs/__init__.py
Normal file
7
app/blueprints/jobs/inputs/__init__.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
|
||||||
|
bp = Blueprint('inputs', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
from . import routes
|
27
app/blueprints/jobs/inputs/routes.py
Normal file
27
app/blueprints/jobs/inputs/routes.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from flask import abort, send_from_directory
|
||||||
|
from flask_login import current_user, login_required
|
||||||
|
from app.models import JobInput
|
||||||
|
from . import bp
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<hashid:job_input_id>/download')
|
||||||
|
@login_required
|
||||||
|
def download_job_input(job_id: int, job_input_id: int):
|
||||||
|
job_input = JobInput.query.filter_by(
|
||||||
|
job_id=job_id,
|
||||||
|
id=job_input_id
|
||||||
|
).first_or_404()
|
||||||
|
|
||||||
|
if not (
|
||||||
|
job_input.job.user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
return send_from_directory(
|
||||||
|
job_input.path.parent,
|
||||||
|
job_input.path.name,
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=job_input.filename,
|
||||||
|
mimetype=job_input.mimetype
|
||||||
|
)
|
@ -1,72 +0,0 @@
|
|||||||
from flask import abort, current_app
|
|
||||||
from flask_login import current_user
|
|
||||||
from threading import Thread
|
|
||||||
from app import db
|
|
||||||
from app.decorators import admin_required, content_negotiation
|
|
||||||
from app.models import Job, JobStatus
|
|
||||||
from . import bp
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:job_id>', methods=['DELETE'])
|
|
||||||
@content_negotiation(produces='application/json')
|
|
||||||
def delete_job(job_id):
|
|
||||||
def _delete_job(app, job_id):
|
|
||||||
with app.app_context():
|
|
||||||
job = Job.query.get(job_id)
|
|
||||||
job.delete()
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
job = Job.query.get_or_404(job_id)
|
|
||||||
if not (job.user == current_user or current_user.is_administrator):
|
|
||||||
abort(403)
|
|
||||||
thread = Thread(
|
|
||||||
target=_delete_job,
|
|
||||||
args=(current_app._get_current_object(), job_id)
|
|
||||||
)
|
|
||||||
thread.start()
|
|
||||||
response_data = {
|
|
||||||
'message': f'Job "{job.title}" marked for deletion'
|
|
||||||
}
|
|
||||||
return response_data, 202
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:job_id>/log')
|
|
||||||
@admin_required
|
|
||||||
@content_negotiation(produces='application/json')
|
|
||||||
def job_log(job_id):
|
|
||||||
job = Job.query.get_or_404(job_id)
|
|
||||||
if job.status not in [JobStatus.COMPLETED, JobStatus.FAILED]:
|
|
||||||
response = {'errors': {'message': 'Job status is not completed or failed'}}
|
|
||||||
return response, 409
|
|
||||||
with open(job.path / 'pipeline_data' / 'logs' / 'pyflow_log.txt') as log_file:
|
|
||||||
log = log_file.read()
|
|
||||||
response_data = {
|
|
||||||
'jobLog': log
|
|
||||||
}
|
|
||||||
return response_data, 200
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:job_id>/restart', methods=['POST'])
|
|
||||||
@content_negotiation(produces='application/json')
|
|
||||||
def restart_job(job_id):
|
|
||||||
def _restart_job(app, job_id):
|
|
||||||
with app.app_context():
|
|
||||||
job = Job.query.get(job_id)
|
|
||||||
job.restart()
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
job = Job.query.get_or_404(job_id)
|
|
||||||
if not (job.user == current_user or current_user.is_administrator):
|
|
||||||
abort(403)
|
|
||||||
if job.status == JobStatus.FAILED:
|
|
||||||
response = {'errors': {'message': 'Job status is not "failed"'}}
|
|
||||||
return response, 409
|
|
||||||
thread = Thread(
|
|
||||||
target=_restart_job,
|
|
||||||
args=(current_app._get_current_object(), job_id)
|
|
||||||
)
|
|
||||||
thread.start()
|
|
||||||
response_data = {
|
|
||||||
'message': f'Job "{job.title}" marked for restarting'
|
|
||||||
}
|
|
||||||
return response_data, 202
|
|
7
app/blueprints/jobs/results/__init__.py
Normal file
7
app/blueprints/jobs/results/__init__.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
|
||||||
|
bp = Blueprint('results', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
from . import routes
|
27
app/blueprints/jobs/results/routes.py
Normal file
27
app/blueprints/jobs/results/routes.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from flask import abort, send_from_directory
|
||||||
|
from flask_login import current_user, login_required
|
||||||
|
from app.models import JobResult
|
||||||
|
from . import bp
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<hashid:job_result_id>/download')
|
||||||
|
@login_required
|
||||||
|
def download_job_result(job_id: int, job_result_id: int):
|
||||||
|
job_result = JobResult.query.filter_by(
|
||||||
|
job_id=job_id,
|
||||||
|
id=job_result_id
|
||||||
|
).first_or_404()
|
||||||
|
|
||||||
|
if not (
|
||||||
|
job_result.job.user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
return send_from_directory(
|
||||||
|
job_result.path.parent,
|
||||||
|
job_result.path.name,
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=job_result.filename,
|
||||||
|
mimetype=job_result.mimetype
|
||||||
|
)
|
@ -1,25 +1,37 @@
|
|||||||
from flask import (
|
from flask import (
|
||||||
abort,
|
abort,
|
||||||
|
current_app,
|
||||||
|
Flask,
|
||||||
|
jsonify,
|
||||||
redirect,
|
redirect,
|
||||||
render_template,
|
render_template,
|
||||||
send_from_directory,
|
|
||||||
url_for
|
url_for
|
||||||
)
|
)
|
||||||
from flask_login import current_user
|
from flask_login import current_user, login_required
|
||||||
from app.models import Job, JobInput, JobResult
|
from threading import Thread
|
||||||
|
from app import db
|
||||||
|
from app.decorators import admin_required
|
||||||
|
from app.models import Job, JobStatus
|
||||||
from . import bp
|
from . import bp
|
||||||
|
|
||||||
|
|
||||||
@bp.route('')
|
@bp.route('')
|
||||||
def jobs():
|
@login_required
|
||||||
|
def index():
|
||||||
return redirect(url_for('main.dashboard', _anchor='jobs'))
|
return redirect(url_for('main.dashboard', _anchor='jobs'))
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:job_id>')
|
@bp.route('/<hashid:job_id>')
|
||||||
def job(job_id):
|
@login_required
|
||||||
|
def job(job_id: int):
|
||||||
job = Job.query.get_or_404(job_id)
|
job = Job.query.get_or_404(job_id)
|
||||||
if not (job.user == current_user or current_user.is_administrator):
|
|
||||||
|
if not (
|
||||||
|
job.user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'jobs/job.html.j2',
|
'jobs/job.html.j2',
|
||||||
title='Job',
|
title='Job',
|
||||||
@ -27,29 +39,73 @@ def job(job_id):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:job_id>/inputs/<hashid:job_input_id>/download')
|
def _delete_job(app: Flask, job_id: int):
|
||||||
def download_job_input(job_id, job_input_id):
|
with app.app_context():
|
||||||
job_input = JobInput.query.filter_by(job_id=job_id, id=job_input_id).first_or_404()
|
job = Job.query.get(job_id)
|
||||||
if not (job_input.job.user == current_user or current_user.is_administrator):
|
job.delete()
|
||||||
abort(403)
|
db.session.commit()
|
||||||
return send_from_directory(
|
|
||||||
job_input.path.parent,
|
|
||||||
job_input.path.name,
|
|
||||||
as_attachment=True,
|
|
||||||
download_name=job_input.filename,
|
|
||||||
mimetype=job_input.mimetype
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:job_id>/results/<hashid:job_result_id>/download')
|
@bp.route('/<hashid:job_id>', methods=['DELETE'])
|
||||||
def download_job_result(job_id, job_result_id):
|
@login_required
|
||||||
job_result = JobResult.query.filter_by(job_id=job_id, id=job_result_id).first_or_404()
|
def delete_job(job_id: int):
|
||||||
if not (job_result.job.user == current_user or current_user.is_administrator):
|
job = Job.query.get_or_404(job_id)
|
||||||
|
|
||||||
|
if not (
|
||||||
|
job.user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
abort(403)
|
abort(403)
|
||||||
return send_from_directory(
|
|
||||||
job_result.path.parent,
|
thread = Thread(
|
||||||
job_result.path.name,
|
target=_delete_job,
|
||||||
as_attachment=True,
|
args=(current_app._get_current_object(), job.id)
|
||||||
download_name=job_result.filename,
|
|
||||||
mimetype=job_result.mimetype
|
|
||||||
)
|
)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
return jsonify(f'Job "{job.title}" marked for deletion.'), 202
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<hashid:job_id>/log')
|
||||||
|
@admin_required
|
||||||
|
def job_log(job_id: int):
|
||||||
|
job = Job.query.get_or_404(job_id)
|
||||||
|
|
||||||
|
if job.status not in [JobStatus.COMPLETED, JobStatus.FAILED]:
|
||||||
|
abort(409)
|
||||||
|
|
||||||
|
log_file_path = job.path / 'pipeline_data' / 'logs' / 'pyflow_log.txt'
|
||||||
|
with log_file_path.open() as log_file:
|
||||||
|
log = log_file.read()
|
||||||
|
|
||||||
|
return jsonify(log)
|
||||||
|
|
||||||
|
|
||||||
|
def _restart_job(app: Flask, job_id: int):
|
||||||
|
with app.app_context():
|
||||||
|
job = Job.query.get(job_id)
|
||||||
|
job.restart()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<hashid:job_id>/restart', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def restart_job(job_id: int):
|
||||||
|
job = Job.query.get_or_404(job_id)
|
||||||
|
|
||||||
|
if not (
|
||||||
|
job.user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
if job.status != JobStatus.FAILED:
|
||||||
|
abort(409)
|
||||||
|
|
||||||
|
thread = Thread(
|
||||||
|
target=_restart_job,
|
||||||
|
args=(current_app._get_current_object(), job.id)
|
||||||
|
)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
return jsonify(f'Job "{job.title}" marked for restarting.'), 202
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
from flask import flash, redirect, render_template, url_for
|
from flask import abort, flash, jsonify, redirect, render_template, url_for
|
||||||
from flask_login import current_user, login_required, login_user
|
from flask_login import current_user, login_required, login_user
|
||||||
from app.blueprints.auth.forms import LoginForm
|
from app.blueprints.auth.forms import LoginForm
|
||||||
from app.models import Corpus, User
|
from app.models import Corpus, User
|
||||||
from . import bp
|
from . import bp
|
||||||
|
from app import db
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/', methods=['GET', 'POST'])
|
@bp.route('/', methods=['GET', 'POST'])
|
||||||
@ -56,7 +57,7 @@ def news():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/privacy_policy')
|
@bp.route('/privacy-policy')
|
||||||
def privacy_policy():
|
def privacy_policy():
|
||||||
return render_template(
|
return render_template(
|
||||||
'main/privacy_policy.html.j2',
|
'main/privacy_policy.html.j2',
|
||||||
@ -64,14 +65,24 @@ def privacy_policy():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/terms_of_use')
|
@bp.route('/terms-of-use')
|
||||||
def terms_of_use():
|
def terms_of_use():
|
||||||
return render_template(
|
return render_template(
|
||||||
'main/terms_of_use.html.j2',
|
'main/terms_of_use.html.j2',
|
||||||
title='Terms of Use'
|
title='Terms of use'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/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')
|
@bp.route('/social')
|
||||||
@login_required
|
@login_required
|
||||||
def social():
|
def social():
|
||||||
|
@ -1,18 +1,7 @@
|
|||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
from flask_login import login_required
|
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint('settings', __name__)
|
bp = Blueprint('settings', __name__)
|
||||||
|
|
||||||
|
|
||||||
@bp.before_request
|
|
||||||
@login_required
|
|
||||||
def before_request():
|
|
||||||
'''
|
|
||||||
Ensures that the routes in this package can only be visited by users that
|
|
||||||
are logged in.
|
|
||||||
'''
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
from . import routes
|
from . import routes
|
||||||
|
@ -39,7 +39,7 @@ class UpdateAccountInformationForm(FlaskForm):
|
|||||||
)
|
)
|
||||||
submit = SubmitField()
|
submit = SubmitField()
|
||||||
|
|
||||||
def __init__(self, user, *args, **kwargs):
|
def __init__(self, user: User, *args, **kwargs):
|
||||||
if 'data' not in kwargs:
|
if 'data' not in kwargs:
|
||||||
kwargs['data'] = user.to_json_serializeable()
|
kwargs['data'] = user.to_json_serializeable()
|
||||||
if 'prefix' not in kwargs:
|
if 'prefix' not in kwargs:
|
||||||
@ -89,7 +89,7 @@ class UpdateProfileInformationForm(FlaskForm):
|
|||||||
)
|
)
|
||||||
submit = SubmitField()
|
submit = SubmitField()
|
||||||
|
|
||||||
def __init__(self, user, *args, **kwargs):
|
def __init__(self, user: User, *args, **kwargs):
|
||||||
if 'data' not in kwargs:
|
if 'data' not in kwargs:
|
||||||
kwargs['data'] = user.to_json_serializeable()
|
kwargs['data'] = user.to_json_serializeable()
|
||||||
if 'prefix' not in kwargs:
|
if 'prefix' not in kwargs:
|
||||||
@ -130,7 +130,7 @@ class UpdatePasswordForm(FlaskForm):
|
|||||||
)
|
)
|
||||||
submit = SubmitField()
|
submit = SubmitField()
|
||||||
|
|
||||||
def __init__(self, user, *args, **kwargs):
|
def __init__(self, user: User, *args, **kwargs):
|
||||||
if 'prefix' not in kwargs:
|
if 'prefix' not in kwargs:
|
||||||
kwargs['prefix'] = 'update-password-form'
|
kwargs['prefix'] = 'update-password-form'
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@ -152,7 +152,7 @@ class UpdateNotificationsForm(FlaskForm):
|
|||||||
)
|
)
|
||||||
submit = SubmitField()
|
submit = SubmitField()
|
||||||
|
|
||||||
def __init__(self, user, *args, **kwargs):
|
def __init__(self, user: User, *args, **kwargs):
|
||||||
if 'data' not in kwargs:
|
if 'data' not in kwargs:
|
||||||
kwargs['data'] = user.to_json_serializeable()
|
kwargs['data'] = user.to_json_serializeable()
|
||||||
if 'prefix' not in kwargs:
|
if 'prefix' not in kwargs:
|
@ -1,10 +1,158 @@
|
|||||||
from flask import g, url_for
|
from flask import (
|
||||||
from flask_login import current_user
|
abort,
|
||||||
from app.blueprints.users.settings.routes import settings as settings_route
|
flash,
|
||||||
|
jsonify,
|
||||||
|
redirect,
|
||||||
|
render_template,
|
||||||
|
request,
|
||||||
|
url_for
|
||||||
|
)
|
||||||
|
from flask_login import current_user, login_required
|
||||||
|
from app import db
|
||||||
|
from app.models import Avatar
|
||||||
from . import bp
|
from . import bp
|
||||||
|
from .forms import (
|
||||||
|
UpdateAvatarForm,
|
||||||
|
UpdatePasswordForm,
|
||||||
|
UpdateNotificationsForm,
|
||||||
|
UpdateAccountInformationForm,
|
||||||
|
UpdateProfileInformationForm
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/settings', methods=['GET', 'POST'])
|
@bp.route('', methods=['GET', 'POST'])
|
||||||
def settings():
|
@login_required
|
||||||
g._nopaque_redirect_location_on_post = url_for('.settings')
|
def index():
|
||||||
return settings_route(current_user.id)
|
update_account_information_form = UpdateAccountInformationForm(current_user)
|
||||||
|
update_profile_information_form = UpdateProfileInformationForm(current_user)
|
||||||
|
update_avatar_form = UpdateAvatarForm()
|
||||||
|
update_password_form = UpdatePasswordForm(current_user)
|
||||||
|
update_notifications_form = UpdateNotificationsForm(current_user)
|
||||||
|
|
||||||
|
# region handle update profile information form
|
||||||
|
if update_profile_information_form.submit.data and update_profile_information_form.validate():
|
||||||
|
current_user.about_me = update_profile_information_form.about_me.data
|
||||||
|
current_user.location = update_profile_information_form.location.data
|
||||||
|
current_user.organization = update_profile_information_form.organization.data
|
||||||
|
current_user.website = update_profile_information_form.website.data
|
||||||
|
current_user.full_name = update_profile_information_form.full_name.data
|
||||||
|
db.session.commit()
|
||||||
|
flash('Your changes have been saved')
|
||||||
|
return redirect(url_for('.index'))
|
||||||
|
# endregion handle update profile information form
|
||||||
|
|
||||||
|
# region handle update avatar form
|
||||||
|
if update_avatar_form.submit.data and update_avatar_form.validate():
|
||||||
|
try:
|
||||||
|
Avatar.create(
|
||||||
|
update_avatar_form.avatar.data,
|
||||||
|
user=current_user
|
||||||
|
)
|
||||||
|
except (AttributeError, OSError):
|
||||||
|
abort(500)
|
||||||
|
db.session.commit()
|
||||||
|
flash('Your changes have been saved')
|
||||||
|
return redirect(url_for('.index'))
|
||||||
|
# endregion handle update avatar form
|
||||||
|
|
||||||
|
# region handle update account information form
|
||||||
|
if update_account_information_form.submit.data and update_account_information_form.validate():
|
||||||
|
current_user.email = update_account_information_form.email.data
|
||||||
|
current_user.username = update_account_information_form.username.data
|
||||||
|
db.session.commit()
|
||||||
|
flash('Profile settings updated')
|
||||||
|
return redirect(url_for('.index'))
|
||||||
|
# endregion handle update account information form
|
||||||
|
|
||||||
|
# region handle update password form
|
||||||
|
if update_password_form.submit.data and update_password_form.validate():
|
||||||
|
current_user.password = update_password_form.new_password.data
|
||||||
|
db.session.commit()
|
||||||
|
flash('Your changes have been saved')
|
||||||
|
return redirect(url_for('.index'))
|
||||||
|
# endregion handle update password form
|
||||||
|
|
||||||
|
# region handle update notifications form
|
||||||
|
if update_notifications_form.submit.data and update_notifications_form.validate():
|
||||||
|
current_user.setting_job_status_mail_notification_level = \
|
||||||
|
update_notifications_form.job_status_mail_notification_level.data
|
||||||
|
db.session.commit()
|
||||||
|
flash('Your changes have been saved')
|
||||||
|
return redirect(url_for('.index'))
|
||||||
|
# endregion handle update notifications form
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'settings/index.html.j2',
|
||||||
|
title='Settings',
|
||||||
|
update_account_information_form=update_account_information_form,
|
||||||
|
update_avatar_form=update_avatar_form,
|
||||||
|
update_notifications_form=update_notifications_form,
|
||||||
|
update_password_form=update_password_form,
|
||||||
|
update_profile_information_form=update_profile_information_form,
|
||||||
|
user=current_user
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/profile-is-public', methods=['PUT'])
|
||||||
|
@login_required
|
||||||
|
def update_profile_is_public():
|
||||||
|
new_value = request.json
|
||||||
|
|
||||||
|
if not isinstance(new_value, bool):
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
current_user.is_public = new_value
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify('Your changes have been saved'), 200
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/profile-show-email', methods=['PUT'])
|
||||||
|
@login_required
|
||||||
|
def update_profile_show_email():
|
||||||
|
new_value = request.json
|
||||||
|
|
||||||
|
if not isinstance(new_value, bool):
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
if new_value:
|
||||||
|
current_user.add_profile_privacy_setting('SHOW_EMAIL')
|
||||||
|
else:
|
||||||
|
current_user.remove_profile_privacy_setting('SHOW_EMAIL')
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify('Your changes have been saved'), 200
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/profile-show-last-seen', methods=['PUT'])
|
||||||
|
@login_required
|
||||||
|
def update_profile_show_last_seen():
|
||||||
|
new_value = request.json
|
||||||
|
|
||||||
|
if not isinstance(new_value, bool):
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
if new_value:
|
||||||
|
current_user.add_profile_privacy_setting('SHOW_LAST_SEEN')
|
||||||
|
else:
|
||||||
|
current_user.remove_profile_privacy_setting('SHOW_LAST_SEEN')
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify('Your changes have been saved'), 200
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/profile-show-member-since', methods=['PUT'])
|
||||||
|
@login_required
|
||||||
|
def update_profile_show_member_since():
|
||||||
|
new_value = request.json
|
||||||
|
|
||||||
|
if not isinstance(new_value, bool):
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
if new_value:
|
||||||
|
current_user.add_profile_privacy_setting('SHOW_MEMBER_SINCE')
|
||||||
|
else:
|
||||||
|
current_user.remove_profile_privacy_setting('SHOW_MEMBER_SINCE')
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify('Your changes have been saved'), 200
|
||||||
|
@ -1,18 +1,7 @@
|
|||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
from flask_login import login_required
|
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint('users', __name__)
|
bp = Blueprint('users', __name__)
|
||||||
|
|
||||||
|
|
||||||
@bp.before_request
|
from . import cli, events, routes
|
||||||
@login_required
|
|
||||||
def before_request():
|
|
||||||
'''
|
|
||||||
Ensures that the routes in this package can only be visited by users that
|
|
||||||
are logged in.
|
|
||||||
'''
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
from . import cli, json_routes, routes, settings
|
|
||||||
|
91
app/blueprints/users/events.py
Normal file
91
app/blueprints/users/events.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
from flask_login import current_user
|
||||||
|
from flask_socketio import join_room, leave_room
|
||||||
|
from app import hashids, socketio
|
||||||
|
from app.decorators import socketio_login_required
|
||||||
|
from app.models import User
|
||||||
|
|
||||||
|
|
||||||
|
@socketio.on('SUBSCRIBE User')
|
||||||
|
@socketio_login_required
|
||||||
|
def subscribe(user_hashid: str) -> dict:
|
||||||
|
if not isinstance(user_hashid, str):
|
||||||
|
return {
|
||||||
|
'code': 400,
|
||||||
|
'name': 'Bad Request',
|
||||||
|
'description': 'Invalid User ID.'
|
||||||
|
}
|
||||||
|
|
||||||
|
user_id = hashids.decode(user_hashid)
|
||||||
|
|
||||||
|
if not isinstance(user_id, int):
|
||||||
|
return {
|
||||||
|
'code': 400,
|
||||||
|
'name': 'Bad Request',
|
||||||
|
'description': 'Invalid User ID.'
|
||||||
|
}
|
||||||
|
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
return {
|
||||||
|
'code': 404,
|
||||||
|
'name': 'Not Found',
|
||||||
|
'description': 'User not found.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if not (
|
||||||
|
user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
'code': 403,
|
||||||
|
'name': 'Forbidden',
|
||||||
|
'description': 'Not allowed to subscribe to this user.'
|
||||||
|
}
|
||||||
|
|
||||||
|
join_room(f'/users/{user.hashid}')
|
||||||
|
|
||||||
|
return {'code': 204, 'name': 'No Content'}
|
||||||
|
|
||||||
|
|
||||||
|
@socketio.on('UNSUBSCRIBE User')
|
||||||
|
@socketio_login_required
|
||||||
|
def unsubscribe(user_hashid: str) -> dict:
|
||||||
|
if not isinstance(user_hashid, str):
|
||||||
|
return {
|
||||||
|
'code': 400,
|
||||||
|
'name': 'Bad Request',
|
||||||
|
'description': 'Invalid User ID.'
|
||||||
|
}
|
||||||
|
|
||||||
|
user_id = hashids.decode(user_hashid)
|
||||||
|
|
||||||
|
if not isinstance(user_id, int):
|
||||||
|
return {
|
||||||
|
'code': 400,
|
||||||
|
'name': 'Bad Request',
|
||||||
|
'description': 'Invalid User ID.'
|
||||||
|
}
|
||||||
|
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
return {
|
||||||
|
'code': 404,
|
||||||
|
'name': 'Not Found',
|
||||||
|
'description': 'User not found.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if not (
|
||||||
|
user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
'code': 403,
|
||||||
|
'name': 'Forbidden',
|
||||||
|
'description': 'Not allowed to unsubscribe from this user.'
|
||||||
|
}
|
||||||
|
|
||||||
|
leave_room(f'/users/{user.hashid}')
|
||||||
|
|
||||||
|
return {'code': 204, 'name': 'No Content'}
|
@ -1,69 +0,0 @@
|
|||||||
from flask import abort, current_app
|
|
||||||
from flask_login import current_user, logout_user
|
|
||||||
from threading import Thread
|
|
||||||
from app import db
|
|
||||||
from app.decorators import content_negotiation
|
|
||||||
from app.models import Avatar, User
|
|
||||||
from . import bp
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:user_id>', methods=['DELETE'])
|
|
||||||
@content_negotiation(produces='application/json')
|
|
||||||
def delete_user(user_id):
|
|
||||||
def _delete_user(app, user_id):
|
|
||||||
with app.app_context():
|
|
||||||
user = User.query.get(user_id)
|
|
||||||
user.delete()
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
user = User.query.get_or_404(user_id)
|
|
||||||
if not (user == current_user or current_user.is_administrator):
|
|
||||||
abort(403)
|
|
||||||
thread = Thread(
|
|
||||||
target=_delete_user,
|
|
||||||
args=(current_app._get_current_object(), user.id)
|
|
||||||
)
|
|
||||||
if user == current_user:
|
|
||||||
logout_user()
|
|
||||||
thread.start()
|
|
||||||
response_data = {
|
|
||||||
'message': f'User "{user.username}" marked for deletion'
|
|
||||||
}
|
|
||||||
return response_data, 202
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:user_id>/avatar', methods=['DELETE'])
|
|
||||||
@content_negotiation(produces='application/json')
|
|
||||||
def delete_user_avatar(user_id):
|
|
||||||
def _delete_avatar(app, avatar_id):
|
|
||||||
with app.app_context():
|
|
||||||
avatar = Avatar.query.get(avatar_id)
|
|
||||||
avatar.delete()
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
user = User.query.get_or_404(user_id)
|
|
||||||
if user.avatar is None:
|
|
||||||
abort(404)
|
|
||||||
if not (user == current_user or current_user.is_administrator):
|
|
||||||
abort(403)
|
|
||||||
thread = Thread(
|
|
||||||
target=_delete_avatar,
|
|
||||||
args=(current_app._get_current_object(), user.avatar.id)
|
|
||||||
)
|
|
||||||
thread.start()
|
|
||||||
response_data = {
|
|
||||||
'message': f'Avatar marked for deletion'
|
|
||||||
}
|
|
||||||
return response_data, 202
|
|
||||||
|
|
||||||
@bp.route('/accept-terms-of-use', methods=['POST'])
|
|
||||||
@content_negotiation(produces='application/json')
|
|
||||||
def accept_terms_of_use():
|
|
||||||
if not (current_user.is_authenticated or current_user.confirmed):
|
|
||||||
abort(403)
|
|
||||||
current_user.terms_of_use_accepted = True
|
|
||||||
db.session.commit()
|
|
||||||
response_data = {
|
|
||||||
'message': 'You accepted the terms of use',
|
|
||||||
}
|
|
||||||
return response_data, 202
|
|
@ -1,25 +1,48 @@
|
|||||||
from flask import (
|
from flask import (
|
||||||
abort,
|
abort,
|
||||||
|
current_app,
|
||||||
|
Flask,
|
||||||
|
jsonify,
|
||||||
redirect,
|
redirect,
|
||||||
render_template,
|
render_template,
|
||||||
|
request,
|
||||||
send_from_directory,
|
send_from_directory,
|
||||||
url_for
|
url_for
|
||||||
)
|
)
|
||||||
from flask_login import current_user
|
from flask_login import current_user, login_required, logout_user
|
||||||
from app.models import User
|
from threading import Thread
|
||||||
|
from app import db
|
||||||
|
from app.models import Avatar, User
|
||||||
from . import bp
|
from . import bp
|
||||||
|
|
||||||
|
|
||||||
@bp.route('')
|
@bp.route('')
|
||||||
def users():
|
@login_required
|
||||||
|
def index():
|
||||||
return redirect(url_for('main.social_area', _anchor='users'))
|
return redirect(url_for('main.social_area', _anchor='users'))
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:user_id>')
|
@bp.route('/<hashid:user_id>')
|
||||||
def user(user_id):
|
@login_required
|
||||||
|
def user(user_id: int):
|
||||||
user = User.query.get_or_404(user_id)
|
user = User.query.get_or_404(user_id)
|
||||||
if not (user.is_public or user == current_user or current_user.is_administrator):
|
|
||||||
|
if not (
|
||||||
|
user.is_public
|
||||||
|
or user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
accept_json = request.accept_mimetypes.accept_json
|
||||||
|
accept_html = request.accept_mimetypes.accept_html
|
||||||
|
|
||||||
|
if accept_json and not accept_html:
|
||||||
|
return user.to_json_serializeable(
|
||||||
|
backrefs=True,
|
||||||
|
relationships=True
|
||||||
|
)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'users/user.html.j2',
|
'users/user.html.j2',
|
||||||
title=user.username,
|
title=user.username,
|
||||||
@ -27,13 +50,51 @@ def user(user_id):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:user_id>/avatar')
|
def _delete_user(app: Flask, user_id: int):
|
||||||
def user_avatar(user_id):
|
with app.app_context():
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
user.delete()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<hashid:user_id>', methods=['DELETE'])
|
||||||
|
@login_required
|
||||||
|
def delete_user(user_id: int):
|
||||||
user = User.query.get_or_404(user_id)
|
user = User.query.get_or_404(user_id)
|
||||||
if not (user.is_public or user == current_user or current_user.is_administrator):
|
|
||||||
|
if not (
|
||||||
|
user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
if user == current_user:
|
||||||
|
logout_user()
|
||||||
|
|
||||||
|
thread = Thread(
|
||||||
|
target=_delete_user,
|
||||||
|
args=(current_app._get_current_object(), user.id)
|
||||||
|
)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
return jsonify(f'User "{user.username}" marked for deletion'), 202
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<hashid:user_id>/avatar')
|
||||||
|
@login_required
|
||||||
|
def user_avatar(user_id: int):
|
||||||
|
user = User.query.get_or_404(user_id)
|
||||||
|
|
||||||
|
if not (
|
||||||
|
user.is_public
|
||||||
|
or user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
abort(403)
|
||||||
|
|
||||||
if user.avatar is None:
|
if user.avatar is None:
|
||||||
return redirect(url_for('static', filename='images/user_avatar.png'))
|
return redirect(url_for('static', filename='images/user_avatar.png'))
|
||||||
|
|
||||||
return send_from_directory(
|
return send_from_directory(
|
||||||
user.avatar.path.parent,
|
user.avatar.path.parent,
|
||||||
user.avatar.path.name,
|
user.avatar.path.name,
|
||||||
@ -41,3 +102,33 @@ def user_avatar(user_id):
|
|||||||
download_name=user.avatar.filename,
|
download_name=user.avatar.filename,
|
||||||
mimetype=user.avatar.mimetype
|
mimetype=user.avatar.mimetype
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_avatar(app: Flask, avatar_id: int):
|
||||||
|
with app.app_context():
|
||||||
|
avatar = Avatar.query.get(avatar_id)
|
||||||
|
avatar.delete()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<hashid:user_id>/avatar', methods=['DELETE'])
|
||||||
|
@login_required
|
||||||
|
def delete_user_avatar(user_id: int):
|
||||||
|
user = User.query.get_or_404(user_id)
|
||||||
|
|
||||||
|
if user.avatar is None:
|
||||||
|
abort(409)
|
||||||
|
|
||||||
|
if not (
|
||||||
|
user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
thread = Thread(
|
||||||
|
target=_delete_avatar,
|
||||||
|
args=(current_app._get_current_object(), user.avatar.id)
|
||||||
|
)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
return jsonify('Avatar marked for deletion'), 202
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
from .. import bp
|
|
||||||
from . import json_routes, routes
|
|
@ -1,49 +0,0 @@
|
|||||||
from flask import abort, request
|
|
||||||
from flask_login import current_user
|
|
||||||
from app import db
|
|
||||||
from app.decorators import content_negotiation
|
|
||||||
from app.models import User, ProfilePrivacySettings
|
|
||||||
from . import bp
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:user_id>/settings/profile-privacy/is-public', methods=['PUT'])
|
|
||||||
@content_negotiation(consumes='application/json', produces='application/json')
|
|
||||||
def update_user_profile_privacy_setting_is_public(user_id):
|
|
||||||
user = User.query.get_or_404(user_id)
|
|
||||||
if not (user == current_user or current_user.is_administrator):
|
|
||||||
abort(403)
|
|
||||||
enabled = request.json
|
|
||||||
if not isinstance(enabled, bool):
|
|
||||||
abort(400)
|
|
||||||
user.is_public = enabled
|
|
||||||
db.session.commit()
|
|
||||||
response_data = {
|
|
||||||
'message': 'Profile privacy settings updated',
|
|
||||||
'category': 'settings'
|
|
||||||
}
|
|
||||||
return response_data, 200
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:user_id>/settings/profile-privacy/<string:profile_privacy_setting_name>', methods=['PUT'])
|
|
||||||
@content_negotiation(consumes='application/json', produces='application/json')
|
|
||||||
def update_user_profile_privacy_settings(user_id, profile_privacy_setting_name):
|
|
||||||
user = User.query.get_or_404(user_id)
|
|
||||||
try:
|
|
||||||
profile_privacy_setting = ProfilePrivacySettings[profile_privacy_setting_name]
|
|
||||||
except KeyError:
|
|
||||||
abort(404)
|
|
||||||
if not (user == current_user or current_user.is_administrator):
|
|
||||||
abort(403)
|
|
||||||
enabled = request.json
|
|
||||||
if not isinstance(enabled, bool):
|
|
||||||
abort(400)
|
|
||||||
if enabled:
|
|
||||||
user.add_profile_privacy_setting(profile_privacy_setting)
|
|
||||||
else:
|
|
||||||
user.remove_profile_privacy_setting(profile_privacy_setting)
|
|
||||||
db.session.commit()
|
|
||||||
response_data = {
|
|
||||||
'message': 'Profile privacy settings updated',
|
|
||||||
'category': 'settings'
|
|
||||||
}
|
|
||||||
return response_data, 200
|
|
@ -1,93 +0,0 @@
|
|||||||
from flask import abort, flash, g, redirect, render_template, url_for
|
|
||||||
from flask_login import current_user
|
|
||||||
from app import db
|
|
||||||
from app.models import Avatar, User
|
|
||||||
from . import bp
|
|
||||||
from .forms import (
|
|
||||||
UpdateAvatarForm,
|
|
||||||
UpdatePasswordForm,
|
|
||||||
UpdateNotificationsForm,
|
|
||||||
UpdateAccountInformationForm,
|
|
||||||
UpdateProfileInformationForm
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:user_id>/settings', methods=['GET', 'POST'])
|
|
||||||
def settings(user_id):
|
|
||||||
user = User.query.get_or_404(user_id)
|
|
||||||
if not (user == current_user or current_user.is_administrator):
|
|
||||||
abort(403)
|
|
||||||
|
|
||||||
redirect_location_on_post = g.pop(
|
|
||||||
'_nopaque_redirect_location_on_post',
|
|
||||||
url_for('.settings', user_id=user_id)
|
|
||||||
)
|
|
||||||
|
|
||||||
update_account_information_form = UpdateAccountInformationForm(user)
|
|
||||||
update_profile_information_form = UpdateProfileInformationForm(user)
|
|
||||||
update_avatar_form = UpdateAvatarForm()
|
|
||||||
update_password_form = UpdatePasswordForm(user)
|
|
||||||
update_notifications_form = UpdateNotificationsForm(user)
|
|
||||||
|
|
||||||
# region handle update profile information form
|
|
||||||
if update_profile_information_form.submit.data and update_profile_information_form.validate():
|
|
||||||
user.about_me = update_profile_information_form.about_me.data
|
|
||||||
user.location = update_profile_information_form.location.data
|
|
||||||
user.organization = update_profile_information_form.organization.data
|
|
||||||
user.website = update_profile_information_form.website.data
|
|
||||||
user.full_name = update_profile_information_form.full_name.data
|
|
||||||
db.session.commit()
|
|
||||||
flash('Your changes have been saved')
|
|
||||||
return redirect(redirect_location_on_post)
|
|
||||||
# endregion handle update profile information form
|
|
||||||
|
|
||||||
# region handle update avatar form
|
|
||||||
if update_avatar_form.submit.data and update_avatar_form.validate():
|
|
||||||
try:
|
|
||||||
Avatar.create(
|
|
||||||
update_avatar_form.avatar.data,
|
|
||||||
user=user
|
|
||||||
)
|
|
||||||
except (AttributeError, OSError):
|
|
||||||
abort(500)
|
|
||||||
db.session.commit()
|
|
||||||
flash('Your changes have been saved')
|
|
||||||
return redirect(redirect_location_on_post)
|
|
||||||
# endregion handle update avatar form
|
|
||||||
|
|
||||||
# region handle update account information form
|
|
||||||
if update_account_information_form.submit.data and update_account_information_form.validate():
|
|
||||||
user.email = update_account_information_form.email.data
|
|
||||||
user.username = update_account_information_form.username.data
|
|
||||||
db.session.commit()
|
|
||||||
flash('Profile settings updated')
|
|
||||||
return redirect(redirect_location_on_post)
|
|
||||||
# endregion handle update account information form
|
|
||||||
|
|
||||||
# region handle update password form
|
|
||||||
if update_password_form.submit.data and update_password_form.validate():
|
|
||||||
user.password = update_password_form.new_password.data
|
|
||||||
db.session.commit()
|
|
||||||
flash('Your changes have been saved')
|
|
||||||
return redirect(redirect_location_on_post)
|
|
||||||
# endregion handle update password form
|
|
||||||
|
|
||||||
# region handle update notifications form
|
|
||||||
if update_notifications_form.submit.data and update_notifications_form.validate():
|
|
||||||
user.setting_job_status_mail_notification_level = \
|
|
||||||
update_notifications_form.job_status_mail_notification_level.data
|
|
||||||
db.session.commit()
|
|
||||||
flash('Your changes have been saved')
|
|
||||||
return redirect(redirect_location_on_post)
|
|
||||||
# endregion handle update notifications form
|
|
||||||
|
|
||||||
return render_template(
|
|
||||||
'users/settings/settings.html.j2',
|
|
||||||
title='Settings',
|
|
||||||
update_account_information_form=update_account_information_form,
|
|
||||||
update_avatar_form=update_avatar_form,
|
|
||||||
update_notifications_form=update_notifications_form,
|
|
||||||
update_password_form=update_password_form,
|
|
||||||
update_profile_information_form=update_profile_information_form,
|
|
||||||
user=user
|
|
||||||
)
|
|
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
|
|
@ -1,14 +1,45 @@
|
|||||||
from .anonymous_user import *
|
from .anonymous_user import AnonymousUser
|
||||||
from .avatar import *
|
from .avatar import Avatar
|
||||||
from .corpus_file import *
|
from .corpus_file import CorpusFile
|
||||||
from .corpus_follower_association import *
|
from .corpus_follower_association import CorpusFollowerAssociation
|
||||||
from .corpus_follower_role import *
|
from .corpus_follower_role import CorpusFollowerPermission, CorpusFollowerRole
|
||||||
from .corpus import *
|
from .corpus import CorpusStatus, Corpus
|
||||||
from .job_input import *
|
from .job_input import JobInput
|
||||||
from .job_result import *
|
from .job_result import JobResult
|
||||||
from .job import *
|
from .job import JobStatus, Job
|
||||||
from .role import *
|
from .role import Permission, Role
|
||||||
from .spacy_nlp_pipeline_model import *
|
from .spacy_nlp_pipeline_model import SpaCyNLPPipelineModel
|
||||||
from .tesseract_ocr_pipeline_model import *
|
from .tesseract_ocr_pipeline_model import TesseractOCRPipelineModel
|
||||||
from .token import *
|
from .token import Token
|
||||||
from .user import *
|
from .user import (
|
||||||
|
ProfilePrivacySettings,
|
||||||
|
UserSettingJobStatusMailNotificationLevel,
|
||||||
|
User
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_models = [
|
||||||
|
Avatar,
|
||||||
|
CorpusFile,
|
||||||
|
CorpusFollowerAssociation,
|
||||||
|
CorpusFollowerRole,
|
||||||
|
Corpus,
|
||||||
|
JobInput,
|
||||||
|
JobResult,
|
||||||
|
Job,
|
||||||
|
Role,
|
||||||
|
SpaCyNLPPipelineModel,
|
||||||
|
TesseractOCRPipelineModel,
|
||||||
|
Token,
|
||||||
|
User
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
_enums = [
|
||||||
|
CorpusFollowerPermission,
|
||||||
|
CorpusStatus,
|
||||||
|
JobStatus,
|
||||||
|
Permission,
|
||||||
|
ProfilePrivacySettings,
|
||||||
|
UserSettingJobStatusMailNotificationLevel
|
||||||
|
]
|
||||||
|
@ -8,7 +8,7 @@ import shutil
|
|||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from app import db
|
from app import db
|
||||||
from app.converters.vrt import normalize_vrt_file
|
from app.converters.vrt import normalize_vrt_file
|
||||||
from app.extensions.nopaque_sqlalchemy_extras import IntEnumColumn
|
from app.extensions.nopaque_sqlalchemy_type_decorators import IntEnumColumn
|
||||||
from .corpus_follower_association import CorpusFollowerAssociation
|
from .corpus_follower_association import CorpusFollowerAssociation
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,9 +42,8 @@ def resource_after_delete(mapper, connection, resource):
|
|||||||
'path': resource.jsonpatch_path
|
'path': resource.jsonpatch_path
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
namespace = '/users'
|
|
||||||
room = f'/users/{resource.user_hashid}'
|
room = f'/users/{resource.user_hashid}'
|
||||||
socketio.emit('patch', jsonpatch, namespace=namespace, room=room)
|
socketio.emit('PATCH', jsonpatch, room=room)
|
||||||
|
|
||||||
|
|
||||||
def cfa_after_delete(mapper, connection, cfa):
|
def cfa_after_delete(mapper, connection, cfa):
|
||||||
@ -55,9 +54,8 @@ def cfa_after_delete(mapper, connection, cfa):
|
|||||||
'path': jsonpatch_path
|
'path': jsonpatch_path
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
namespace = '/users'
|
|
||||||
room = f'/users/{cfa.corpus.user.hashid}'
|
room = f'/users/{cfa.corpus.user.hashid}'
|
||||||
socketio.emit('patch', jsonpatch, namespace=namespace, room=room)
|
socketio.emit('PATCH', jsonpatch, room=room)
|
||||||
|
|
||||||
|
|
||||||
def resource_after_insert(mapper, connection, resource):
|
def resource_after_insert(mapper, connection, resource):
|
||||||
@ -71,9 +69,8 @@ def resource_after_insert(mapper, connection, resource):
|
|||||||
'value': jsonpatch_value
|
'value': jsonpatch_value
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
namespace = '/users'
|
|
||||||
room = f'/users/{resource.user_hashid}'
|
room = f'/users/{resource.user_hashid}'
|
||||||
socketio.emit('patch', jsonpatch, namespace=namespace, room=room)
|
socketio.emit('PATCH', jsonpatch, room=room)
|
||||||
|
|
||||||
|
|
||||||
def cfa_after_insert(mapper, connection, cfa):
|
def cfa_after_insert(mapper, connection, cfa):
|
||||||
@ -86,9 +83,8 @@ def cfa_after_insert(mapper, connection, cfa):
|
|||||||
'value': jsonpatch_value
|
'value': jsonpatch_value
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
namespace = '/users'
|
|
||||||
room = f'/users/{cfa.corpus.user.hashid}'
|
room = f'/users/{cfa.corpus.user.hashid}'
|
||||||
socketio.emit('patch', jsonpatch, namespace=namespace, room=room)
|
socketio.emit('PATCH', jsonpatch, room=room)
|
||||||
|
|
||||||
|
|
||||||
def resource_after_update(mapper, connection, resource):
|
def resource_after_update(mapper, connection, resource):
|
||||||
@ -113,9 +109,8 @@ def resource_after_update(mapper, connection, resource):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
if jsonpatch:
|
if jsonpatch:
|
||||||
namespace = '/users'
|
|
||||||
room = f'/users/{resource.user_hashid}'
|
room = f'/users/{resource.user_hashid}'
|
||||||
socketio.emit('patch', jsonpatch, namespace=namespace, room=room)
|
socketio.emit('PATCH', jsonpatch, room=room)
|
||||||
|
|
||||||
|
|
||||||
def job_after_update(mapper, connection, job):
|
def job_after_update(mapper, connection, job):
|
||||||
|
@ -6,7 +6,7 @@ from time import sleep
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
from app import db
|
from app import db
|
||||||
from app.extensions.nopaque_sqlalchemy_extras import ContainerColumn, IntEnumColumn
|
from app.extensions.nopaque_sqlalchemy_type_decorators import ContainerColumn, IntEnumColumn
|
||||||
|
|
||||||
|
|
||||||
class JobStatus(IntEnum):
|
class JobStatus(IntEnum):
|
||||||
|
@ -20,14 +20,6 @@ class JobInput(FileMixin, HashidMixin, db.Model):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<JobInput {self.filename}>'
|
return f'<JobInput {self.filename}>'
|
||||||
|
|
||||||
@property
|
|
||||||
def content_url(self):
|
|
||||||
return url_for(
|
|
||||||
'jobs.download_job_input',
|
|
||||||
job_id=self.job.id,
|
|
||||||
job_input_id=self.id
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def jsonpatch_path(self):
|
def jsonpatch_path(self):
|
||||||
return f'{self.job.jsonpatch_path}/inputs/{self.hashid}'
|
return f'{self.job.jsonpatch_path}/inputs/{self.hashid}'
|
||||||
@ -40,7 +32,7 @@ class JobInput(FileMixin, HashidMixin, db.Model):
|
|||||||
def url(self):
|
def url(self):
|
||||||
return url_for(
|
return url_for(
|
||||||
'jobs.job',
|
'jobs.job',
|
||||||
job_id=self.job_id,
|
job_input_id=self.id,
|
||||||
_anchor=f'job-{self.job.hashid}-input-{self.hashid}'
|
_anchor=f'job-{self.job.hashid}-input-{self.hashid}'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -22,14 +22,6 @@ class JobResult(FileMixin, HashidMixin, db.Model):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<JobResult {self.filename}>'
|
return f'<JobResult {self.filename}>'
|
||||||
|
|
||||||
@property
|
|
||||||
def download_url(self):
|
|
||||||
return url_for(
|
|
||||||
'jobs.download_job_result',
|
|
||||||
job_id=self.job_id,
|
|
||||||
job_result_id=self.id
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def jsonpatch_path(self):
|
def jsonpatch_path(self):
|
||||||
return f'{self.job.jsonpatch_path}/results/{self.hashid}'
|
return f'{self.job.jsonpatch_path}/results/{self.hashid}'
|
||||||
@ -41,8 +33,8 @@ class JobResult(FileMixin, HashidMixin, db.Model):
|
|||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self):
|
||||||
return url_for(
|
return url_for(
|
||||||
'jobs.job',
|
'job_results.job_result',
|
||||||
job_id=self.job_id,
|
job_result_id=self.id,
|
||||||
_anchor=f'job-{self.job.hashid}-result-{self.hashid}'
|
_anchor=f'job-{self.job.hashid}-result-{self.hashid}'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ from pathlib import Path
|
|||||||
import requests
|
import requests
|
||||||
import yaml
|
import yaml
|
||||||
from app import db
|
from app import db
|
||||||
from app.extensions.nopaque_sqlalchemy_extras import ContainerColumn
|
from app.extensions.nopaque_sqlalchemy_type_decorators import ContainerColumn
|
||||||
from .file_mixin import FileMixin
|
from .file_mixin import FileMixin
|
||||||
from .user import User
|
from .user import User
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ from pathlib import Path
|
|||||||
import requests
|
import requests
|
||||||
import yaml
|
import yaml
|
||||||
from app import db
|
from app import db
|
||||||
from app.extensions.nopaque_sqlalchemy_extras import ContainerColumn
|
from app.extensions.nopaque_sqlalchemy_type_decorators import ContainerColumn
|
||||||
from .file_mixin import FileMixin
|
from .file_mixin import FileMixin
|
||||||
from .user import User
|
from .user import User
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ import re
|
|||||||
import secrets
|
import secrets
|
||||||
import shutil
|
import shutil
|
||||||
from app import db, hashids
|
from app import db, hashids
|
||||||
from app.extensions.nopaque_sqlalchemy_extras import IntEnumColumn
|
from app.extensions.nopaque_sqlalchemy_type_decorators import IntEnumColumn
|
||||||
from .corpus import Corpus
|
from .corpus import Corpus
|
||||||
from .corpus_follower_association import CorpusFollowerAssociation
|
from .corpus_follower_association import CorpusFollowerAssociation
|
||||||
from .corpus_follower_role import CorpusFollowerRole
|
from .corpus_follower_role import CorpusFollowerRole
|
||||||
|
@ -162,13 +162,14 @@ class CQiOverSocketIONamespace(Namespace):
|
|||||||
|
|
||||||
if fn_name in CQI_API_FUNCTION_NAMES:
|
if fn_name in CQI_API_FUNCTION_NAMES:
|
||||||
fn = getattr(cqi_client.api, fn_name)
|
fn = getattr(cqi_client.api, fn_name)
|
||||||
elif fn_name in cqi_extension_functions.CQI_EXTENSION_FUNCTION_NAMES:
|
elif fn_name in CQI_EXTENSION_FUNCTION_NAMES:
|
||||||
fn = getattr(cqi_extension_functions, fn_name)
|
fn = getattr(cqi_extension_functions, fn_name)
|
||||||
else:
|
else:
|
||||||
return {'code': 400, 'msg': 'Bad Request'}
|
return {'code': 400, 'msg': 'Bad Request'}
|
||||||
|
|
||||||
for param in signature(fn).parameters.values():
|
for param in signature(fn).parameters.values():
|
||||||
# Check if the parameter is optional or required
|
# Check if the parameter is optional or required
|
||||||
|
# The following is true for required parameters
|
||||||
if param.default is param.empty:
|
if param.default is param.empty:
|
||||||
if param.name not in fn_args:
|
if param.name not in fn_args:
|
||||||
return {'code': 400, 'msg': 'Bad Request'}
|
return {'code': 400, 'msg': 'Bad Request'}
|
||||||
|
@ -1,109 +0,0 @@
|
|||||||
from flask import current_app, Flask
|
|
||||||
from flask_login import current_user
|
|
||||||
from flask_socketio import Namespace
|
|
||||||
from app import db, hashids, socketio
|
|
||||||
from app.decorators import socketio_admin_required, socketio_login_required
|
|
||||||
from app.models import Job, JobStatus
|
|
||||||
|
|
||||||
|
|
||||||
def _delete_job(app: Flask, job_id: int):
|
|
||||||
with app.app_context():
|
|
||||||
job = Job.query.get(job_id)
|
|
||||||
job.delete()
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def _restart_job(app, job_id):
|
|
||||||
with app.app_context():
|
|
||||||
job = Job.query.get(job_id)
|
|
||||||
job.restart()
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
class UsersNamespace(Namespace):
|
|
||||||
@socketio_login_required
|
|
||||||
def on_delete(self, job_hashid: str) -> dict:
|
|
||||||
job_id = hashids.decode(job_hashid)
|
|
||||||
|
|
||||||
if not isinstance(job_id, int):
|
|
||||||
return {'status': 400, 'statusText': 'Bad Request'}
|
|
||||||
|
|
||||||
job = Job.query.get(job_id)
|
|
||||||
|
|
||||||
if job is None:
|
|
||||||
return {'status': 404, 'statusText': 'Not found'}
|
|
||||||
|
|
||||||
if not (
|
|
||||||
job.user == current_user
|
|
||||||
or current_user.is_administrator
|
|
||||||
):
|
|
||||||
return {'status': 403, 'statusText': 'Forbidden'}
|
|
||||||
|
|
||||||
socketio.start_background_task(
|
|
||||||
_delete_job,
|
|
||||||
current_app._get_current_object(),
|
|
||||||
job_id
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'body': f'Job "{job.title}" marked for deletion',
|
|
||||||
'status': 202,
|
|
||||||
'statusText': 'Accepted'
|
|
||||||
}
|
|
||||||
|
|
||||||
@socketio_admin_required
|
|
||||||
def on_log(self, job_hashid: str):
|
|
||||||
job_id = hashids.decode(job_hashid)
|
|
||||||
|
|
||||||
if not isinstance(job_id, int):
|
|
||||||
return {'status': 400, 'statusText': 'Bad Request'}
|
|
||||||
|
|
||||||
job = Job.query.get(job_id)
|
|
||||||
|
|
||||||
if job is None:
|
|
||||||
return {'status': 404, 'statusText': 'Not found'}
|
|
||||||
|
|
||||||
if job.status not in [JobStatus.COMPLETED, JobStatus.FAILED]:
|
|
||||||
return {'status': 409, 'statusText': 'Conflict'}
|
|
||||||
|
|
||||||
with open(job.path / 'pipeline_data' / 'logs' / 'pyflow_log.txt') as log_file:
|
|
||||||
log = log_file.read()
|
|
||||||
|
|
||||||
return {
|
|
||||||
'body': log,
|
|
||||||
'status': 200,
|
|
||||||
'statusText': 'Forbidden'
|
|
||||||
}
|
|
||||||
|
|
||||||
socketio_login_required
|
|
||||||
def on_restart(self, job_hashid: str):
|
|
||||||
job_id = hashids.decode(job_hashid)
|
|
||||||
|
|
||||||
if not isinstance(job_id, int):
|
|
||||||
return {'status': 400, 'statusText': 'Bad Request'}
|
|
||||||
|
|
||||||
job = Job.query.get(job_id)
|
|
||||||
|
|
||||||
if job is None:
|
|
||||||
return {'status': 404, 'statusText': 'Not found'}
|
|
||||||
|
|
||||||
if not (
|
|
||||||
job.user == current_user
|
|
||||||
or current_user.is_administrator
|
|
||||||
):
|
|
||||||
return {'status': 403, 'statusText': 'Forbidden'}
|
|
||||||
|
|
||||||
if job.status == JobStatus.FAILED:
|
|
||||||
return {'status': 409, 'statusText': 'Conflict'}
|
|
||||||
|
|
||||||
socketio.start_background_task(
|
|
||||||
_restart_job,
|
|
||||||
current_app._get_current_object(),
|
|
||||||
job_id
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'body': f'Job "{job.title}" marked for restarting',
|
|
||||||
'status': 202,
|
|
||||||
'statusText': 'Accepted'
|
|
||||||
}
|
|
@ -1,116 +0,0 @@
|
|||||||
from flask import current_app, Flask
|
|
||||||
from flask_login import current_user
|
|
||||||
from flask_socketio import join_room, leave_room, Namespace
|
|
||||||
from app import db, hashids, socketio
|
|
||||||
from app.decorators import socketio_login_required
|
|
||||||
from app.models import User
|
|
||||||
|
|
||||||
|
|
||||||
def _delete_user(app: Flask, user_id: int):
|
|
||||||
with app.app_context():
|
|
||||||
user = User.query.get(user_id)
|
|
||||||
user.delete()
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
class UsersNamespace(Namespace):
|
|
||||||
@socketio_login_required
|
|
||||||
def on_get(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(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(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'}
|
|
||||||
|
|
||||||
@socketio_login_required
|
|
||||||
def on_delete(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'}
|
|
||||||
|
|
||||||
socketio.start_background_task(
|
|
||||||
_delete_user,
|
|
||||||
current_app._get_current_object(),
|
|
||||||
user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'body': f'User "{user.username}" marked for deletion',
|
|
||||||
'status': 202,
|
|
||||||
'statusText': 'Accepted'
|
|
||||||
}
|
|
@ -2,6 +2,10 @@
|
|||||||
--corpus-status-content: "unprepared";
|
--corpus-status-content: "unprepared";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-corpus-status="SUBMITTED"] {
|
||||||
|
--corpus-status-content: "submitted";
|
||||||
|
}
|
||||||
|
|
||||||
[data-corpus-status="QUEUED"] {
|
[data-corpus-status="QUEUED"] {
|
||||||
--corpus-status-content: "queued";
|
--corpus-status-content: "queued";
|
||||||
}
|
}
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
nopaque.App = class App {
|
|
||||||
constructor() {
|
|
||||||
this.socket = io({transports: ['websocket'], upgrade: false});
|
|
||||||
|
|
||||||
this.ui = new nopaque.UIExtension(this);
|
|
||||||
this.liveUserRegistry = new nopaque.LiveUserRegistryExtension(this);
|
|
||||||
this.users = new nopaque.UsersExtension(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.ui.init();
|
|
||||||
this.liveUserRegistry.init();
|
|
||||||
this.users.init();
|
|
||||||
}
|
|
||||||
};
|
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
nopaque.UIExtension = class UIExtension {
|
nopaque.app.extensions.UI = class UI {
|
||||||
constructor(app) {
|
constructor(app) {
|
||||||
this.app = app;
|
this.app = app;
|
||||||
}
|
}
|
||||||
@ -71,10 +71,7 @@ nopaque.UIExtension = class UIExtension {
|
|||||||
M.Modal.init(
|
M.Modal.init(
|
||||||
document.querySelector('#terms-of-use-modal'),
|
document.querySelector('#terms-of-use-modal'),
|
||||||
{
|
{
|
||||||
dismissible: false,
|
dismissible: false
|
||||||
onCloseEnd: (modalElement) => {
|
|
||||||
nopaque.requests.users.entity.acceptTermsOfUse();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
// #endregion
|
// #endregion
|
@ -1,4 +1,4 @@
|
|||||||
nopaque.LiveUserRegistryExtension = class LiveUserRegistryExtension extends EventTarget {
|
nopaque.app.extensions.UserHub = class UserHub extends EventTarget {
|
||||||
#data;
|
#data;
|
||||||
|
|
||||||
constructor(app) {
|
constructor(app) {
|
||||||
@ -13,7 +13,7 @@ nopaque.LiveUserRegistryExtension = class LiveUserRegistryExtension extends Even
|
|||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.app.users.socket.on('patch', (patch) => {this.#onPatch(patch)});
|
this.app.socket.on('PATCH', (patch) => {this.#onPatch(patch)});
|
||||||
}
|
}
|
||||||
|
|
||||||
add(userId) {
|
add(userId) {
|
||||||
@ -36,35 +36,33 @@ nopaque.LiveUserRegistryExtension = class LiveUserRegistryExtension extends Even
|
|||||||
|
|
||||||
#onPatch(patch) {
|
#onPatch(patch) {
|
||||||
// Filter patch to only include operations on users that are initialized
|
// Filter patch to only include operations on users that are initialized
|
||||||
let filterRegExp = new RegExp(`^/users/(${Object.keys(this.#data.users).join('|')})`);
|
const filterRegExp = new RegExp(`^/users/(${Object.keys(this.#data.users).join('|')})`);
|
||||||
let filteredPatch = patch.filter(operation => filterRegExp.test(operation.path));
|
const filteredPatch = patch.filter(operation => filterRegExp.test(operation.path));
|
||||||
|
|
||||||
// Apply patch
|
// Apply patch
|
||||||
jsonpatch.applyPatch(this.#data, filteredPatch);
|
jsonpatch.applyPatch(this.#data, filteredPatch);
|
||||||
|
|
||||||
// Notify event listeners
|
// Notify event listeners
|
||||||
let event = new CustomEvent('patch', {detail: filteredPatch});
|
const patchEventa = new CustomEvent('patch', {detail: filteredPatch});
|
||||||
this.dispatchEvent(event);
|
this.dispatchEvent(patchEventa);
|
||||||
|
|
||||||
/*
|
|
||||||
// Notify event listeners. Event type: "patch *"
|
// Notify event listeners. Event type: "patch *"
|
||||||
let event = new CustomEvent('patch *', {detail: filteredPatch});
|
const patchEvent = new CustomEvent('patch *', {detail: filteredPatch});
|
||||||
this.dispatchEvent(event);
|
this.dispatchEvent(patchEvent);
|
||||||
|
|
||||||
// Group patches by user id: {<user-id>: [op, ...], ...}
|
// Group patches by user id: {<user-id>: [op, ...], ...}
|
||||||
let patches = {};
|
const patches = {};
|
||||||
let matchRegExp = new RegExp(`^/users/([A-Za-z0-9]+)`);
|
const matchRegExp = new RegExp(`^/users/([A-Za-z0-9]+)`);
|
||||||
for (let operation of filteredPatch) {
|
for (let operation of filteredPatch) {
|
||||||
let [match, userId] = operation.path.match(matchRegExp);
|
const [match, userId] = operation.path.match(matchRegExp);
|
||||||
if (!(userId in patches)) {patches[userId] = [];}
|
if (!(userId in patches)) {patches[userId] = [];}
|
||||||
patches[userId].push(operation);
|
patches[userId].push(operation);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify event listeners. Event type: "patch <user-id>"
|
// Notify event listeners. Event type: "patch <user-id>"
|
||||||
for (let [userId, patch] of Object.entries(patches)) {
|
for (let [userId, patch] of Object.entries(patches)) {
|
||||||
let event = new CustomEvent(`patch ${userId}`, {detail: patch});
|
const userPatchEvent = new CustomEvent(`patch ${userId}`, {detail: patch});
|
||||||
this.dispatchEvent(event);
|
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 = {};
|
@ -1,43 +0,0 @@
|
|||||||
nopaque.UsersExtension = class UsersExtension {
|
|
||||||
constructor(app) {
|
|
||||||
this.app = app;
|
|
||||||
|
|
||||||
this.socket = io('/users', {transports: ['websocket'], upgrade: false});
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {}
|
|
||||||
|
|
||||||
async get(userId) {
|
|
||||||
const response = await this.socket.emitWithAck('get', userId);
|
|
||||||
|
|
||||||
if (response.status !== 200) {
|
|
||||||
throw new Error(`[${response.status}] ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.body;
|
|
||||||
}
|
|
||||||
|
|
||||||
async subscribe(userId) {
|
|
||||||
const response = await this.socket.emitWithAck('subscribe', userId);
|
|
||||||
|
|
||||||
if (response.status != 200) {
|
|
||||||
throw new Error(`[${response.status}] ${response.statusText}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async unsubscribe(userId) {
|
|
||||||
const response = await this.socket.emitWithAck('unsubscribe', userId);
|
|
||||||
|
|
||||||
if (response.status != 200) {
|
|
||||||
throw new Error(`[${response.status}] ${response.statusText}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(userId) {
|
|
||||||
const response = await this.socket.emitWithAck('delete', userId);
|
|
||||||
|
|
||||||
if (response.status != 202) {
|
|
||||||
throw new Error(`[${response.status}] ${response.statusText}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -7,7 +7,6 @@ nopaque.corpus_analysis.StaticVisualizationExtension = class StaticVisualization
|
|||||||
stopwords: undefined,
|
stopwords: undefined,
|
||||||
originalStopwords: {},
|
originalStopwords: {},
|
||||||
stopwordCache: {},
|
stopwordCache: {},
|
||||||
promises: {getStopwords: undefined},
|
|
||||||
tokenSet: new Set()
|
tokenSet: new Set()
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -73,22 +72,11 @@ nopaque.corpus_analysis.StaticVisualizationExtension = class StaticVisualization
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getStopwords() {
|
async getStopwords() {
|
||||||
this.data.promises.getStopwords = new Promise((resolve, reject) => {
|
const stopwords = await app.corpora.getStopwords(this.app.corpusId);
|
||||||
nopaque.requests.corpora.entity.getStopwords()
|
this.data.originalStopwords = structuredClone(stopwords);
|
||||||
.then((response) => {
|
this.data.stopwords = structuredClone(stopwords);
|
||||||
response.json()
|
return stopwords;
|
||||||
.then((json) => {
|
|
||||||
this.data.originalStopwords = structuredClone(json);
|
|
||||||
this.data.stopwords = structuredClone(json);
|
|
||||||
resolve(this.data.stopwords);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return this.data.promises.getStopwords;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderGeneralCorpusInfo() {
|
renderGeneralCorpusInfo() {
|
||||||
|
@ -5,50 +5,6 @@ nopaque.requests.corpora = {};
|
|||||||
|
|
||||||
nopaque.requests.corpora.entity = {};
|
nopaque.requests.corpora.entity = {};
|
||||||
|
|
||||||
nopaque.requests.corpora.entity.delete = (corpusId) => {
|
|
||||||
let input = `/corpora/${corpusId}`;
|
|
||||||
let init = {
|
|
||||||
method: 'DELETE'
|
|
||||||
};
|
|
||||||
return nopaque.requests.JSONfetch(input, init);
|
|
||||||
};
|
|
||||||
|
|
||||||
nopaque.requests.corpora.entity.build = (corpusId) => {
|
|
||||||
let input = `/corpora/${corpusId}/build`;
|
|
||||||
let init = {
|
|
||||||
method: 'POST',
|
|
||||||
};
|
|
||||||
return nopaque.requests.JSONfetch(input, init);
|
|
||||||
};
|
|
||||||
|
|
||||||
nopaque.requests.corpora.entity.generateShareLink = (corpusId, role, expiration) => {
|
|
||||||
let input = `/corpora/${corpusId}/generate-share-link`;
|
|
||||||
let init = {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({role: role, expiration: expiration})
|
|
||||||
};
|
|
||||||
return nopaque.requests.JSONfetch(input, init);
|
|
||||||
};
|
|
||||||
|
|
||||||
nopaque.requests.corpora.entity.getStopwords = () => {
|
|
||||||
let input = `/corpora/stopwords`;
|
|
||||||
let init = {
|
|
||||||
method: 'GET'
|
|
||||||
};
|
|
||||||
return nopaque.requests.JSONfetch(input, init);
|
|
||||||
};
|
|
||||||
|
|
||||||
nopaque.requests.corpora.entity.isPublic = {};
|
|
||||||
|
|
||||||
nopaque.requests.corpora.entity.isPublic.update = (corpusId, isPublic) => {
|
|
||||||
let input = `/corpora/${corpusId}/is_public`;
|
|
||||||
let init = {
|
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify(isPublic)
|
|
||||||
};
|
|
||||||
return nopaque.requests.JSONfetch(input, init);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/*****************************************************************************
|
/*****************************************************************************
|
||||||
* Requests for /corpora/<entity>/files routes *
|
* Requests for /corpora/<entity>/files routes *
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
/*****************************************************************************
|
|
||||||
* Requests for /jobs routes *
|
|
||||||
*****************************************************************************/
|
|
||||||
nopaque.requests.jobs = {};
|
|
||||||
|
|
||||||
nopaque.requests.jobs.entity = {};
|
|
||||||
|
|
||||||
nopaque.requests.jobs.entity.delete = (jobId) => {
|
|
||||||
let input = `/jobs/${jobId}`;
|
|
||||||
let init = {
|
|
||||||
method: 'DELETE'
|
|
||||||
};
|
|
||||||
return nopaque.requests.JSONfetch(input, init);
|
|
||||||
};
|
|
||||||
|
|
||||||
nopaque.requests.jobs.entity.log = (jobId) => {
|
|
||||||
let input = `/jobs/${jobId}/log`;
|
|
||||||
let init = {
|
|
||||||
method: 'GET'
|
|
||||||
};
|
|
||||||
return nopaque.requests.JSONfetch(input, init);
|
|
||||||
};
|
|
||||||
|
|
||||||
nopaque.requests.jobs.entity.restart = (jobId) => {
|
|
||||||
let input = `/jobs/${jobId}/restart`;
|
|
||||||
let init = {
|
|
||||||
method: 'POST'
|
|
||||||
};
|
|
||||||
return nopaque.requests.JSONfetch(input, init);
|
|
||||||
};
|
|
@ -7,7 +7,7 @@ nopaque.resource_displays.CorpusDisplay = class CorpusDisplay extends nopaque.re
|
|||||||
this.displayElement
|
this.displayElement
|
||||||
.querySelector('.action-button[data-action="build-request"]')
|
.querySelector('.action-button[data-action="build-request"]')
|
||||||
.addEventListener('click', (event) => {
|
.addEventListener('click', (event) => {
|
||||||
nopaque.requests.corpora.entity.build(this.corpusId);
|
app.corpora.build(this.corpusId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,22 +52,23 @@ nopaque.resource_displays.CorpusDisplay = class CorpusDisplay extends nopaque.re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setTitle(title) {
|
async setTitle(title) {
|
||||||
this.setElements(this.displayElement.querySelectorAll('.corpus-title'), title);
|
const corpusTitleElements = this.displayElement.querySelectorAll('.corpus-title');
|
||||||
|
this.setElements(corpusTitleElements, title);
|
||||||
}
|
}
|
||||||
|
|
||||||
setNumTokens(numTokens) {
|
setNumTokens(numTokens) {
|
||||||
this.setElements(
|
const corpusTokenRatioElements = this.displayElement.querySelectorAll('.corpus-token-ratio');
|
||||||
this.displayElement.querySelectorAll('.corpus-token-ratio'),
|
const maxNumTokens = 2147483647;
|
||||||
`${numTokens}/${app.data.users[this.userId].corpora[this.corpusId].max_num_tokens}`
|
|
||||||
);
|
this.setElements(corpusTokenRatioElements, `${numTokens}/${maxNumTokens}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
setDescription(description) {
|
setDescription(description) {
|
||||||
this.setElements(this.displayElement.querySelectorAll('.corpus-description'), description);
|
this.setElements(this.displayElement.querySelectorAll('.corpus-description'), description);
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatus(status) {
|
async setStatus(status) {
|
||||||
let elements = this.displayElement.querySelectorAll('.action-button[data-action="analyze"]');
|
let elements = this.displayElement.querySelectorAll('.action-button[data-action="analyze"]');
|
||||||
for (let element of elements) {
|
for (let element of elements) {
|
||||||
if (['BUILT', 'STARTING_ANALYSIS_SESSION', 'RUNNING_ANALYSIS_SESSION', 'CANCELING_ANALYSIS_SESSION'].includes(status)) {
|
if (['BUILT', 'STARTING_ANALYSIS_SESSION', 'RUNNING_ANALYSIS_SESSION', 'CANCELING_ANALYSIS_SESSION'].includes(status)) {
|
||||||
@ -77,8 +78,10 @@ nopaque.resource_displays.CorpusDisplay = class CorpusDisplay extends nopaque.re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
elements = this.displayElement.querySelectorAll('.action-button[data-action="build-request"]');
|
elements = this.displayElement.querySelectorAll('.action-button[data-action="build-request"]');
|
||||||
|
const user = await app.userHub.get(this.userId);
|
||||||
|
const corpusFiles = user.corpora[this.corpusId].files;
|
||||||
for (let element of elements) {
|
for (let element of elements) {
|
||||||
if (['UNPREPARED', 'FAILED'].includes(status) && Object.values(app.data.users[this.userId].corpora[this.corpusId].files).length > 0) {
|
if (['UNPREPARED', 'FAILED'].includes(status) && Object.values(corpusFiles.length > 0)) {
|
||||||
element.classList.remove('disabled');
|
element.classList.remove('disabled');
|
||||||
} else {
|
} else {
|
||||||
element.classList.add('disabled');
|
element.classList.add('disabled');
|
||||||
|
@ -6,10 +6,10 @@ nopaque.resource_displays.ResourceDisplay = class ResourceDisplay {
|
|||||||
this.userId = this.displayElement.dataset.userId;
|
this.userId = this.displayElement.dataset.userId;
|
||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
if (this.userId === undefined) {return;}
|
if (this.userId === undefined) {return;}
|
||||||
app.liveUserRegistry.addEventListener('patch', (event) => {
|
app.userHub.addEventListener('patch', (event) => {
|
||||||
if (this.isInitialized) {this.onPatch(event.detail);}
|
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||||
});
|
});
|
||||||
app.liveUserRegistry.get(this.userId).then((user) => {
|
app.userHub.get(this.userId).then((user) => {
|
||||||
this.init(user);
|
this.init(user);
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
});
|
});
|
||||||
|
@ -14,10 +14,10 @@ nopaque.resource_lists.CorpusFileList = class CorpusFileList extends nopaque.res
|
|||||||
this.hasPermissionView = listContainerElement.dataset?.hasPermissionView == 'true' || false;
|
this.hasPermissionView = listContainerElement.dataset?.hasPermissionView == 'true' || false;
|
||||||
this.hasPermissionManageFiles = listContainerElement.dataset?.hasPermissionManageFiles == 'true' || false;
|
this.hasPermissionManageFiles = listContainerElement.dataset?.hasPermissionManageFiles == 'true' || false;
|
||||||
if (this.userId === undefined || this.corpusId === undefined) {return;}
|
if (this.userId === undefined || this.corpusId === undefined) {return;}
|
||||||
app.liveUserRegistry.addEventListener('patch', (event) => {
|
app.userHub.addEventListener('patch', (event) => {
|
||||||
if (this.isInitialized) {this.onPatch(event.detail);}
|
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||||
});
|
});
|
||||||
app.liveUserRegistry.get(this.userId).then((user) => {
|
app.userHub.get(this.userId).then((user) => {
|
||||||
// TODO: Make this better understandable
|
// TODO: Make this better understandable
|
||||||
this.add(Object.values(user.corpora[this.corpusId].files || user.followed_corpora[this.corpusId].files));
|
this.add(Object.values(user.corpora[this.corpusId].files || user.followed_corpora[this.corpusId].files));
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
|
@ -12,10 +12,10 @@ nopaque.resource_lists.CorpusFollowerList = class CorpusFollowerList extends nop
|
|||||||
this.userId = listContainerElement.dataset.userId;
|
this.userId = listContainerElement.dataset.userId;
|
||||||
this.corpusId = listContainerElement.dataset.corpusId;
|
this.corpusId = listContainerElement.dataset.corpusId;
|
||||||
if (this.userId === undefined || this.corpusId === undefined) {return;}
|
if (this.userId === undefined || this.corpusId === undefined) {return;}
|
||||||
app.liveUserRegistry.addEventListener('patch', (event) => {
|
app.userHub.addEventListener('patch', (event) => {
|
||||||
if (this.isInitialized) {this.onPatch(event.detail);}
|
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||||
});
|
});
|
||||||
app.liveUserRegistry.get(this.userId).then((user) => {
|
app.userHub.get(this.userId).then((user) => {
|
||||||
// TODO: Check if the following is better
|
// TODO: Check if the following is better
|
||||||
// let corpusFollowerAssociations = Object.values(user.corpora[this.corpusId].corpus_follower_associations);
|
// let corpusFollowerAssociations = Object.values(user.corpora[this.corpusId].corpus_follower_associations);
|
||||||
// let filteredList = corpusFollowerAssociations.filter(association => association.follower.id != currentUserId);
|
// let filteredList = corpusFollowerAssociations.filter(association => association.follower.id != currentUserId);
|
||||||
|
@ -11,10 +11,10 @@ nopaque.resource_lists.CorpusList = class CorpusList extends nopaque.resource_li
|
|||||||
this.selectedItemIds = new Set();
|
this.selectedItemIds = new Set();
|
||||||
this.userId = listContainerElement.dataset.userId;
|
this.userId = listContainerElement.dataset.userId;
|
||||||
if (this.userId === undefined) {return;}
|
if (this.userId === undefined) {return;}
|
||||||
app.liveUserRegistry.addEventListener('patch', (event) => {
|
app.userHub.addEventListener('patch', (event) => {
|
||||||
if (this.isInitialized) {this.onPatch(event.detail);}
|
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||||
});
|
});
|
||||||
app.liveUserRegistry.get(this.userId).then((user) => {
|
app.userHub.get(this.userId).then((user) => {
|
||||||
this.add(this.aggregateData(user));
|
this.add(this.aggregateData(user));
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
});
|
});
|
||||||
@ -180,7 +180,7 @@ nopaque.resource_lists.CorpusList = class CorpusList extends nopaque.resource_li
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
nopaque.requests.corpora.entity.delete(itemId);
|
app.corpora.delete(itemId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
modal.open();
|
modal.open();
|
||||||
@ -276,7 +276,7 @@ nopaque.resource_lists.CorpusList = class CorpusList extends nopaque.resource_li
|
|||||||
let listItem = this.listjs.get('id', selectedItemId)[0].elm;
|
let listItem = this.listjs.get('id', selectedItemId)[0].elm;
|
||||||
let values = this.listjs.get('id', listItem.dataset.id)[0].values();
|
let values = this.listjs.get('id', listItem.dataset.id)[0].values();
|
||||||
if (values['is-owner']) {
|
if (values['is-owner']) {
|
||||||
nopaque.requests.corpora.entity.delete(selectedItemId);
|
app.corpora.delete(selectedItemId);
|
||||||
} else {
|
} else {
|
||||||
nopaque.requests.corpora.entity.followers.entity.delete(selectedItemId, currentUserId);
|
nopaque.requests.corpora.entity.followers.entity.delete(selectedItemId, currentUserId);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -8,10 +8,10 @@ nopaque.resource_lists.JobInputList = class JobInputList extends nopaque.resourc
|
|||||||
this.userId = listContainerElement.dataset.userId;
|
this.userId = listContainerElement.dataset.userId;
|
||||||
this.jobId = listContainerElement.dataset.jobId;
|
this.jobId = listContainerElement.dataset.jobId;
|
||||||
if (this.userId === undefined || this.jobId === undefined) {return;}
|
if (this.userId === undefined || this.jobId === undefined) {return;}
|
||||||
// app.liveUserRegistry.addEventListener('patch', (event) => {
|
// app.userHub.addEventListener('patch', (event) => {
|
||||||
// if (this.isInitialized) {this.onPatch(event.detail);}
|
// if (this.isInitialized) {this.onPatch(event.detail);}
|
||||||
// });
|
// });
|
||||||
app.liveUserRegistry.get(this.userId).then((user) => {
|
app.userHub.get(this.userId).then((user) => {
|
||||||
this.add(Object.values(user.jobs[this.jobId].inputs));
|
this.add(Object.values(user.jobs[this.jobId].inputs));
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
});
|
});
|
||||||
|
@ -12,10 +12,10 @@ nopaque.resource_lists.JobList = class JobList extends nopaque.resource_lists.Re
|
|||||||
this.selectedItemIds = new Set();
|
this.selectedItemIds = new Set();
|
||||||
this.userId = listContainerElement.dataset.userId;
|
this.userId = listContainerElement.dataset.userId;
|
||||||
if (this.userId === undefined) {return;}
|
if (this.userId === undefined) {return;}
|
||||||
app.liveUserRegistry.addEventListener('patch', (event) => {
|
app.userHub.addEventListener('patch', (event) => {
|
||||||
if (this.isInitialized) {this.onPatch(event.detail);}
|
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||||
});
|
});
|
||||||
app.liveUserRegistry.get(this.userId).then((user) => {
|
app.userHub.get(this.userId).then((user) => {
|
||||||
this.add(Object.values(user.jobs));
|
this.add(Object.values(user.jobs));
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
});
|
});
|
||||||
@ -136,8 +136,9 @@ nopaque.resource_lists.JobList = class JobList extends nopaque.resource_lists.Re
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]');
|
let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]');
|
||||||
confirmElement.addEventListener('click', (event) => {
|
confirmElement.addEventListener('click', async (event) => {
|
||||||
nopaque.requests.jobs.entity.delete(itemId);
|
const message = await app.jobs.delete(itemId);
|
||||||
|
app.ui.flash(message, 'job');
|
||||||
});
|
});
|
||||||
modal.open();
|
modal.open();
|
||||||
break;
|
break;
|
||||||
@ -221,8 +222,9 @@ nopaque.resource_lists.JobList = class JobList extends nopaque.resource_lists.Re
|
|||||||
);
|
);
|
||||||
let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]');
|
let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]');
|
||||||
confirmElement.addEventListener('click', (event) => {
|
confirmElement.addEventListener('click', (event) => {
|
||||||
this.selectedItemIds.forEach(selectedItemId => {
|
this.selectedItemIds.forEach(async (selectedItemId) => {
|
||||||
nopaque.requests.jobs.entity.delete(selectedItemId);
|
const message = await app.jobs.delete(selectedItemId);
|
||||||
|
app.ui.flash(message, 'job');
|
||||||
});
|
});
|
||||||
this.selectedItemIds.clear();
|
this.selectedItemIds.clear();
|
||||||
this.renderingItemSelection();
|
this.renderingItemSelection();
|
||||||
|
@ -8,10 +8,10 @@ nopaque.resource_lists.JobResultList = class JobResultList extends nopaque.resou
|
|||||||
this.userId = listContainerElement.dataset.userId;
|
this.userId = listContainerElement.dataset.userId;
|
||||||
this.jobId = listContainerElement.dataset.jobId;
|
this.jobId = listContainerElement.dataset.jobId;
|
||||||
if (this.userId === undefined || this.jobId === undefined) {return;}
|
if (this.userId === undefined || this.jobId === undefined) {return;}
|
||||||
app.liveUserRegistry.addEventListener('patch', (event) => {
|
app.userHub.addEventListener('patch', (event) => {
|
||||||
if (this.isInitialized) {this.onPatch(event.detail);}
|
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||||
});
|
});
|
||||||
app.liveUserRegistry.get(this.userId).then((user) => {
|
app.userHub.get(this.userId).then((user) => {
|
||||||
this.add(Object.values(user.jobs[this.jobId].results));
|
this.add(Object.values(user.jobs[this.jobId].results));
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
});
|
});
|
||||||
|
@ -8,10 +8,10 @@ nopaque.resource_lists.SpaCyNLPPipelineModelList = class SpaCyNLPPipelineModelLi
|
|||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
this.userId = listContainerElement.dataset.userId;
|
this.userId = listContainerElement.dataset.userId;
|
||||||
if (this.userId === undefined) {return;}
|
if (this.userId === undefined) {return;}
|
||||||
app.liveUserRegistry.addEventListener('patch', (event) => {
|
app.userHub.addEventListener('patch', (event) => {
|
||||||
if (this.isInitialized) {this.onPatch(event.detail);}
|
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||||
});
|
});
|
||||||
app.liveUserRegistry.get(this.userId).then((user) => {
|
app.userHub.get(this.userId).then((user) => {
|
||||||
this.add(Object.values(user.spacy_nlp_pipeline_models));
|
this.add(Object.values(user.spacy_nlp_pipeline_models));
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
});
|
});
|
||||||
|
@ -8,10 +8,10 @@ nopaque.resource_lists.TesseractOCRPipelineModelList = class TesseractOCRPipelin
|
|||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
this.userId = listContainerElement.dataset.userId;
|
this.userId = listContainerElement.dataset.userId;
|
||||||
if (this.userId === undefined) {return;}
|
if (this.userId === undefined) {return;}
|
||||||
app.liveUserRegistry.addEventListener('patch', (event) => {
|
app.userHub.addEventListener('patch', (event) => {
|
||||||
if (this.isInitialized) {this.onPatch(event.detail);}
|
if (this.isInitialized) {this.onPatch(event.detail);}
|
||||||
});
|
});
|
||||||
app.liveUserRegistry.get(this.userId).then((user) => {
|
app.userHub.get(this.userId).then((user) => {
|
||||||
this.add(Object.values(user.tesseract_ocr_pipeline_models));
|
this.add(Object.values(user.tesseract_ocr_pipeline_models));
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
});
|
});
|
||||||
|
@ -44,8 +44,8 @@
|
|||||||
Your profile
|
Your profile
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li {% if request.path == url_for('settings.settings') %}class="active"{% endif %}>
|
<li {% if request.path == url_for('settings.index') %}class="active"{% endif %}>
|
||||||
<a href="{{ url_for('settings.settings') }}">
|
<a href="{{ url_for('settings.index') }}">
|
||||||
<i class="material-icons">settings</i>
|
<i class="material-icons">settings</i>
|
||||||
Settings
|
Settings
|
||||||
</a>
|
</a>
|
||||||
@ -56,5 +56,24 @@
|
|||||||
Log out
|
Log out
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% if current_user.can('USE_API') or current_user.is_administrator %}
|
||||||
|
<li class="divider" tabindex="-1"></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if current_user.can('USE_API') %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('apifairy.docs') }}">
|
||||||
|
<i class="material-icons left">api</i>
|
||||||
|
API
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if current_user.is_administrator %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('admin.index') }}">
|
||||||
|
<i class="material-icons left">admin_panel_settings</i>
|
||||||
|
Administration
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,34 +1,28 @@
|
|||||||
{% if current_user.is_authenticated and not current_user.terms_of_use_accepted %}
|
<div class="modal modal-fixed-footer" id="terms-of-use-modal">
|
||||||
<div id="terms-of-use-modal" class="modal modal-fixed-footer">
|
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="container">
|
<div class="card-panel primary-color white-text">
|
||||||
<div class="row">
|
<h4 class="m-3 center-align">Terms of use</h4>
|
||||||
<div class="col s12">
|
</div>
|
||||||
<h1 id="title">Terms of use</h1>
|
|
||||||
</div>
|
<ul class="tabs tabs-fixed-width z-depth-1">
|
||||||
<div class="col s12">
|
<li class="tab"><a class="active" href="#terms-of-use-modal-content-german">German</a></li>
|
||||||
<div class="switch">
|
<li class="tab"><a href="#terms-of-use-modal-content-english">English</a></li>
|
||||||
<label>
|
</ul>
|
||||||
DE
|
|
||||||
<input type="checkbox" id="terms-of-use-modal-switch">
|
<div id="terms-of-use-modal-content-german">
|
||||||
<span class="lever"></span>
|
{% include 'main/_terms_of_use/german.html.j2' %}
|
||||||
EN
|
</div>
|
||||||
</label>
|
|
||||||
</div>
|
<div id="terms-of-use-modal-content-english">
|
||||||
<br>
|
{% include 'main/_terms_of_use/english.html.j2' %}
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<div class="modal-footer">
|
<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>
|
{% if current_user.is_authenticated and not current_user.terms_of_use_accepted %}
|
||||||
<a href="#!" class="modal-close waves-effect waves-green btn">Yes</a>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
@ -8,10 +8,18 @@
|
|||||||
filters='rjsmin',
|
filters='rjsmin',
|
||||||
output='gen/nopaque.%(version)s.js',
|
output='gen/nopaque.%(version)s.js',
|
||||||
'js/index.js',
|
'js/index.js',
|
||||||
'js/app.js',
|
'js/app/index.js',
|
||||||
'js/app/ui.js',
|
'js/app/client.js',
|
||||||
'js/app/user-live-registry.js',
|
'js/app/endpoints/index.js',
|
||||||
'js/app/users.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/utils.js',
|
||||||
|
|
||||||
'js/forms/index.js',
|
'js/forms/index.js',
|
||||||
@ -46,7 +54,6 @@
|
|||||||
'js/requests/admin.js',
|
'js/requests/admin.js',
|
||||||
'js/requests/contributions.js',
|
'js/requests/contributions.js',
|
||||||
'js/requests/corpora.js',
|
'js/requests/corpora.js',
|
||||||
'js/requests/jobs.js',
|
|
||||||
'js/requests/users.js',
|
'js/requests/users.js',
|
||||||
|
|
||||||
'js/corpus-analysis/index.js',
|
'js/corpus-analysis/index.js',
|
||||||
@ -75,36 +82,50 @@
|
|||||||
<script src="{{ ASSET_URL }}"></script>
|
<script src="{{ ASSET_URL }}"></script>
|
||||||
{% endassets -%}
|
{% endassets -%}
|
||||||
|
|
||||||
|
{# TODO: Think about implementing the following inside a main.js(.j2) #}
|
||||||
<script>
|
<script>
|
||||||
// TODO: Implement an app.run method and use this for all of the following
|
var app;
|
||||||
const app = new nopaque.App();
|
var currentUserId;
|
||||||
app.init();
|
|
||||||
|
|
||||||
{% if current_user.is_authenticated -%}
|
async function main() {
|
||||||
const currentUserId = {{ current_user.hashid|tojson }};
|
app = new nopaque.app.Client();
|
||||||
|
app.init();
|
||||||
|
|
||||||
app.liveUserRegistry.add(currentUserId)
|
{% if not current_user.is_authenticated %}
|
||||||
.catch((error) => {throw JSON.stringify(error);});
|
const currentUserId = null;
|
||||||
|
{% else %}
|
||||||
|
currentUserId = {{ current_user.hashid|tojson }};
|
||||||
|
|
||||||
{% if not current_user.terms_of_use_accepted -%}
|
try {
|
||||||
M.Modal.getInstance(document.querySelector('#terms-of-use-modal')).open();
|
await app.userHub.add(currentUserId);
|
||||||
{% endif -%}
|
} catch (error) {
|
||||||
{% endif -%}
|
app.ui.flash('Failed to load user data.', 'error');
|
||||||
|
}
|
||||||
|
|
||||||
// Display flashed messages
|
{% if not current_user.terms_of_use_accepted %}
|
||||||
for (let [category, message] of {{ get_flashed_messages(with_categories=True)|tojson }}) {
|
const termsOfUseAcceptButtonElement = document.querySelector('#terms-of-use-modal-accept-button');
|
||||||
app.ui.flash(message, message);
|
termsOfUseAcceptButtonElement.addEventListener('click', async () => {
|
||||||
}
|
try {
|
||||||
</script>
|
await app.main.acceptTermsOfUse();
|
||||||
|
app.ui.flash('Terms of use accepted.');
|
||||||
<script>
|
} catch (error) {
|
||||||
let languageModalSwitch = document.querySelector('#terms-of-use-modal-switch');
|
app.ui.flash('Failed to accept terms of use.', 'error');
|
||||||
let termsOfUseModalContent = document.querySelectorAll('.terms-of-use-modal-content');
|
}
|
||||||
if (languageModalSwitch) {
|
|
||||||
languageModalSwitch.addEventListener('change', () => {
|
|
||||||
termsOfUseModalContent.forEach(content => {
|
|
||||||
content.classList.toggle('hide');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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>
|
</script>
|
||||||
|
@ -85,8 +85,8 @@
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
{# settings #}
|
{# settings #}
|
||||||
<li {% if request.path == url_for('settings.settings') %}class="active"{% endif %}>
|
<li {% if request.path == url_for('settings.index') %}class="active"{% endif %}>
|
||||||
<a class="waves-effect" href="{{ url_for('settings.settings') }}"><i class="material-icons">settings</i>Settings</a>
|
<a class="waves-effect" href="{{ url_for('settings.index') }}"><i class="material-icons">settings</i>Settings</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{# log out #}
|
{# log out #}
|
||||||
@ -105,19 +105,21 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if current_user.is_authenticated and current_user.can('ADMINISTRATE') %}
|
{% if current_user.can('USE_API') or current_user.is_administrator %}
|
||||||
{# administration section #}
|
|
||||||
<li><div class="divider"></div></li>
|
<li><div class="divider"></div></li>
|
||||||
<li><a class="subheader">Administration</a></li>
|
{% endif %}
|
||||||
|
|
||||||
{# corpora #}
|
{% if current_user.can('USE_API') %}
|
||||||
|
{# API #}
|
||||||
<li>
|
<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('apifairy.docs') }}"><i class="material-icons">api</i>API</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{# users #}
|
{% if current_user.is_administrator %}
|
||||||
|
{# Administration #}
|
||||||
<li>
|
<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('admin.index') }}"><i class="material-icons">admin_panel_settings</i>Administration</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -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 %}
|
|
@ -69,10 +69,9 @@
|
|||||||
{{ super() }}
|
{{ super() }}
|
||||||
<div class="modal no-autoinit" id="corpus-analysis-init-modal">
|
<div class="modal no-autoinit" id="corpus-analysis-init-modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="card-panel service-color darken white-text" data-service="corpus-analysis">
|
<div class="card-panel primary-color white-text" data-service="corpus-analysis">
|
||||||
<h4 class="m-3"><i class="material-icons left" style="font-size: inherit; line-height: inherit;">hourglass_empty</i>We are preparing your analysis session</h4>
|
<h4 class="m-3 center-align">Preparing your analysis session</h4>
|
||||||
</div>
|
</div>
|
||||||
<h4>We are preparing your analysis session</h4>
|
|
||||||
<p>
|
<p>
|
||||||
Our server works as hard as it can to prepare your analysis session. Please be patient and give it some time.<br>
|
Our server works as hard as it can to prepare your analysis session. Please be patient and give it some time.<br>
|
||||||
If initialization takes longer than usual or an error occurs, <a onclick="window.location.reload()" href="#">reload the page</a>.
|
If initialization takes longer than usual or an error occurs, <a onclick="window.location.reload()" href="#">reload the page</a>.
|
||||||
|
@ -246,7 +246,7 @@
|
|||||||
let publishingModalIsPublicSwitchElement = document.querySelector('#publishing-modal-is-public-switch');
|
let publishingModalIsPublicSwitchElement = document.querySelector('#publishing-modal-is-public-switch');
|
||||||
publishingModalIsPublicSwitchElement.addEventListener('change', (event) => {
|
publishingModalIsPublicSwitchElement.addEventListener('change', (event) => {
|
||||||
let newIsPublic = publishingModalIsPublicSwitchElement.checked;
|
let newIsPublic = publishingModalIsPublicSwitchElement.checked;
|
||||||
nopaque.requests.corpora.entity.isPublic.update({{ corpus.hashid|tojson }}, newIsPublic)
|
app.corpora.updateIsPublic({{ corpus.hashid|tojson }}, newIsPublic)
|
||||||
.catch((response) => {
|
.catch((response) => {
|
||||||
publishingModalIsPublicSwitchElement.checked = !newIsPublic;
|
publishingModalIsPublicSwitchElement.checked = !newIsPublic;
|
||||||
});
|
});
|
||||||
@ -256,7 +256,7 @@ publishingModalIsPublicSwitchElement.addEventListener('change', (event) => {
|
|||||||
// #region Delete
|
// #region Delete
|
||||||
let deleteModalDeleteButtonElement = document.querySelector('#delete-modal-delete-button');
|
let deleteModalDeleteButtonElement = document.querySelector('#delete-modal-delete-button');
|
||||||
deleteModalDeleteButtonElement.addEventListener('click', (event) => {
|
deleteModalDeleteButtonElement.addEventListener('click', (event) => {
|
||||||
nopaque.requests.corpora.entity.delete({{ corpus.hashid|tojson }})
|
app.corpora.delete({{ corpus.hashid|tojson }})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
window.location.href = {{ url_for('main.dashboard')|tojson }};
|
window.location.href = {{ url_for('main.dashboard')|tojson }};
|
||||||
});
|
});
|
||||||
@ -346,19 +346,14 @@ M.Modal.init(
|
|||||||
shareLinkModalOutputContainerElement.classList.add('hide');
|
shareLinkModalOutputContainerElement.classList.add('hide');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
shareLinkModalCreateButtonElement.addEventListener('click', (event) => {
|
shareLinkModalCreateButtonElement.addEventListener('click', async (event) => {
|
||||||
let role = shareLinkModalCorpusFollowerRoleSelectElement.value;
|
const role = shareLinkModalCorpusFollowerRoleSelectElement.value;
|
||||||
let expiration = shareLinkModalExpirationDateDatepickerElement.value
|
const expiration = shareLinkModalExpirationDateDatepickerElement.value;
|
||||||
nopaque.requests.corpora.entity.generateShareLink({{ corpus.hashid|tojson }}, role, expiration)
|
const shareLink = await app.corpora.createShareLink({{ corpus.hashid|tojson }}, expiration, role);
|
||||||
.then((response) => {
|
shareLinkModalOutputContainerElement.classList.remove('hide');
|
||||||
response.json()
|
shareLinkModalOutputFieldElement.value = shareLink;
|
||||||
.then((json) => {
|
|
||||||
shareLinkModalOutputContainerElement.classList.remove('hide');
|
|
||||||
shareLinkModalOutputFieldElement.value = json.corpusShareLink;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
shareLinkModalOutputCopyButtonElement.addEventListener('click', (event) => {
|
shareLinkModalOutputCopyButtonElement.addEventListener('click', (event) => {
|
||||||
|
@ -273,7 +273,7 @@ publicCorpusFollowerList.add(
|
|||||||
{% if cfr.has_permission('MANAGE_FILES') %}
|
{% if cfr.has_permission('MANAGE_FILES') %}
|
||||||
let followerBuildRequest = document.querySelector('#follower-build-request');
|
let followerBuildRequest = document.querySelector('#follower-build-request');
|
||||||
followerBuildRequest.addEventListener('click', () => {
|
followerBuildRequest.addEventListener('click', () => {
|
||||||
nopaque.requests.corpora.entity.build({{ corpus.hashid|tojson }})
|
app.corpora.build({{ corpus.hashid|tojson }})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
});
|
});
|
||||||
@ -380,17 +380,12 @@ M.Modal.init(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
shareLinkModalCreateButtonElement.addEventListener('click', (event) => {
|
shareLinkModalCreateButtonElement.addEventListener('click', async (event) => {
|
||||||
let role = shareLinkModalCorpusFollowerRoleSelectElement.value;
|
const roleName = shareLinkModalCorpusFollowerRoleSelectElement.value;
|
||||||
let expiration = shareLinkModalExpirationDateDatepickerElement.value
|
const expirationDate = shareLinkModalExpirationDateDatepickerElement.value;
|
||||||
nopaque.requests.corpora.entity.generateShareLink({{ corpus.hashid|tojson }}, role, expiration)
|
const shareLink = await app.corpora.createShareLink({{ corpus.hashid|tojson }}, expiration, role);
|
||||||
.then((response) => {
|
shareLinkModalOutputContainerElement.classList.remove('hide');
|
||||||
response.json()
|
shareLinkModalOutputFieldElement.value = shareLink;
|
||||||
.then((json) => {
|
|
||||||
shareLinkModalOutputContainerElement.classList.remove('hide');
|
|
||||||
shareLinkModalOutputFieldElement.value = json.corpusShareLink;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
shareLinkModalOutputCopyButtonElement.addEventListener('click', (event) => {
|
shareLinkModalOutputCopyButtonElement.addEventListener('click', (event) => {
|
||||||
|
@ -137,28 +137,26 @@
|
|||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<script>
|
<script>
|
||||||
let deleteJobRequestElement = document.querySelector('#delete-job-request');
|
const deleteJobRequestElement = document.querySelector('#delete-job-request');
|
||||||
let restartJobRequestElement = document.querySelector('#restart-job-request');
|
const restartJobRequestElement = document.querySelector('#restart-job-request');
|
||||||
deleteJobRequestElement.addEventListener('click', (event) => {
|
|
||||||
nopaque.requests.jobs.entity.delete({{ job.hashid|tojson }});
|
deleteJobRequestElement.addEventListener('click', async (event) => {
|
||||||
});
|
const message = await app.jobs.delete({{ job.hashid|tojson }});
|
||||||
restartJobRequestElement.addEventListener('click', (event) => {
|
app.ui.flash(message, 'job');
|
||||||
nopaque.requests.jobs.entity.restart({{ job.hashid|tojson }});
|
});
|
||||||
|
restartJobRequestElement.addEventListener('click', async (event) => {
|
||||||
|
const message = await app.jobs.restart({{ job.hashid|tojson }});
|
||||||
|
app.ui.flash(message, 'job');
|
||||||
});
|
});
|
||||||
|
|
||||||
if ({{ current_user.is_administrator|tojson }}) {
|
{% if current_user.is_administrator %}
|
||||||
let jobLogButtonElement = document.querySelector('#job-log-button');
|
const jobLogButtonElement = document.querySelector('#job-log-button');
|
||||||
jobLogButtonElement.addEventListener('click', (event) => {
|
const jobLogModalElement = document.querySelector('#job-log-modal');
|
||||||
nopaque.requests.jobs.entity.log({{ job.hashid|tojson }})
|
|
||||||
.then(
|
jobLogButtonElement.addEventListener('click', async (event) => {
|
||||||
(response) => {
|
const log = await app.jobs.log({{ job.hashid|tojson }});
|
||||||
response.json()
|
jobLogModalElement.querySelector('pre code').textContent = log;
|
||||||
.then((json) => {
|
});
|
||||||
let jobLogModalElement = document.querySelector('#job-log-modal');
|
{% endif %}
|
||||||
jobLogModalElement.querySelector('pre code').textContent = json.jobLog;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock scripts %}
|
{% endblock scripts %}
|
||||||
|
124
app/templates/main/_terms_of_use/english.html.j2
Normal file
124
app/templates/main/_terms_of_use/english.html.j2
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
<div class="card red">
|
||||||
|
<div class="card-content">
|
||||||
|
<span class="card-title">Notice</span>
|
||||||
|
<p>This page is translated for the understanding of English-speaking users. However, only the version of the German Terms of Use in accordance with German law applies. </p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<p>With the usage of the nopaque platform you declare your acceptance of the General Terms of Use and that you have taken note of the legal framework and the data protection declaration.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h5>§ 1 Content and scope of the Services offered</h5>
|
||||||
|
<p>(1) These General Terms and Conditions apply to the use of the Services offered in the browser-based data processing and text analysis tool "nopaque" (hereinafter "Services") by Bielefeld University (hereinafter "University"), which can be used free of charge by authorized Users via the website www.nopaque.uni-bielefeld.de.</p>
|
||||||
|
<p>(2) The authorized use of these services is exclusively granted to:</p>
|
||||||
|
<ol type="a">
|
||||||
|
<li>Students, teaching staff, and employees of Bielefeld University</li>
|
||||||
|
<li>External researchers for use in non-commercial research</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h5>§ 2 Services</h5>
|
||||||
|
<p>(1) As part of the Services offered, user-uploaded data can be processed and analyzed automatically. This is implemented in the form of the following Services, each of which can be executed individually:</p>
|
||||||
|
<ol type="a">
|
||||||
|
<li><b>File Setup</b>: Digital copies of text-based research data (books, letters, etc.) often consist of different files and formats. nopaque converts and merges these files to facilitate further processing and the use of other services.</li>
|
||||||
|
<li><b>Optical Character Recognition</b> (OCR): nopaque converts image data - such as photos or scans - into text data using OCR and makes it machine-readable. This step enables the further computational analysis of documents.</li>
|
||||||
|
<li><b>Handwritten Text Recognition</b> (HTR): nopaque converts image data from handwritten text - such as photos or scans - into text data using HTR and makes it machine-readable. This step enables further computational analysis of documents.</li>
|
||||||
|
<li><b>Natural Language Processing</b> (NLP): Using computer-aided linguistic data processing (tokenization, lemmatization, part-of-speech tagging and named entity recognition), nopaque extracts additional information from text.</li>
|
||||||
|
<li><b>Corpus analysis</b>: With nopaque, one can create and upload as many text corpora as desired. It uses the CQP Query Language, which enables complex search queries using metadata and NLP tags.</li>
|
||||||
|
</ol>
|
||||||
|
<p>(2) The User is also entitled to share text edits (corpora) created as part of the Services offered with other registered Users within the Service’s user platform using the "Share" functions. Access to third-party corpora can be granted with reading rights (viewer), reading and editing rights (contributor) or co-administrator rights (administrator).</p>
|
||||||
|
<p>(3) Users can upload their own language models as part of their user account and use them to analyze uploaded files. After activation by an administrator, Users can also make their uploaded language models publicly available on the platform.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h5>§ 3 Access to the Services</h5>
|
||||||
|
<p>(1) Users register by entering the requested data in the login screen. The User must then select and enter a username and password of their choice.</p>
|
||||||
|
<p>(2) Users are responsible for keeping their username and password confidential.</p>
|
||||||
|
<p>(3) After activation by an administrator, Users can create personal access tokens via an application programming interface (API) and access all user-accessible data. </p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h5>§ 4 General User Obligations</h5>
|
||||||
|
<p>(1) By registering, Users declare their consent to the terms of use. The unlawful uploading, processing, and sharing of content, in particular contrary to criminal law, personal rights regulations, data protection law or copyright regulations, is not permitted. By uploading, editing and/or sharing corresponding texts, corpora and/or analysis results, the respective User declares to be the owner of all rights required in each case.</p>
|
||||||
|
<p>(2) Users are required to use media and texts only in a legally permitted manner and refrain from making illegal statements. This includes the design of their personal user profile and when using the forums on the nopaque platform.</p>
|
||||||
|
<p>(3) The Services offered are only suitable for the processing of data without heightened security requirements. Data that is subject to special data protection requirements (e.g. health data, see Art. 9 GDPR) may not be stored or processed.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h5>§ 5 User liability</h5>
|
||||||
|
<p>(1) If claims are asserted against Bielefeld University by third parties due to the use of an unauthorized User (see § 1 para. 2) or due to unauthorized use (see § 4 para. 1, 2) of texts, analysis results or other media (e.g. an uploaded profile picture) within the scope of the services offered, the User responsible for the infringement shall indemnify Bielefeld University against these claims.</p>
|
||||||
|
<p>(2) Users are liable for any damage they incur regarding the confidentiality and disclosure of access data. If claims are asserted against Bielefeld University by third parties due to unauthorized use of access data/tokens, the User responsible for the infringement shall indemnify Bielefeld University against these claims.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h5>§ 6 User rights of use regarding generated corpora and analysis reports</h5>
|
||||||
|
<p>(1) Within the limits of § 4, the User is entitled to prepare texts with the web application, to view them within the web application and to download them. Regulations on sharing the corpora created in this way are set out in § 7 of these GTC</p>
|
||||||
|
<p>(2) The User is also entitled to display, download, reproduce, and publish analysis reports generated based on the prepared texts for the purposes of teaching and research within the limits of § 4. These rights are only granted to the Users that generate the corresponding analysis reports. The authorization is subject to the resolutive condition that in the context of corresponding reproductions/publications of analysis reports or their excerpts, the following is noted as the source: nopaque [Software]. (2020). SFB 1288 - Subproject INF, Bielefeld University. https://nopaque.uni-bielefeld.de/.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h5>§ 7 Sharing previously created corpora</h5>
|
||||||
|
<p>(1) Sharing of corpora is possible in the following ways:</p>
|
||||||
|
<ol type="a">
|
||||||
|
<li>A link can be generated allowing other registered Users to access another User’s corpora stored on the servers of the Service.</li>
|
||||||
|
<li>Corpora can be added to a list visible to all other Users. In the context of this list, other Users can find information about the available content and request access by contacting the relevant creator. The creator can then decide, on their own responsibility, whether the requested access is permissible and can allow the requesting User to access the corpora – until this access is revoked – by sharing a link as described above.</li>
|
||||||
|
</ol>
|
||||||
|
<p>(2) With regard to the sharing of corpora containing copyright-protected media, § 60d UrhG must be observed most particularly. The User is required to comply with the deletion periods stated therein. It is expressly pointed out that the improper distribution of corpora or source texts can lead to breaches of duty in accordance with § 4, the financial loss of which is to be borne by the respective users.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h5>§ 8 Availability and support</h5>
|
||||||
|
<p>(1) The University has the right to conduct maintenance during operating times as far as it is in user interest. Disruption of data access may occur during maintenance, which the University will minimize as much as possible. </p>
|
||||||
|
<p>(2) The University has the right to change and/or modify the range of functions of the Services without prior notification of its Users. The University can, without being required to do so, update or develop the software at any time, especially due to changed laws and regulations, technical developments, scientific or thematic restructuring of the project or for IT security improvements.</p>
|
||||||
|
<p>(3) Users are provided with a support contact form on the website.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h5>§ 9 Defect rights and liability of the University</h5>
|
||||||
|
<p>(1) The University does not guarantee that the information provided by the service is correct, up-to-date or usable or that it will lead to the desired results. In this respect, the University accepts no liability whatsoever.</p>
|
||||||
|
<p>(2) The Services are provided based on “best effort” practices according to customary operational diligence. The University does not guarantee uninterrupted, error-free system operation. Possible data loss due to technical disruptions cannot be ruled out. The University is not liable for data loss based on user neglect to create data backups and to ensure that lost data can be restored with reasonable expense and effort.</p>
|
||||||
|
<p>(3) Otherwise, the University shall only be liable in the event of wrongful intent or gross negligence on the part of its employees, unless there is a culpable breach of material obligations within the scope of these Terms and Conditions of use. In this case, liability is limited to typical damages foreseeable at the time the user relationship was established; the University is not liable for other damages, e.g. for loss of profit, for loss of production, for other indirect damages or for loss of data and information. Liability for damage resulting from injury to life, body or health remains unaffected.</p>
|
||||||
|
<p>(4) Possible official liability claims against the university remain unaffected by the above provisions.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h5>§ 10 Wrongful use</h5>
|
||||||
|
<p>The University reserves the right to investigate suspected misuse or significant violations of these Terms and Conditions, to take appropriate precautions and, in the event of reasonable suspicion, to block the User's access to the Services – at least until the user has cleared up the suspicion – and/or, if necessary, to delete the user account in the event of particularly serious violations. If the User clears up the suspicion, the block will be lifted. Every User must notify the University immediately of any indications of misuse of the service.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h5>§ 11 Applicable law</h5>
|
||||||
|
<p>The law of the Federal Republic of Germany shall apply exclusively. The mandatory provisions of the country in which you have your habitual residence remain unaffected</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h5>§ 12 Place of jurisdiction</h5>
|
||||||
|
<p>If you do not have a place of residence in the Federal Republic of Germany, if you move your place of residence abroad after registration, or if your place of residence is not known at the time the action is filed, the place of jurisdiction for all disputes arising from and in connection with the user relationship shall be the local ("Amtsgericht") or regional ("Landgericht") court of Bielefeld.</p>
|
||||||
|
</div>
|
117
app/templates/main/_terms_of_use/german.html.j2
Normal file
117
app/templates/main/_terms_of_use/german.html.j2
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<div class="section">
|
||||||
|
<p>Mit Nutzung der Plattform nopaque stimmen Sie den Allgemeinen Geschäftsbedingungen sowie der Zurkenntnisnahme des rechtlichen Rahmens und der Datenschutzerklärung.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h5>§ 1 Inhalt und Umfang des Serviceangebots</h5>
|
||||||
|
<p>(1) Diese Allgemeinen Geschäftsbedingungen gelten für die Nutzung der Angebote des browserbasierten Datenverarbeitungs- und Textanalysetools „nopaque“ (nachfolgend „Serviceangebot“), der Universität Bielefeld (nachfolgend Universität), das von berechtigten Nutzern kostenlos über die Internetseite www.nopaque.uni-bielefeld.de verwendet werden kann.</p>
|
||||||
|
<p>(2) berechtigt zur Nutzung des Serviceangebotes sind lediglich</p>
|
||||||
|
<ol type="a">
|
||||||
|
<li>Studierende, Lehrende und Beschäftigte der Universität Bielefeld</li>
|
||||||
|
<li>Externe Wissenschaftler*innen im Rahmen nichtkommerzieller Forschung</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h5>§ 2 Serviceangebot</h5>
|
||||||
|
<p>(1) Im Rahmen des Serviceangebots können von den Nutzern hochzuladende Daten automatisiert verarbeitet und analysiert werden. Dies ist in Form von folgenden, einzeln ansteuerbaren, Services umgesetzt:</p>
|
||||||
|
<ol type="a">
|
||||||
|
<li><b>File Setup</b>: Digitale Kopien von textbasierten Forschungsdaten (Bücher, Briefe usw.) bestehen oft aus verschiedenen Dateien und Formaten. Nopaque konvertiert und führt diese Dateien zusammen, um die Weiterverarbeitung und die Anwendung anderer Dienste zu erleichtern.</li>
|
||||||
|
<li><b>Optical Character Recognition</b> (OCR): nopaque wandelt Ihre Bilddaten - wie Fotos oder Scans - durch OCR in Textdaten um und macht sie maschinenlesbar. Dieser Schritt ermöglicht es Ihnen, mit der weiteren rechnerischen Analyse Ihrer Dokumente fortzufahren.</li>
|
||||||
|
<li><b>Handwritten Text Recognition</b> (HTR): nopaque wandelt Ihre Bilddaten von handschriftlichen Texten - wie Fotos oder Scans - mittels HTR in Textdaten um und macht sie maschinenlesbar. Dieser Schritt ermöglicht es Ihnen, mit der weiteren rechnerischen Analyse Ihrer Dokumente fortzufahren.</li>
|
||||||
|
<li><b>Natural Language Processing</b> (NLP): Mittels computergestützter linguistischer Datenverarbeitung (Tokenisierung, Lemmatisierung, Part-of-Speech-Tagging und Named-Entity-Erkennung) extrahiert nopaque zusätzliche Informationen aus Ihrem Text.</li>
|
||||||
|
<li><b>Corpus analysis</b>: Mit nopaque können Sie so viele Textkorpora erstellen und hochladen, wie Sie möchten. Es nutzt die CQP Query Language, die komplexe Suchanfragen mit Hilfe von Metadaten und NLP-Tags ermöglicht.</li>
|
||||||
|
</ol>
|
||||||
|
<p>(2) Der Nutzer ist auch berechtigt, im Rahmen des Serviceangebots erstellte Textaufbereitungen (Corpora) innerhalb der Nutzerplattform des Serviceangebots durch die „Teilen“-Funktionen mit anderen registrierten Nutzern zu teilen. Ein Zugang zu fremden Corpora kann jeweils mit Leserechten (Viewer), mit Lese-und Bearbeitungsrechten (Contributor) oder Co-Administratorrechten (Administrator) eingeräumt werden.</p>
|
||||||
|
<p>(3) Die Nutzer können im Rahmen Ihres Nutzerkontos eigene Sprachmodelle hochladen und diese zur Analyse hochgeladener Dateien verwenden. Nach Freischaltung durch einen Administrator können Nutzer ihre hochgeladenen Sprachmodelle auch öffentlich im Rahmen der Plattform zur Verfügung stellen.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h5>§ 3 Zugang zum Serviceangebot</h5>
|
||||||
|
<p>(1) Die Nutzer registrieren sich über die Eingabe der abgefragten Daten in der Anmeldemaske. Danach muss der Nutzer einen von ihm gewählten Benutzernamen und ein Passwort bestimmen und eingeben.</p>
|
||||||
|
<p>(2) Der Nutzer ist für die Geheimhaltung des Benutzernamens sowie des Passwortes selbst verantwortlich.</p>
|
||||||
|
<p>(3) Nach Freischaltung durch einen Administrator können Nutzer durch eine API-Schnittstelle auch persönliche Zugangstoken erstellen und auf alle Daten zugreifen kann, auf die der Nutzer selbst zugreifen kann.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h5>§ 4 Allgemeine Pflichten der Nutzer</h5>
|
||||||
|
<p>(1) Mit der Registrierung erklären die Nutzer Ihre Zustimmung zu den Nutzungsbedingungen. Das rechtswidrige Hochladen, Verarbeiten und Teilen von Inhalten insbesondere entgegen strafrechtlicher, persönlichkeitsrechtlicher, datenschutzrechtlicher oder urheberrechtlicher Regelungen ist nicht gestattet. Durch das Hochladen, bearbeiten und/oder Teilen entsprechender Texte, Corpora und/oder Analyseergebnisse erklärt der jeweilige Nutzer, Inhaber aller dazu jeweils erforderlichen Rechte zu sein.</p>
|
||||||
|
<p>(2) Die Nutzer verpflichten sich, auch im Rahmen der Gestaltung des persönlichen Nutzerprofils und der Nutzung von Foren der Plattform, Medien und Texte nur auf gesetzlich erlaubte Art und Weise zu verwenden und illegale Äußerungen zu unterlassen.</p>
|
||||||
|
<p>(3) Das Serviceangebot ist lediglich zur Verarbeitung von Daten ohne erhöhte Schutzbedürfnisse geeignet. Daten, welche besonderen datenschutzrechtlichen Anforderungen unterliegen (bspw. Gesundheitsdaten, siehe Art. 9 DSGVO) dürfen nicht gespeichert oder verarbeitet werden.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h5>§ 5 Haftung der Nutzer</h5>
|
||||||
|
<p>(1) Sofern aufgrund der Nutzung eines nicht berechtigten Nutzers (siehe hierzu § 1 Abs. 2) oder aufgrund einer unerlaubten Nutzung (siehe hierzu § 4 Abs. 1, 2) von Texten, Analyseergebnissen oder sonstigen Medien (bspw. einem hochgeladenen Profilbild) im Rahmen des Serviceangebots Ansprüche Dritter gegen die Universität Bielefeld geltend gemacht werden, stellt der für die Rechtsverletzung verantwortliche Nutzer die Universität von diesen Ansprüchen frei.</p>
|
||||||
|
<p>(2) Die Nutzer haften bezgl. der Geheimhaltung und Weitergabe von Zugangsdaten für jegliche Schäden, die Ihnen entstehen. Sofern aufgrund einer unerlaubten Verwendung von Zugangsdaten/Token Ansprüche Dritter gegen die Universität Bielefeld geltend gemacht werden, stellt der für die Rechtsverletzung verantwortliche Nutzer die Universität von diesen Ansprüchen frei.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h5>§ 6 Nutzungsrechte des Nutzers bzgl. erstellter Corpora und Analyseberichte</h5>
|
||||||
|
<p>(1) Der Nutzer ist in den Grenzen des § 4 berechtigt, Texte mit der Webanwendung aufzubereiten, im Rahmen der Webanwendung einzusehen und herunterzuladen. Regelungen zum Teilen der so erstellten Corpora ergeben sich nach § 7 dieser AGB.</p>
|
||||||
|
<p>(2) Der Nutzer ist auch berechtigt, den jeweils auf Grundlage der aufbereiteten Texte zur Verfügung gestellten Analysebericht zu Zwecken von Forschung und Lehre in den Grenzen des § 4 im Rahmen der Online-Anwendung zu lesen, herunterzuladen, zu vervielfältigen und zu veröffentlichen. Die Berechtigung steht nur denjenigen Nutzern zu, welche die entsprechenden Analyseberichte generieren. Die Berechtigung steht unter der auflösenden Bedingung, dass im Rahmen entsprechender Vervielfältigungen/Veröffentlichungen von Analyseberichten oder deren Ausschnitten als Quellenangabe vermerkt wird: nopaque [Software]. (2020). SFB 1288 – Teilprojekt INF, Universität Bielefeld. https://nopaque.uni-bielefeld.de/</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h5>§ 7 Teilen von zuvor erstellten Corpora</h5>
|
||||||
|
<p>(1) Das Teilen von Corpora wird auf folgende Art und Weise ermöglicht:</p>
|
||||||
|
<ol type="a">
|
||||||
|
<li>Es kann ein Link generiert werden, welcher anderen angemeldeten Nutzern Zugang zu den auf den Servern des Serviceangebots gespeicherten Corpora eines Nutzers ermöglicht.</li>
|
||||||
|
<li>Corpora können in eine durch alle Nutzer einsehbare Liste eingefügt werden, in deren Rahmen sich andere Nutzer über den vorhandene Bestand informieren und durch Kontaktierung des jeweiligen Erstellers einen Zugang erbitten können. Der Ersteller entscheidet dann in eigener Verantwortung über die Zulässigkeit des erbetenen Zugangs und ermöglicht bis zum Widerruf den Zugriff auf die Corpora durch die jeweiligen anderen Nutzer durch das Teilen eines Links im obigen Sinne.</li>
|
||||||
|
</ol>
|
||||||
|
<p>(2) In Bezug auf das Teilen von Corpora, welche urheberrechtlich geschützte Medien enthalten, ist insb. § 60d UrhG einzuhalten. Es sind durch den hochladenden Nutzer insb. die dort genannten Löschfristen einzuhalten. Es wird ausdrücklich darauf hingewiesen, dass das regelwidrige Verbreiten von Corpora oder Ausgangstexten zu Pflichtverletzungen nach § 4 führen kann, deren finanzieller Schaden von den jeweiligen Nutzern zu tragen ist.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h5>§ 8 Verfügbarkeit und Support</h5>
|
||||||
|
<p>(1) Die Universität ist berechtigt, soweit es im Interesse des Nutzers erforderlich ist, Wartungsarbeiten auch während der Betriebszeit vorzunehmen. Hierbei kann es zu Störungen des Datenabrufs kommen, die die Universität möglichst geringhalten wird.</p>
|
||||||
|
<p>(2) Die Universität ist berechtigt, den Funktionsumfang des Serviceangebots zu ändern und/oder zu modifizieren ohne die Nutzer zuvor zu benachrichtigen. Die Universität kann, ohne hierzu verpflichtet zu sein, die Software jederzeit aktualisieren oder weiterentwickeln und insbesondere aufgrund geänderter Rechtslage, technischer Entwicklungen, aufgrund einer wissenschaftlichen oder thematischen Umstrukturierung des Projekts oder zur Verbesserung der IT-Sicherheit anpassen.</p>
|
||||||
|
<p>(3) Nutzern wird im Rahmen des Webauftritts ein Supportformular zur Verfügung gestellt.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h5>§ 9 Mängelrechte, Haftung der Universität</h5>
|
||||||
|
<p>(1) Die Universität übernimmt keine Gewähr dafür, dass die durch das Serviceangebot zur Verfügung gestellten Informationen inhaltlich richtig, aktuell oder brauchbar sind oder zu einem gewünschten Erfolg führen. Insoweit ist jegliche Haftung der Universität ausgeschlossen.</p>
|
||||||
|
<p>(2) Das Serviceangebot wird auf Basis der Praxis „Best Effort“ nach betriebsüblicher Sorgfalt zur Verfügung gestellt. Die Universität Bielefeld übernimmt keine Garantie dafür, dass die Systeme fehlerfrei und ohne Unterbrechung laufen. Eventuelle Datenverluste infolge technischer Störungen können nicht ausgeschlossen werden. Für den Verlust von Daten haftet die Universität Bielefeld insoweit nicht, als der Schaden darauf beruht, dass es die Nutzer*innen unterlassen haben, Datensicherungen durchzuführen und dadurch sicherzustellen, dass verloren gegangene Daten mit vertretbarem Aufwand wiederhergestellt werden können.</p>
|
||||||
|
<p>(3) Im Übrigen haftet die Universität nur bei Vorsatz oder grober Fahrlässigkeit ihrer Mitarbeiter*innen, es sei denn, dass eine schuldhafte Verletzung wesentlicher Pflichten im Sinne dieser Nutzungsbedingungen vorliegt. In diesem Fall ist die Haftung auf typische, bei Begründung des Nutzungsverhältnisses vorhersehbare Schäden begrenzt; die Universität haftet nicht für andere Schäden, z.B. für entgangenen Gewinn, für Produktionsausfall, für sonstige mittelbare Schäden oder für Verlust von Daten und Informationen. Die Haftung für Schäden aus der Verletzung des Lebens, des Körpers oder der Gesundheit bleibt unberührt.</p>
|
||||||
|
<p>(4) Mögliche Amtshaftungsansprüche gegen die Hochschule bleiben von den vorstehenden Regelungen unberührt.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h5>§ 10 Missbräuchliche Nutzung</h5>
|
||||||
|
<p>Die Universität behält sich vor, bei Verdacht einer missbräuchlichen Nutzung oder wesentlicher Verletzungen dieser Nutzungsbedingungen diesen Vorgängen nachzugehen, entsprechende Vorkehrungen zu treffen und bei einem begründeten Verdacht gegebenenfalls den Zugang des Nutzers zum Serviceangebot – mindestens bis zu einer Verdachtsausräumung seitens des Nutzers – zu sperren und/oder gegebenenfalls bei besonders schwerwiegenden Verstößen auch das Nutzerkonto zu löschen. Soweit der Nutzer den Verdacht ausräumt, wird die Sperrung aufgehoben. Hinweise auf eine missbräuchliche Nutzung des Serviceangebotes hat jeder Nutzer der Universität unverzüglich mitzuteilen.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h5>§ 11 Anwendbares Recht</h5>
|
||||||
|
<p>Es gilt ausschließlich das Recht der Bundesrepublik Deutschland. Unberührt davon bleiben die zwingenden Bestimmungen des Staates, in dem Sie Ihren gewöhnlichen Aufenthalt haben.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h5>§ 12 Gerichtsstand</h5>
|
||||||
|
<p>Sofern Sie keinen Wohnsitz in der Bundesrepublik Deutschland haben oder nach Anmeldung Ihren Wohnsitz ins Ausland verlegen oder Ihr Wohnsitz zum Zeitpunkt der Klageerhebung nicht bekannt ist, ist Gerichtsstand für alle Streitigkeiten aus und im Zusammenhang mit dem Nutzungsverhältnis das Amts- oder Landgericht Bielefeld.</p>
|
||||||
|
</div>
|
@ -2,40 +2,19 @@
|
|||||||
|
|
||||||
{% block page_content %}
|
{% block page_content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<h1 id="title">{{ title }}</h1>
|
||||||
<div class="col s12">
|
|
||||||
<h1 id="title">{{ title }}</h1>
|
<ul class="tabs tabs-fixed-width z-depth-1">
|
||||||
</div>
|
<li class="tab"><a class="active" href="#terms-of-use-page-content-german">German</a></li>
|
||||||
<div class="col s12">
|
<li class="tab"><a href="#terms-of-use-page-content-english">English</a></li>
|
||||||
<div class="switch">
|
</ul>
|
||||||
<label>
|
|
||||||
DE
|
<div id="terms-of-use-page-content-german">
|
||||||
<input type="checkbox" id="terms-of-use-page-switch">
|
{% include "main/_terms_of_use/german.html.j2" %}
|
||||||
<span class="lever"></span>
|
</div>
|
||||||
EN
|
|
||||||
</label>
|
<div id="terms-of-use-page-content-english">
|
||||||
</div>
|
{% include "main/_terms_of_use/english.html.j2" %}
|
||||||
<br>
|
|
||||||
</div>
|
|
||||||
<div class="terms-of-use-page-content hide">
|
|
||||||
{% include "main/terms_of_use_en.html.j2" %}
|
|
||||||
</div>
|
|
||||||
<div class="terms-of-use-page-content">
|
|
||||||
{% include "main/terms_of_use_de.html.j2" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock page_content %}
|
{% endblock page_content %}
|
||||||
|
|
||||||
{% block scripts %}
|
|
||||||
{{ super() }}
|
|
||||||
<script>
|
|
||||||
let languagePageSwitch = document.querySelector('#terms-of-use-page-switch');
|
|
||||||
let termsOfUsePageContent = document.querySelectorAll('.terms-of-use-page-content');
|
|
||||||
languagePageSwitch.addEventListener('change', function() {
|
|
||||||
termsOfUsePageContent.forEach(content => {
|
|
||||||
content.classList.toggle('hide');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock scripts %}
|
|
||||||
|
@ -1,141 +0,0 @@
|
|||||||
<div class="col s12">
|
|
||||||
<p>Mit Nutzung der Plattform nopaque stimmen Sie den Allgemeinen Geschäftsbedingungen sowie der Zurkenntnisnahme des rechtlichen Rahmens und der Datenschutzerklärung.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col s12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
<span class="card-title">§ 1 Inhalt und Umfang des Serviceangebots</span>
|
|
||||||
<p>(1) Diese Allgemeinen Geschäftsbedingungen gelten für die Nutzung der Angebote des browserbasierten Datenverarbeitungs- und Textanalysetools „nopaque“ (nachfolgend „Serviceangebot“), der Universität Bielefeld (nachfolgend Universität), das von berechtigten Nutzern kostenlos über die Internetseite www.nopaque.uni-bielefeld.de verwendet werden kann.</p>
|
|
||||||
<p>(2) berechtigt zur Nutzung des Serviceangebotes sind lediglich</p>
|
|
||||||
<ol type="a">
|
|
||||||
<li>Studierende, Lehrende und Beschäftigte der Universität Bielefeld</li>
|
|
||||||
<li>Externe Wissenschaftler*innen im Rahmen nichtkommerzieller Forschung</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col s12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
<span class="card-title">§ 2 Serviceangebot</span>
|
|
||||||
<p>(1) Im Rahmen des Serviceangebots können von den Nutzern hochzuladende Daten automatisiert verarbeitet und analysiert werden. Dies ist in Form von folgenden, einzeln ansteuerbaren, Services umgesetzt:</p>
|
|
||||||
<ol type="a">
|
|
||||||
<li><b>File Setup</b>: Digitale Kopien von textbasierten Forschungsdaten (Bücher, Briefe usw.) bestehen oft aus verschiedenen Dateien und Formaten. Nopaque konvertiert und führt diese Dateien zusammen, um die Weiterverarbeitung und die Anwendung anderer Dienste zu erleichtern.</li>
|
|
||||||
<li><b>Optical Character Recognition</b> (OCR): nopaque wandelt Ihre Bilddaten - wie Fotos oder Scans - durch OCR in Textdaten um und macht sie maschinenlesbar. Dieser Schritt ermöglicht es Ihnen, mit der weiteren rechnerischen Analyse Ihrer Dokumente fortzufahren.</li>
|
|
||||||
<li><b>Handwritten Text Recognition</b> (HTR): nopaque wandelt Ihre Bilddaten von handschriftlichen Texten - wie Fotos oder Scans - mittels HTR in Textdaten um und macht sie maschinenlesbar. Dieser Schritt ermöglicht es Ihnen, mit der weiteren rechnerischen Analyse Ihrer Dokumente fortzufahren.</li>
|
|
||||||
<li><b>Natural Language Processing</b> (NLP): Mittels computergestützter linguistischer Datenverarbeitung (Tokenisierung, Lemmatisierung, Part-of-Speech-Tagging und Named-Entity-Erkennung) extrahiert nopaque zusätzliche Informationen aus Ihrem Text.</li>
|
|
||||||
<li><b>Corpus analysis</b>: Mit nopaque können Sie so viele Textkorpora erstellen und hochladen, wie Sie möchten. Es nutzt die CQP Query Language, die komplexe Suchanfragen mit Hilfe von Metadaten und NLP-Tags ermöglicht.</li>
|
|
||||||
</ol>
|
|
||||||
<p>(2) Der Nutzer ist auch berechtigt, im Rahmen des Serviceangebots erstellte Textaufbereitungen (Corpora) innerhalb der Nutzerplattform des Serviceangebots durch die „Teilen“-Funktionen mit anderen registrierten Nutzern zu teilen. Ein Zugang zu fremden Corpora kann jeweils mit Leserechten (Viewer), mit Lese-und Bearbeitungsrechten (Contributor) oder Co-Administratorrechten (Administrator) eingeräumt werden.</p>
|
|
||||||
<p>(3) Die Nutzer können im Rahmen Ihres Nutzerkontos eigene Sprachmodelle hochladen und diese zur Analyse hochgeladener Dateien verwenden. Nach Freischaltung durch einen Administrator können Nutzer ihre hochgeladenen Sprachmodelle auch öffentlich im Rahmen der Plattform zur Verfügung stellen.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col s12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
<span class="card-title">§ 3 Zugang zum Serviceangebot</span>
|
|
||||||
<p>(1) Die Nutzer registrieren sich über die Eingabe der abgefragten Daten in der Anmeldemaske. Danach muss der Nutzer einen von ihm gewählten Benutzernamen und ein Passwort bestimmen und eingeben.</p>
|
|
||||||
<p>(2) Der Nutzer ist für die Geheimhaltung des Benutzernamens sowie des Passwortes selbst verantwortlich.</p>
|
|
||||||
<p>(3) Nach Freischaltung durch einen Administrator können Nutzer durch eine API-Schnittstelle auch persönliche Zugangstoken erstellen und auf alle Daten zugreifen kann, auf die der Nutzer selbst zugreifen kann.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col s12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
<span class="card-title">§ 4 Allgemeine Pflichten der Nutzer</span>
|
|
||||||
<p>(1) Mit der Registrierung erklären die Nutzer Ihre Zustimmung zu den Nutzungsbedingungen. Das rechtswidrige Hochladen, Verarbeiten und Teilen von Inhalten insbesondere entgegen strafrechtlicher, persönlichkeitsrechtlicher, datenschutzrechtlicher oder urheberrechtlicher Regelungen ist nicht gestattet. Durch das Hochladen, bearbeiten und/oder Teilen entsprechender Texte, Corpora und/oder Analyseergebnisse erklärt der jeweilige Nutzer, Inhaber aller dazu jeweils erforderlichen Rechte zu sein.</p>
|
|
||||||
<p>(2) Die Nutzer verpflichten sich, auch im Rahmen der Gestaltung des persönlichen Nutzerprofils und der Nutzung von Foren der Plattform, Medien und Texte nur auf gesetzlich erlaubte Art und Weise zu verwenden und illegale Äußerungen zu unterlassen.</p>
|
|
||||||
<p>(3) Das Serviceangebot ist lediglich zur Verarbeitung von Daten ohne erhöhte Schutzbedürfnisse geeignet. Daten, welche besonderen datenschutzrechtlichen Anforderungen unterliegen (bspw. Gesundheitsdaten, siehe Art. 9 DSGVO) dürfen nicht gespeichert oder verarbeitet werden.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col s12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
<span class="card-title">§ 5 Haftung der Nutzer</span>
|
|
||||||
<p>(1) Sofern aufgrund der Nutzung eines nicht berechtigten Nutzers (siehe hierzu § 1 Abs. 2) oder aufgrund einer unerlaubten Nutzung (siehe hierzu § 4 Abs. 1, 2) von Texten, Analyseergebnissen oder sonstigen Medien (bspw. einem hochgeladenen Profilbild) im Rahmen des Serviceangebots Ansprüche Dritter gegen die Universität Bielefeld geltend gemacht werden, stellt der für die Rechtsverletzung verantwortliche Nutzer die Universität von diesen Ansprüchen frei.</p>
|
|
||||||
<p>(2) Die Nutzer haften bezgl. der Geheimhaltung und Weitergabe von Zugangsdaten für jegliche Schäden, die Ihnen entstehen. Sofern aufgrund einer unerlaubten Verwendung von Zugangsdaten/Token Ansprüche Dritter gegen die Universität Bielefeld geltend gemacht werden, stellt der für die Rechtsverletzung verantwortliche Nutzer die Universität von diesen Ansprüchen frei.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col s12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
<span class="card-title">§ 6 Nutzungsrechte des Nutzers bzgl. erstellter Corpora und Analyseberichte</span>
|
|
||||||
<p>(1) Der Nutzer ist in den Grenzen des § 4 berechtigt, Texte mit der Webanwendung aufzubereiten, im Rahmen der Webanwendung einzusehen und herunterzuladen. Regelungen zum Teilen der so erstellten Corpora ergeben sich nach § 7 dieser AGB.</p>
|
|
||||||
<p>(2) Der Nutzer ist auch berechtigt, den jeweils auf Grundlage der aufbereiteten Texte zur Verfügung gestellten Analysebericht zu Zwecken von Forschung und Lehre in den Grenzen des § 4 im Rahmen der Online-Anwendung zu lesen, herunterzuladen, zu vervielfältigen und zu veröffentlichen. Die Berechtigung steht nur denjenigen Nutzern zu, welche die entsprechenden Analyseberichte generieren. Die Berechtigung steht unter der auflösenden Bedingung, dass im Rahmen entsprechender Vervielfältigungen/Veröffentlichungen von Analyseberichten oder deren Ausschnitten als Quellenangabe vermerkt wird: nopaque [Software]. (2020). SFB 1288 – Teilprojekt INF, Universität Bielefeld. https://nopaque.uni-bielefeld.de/</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col s12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
<span class="card-title">§ 7 Teilen von zuvor erstellten Corpora</span>
|
|
||||||
<p>(1) Das Teilen von Corpora wird auf folgende Art und Weise ermöglicht:</p>
|
|
||||||
<ol type="a">
|
|
||||||
<li>Es kann ein Link generiert werden, welcher anderen angemeldeten Nutzern Zugang zu den auf den Servern des Serviceangebots gespeicherten Corpora eines Nutzers ermöglicht.</li>
|
|
||||||
<li>Corpora können in eine durch alle Nutzer einsehbare Liste eingefügt werden, in deren Rahmen sich andere Nutzer über den vorhandene Bestand informieren und durch Kontaktierung des jeweiligen Erstellers einen Zugang erbitten können. Der Ersteller entscheidet dann in eigener Verantwortung über die Zulässigkeit des erbetenen Zugangs und ermöglicht bis zum Widerruf den Zugriff auf die Corpora durch die jeweiligen anderen Nutzer durch das Teilen eines Links im obigen Sinne.</li>
|
|
||||||
</ol>
|
|
||||||
<p>(2) In Bezug auf das Teilen von Corpora, welche urheberrechtlich geschützte Medien enthalten, ist insb. § 60d UrhG einzuhalten. Es sind durch den hochladenden Nutzer insb. die dort genannten Löschfristen einzuhalten. Es wird ausdrücklich darauf hingewiesen, dass das regelwidrige Verbreiten von Corpora oder Ausgangstexten zu Pflichtverletzungen nach § 4 führen kann, deren finanzieller Schaden von den jeweiligen Nutzern zu tragen ist.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col s12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
<span class="card-title">§ 8 Verfügbarkeit und Support</span>
|
|
||||||
<p>(1) Die Universität ist berechtigt, soweit es im Interesse des Nutzers erforderlich ist, Wartungsarbeiten auch während der Betriebszeit vorzunehmen. Hierbei kann es zu Störungen des Datenabrufs kommen, die die Universität möglichst geringhalten wird.</p>
|
|
||||||
<p>(2) Die Universität ist berechtigt, den Funktionsumfang des Serviceangebots zu ändern und/oder zu modifizieren ohne die Nutzer zuvor zu benachrichtigen. Die Universität kann, ohne hierzu verpflichtet zu sein, die Software jederzeit aktualisieren oder weiterentwickeln und insbesondere aufgrund geänderter Rechtslage, technischer Entwicklungen, aufgrund einer wissenschaftlichen oder thematischen Umstrukturierung des Projekts oder zur Verbesserung der IT-Sicherheit anpassen.</p>
|
|
||||||
<p>(3) Nutzern wird im Rahmen des Webauftritts ein Supportformular zur Verfügung gestellt.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col s12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
<span class="card-title">§ 9 Mängelrechte, Haftung der Universität</span>
|
|
||||||
<p>(1) Die Universität übernimmt keine Gewähr dafür, dass die durch das Serviceangebot zur Verfügung gestellten Informationen inhaltlich richtig, aktuell oder brauchbar sind oder zu einem gewünschten Erfolg führen. Insoweit ist jegliche Haftung der Universität ausgeschlossen.</p>
|
|
||||||
<p>(2) Das Serviceangebot wird auf Basis der Praxis „Best Effort“ nach betriebsüblicher Sorgfalt zur Verfügung gestellt. Die Universität Bielefeld übernimmt keine Garantie dafür, dass die Systeme fehlerfrei und ohne Unterbrechung laufen. Eventuelle Datenverluste infolge technischer Störungen können nicht ausgeschlossen werden. Für den Verlust von Daten haftet die Universität Bielefeld insoweit nicht, als der Schaden darauf beruht, dass es die Nutzer*innen unterlassen haben, Datensicherungen durchzuführen und dadurch sicherzustellen, dass verloren gegangene Daten mit vertretbarem Aufwand wiederhergestellt werden können.</p>
|
|
||||||
<p>(3) Im Übrigen haftet die Universität nur bei Vorsatz oder grober Fahrlässigkeit ihrer Mitarbeiter*innen, es sei denn, dass eine schuldhafte Verletzung wesentlicher Pflichten im Sinne dieser Nutzungsbedingungen vorliegt. In diesem Fall ist die Haftung auf typische, bei Begründung des Nutzungsverhältnisses vorhersehbare Schäden begrenzt; die Universität haftet nicht für andere Schäden, z.B. für entgangenen Gewinn, für Produktionsausfall, für sonstige mittelbare Schäden oder für Verlust von Daten und Informationen. Die Haftung für Schäden aus der Verletzung des Lebens, des Körpers oder der Gesundheit bleibt unberührt.</p>
|
|
||||||
<p>(4) Mögliche Amtshaftungsansprüche gegen die Hochschule bleiben von den vorstehenden Regelungen unberührt.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col s12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
<span class="card-title">§ 10 Missbräuchliche Nutzung</span>
|
|
||||||
<p>Die Universität behält sich vor, bei Verdacht einer missbräuchlichen Nutzung oder wesentlicher Verletzungen dieser Nutzungsbedingungen diesen Vorgängen nachzugehen, entsprechende Vorkehrungen zu treffen und bei einem begründeten Verdacht gegebenenfalls den Zugang des Nutzers zum Serviceangebot – mindestens bis zu einer Verdachtsausräumung seitens des Nutzers – zu sperren und/oder gegebenenfalls bei besonders schwerwiegenden Verstößen auch das Nutzerkonto zu löschen. Soweit der Nutzer den Verdacht ausräumt, wird die Sperrung aufgehoben. Hinweise auf eine missbräuchliche Nutzung des Serviceangebotes hat jeder Nutzer der Universität unverzüglich mitzuteilen.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col s12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
<span class="card-title">§ 11 Anwendbares Recht</span>
|
|
||||||
<p>Es gilt ausschließlich das Recht der Bundesrepublik Deutschland. Unberührt davon bleiben die zwingenden Bestimmungen des Staates, in dem Sie Ihren gewöhnlichen Aufenthalt haben.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col s12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
<span class="card-title">§ 12 Gerichtsstand</span>
|
|
||||||
<p>Sofern Sie keinen Wohnsitz in der Bundesrepublik Deutschland haben oder nach Anmeldung Ihren Wohnsitz ins Ausland verlegen oder Ihr Wohnsitz zum Zeitpunkt der Klageerhebung nicht bekannt ist, ist Gerichtsstand für alle Streitigkeiten aus und im Zusammenhang mit dem Nutzungsverhältnis das Amts- oder Landgericht Bielefeld.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,150 +0,0 @@
|
|||||||
<div class="col s12">
|
|
||||||
<div class="card red darken-2">
|
|
||||||
<div class="card-content white-text">
|
|
||||||
<span class="card-title">Notice</span>
|
|
||||||
<p>This page is translated for the understanding of English-speaking users. However, only the version of the German Terms of Use in accordance with German law applies. </p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col s12">
|
|
||||||
<p>With the usage of the nopaque platform you declare your acceptance of the General Terms of Use and that you have taken note of the legal framework and the data protection declaration.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col s12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
<span class="card-title">§ 1 Content and scope of the Services offered</span>
|
|
||||||
<p>(1) These General Terms and Conditions apply to the use of the Services offered in the browser-based data processing and text analysis tool "nopaque" (hereinafter "Services") by Bielefeld University (hereinafter "University"), which can be used free of charge by authorized Users via the website www.nopaque.uni-bielefeld.de.</p>
|
|
||||||
<p>(2) The authorized use of these services is exclusively granted to:</p>
|
|
||||||
<ol type="a">
|
|
||||||
<li>Students, teaching staff, and employees of Bielefeld University</li>
|
|
||||||
<li>External researchers for use in non-commercial research</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col s12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
<span class="card-title">§ 2 Services</span>
|
|
||||||
<p>(1) As part of the Services offered, user-uploaded data can be processed and analyzed automatically. This is implemented in the form of the following Services, each of which can be executed individually:</p>
|
|
||||||
<ol type="a">
|
|
||||||
<li><b>File Setup</b>: Digital copies of text-based research data (books, letters, etc.) often consist of different files and formats. nopaque converts and merges these files to facilitate further processing and the use of other services.</li>
|
|
||||||
<li><b>Optical Character Recognition</b> (OCR): nopaque converts image data - such as photos or scans - into text data using OCR and makes it machine-readable. This step enables the further computational analysis of documents.</li>
|
|
||||||
<li><b>Handwritten Text Recognition</b> (HTR): nopaque converts image data from handwritten text - such as photos or scans - into text data using HTR and makes it machine-readable. This step enables further computational analysis of documents.</li>
|
|
||||||
<li><b>Natural Language Processing</b> (NLP): Using computer-aided linguistic data processing (tokenization, lemmatization, part-of-speech tagging and named entity recognition), nopaque extracts additional information from text.</li>
|
|
||||||
<li><b>Corpus analysis</b>: With nopaque, one can create and upload as many text corpora as desired. It uses the CQP Query Language, which enables complex search queries using metadata and NLP tags.</li>
|
|
||||||
</ol>
|
|
||||||
<p>(2) The User is also entitled to share text edits (corpora) created as part of the Services offered with other registered Users within the Service’s user platform using the "Share" functions. Access to third-party corpora can be granted with reading rights (viewer), reading and editing rights (contributor) or co-administrator rights (administrator).</p>
|
|
||||||
<p>(3) Users can upload their own language models as part of their user account and use them to analyze uploaded files. After activation by an administrator, Users can also make their uploaded language models publicly available on the platform.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col s12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
<span class="card-title">§ 3 Access to the Services</span>
|
|
||||||
<p>(1) Users register by entering the requested data in the login screen. The User must then select and enter a username and password of their choice.</p>
|
|
||||||
<p>(2) Users are responsible for keeping their username and password confidential.</p>
|
|
||||||
<p>(3) After activation by an administrator, Users can create personal access tokens via an application programming interface (API) and access all user-accessible data. </p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col s12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
<span class="card-title">§ 4 General User Obligations</span>
|
|
||||||
<p>(1) By registering, Users declare their consent to the terms of use. The unlawful uploading, processing, and sharing of content, in particular contrary to criminal law, personal rights regulations, data protection law or copyright regulations, is not permitted. By uploading, editing and/or sharing corresponding texts, corpora and/or analysis results, the respective User declares to be the owner of all rights required in each case.</p>
|
|
||||||
<p>(2) Users are required to use media and texts only in a legally permitted manner and refrain from making illegal statements. This includes the design of their personal user profile and when using the forums on the nopaque platform.</p>
|
|
||||||
<p>(3) The Services offered are only suitable for the processing of data without heightened security requirements. Data that is subject to special data protection requirements (e.g. health data, see Art. 9 GDPR) may not be stored or processed.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col s12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
<span class="card-title">§ 5 User liability</span>
|
|
||||||
<p>(1) If claims are asserted against Bielefeld University by third parties due to the use of an unauthorized User (see § 1 para. 2) or due to unauthorized use (see § 4 para. 1, 2) of texts, analysis results or other media (e.g. an uploaded profile picture) within the scope of the services offered, the User responsible for the infringement shall indemnify Bielefeld University against these claims.</p>
|
|
||||||
<p>(2) Users are liable for any damage they incur regarding the confidentiality and disclosure of access data. If claims are asserted against Bielefeld University by third parties due to unauthorized use of access data/tokens, the User responsible for the infringement shall indemnify Bielefeld University against these claims.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col s12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
<span class="card-title">§ 6 User rights of use regarding generated corpora and analysis reports</span>
|
|
||||||
<p>(1) Within the limits of § 4, the User is entitled to prepare texts with the web application, to view them within the web application and to download them. Regulations on sharing the corpora created in this way are set out in § 7 of these GTC</p>
|
|
||||||
<p>(2) The User is also entitled to display, download, reproduce, and publish analysis reports generated based on the prepared texts for the purposes of teaching and research within the limits of § 4. These rights are only granted to the Users that generate the corresponding analysis reports. The authorization is subject to the resolutive condition that in the context of corresponding reproductions/publications of analysis reports or their excerpts, the following is noted as the source: nopaque [Software]. (2020). SFB 1288 - Subproject INF, Bielefeld University. https://nopaque.uni-bielefeld.de/.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col s12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
<span class="card-title">§ 7 Sharing previously created corpora</span>
|
|
||||||
<p>(1) Sharing of corpora is possible in the following ways:</p>
|
|
||||||
<ol type="a">
|
|
||||||
<li>A link can be generated allowing other registered Users to access another User’s corpora stored on the servers of the Service.</li>
|
|
||||||
<li>Corpora can be added to a list visible to all other Users. In the context of this list, other Users can find information about the available content and request access by contacting the relevant creator. The creator can then decide, on their own responsibility, whether the requested access is permissible and can allow the requesting User to access the corpora – until this access is revoked – by sharing a link as described above.</li>
|
|
||||||
</ol>
|
|
||||||
<p>(2) With regard to the sharing of corpora containing copyright-protected media, § 60d UrhG must be observed most particularly. The User is required to comply with the deletion periods stated therein. It is expressly pointed out that the improper distribution of corpora or source texts can lead to breaches of duty in accordance with § 4, the financial loss of which is to be borne by the respective users.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col s12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
<span class="card-title">§ 8 Availability and support</span>
|
|
||||||
<p>(1) The University has the right to conduct maintenance during operating times as far as it is in user interest. Disruption of data access may occur during maintenance, which the University will minimize as much as possible. </p>
|
|
||||||
<p>(2) The University has the right to change and/or modify the range of functions of the Services without prior notification of its Users. The University can, without being required to do so, update or develop the software at any time, especially due to changed laws and regulations, technical developments, scientific or thematic restructuring of the project or for IT security improvements.</p>
|
|
||||||
<p>(3) Users are provided with a support contact form on the website.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col s12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
<span class="card-title">§ 9 Defect rights and liability of the University</span>
|
|
||||||
<p>(1) The University does not guarantee that the information provided by the service is correct, up-to-date or usable or that it will lead to the desired results. In this respect, the University accepts no liability whatsoever.</p>
|
|
||||||
<p>(2) The Services are provided based on “best effort” practices according to customary operational diligence. The University does not guarantee uninterrupted, error-free system operation. Possible data loss due to technical disruptions cannot be ruled out. The University is not liable for data loss based on user neglect to create data backups and to ensure that lost data can be restored with reasonable expense and effort.</p>
|
|
||||||
<p>(3) Otherwise, the University shall only be liable in the event of wrongful intent or gross negligence on the part of its employees, unless there is a culpable breach of material obligations within the scope of these Terms and Conditions of use. In this case, liability is limited to typical damages foreseeable at the time the user relationship was established; the University is not liable for other damages, e.g. for loss of profit, for loss of production, for other indirect damages or for loss of data and information. Liability for damage resulting from injury to life, body or health remains unaffected.</p>
|
|
||||||
<p>(4) Possible official liability claims against the university remain unaffected by the above provisions.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col s12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
<span class="card-title">§ 10 Wrongful use</span>
|
|
||||||
<p>The University reserves the right to investigate suspected misuse or significant violations of these Terms and Conditions, to take appropriate precautions and, in the event of reasonable suspicion, to block the User's access to the Services – at least until the user has cleared up the suspicion – and/or, if necessary, to delete the user account in the event of particularly serious violations. If the User clears up the suspicion, the block will be lifted. Every User must notify the University immediately of any indications of misuse of the service.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col s12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
<span class="card-title">§ 11 Applicable law</span>
|
|
||||||
<p>The law of the Federal Republic of Germany shall apply exclusively. The mandatory provisions of the country in which you have your habitual residence remain unaffected</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col s12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
<span class="card-title">§ 12 Place of jurisdiction</span>
|
|
||||||
<p>If you do not have a place of residence in the Federal Republic of Germany, if you move your place of residence abroad after registration, or if your place of residence is not known at the time the action is filed, the place of jurisdiction for all disputes arising from and in connection with the user relationship shall be the local ("Amtsgericht") or regional ("Landgericht") court of Bielefeld.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -26,7 +26,6 @@
|
|||||||
<div class="corpus-list" data-user-id="{{ current_user.hashid }}"></div>
|
<div class="corpus-list" data-user-id="{{ current_user.hashid }}"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-action right-align">
|
<div class="card-action right-align">
|
||||||
<a class="btn service-color darken disabled waves-effect waves-light" href="{{ url_for('corpora.import_corpus') }}">Import Corpus<i class="material-icons right">import_export</i></a>
|
|
||||||
<a class="btn service-color darken waves-effect waves-light" href="{{ url_for('corpora.create_corpus') }}">Create corpus<i class="material-icons right">add</i></a>
|
<a class="btn service-color darken waves-effect waves-light" href="{{ url_for('corpora.create_corpus') }}">Create corpus<i class="material-icons right">add</i></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -46,19 +46,19 @@
|
|||||||
<div class="row" style="margin-left: 24px;">
|
<div class="row" style="margin-left: 24px;">
|
||||||
<div class="col s12 l3">
|
<div class="col s12 l3">
|
||||||
<label>
|
<label>
|
||||||
<input {% if user.has_profile_privacy_setting('SHOW_EMAIL') %}checked{% endif %} class="profile-privacy-setting-checkbox" data-profile-privacy-setting-name="SHOW_EMAIL" {% if not user.is_public %}disabled{% endif %} type="checkbox">
|
<input {% if user.has_profile_privacy_setting('SHOW_EMAIL') %}checked{% endif %} class="profile-privacy-setting-checkbox" data-profile-privacy-setting-name="ProfileShowEmail" {% if not user.is_public %}disabled{% endif %} type="checkbox">
|
||||||
<span>Email</span>
|
<span>Email</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col s12 l3">
|
<div class="col s12 l3">
|
||||||
<label>
|
<label>
|
||||||
<input {% if user.has_profile_privacy_setting('SHOW_LAST_SEEN') %}checked{% endif %} class="profile-privacy-setting-checkbox" data-profile-privacy-setting-name="SHOW_LAST_SEEN" {% if not user.is_public %}disabled{% endif %} type="checkbox">
|
<input {% if user.has_profile_privacy_setting('SHOW_LAST_SEEN') %}checked{% endif %} class="profile-privacy-setting-checkbox" data-profile-privacy-setting-name="ProfileShowLastSeen" {% if not user.is_public %}disabled{% endif %} type="checkbox">
|
||||||
<span>Last seen</span>
|
<span>Last seen</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col s12 l3">
|
<div class="col s12 l3">
|
||||||
<label>
|
<label>
|
||||||
<input {% if user.has_profile_privacy_setting('SHOW_MEMBER_SINCE') %}checked{% endif %} class="profile-privacy-setting-checkbox" data-profile-privacy-setting-name="SHOW_MEMBER_SINCE" {% if not user.is_public %}disabled{% endif %} type="checkbox">
|
<input {% if user.has_profile_privacy_setting('SHOW_MEMBER_SINCE') %}checked{% endif %} class="profile-privacy-setting-checkbox" data-profile-privacy-setting-name="ProfileShowMemberSince" {% if not user.is_public %}disabled{% endif %} type="checkbox">
|
||||||
<span>Member since</span>
|
<span>Member since</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@ -74,7 +74,7 @@
|
|||||||
<form method="POST">
|
<form method="POST">
|
||||||
{{ update_profile_information_form.hidden_tag() }}
|
{{ update_profile_information_form.hidden_tag() }}
|
||||||
{{ wtf.render_field(update_profile_information_form.full_name, material_icon='badge') }}
|
{{ wtf.render_field(update_profile_information_form.full_name, material_icon='badge') }}
|
||||||
{{ wtf.render_field(update_profile_information_form.about_me, material_icon='description', id='about-me-textfield') }}
|
{{ wtf.render_field(update_profile_information_form.about_me, material_icon='description') }}
|
||||||
{{ wtf.render_field(update_profile_information_form.website, material_icon='laptop') }}
|
{{ wtf.render_field(update_profile_information_form.website, material_icon='laptop') }}
|
||||||
{{ wtf.render_field(update_profile_information_form.organization, material_icon='business') }}
|
{{ wtf.render_field(update_profile_information_form.organization, material_icon='business') }}
|
||||||
{{ wtf.render_field(update_profile_information_form.location, material_icon='location_on') }}
|
{{ wtf.render_field(update_profile_information_form.location, material_icon='location_on') }}
|
||||||
@ -172,8 +172,6 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% block admin_settings %}{% endblock admin_settings %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock page_content %}
|
{% endblock page_content %}
|
||||||
@ -252,28 +250,28 @@ for (let collapsibleElement of document.querySelectorAll('.collapsible.no-autoin
|
|||||||
// #region Profile Privacy settings
|
// #region Profile Privacy settings
|
||||||
let profileIsPublicSwitchElement = document.querySelector('#profile-is-public-switch');
|
let profileIsPublicSwitchElement = document.querySelector('#profile-is-public-switch');
|
||||||
let profilePrivacySettingCheckboxElements = document.querySelectorAll('.profile-privacy-setting-checkbox');
|
let profilePrivacySettingCheckboxElements = document.querySelectorAll('.profile-privacy-setting-checkbox');
|
||||||
profileIsPublicSwitchElement.addEventListener('change', (event) => {
|
profileIsPublicSwitchElement.addEventListener('change', async (event) => {
|
||||||
let newEnabled = profileIsPublicSwitchElement.checked;
|
const newEnabled = profileIsPublicSwitchElement.checked;
|
||||||
nopaque.requests.users.entity.settings.profilePrivacy.update({{ user.hashid|tojson }}, 'is-public', newEnabled)
|
try {
|
||||||
.then(
|
const message = await app.settings.updateProfileIsPublic(newEnabled);
|
||||||
(response) => {
|
for (let profilePrivacySettingCheckboxElement of profilePrivacySettingCheckboxElements) {
|
||||||
for (let profilePrivacySettingCheckboxElement of document.querySelectorAll('.profile-privacy-setting-checkbox')) {
|
profilePrivacySettingCheckboxElement.disabled = !newEnabled;
|
||||||
profilePrivacySettingCheckboxElement.disabled = !newEnabled;
|
}
|
||||||
}
|
app.ui.flash(message);
|
||||||
},
|
} catch (e) {
|
||||||
(response) => {
|
profileIsPublicSwitchElement.checked = !newEnabled;
|
||||||
profileIsPublicSwitchElement.checked = !newEnabled;
|
app.ui.flash(e.message, 'error');
|
||||||
}
|
}
|
||||||
);
|
|
||||||
});
|
});
|
||||||
for (let profilePrivacySettingCheckboxElement of profilePrivacySettingCheckboxElements) {
|
for (let profilePrivacySettingCheckboxElement of profilePrivacySettingCheckboxElements) {
|
||||||
profilePrivacySettingCheckboxElement.addEventListener('change', (event) => {
|
profilePrivacySettingCheckboxElement.addEventListener('change', async (event) => {
|
||||||
let newEnabled = profilePrivacySettingCheckboxElement.checked;
|
const newEnabled = profilePrivacySettingCheckboxElement.checked;
|
||||||
let valueName = profilePrivacySettingCheckboxElement.dataset.profilePrivacySettingName;
|
const valueName = profilePrivacySettingCheckboxElement.dataset.profilePrivacySettingName;
|
||||||
nopaque.requests.users.entity.settings.profilePrivacy.update({{ user.hashid|tojson }}, valueName, newEnabled)
|
try {
|
||||||
.catch((response) => {
|
app.settings[`update${valueName}`](newEnabled)
|
||||||
|
} catch (error) {
|
||||||
profilePrivacySettingCheckboxElement.checked = !newEnabled;
|
profilePrivacySettingCheckboxElement.checked = !newEnabled;
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// #endregion Profile Privacy settings
|
// #endregion Profile Privacy settings
|
@ -7,6 +7,8 @@
|
|||||||
{{ render_integer_field(field, *args, **kwargs) }}
|
{{ render_integer_field(field, *args, **kwargs) }}
|
||||||
{% elif field.type == 'MultipleFileField' %}
|
{% elif field.type == 'MultipleFileField' %}
|
||||||
{{ render_multiple_file_field(field, *args, **kwargs) }}
|
{{ render_multiple_file_field(field, *args, **kwargs) }}
|
||||||
|
{% elif field.type == 'StringField' %}
|
||||||
|
{{ render_string_field(field, *args, **kwargs) }}
|
||||||
{% elif field.type == 'SubmitField' %}
|
{% elif field.type == 'SubmitField' %}
|
||||||
{{ render_submit_field(field, *args, **kwargs) }}
|
{{ render_submit_field(field, *args, **kwargs) }}
|
||||||
{% elif field.type == 'TextAreaField' %}
|
{% elif field.type == 'TextAreaField' %}
|
||||||
@ -20,7 +22,7 @@
|
|||||||
{% macro render_boolean_field(field) %}
|
{% macro render_boolean_field(field) %}
|
||||||
<div>
|
<div>
|
||||||
<label>
|
<label>
|
||||||
<input id="{{ field.id }}" name="{{ field.name }}" type="checkbox">
|
{{ field(*args, **kwargs) }}
|
||||||
<span>{{ field.label.text }}</span>
|
<span>{{ field.label.text }}</span>
|
||||||
{% for error in field.errors %}
|
{% for error in field.errors %}
|
||||||
<span class="helper-text error-color-text">{{ error }}</span>
|
<span class="helper-text error-color-text">{{ error }}</span>
|
||||||
@ -36,25 +38,7 @@
|
|||||||
<div class="file-field input-field">
|
<div class="file-field input-field">
|
||||||
<div class="btn">
|
<div class="btn">
|
||||||
<span>{{ field.label.text }}</span>
|
<span>{{ field.label.text }}</span>
|
||||||
<input id="{{ field.id }}" name="{{ field.name }}" type="file">
|
{{ field(*args, **kwargs) }}
|
||||||
</div>
|
|
||||||
<div class="file-path-wrapper">
|
|
||||||
<input class="file-path validate" type="text" placeholder="{{ placeholder }}">
|
|
||||||
</div>
|
|
||||||
{% for error in field.errors %}
|
|
||||||
<span class="helper-text error-color-text">{{ error }}</span>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
|
|
||||||
{% macro render_multiple_file_field(field) %}
|
|
||||||
{% set placeholder = kwargs.pop('placeholder', '') %}
|
|
||||||
|
|
||||||
<div class="file-field input-field">
|
|
||||||
<div class="btn">
|
|
||||||
<span>{{ field.label.text }}</span>
|
|
||||||
<input id="{{ field.id }}" name="{{ field.name }}" type="file" multiple>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="file-path-wrapper">
|
<div class="file-path-wrapper">
|
||||||
<input class="file-path validate" type="text" placeholder="{{ placeholder }}">
|
<input class="file-path validate" type="text" placeholder="{{ placeholder }}">
|
||||||
@ -67,12 +51,29 @@
|
|||||||
|
|
||||||
|
|
||||||
{% macro render_integer_field(field) %}
|
{% macro render_integer_field(field) %}
|
||||||
<div class="input-field">
|
{% set classes = kwargs.pop('class_', '').split(' ') %}
|
||||||
{% if 'material_icon' in kwargs %}
|
|
||||||
<i class="material-icons prefix">{{ kwargs.pop('material_icon') }}</i>
|
{% if 'validate' not in classes %}
|
||||||
|
{% set _ = classes.append('validate') %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<input class="validate" id="{{ field.id }}" name="{{ field.name }}" type="number">
|
|
||||||
<label for="{{ field.id }}">{{ field.label.text }}</label>
|
{% set _ = kwargs.update({'class_': ' '.join(classes)}) %}
|
||||||
|
|
||||||
|
{{ render_generic_field(field, *args, **kwargs) }}
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
|
{% macro render_multiple_file_field(field) %}
|
||||||
|
{% set placeholder = kwargs.pop('placeholder', '') %}
|
||||||
|
|
||||||
|
<div class="file-field input-field">
|
||||||
|
<div class="btn">
|
||||||
|
<span>{{ field.label.text }}</span>
|
||||||
|
{{ field(*args, **kwargs) }}
|
||||||
|
</div>
|
||||||
|
<div class="file-path-wrapper">
|
||||||
|
<input class="file-path validate" type="text" placeholder="{{ placeholder }}">
|
||||||
|
</div>
|
||||||
{% for error in field.errors %}
|
{% for error in field.errors %}
|
||||||
<span class="helper-text error-color-text">{{ error }}</span>
|
<span class="helper-text error-color-text">{{ error }}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -80,8 +81,21 @@
|
|||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
|
{% macro render_string_field(field) %}
|
||||||
|
{% set classes = kwargs.pop('class_', '').split(' ') %}
|
||||||
|
|
||||||
|
{% if 'validate' not in classes %}
|
||||||
|
{% set _ = classes.append('validate') %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% set _ = kwargs.update({'class_': ' '.join(classes)}) %}
|
||||||
|
|
||||||
|
{{ render_generic_field(field, *args, **kwargs) }}
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
{% macro render_submit_field(field) %}
|
{% macro render_submit_field(field) %}
|
||||||
<button class="btn waves-effect waves-light" id="{{ field.id }}" name="{{ field.name }}" type="submit">
|
<button class="btn waves-effect waves-light" id="{{ field.id }}" name="{{ field.name }}" type="submit" value="Submit">
|
||||||
{{ field.label.text }}
|
{{ field.label.text }}
|
||||||
{% if 'material_icon' in kwargs %}
|
{% if 'material_icon' in kwargs %}
|
||||||
<i class="material-icons right">{{ kwargs.pop('material_icon') }}</i>
|
<i class="material-icons right">{{ kwargs.pop('material_icon') }}</i>
|
||||||
@ -91,26 +105,23 @@
|
|||||||
|
|
||||||
|
|
||||||
{% macro render_text_area_field(field) %}
|
{% macro render_text_area_field(field) %}
|
||||||
<div class="input-field">
|
{% set classes = kwargs.pop('class_', '').split(' ') %}
|
||||||
{% if 'material_icon' in kwargs %}
|
|
||||||
<i class="material-icons prefix">{{ kwargs.pop('material_icon') }}</i>
|
{% if 'materialize-textarea' not in classes %}
|
||||||
|
{% set _ = classes.append('materialize-textarea') %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<textarea class="materialize-textarea validate" id="{{ field.id }}" name="{{ field.name }}"></textarea>
|
|
||||||
<label for="{{ field.id }}">{{ field.label.text }}</label>
|
{% if 'validate' not in classes %}
|
||||||
{% for error in field.errors %}
|
{% set _ = classes.append('validate') %}
|
||||||
<span class="helper-text error-color-text">{{ error }}</span>
|
{% endif %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
{% set _ = kwargs.update({'class_': ' '.join(classes)}) %}
|
||||||
|
|
||||||
|
{{ render_generic_field(field, *args, **kwargs) }}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
{% macro render_generic_field(field) %}
|
{% macro render_generic_field(field) %}
|
||||||
{% set classes_ = kwargs.pop('class_', '').split(' ') %}
|
|
||||||
{% if 'validate' not in classes_ %}
|
|
||||||
{% set _ = classes_.append('validate') %}
|
|
||||||
{% endif %}
|
|
||||||
{% set _ = kwargs.update({'class_': ' '.join(classes_)}) %}
|
|
||||||
|
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
{% if 'material_icon' in kwargs %}
|
{% if 'material_icon' in kwargs %}
|
||||||
<i class="material-icons prefix">{{ kwargs.pop('material_icon') }}</i>
|
<i class="material-icons prefix">{{ kwargs.pop('material_icon') }}</i>
|
||||||
|
@ -94,5 +94,5 @@ class Config:
|
|||||||
NOPAQUE_READCOOP_USERNAME = os.environ.get('NOPAQUE_READCOOP_USERNAME')
|
NOPAQUE_READCOOP_USERNAME = os.environ.get('NOPAQUE_READCOOP_USERNAME')
|
||||||
NOPAQUE_READCOOP_PASSWORD = os.environ.get('NOPAQUE_READCOOP_PASSWORD')
|
NOPAQUE_READCOOP_PASSWORD = os.environ.get('NOPAQUE_READCOOP_PASSWORD')
|
||||||
|
|
||||||
NOPAQUE_VERSION='1.0.2'
|
NOPAQUE_VERSION='1.1.0'
|
||||||
# endregion nopaque
|
# endregion nopaque
|
||||||
|
@ -14,6 +14,7 @@ docker==7.0.0
|
|||||||
email_validator==2.1.1
|
email_validator==2.1.1
|
||||||
eventlet==0.34.2
|
eventlet==0.34.2
|
||||||
Flask==2.3.3
|
Flask==2.3.3
|
||||||
|
Flask-Admin==1.6.1
|
||||||
Flask-APScheduler==1.13.1
|
Flask-APScheduler==1.13.1
|
||||||
Flask-Assets==2.1.0
|
Flask-Assets==2.1.0
|
||||||
Flask-Hashids==1.0.3
|
Flask-Hashids==1.0.3
|
||||||
|
@ -4,6 +4,7 @@ dnspython==2.5.0
|
|||||||
docker
|
docker
|
||||||
eventlet==0.34.2
|
eventlet==0.34.2
|
||||||
Flask==2.3.3
|
Flask==2.3.3
|
||||||
|
Flask-Admin==1.6.1
|
||||||
Flask-APScheduler
|
Flask-APScheduler
|
||||||
Flask-Assets
|
Flask-Assets
|
||||||
Flask-Hashids
|
Flask-Hashids
|
||||||
|
Reference in New Issue
Block a user