mirror of
https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
synced 2025-07-01 02:10:34 +00:00
Compare commits
13 Commits
bb60a2ba67
...
1.1.1
Author | SHA1 | Date | |
---|---|---|---|
41a88fce33 | |||
56844e0898 | |||
c28d534942 | |||
80604bf8de | |||
d4cd313940 | |||
c405061574 | |||
6c1f48eb2f | |||
cda28910f5 | |||
9a805b9d14 | |||
16bf891654 | |||
cb53b27ebf | |||
6684257bc4 | |||
0d1805fb76 |
@ -3,6 +3,7 @@ from config import Config
|
||||
from docker import DockerClient
|
||||
from flask import Flask
|
||||
from flask.logging import default_handler
|
||||
from flask_admin import Admin
|
||||
from flask_apscheduler import APScheduler
|
||||
from flask_assets import Environment
|
||||
from flask_login import LoginManager
|
||||
@ -15,10 +16,12 @@ from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_hashids import Hashids
|
||||
from logging import Formatter, StreamHandler
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
from .extensions.nopaque_flask_admin_views import AdminIndexView, ModelView
|
||||
|
||||
|
||||
docker_client = DockerClient.from_env()
|
||||
|
||||
admin = Admin()
|
||||
apifairy = APIFairy()
|
||||
assets = Environment()
|
||||
db = SQLAlchemy()
|
||||
@ -74,6 +77,7 @@ def create_app(config: Config = Config) -> Flask:
|
||||
|
||||
from .models import AnonymousUser, User
|
||||
|
||||
admin.init_app(app, index_view=AdminIndexView())
|
||||
apifairy.init_app(app)
|
||||
assets.init_app(app)
|
||||
db.init_app(app)
|
||||
@ -92,9 +96,6 @@ def create_app(config: Config = Config) -> Flask:
|
||||
# endregion Extensions
|
||||
|
||||
# region Blueprints
|
||||
from .blueprints.admin import bp as admin_blueprint
|
||||
app.register_blueprint(admin_blueprint, url_prefix='/admin')
|
||||
|
||||
from .blueprints.api import bp as api_blueprint
|
||||
app.register_blueprint(api_blueprint, url_prefix='/api')
|
||||
|
||||
@ -127,17 +128,15 @@ def create_app(config: Config = Config) -> Flask:
|
||||
|
||||
from .blueprints.workshops import bp as workshops_blueprint
|
||||
app.register_blueprint(workshops_blueprint, url_prefix='/workshops')
|
||||
|
||||
from .models import _models
|
||||
for model in _models:
|
||||
admin.add_view(ModelView(model, db.session, category='Database'))
|
||||
# endregion Blueprints
|
||||
|
||||
# region SocketIO Namespaces
|
||||
from .namespaces.cqi_over_sio import CQiOverSocketIONamespace
|
||||
socketio.on_namespace(CQiOverSocketIONamespace('/cqi_over_sio'))
|
||||
|
||||
from .namespaces.corpora import CorporaNamespace
|
||||
socketio.on_namespace(CorporaNamespace('/corpora'))
|
||||
|
||||
from .namespaces.users import UsersNamespace
|
||||
socketio.on_namespace(UsersNamespace('/users'))
|
||||
# endregion SocketIO Namespaces
|
||||
|
||||
# region Database event Listeners
|
||||
|
@ -1,20 +0,0 @@
|
||||
from flask import Blueprint
|
||||
from flask_login import login_required
|
||||
from app.decorators import admin_required
|
||||
|
||||
|
||||
bp = Blueprint('admin', __name__)
|
||||
|
||||
|
||||
@bp.before_request
|
||||
@login_required
|
||||
@admin_required
|
||||
def before_request():
|
||||
'''
|
||||
Ensures that the routes in this package can be visited only by users with
|
||||
administrator privileges (login_required and admin_required).
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
from . import json_routes, routes
|
@ -1,16 +0,0 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import SelectField, SubmitField
|
||||
from app.models import Role
|
||||
|
||||
|
||||
class UpdateUserForm(FlaskForm):
|
||||
role = SelectField('Role')
|
||||
submit = SubmitField()
|
||||
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
if 'data' not in kwargs:
|
||||
kwargs['data'] = {'role': user.role.hashid}
|
||||
if 'prefix' not in kwargs:
|
||||
kwargs['prefix'] = 'update-user-form'
|
||||
super().__init__(*args, **kwargs)
|
||||
self.role.choices = [(x.hashid, x.name) for x in Role.query.all()]
|
@ -1,23 +0,0 @@
|
||||
from flask import abort, request
|
||||
from app.decorators import content_negotiation
|
||||
from app import db
|
||||
from app.models import User
|
||||
from . import bp
|
||||
|
||||
|
||||
@bp.route('/users/<hashid:user_id>/confirmed', methods=['PUT'])
|
||||
@content_negotiation(consumes='application/json', produces='application/json')
|
||||
def update_user_role(user_id):
|
||||
confirmed = request.json
|
||||
if not isinstance(confirmed, bool):
|
||||
abort(400)
|
||||
user = User.query.get_or_404(user_id)
|
||||
user.confirmed = confirmed
|
||||
db.session.commit()
|
||||
response_data = {
|
||||
'message': (
|
||||
f'User "{user.username}" is now '
|
||||
f'{"confirmed" if confirmed else "unconfirmed"}'
|
||||
)
|
||||
}
|
||||
return response_data, 200
|
@ -1,136 +0,0 @@
|
||||
from flask import abort, flash, redirect, render_template, url_for
|
||||
from app import db, hashids
|
||||
from app.models import Avatar, Corpus, Role, User
|
||||
from app.blueprints.users.settings.forms import (
|
||||
UpdateAvatarForm,
|
||||
UpdatePasswordForm,
|
||||
UpdateNotificationsForm,
|
||||
UpdateAccountInformationForm,
|
||||
UpdateProfileInformationForm
|
||||
)
|
||||
from . import bp
|
||||
from .forms import UpdateUserForm
|
||||
|
||||
|
||||
@bp.route('')
|
||||
def admin():
|
||||
return render_template(
|
||||
'admin/admin.html.j2',
|
||||
title='Administration'
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/corpora')
|
||||
def corpora():
|
||||
corpora = Corpus.query.all()
|
||||
return render_template(
|
||||
'admin/corpora.html.j2',
|
||||
title='Corpora',
|
||||
corpora=corpora
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/users')
|
||||
def users():
|
||||
users = User.query.all()
|
||||
return render_template(
|
||||
'admin/users.html.j2',
|
||||
title='Users',
|
||||
users=users
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/users/<hashid:user_id>')
|
||||
def user(user_id):
|
||||
user = User.query.get_or_404(user_id)
|
||||
corpora = Corpus.query.filter(Corpus.user == user).all()
|
||||
return render_template(
|
||||
'admin/user.html.j2',
|
||||
title=user.username,
|
||||
user=user,
|
||||
corpora=corpora
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/users/<hashid:user_id>/settings', methods=['GET', 'POST'])
|
||||
def user_settings(user_id):
|
||||
user = User.query.get_or_404(user_id)
|
||||
update_account_information_form = UpdateAccountInformationForm(user)
|
||||
update_profile_information_form = UpdateProfileInformationForm(user)
|
||||
update_avatar_form = UpdateAvatarForm()
|
||||
update_password_form = UpdatePasswordForm(user)
|
||||
update_notifications_form = UpdateNotificationsForm(user)
|
||||
update_user_form = UpdateUserForm(user)
|
||||
|
||||
# region handle update profile information form
|
||||
if update_profile_information_form.submit.data and update_profile_information_form.validate():
|
||||
user.about_me = update_profile_information_form.about_me.data
|
||||
user.location = update_profile_information_form.location.data
|
||||
user.organization = update_profile_information_form.organization.data
|
||||
user.website = update_profile_information_form.website.data
|
||||
user.full_name = update_profile_information_form.full_name.data
|
||||
db.session.commit()
|
||||
flash('Your changes have been saved')
|
||||
return redirect(url_for('.user_settings', user_id=user.id))
|
||||
# endregion handle update profile information form
|
||||
|
||||
# region handle update avatar form
|
||||
if update_avatar_form.submit.data and update_avatar_form.validate():
|
||||
try:
|
||||
Avatar.create(
|
||||
update_avatar_form.avatar.data,
|
||||
user=user
|
||||
)
|
||||
except (AttributeError, OSError):
|
||||
abort(500)
|
||||
db.session.commit()
|
||||
flash('Your changes have been saved')
|
||||
return redirect(url_for('.user_settings', user_id=user.id))
|
||||
# endregion handle update avatar form
|
||||
|
||||
# region handle update account information form
|
||||
if update_account_information_form.submit.data and update_account_information_form.validate():
|
||||
user.email = update_account_information_form.email.data
|
||||
user.username = update_account_information_form.username.data
|
||||
db.session.commit()
|
||||
flash('Profile settings updated')
|
||||
return redirect(url_for('.user_settings', user_id=user.id))
|
||||
# endregion handle update account information form
|
||||
|
||||
# region handle update password form
|
||||
if update_password_form.submit.data and update_password_form.validate():
|
||||
user.password = update_password_form.new_password.data
|
||||
db.session.commit()
|
||||
flash('Your changes have been saved')
|
||||
return redirect(url_for('.user_settings', user_id=user.id))
|
||||
# endregion handle update password form
|
||||
|
||||
# region handle update notifications form
|
||||
if update_notifications_form.submit.data and update_notifications_form.validate():
|
||||
user.setting_job_status_mail_notification_level = \
|
||||
update_notifications_form.job_status_mail_notification_level.data
|
||||
db.session.commit()
|
||||
flash('Your changes have been saved')
|
||||
return redirect(url_for('.user_settings', user_id=user.id))
|
||||
# endregion handle update notifications form
|
||||
|
||||
# region handle update user form
|
||||
if update_user_form.submit.data and update_user_form.validate():
|
||||
role_id = hashids.decode(update_user_form.role.data)
|
||||
user.role = Role.query.get(role_id)
|
||||
db.session.commit()
|
||||
flash('Your changes have been saved')
|
||||
return redirect(url_for('.user_settings', user_id=user.id))
|
||||
# endregion handle update user form
|
||||
|
||||
return render_template(
|
||||
'admin/user_settings.html.j2',
|
||||
title='Settings',
|
||||
update_account_information_form=update_account_information_form,
|
||||
update_avatar_form=update_avatar_form,
|
||||
update_notifications_form=update_notifications_form,
|
||||
update_password_form=update_password_form,
|
||||
update_profile_information_form=update_profile_information_form,
|
||||
update_user_form=update_user_form,
|
||||
user=user
|
||||
)
|
@ -19,11 +19,9 @@ def before_request():
|
||||
and request.endpoint
|
||||
and request.blueprint != 'auth'
|
||||
and request.endpoint != 'static'
|
||||
and request.endpoint != 'main.accept_terms_of_use'
|
||||
):
|
||||
return redirect(url_for('auth.unconfirmed'))
|
||||
|
||||
if not current_user.terms_of_use_accepted:
|
||||
return redirect(url_for('main.terms_of_use'))
|
||||
|
||||
|
||||
from . import routes
|
||||
|
@ -16,4 +16,4 @@ def before_request():
|
||||
pass
|
||||
|
||||
|
||||
from . import cli, files, followers, routes, json_routes
|
||||
from . import cli, files, followers, routes
|
||||
|
@ -1,125 +0,0 @@
|
||||
from datetime import datetime
|
||||
from flask import abort, current_app, request, url_for
|
||||
from flask_login import current_user
|
||||
from threading import Thread
|
||||
from app import db
|
||||
from app.decorators import content_negotiation
|
||||
from app.models import Corpus, CorpusFollowerRole
|
||||
from . import bp
|
||||
from .decorators import corpus_follower_permission_required, corpus_owner_or_admin_required
|
||||
import nltk
|
||||
from string import punctuation
|
||||
|
||||
|
||||
@bp.route('/<hashid:corpus_id>', methods=['DELETE'])
|
||||
@corpus_owner_or_admin_required
|
||||
@content_negotiation(produces='application/json')
|
||||
def delete_corpus(corpus_id):
|
||||
def _delete_corpus(app, corpus_id):
|
||||
with app.app_context():
|
||||
corpus = Corpus.query.get(corpus_id)
|
||||
corpus.delete()
|
||||
db.session.commit()
|
||||
|
||||
corpus = Corpus.query.get_or_404(corpus_id)
|
||||
thread = Thread(
|
||||
target=_delete_corpus,
|
||||
args=(current_app._get_current_object(), corpus.id)
|
||||
)
|
||||
thread.start()
|
||||
response_data = {
|
||||
'message': f'Corpus "{corpus.title}" marked for deletion',
|
||||
'category': 'corpus'
|
||||
}
|
||||
return response_data, 200
|
||||
|
||||
|
||||
@bp.route('/<hashid:corpus_id>/build', methods=['POST'])
|
||||
@corpus_follower_permission_required('MANAGE_FILES')
|
||||
@content_negotiation(produces='application/json')
|
||||
def build_corpus(corpus_id):
|
||||
def _build_corpus(app, corpus_id):
|
||||
with app.app_context():
|
||||
corpus = Corpus.query.get(corpus_id)
|
||||
corpus.build()
|
||||
db.session.commit()
|
||||
|
||||
corpus = Corpus.query.get_or_404(corpus_id)
|
||||
if len(corpus.files.all()) == 0:
|
||||
abort(409)
|
||||
thread = Thread(
|
||||
target=_build_corpus,
|
||||
args=(current_app._get_current_object(), corpus_id)
|
||||
)
|
||||
thread.start()
|
||||
response_data = {
|
||||
'message': f'Corpus "{corpus.title}" marked for building',
|
||||
'category': 'corpus'
|
||||
}
|
||||
return response_data, 202
|
||||
|
||||
@bp.route('/stopwords')
|
||||
@content_negotiation(produces='application/json')
|
||||
def get_stopwords():
|
||||
nltk.download('stopwords', quiet=True)
|
||||
languages = ["german", "english", "catalan", "greek", "spanish", "french", "italian", "russian", "chinese"]
|
||||
stopwords = {}
|
||||
for language in languages:
|
||||
stopwords[language] = nltk.corpus.stopwords.words(language)
|
||||
stopwords['punctuation'] = list(punctuation) + ['—', '|', '–', '“', '„', '--']
|
||||
stopwords['user_stopwords'] = []
|
||||
response_data = stopwords
|
||||
return response_data, 202
|
||||
|
||||
@bp.route('/<hashid:corpus_id>/generate-share-link', methods=['POST'])
|
||||
@corpus_follower_permission_required('MANAGE_FOLLOWERS')
|
||||
@content_negotiation(consumes='application/json', produces='application/json')
|
||||
def generate_corpus_share_link(corpus_id):
|
||||
data = request.json
|
||||
if not isinstance(data, dict):
|
||||
abort(400)
|
||||
expiration = data.get('expiration')
|
||||
if not isinstance(expiration, str):
|
||||
abort(400)
|
||||
role_name = data.get('role')
|
||||
if not isinstance(role_name, str):
|
||||
abort(400)
|
||||
expiration_date = datetime.strptime(expiration, '%b %d, %Y')
|
||||
cfr = CorpusFollowerRole.query.filter_by(name=role_name).first()
|
||||
if cfr is None:
|
||||
abort(400)
|
||||
corpus = Corpus.query.get_or_404(corpus_id)
|
||||
token = current_user.generate_follow_corpus_token(corpus.hashid, role_name, expiration_date)
|
||||
corpus_share_link = url_for(
|
||||
'corpora.follow_corpus',
|
||||
corpus_id=corpus_id,
|
||||
token=token,
|
||||
_external=True
|
||||
)
|
||||
response_data = {
|
||||
'message': 'Corpus share link generated',
|
||||
'category': 'corpus',
|
||||
'corpusShareLink': corpus_share_link
|
||||
}
|
||||
return response_data, 200
|
||||
|
||||
|
||||
|
||||
@bp.route('/<hashid:corpus_id>/is_public', methods=['PUT'])
|
||||
@corpus_owner_or_admin_required
|
||||
@content_negotiation(consumes='application/json', produces='application/json')
|
||||
def update_corpus_is_public(corpus_id):
|
||||
is_public = request.json
|
||||
if not isinstance(is_public, bool):
|
||||
abort(400)
|
||||
corpus = Corpus.query.get_or_404(corpus_id)
|
||||
corpus.is_public = is_public
|
||||
db.session.commit()
|
||||
response_data = {
|
||||
'message': (
|
||||
f'Corpus "{corpus.title}" is now'
|
||||
f' {"public" if is_public else "private"}'
|
||||
),
|
||||
'category': 'corpus'
|
||||
}
|
||||
return response_data, 200
|
@ -1,5 +1,19 @@
|
||||
from flask import abort, flash, redirect, render_template, url_for
|
||||
from datetime import datetime
|
||||
from flask import (
|
||||
abort,
|
||||
current_app,
|
||||
flash,
|
||||
Flask,
|
||||
jsonify,
|
||||
redirect,
|
||||
request,
|
||||
render_template,
|
||||
url_for
|
||||
)
|
||||
from flask_login import current_user
|
||||
from string import punctuation
|
||||
from threading import Thread
|
||||
import nltk
|
||||
from app import db
|
||||
from app.models import (
|
||||
Corpus,
|
||||
@ -12,6 +26,21 @@ from .decorators import corpus_follower_permission_required
|
||||
from .forms import CreateCorpusForm
|
||||
|
||||
|
||||
|
||||
def _delete_corpus(app: Flask, corpus_id: int):
|
||||
with app.app_context():
|
||||
corpus: Corpus = Corpus.query.get(corpus_id)
|
||||
corpus.delete()
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def _build_corpus(app: Flask, corpus_id: int):
|
||||
with app.app_context():
|
||||
corpus = Corpus.query.get(corpus_id)
|
||||
corpus.build()
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@bp.route('')
|
||||
def corpora():
|
||||
return redirect(url_for('main.dashboard', _anchor='corpora'))
|
||||
@ -20,6 +49,7 @@ def corpora():
|
||||
@bp.route('/create', methods=['GET', 'POST'])
|
||||
def create_corpus():
|
||||
form = CreateCorpusForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
corpus = Corpus.create(
|
||||
@ -30,8 +60,10 @@ def create_corpus():
|
||||
except OSError:
|
||||
abort(500)
|
||||
db.session.commit()
|
||||
|
||||
flash(f'Corpus "{corpus.title}" created', 'corpus')
|
||||
return redirect(corpus.url)
|
||||
|
||||
return render_template(
|
||||
'corpora/create.html.j2',
|
||||
title='Create corpus',
|
||||
@ -40,12 +72,14 @@ def create_corpus():
|
||||
|
||||
|
||||
@bp.route('/<hashid:corpus_id>')
|
||||
def corpus(corpus_id):
|
||||
def corpus(corpus_id: int):
|
||||
corpus = Corpus.query.get_or_404(corpus_id)
|
||||
cfrs = CorpusFollowerRole.query.all()
|
||||
# TODO: Better solution for filtering admin
|
||||
users = User.query.filter(User.is_public == True, User.id != current_user.id, User.id != corpus.user.id, User.role_id < 4).all()
|
||||
cfa = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=current_user.id).first()
|
||||
|
||||
cfa = CorpusFollowerAssociation.query.filter_by(
|
||||
corpus_id=corpus_id,
|
||||
follower_id=current_user.id
|
||||
).first()
|
||||
|
||||
if cfa is None:
|
||||
if corpus.user == current_user or current_user.is_administrator:
|
||||
cfr = CorpusFollowerRole.query.filter_by(name='Administrator').first()
|
||||
@ -53,7 +87,21 @@ def corpus(corpus_id):
|
||||
cfr = CorpusFollowerRole.query.filter_by(name='Anonymous').first()
|
||||
else:
|
||||
cfr = cfa.role
|
||||
if corpus.user == current_user or current_user.is_administrator:
|
||||
|
||||
cfrs = CorpusFollowerRole.query.all()
|
||||
|
||||
# TODO: Better solution for filtering admin
|
||||
users = User.query.filter(
|
||||
User.is_public == True,
|
||||
User.id != current_user.id,
|
||||
User.id != corpus.user.id,
|
||||
User.role_id < 4
|
||||
).all()
|
||||
|
||||
if (
|
||||
corpus.user == current_user
|
||||
or current_user.is_administrator
|
||||
):
|
||||
return render_template(
|
||||
'corpora/corpus.html.j2',
|
||||
title=corpus.title,
|
||||
@ -62,8 +110,15 @@ def corpus(corpus_id):
|
||||
cfrs=cfrs,
|
||||
users=users
|
||||
)
|
||||
if (current_user.is_following_corpus(corpus) or corpus.is_public):
|
||||
cfas = CorpusFollowerAssociation.query.filter(Corpus.id == corpus_id, CorpusFollowerAssociation.follower_id != corpus.user.id).all()
|
||||
|
||||
if (
|
||||
current_user.is_following_corpus(corpus)
|
||||
or corpus.is_public
|
||||
):
|
||||
cfas = CorpusFollowerAssociation.query.filter(
|
||||
Corpus.id == corpus_id,
|
||||
CorpusFollowerAssociation.follower_id != corpus.user.id
|
||||
).all()
|
||||
return render_template(
|
||||
'corpora/public_corpus.html.j2',
|
||||
title=corpus.title,
|
||||
@ -73,14 +128,110 @@ def corpus(corpus_id):
|
||||
cfas=cfas,
|
||||
users=users
|
||||
)
|
||||
|
||||
abort(403)
|
||||
|
||||
|
||||
@bp.route('/<hashid:corpus_id>', methods=['DELETE'])
|
||||
def delete_corpus(corpus_id: int):
|
||||
corpus = Corpus.query.get_or_404(corpus_id)
|
||||
|
||||
if not (
|
||||
corpus.user == current_user
|
||||
or current_user.is_administrator
|
||||
):
|
||||
abort(403)
|
||||
|
||||
thread = Thread(
|
||||
target=_delete_corpus,
|
||||
args=(current_app._get_current_object(), corpus.id)
|
||||
)
|
||||
thread.start()
|
||||
|
||||
return jsonify(f'Corpus "{corpus.title}" marked for deletion.'), 202
|
||||
|
||||
|
||||
@bp.route('/<hashid:corpus_id>/build', methods=['POST'])
|
||||
def build_corpus(corpus_id: int):
|
||||
corpus = Corpus.query.get_or_404(corpus_id)
|
||||
|
||||
cfa = CorpusFollowerAssociation.query.filter_by(
|
||||
corpus_id=corpus_id,
|
||||
follower_id=current_user.id
|
||||
).first()
|
||||
|
||||
if not (
|
||||
cfa is not None and cfa.role.has_permission('MANAGE_FILES')
|
||||
or corpus.user == current_user
|
||||
or current_user.is_administrator
|
||||
):
|
||||
abort(403)
|
||||
|
||||
if len(corpus.files.all()) == 0:
|
||||
abort(409)
|
||||
|
||||
thread = Thread(
|
||||
target=_build_corpus,
|
||||
args=(current_app._get_current_object(), corpus.id)
|
||||
)
|
||||
thread.start()
|
||||
|
||||
return jsonify(f'Corpus "{corpus.title}" marked for building.'), 202
|
||||
|
||||
|
||||
@bp.route('/<hashid:corpus_id>/create-share-link', methods=['POST'])
|
||||
def create_share_link(corpus_id: int):
|
||||
data = request.json
|
||||
|
||||
expiration_date = data['expiration_date']
|
||||
if not isinstance(expiration_date, str):
|
||||
abort(400)
|
||||
|
||||
role_name = data['role_name']
|
||||
if not isinstance(role_name, str):
|
||||
abort(400)
|
||||
|
||||
corpus = Corpus.query.get_or_404(corpus_id)
|
||||
|
||||
cfa = CorpusFollowerAssociation.query.filter_by(
|
||||
corpus_id=corpus_id,
|
||||
follower_id=current_user.id
|
||||
).first()
|
||||
|
||||
if not (
|
||||
cfa is not None and cfa.role.has_permission('MANAGE_FOLLOWERS')
|
||||
or corpus.user == current_user
|
||||
or current_user.is_administrator
|
||||
):
|
||||
abort(403)
|
||||
|
||||
_expiration_date = datetime.strptime(expiration_date, '%b %d, %Y')
|
||||
|
||||
cfr = CorpusFollowerRole.query.filter_by(name=role_name).first()
|
||||
if cfr is None:
|
||||
abort(400)
|
||||
|
||||
token = current_user.generate_follow_corpus_token(
|
||||
corpus.hashid,
|
||||
role_name,
|
||||
_expiration_date
|
||||
)
|
||||
|
||||
corpus_share_link = url_for(
|
||||
'corpora.follow_corpus',
|
||||
corpus_id=corpus_id,
|
||||
token=token,
|
||||
_external=True
|
||||
)
|
||||
|
||||
return jsonify(corpus_share_link)
|
||||
|
||||
|
||||
@bp.route('/<hashid:corpus_id>/analysis')
|
||||
@corpus_follower_permission_required('VIEW')
|
||||
def analysis(corpus_id):
|
||||
def analysis(corpus_id: int):
|
||||
corpus = Corpus.query.get_or_404(corpus_id)
|
||||
|
||||
return render_template(
|
||||
'corpora/analysis.html.j2',
|
||||
corpus=corpus,
|
||||
@ -88,22 +239,61 @@ def analysis(corpus_id):
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/<hashid:corpus_id>/analysis/stopwords')
|
||||
def get_stopwords(corpus_id: int):
|
||||
languages = [
|
||||
'german',
|
||||
'english',
|
||||
'catalan',
|
||||
'greek',
|
||||
'spanish',
|
||||
'french',
|
||||
'italian',
|
||||
'russian',
|
||||
'chinese'
|
||||
]
|
||||
|
||||
nltk.download('stopwords', quiet=True)
|
||||
stopwords = {
|
||||
language: nltk.corpus.stopwords.words(language)
|
||||
for language in languages
|
||||
}
|
||||
stopwords['punctuation'] = list(punctuation)
|
||||
stopwords['punctuation'] += ['—', '|', '–', '“', '„', '--']
|
||||
stopwords['user_stopwords'] = []
|
||||
|
||||
return jsonify(stopwords)
|
||||
|
||||
|
||||
@bp.route('/<hashid:corpus_id>/follow/<token>')
|
||||
def follow_corpus(corpus_id, token):
|
||||
def follow_corpus(corpus_id: int, token: str):
|
||||
corpus = Corpus.query.get_or_404(corpus_id)
|
||||
if current_user.follow_corpus_by_token(token):
|
||||
db.session.commit()
|
||||
flash(f'You are following "{corpus.title}" now', category='corpus')
|
||||
return redirect(url_for('corpora.corpus', corpus_id=corpus_id))
|
||||
abort(403)
|
||||
|
||||
if not current_user.follow_corpus_by_token(token):
|
||||
abort(403)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
flash(f'You are following "{corpus.title}" now', category='corpus')
|
||||
return redirect(corpus.url)
|
||||
|
||||
|
||||
@bp.route('/import', methods=['GET', 'POST'])
|
||||
def import_corpus():
|
||||
abort(503)
|
||||
@bp.route('/<hashid:corpus_id>/is-public', methods=['PUT'])
|
||||
def update_is_public(corpus_id):
|
||||
new_value = request.json
|
||||
if not isinstance(new_value, bool):
|
||||
abort(400)
|
||||
|
||||
corpus = Corpus.query.get_or_404(corpus_id)
|
||||
|
||||
if not (
|
||||
corpus.user == current_user
|
||||
or current_user.is_administrator
|
||||
):
|
||||
abort(403)
|
||||
|
||||
|
||||
@bp.route('/<hashid:corpus_id>/export')
|
||||
@corpus_follower_permission_required('VIEW')
|
||||
def export_corpus(corpus_id):
|
||||
abort(503)
|
||||
corpus.is_public = new_value
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(f'Corpus "{corpus.title}" is now {"public" if new_value else "private"}'), 200
|
||||
|
@ -1,18 +1,13 @@
|
||||
from flask import Blueprint
|
||||
from flask_login import login_required
|
||||
|
||||
|
||||
bp = Blueprint('jobs', __name__)
|
||||
|
||||
|
||||
@bp.before_request
|
||||
@login_required
|
||||
def before_request():
|
||||
'''
|
||||
Ensures that the routes in this package can only be visited by users that
|
||||
are logged in.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
from . import routes
|
||||
|
||||
from .inputs import bp as inputs_bp
|
||||
bp.register_blueprint(inputs_bp, url_prefix='/<hashid:job_id>/inputs')
|
||||
|
||||
from .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
|
||||
)
|
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
|
||||
)
|
@ -5,36 +5,24 @@ from flask import (
|
||||
jsonify,
|
||||
redirect,
|
||||
render_template,
|
||||
send_from_directory,
|
||||
url_for
|
||||
)
|
||||
from flask_login import current_user
|
||||
from flask_login import current_user, login_required
|
||||
from threading import Thread
|
||||
from app import db
|
||||
from app.models import Job, JobInput, JobResult, JobStatus
|
||||
from app.decorators import admin_required
|
||||
from app.models import Job, JobStatus
|
||||
from . import bp
|
||||
|
||||
|
||||
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: Flask, job_id: int):
|
||||
with app.app_context():
|
||||
job = Job.query.get(job_id)
|
||||
job.restart()
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@bp.route('')
|
||||
def jobs():
|
||||
@login_required
|
||||
def index():
|
||||
return redirect(url_for('main.dashboard', _anchor='jobs'))
|
||||
|
||||
|
||||
@bp.route('/<hashid:job_id>')
|
||||
@login_required
|
||||
def job(job_id: int):
|
||||
job = Job.query.get_or_404(job_id)
|
||||
|
||||
@ -51,7 +39,15 @@ def job(job_id: int):
|
||||
)
|
||||
|
||||
|
||||
def _delete_job(app: Flask, job_id: int):
|
||||
with app.app_context():
|
||||
job = Job.query.get(job_id)
|
||||
job.delete()
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@bp.route('/<hashid:job_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
def delete_job(job_id: int):
|
||||
job = Job.query.get_or_404(job_id)
|
||||
|
||||
@ -71,12 +67,10 @@ def delete_job(job_id: int):
|
||||
|
||||
|
||||
@bp.route('/<hashid:job_id>/log')
|
||||
def get_job_log(job_id: int):
|
||||
@admin_required
|
||||
def job_log(job_id: int):
|
||||
job = Job.query.get_or_404(job_id)
|
||||
|
||||
if not current_user.is_administrator:
|
||||
abort(403)
|
||||
|
||||
if job.status not in [JobStatus.COMPLETED, JobStatus.FAILED]:
|
||||
abort(409)
|
||||
|
||||
@ -87,7 +81,15 @@ def get_job_log(job_id: int):
|
||||
return jsonify(log)
|
||||
|
||||
|
||||
@bp.route('/<hashid:job_id>/restart')
|
||||
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)
|
||||
|
||||
@ -107,47 +109,3 @@ def restart_job(job_id: int):
|
||||
thread.start()
|
||||
|
||||
return jsonify(f'Job "{job.title}" marked for restarting.'), 202
|
||||
|
||||
|
||||
@bp.route('/<hashid:job_id>/inputs/<hashid:job_input_id>/download')
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/<hashid:job_id>/results/<hashid:job_result_id>/download')
|
||||
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,8 +1,9 @@
|
||||
from flask import flash, redirect, render_template, url_for
|
||||
from flask import abort, flash, jsonify, redirect, render_template, url_for
|
||||
from flask_login import current_user, login_required, login_user
|
||||
from app.blueprints.auth.forms import LoginForm
|
||||
from app.models import Corpus, User
|
||||
from . import bp
|
||||
from app import db
|
||||
|
||||
|
||||
@bp.route('/', methods=['GET', 'POST'])
|
||||
@ -56,7 +57,7 @@ def news():
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/privacy_policy')
|
||||
@bp.route('/privacy-policy')
|
||||
def privacy_policy():
|
||||
return render_template(
|
||||
'main/privacy_policy.html.j2',
|
||||
@ -64,14 +65,24 @@ def privacy_policy():
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/terms_of_use')
|
||||
@bp.route('/terms-of-use')
|
||||
def terms_of_use():
|
||||
return render_template(
|
||||
'main/terms_of_use.html.j2',
|
||||
title='Terms of Use'
|
||||
title='Terms of use'
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/accept-terms-of-use', methods=['POST'])
|
||||
@login_required
|
||||
def accept_terms_of_use():
|
||||
current_user.terms_of_use_accepted = True
|
||||
db.session.commit()
|
||||
|
||||
return jsonify('You accepted the terms of use'), 202
|
||||
|
||||
|
||||
|
||||
@bp.route('/social')
|
||||
@login_required
|
||||
def social():
|
||||
|
@ -1,18 +1,7 @@
|
||||
from flask import Blueprint
|
||||
from flask_login import login_required
|
||||
|
||||
|
||||
bp = Blueprint('settings', __name__)
|
||||
|
||||
|
||||
@bp.before_request
|
||||
@login_required
|
||||
def before_request():
|
||||
'''
|
||||
Ensures that the routes in this package can only be visited by users that
|
||||
are logged in.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
from . import routes
|
||||
|
@ -38,8 +38,8 @@ class UpdateAccountInformationForm(FlaskForm):
|
||||
]
|
||||
)
|
||||
submit = SubmitField()
|
||||
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
|
||||
def __init__(self, user: User, *args, **kwargs):
|
||||
if 'data' not in kwargs:
|
||||
kwargs['data'] = user.to_json_serializeable()
|
||||
if 'prefix' not in kwargs:
|
||||
@ -64,7 +64,7 @@ class UpdateProfileInformationForm(FlaskForm):
|
||||
validators=[Length(max=128)]
|
||||
)
|
||||
about_me = TextAreaField(
|
||||
'About me',
|
||||
'About me',
|
||||
validators=[
|
||||
Length(max=254)
|
||||
]
|
||||
@ -89,7 +89,7 @@ class UpdateProfileInformationForm(FlaskForm):
|
||||
)
|
||||
submit = SubmitField()
|
||||
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
def __init__(self, user: User, *args, **kwargs):
|
||||
if 'data' not in kwargs:
|
||||
kwargs['data'] = user.to_json_serializeable()
|
||||
if 'prefix' not in kwargs:
|
||||
@ -130,7 +130,7 @@ class UpdatePasswordForm(FlaskForm):
|
||||
)
|
||||
submit = SubmitField()
|
||||
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
def __init__(self, user: User, *args, **kwargs):
|
||||
if 'prefix' not in kwargs:
|
||||
kwargs['prefix'] = 'update-password-form'
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -152,7 +152,7 @@ class UpdateNotificationsForm(FlaskForm):
|
||||
)
|
||||
submit = SubmitField()
|
||||
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
def __init__(self, user: User, *args, **kwargs):
|
||||
if 'data' not in kwargs:
|
||||
kwargs['data'] = user.to_json_serializeable()
|
||||
if 'prefix' not in kwargs:
|
@ -1,10 +1,158 @@
|
||||
from flask import g, url_for
|
||||
from flask_login import current_user
|
||||
from app.blueprints.users.settings.routes import settings as settings_route
|
||||
from flask import (
|
||||
abort,
|
||||
flash,
|
||||
jsonify,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
url_for
|
||||
)
|
||||
from flask_login import current_user, login_required
|
||||
from app import db
|
||||
from app.models import Avatar
|
||||
from . import bp
|
||||
from .forms import (
|
||||
UpdateAvatarForm,
|
||||
UpdatePasswordForm,
|
||||
UpdateNotificationsForm,
|
||||
UpdateAccountInformationForm,
|
||||
UpdateProfileInformationForm
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/settings', methods=['GET', 'POST'])
|
||||
def settings():
|
||||
g._nopaque_redirect_location_on_post = url_for('.settings')
|
||||
return settings_route(current_user.id)
|
||||
@bp.route('', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def index():
|
||||
update_account_information_form = UpdateAccountInformationForm(current_user)
|
||||
update_profile_information_form = UpdateProfileInformationForm(current_user)
|
||||
update_avatar_form = UpdateAvatarForm()
|
||||
update_password_form = UpdatePasswordForm(current_user)
|
||||
update_notifications_form = UpdateNotificationsForm(current_user)
|
||||
|
||||
# region handle update profile information form
|
||||
if update_profile_information_form.submit.data and update_profile_information_form.validate():
|
||||
current_user.about_me = update_profile_information_form.about_me.data
|
||||
current_user.location = update_profile_information_form.location.data
|
||||
current_user.organization = update_profile_information_form.organization.data
|
||||
current_user.website = update_profile_information_form.website.data
|
||||
current_user.full_name = update_profile_information_form.full_name.data
|
||||
db.session.commit()
|
||||
flash('Your changes have been saved')
|
||||
return redirect(url_for('.index'))
|
||||
# endregion handle update profile information form
|
||||
|
||||
# region handle update avatar form
|
||||
if update_avatar_form.submit.data and update_avatar_form.validate():
|
||||
try:
|
||||
Avatar.create(
|
||||
update_avatar_form.avatar.data,
|
||||
user=current_user
|
||||
)
|
||||
except (AttributeError, OSError):
|
||||
abort(500)
|
||||
db.session.commit()
|
||||
flash('Your changes have been saved')
|
||||
return redirect(url_for('.index'))
|
||||
# endregion handle update avatar form
|
||||
|
||||
# region handle update account information form
|
||||
if update_account_information_form.submit.data and update_account_information_form.validate():
|
||||
current_user.email = update_account_information_form.email.data
|
||||
current_user.username = update_account_information_form.username.data
|
||||
db.session.commit()
|
||||
flash('Profile settings updated')
|
||||
return redirect(url_for('.index'))
|
||||
# endregion handle update account information form
|
||||
|
||||
# region handle update password form
|
||||
if update_password_form.submit.data and update_password_form.validate():
|
||||
current_user.password = update_password_form.new_password.data
|
||||
db.session.commit()
|
||||
flash('Your changes have been saved')
|
||||
return redirect(url_for('.index'))
|
||||
# endregion handle update password form
|
||||
|
||||
# region handle update notifications form
|
||||
if update_notifications_form.submit.data and update_notifications_form.validate():
|
||||
current_user.setting_job_status_mail_notification_level = \
|
||||
update_notifications_form.job_status_mail_notification_level.data
|
||||
db.session.commit()
|
||||
flash('Your changes have been saved')
|
||||
return redirect(url_for('.index'))
|
||||
# endregion handle update notifications form
|
||||
|
||||
return render_template(
|
||||
'settings/index.html.j2',
|
||||
title='Settings',
|
||||
update_account_information_form=update_account_information_form,
|
||||
update_avatar_form=update_avatar_form,
|
||||
update_notifications_form=update_notifications_form,
|
||||
update_password_form=update_password_form,
|
||||
update_profile_information_form=update_profile_information_form,
|
||||
user=current_user
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/profile-is-public', methods=['PUT'])
|
||||
@login_required
|
||||
def update_profile_is_public():
|
||||
new_value = request.json
|
||||
|
||||
if not isinstance(new_value, bool):
|
||||
abort(400)
|
||||
|
||||
current_user.is_public = new_value
|
||||
db.session.commit()
|
||||
|
||||
return jsonify('Your changes have been saved'), 200
|
||||
|
||||
|
||||
@bp.route('/profile-show-email', methods=['PUT'])
|
||||
@login_required
|
||||
def update_profile_show_email():
|
||||
new_value = request.json
|
||||
|
||||
if not isinstance(new_value, bool):
|
||||
abort(400)
|
||||
|
||||
if new_value:
|
||||
current_user.add_profile_privacy_setting('SHOW_EMAIL')
|
||||
else:
|
||||
current_user.remove_profile_privacy_setting('SHOW_EMAIL')
|
||||
db.session.commit()
|
||||
|
||||
return jsonify('Your changes have been saved'), 200
|
||||
|
||||
|
||||
@bp.route('/profile-show-last-seen', methods=['PUT'])
|
||||
@login_required
|
||||
def update_profile_show_last_seen():
|
||||
new_value = request.json
|
||||
|
||||
if not isinstance(new_value, bool):
|
||||
abort(400)
|
||||
|
||||
if new_value:
|
||||
current_user.add_profile_privacy_setting('SHOW_LAST_SEEN')
|
||||
else:
|
||||
current_user.remove_profile_privacy_setting('SHOW_LAST_SEEN')
|
||||
db.session.commit()
|
||||
|
||||
return jsonify('Your changes have been saved'), 200
|
||||
|
||||
|
||||
@bp.route('/profile-show-member-since', methods=['PUT'])
|
||||
@login_required
|
||||
def update_profile_show_member_since():
|
||||
new_value = request.json
|
||||
|
||||
if not isinstance(new_value, bool):
|
||||
abort(400)
|
||||
|
||||
if new_value:
|
||||
current_user.add_profile_privacy_setting('SHOW_MEMBER_SINCE')
|
||||
else:
|
||||
current_user.remove_profile_privacy_setting('SHOW_MEMBER_SINCE')
|
||||
db.session.commit()
|
||||
|
||||
return jsonify('Your changes have been saved'), 200
|
||||
|
@ -1,18 +1,7 @@
|
||||
from flask import Blueprint
|
||||
from flask_login import login_required
|
||||
|
||||
|
||||
bp = Blueprint('users', __name__)
|
||||
|
||||
|
||||
@bp.before_request
|
||||
@login_required
|
||||
def before_request():
|
||||
'''
|
||||
Ensures that the routes in this package can only be visited by users that
|
||||
are logged in.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
from . import cli, json_routes, routes, settings
|
||||
from . import cli, events, routes
|
||||
|
91
app/blueprints/users/events.py
Normal file
91
app/blueprints/users/events.py
Normal file
@ -0,0 +1,91 @@
|
||||
from flask_login import current_user
|
||||
from flask_socketio import join_room, leave_room
|
||||
from app import hashids, socketio
|
||||
from app.decorators import socketio_login_required
|
||||
from app.models import User
|
||||
|
||||
|
||||
@socketio.on('SUBSCRIBE User')
|
||||
@socketio_login_required
|
||||
def subscribe(user_hashid: str) -> dict:
|
||||
if not isinstance(user_hashid, str):
|
||||
return {
|
||||
'code': 400,
|
||||
'name': 'Bad Request',
|
||||
'description': 'Invalid User ID.'
|
||||
}
|
||||
|
||||
user_id = hashids.decode(user_hashid)
|
||||
|
||||
if not isinstance(user_id, int):
|
||||
return {
|
||||
'code': 400,
|
||||
'name': 'Bad Request',
|
||||
'description': 'Invalid User ID.'
|
||||
}
|
||||
|
||||
user = User.query.get(user_id)
|
||||
|
||||
if user is None:
|
||||
return {
|
||||
'code': 404,
|
||||
'name': 'Not Found',
|
||||
'description': 'User not found.'
|
||||
}
|
||||
|
||||
if not (
|
||||
user == current_user
|
||||
or current_user.is_administrator
|
||||
):
|
||||
return {
|
||||
'code': 403,
|
||||
'name': 'Forbidden',
|
||||
'description': 'Not allowed to subscribe to this user.'
|
||||
}
|
||||
|
||||
join_room(f'/users/{user.hashid}')
|
||||
|
||||
return {'code': 204, 'name': 'No Content'}
|
||||
|
||||
|
||||
@socketio.on('UNSUBSCRIBE User')
|
||||
@socketio_login_required
|
||||
def unsubscribe(user_hashid: str) -> dict:
|
||||
if not isinstance(user_hashid, str):
|
||||
return {
|
||||
'code': 400,
|
||||
'name': 'Bad Request',
|
||||
'description': 'Invalid User ID.'
|
||||
}
|
||||
|
||||
user_id = hashids.decode(user_hashid)
|
||||
|
||||
if not isinstance(user_id, int):
|
||||
return {
|
||||
'code': 400,
|
||||
'name': 'Bad Request',
|
||||
'description': 'Invalid User ID.'
|
||||
}
|
||||
|
||||
user = User.query.get(user_id)
|
||||
|
||||
if user is None:
|
||||
return {
|
||||
'code': 404,
|
||||
'name': 'Not Found',
|
||||
'description': 'User not found.'
|
||||
}
|
||||
|
||||
if not (
|
||||
user == current_user
|
||||
or current_user.is_administrator
|
||||
):
|
||||
return {
|
||||
'code': 403,
|
||||
'name': 'Forbidden',
|
||||
'description': 'Not allowed to unsubscribe from this user.'
|
||||
}
|
||||
|
||||
leave_room(f'/users/{user.hashid}')
|
||||
|
||||
return {'code': 204, 'name': 'No Content'}
|
@ -1,69 +0,0 @@
|
||||
from flask import abort, current_app
|
||||
from flask_login import current_user, logout_user
|
||||
from threading import Thread
|
||||
from app import db
|
||||
from app.decorators import content_negotiation
|
||||
from app.models import Avatar, User
|
||||
from . import bp
|
||||
|
||||
|
||||
@bp.route('/<hashid:user_id>', methods=['DELETE'])
|
||||
@content_negotiation(produces='application/json')
|
||||
def delete_user(user_id):
|
||||
def _delete_user(app, user_id):
|
||||
with app.app_context():
|
||||
user = User.query.get(user_id)
|
||||
user.delete()
|
||||
db.session.commit()
|
||||
|
||||
user = User.query.get_or_404(user_id)
|
||||
if not (user == current_user or current_user.is_administrator):
|
||||
abort(403)
|
||||
thread = Thread(
|
||||
target=_delete_user,
|
||||
args=(current_app._get_current_object(), user.id)
|
||||
)
|
||||
if user == current_user:
|
||||
logout_user()
|
||||
thread.start()
|
||||
response_data = {
|
||||
'message': f'User "{user.username}" marked for deletion'
|
||||
}
|
||||
return response_data, 202
|
||||
|
||||
|
||||
@bp.route('/<hashid:user_id>/avatar', methods=['DELETE'])
|
||||
@content_negotiation(produces='application/json')
|
||||
def delete_user_avatar(user_id):
|
||||
def _delete_avatar(app, avatar_id):
|
||||
with app.app_context():
|
||||
avatar = Avatar.query.get(avatar_id)
|
||||
avatar.delete()
|
||||
db.session.commit()
|
||||
|
||||
user = User.query.get_or_404(user_id)
|
||||
if user.avatar is None:
|
||||
abort(404)
|
||||
if not (user == current_user or current_user.is_administrator):
|
||||
abort(403)
|
||||
thread = Thread(
|
||||
target=_delete_avatar,
|
||||
args=(current_app._get_current_object(), user.avatar.id)
|
||||
)
|
||||
thread.start()
|
||||
response_data = {
|
||||
'message': f'Avatar marked for deletion'
|
||||
}
|
||||
return response_data, 202
|
||||
|
||||
@bp.route('/accept-terms-of-use', methods=['POST'])
|
||||
@content_negotiation(produces='application/json')
|
||||
def accept_terms_of_use():
|
||||
if not (current_user.is_authenticated or current_user.confirmed):
|
||||
abort(403)
|
||||
current_user.terms_of_use_accepted = True
|
||||
db.session.commit()
|
||||
response_data = {
|
||||
'message': 'You accepted the terms of use',
|
||||
}
|
||||
return response_data, 202
|
@ -1,25 +1,48 @@
|
||||
from flask import (
|
||||
abort,
|
||||
redirect,
|
||||
render_template,
|
||||
current_app,
|
||||
Flask,
|
||||
jsonify,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
send_from_directory,
|
||||
url_for
|
||||
)
|
||||
from flask_login import current_user
|
||||
from app.models import User
|
||||
from flask_login import current_user, login_required, logout_user
|
||||
from threading import Thread
|
||||
from app import db
|
||||
from app.models import Avatar, User
|
||||
from . import bp
|
||||
|
||||
|
||||
@bp.route('')
|
||||
def users():
|
||||
@login_required
|
||||
def index():
|
||||
return redirect(url_for('main.social_area', _anchor='users'))
|
||||
|
||||
|
||||
@bp.route('/<hashid:user_id>')
|
||||
def user(user_id):
|
||||
@login_required
|
||||
def user(user_id: int):
|
||||
user = User.query.get_or_404(user_id)
|
||||
if not (user.is_public or user == current_user or current_user.is_administrator):
|
||||
|
||||
if not (
|
||||
user.is_public
|
||||
or user == current_user
|
||||
or current_user.is_administrator
|
||||
):
|
||||
abort(403)
|
||||
|
||||
accept_json = request.accept_mimetypes.accept_json
|
||||
accept_html = request.accept_mimetypes.accept_html
|
||||
|
||||
if accept_json and not accept_html:
|
||||
return user.to_json_serializeable(
|
||||
backrefs=True,
|
||||
relationships=True
|
||||
)
|
||||
|
||||
return render_template(
|
||||
'users/user.html.j2',
|
||||
title=user.username,
|
||||
@ -27,13 +50,51 @@ def user(user_id):
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/<hashid:user_id>/avatar')
|
||||
def user_avatar(user_id):
|
||||
def _delete_user(app: Flask, user_id: int):
|
||||
with app.app_context():
|
||||
user = User.query.get(user_id)
|
||||
user.delete()
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@bp.route('/<hashid:user_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
def delete_user(user_id: int):
|
||||
user = User.query.get_or_404(user_id)
|
||||
if not (user.is_public or user == current_user or current_user.is_administrator):
|
||||
|
||||
if not (
|
||||
user == current_user
|
||||
or current_user.is_administrator
|
||||
):
|
||||
abort(403)
|
||||
|
||||
if user == current_user:
|
||||
logout_user()
|
||||
|
||||
thread = Thread(
|
||||
target=_delete_user,
|
||||
args=(current_app._get_current_object(), user.id)
|
||||
)
|
||||
thread.start()
|
||||
|
||||
return jsonify(f'User "{user.username}" marked for deletion'), 202
|
||||
|
||||
|
||||
@bp.route('/<hashid:user_id>/avatar')
|
||||
@login_required
|
||||
def user_avatar(user_id: int):
|
||||
user = User.query.get_or_404(user_id)
|
||||
|
||||
if not (
|
||||
user.is_public
|
||||
or user == current_user
|
||||
or current_user.is_administrator
|
||||
):
|
||||
abort(403)
|
||||
|
||||
if user.avatar is None:
|
||||
return redirect(url_for('static', filename='images/user_avatar.png'))
|
||||
|
||||
return send_from_directory(
|
||||
user.avatar.path.parent,
|
||||
user.avatar.path.name,
|
||||
@ -41,3 +102,33 @@ def user_avatar(user_id):
|
||||
download_name=user.avatar.filename,
|
||||
mimetype=user.avatar.mimetype
|
||||
)
|
||||
|
||||
|
||||
def _delete_avatar(app: Flask, avatar_id: int):
|
||||
with app.app_context():
|
||||
avatar = Avatar.query.get(avatar_id)
|
||||
avatar.delete()
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@bp.route('/<hashid:user_id>/avatar', methods=['DELETE'])
|
||||
@login_required
|
||||
def delete_user_avatar(user_id: int):
|
||||
user = User.query.get_or_404(user_id)
|
||||
|
||||
if user.avatar is None:
|
||||
abort(409)
|
||||
|
||||
if not (
|
||||
user == current_user
|
||||
or current_user.is_administrator
|
||||
):
|
||||
abort(403)
|
||||
|
||||
thread = Thread(
|
||||
target=_delete_avatar,
|
||||
args=(current_app._get_current_object(), user.avatar.id)
|
||||
)
|
||||
thread.start()
|
||||
|
||||
return jsonify('Avatar marked for deletion'), 202
|
||||
|
@ -1,2 +0,0 @@
|
||||
from .. import bp
|
||||
from . import json_routes, routes
|
@ -1,49 +0,0 @@
|
||||
from flask import abort, request
|
||||
from flask_login import current_user
|
||||
from app import db
|
||||
from app.decorators import content_negotiation
|
||||
from app.models import User, ProfilePrivacySettings
|
||||
from . import bp
|
||||
|
||||
|
||||
@bp.route('/<hashid:user_id>/settings/profile-privacy/is-public', methods=['PUT'])
|
||||
@content_negotiation(consumes='application/json', produces='application/json')
|
||||
def update_user_profile_privacy_setting_is_public(user_id):
|
||||
user = User.query.get_or_404(user_id)
|
||||
if not (user == current_user or current_user.is_administrator):
|
||||
abort(403)
|
||||
enabled = request.json
|
||||
if not isinstance(enabled, bool):
|
||||
abort(400)
|
||||
user.is_public = enabled
|
||||
db.session.commit()
|
||||
response_data = {
|
||||
'message': 'Profile privacy settings updated',
|
||||
'category': 'settings'
|
||||
}
|
||||
return response_data, 200
|
||||
|
||||
|
||||
@bp.route('/<hashid:user_id>/settings/profile-privacy/<string:profile_privacy_setting_name>', methods=['PUT'])
|
||||
@content_negotiation(consumes='application/json', produces='application/json')
|
||||
def update_user_profile_privacy_settings(user_id, profile_privacy_setting_name):
|
||||
user = User.query.get_or_404(user_id)
|
||||
try:
|
||||
profile_privacy_setting = ProfilePrivacySettings[profile_privacy_setting_name]
|
||||
except KeyError:
|
||||
abort(404)
|
||||
if not (user == current_user or current_user.is_administrator):
|
||||
abort(403)
|
||||
enabled = request.json
|
||||
if not isinstance(enabled, bool):
|
||||
abort(400)
|
||||
if enabled:
|
||||
user.add_profile_privacy_setting(profile_privacy_setting)
|
||||
else:
|
||||
user.remove_profile_privacy_setting(profile_privacy_setting)
|
||||
db.session.commit()
|
||||
response_data = {
|
||||
'message': 'Profile privacy settings updated',
|
||||
'category': 'settings'
|
||||
}
|
||||
return response_data, 200
|
@ -1,93 +0,0 @@
|
||||
from flask import abort, flash, g, redirect, render_template, url_for
|
||||
from flask_login import current_user
|
||||
from app import db
|
||||
from app.models import Avatar, User
|
||||
from . import bp
|
||||
from .forms import (
|
||||
UpdateAvatarForm,
|
||||
UpdatePasswordForm,
|
||||
UpdateNotificationsForm,
|
||||
UpdateAccountInformationForm,
|
||||
UpdateProfileInformationForm
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/<hashid:user_id>/settings', methods=['GET', 'POST'])
|
||||
def settings(user_id):
|
||||
user = User.query.get_or_404(user_id)
|
||||
if not (user == current_user or current_user.is_administrator):
|
||||
abort(403)
|
||||
|
||||
redirect_location_on_post = g.pop(
|
||||
'_nopaque_redirect_location_on_post',
|
||||
url_for('.settings', user_id=user_id)
|
||||
)
|
||||
|
||||
update_account_information_form = UpdateAccountInformationForm(user)
|
||||
update_profile_information_form = UpdateProfileInformationForm(user)
|
||||
update_avatar_form = UpdateAvatarForm()
|
||||
update_password_form = UpdatePasswordForm(user)
|
||||
update_notifications_form = UpdateNotificationsForm(user)
|
||||
|
||||
# region handle update profile information form
|
||||
if update_profile_information_form.submit.data and update_profile_information_form.validate():
|
||||
user.about_me = update_profile_information_form.about_me.data
|
||||
user.location = update_profile_information_form.location.data
|
||||
user.organization = update_profile_information_form.organization.data
|
||||
user.website = update_profile_information_form.website.data
|
||||
user.full_name = update_profile_information_form.full_name.data
|
||||
db.session.commit()
|
||||
flash('Your changes have been saved')
|
||||
return redirect(redirect_location_on_post)
|
||||
# endregion handle update profile information form
|
||||
|
||||
# region handle update avatar form
|
||||
if update_avatar_form.submit.data and update_avatar_form.validate():
|
||||
try:
|
||||
Avatar.create(
|
||||
update_avatar_form.avatar.data,
|
||||
user=user
|
||||
)
|
||||
except (AttributeError, OSError):
|
||||
abort(500)
|
||||
db.session.commit()
|
||||
flash('Your changes have been saved')
|
||||
return redirect(redirect_location_on_post)
|
||||
# endregion handle update avatar form
|
||||
|
||||
# region handle update account information form
|
||||
if update_account_information_form.submit.data and update_account_information_form.validate():
|
||||
user.email = update_account_information_form.email.data
|
||||
user.username = update_account_information_form.username.data
|
||||
db.session.commit()
|
||||
flash('Profile settings updated')
|
||||
return redirect(redirect_location_on_post)
|
||||
# endregion handle update account information form
|
||||
|
||||
# region handle update password form
|
||||
if update_password_form.submit.data and update_password_form.validate():
|
||||
user.password = update_password_form.new_password.data
|
||||
db.session.commit()
|
||||
flash('Your changes have been saved')
|
||||
return redirect(redirect_location_on_post)
|
||||
# endregion handle update password form
|
||||
|
||||
# region handle update notifications form
|
||||
if update_notifications_form.submit.data and update_notifications_form.validate():
|
||||
user.setting_job_status_mail_notification_level = \
|
||||
update_notifications_form.job_status_mail_notification_level.data
|
||||
db.session.commit()
|
||||
flash('Your changes have been saved')
|
||||
return redirect(redirect_location_on_post)
|
||||
# endregion handle update notifications form
|
||||
|
||||
return render_template(
|
||||
'users/settings/settings.html.j2',
|
||||
title='Settings',
|
||||
update_account_information_form=update_account_information_form,
|
||||
update_avatar_form=update_avatar_form,
|
||||
update_notifications_form=update_notifications_form,
|
||||
update_password_form=update_password_form,
|
||||
update_profile_information_form=update_profile_information_form,
|
||||
user=user
|
||||
)
|
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 .avatar import *
|
||||
from .corpus_file import *
|
||||
from .corpus_follower_association import *
|
||||
from .corpus_follower_role import *
|
||||
from .corpus import *
|
||||
from .job_input import *
|
||||
from .job_result import *
|
||||
from .job import *
|
||||
from .role import *
|
||||
from .spacy_nlp_pipeline_model import *
|
||||
from .tesseract_ocr_pipeline_model import *
|
||||
from .token import *
|
||||
from .user import *
|
||||
from .anonymous_user import AnonymousUser
|
||||
from .avatar import Avatar
|
||||
from .corpus_file import CorpusFile
|
||||
from .corpus_follower_association import CorpusFollowerAssociation
|
||||
from .corpus_follower_role import CorpusFollowerPermission, CorpusFollowerRole
|
||||
from .corpus import CorpusStatus, Corpus
|
||||
from .job_input import JobInput
|
||||
from .job_result import JobResult
|
||||
from .job import JobStatus, Job
|
||||
from .role import Permission, Role
|
||||
from .spacy_nlp_pipeline_model import SpaCyNLPPipelineModel
|
||||
from .tesseract_ocr_pipeline_model import TesseractOCRPipelineModel
|
||||
from .token import Token
|
||||
from .user import (
|
||||
ProfilePrivacySettings,
|
||||
UserSettingJobStatusMailNotificationLevel,
|
||||
User
|
||||
)
|
||||
|
||||
|
||||
_models = [
|
||||
Avatar,
|
||||
CorpusFile,
|
||||
CorpusFollowerAssociation,
|
||||
CorpusFollowerRole,
|
||||
Corpus,
|
||||
JobInput,
|
||||
JobResult,
|
||||
Job,
|
||||
Role,
|
||||
SpaCyNLPPipelineModel,
|
||||
TesseractOCRPipelineModel,
|
||||
Token,
|
||||
User
|
||||
]
|
||||
|
||||
|
||||
_enums = [
|
||||
CorpusFollowerPermission,
|
||||
CorpusStatus,
|
||||
JobStatus,
|
||||
Permission,
|
||||
ProfilePrivacySettings,
|
||||
UserSettingJobStatusMailNotificationLevel
|
||||
]
|
||||
|
@ -8,7 +8,7 @@ import shutil
|
||||
import xml.etree.ElementTree as ET
|
||||
from app import db
|
||||
from app.converters.vrt import normalize_vrt_file
|
||||
from app.extensions.nopaque_sqlalchemy_extras import IntEnumColumn
|
||||
from app.extensions.nopaque_sqlalchemy_type_decorators import IntEnumColumn
|
||||
from .corpus_follower_association import CorpusFollowerAssociation
|
||||
|
||||
|
||||
|
@ -42,9 +42,8 @@ def resource_after_delete(mapper, connection, resource):
|
||||
'path': resource.jsonpatch_path
|
||||
}
|
||||
]
|
||||
namespace = '/users'
|
||||
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):
|
||||
@ -55,9 +54,8 @@ def cfa_after_delete(mapper, connection, cfa):
|
||||
'path': jsonpatch_path
|
||||
}
|
||||
]
|
||||
namespace = '/users'
|
||||
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):
|
||||
@ -71,9 +69,8 @@ def resource_after_insert(mapper, connection, resource):
|
||||
'value': jsonpatch_value
|
||||
}
|
||||
]
|
||||
namespace = '/users'
|
||||
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):
|
||||
@ -86,9 +83,8 @@ def cfa_after_insert(mapper, connection, cfa):
|
||||
'value': jsonpatch_value
|
||||
}
|
||||
]
|
||||
namespace = '/users'
|
||||
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):
|
||||
@ -113,9 +109,8 @@ def resource_after_update(mapper, connection, resource):
|
||||
}
|
||||
)
|
||||
if jsonpatch:
|
||||
namespace = '/users'
|
||||
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):
|
||||
|
@ -6,7 +6,7 @@ from time import sleep
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
from app import db
|
||||
from app.extensions.nopaque_sqlalchemy_extras import ContainerColumn, IntEnumColumn
|
||||
from app.extensions.nopaque_sqlalchemy_type_decorators import ContainerColumn, IntEnumColumn
|
||||
|
||||
|
||||
class JobStatus(IntEnum):
|
||||
|
@ -20,14 +20,6 @@ class JobInput(FileMixin, HashidMixin, db.Model):
|
||||
def __repr__(self):
|
||||
return f'<JobInput {self.filename}>'
|
||||
|
||||
@property
|
||||
def content_url(self):
|
||||
return url_for(
|
||||
'jobs.download_job_input',
|
||||
job_id=self.job.id,
|
||||
job_input_id=self.id
|
||||
)
|
||||
|
||||
@property
|
||||
def jsonpatch_path(self):
|
||||
return f'{self.job.jsonpatch_path}/inputs/{self.hashid}'
|
||||
@ -40,7 +32,7 @@ class JobInput(FileMixin, HashidMixin, db.Model):
|
||||
def url(self):
|
||||
return url_for(
|
||||
'jobs.job',
|
||||
job_id=self.job_id,
|
||||
job_input_id=self.id,
|
||||
_anchor=f'job-{self.job.hashid}-input-{self.hashid}'
|
||||
)
|
||||
|
||||
|
@ -22,14 +22,6 @@ class JobResult(FileMixin, HashidMixin, db.Model):
|
||||
def __repr__(self):
|
||||
return f'<JobResult {self.filename}>'
|
||||
|
||||
@property
|
||||
def download_url(self):
|
||||
return url_for(
|
||||
'jobs.download_job_result',
|
||||
job_id=self.job_id,
|
||||
job_result_id=self.id
|
||||
)
|
||||
|
||||
@property
|
||||
def jsonpatch_path(self):
|
||||
return f'{self.job.jsonpatch_path}/results/{self.hashid}'
|
||||
@ -41,8 +33,8 @@ class JobResult(FileMixin, HashidMixin, db.Model):
|
||||
@property
|
||||
def url(self):
|
||||
return url_for(
|
||||
'jobs.job',
|
||||
job_id=self.job_id,
|
||||
'job_results.job_result',
|
||||
job_result_id=self.id,
|
||||
_anchor=f'job-{self.job.hashid}-result-{self.hashid}'
|
||||
)
|
||||
|
||||
|
@ -5,7 +5,7 @@ from pathlib import Path
|
||||
import requests
|
||||
import yaml
|
||||
from app import db
|
||||
from app.extensions.nopaque_sqlalchemy_extras import ContainerColumn
|
||||
from app.extensions.nopaque_sqlalchemy_type_decorators import ContainerColumn
|
||||
from .file_mixin import FileMixin
|
||||
from .user import User
|
||||
|
||||
|
@ -5,7 +5,7 @@ from pathlib import Path
|
||||
import requests
|
||||
import yaml
|
||||
from app import db
|
||||
from app.extensions.nopaque_sqlalchemy_extras import ContainerColumn
|
||||
from app.extensions.nopaque_sqlalchemy_type_decorators import ContainerColumn
|
||||
from .file_mixin import FileMixin
|
||||
from .user import User
|
||||
|
||||
|
@ -11,7 +11,7 @@ import re
|
||||
import secrets
|
||||
import shutil
|
||||
from app import db, hashids
|
||||
from app.extensions.nopaque_sqlalchemy_extras import IntEnumColumn
|
||||
from app.extensions.nopaque_sqlalchemy_type_decorators import IntEnumColumn
|
||||
from .corpus import Corpus
|
||||
from .corpus_follower_association import CorpusFollowerAssociation
|
||||
from .corpus_follower_role import CorpusFollowerRole
|
||||
|
@ -1,215 +0,0 @@
|
||||
from datetime import datetime
|
||||
from flask import current_app, Flask, url_for
|
||||
from flask_login import current_user
|
||||
from flask_socketio import Namespace
|
||||
from string import punctuation
|
||||
import nltk
|
||||
from app import db, hashids, socketio
|
||||
from app.decorators import socketio_login_required
|
||||
from app.models import Corpus, CorpusFollowerAssociation, CorpusFollowerRole
|
||||
|
||||
|
||||
def _delete_corpus(app: Flask, corpus_id: int):
|
||||
with app.app_context():
|
||||
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()
|
||||
|
||||
|
||||
class CorporaNamespace(Namespace):
|
||||
@socketio_login_required
|
||||
def on_delete(self, corpus_hashid: str) -> dict:
|
||||
if not isinstance(corpus_hashid, str):
|
||||
return {'status': 400, 'statusText': 'Bad Request'}
|
||||
|
||||
corpus_id = hashids.decode(corpus_hashid)
|
||||
|
||||
if not isinstance(corpus_id, int):
|
||||
return {'status': 400, 'statusText': 'Bad Request'}
|
||||
|
||||
corpus = Corpus.query.get(corpus_id)
|
||||
|
||||
if corpus is None:
|
||||
return {'status': 404, 'statusText': 'Not Found'}
|
||||
|
||||
if not (
|
||||
corpus.user == current_user
|
||||
or current_user.is_administrator
|
||||
):
|
||||
return {'status': 403, 'statusText': 'Forbidden'}
|
||||
|
||||
socketio.start_background_task(
|
||||
_delete_corpus,
|
||||
current_app._get_current_object(),
|
||||
corpus_id
|
||||
)
|
||||
|
||||
return {
|
||||
'body': f'Corpus "{corpus.title}" marked for deletion',
|
||||
'status': 202,
|
||||
'statusText': 'Accepted'
|
||||
}
|
||||
|
||||
@socketio_login_required
|
||||
def on_build(self, corpus_hashid: str) -> dict:
|
||||
if not isinstance(corpus_hashid, str):
|
||||
return {'status': 400, 'statusText': 'Bad Request'}
|
||||
|
||||
corpus_id = hashids.decode(corpus_hashid)
|
||||
|
||||
if not isinstance(corpus_id, int):
|
||||
return {'status': 400, 'statusText': 'Bad Request'}
|
||||
|
||||
corpus = Corpus.query.get(corpus_id)
|
||||
|
||||
if corpus is None:
|
||||
return {'status': 404, 'statusText': 'Not Found'}
|
||||
|
||||
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
|
||||
):
|
||||
return {'status': 403, 'statusText': 'Forbidden'}
|
||||
|
||||
if len(corpus.files.all()) == 0:
|
||||
return {'status': 409, 'statusText': 'Conflict'}
|
||||
|
||||
socketio.start_background_task(
|
||||
_build_corpus,
|
||||
current_app._get_current_object(),
|
||||
corpus_id
|
||||
)
|
||||
|
||||
return {
|
||||
'body': f'Corpus "{corpus.title}" marked for building',
|
||||
'status': 202,
|
||||
'statusText': 'Accepted'
|
||||
}
|
||||
|
||||
# TODO: Think about where to place this, as this does not belong here...
|
||||
@socketio_login_required
|
||||
def on_get_stopwords(self):
|
||||
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 {
|
||||
'body': stopwords,
|
||||
'status': 200,
|
||||
'statusText': 'OK'
|
||||
}
|
||||
|
||||
@socketio_login_required
|
||||
def on_create_share_link(self, corpus_hashid: str, expiration_date: str, role_name: str) -> dict:
|
||||
if not isinstance(corpus_hashid, str):
|
||||
return {'status': 400, 'statusText': 'Bad Request'}
|
||||
|
||||
if not isinstance(expiration_date, str):
|
||||
return {'status': 400, 'statusText': 'Bad Request'}
|
||||
|
||||
if not isinstance(role_name, str):
|
||||
return {'status': 400, 'statusText': 'Bad Request'}
|
||||
|
||||
print(corpus_hashid, expiration_date, role_name)
|
||||
|
||||
corpus_id = hashids.decode(corpus_hashid)
|
||||
|
||||
if not isinstance(corpus_id, int):
|
||||
return {'status': 400, 'statusText': 'Bad Request'}
|
||||
|
||||
corpus = Corpus.query.get(corpus_id)
|
||||
|
||||
if corpus is None:
|
||||
return {'status': 404, 'statusText': 'Not Found'}
|
||||
|
||||
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
|
||||
):
|
||||
return {'status': 403, 'statusText': 'Forbidden'}
|
||||
|
||||
_expiration_date = datetime.strptime(expiration_date, '%b %d, %Y')
|
||||
|
||||
cfr = CorpusFollowerRole.query.filter_by(name=role_name).first()
|
||||
if cfr is None:
|
||||
return {'status': 400, 'statusText': 'Bad Request'}
|
||||
|
||||
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 {
|
||||
'body': corpus_share_link,
|
||||
'status': 200,
|
||||
'statusText': 'OK'
|
||||
}
|
||||
|
||||
@socketio_login_required
|
||||
def on_set_is_public(corpus_hashid: str, new_value: bool) -> dict:
|
||||
if not isinstance(corpus_id, str):
|
||||
return {'status': 400, 'statusText': 'Bad Request'}
|
||||
|
||||
if not isinstance(new_value, bool):
|
||||
return {'status': 400, 'statusText': 'Bad Request'}
|
||||
|
||||
corpus_id = hashids.decode(corpus_hashid)
|
||||
|
||||
if not isinstance(corpus_id, int):
|
||||
return {'status': 400, 'statusText': 'Bad Request'}
|
||||
|
||||
corpus = Corpus.query.get(corpus_id)
|
||||
|
||||
if corpus is None:
|
||||
return {'status': 404, 'statusText': 'Not Found'}
|
||||
|
||||
if not (
|
||||
corpus.user == current_user
|
||||
or current_user.is_administrator
|
||||
):
|
||||
return {'status': 403, 'statusText': 'Forbidden'}
|
||||
|
||||
corpus.is_public = new_value
|
||||
db.session.commit()
|
||||
|
||||
return {
|
||||
'body': f'Corpus "{corpus.title}" is now {"public" if new_value else "private"}',
|
||||
'status': 200,
|
||||
'statusText': 'OK'
|
||||
}
|
@ -1,128 +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:
|
||||
if not isinstance(user_hashid, str):
|
||||
return {'status': 400, 'statusText': 'Bad Request'}
|
||||
|
||||
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:
|
||||
if not isinstance(user_hashid, str):
|
||||
return {'status': 400, 'statusText': 'Bad Request'}
|
||||
|
||||
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:
|
||||
if not isinstance(user_hashid, str):
|
||||
return {'status': 400, 'statusText': 'Bad Request'}
|
||||
|
||||
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:
|
||||
if not isinstance(user_hashid, str):
|
||||
return {'status': 400, 'statusText': 'Bad Request'}
|
||||
|
||||
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'
|
||||
}
|
@ -5,6 +5,8 @@ nopaque.app.Client = class Client {
|
||||
// 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
|
||||
|
@ -5,53 +5,89 @@ nopaque.app.endpoints.Corpora = class Corpora {
|
||||
this.socket = io('/corpora', {transports: ['websocket'], upgrade: false});
|
||||
}
|
||||
|
||||
async delete(id) {
|
||||
const response = await this.socket.emitWithAck('delete', id);
|
||||
async delete(corpusId) {
|
||||
const options = {
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
method: 'DELETE'
|
||||
};
|
||||
|
||||
if (response.status != 202) {
|
||||
throw new Error(`[${response.status}] ${response.statusText}`);
|
||||
}
|
||||
const response = await fetch(`/corpora/${corpusId}`, options);
|
||||
const data = await response.json();
|
||||
|
||||
return response.body;
|
||||
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async build(id) {
|
||||
const response = await this.socket.emitWithAck('build', id);
|
||||
async build(corpusId) {
|
||||
const options = {
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
method: 'POST'
|
||||
};
|
||||
|
||||
if (response.status != 202) {
|
||||
throw new Error(`[${response.status}] ${response.statusText}`);
|
||||
}
|
||||
const response = await fetch(`/corpora/${corpusId}/build`, options);
|
||||
const data = await response.json();
|
||||
|
||||
return response.body;
|
||||
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async getStopwords() {
|
||||
const response = await this.socket.emitWithAck('get_stopwords');
|
||||
async getStopwords(corpusId) {
|
||||
const options = {
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
if (response.status != 200) {
|
||||
throw new Error(`[${response.status}] ${response.statusText}`);
|
||||
}
|
||||
const response = await fetch(`/corpora/${corpusId}/analysis/stopwords`, options);
|
||||
const data = await response.json();
|
||||
|
||||
return response.body;
|
||||
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async createShareLink(id, expirationDate, roleName) {
|
||||
const response = await this.socket.emitWithAck('create_share_link', id, expirationDate, roleName);
|
||||
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'
|
||||
};
|
||||
|
||||
if (response.status != 200) {
|
||||
throw new Error(`[${response.status}] ${response.statusText}`);
|
||||
}
|
||||
const response = await fetch(`/corpora/${corpusId}/create-share-link`, options);
|
||||
const data = await response.json();
|
||||
|
||||
return response.body;
|
||||
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async setIsPublic(id, newValue) {
|
||||
const response = await this.socket.emitWithAck('set_is_public', id, newValue);
|
||||
async updateIsPublic(corpusId, newValue) {
|
||||
const options = {
|
||||
body: JSON.stringify(newValue),
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
method: 'PUT',
|
||||
};
|
||||
|
||||
if (response.status != 200) {
|
||||
throw new Error(`[${response.status}] ${response.statusText}`);
|
||||
}
|
||||
const response = await fetch(`/corpora/${corpusId}/is-public`, options);
|
||||
const data = await response.json();
|
||||
|
||||
return response.body;
|
||||
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,8 @@
|
||||
nopaque.app.endpoints.Jobs = class Jobs {
|
||||
constructor(app) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
async delete(jobId) {
|
||||
const options = {
|
||||
headers: {
|
||||
@ -34,7 +38,8 @@ nopaque.app.endpoints.Jobs = class Jobs {
|
||||
const options = {
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
}
|
||||
},
|
||||
method: 'POST'
|
||||
};
|
||||
|
||||
const response = await fetch(`/jobs/${jobId}/restart`, options);
|
||||
|
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;
|
||||
}
|
||||
}
|
@ -1,43 +1,52 @@
|
||||
nopaque.app.endpoints.Users = class Users {
|
||||
constructor(app) {
|
||||
this.app = app;
|
||||
|
||||
this.socket = io('/users', {transports: ['websocket'], upgrade: false});
|
||||
}
|
||||
|
||||
async get(id) {
|
||||
const response = await this.socket.emitWithAck('get', id);
|
||||
async get(userId) {
|
||||
const options = {
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`[${response.status}] ${response.statusText}`);
|
||||
}
|
||||
const response = await fetch(`/users/${userId}`, options);
|
||||
const data = await response.json();
|
||||
|
||||
return response.body;
|
||||
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async subscribe(id) {
|
||||
const response = await this.socket.emitWithAck('subscribe', id);
|
||||
async subscribe(userId) {
|
||||
const response = await this.app.socket.emitWithAck('SUBSCRIBE User', userId);
|
||||
|
||||
if (response.status != 200) {
|
||||
throw new Error(`[${response.status}] ${response.statusText}`);
|
||||
if (response.code != 204) {
|
||||
throw new Error(`${response.name}: ${response.description}`);
|
||||
}
|
||||
}
|
||||
|
||||
async unsubscribe(id) {
|
||||
const response = await this.socket.emitWithAck('unsubscribe', id);
|
||||
async unsubscribe(userId) {
|
||||
const response = await this.app.socket.emitWithAck('UNSUBSCRIBE User', userId);
|
||||
|
||||
if (response.status != 200) {
|
||||
throw new Error(`[${response.status}] ${response.statusText}`);
|
||||
if (response.status != 204) {
|
||||
throw new Error(`${response.name}: ${response.description}`);
|
||||
}
|
||||
}
|
||||
|
||||
async delete(id) {
|
||||
const response = await this.socket.emitWithAck('delete', id);
|
||||
async delete(userId) {
|
||||
const options = {
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
method: 'DELETE'
|
||||
};
|
||||
|
||||
if (response.status != 202) {
|
||||
throw new Error(`[${response.status}] ${response.statusText}`);
|
||||
}
|
||||
const response = await fetch(`/users/${userId}`, options);
|
||||
const data = await response.json();
|
||||
|
||||
return response.body;
|
||||
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
@ -71,10 +71,7 @@ nopaque.app.extensions.UI = class UI {
|
||||
M.Modal.init(
|
||||
document.querySelector('#terms-of-use-modal'),
|
||||
{
|
||||
dismissible: false,
|
||||
onCloseEnd: (modalElement) => {
|
||||
nopaque.requests.users.entity.acceptTermsOfUse();
|
||||
}
|
||||
dismissible: false
|
||||
}
|
||||
);
|
||||
// #endregion
|
||||
|
@ -13,7 +13,7 @@ nopaque.app.extensions.UserHub = class UserHub extends EventTarget {
|
||||
}
|
||||
|
||||
init() {
|
||||
this.app.users.socket.on('patch', (patch) => {this.#onPatch(patch)});
|
||||
this.app.socket.on('PATCH', (patch) => {this.#onPatch(patch)});
|
||||
}
|
||||
|
||||
add(userId) {
|
||||
|
@ -73,7 +73,7 @@ nopaque.corpus_analysis.StaticVisualizationExtension = class StaticVisualization
|
||||
}
|
||||
|
||||
async getStopwords() {
|
||||
const stopwords = await app.corpora.getStopwords();
|
||||
const stopwords = await app.corpora.getStopwords(this.app.corpusId);
|
||||
this.data.originalStopwords = structuredClone(stopwords);
|
||||
this.data.stopwords = structuredClone(stopwords);
|
||||
return stopwords;
|
||||
|
@ -44,8 +44,8 @@
|
||||
Your profile
|
||||
</a>
|
||||
</li>
|
||||
<li {% if request.path == url_for('settings.settings') %}class="active"{% endif %}>
|
||||
<a href="{{ url_for('settings.settings') }}">
|
||||
<li {% if request.path == url_for('settings.index') %}class="active"{% endif %}>
|
||||
<a href="{{ url_for('settings.index') }}">
|
||||
<i class="material-icons">settings</i>
|
||||
Settings
|
||||
</a>
|
||||
@ -68,8 +68,8 @@
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if current_user.is_administrator %}
|
||||
<li {% if request.path == url_for('admin.admin') %}class="active"{% endif %}>
|
||||
<a href="{{ url_for('admin.admin') }}">
|
||||
<li>
|
||||
<a href="{{ url_for('admin.index') }}">
|
||||
<i class="material-icons left">admin_panel_settings</i>
|
||||
Administration
|
||||
</a>
|
||||
|
@ -1,34 +1,28 @@
|
||||
{% if current_user.is_authenticated and not current_user.terms_of_use_accepted %}
|
||||
<div id="terms-of-use-modal" class="modal modal-fixed-footer">
|
||||
<div class="modal modal-fixed-footer" id="terms-of-use-modal">
|
||||
<div class="modal-content">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col s12">
|
||||
<h1 id="title">Terms of use</h1>
|
||||
</div>
|
||||
<div class="col s12">
|
||||
<div class="switch">
|
||||
<label>
|
||||
DE
|
||||
<input type="checkbox" id="terms-of-use-modal-switch">
|
||||
<span class="lever"></span>
|
||||
EN
|
||||
</label>
|
||||
</div>
|
||||
<br>
|
||||
</div>
|
||||
<div class="terms-of-use-modal-content hide">
|
||||
{% include "main/terms_of_use_en.html.j2" %}
|
||||
</div>
|
||||
<div class="terms-of-use-modal-content">
|
||||
{% include "main/terms_of_use_de.html.j2" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-panel primary-color white-text">
|
||||
<h4 class="m-3 center-align">Terms of use</h4>
|
||||
</div>
|
||||
|
||||
<ul class="tabs tabs-fixed-width z-depth-1">
|
||||
<li class="tab"><a class="active" href="#terms-of-use-modal-content-german">German</a></li>
|
||||
<li class="tab"><a href="#terms-of-use-modal-content-english">English</a></li>
|
||||
</ul>
|
||||
|
||||
<div id="terms-of-use-modal-content-german">
|
||||
{% include 'main/_terms_of_use/german.html.j2' %}
|
||||
</div>
|
||||
|
||||
<div id="terms-of-use-modal-content-english">
|
||||
{% include 'main/_terms_of_use/english.html.j2' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<span style="margin-right:20px;">I have taken note of the new GTC and agree to their validity in the context of my further use.</span>
|
||||
<a href="#!" class="modal-close waves-effect waves-green btn">Yes</a>
|
||||
{% if current_user.is_authenticated and not current_user.terms_of_use_accepted %}
|
||||
<a href="#!" class="btn waves-effect waves-light modal-close" id="terms-of-use-modal-accept-button">Accept</a>
|
||||
{% else %}
|
||||
<a href="#!" class="btn-flat waves-effect waves-light modal-close">Close</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -13,6 +13,8 @@
|
||||
'js/app/endpoints/index.js',
|
||||
'js/app/endpoints/corpora.js',
|
||||
'js/app/endpoints/jobs.js',
|
||||
'js/app/endpoints/main.js',
|
||||
'js/app/endpoints/settings.js',
|
||||
'js/app/endpoints/users.js',
|
||||
'js/app/extensions/index.js',
|
||||
'js/app/extensions/toaster.js',
|
||||
@ -82,36 +84,48 @@
|
||||
|
||||
{# TODO: Think about implementing the following inside a main.js(.j2) #}
|
||||
<script>
|
||||
const app = new nopaque.app.Client();
|
||||
app.init();
|
||||
var app;
|
||||
var currentUserId;
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
const currentUserId = {{ current_user.hashid|tojson }};
|
||||
async function main() {
|
||||
app = new nopaque.app.Client();
|
||||
app.init();
|
||||
|
||||
app.userHub.add(currentUserId)
|
||||
.catch((error) => {throw JSON.stringify(error);});
|
||||
{% if not current_user.is_authenticated %}
|
||||
const currentUserId = null;
|
||||
{% else %}
|
||||
currentUserId = {{ current_user.hashid|tojson }};
|
||||
|
||||
{% if not current_user.terms_of_use_accepted %}
|
||||
M.Modal.getInstance(document.querySelector('#terms-of-use-modal')).open();
|
||||
{% endif %}
|
||||
{% else %}
|
||||
const currentUserId = null;
|
||||
{% endif %}
|
||||
try {
|
||||
await app.userHub.add(currentUserId);
|
||||
} catch (error) {
|
||||
app.ui.flash('Failed to load user data.', 'error');
|
||||
}
|
||||
|
||||
// Display flashed messages
|
||||
for (let [category, message] of {{ get_flashed_messages(with_categories=True)|tojson }}) {
|
||||
app.ui.flash(message, message);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
let languageModalSwitch = document.querySelector('#terms-of-use-modal-switch');
|
||||
let termsOfUseModalContent = document.querySelectorAll('.terms-of-use-modal-content');
|
||||
if (languageModalSwitch) {
|
||||
languageModalSwitch.addEventListener('change', () => {
|
||||
termsOfUseModalContent.forEach(content => {
|
||||
content.classList.toggle('hide');
|
||||
});
|
||||
{% if not current_user.terms_of_use_accepted %}
|
||||
const termsOfUseAcceptButtonElement = document.querySelector('#terms-of-use-modal-accept-button');
|
||||
termsOfUseAcceptButtonElement.addEventListener('click', async () => {
|
||||
try {
|
||||
await app.main.acceptTermsOfUse();
|
||||
app.ui.flash('Terms of use accepted.');
|
||||
} catch (error) {
|
||||
app.ui.flash('Failed to accept terms of use.', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
const termsOfUseModalElement = document.querySelector('#terms-of-use-modal');
|
||||
const termsOfUseModal = M.Modal.getInstance(termsOfUseModalElement);
|
||||
|
||||
termsOfUseModal.open();
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
const flashedMessages = {{ get_flashed_messages(with_categories=true)|tojson }};
|
||||
|
||||
for (let [category, message] of flashedMessages) {
|
||||
app.ui.flash(message, category);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
</script>
|
||||
|
@ -85,8 +85,8 @@
|
||||
</li>
|
||||
|
||||
{# settings #}
|
||||
<li {% if request.path == url_for('settings.settings') %}class="active"{% endif %}>
|
||||
<a class="waves-effect" href="{{ url_for('settings.settings') }}"><i class="material-icons">settings</i>Settings</a>
|
||||
<li {% if request.path == url_for('settings.index') %}class="active"{% endif %}>
|
||||
<a class="waves-effect" href="{{ url_for('settings.index') }}"><i class="material-icons">settings</i>Settings</a>
|
||||
</li>
|
||||
|
||||
{# log out #}
|
||||
@ -118,8 +118,8 @@
|
||||
|
||||
{% if current_user.is_administrator %}
|
||||
{# Administration #}
|
||||
<li {% if request.path == url_for('admin.admin') %}class="active"{% endif %}>
|
||||
<a class="waves-effect" href="{{ url_for('admin.admin') }}"><i class="material-icons">admin_panel_settings</i>Administration</a>
|
||||
<li>
|
||||
<a class="waves-effect" href="{{ url_for('admin.index') }}"><i class="material-icons">admin_panel_settings</i>Administration</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</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 %}
|
@ -70,7 +70,7 @@
|
||||
<div class="modal no-autoinit" id="corpus-analysis-init-modal">
|
||||
<div class="modal-content">
|
||||
<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>
|
||||
<p>
|
||||
Our server works as hard as it can to prepare your analysis session. Please be patient and give it some time.<br>
|
||||
|
@ -246,7 +246,7 @@
|
||||
let publishingModalIsPublicSwitchElement = document.querySelector('#publishing-modal-is-public-switch');
|
||||
publishingModalIsPublicSwitchElement.addEventListener('change', (event) => {
|
||||
let newIsPublic = publishingModalIsPublicSwitchElement.checked;
|
||||
app.corpora.setIsPublic.update({{ corpus.hashid|tojson }}, newIsPublic)
|
||||
app.corpora.updateIsPublic({{ corpus.hashid|tojson }}, newIsPublic)
|
||||
.catch((response) => {
|
||||
publishingModalIsPublicSwitchElement.checked = !newIsPublic;
|
||||
});
|
||||
|
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 %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col s12">
|
||||
<h1 id="title">{{ title }}</h1>
|
||||
</div>
|
||||
<div class="col s12">
|
||||
<div class="switch">
|
||||
<label>
|
||||
DE
|
||||
<input type="checkbox" id="terms-of-use-page-switch">
|
||||
<span class="lever"></span>
|
||||
EN
|
||||
</label>
|
||||
</div>
|
||||
<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>
|
||||
<h1 id="title">{{ title }}</h1>
|
||||
|
||||
<ul class="tabs tabs-fixed-width z-depth-1">
|
||||
<li class="tab"><a class="active" href="#terms-of-use-page-content-german">German</a></li>
|
||||
<li class="tab"><a href="#terms-of-use-page-content-english">English</a></li>
|
||||
</ul>
|
||||
|
||||
<div id="terms-of-use-page-content-german">
|
||||
{% include "main/_terms_of_use/german.html.j2" %}
|
||||
</div>
|
||||
|
||||
<div id="terms-of-use-page-content-english">
|
||||
{% include "main/_terms_of_use/english.html.j2" %}
|
||||
</div>
|
||||
</div>
|
||||
{% 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>
|
@ -46,19 +46,19 @@
|
||||
<div class="row" style="margin-left: 24px;">
|
||||
<div class="col s12 l3">
|
||||
<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>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col s12 l3">
|
||||
<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>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col s12 l3">
|
||||
<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>
|
||||
</label>
|
||||
</div>
|
||||
@ -74,7 +74,7 @@
|
||||
<form method="POST">
|
||||
{{ 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.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.organization, material_icon='business') }}
|
||||
{{ wtf.render_field(update_profile_information_form.location, material_icon='location_on') }}
|
||||
@ -172,8 +172,6 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% block admin_settings %}{% endblock admin_settings %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock page_content %}
|
||||
@ -252,28 +250,28 @@ for (let collapsibleElement of document.querySelectorAll('.collapsible.no-autoin
|
||||
// #region Profile Privacy settings
|
||||
let profileIsPublicSwitchElement = document.querySelector('#profile-is-public-switch');
|
||||
let profilePrivacySettingCheckboxElements = document.querySelectorAll('.profile-privacy-setting-checkbox');
|
||||
profileIsPublicSwitchElement.addEventListener('change', (event) => {
|
||||
let newEnabled = profileIsPublicSwitchElement.checked;
|
||||
nopaque.requests.users.entity.settings.profilePrivacy.update({{ user.hashid|tojson }}, 'is-public', newEnabled)
|
||||
.then(
|
||||
(response) => {
|
||||
for (let profilePrivacySettingCheckboxElement of document.querySelectorAll('.profile-privacy-setting-checkbox')) {
|
||||
profilePrivacySettingCheckboxElement.disabled = !newEnabled;
|
||||
}
|
||||
},
|
||||
(response) => {
|
||||
profileIsPublicSwitchElement.checked = !newEnabled;
|
||||
}
|
||||
);
|
||||
profileIsPublicSwitchElement.addEventListener('change', async (event) => {
|
||||
const newEnabled = profileIsPublicSwitchElement.checked;
|
||||
try {
|
||||
const message = await app.settings.updateProfileIsPublic(newEnabled);
|
||||
for (let profilePrivacySettingCheckboxElement of profilePrivacySettingCheckboxElements) {
|
||||
profilePrivacySettingCheckboxElement.disabled = !newEnabled;
|
||||
}
|
||||
app.ui.flash(message);
|
||||
} catch (e) {
|
||||
profileIsPublicSwitchElement.checked = !newEnabled;
|
||||
app.ui.flash(e.message, 'error');
|
||||
}
|
||||
});
|
||||
for (let profilePrivacySettingCheckboxElement of profilePrivacySettingCheckboxElements) {
|
||||
profilePrivacySettingCheckboxElement.addEventListener('change', (event) => {
|
||||
let newEnabled = profilePrivacySettingCheckboxElement.checked;
|
||||
let valueName = profilePrivacySettingCheckboxElement.dataset.profilePrivacySettingName;
|
||||
nopaque.requests.users.entity.settings.profilePrivacy.update({{ user.hashid|tojson }}, valueName, newEnabled)
|
||||
.catch((response) => {
|
||||
profilePrivacySettingCheckboxElement.addEventListener('change', async (event) => {
|
||||
const newEnabled = profilePrivacySettingCheckboxElement.checked;
|
||||
const valueName = profilePrivacySettingCheckboxElement.dataset.profilePrivacySettingName;
|
||||
try {
|
||||
app.settings[`update${valueName}`](newEnabled)
|
||||
} catch (error) {
|
||||
profilePrivacySettingCheckboxElement.checked = !newEnabled;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// #endregion Profile Privacy settings
|
@ -7,6 +7,8 @@
|
||||
{{ render_integer_field(field, *args, **kwargs) }}
|
||||
{% elif field.type == 'MultipleFileField' %}
|
||||
{{ render_multiple_file_field(field, *args, **kwargs) }}
|
||||
{% elif field.type == 'StringField' %}
|
||||
{{ render_string_field(field, *args, **kwargs) }}
|
||||
{% elif field.type == 'SubmitField' %}
|
||||
{{ render_submit_field(field, *args, **kwargs) }}
|
||||
{% elif field.type == 'TextAreaField' %}
|
||||
@ -20,7 +22,7 @@
|
||||
{% macro render_boolean_field(field) %}
|
||||
<div>
|
||||
<label>
|
||||
<input id="{{ field.id }}" name="{{ field.name }}" type="checkbox">
|
||||
{{ field(*args, **kwargs) }}
|
||||
<span>{{ field.label.text }}</span>
|
||||
{% for error in field.errors %}
|
||||
<span class="helper-text error-color-text">{{ error }}</span>
|
||||
@ -36,25 +38,7 @@
|
||||
<div class="file-field input-field">
|
||||
<div class="btn">
|
||||
<span>{{ field.label.text }}</span>
|
||||
<input id="{{ field.id }}" name="{{ field.name }}" type="file">
|
||||
</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>
|
||||
{{ field(*args, **kwargs) }}
|
||||
</div>
|
||||
<div class="file-path-wrapper">
|
||||
<input class="file-path validate" type="text" placeholder="{{ placeholder }}">
|
||||
@ -67,12 +51,29 @@
|
||||
|
||||
|
||||
{% macro render_integer_field(field) %}
|
||||
<div class="input-field">
|
||||
{% if 'material_icon' in kwargs %}
|
||||
<i class="material-icons prefix">{{ kwargs.pop('material_icon') }}</i>
|
||||
{% set classes = kwargs.pop('class_', '').split(' ') %}
|
||||
|
||||
{% if 'validate' not in classes %}
|
||||
{% set _ = classes.append('validate') %}
|
||||
{% 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 %}
|
||||
<span class="helper-text error-color-text">{{ error }}</span>
|
||||
{% endfor %}
|
||||
@ -80,8 +81,21 @@
|
||||
{% 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) %}
|
||||
<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 }}
|
||||
{% if 'material_icon' in kwargs %}
|
||||
<i class="material-icons right">{{ kwargs.pop('material_icon') }}</i>
|
||||
@ -91,26 +105,23 @@
|
||||
|
||||
|
||||
{% macro render_text_area_field(field) %}
|
||||
<div class="input-field">
|
||||
{% if 'material_icon' in kwargs %}
|
||||
<i class="material-icons prefix">{{ kwargs.pop('material_icon') }}</i>
|
||||
{% set classes = kwargs.pop('class_', '').split(' ') %}
|
||||
|
||||
{% if 'materialize-textarea' not in classes %}
|
||||
{% set _ = classes.append('materialize-textarea') %}
|
||||
{% endif %}
|
||||
<textarea class="materialize-textarea validate" id="{{ field.id }}" name="{{ field.name }}"></textarea>
|
||||
<label for="{{ field.id }}">{{ field.label.text }}</label>
|
||||
{% for error in field.errors %}
|
||||
<span class="helper-text error-color-text">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% 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_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">
|
||||
{% if 'material_icon' in kwargs %}
|
||||
<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_PASSWORD = os.environ.get('NOPAQUE_READCOOP_PASSWORD')
|
||||
|
||||
NOPAQUE_VERSION='1.1.0'
|
||||
NOPAQUE_VERSION='1.1.1'
|
||||
# endregion nopaque
|
||||
|
@ -14,6 +14,7 @@ docker==7.0.0
|
||||
email_validator==2.1.1
|
||||
eventlet==0.34.2
|
||||
Flask==2.3.3
|
||||
Flask-Admin==1.6.1
|
||||
Flask-APScheduler==1.13.1
|
||||
Flask-Assets==2.1.0
|
||||
Flask-Hashids==1.0.3
|
||||
|
@ -4,6 +4,7 @@ dnspython==2.5.0
|
||||
docker
|
||||
eventlet==0.34.2
|
||||
Flask==2.3.3
|
||||
Flask-Admin==1.6.1
|
||||
Flask-APScheduler
|
||||
Flask-Assets
|
||||
Flask-Hashids
|
||||
|
Reference in New Issue
Block a user