mirror of
https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
synced 2025-03-13 14:10:35 +00:00
Compare commits
No commits in common. "d4cd3139402a5cd29fac51a4dda72f56a7657b05" and "713a7645dbcadf4c2e7d03b57562ac3037840fec" have entirely different histories.
d4cd313940
...
713a7645db
@ -3,7 +3,6 @@ from config import Config
|
|||||||
from docker import DockerClient
|
from docker import DockerClient
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask.logging import default_handler
|
from flask.logging import default_handler
|
||||||
from flask_admin import Admin
|
|
||||||
from flask_apscheduler import APScheduler
|
from flask_apscheduler import APScheduler
|
||||||
from flask_assets import Environment
|
from flask_assets import Environment
|
||||||
from flask_login import LoginManager
|
from flask_login import LoginManager
|
||||||
@ -16,12 +15,10 @@ from flask_sqlalchemy import SQLAlchemy
|
|||||||
from flask_hashids import Hashids
|
from flask_hashids import Hashids
|
||||||
from logging import Formatter, StreamHandler
|
from logging import Formatter, StreamHandler
|
||||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
from .extensions.nopaque_flask_admin_views import AdminIndexView, ModelView
|
|
||||||
|
|
||||||
|
|
||||||
docker_client = DockerClient.from_env()
|
docker_client = DockerClient.from_env()
|
||||||
|
|
||||||
admin = Admin()
|
|
||||||
apifairy = APIFairy()
|
apifairy = APIFairy()
|
||||||
assets = Environment()
|
assets = Environment()
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
@ -77,7 +74,6 @@ def create_app(config: Config = Config) -> Flask:
|
|||||||
|
|
||||||
from .models import AnonymousUser, User
|
from .models import AnonymousUser, User
|
||||||
|
|
||||||
admin.init_app(app, index_view=AdminIndexView())
|
|
||||||
apifairy.init_app(app)
|
apifairy.init_app(app)
|
||||||
assets.init_app(app)
|
assets.init_app(app)
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
@ -96,6 +92,9 @@ def create_app(config: Config = Config) -> Flask:
|
|||||||
# endregion Extensions
|
# endregion Extensions
|
||||||
|
|
||||||
# region Blueprints
|
# region Blueprints
|
||||||
|
from .blueprints.admin import bp as admin_blueprint
|
||||||
|
app.register_blueprint(admin_blueprint, url_prefix='/admin')
|
||||||
|
|
||||||
from .blueprints.api import bp as api_blueprint
|
from .blueprints.api import bp as api_blueprint
|
||||||
app.register_blueprint(api_blueprint, url_prefix='/api')
|
app.register_blueprint(api_blueprint, url_prefix='/api')
|
||||||
|
|
||||||
@ -128,15 +127,14 @@ def create_app(config: Config = Config) -> Flask:
|
|||||||
|
|
||||||
from .blueprints.workshops import bp as workshops_blueprint
|
from .blueprints.workshops import bp as workshops_blueprint
|
||||||
app.register_blueprint(workshops_blueprint, url_prefix='/workshops')
|
app.register_blueprint(workshops_blueprint, url_prefix='/workshops')
|
||||||
|
|
||||||
from .models import _models
|
|
||||||
for model in _models:
|
|
||||||
admin.add_view(ModelView(model, db.session, category='Database'))
|
|
||||||
# endregion Blueprints
|
# endregion Blueprints
|
||||||
|
|
||||||
# region SocketIO Namespaces
|
# region SocketIO Namespaces
|
||||||
from .namespaces.cqi_over_sio import CQiOverSocketIONamespace
|
from .namespaces.cqi_over_sio import CQiOverSocketIONamespace
|
||||||
socketio.on_namespace(CQiOverSocketIONamespace('/cqi_over_sio'))
|
socketio.on_namespace(CQiOverSocketIONamespace('/cqi_over_sio'))
|
||||||
|
|
||||||
|
from .namespaces.users import UsersNamespace
|
||||||
|
socketio.on_namespace(UsersNamespace('/users'))
|
||||||
# endregion SocketIO Namespaces
|
# endregion SocketIO Namespaces
|
||||||
|
|
||||||
# region Database event Listeners
|
# region Database event Listeners
|
||||||
|
20
app/blueprints/admin/__init__.py
Normal file
20
app/blueprints/admin/__init__.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
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
|
16
app/blueprints/admin/forms.py
Normal file
16
app/blueprints/admin/forms.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
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()]
|
23
app/blueprints/admin/json_routes.py
Normal file
23
app/blueprints/admin/json_routes.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
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
|
136
app/blueprints/admin/routes.py
Normal file
136
app/blueprints/admin/routes.py
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
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
|
||||||
|
)
|
@ -16,4 +16,4 @@ def before_request():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
from . import cli, files, followers, routes
|
from . import cli, files, followers, routes, json_routes
|
||||||
|
45
app/blueprints/corpora/events.py
Normal file
45
app/blueprints/corpora/events.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
from flask_login import current_user
|
||||||
|
from flask_socketio import join_room
|
||||||
|
from app import hashids, socketio
|
||||||
|
from app.decorators import socketio_login_required
|
||||||
|
from app.models import Corpus
|
||||||
|
|
||||||
|
|
||||||
|
@socketio.on('GET /corpora/<corpus_id>')
|
||||||
|
@socketio_login_required
|
||||||
|
def get_corpus(corpus_hashid):
|
||||||
|
corpus_id = hashids.decode(corpus_hashid)
|
||||||
|
corpus = Corpus.query.get(corpus_id)
|
||||||
|
if corpus is None:
|
||||||
|
return {'options': {'status': 404, 'statusText': 'Not found'}}
|
||||||
|
if not (
|
||||||
|
corpus.is_public
|
||||||
|
or corpus.user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
return {'options': {'status': 403, 'statusText': 'Forbidden'}}
|
||||||
|
return {
|
||||||
|
'body': corpus.to_json_serializable(),
|
||||||
|
'options': {
|
||||||
|
'status': 200,
|
||||||
|
'statusText': 'OK',
|
||||||
|
'headers': {'Content-Type: application/json'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@socketio.on('SUBSCRIBE /corpora/<corpus_id>')
|
||||||
|
@socketio_login_required
|
||||||
|
def subscribe_corpus(corpus_hashid):
|
||||||
|
corpus_id = hashids.decode(corpus_hashid)
|
||||||
|
corpus = Corpus.query.get(corpus_id)
|
||||||
|
if corpus is None:
|
||||||
|
return {'options': {'status': 404, 'statusText': 'Not found'}}
|
||||||
|
if not (
|
||||||
|
corpus.is_public
|
||||||
|
or corpus.user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
return {'options': {'status': 403, 'statusText': 'Forbidden'}}
|
||||||
|
join_room(f'/corpora/{corpus.hashid}')
|
||||||
|
return {'options': {'status': 200, 'statusText': 'OK'}}
|
125
app/blueprints/corpora/json_routes.py
Normal file
125
app/blueprints/corpora/json_routes.py
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
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,19 +1,5 @@
|
|||||||
from datetime import datetime
|
from flask import abort, flash, redirect, render_template, url_for
|
||||||
from flask import (
|
|
||||||
abort,
|
|
||||||
current_app,
|
|
||||||
flash,
|
|
||||||
Flask,
|
|
||||||
jsonify,
|
|
||||||
redirect,
|
|
||||||
request,
|
|
||||||
render_template,
|
|
||||||
url_for
|
|
||||||
)
|
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from string import punctuation
|
|
||||||
from threading import Thread
|
|
||||||
import nltk
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import (
|
from app.models import (
|
||||||
Corpus,
|
Corpus,
|
||||||
@ -26,21 +12,6 @@ from .decorators import corpus_follower_permission_required
|
|||||||
from .forms import CreateCorpusForm
|
from .forms import CreateCorpusForm
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _delete_corpus(app: Flask, corpus_id: int):
|
|
||||||
with app.app_context():
|
|
||||||
corpus: Corpus = Corpus.query.get(corpus_id)
|
|
||||||
corpus.delete()
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def _build_corpus(app: Flask, corpus_id: int):
|
|
||||||
with app.app_context():
|
|
||||||
corpus = Corpus.query.get(corpus_id)
|
|
||||||
corpus.build()
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('')
|
@bp.route('')
|
||||||
def corpora():
|
def corpora():
|
||||||
return redirect(url_for('main.dashboard', _anchor='corpora'))
|
return redirect(url_for('main.dashboard', _anchor='corpora'))
|
||||||
@ -49,7 +20,6 @@ def corpora():
|
|||||||
@bp.route('/create', methods=['GET', 'POST'])
|
@bp.route('/create', methods=['GET', 'POST'])
|
||||||
def create_corpus():
|
def create_corpus():
|
||||||
form = CreateCorpusForm()
|
form = CreateCorpusForm()
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
try:
|
try:
|
||||||
corpus = Corpus.create(
|
corpus = Corpus.create(
|
||||||
@ -60,10 +30,8 @@ def create_corpus():
|
|||||||
except OSError:
|
except OSError:
|
||||||
abort(500)
|
abort(500)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
flash(f'Corpus "{corpus.title}" created', 'corpus')
|
flash(f'Corpus "{corpus.title}" created', 'corpus')
|
||||||
return redirect(corpus.url)
|
return redirect(corpus.url)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'corpora/create.html.j2',
|
'corpora/create.html.j2',
|
||||||
title='Create corpus',
|
title='Create corpus',
|
||||||
@ -72,14 +40,12 @@ def create_corpus():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:corpus_id>')
|
@bp.route('/<hashid:corpus_id>')
|
||||||
def corpus(corpus_id: int):
|
def corpus(corpus_id):
|
||||||
corpus = Corpus.query.get_or_404(corpus_id)
|
corpus = Corpus.query.get_or_404(corpus_id)
|
||||||
|
cfrs = CorpusFollowerRole.query.all()
|
||||||
cfa = CorpusFollowerAssociation.query.filter_by(
|
# TODO: Better solution for filtering admin
|
||||||
corpus_id=corpus_id,
|
users = User.query.filter(User.is_public == True, User.id != current_user.id, User.id != corpus.user.id, User.role_id < 4).all()
|
||||||
follower_id=current_user.id
|
cfa = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=current_user.id).first()
|
||||||
).first()
|
|
||||||
|
|
||||||
if cfa is None:
|
if cfa is None:
|
||||||
if corpus.user == current_user or current_user.is_administrator:
|
if corpus.user == current_user or current_user.is_administrator:
|
||||||
cfr = CorpusFollowerRole.query.filter_by(name='Administrator').first()
|
cfr = CorpusFollowerRole.query.filter_by(name='Administrator').first()
|
||||||
@ -87,21 +53,7 @@ def corpus(corpus_id: int):
|
|||||||
cfr = CorpusFollowerRole.query.filter_by(name='Anonymous').first()
|
cfr = CorpusFollowerRole.query.filter_by(name='Anonymous').first()
|
||||||
else:
|
else:
|
||||||
cfr = cfa.role
|
cfr = cfa.role
|
||||||
|
if corpus.user == current_user or current_user.is_administrator:
|
||||||
cfrs = CorpusFollowerRole.query.all()
|
|
||||||
|
|
||||||
# TODO: Better solution for filtering admin
|
|
||||||
users = User.query.filter(
|
|
||||||
User.is_public == True,
|
|
||||||
User.id != current_user.id,
|
|
||||||
User.id != corpus.user.id,
|
|
||||||
User.role_id < 4
|
|
||||||
).all()
|
|
||||||
|
|
||||||
if (
|
|
||||||
corpus.user == current_user
|
|
||||||
or current_user.is_administrator
|
|
||||||
):
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'corpora/corpus.html.j2',
|
'corpora/corpus.html.j2',
|
||||||
title=corpus.title,
|
title=corpus.title,
|
||||||
@ -110,15 +62,8 @@ def corpus(corpus_id: int):
|
|||||||
cfrs=cfrs,
|
cfrs=cfrs,
|
||||||
users=users
|
users=users
|
||||||
)
|
)
|
||||||
|
if (current_user.is_following_corpus(corpus) or corpus.is_public):
|
||||||
if (
|
cfas = CorpusFollowerAssociation.query.filter(Corpus.id == corpus_id, CorpusFollowerAssociation.follower_id != corpus.user.id).all()
|
||||||
current_user.is_following_corpus(corpus)
|
|
||||||
or corpus.is_public
|
|
||||||
):
|
|
||||||
cfas = CorpusFollowerAssociation.query.filter(
|
|
||||||
Corpus.id == corpus_id,
|
|
||||||
CorpusFollowerAssociation.follower_id != corpus.user.id
|
|
||||||
).all()
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'corpora/public_corpus.html.j2',
|
'corpora/public_corpus.html.j2',
|
||||||
title=corpus.title,
|
title=corpus.title,
|
||||||
@ -128,110 +73,14 @@ def corpus(corpus_id: int):
|
|||||||
cfas=cfas,
|
cfas=cfas,
|
||||||
users=users
|
users=users
|
||||||
)
|
)
|
||||||
|
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:corpus_id>', methods=['DELETE'])
|
|
||||||
def delete_corpus(corpus_id: int):
|
|
||||||
corpus = Corpus.query.get_or_404(corpus_id)
|
|
||||||
|
|
||||||
if not (
|
|
||||||
corpus.user == current_user
|
|
||||||
or current_user.is_administrator
|
|
||||||
):
|
|
||||||
abort(403)
|
|
||||||
|
|
||||||
thread = Thread(
|
|
||||||
target=_delete_corpus,
|
|
||||||
args=(current_app._get_current_object(), corpus.id)
|
|
||||||
)
|
|
||||||
thread.start()
|
|
||||||
|
|
||||||
return jsonify(f'Corpus "{corpus.title}" marked for deletion.'), 202
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:corpus_id>/build', methods=['POST'])
|
|
||||||
def build_corpus(corpus_id: int):
|
|
||||||
corpus = Corpus.query.get_or_404(corpus_id)
|
|
||||||
|
|
||||||
cfa = CorpusFollowerAssociation.query.filter_by(
|
|
||||||
corpus_id=corpus_id,
|
|
||||||
follower_id=current_user.id
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if not (
|
|
||||||
cfa is not None and cfa.role.has_permission('MANAGE_FILES')
|
|
||||||
or corpus.user == current_user
|
|
||||||
or current_user.is_administrator
|
|
||||||
):
|
|
||||||
abort(403)
|
|
||||||
|
|
||||||
if len(corpus.files.all()) == 0:
|
|
||||||
abort(409)
|
|
||||||
|
|
||||||
thread = Thread(
|
|
||||||
target=_build_corpus,
|
|
||||||
args=(current_app._get_current_object(), corpus.id)
|
|
||||||
)
|
|
||||||
thread.start()
|
|
||||||
|
|
||||||
return jsonify(f'Corpus "{corpus.title}" marked for building.'), 202
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:corpus_id>/create-share-link', methods=['POST'])
|
|
||||||
def create_share_link(corpus_id: int):
|
|
||||||
data = request.json
|
|
||||||
|
|
||||||
expiration_date = data['expiration_date']
|
|
||||||
if not isinstance(expiration_date, str):
|
|
||||||
abort(400)
|
|
||||||
|
|
||||||
role_name = data['role_name']
|
|
||||||
if not isinstance(role_name, str):
|
|
||||||
abort(400)
|
|
||||||
|
|
||||||
corpus = Corpus.query.get_or_404(corpus_id)
|
|
||||||
|
|
||||||
cfa = CorpusFollowerAssociation.query.filter_by(
|
|
||||||
corpus_id=corpus_id,
|
|
||||||
follower_id=current_user.id
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if not (
|
|
||||||
cfa is not None and cfa.role.has_permission('MANAGE_FOLLOWERS')
|
|
||||||
or corpus.user == current_user
|
|
||||||
or current_user.is_administrator
|
|
||||||
):
|
|
||||||
abort(403)
|
|
||||||
|
|
||||||
_expiration_date = datetime.strptime(expiration_date, '%b %d, %Y')
|
|
||||||
|
|
||||||
cfr = CorpusFollowerRole.query.filter_by(name=role_name).first()
|
|
||||||
if cfr is None:
|
|
||||||
abort(400)
|
|
||||||
|
|
||||||
token = current_user.generate_follow_corpus_token(
|
|
||||||
corpus.hashid,
|
|
||||||
role_name,
|
|
||||||
_expiration_date
|
|
||||||
)
|
|
||||||
|
|
||||||
corpus_share_link = url_for(
|
|
||||||
'corpora.follow_corpus',
|
|
||||||
corpus_id=corpus_id,
|
|
||||||
token=token,
|
|
||||||
_external=True
|
|
||||||
)
|
|
||||||
|
|
||||||
return jsonify(corpus_share_link)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:corpus_id>/analysis')
|
@bp.route('/<hashid:corpus_id>/analysis')
|
||||||
@corpus_follower_permission_required('VIEW')
|
@corpus_follower_permission_required('VIEW')
|
||||||
def analysis(corpus_id: int):
|
def analysis(corpus_id):
|
||||||
corpus = Corpus.query.get_or_404(corpus_id)
|
corpus = Corpus.query.get_or_404(corpus_id)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'corpora/analysis.html.j2',
|
'corpora/analysis.html.j2',
|
||||||
corpus=corpus,
|
corpus=corpus,
|
||||||
@ -239,61 +88,22 @@ def analysis(corpus_id: int):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:corpus_id>/analysis/stopwords')
|
|
||||||
def get_stopwords(corpus_id: int):
|
|
||||||
languages = [
|
|
||||||
'german',
|
|
||||||
'english',
|
|
||||||
'catalan',
|
|
||||||
'greek',
|
|
||||||
'spanish',
|
|
||||||
'french',
|
|
||||||
'italian',
|
|
||||||
'russian',
|
|
||||||
'chinese'
|
|
||||||
]
|
|
||||||
|
|
||||||
nltk.download('stopwords', quiet=True)
|
|
||||||
stopwords = {
|
|
||||||
language: nltk.corpus.stopwords.words(language)
|
|
||||||
for language in languages
|
|
||||||
}
|
|
||||||
stopwords['punctuation'] = list(punctuation)
|
|
||||||
stopwords['punctuation'] += ['—', '|', '–', '“', '„', '--']
|
|
||||||
stopwords['user_stopwords'] = []
|
|
||||||
|
|
||||||
return jsonify(stopwords)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:corpus_id>/follow/<token>')
|
@bp.route('/<hashid:corpus_id>/follow/<token>')
|
||||||
def follow_corpus(corpus_id: int, token: str):
|
def follow_corpus(corpus_id, token):
|
||||||
corpus = Corpus.query.get_or_404(corpus_id)
|
corpus = Corpus.query.get_or_404(corpus_id)
|
||||||
|
if current_user.follow_corpus_by_token(token):
|
||||||
if not current_user.follow_corpus_by_token(token):
|
db.session.commit()
|
||||||
abort(403)
|
flash(f'You are following "{corpus.title}" now', category='corpus')
|
||||||
|
return redirect(url_for('corpora.corpus', corpus_id=corpus_id))
|
||||||
db.session.commit()
|
abort(403)
|
||||||
|
|
||||||
flash(f'You are following "{corpus.title}" now', category='corpus')
|
|
||||||
return redirect(corpus.url)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:corpus_id>/is-public', methods=['PUT'])
|
@bp.route('/import', methods=['GET', 'POST'])
|
||||||
def update_is_public(corpus_id):
|
def import_corpus():
|
||||||
new_value = request.json
|
abort(503)
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
corpus.is_public = new_value
|
@bp.route('/<hashid:corpus_id>/export')
|
||||||
db.session.commit()
|
@corpus_follower_permission_required('VIEW')
|
||||||
|
def export_corpus(corpus_id):
|
||||||
return jsonify(f'Corpus "{corpus.title}" is now {"public" if new_value else "private"}'), 200
|
abort(503)
|
||||||
|
@ -4,17 +4,11 @@ from . import bp
|
|||||||
|
|
||||||
|
|
||||||
@bp.app_errorhandler(HTTPException)
|
@bp.app_errorhandler(HTTPException)
|
||||||
def handle_http_exception(e: HTTPException):
|
def handle_http_exception(error):
|
||||||
''' Generic HTTP exception handler '''
|
''' Generic HTTP exception handler '''
|
||||||
accept_json = request.accept_mimetypes.accept_json
|
accept_json = request.accept_mimetypes.accept_json
|
||||||
accept_html = request.accept_mimetypes.accept_html
|
accept_html = request.accept_mimetypes.accept_html
|
||||||
|
|
||||||
if accept_json and not accept_html:
|
if accept_json and not accept_html:
|
||||||
error = {
|
response = jsonify(str(error))
|
||||||
'code': e.code,
|
return response, error.code
|
||||||
'name': e.name,
|
return render_template('errors/error.html.j2', error=error), error.code
|
||||||
'description': e.description
|
|
||||||
}
|
|
||||||
return jsonify(error), e.code
|
|
||||||
|
|
||||||
return render_template('errors/error.html.j2', error=e), e.code
|
|
||||||
|
@ -1,13 +1,18 @@
|
|||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
|
from flask_login import login_required
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint('jobs', __name__)
|
bp = Blueprint('jobs', __name__)
|
||||||
|
|
||||||
|
|
||||||
from . import routes
|
@bp.before_request
|
||||||
|
@login_required
|
||||||
|
def before_request():
|
||||||
|
'''
|
||||||
|
Ensures that the routes in this package can only be visited by users that
|
||||||
|
are logged in.
|
||||||
|
'''
|
||||||
|
pass
|
||||||
|
|
||||||
from .inputs import bp as inputs_bp
|
|
||||||
bp.register_blueprint(inputs_bp, url_prefix='/<hashid:job_id>/inputs')
|
|
||||||
|
|
||||||
from .results import bp as results_bp
|
from . import routes, json_routes
|
||||||
bp.register_blueprint(results_bp, url_prefix='/<hashid:job_id>/results')
|
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
from flask import Blueprint
|
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint('inputs', __name__)
|
|
||||||
|
|
||||||
|
|
||||||
from . import routes
|
|
@ -1,27 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
72
app/blueprints/jobs/json_routes.py
Normal file
72
app/blueprints/jobs/json_routes.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
from flask import abort, current_app
|
||||||
|
from flask_login import current_user
|
||||||
|
from threading import Thread
|
||||||
|
from app import db
|
||||||
|
from app.decorators import admin_required, content_negotiation
|
||||||
|
from app.models import Job, JobStatus
|
||||||
|
from . import bp
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<hashid:job_id>', methods=['DELETE'])
|
||||||
|
@content_negotiation(produces='application/json')
|
||||||
|
def delete_job(job_id):
|
||||||
|
def _delete_job(app, job_id):
|
||||||
|
with app.app_context():
|
||||||
|
job = Job.query.get(job_id)
|
||||||
|
job.delete()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
job = Job.query.get_or_404(job_id)
|
||||||
|
if not (job.user == current_user or current_user.is_administrator):
|
||||||
|
abort(403)
|
||||||
|
thread = Thread(
|
||||||
|
target=_delete_job,
|
||||||
|
args=(current_app._get_current_object(), job_id)
|
||||||
|
)
|
||||||
|
thread.start()
|
||||||
|
response_data = {
|
||||||
|
'message': f'Job "{job.title}" marked for deletion'
|
||||||
|
}
|
||||||
|
return response_data, 202
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<hashid:job_id>/log')
|
||||||
|
@admin_required
|
||||||
|
@content_negotiation(produces='application/json')
|
||||||
|
def job_log(job_id):
|
||||||
|
job = Job.query.get_or_404(job_id)
|
||||||
|
if job.status not in [JobStatus.COMPLETED, JobStatus.FAILED]:
|
||||||
|
response = {'errors': {'message': 'Job status is not completed or failed'}}
|
||||||
|
return response, 409
|
||||||
|
with open(job.path / 'pipeline_data' / 'logs' / 'pyflow_log.txt') as log_file:
|
||||||
|
log = log_file.read()
|
||||||
|
response_data = {
|
||||||
|
'jobLog': log
|
||||||
|
}
|
||||||
|
return response_data, 200
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<hashid:job_id>/restart', methods=['POST'])
|
||||||
|
@content_negotiation(produces='application/json')
|
||||||
|
def restart_job(job_id):
|
||||||
|
def _restart_job(app, job_id):
|
||||||
|
with app.app_context():
|
||||||
|
job = Job.query.get(job_id)
|
||||||
|
job.restart()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
job = Job.query.get_or_404(job_id)
|
||||||
|
if not (job.user == current_user or current_user.is_administrator):
|
||||||
|
abort(403)
|
||||||
|
if job.status == JobStatus.FAILED:
|
||||||
|
response = {'errors': {'message': 'Job status is not "failed"'}}
|
||||||
|
return response, 409
|
||||||
|
thread = Thread(
|
||||||
|
target=_restart_job,
|
||||||
|
args=(current_app._get_current_object(), job_id)
|
||||||
|
)
|
||||||
|
thread.start()
|
||||||
|
response_data = {
|
||||||
|
'message': f'Job "{job.title}" marked for restarting'
|
||||||
|
}
|
||||||
|
return response_data, 202
|
@ -1,7 +0,0 @@
|
|||||||
from flask import Blueprint
|
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint('results', __name__)
|
|
||||||
|
|
||||||
|
|
||||||
from . import routes
|
|
@ -1,27 +0,0 @@
|
|||||||
from flask import abort, send_from_directory
|
|
||||||
from flask_login import current_user, login_required
|
|
||||||
from app.models import JobResult
|
|
||||||
from . import bp
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:job_result_id>/download')
|
|
||||||
@login_required
|
|
||||||
def download_job_result(job_id: int, job_result_id: int):
|
|
||||||
job_result = JobResult.query.filter_by(
|
|
||||||
job_id=job_id,
|
|
||||||
id=job_result_id
|
|
||||||
).first_or_404()
|
|
||||||
|
|
||||||
if not (
|
|
||||||
job_result.job.user == current_user
|
|
||||||
or current_user.is_administrator
|
|
||||||
):
|
|
||||||
abort(403)
|
|
||||||
|
|
||||||
return send_from_directory(
|
|
||||||
job_result.path.parent,
|
|
||||||
job_result.path.name,
|
|
||||||
as_attachment=True,
|
|
||||||
download_name=job_result.filename,
|
|
||||||
mimetype=job_result.mimetype
|
|
||||||
)
|
|
@ -1,37 +1,25 @@
|
|||||||
from flask import (
|
from flask import (
|
||||||
abort,
|
abort,
|
||||||
current_app,
|
|
||||||
Flask,
|
|
||||||
jsonify,
|
|
||||||
redirect,
|
redirect,
|
||||||
render_template,
|
render_template,
|
||||||
|
send_from_directory,
|
||||||
url_for
|
url_for
|
||||||
)
|
)
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user
|
||||||
from threading import Thread
|
from app.models import Job, JobInput, JobResult
|
||||||
from app import db
|
|
||||||
from app.decorators import admin_required
|
|
||||||
from app.models import Job, JobStatus
|
|
||||||
from . import bp
|
from . import bp
|
||||||
|
|
||||||
|
|
||||||
@bp.route('')
|
@bp.route('')
|
||||||
@login_required
|
def jobs():
|
||||||
def index():
|
|
||||||
return redirect(url_for('main.dashboard', _anchor='jobs'))
|
return redirect(url_for('main.dashboard', _anchor='jobs'))
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:job_id>')
|
@bp.route('/<hashid:job_id>')
|
||||||
@login_required
|
def job(job_id):
|
||||||
def job(job_id: int):
|
|
||||||
job = Job.query.get_or_404(job_id)
|
job = Job.query.get_or_404(job_id)
|
||||||
|
if not (job.user == current_user or current_user.is_administrator):
|
||||||
if not (
|
|
||||||
job.user == current_user
|
|
||||||
or current_user.is_administrator
|
|
||||||
):
|
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'jobs/job.html.j2',
|
'jobs/job.html.j2',
|
||||||
title='Job',
|
title='Job',
|
||||||
@ -39,73 +27,29 @@ def job(job_id: int):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _delete_job(app: Flask, job_id: int):
|
@bp.route('/<hashid:job_id>/inputs/<hashid:job_input_id>/download')
|
||||||
with app.app_context():
|
def download_job_input(job_id, job_input_id):
|
||||||
job = Job.query.get(job_id)
|
job_input = JobInput.query.filter_by(job_id=job_id, id=job_input_id).first_or_404()
|
||||||
job.delete()
|
if not (job_input.job.user == current_user or current_user.is_administrator):
|
||||||
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)
|
|
||||||
|
|
||||||
if not (
|
|
||||||
job.user == current_user
|
|
||||||
or current_user.is_administrator
|
|
||||||
):
|
|
||||||
abort(403)
|
abort(403)
|
||||||
|
return send_from_directory(
|
||||||
thread = Thread(
|
job_input.path.parent,
|
||||||
target=_delete_job,
|
job_input.path.name,
|
||||||
args=(current_app._get_current_object(), job.id)
|
as_attachment=True,
|
||||||
|
download_name=job_input.filename,
|
||||||
|
mimetype=job_input.mimetype
|
||||||
)
|
)
|
||||||
thread.start()
|
|
||||||
|
|
||||||
return jsonify(f'Job "{job.title}" marked for deletion.'), 202
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:job_id>/log')
|
@bp.route('/<hashid:job_id>/results/<hashid:job_result_id>/download')
|
||||||
@admin_required
|
def download_job_result(job_id, job_result_id):
|
||||||
def job_log(job_id: int):
|
job_result = JobResult.query.filter_by(job_id=job_id, id=job_result_id).first_or_404()
|
||||||
job = Job.query.get_or_404(job_id)
|
if not (job_result.job.user == current_user or current_user.is_administrator):
|
||||||
|
|
||||||
if job.status not in [JobStatus.COMPLETED, JobStatus.FAILED]:
|
|
||||||
abort(409)
|
|
||||||
|
|
||||||
log_file_path = job.path / 'pipeline_data' / 'logs' / 'pyflow_log.txt'
|
|
||||||
with log_file_path.open() as log_file:
|
|
||||||
log = log_file.read()
|
|
||||||
|
|
||||||
return jsonify(log)
|
|
||||||
|
|
||||||
|
|
||||||
def _restart_job(app: Flask, job_id: int):
|
|
||||||
with app.app_context():
|
|
||||||
job = Job.query.get(job_id)
|
|
||||||
job.restart()
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:job_id>/restart', methods=['POST'])
|
|
||||||
@login_required
|
|
||||||
def restart_job(job_id: int):
|
|
||||||
job = Job.query.get_or_404(job_id)
|
|
||||||
|
|
||||||
if not (
|
|
||||||
job.user == current_user
|
|
||||||
or current_user.is_administrator
|
|
||||||
):
|
|
||||||
abort(403)
|
abort(403)
|
||||||
|
return send_from_directory(
|
||||||
if job.status != JobStatus.FAILED:
|
job_result.path.parent,
|
||||||
abort(409)
|
job_result.path.name,
|
||||||
|
as_attachment=True,
|
||||||
thread = Thread(
|
download_name=job_result.filename,
|
||||||
target=_restart_job,
|
mimetype=job_result.mimetype
|
||||||
args=(current_app._get_current_object(), job.id)
|
|
||||||
)
|
)
|
||||||
thread.start()
|
|
||||||
|
|
||||||
return jsonify(f'Job "{job.title}" marked for restarting.'), 202
|
|
||||||
|
@ -1,7 +1,18 @@
|
|||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
|
from flask_login import login_required
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint('settings', __name__)
|
bp = Blueprint('settings', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.before_request
|
||||||
|
@login_required
|
||||||
|
def before_request():
|
||||||
|
'''
|
||||||
|
Ensures that the routes in this package can only be visited by users that
|
||||||
|
are logged in.
|
||||||
|
'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
from . import routes
|
from . import routes
|
||||||
|
@ -1,158 +1,10 @@
|
|||||||
from flask import (
|
from flask import g, url_for
|
||||||
abort,
|
from flask_login import current_user
|
||||||
flash,
|
from app.blueprints.users.settings.routes import settings as settings_route
|
||||||
jsonify,
|
|
||||||
redirect,
|
|
||||||
render_template,
|
|
||||||
request,
|
|
||||||
url_for
|
|
||||||
)
|
|
||||||
from flask_login import current_user, login_required
|
|
||||||
from app import db
|
|
||||||
from app.models import Avatar
|
|
||||||
from . import bp
|
from . import bp
|
||||||
from .forms import (
|
|
||||||
UpdateAvatarForm,
|
|
||||||
UpdatePasswordForm,
|
|
||||||
UpdateNotificationsForm,
|
|
||||||
UpdateAccountInformationForm,
|
|
||||||
UpdateProfileInformationForm
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('', methods=['GET', 'POST'])
|
@bp.route('/settings', methods=['GET', 'POST'])
|
||||||
@login_required
|
def settings():
|
||||||
def index():
|
g._nopaque_redirect_location_on_post = url_for('.settings')
|
||||||
update_account_information_form = UpdateAccountInformationForm(current_user)
|
return settings_route(current_user.id)
|
||||||
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,7 +1,18 @@
|
|||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
|
from flask_login import login_required
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint('users', __name__)
|
bp = Blueprint('users', __name__)
|
||||||
|
|
||||||
|
|
||||||
from . import cli, events, routes
|
@bp.before_request
|
||||||
|
@login_required
|
||||||
|
def before_request():
|
||||||
|
'''
|
||||||
|
Ensures that the routes in this package can only be visited by users that
|
||||||
|
are logged in.
|
||||||
|
'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
from . import cli, json_routes, routes, settings
|
||||||
|
@ -1,58 +0,0 @@
|
|||||||
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 {'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.on('UNSUBSCRIBE User')
|
|
||||||
@socketio_login_required
|
|
||||||
def unsubscribe(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'}
|
|
69
app/blueprints/users/json_routes.py
Normal file
69
app/blueprints/users/json_routes.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
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,48 +1,25 @@
|
|||||||
from flask import (
|
from flask import (
|
||||||
abort,
|
abort,
|
||||||
current_app,
|
|
||||||
Flask,
|
|
||||||
jsonify,
|
|
||||||
redirect,
|
redirect,
|
||||||
render_template,
|
render_template,
|
||||||
request,
|
|
||||||
send_from_directory,
|
send_from_directory,
|
||||||
url_for
|
url_for
|
||||||
)
|
)
|
||||||
from flask_login import current_user, login_required, logout_user
|
from flask_login import current_user
|
||||||
from threading import Thread
|
from app.models import User
|
||||||
from app import db
|
|
||||||
from app.models import Avatar, User
|
|
||||||
from . import bp
|
from . import bp
|
||||||
|
|
||||||
|
|
||||||
@bp.route('')
|
@bp.route('')
|
||||||
@login_required
|
def users():
|
||||||
def index():
|
|
||||||
return redirect(url_for('main.social_area', _anchor='users'))
|
return redirect(url_for('main.social_area', _anchor='users'))
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:user_id>')
|
@bp.route('/<hashid:user_id>')
|
||||||
@login_required
|
def user(user_id):
|
||||||
def user(user_id: int):
|
|
||||||
user = User.query.get_or_404(user_id)
|
user = User.query.get_or_404(user_id)
|
||||||
|
if not (user.is_public or user == current_user or current_user.is_administrator):
|
||||||
if not (
|
|
||||||
user.is_public
|
|
||||||
or user == current_user
|
|
||||||
or current_user.is_administrator
|
|
||||||
):
|
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
accept_json = request.accept_mimetypes.accept_json
|
|
||||||
accept_html = request.accept_mimetypes.accept_html
|
|
||||||
|
|
||||||
if accept_json and not accept_html:
|
|
||||||
return user.to_json_serializeable(
|
|
||||||
backrefs=True,
|
|
||||||
relationships=True
|
|
||||||
)
|
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'users/user.html.j2',
|
'users/user.html.j2',
|
||||||
title=user.username,
|
title=user.username,
|
||||||
@ -50,51 +27,13 @@ def user(user_id: int):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
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 == 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')
|
@bp.route('/<hashid:user_id>/avatar')
|
||||||
@login_required
|
def user_avatar(user_id):
|
||||||
def user_avatar(user_id: int):
|
|
||||||
user = User.query.get_or_404(user_id)
|
user = User.query.get_or_404(user_id)
|
||||||
|
if not (user.is_public or user == current_user or current_user.is_administrator):
|
||||||
if not (
|
|
||||||
user.is_public
|
|
||||||
or user == current_user
|
|
||||||
or current_user.is_administrator
|
|
||||||
):
|
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
if user.avatar is None:
|
if user.avatar is None:
|
||||||
return redirect(url_for('static', filename='images/user_avatar.png'))
|
return redirect(url_for('static', filename='images/user_avatar.png'))
|
||||||
|
|
||||||
return send_from_directory(
|
return send_from_directory(
|
||||||
user.avatar.path.parent,
|
user.avatar.path.parent,
|
||||||
user.avatar.path.name,
|
user.avatar.path.name,
|
||||||
@ -102,49 +41,3 @@ def user_avatar(user_id: int):
|
|||||||
download_name=user.avatar.filename,
|
download_name=user.avatar.filename,
|
||||||
mimetype=user.avatar.mimetype
|
mimetype=user.avatar.mimetype
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _delete_avatar(app: Flask, avatar_id: int):
|
|
||||||
with app.app_context():
|
|
||||||
avatar = Avatar.query.get(avatar_id)
|
|
||||||
avatar.delete()
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:user_id>/avatar', methods=['DELETE'])
|
|
||||||
@login_required
|
|
||||||
def delete_user_avatar(user_id: int):
|
|
||||||
user = User.query.get_or_404(user_id)
|
|
||||||
|
|
||||||
if user.avatar is None:
|
|
||||||
abort(409)
|
|
||||||
|
|
||||||
if not (
|
|
||||||
user == current_user
|
|
||||||
or current_user.is_administrator
|
|
||||||
):
|
|
||||||
abort(403)
|
|
||||||
|
|
||||||
thread = Thread(
|
|
||||||
target=_delete_avatar,
|
|
||||||
args=(current_app._get_current_object(), user.avatar.id)
|
|
||||||
)
|
|
||||||
thread.start()
|
|
||||||
|
|
||||||
return jsonify('Avatar marked for deletion'), 202
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: Move this to main blueprint(?)
|
|
||||||
@bp.route('/accept-terms-of-use', methods=['POST'])
|
|
||||||
@login_required
|
|
||||||
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()
|
|
||||||
|
|
||||||
return jsonify('You accepted the terms of use'), 202
|
|
||||||
|
2
app/blueprints/users/settings/__init__.py
Normal file
2
app/blueprints/users/settings/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from .. import bp
|
||||||
|
from . import json_routes, routes
|
@ -39,7 +39,7 @@ class UpdateAccountInformationForm(FlaskForm):
|
|||||||
)
|
)
|
||||||
submit = SubmitField()
|
submit = SubmitField()
|
||||||
|
|
||||||
def __init__(self, user: User, *args, **kwargs):
|
def __init__(self, user, *args, **kwargs):
|
||||||
if 'data' not in kwargs:
|
if 'data' not in kwargs:
|
||||||
kwargs['data'] = user.to_json_serializeable()
|
kwargs['data'] = user.to_json_serializeable()
|
||||||
if 'prefix' not in kwargs:
|
if 'prefix' not in kwargs:
|
||||||
@ -89,7 +89,7 @@ class UpdateProfileInformationForm(FlaskForm):
|
|||||||
)
|
)
|
||||||
submit = SubmitField()
|
submit = SubmitField()
|
||||||
|
|
||||||
def __init__(self, user: User, *args, **kwargs):
|
def __init__(self, user, *args, **kwargs):
|
||||||
if 'data' not in kwargs:
|
if 'data' not in kwargs:
|
||||||
kwargs['data'] = user.to_json_serializeable()
|
kwargs['data'] = user.to_json_serializeable()
|
||||||
if 'prefix' not in kwargs:
|
if 'prefix' not in kwargs:
|
||||||
@ -130,7 +130,7 @@ class UpdatePasswordForm(FlaskForm):
|
|||||||
)
|
)
|
||||||
submit = SubmitField()
|
submit = SubmitField()
|
||||||
|
|
||||||
def __init__(self, user: User, *args, **kwargs):
|
def __init__(self, user, *args, **kwargs):
|
||||||
if 'prefix' not in kwargs:
|
if 'prefix' not in kwargs:
|
||||||
kwargs['prefix'] = 'update-password-form'
|
kwargs['prefix'] = 'update-password-form'
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@ -152,7 +152,7 @@ class UpdateNotificationsForm(FlaskForm):
|
|||||||
)
|
)
|
||||||
submit = SubmitField()
|
submit = SubmitField()
|
||||||
|
|
||||||
def __init__(self, user: User, *args, **kwargs):
|
def __init__(self, user, *args, **kwargs):
|
||||||
if 'data' not in kwargs:
|
if 'data' not in kwargs:
|
||||||
kwargs['data'] = user.to_json_serializeable()
|
kwargs['data'] = user.to_json_serializeable()
|
||||||
if 'prefix' not in kwargs:
|
if 'prefix' not in kwargs:
|
49
app/blueprints/users/settings/json_routes.py
Normal file
49
app/blueprints/users/settings/json_routes.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
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
|
93
app/blueprints/users/settings/routes.py
Normal file
93
app/blueprints/users/settings/routes.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
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
|
||||||
|
)
|
@ -1,20 +0,0 @@
|
|||||||
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
|
|
2
app/extensions/nopaque_sqlalchemy_extras/__init__.py
Normal file
2
app/extensions/nopaque_sqlalchemy_extras/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from .types import ContainerColumn
|
||||||
|
from .types import IntEnumColumn
|
@ -1,45 +1,14 @@
|
|||||||
from .anonymous_user import AnonymousUser
|
from .anonymous_user import *
|
||||||
from .avatar import Avatar
|
from .avatar import *
|
||||||
from .corpus_file import CorpusFile
|
from .corpus_file import *
|
||||||
from .corpus_follower_association import CorpusFollowerAssociation
|
from .corpus_follower_association import *
|
||||||
from .corpus_follower_role import CorpusFollowerPermission, CorpusFollowerRole
|
from .corpus_follower_role import *
|
||||||
from .corpus import CorpusStatus, Corpus
|
from .corpus import *
|
||||||
from .job_input import JobInput
|
from .job_input import *
|
||||||
from .job_result import JobResult
|
from .job_result import *
|
||||||
from .job import JobStatus, Job
|
from .job import *
|
||||||
from .role import Permission, Role
|
from .role import *
|
||||||
from .spacy_nlp_pipeline_model import SpaCyNLPPipelineModel
|
from .spacy_nlp_pipeline_model import *
|
||||||
from .tesseract_ocr_pipeline_model import TesseractOCRPipelineModel
|
from .tesseract_ocr_pipeline_model import *
|
||||||
from .token import Token
|
from .token import *
|
||||||
from .user import (
|
from .user import *
|
||||||
ProfilePrivacySettings,
|
|
||||||
UserSettingJobStatusMailNotificationLevel,
|
|
||||||
User
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
_models = [
|
|
||||||
Avatar,
|
|
||||||
CorpusFile,
|
|
||||||
CorpusFollowerAssociation,
|
|
||||||
CorpusFollowerRole,
|
|
||||||
Corpus,
|
|
||||||
JobInput,
|
|
||||||
JobResult,
|
|
||||||
Job,
|
|
||||||
Role,
|
|
||||||
SpaCyNLPPipelineModel,
|
|
||||||
TesseractOCRPipelineModel,
|
|
||||||
Token,
|
|
||||||
User
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
_enums = [
|
|
||||||
CorpusFollowerPermission,
|
|
||||||
CorpusStatus,
|
|
||||||
JobStatus,
|
|
||||||
Permission,
|
|
||||||
ProfilePrivacySettings,
|
|
||||||
UserSettingJobStatusMailNotificationLevel
|
|
||||||
]
|
|
||||||
|
@ -8,7 +8,7 @@ import shutil
|
|||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from app import db
|
from app import db
|
||||||
from app.converters.vrt import normalize_vrt_file
|
from app.converters.vrt import normalize_vrt_file
|
||||||
from app.extensions.nopaque_sqlalchemy_type_decorators import IntEnumColumn
|
from app.extensions.nopaque_sqlalchemy_extras import IntEnumColumn
|
||||||
from .corpus_follower_association import CorpusFollowerAssociation
|
from .corpus_follower_association import CorpusFollowerAssociation
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,8 +42,9 @@ def resource_after_delete(mapper, connection, resource):
|
|||||||
'path': resource.jsonpatch_path
|
'path': resource.jsonpatch_path
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
namespace = '/users'
|
||||||
room = f'/users/{resource.user_hashid}'
|
room = f'/users/{resource.user_hashid}'
|
||||||
socketio.emit('PATCH', jsonpatch, room=room)
|
socketio.emit('patch', jsonpatch, namespace=namespace, room=room)
|
||||||
|
|
||||||
|
|
||||||
def cfa_after_delete(mapper, connection, cfa):
|
def cfa_after_delete(mapper, connection, cfa):
|
||||||
@ -54,8 +55,9 @@ def cfa_after_delete(mapper, connection, cfa):
|
|||||||
'path': jsonpatch_path
|
'path': jsonpatch_path
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
namespace = '/users'
|
||||||
room = f'/users/{cfa.corpus.user.hashid}'
|
room = f'/users/{cfa.corpus.user.hashid}'
|
||||||
socketio.emit('PATCH', jsonpatch, room=room)
|
socketio.emit('patch', jsonpatch, namespace=namespace, room=room)
|
||||||
|
|
||||||
|
|
||||||
def resource_after_insert(mapper, connection, resource):
|
def resource_after_insert(mapper, connection, resource):
|
||||||
@ -69,8 +71,9 @@ def resource_after_insert(mapper, connection, resource):
|
|||||||
'value': jsonpatch_value
|
'value': jsonpatch_value
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
namespace = '/users'
|
||||||
room = f'/users/{resource.user_hashid}'
|
room = f'/users/{resource.user_hashid}'
|
||||||
socketio.emit('PATCH', jsonpatch, room=room)
|
socketio.emit('patch', jsonpatch, namespace=namespace, room=room)
|
||||||
|
|
||||||
|
|
||||||
def cfa_after_insert(mapper, connection, cfa):
|
def cfa_after_insert(mapper, connection, cfa):
|
||||||
@ -83,8 +86,9 @@ def cfa_after_insert(mapper, connection, cfa):
|
|||||||
'value': jsonpatch_value
|
'value': jsonpatch_value
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
namespace = '/users'
|
||||||
room = f'/users/{cfa.corpus.user.hashid}'
|
room = f'/users/{cfa.corpus.user.hashid}'
|
||||||
socketio.emit('PATCH', jsonpatch, room=room)
|
socketio.emit('patch', jsonpatch, namespace=namespace, room=room)
|
||||||
|
|
||||||
|
|
||||||
def resource_after_update(mapper, connection, resource):
|
def resource_after_update(mapper, connection, resource):
|
||||||
@ -109,8 +113,9 @@ def resource_after_update(mapper, connection, resource):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
if jsonpatch:
|
if jsonpatch:
|
||||||
|
namespace = '/users'
|
||||||
room = f'/users/{resource.user_hashid}'
|
room = f'/users/{resource.user_hashid}'
|
||||||
socketio.emit('PATCH', jsonpatch, room=room)
|
socketio.emit('patch', jsonpatch, namespace=namespace, room=room)
|
||||||
|
|
||||||
|
|
||||||
def job_after_update(mapper, connection, job):
|
def job_after_update(mapper, connection, job):
|
||||||
|
@ -6,7 +6,7 @@ from time import sleep
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
from app import db
|
from app import db
|
||||||
from app.extensions.nopaque_sqlalchemy_type_decorators import ContainerColumn, IntEnumColumn
|
from app.extensions.nopaque_sqlalchemy_extras import ContainerColumn, IntEnumColumn
|
||||||
|
|
||||||
|
|
||||||
class JobStatus(IntEnum):
|
class JobStatus(IntEnum):
|
||||||
|
@ -20,6 +20,14 @@ class JobInput(FileMixin, HashidMixin, db.Model):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<JobInput {self.filename}>'
|
return f'<JobInput {self.filename}>'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def content_url(self):
|
||||||
|
return url_for(
|
||||||
|
'jobs.download_job_input',
|
||||||
|
job_id=self.job.id,
|
||||||
|
job_input_id=self.id
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def jsonpatch_path(self):
|
def jsonpatch_path(self):
|
||||||
return f'{self.job.jsonpatch_path}/inputs/{self.hashid}'
|
return f'{self.job.jsonpatch_path}/inputs/{self.hashid}'
|
||||||
@ -32,7 +40,7 @@ class JobInput(FileMixin, HashidMixin, db.Model):
|
|||||||
def url(self):
|
def url(self):
|
||||||
return url_for(
|
return url_for(
|
||||||
'jobs.job',
|
'jobs.job',
|
||||||
job_input_id=self.id,
|
job_id=self.job_id,
|
||||||
_anchor=f'job-{self.job.hashid}-input-{self.hashid}'
|
_anchor=f'job-{self.job.hashid}-input-{self.hashid}'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -22,6 +22,14 @@ class JobResult(FileMixin, HashidMixin, db.Model):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<JobResult {self.filename}>'
|
return f'<JobResult {self.filename}>'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def download_url(self):
|
||||||
|
return url_for(
|
||||||
|
'jobs.download_job_result',
|
||||||
|
job_id=self.job_id,
|
||||||
|
job_result_id=self.id
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def jsonpatch_path(self):
|
def jsonpatch_path(self):
|
||||||
return f'{self.job.jsonpatch_path}/results/{self.hashid}'
|
return f'{self.job.jsonpatch_path}/results/{self.hashid}'
|
||||||
@ -33,8 +41,8 @@ class JobResult(FileMixin, HashidMixin, db.Model):
|
|||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self):
|
||||||
return url_for(
|
return url_for(
|
||||||
'job_results.job_result',
|
'jobs.job',
|
||||||
job_result_id=self.id,
|
job_id=self.job_id,
|
||||||
_anchor=f'job-{self.job.hashid}-result-{self.hashid}'
|
_anchor=f'job-{self.job.hashid}-result-{self.hashid}'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ from pathlib import Path
|
|||||||
import requests
|
import requests
|
||||||
import yaml
|
import yaml
|
||||||
from app import db
|
from app import db
|
||||||
from app.extensions.nopaque_sqlalchemy_type_decorators import ContainerColumn
|
from app.extensions.nopaque_sqlalchemy_extras import ContainerColumn
|
||||||
from .file_mixin import FileMixin
|
from .file_mixin import FileMixin
|
||||||
from .user import User
|
from .user import User
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ from pathlib import Path
|
|||||||
import requests
|
import requests
|
||||||
import yaml
|
import yaml
|
||||||
from app import db
|
from app import db
|
||||||
from app.extensions.nopaque_sqlalchemy_type_decorators import ContainerColumn
|
from app.extensions.nopaque_sqlalchemy_extras import ContainerColumn
|
||||||
from .file_mixin import FileMixin
|
from .file_mixin import FileMixin
|
||||||
from .user import User
|
from .user import User
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ import re
|
|||||||
import secrets
|
import secrets
|
||||||
import shutil
|
import shutil
|
||||||
from app import db, hashids
|
from app import db, hashids
|
||||||
from app.extensions.nopaque_sqlalchemy_type_decorators import IntEnumColumn
|
from app.extensions.nopaque_sqlalchemy_extras import IntEnumColumn
|
||||||
from .corpus import Corpus
|
from .corpus import Corpus
|
||||||
from .corpus_follower_association import CorpusFollowerAssociation
|
from .corpus_follower_association import CorpusFollowerAssociation
|
||||||
from .corpus_follower_role import CorpusFollowerRole
|
from .corpus_follower_role import CorpusFollowerRole
|
||||||
|
@ -169,7 +169,6 @@ class CQiOverSocketIONamespace(Namespace):
|
|||||||
|
|
||||||
for param in signature(fn).parameters.values():
|
for param in signature(fn).parameters.values():
|
||||||
# Check if the parameter is optional or required
|
# Check if the parameter is optional or required
|
||||||
# The following is true for required parameters
|
|
||||||
if param.default is param.empty:
|
if param.default is param.empty:
|
||||||
if param.name not in fn_args:
|
if param.name not in fn_args:
|
||||||
return {'code': 400, 'msg': 'Bad Request'}
|
return {'code': 400, 'msg': 'Bad Request'}
|
||||||
|
109
app/namespaces/jobs.py
Normal file
109
app/namespaces/jobs.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
from flask import current_app, Flask
|
||||||
|
from flask_login import current_user
|
||||||
|
from flask_socketio import Namespace
|
||||||
|
from app import db, hashids, socketio
|
||||||
|
from app.decorators import socketio_admin_required, socketio_login_required
|
||||||
|
from app.models import Job, JobStatus
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_job(app: Flask, job_id: int):
|
||||||
|
with app.app_context():
|
||||||
|
job = Job.query.get(job_id)
|
||||||
|
job.delete()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def _restart_job(app, job_id):
|
||||||
|
with app.app_context():
|
||||||
|
job = Job.query.get(job_id)
|
||||||
|
job.restart()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
class UsersNamespace(Namespace):
|
||||||
|
@socketio_login_required
|
||||||
|
def on_delete(self, job_hashid: str) -> dict:
|
||||||
|
job_id = hashids.decode(job_hashid)
|
||||||
|
|
||||||
|
if not isinstance(job_id, int):
|
||||||
|
return {'status': 400, 'statusText': 'Bad Request'}
|
||||||
|
|
||||||
|
job = Job.query.get(job_id)
|
||||||
|
|
||||||
|
if job is None:
|
||||||
|
return {'status': 404, 'statusText': 'Not found'}
|
||||||
|
|
||||||
|
if not (
|
||||||
|
job.user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
return {'status': 403, 'statusText': 'Forbidden'}
|
||||||
|
|
||||||
|
socketio.start_background_task(
|
||||||
|
_delete_job,
|
||||||
|
current_app._get_current_object(),
|
||||||
|
job_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'body': f'Job "{job.title}" marked for deletion',
|
||||||
|
'status': 202,
|
||||||
|
'statusText': 'Accepted'
|
||||||
|
}
|
||||||
|
|
||||||
|
@socketio_admin_required
|
||||||
|
def on_log(self, job_hashid: str):
|
||||||
|
job_id = hashids.decode(job_hashid)
|
||||||
|
|
||||||
|
if not isinstance(job_id, int):
|
||||||
|
return {'status': 400, 'statusText': 'Bad Request'}
|
||||||
|
|
||||||
|
job = Job.query.get(job_id)
|
||||||
|
|
||||||
|
if job is None:
|
||||||
|
return {'status': 404, 'statusText': 'Not found'}
|
||||||
|
|
||||||
|
if job.status not in [JobStatus.COMPLETED, JobStatus.FAILED]:
|
||||||
|
return {'status': 409, 'statusText': 'Conflict'}
|
||||||
|
|
||||||
|
with open(job.path / 'pipeline_data' / 'logs' / 'pyflow_log.txt') as log_file:
|
||||||
|
log = log_file.read()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'body': log,
|
||||||
|
'status': 200,
|
||||||
|
'statusText': 'Forbidden'
|
||||||
|
}
|
||||||
|
|
||||||
|
socketio_login_required
|
||||||
|
def on_restart(self, job_hashid: str):
|
||||||
|
job_id = hashids.decode(job_hashid)
|
||||||
|
|
||||||
|
if not isinstance(job_id, int):
|
||||||
|
return {'status': 400, 'statusText': 'Bad Request'}
|
||||||
|
|
||||||
|
job = Job.query.get(job_id)
|
||||||
|
|
||||||
|
if job is None:
|
||||||
|
return {'status': 404, 'statusText': 'Not found'}
|
||||||
|
|
||||||
|
if not (
|
||||||
|
job.user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
return {'status': 403, 'statusText': 'Forbidden'}
|
||||||
|
|
||||||
|
if job.status == JobStatus.FAILED:
|
||||||
|
return {'status': 409, 'statusText': 'Conflict'}
|
||||||
|
|
||||||
|
socketio.start_background_task(
|
||||||
|
_restart_job,
|
||||||
|
current_app._get_current_object(),
|
||||||
|
job_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'body': f'Job "{job.title}" marked for restarting',
|
||||||
|
'status': 202,
|
||||||
|
'statusText': 'Accepted'
|
||||||
|
}
|
116
app/namespaces/users.py
Normal file
116
app/namespaces/users.py
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
from flask import current_app, Flask
|
||||||
|
from flask_login import current_user
|
||||||
|
from flask_socketio import join_room, leave_room, Namespace
|
||||||
|
from app import db, hashids, socketio
|
||||||
|
from app.decorators import socketio_login_required
|
||||||
|
from app.models import User
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_user(app: Flask, user_id: int):
|
||||||
|
with app.app_context():
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
user.delete()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
class UsersNamespace(Namespace):
|
||||||
|
@socketio_login_required
|
||||||
|
def on_get(self, user_hashid: str) -> dict:
|
||||||
|
user_id = hashids.decode(user_hashid)
|
||||||
|
|
||||||
|
if not isinstance(user_id, int):
|
||||||
|
return {'status': 400, 'statusText': 'Bad Request'}
|
||||||
|
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
return {'status': 404, 'statusText': 'Not found'}
|
||||||
|
|
||||||
|
if not (
|
||||||
|
user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
return {'status': 403, 'statusText': 'Forbidden'}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'body': user.to_json_serializeable(
|
||||||
|
backrefs=True,
|
||||||
|
relationships=True
|
||||||
|
),
|
||||||
|
'status': 200,
|
||||||
|
'statusText': 'OK'
|
||||||
|
}
|
||||||
|
|
||||||
|
@socketio_login_required
|
||||||
|
def on_subscribe(self, user_hashid: str) -> dict:
|
||||||
|
user_id = hashids.decode(user_hashid)
|
||||||
|
|
||||||
|
if not isinstance(user_id, int):
|
||||||
|
return {'status': 400, 'statusText': 'Bad Request'}
|
||||||
|
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
return {'status': 404, 'statusText': 'Not found'}
|
||||||
|
|
||||||
|
if not (
|
||||||
|
user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
return {'status': 403, 'statusText': 'Forbidden'}
|
||||||
|
|
||||||
|
join_room(f'/users/{user.hashid}')
|
||||||
|
|
||||||
|
return {'status': 200, 'statusText': 'OK'}
|
||||||
|
|
||||||
|
@socketio_login_required
|
||||||
|
def on_unsubscribe(self, user_hashid: str) -> dict:
|
||||||
|
user_id = hashids.decode(user_hashid)
|
||||||
|
|
||||||
|
if not isinstance(user_id, int):
|
||||||
|
return {'status': 400, 'statusText': 'Bad Request'}
|
||||||
|
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
return {'status': 404, 'statusText': 'Not found'}
|
||||||
|
|
||||||
|
if not (
|
||||||
|
user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
return {'status': 403, 'statusText': 'Forbidden'}
|
||||||
|
|
||||||
|
leave_room(f'/users/{user.hashid}')
|
||||||
|
|
||||||
|
return {'status': 200, 'statusText': 'OK'}
|
||||||
|
|
||||||
|
@socketio_login_required
|
||||||
|
def on_delete(self, user_hashid: str) -> dict:
|
||||||
|
user_id = hashids.decode(user_hashid)
|
||||||
|
|
||||||
|
if not isinstance(user_id, int):
|
||||||
|
return {'status': 400, 'statusText': 'Bad Request'}
|
||||||
|
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
return {'status': 404, 'statusText': 'Not found'}
|
||||||
|
|
||||||
|
if not (
|
||||||
|
user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
return {'status': 403, 'statusText': 'Forbidden'}
|
||||||
|
|
||||||
|
socketio.start_background_task(
|
||||||
|
_delete_user,
|
||||||
|
current_app._get_current_object(),
|
||||||
|
user.id
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'body': f'User "{user.username}" marked for deletion',
|
||||||
|
'status': 202,
|
||||||
|
'statusText': 'Accepted'
|
||||||
|
}
|
@ -1,11 +1,8 @@
|
|||||||
nopaque.app.Client = class Client {
|
nopaque.App = class App {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.socket = io({transports: ['websocket'], upgrade: false});
|
this.socket = io({transports: ['websocket'], upgrade: false});
|
||||||
|
|
||||||
// Endpoints
|
// Endpoints
|
||||||
this.corpora = new nopaque.app.endpoints.Corpora(this);
|
|
||||||
this.jobs = new nopaque.app.endpoints.Jobs(this);
|
|
||||||
this.settings = new nopaque.app.endpoints.Settings(this);
|
|
||||||
this.users = new nopaque.app.endpoints.Users(this);
|
this.users = new nopaque.app.endpoints.Users(this);
|
||||||
|
|
||||||
// Extensions
|
// Extensions
|
@ -1,93 +0,0 @@
|
|||||||
nopaque.app.endpoints.Corpora = class Corpora {
|
|
||||||
constructor(app) {
|
|
||||||
this.app = app;
|
|
||||||
|
|
||||||
this.socket = io('/corpora', {transports: ['websocket'], upgrade: false});
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(corpusId) {
|
|
||||||
const options = {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json'
|
|
||||||
},
|
|
||||||
method: 'DELETE'
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetch(`/corpora/${corpusId}`, options);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async build(corpusId) {
|
|
||||||
const options = {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json'
|
|
||||||
},
|
|
||||||
method: 'POST'
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetch(`/corpora/${corpusId}/build`, options);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getStopwords(corpusId) {
|
|
||||||
const options = {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetch(`/corpora/${corpusId}/analysis/stopwords`, options);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async createShareLink(corpusId, expirationDate, roleName) {
|
|
||||||
const options = {
|
|
||||||
body: JSON.stringify({
|
|
||||||
'expiration_date': expirationDate,
|
|
||||||
'role_name': roleName
|
|
||||||
}),
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
method: 'POST'
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetch(`/corpora/${corpusId}/create-share-link`, options);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateIsPublic(corpusId, newValue) {
|
|
||||||
const options = {
|
|
||||||
body: JSON.stringify(newValue),
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
method: 'PUT',
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetch(`/corpora/${corpusId}/is-public`, options);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
nopaque.app.endpoints.Jobs = class Jobs {
|
|
||||||
constructor(app) {
|
|
||||||
this.app = app;
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(jobId) {
|
|
||||||
const options = {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json'
|
|
||||||
},
|
|
||||||
method: 'DELETE'
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetch(`/jobs/${jobId}`, options);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async log(jobId) {
|
|
||||||
const options = {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetch(`/jobs/${jobId}/log`, options);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async restart(jobId) {
|
|
||||||
const options = {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json'
|
|
||||||
},
|
|
||||||
method: 'POST'
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetch(`/jobs/${jobId}/restart`, options);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,77 +0,0 @@
|
|||||||
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,25 +1,22 @@
|
|||||||
nopaque.app.endpoints.Users = class Users {
|
nopaque.app.endpoints.Users = class Users {
|
||||||
constructor(app) {
|
constructor(app) {
|
||||||
this.app = app;
|
this.app = app;
|
||||||
|
|
||||||
|
this.socket = io('/users', {transports: ['websocket'], upgrade: false});
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(userId) {
|
async get(userId) {
|
||||||
const options = {
|
const response = await this.socket.emitWithAck('get', userId);
|
||||||
headers: {
|
|
||||||
Accept: 'application/json'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetch(`/users/${userId}`, options);
|
if (response.status !== 200) {
|
||||||
const data = await response.json();
|
throw new Error(`[${response.status}] ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
return response.body;
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async subscribe(userId) {
|
async subscribe(userId) {
|
||||||
const response = await this.app.socket.emitWithAck('SUBSCRIBE User', userId);
|
const response = await this.socket.emitWithAck('subscribe', userId);
|
||||||
|
|
||||||
if (response.status != 200) {
|
if (response.status != 200) {
|
||||||
throw new Error(`[${response.status}] ${response.statusText}`);
|
throw new Error(`[${response.status}] ${response.statusText}`);
|
||||||
@ -27,7 +24,7 @@ nopaque.app.endpoints.Users = class Users {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async unsubscribe(userId) {
|
async unsubscribe(userId) {
|
||||||
const response = await this.app.socket.emitWithAck('UNSUBSCRIBE User', userId);
|
const response = await this.socket.emitWithAck('unsubscribe', userId);
|
||||||
|
|
||||||
if (response.status != 200) {
|
if (response.status != 200) {
|
||||||
throw new Error(`[${response.status}] ${response.statusText}`);
|
throw new Error(`[${response.status}] ${response.statusText}`);
|
||||||
@ -35,18 +32,10 @@ nopaque.app.endpoints.Users = class Users {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async delete(userId) {
|
async delete(userId) {
|
||||||
const options = {
|
const response = await this.socket.emitWithAck('delete', userId);
|
||||||
headers: {
|
|
||||||
Accept: 'application/json'
|
|
||||||
},
|
|
||||||
method: 'DELETE'
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetch(`/users/${userId}`, options);
|
if (response.status != 202) {
|
||||||
const data = await response.json();
|
throw new Error(`[${response.status}] ${response.statusText}`);
|
||||||
|
}
|
||||||
if (!response.ok) {throw new Error(`${data.name}: ${data.description}`);}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ nopaque.app.extensions.UserHub = class UserHub extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.app.socket.on('PATCH', (patch) => {this.#onPatch(patch)});
|
this.app.users.socket.on('patch', (patch) => {this.#onPatch(patch)});
|
||||||
}
|
}
|
||||||
|
|
||||||
add(userId) {
|
add(userId) {
|
||||||
|
@ -7,6 +7,7 @@ nopaque.corpus_analysis.StaticVisualizationExtension = class StaticVisualization
|
|||||||
stopwords: undefined,
|
stopwords: undefined,
|
||||||
originalStopwords: {},
|
originalStopwords: {},
|
||||||
stopwordCache: {},
|
stopwordCache: {},
|
||||||
|
promises: {getStopwords: undefined},
|
||||||
tokenSet: new Set()
|
tokenSet: new Set()
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -72,11 +73,22 @@ nopaque.corpus_analysis.StaticVisualizationExtension = class StaticVisualization
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStopwords() {
|
getStopwords() {
|
||||||
const stopwords = await app.corpora.getStopwords(this.app.corpusId);
|
this.data.promises.getStopwords = new Promise((resolve, reject) => {
|
||||||
this.data.originalStopwords = structuredClone(stopwords);
|
nopaque.requests.corpora.entity.getStopwords()
|
||||||
this.data.stopwords = structuredClone(stopwords);
|
.then((response) => {
|
||||||
return stopwords;
|
response.json()
|
||||||
|
.then((json) => {
|
||||||
|
this.data.originalStopwords = structuredClone(json);
|
||||||
|
this.data.stopwords = structuredClone(json);
|
||||||
|
resolve(this.data.stopwords);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return this.data.promises.getStopwords;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderGeneralCorpusInfo() {
|
renderGeneralCorpusInfo() {
|
||||||
|
@ -5,6 +5,50 @@ nopaque.requests.corpora = {};
|
|||||||
|
|
||||||
nopaque.requests.corpora.entity = {};
|
nopaque.requests.corpora.entity = {};
|
||||||
|
|
||||||
|
nopaque.requests.corpora.entity.delete = (corpusId) => {
|
||||||
|
let input = `/corpora/${corpusId}`;
|
||||||
|
let init = {
|
||||||
|
method: 'DELETE'
|
||||||
|
};
|
||||||
|
return nopaque.requests.JSONfetch(input, init);
|
||||||
|
};
|
||||||
|
|
||||||
|
nopaque.requests.corpora.entity.build = (corpusId) => {
|
||||||
|
let input = `/corpora/${corpusId}/build`;
|
||||||
|
let init = {
|
||||||
|
method: 'POST',
|
||||||
|
};
|
||||||
|
return nopaque.requests.JSONfetch(input, init);
|
||||||
|
};
|
||||||
|
|
||||||
|
nopaque.requests.corpora.entity.generateShareLink = (corpusId, role, expiration) => {
|
||||||
|
let input = `/corpora/${corpusId}/generate-share-link`;
|
||||||
|
let init = {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({role: role, expiration: expiration})
|
||||||
|
};
|
||||||
|
return nopaque.requests.JSONfetch(input, init);
|
||||||
|
};
|
||||||
|
|
||||||
|
nopaque.requests.corpora.entity.getStopwords = () => {
|
||||||
|
let input = `/corpora/stopwords`;
|
||||||
|
let init = {
|
||||||
|
method: 'GET'
|
||||||
|
};
|
||||||
|
return nopaque.requests.JSONfetch(input, init);
|
||||||
|
};
|
||||||
|
|
||||||
|
nopaque.requests.corpora.entity.isPublic = {};
|
||||||
|
|
||||||
|
nopaque.requests.corpora.entity.isPublic.update = (corpusId, isPublic) => {
|
||||||
|
let input = `/corpora/${corpusId}/is_public`;
|
||||||
|
let init = {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(isPublic)
|
||||||
|
};
|
||||||
|
return nopaque.requests.JSONfetch(input, init);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/*****************************************************************************
|
/*****************************************************************************
|
||||||
* Requests for /corpora/<entity>/files routes *
|
* Requests for /corpora/<entity>/files routes *
|
||||||
|
30
app/static/js/requests/jobs.js
Normal file
30
app/static/js/requests/jobs.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Requests for /jobs routes *
|
||||||
|
*****************************************************************************/
|
||||||
|
nopaque.requests.jobs = {};
|
||||||
|
|
||||||
|
nopaque.requests.jobs.entity = {};
|
||||||
|
|
||||||
|
nopaque.requests.jobs.entity.delete = (jobId) => {
|
||||||
|
let input = `/jobs/${jobId}`;
|
||||||
|
let init = {
|
||||||
|
method: 'DELETE'
|
||||||
|
};
|
||||||
|
return nopaque.requests.JSONfetch(input, init);
|
||||||
|
};
|
||||||
|
|
||||||
|
nopaque.requests.jobs.entity.log = (jobId) => {
|
||||||
|
let input = `/jobs/${jobId}/log`;
|
||||||
|
let init = {
|
||||||
|
method: 'GET'
|
||||||
|
};
|
||||||
|
return nopaque.requests.JSONfetch(input, init);
|
||||||
|
};
|
||||||
|
|
||||||
|
nopaque.requests.jobs.entity.restart = (jobId) => {
|
||||||
|
let input = `/jobs/${jobId}/restart`;
|
||||||
|
let init = {
|
||||||
|
method: 'POST'
|
||||||
|
};
|
||||||
|
return nopaque.requests.JSONfetch(input, init);
|
||||||
|
};
|
@ -7,7 +7,7 @@ nopaque.resource_displays.CorpusDisplay = class CorpusDisplay extends nopaque.re
|
|||||||
this.displayElement
|
this.displayElement
|
||||||
.querySelector('.action-button[data-action="build-request"]')
|
.querySelector('.action-button[data-action="build-request"]')
|
||||||
.addEventListener('click', (event) => {
|
.addEventListener('click', (event) => {
|
||||||
app.corpora.build(this.corpusId);
|
nopaque.requests.corpora.entity.build(this.corpusId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,7 +180,7 @@ nopaque.resource_lists.CorpusList = class CorpusList extends nopaque.resource_li
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
app.corpora.delete(itemId);
|
nopaque.requests.corpora.entity.delete(itemId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
modal.open();
|
modal.open();
|
||||||
@ -276,7 +276,7 @@ nopaque.resource_lists.CorpusList = class CorpusList extends nopaque.resource_li
|
|||||||
let listItem = this.listjs.get('id', selectedItemId)[0].elm;
|
let listItem = this.listjs.get('id', selectedItemId)[0].elm;
|
||||||
let values = this.listjs.get('id', listItem.dataset.id)[0].values();
|
let values = this.listjs.get('id', listItem.dataset.id)[0].values();
|
||||||
if (values['is-owner']) {
|
if (values['is-owner']) {
|
||||||
app.corpora.delete(selectedItemId);
|
nopaque.requests.corpora.entity.delete(selectedItemId);
|
||||||
} else {
|
} else {
|
||||||
nopaque.requests.corpora.entity.followers.entity.delete(selectedItemId, currentUserId);
|
nopaque.requests.corpora.entity.followers.entity.delete(selectedItemId, currentUserId);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -136,9 +136,8 @@ nopaque.resource_lists.JobList = class JobList extends nopaque.resource_lists.Re
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]');
|
let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]');
|
||||||
confirmElement.addEventListener('click', async (event) => {
|
confirmElement.addEventListener('click', (event) => {
|
||||||
const message = await app.jobs.delete(itemId);
|
nopaque.requests.jobs.entity.delete(itemId);
|
||||||
app.ui.flash(message, 'job');
|
|
||||||
});
|
});
|
||||||
modal.open();
|
modal.open();
|
||||||
break;
|
break;
|
||||||
@ -222,9 +221,8 @@ nopaque.resource_lists.JobList = class JobList extends nopaque.resource_lists.Re
|
|||||||
);
|
);
|
||||||
let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]');
|
let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]');
|
||||||
confirmElement.addEventListener('click', (event) => {
|
confirmElement.addEventListener('click', (event) => {
|
||||||
this.selectedItemIds.forEach(async (selectedItemId) => {
|
this.selectedItemIds.forEach(selectedItemId => {
|
||||||
const message = await app.jobs.delete(selectedItemId);
|
nopaque.requests.jobs.entity.delete(selectedItemId);
|
||||||
app.ui.flash(message, 'job');
|
|
||||||
});
|
});
|
||||||
this.selectedItemIds.clear();
|
this.selectedItemIds.clear();
|
||||||
this.renderingItemSelection();
|
this.renderingItemSelection();
|
||||||
|
@ -44,8 +44,8 @@
|
|||||||
Your profile
|
Your profile
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li {% if request.path == url_for('settings.index') %}class="active"{% endif %}>
|
<li {% if request.path == url_for('settings.settings') %}class="active"{% endif %}>
|
||||||
<a href="{{ url_for('settings.index') }}">
|
<a href="{{ url_for('settings.settings') }}">
|
||||||
<i class="material-icons">settings</i>
|
<i class="material-icons">settings</i>
|
||||||
Settings
|
Settings
|
||||||
</a>
|
</a>
|
||||||
@ -56,24 +56,5 @@
|
|||||||
Log out
|
Log out
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% if current_user.can('USE_API') or current_user.is_administrator %}
|
|
||||||
<li class="divider" tabindex="-1"></li>
|
|
||||||
{% endif %}
|
|
||||||
{% if current_user.can('USE_API') %}
|
|
||||||
<li>
|
|
||||||
<a href="{{ url_for('apifairy.docs') }}">
|
|
||||||
<i class="material-icons left">api</i>
|
|
||||||
API
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% if current_user.is_administrator %}
|
|
||||||
<li>
|
|
||||||
<a href="{{ url_for('admin.index') }}">
|
|
||||||
<i class="material-icons left">admin_panel_settings</i>
|
|
||||||
Administration
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -8,12 +8,9 @@
|
|||||||
filters='rjsmin',
|
filters='rjsmin',
|
||||||
output='gen/nopaque.%(version)s.js',
|
output='gen/nopaque.%(version)s.js',
|
||||||
'js/index.js',
|
'js/index.js',
|
||||||
|
'js/app.js',
|
||||||
'js/app/index.js',
|
'js/app/index.js',
|
||||||
'js/app/client.js',
|
|
||||||
'js/app/endpoints/index.js',
|
'js/app/endpoints/index.js',
|
||||||
'js/app/endpoints/corpora.js',
|
|
||||||
'js/app/endpoints/jobs.js',
|
|
||||||
'js/app/endpoints/settings.js',
|
|
||||||
'js/app/endpoints/users.js',
|
'js/app/endpoints/users.js',
|
||||||
'js/app/extensions/index.js',
|
'js/app/extensions/index.js',
|
||||||
'js/app/extensions/toaster.js',
|
'js/app/extensions/toaster.js',
|
||||||
@ -53,6 +50,7 @@
|
|||||||
'js/requests/admin.js',
|
'js/requests/admin.js',
|
||||||
'js/requests/contributions.js',
|
'js/requests/contributions.js',
|
||||||
'js/requests/corpora.js',
|
'js/requests/corpora.js',
|
||||||
|
'js/requests/jobs.js',
|
||||||
'js/requests/users.js',
|
'js/requests/users.js',
|
||||||
|
|
||||||
'js/corpus-analysis/index.js',
|
'js/corpus-analysis/index.js',
|
||||||
@ -81,9 +79,9 @@
|
|||||||
<script src="{{ ASSET_URL }}"></script>
|
<script src="{{ ASSET_URL }}"></script>
|
||||||
{% endassets -%}
|
{% endassets -%}
|
||||||
|
|
||||||
{# TODO: Think about implementing the following inside a main.js(.j2) #}
|
|
||||||
<script>
|
<script>
|
||||||
const app = new nopaque.app.Client();
|
// TODO: Implement an app.run method and use this for all of the following
|
||||||
|
const app = new nopaque.App();
|
||||||
app.init();
|
app.init();
|
||||||
|
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
|
@ -85,8 +85,8 @@
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
{# settings #}
|
{# settings #}
|
||||||
<li {% if request.path == url_for('settings.index') %}class="active"{% endif %}>
|
<li {% if request.path == url_for('settings.settings') %}class="active"{% endif %}>
|
||||||
<a class="waves-effect" href="{{ url_for('settings.index') }}"><i class="material-icons">settings</i>Settings</a>
|
<a class="waves-effect" href="{{ url_for('settings.settings') }}"><i class="material-icons">settings</i>Settings</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{# log out #}
|
{# log out #}
|
||||||
@ -105,21 +105,19 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if current_user.can('USE_API') or current_user.is_administrator %}
|
{% if current_user.is_authenticated and current_user.can('ADMINISTRATE') %}
|
||||||
|
{# administration section #}
|
||||||
<li><div class="divider"></div></li>
|
<li><div class="divider"></div></li>
|
||||||
{% endif %}
|
<li><a class="subheader">Administration</a></li>
|
||||||
|
|
||||||
{% if current_user.can('USE_API') %}
|
{# corpora #}
|
||||||
{# API #}
|
|
||||||
<li>
|
<li>
|
||||||
<a class="waves-effect" href="{{ url_for('apifairy.docs') }}"><i class="material-icons">api</i>API</a>
|
<a class="waves-effect" href="{{ url_for('admin.corpora') }}"><i class="nopaque-icons">I</i>Corpora</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if current_user.is_administrator %}
|
{# users #}
|
||||||
{# Administration #}
|
|
||||||
<li>
|
<li>
|
||||||
<a class="waves-effect" href="{{ url_for('admin.index') }}"><i class="material-icons">admin_panel_settings</i>Administration</a>
|
<a class="waves-effect" href="{{ url_for('admin.users') }}"><i class="material-icons">manage_accounts</i>Users</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
43
app/templates/admin/admin.html.j2
Normal file
43
app/templates/admin/admin.html.j2
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{% 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 %}
|
29
app/templates/admin/corpora.html.j2
Normal file
29
app/templates/admin/corpora.html.j2
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{% 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 %}
|
104
app/templates/admin/edit_user.html.j2
Normal file
104
app/templates/admin/edit_user.html.j2
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
{% 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 %}
|
131
app/templates/admin/user.html.j2
Normal file
131
app/templates/admin/user.html.j2
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
{% 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 %}
|
66
app/templates/admin/user_settings.html.j2
Normal file
66
app/templates/admin/user_settings.html.j2
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
{% 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 %}
|
34
app/templates/admin/users.html.j2
Normal file
34
app/templates/admin/users.html.j2
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{% 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 %}
|
@ -246,7 +246,7 @@
|
|||||||
let publishingModalIsPublicSwitchElement = document.querySelector('#publishing-modal-is-public-switch');
|
let publishingModalIsPublicSwitchElement = document.querySelector('#publishing-modal-is-public-switch');
|
||||||
publishingModalIsPublicSwitchElement.addEventListener('change', (event) => {
|
publishingModalIsPublicSwitchElement.addEventListener('change', (event) => {
|
||||||
let newIsPublic = publishingModalIsPublicSwitchElement.checked;
|
let newIsPublic = publishingModalIsPublicSwitchElement.checked;
|
||||||
app.corpora.updateIsPublic({{ corpus.hashid|tojson }}, newIsPublic)
|
nopaque.requests.corpora.entity.isPublic.update({{ corpus.hashid|tojson }}, newIsPublic)
|
||||||
.catch((response) => {
|
.catch((response) => {
|
||||||
publishingModalIsPublicSwitchElement.checked = !newIsPublic;
|
publishingModalIsPublicSwitchElement.checked = !newIsPublic;
|
||||||
});
|
});
|
||||||
@ -256,7 +256,7 @@ publishingModalIsPublicSwitchElement.addEventListener('change', (event) => {
|
|||||||
// #region Delete
|
// #region Delete
|
||||||
let deleteModalDeleteButtonElement = document.querySelector('#delete-modal-delete-button');
|
let deleteModalDeleteButtonElement = document.querySelector('#delete-modal-delete-button');
|
||||||
deleteModalDeleteButtonElement.addEventListener('click', (event) => {
|
deleteModalDeleteButtonElement.addEventListener('click', (event) => {
|
||||||
app.corpora.delete({{ corpus.hashid|tojson }})
|
nopaque.requests.corpora.entity.delete({{ corpus.hashid|tojson }})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
window.location.href = {{ url_for('main.dashboard')|tojson }};
|
window.location.href = {{ url_for('main.dashboard')|tojson }};
|
||||||
});
|
});
|
||||||
@ -346,14 +346,19 @@ M.Modal.init(
|
|||||||
shareLinkModalOutputContainerElement.classList.add('hide');
|
shareLinkModalOutputContainerElement.classList.add('hide');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
|
|
||||||
shareLinkModalCreateButtonElement.addEventListener('click', async (event) => {
|
shareLinkModalCreateButtonElement.addEventListener('click', (event) => {
|
||||||
const role = shareLinkModalCorpusFollowerRoleSelectElement.value;
|
let role = shareLinkModalCorpusFollowerRoleSelectElement.value;
|
||||||
const expiration = shareLinkModalExpirationDateDatepickerElement.value;
|
let expiration = shareLinkModalExpirationDateDatepickerElement.value
|
||||||
const shareLink = await app.corpora.createShareLink({{ corpus.hashid|tojson }}, expiration, role);
|
nopaque.requests.corpora.entity.generateShareLink({{ corpus.hashid|tojson }}, role, expiration)
|
||||||
shareLinkModalOutputContainerElement.classList.remove('hide');
|
.then((response) => {
|
||||||
shareLinkModalOutputFieldElement.value = shareLink;
|
response.json()
|
||||||
|
.then((json) => {
|
||||||
|
shareLinkModalOutputContainerElement.classList.remove('hide');
|
||||||
|
shareLinkModalOutputFieldElement.value = json.corpusShareLink;
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
shareLinkModalOutputCopyButtonElement.addEventListener('click', (event) => {
|
shareLinkModalOutputCopyButtonElement.addEventListener('click', (event) => {
|
||||||
|
@ -273,7 +273,7 @@ publicCorpusFollowerList.add(
|
|||||||
{% if cfr.has_permission('MANAGE_FILES') %}
|
{% if cfr.has_permission('MANAGE_FILES') %}
|
||||||
let followerBuildRequest = document.querySelector('#follower-build-request');
|
let followerBuildRequest = document.querySelector('#follower-build-request');
|
||||||
followerBuildRequest.addEventListener('click', () => {
|
followerBuildRequest.addEventListener('click', () => {
|
||||||
app.corpora.build({{ corpus.hashid|tojson }})
|
nopaque.requests.corpora.entity.build({{ corpus.hashid|tojson }})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
});
|
});
|
||||||
@ -380,12 +380,17 @@ M.Modal.init(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
shareLinkModalCreateButtonElement.addEventListener('click', async (event) => {
|
shareLinkModalCreateButtonElement.addEventListener('click', (event) => {
|
||||||
const roleName = shareLinkModalCorpusFollowerRoleSelectElement.value;
|
let role = shareLinkModalCorpusFollowerRoleSelectElement.value;
|
||||||
const expirationDate = shareLinkModalExpirationDateDatepickerElement.value;
|
let expiration = shareLinkModalExpirationDateDatepickerElement.value
|
||||||
const shareLink = await app.corpora.createShareLink({{ corpus.hashid|tojson }}, expiration, role);
|
nopaque.requests.corpora.entity.generateShareLink({{ corpus.hashid|tojson }}, role, expiration)
|
||||||
shareLinkModalOutputContainerElement.classList.remove('hide');
|
.then((response) => {
|
||||||
shareLinkModalOutputFieldElement.value = shareLink;
|
response.json()
|
||||||
|
.then((json) => {
|
||||||
|
shareLinkModalOutputContainerElement.classList.remove('hide');
|
||||||
|
shareLinkModalOutputFieldElement.value = json.corpusShareLink;
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
shareLinkModalOutputCopyButtonElement.addEventListener('click', (event) => {
|
shareLinkModalOutputCopyButtonElement.addEventListener('click', (event) => {
|
||||||
|
@ -137,26 +137,28 @@
|
|||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<script>
|
<script>
|
||||||
const deleteJobRequestElement = document.querySelector('#delete-job-request');
|
let deleteJobRequestElement = document.querySelector('#delete-job-request');
|
||||||
const restartJobRequestElement = document.querySelector('#restart-job-request');
|
let restartJobRequestElement = document.querySelector('#restart-job-request');
|
||||||
|
deleteJobRequestElement.addEventListener('click', (event) => {
|
||||||
deleteJobRequestElement.addEventListener('click', async (event) => {
|
nopaque.requests.jobs.entity.delete({{ job.hashid|tojson }});
|
||||||
const message = await app.jobs.delete({{ job.hashid|tojson }});
|
});
|
||||||
app.ui.flash(message, 'job');
|
restartJobRequestElement.addEventListener('click', (event) => {
|
||||||
});
|
nopaque.requests.jobs.entity.restart({{ job.hashid|tojson }});
|
||||||
restartJobRequestElement.addEventListener('click', async (event) => {
|
|
||||||
const message = await app.jobs.restart({{ job.hashid|tojson }});
|
|
||||||
app.ui.flash(message, 'job');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
{% if current_user.is_administrator %}
|
if ({{ current_user.is_administrator|tojson }}) {
|
||||||
const jobLogButtonElement = document.querySelector('#job-log-button');
|
let jobLogButtonElement = document.querySelector('#job-log-button');
|
||||||
const jobLogModalElement = document.querySelector('#job-log-modal');
|
jobLogButtonElement.addEventListener('click', (event) => {
|
||||||
|
nopaque.requests.jobs.entity.log({{ job.hashid|tojson }})
|
||||||
jobLogButtonElement.addEventListener('click', async (event) => {
|
.then(
|
||||||
const log = await app.jobs.log({{ job.hashid|tojson }});
|
(response) => {
|
||||||
jobLogModalElement.querySelector('pre code').textContent = log;
|
response.json()
|
||||||
});
|
.then((json) => {
|
||||||
{% endif %}
|
let jobLogModalElement = document.querySelector('#job-log-modal');
|
||||||
|
jobLogModalElement.querySelector('pre code').textContent = json.jobLog;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock scripts %}
|
{% endblock scripts %}
|
||||||
|
@ -46,19 +46,19 @@
|
|||||||
<div class="row" style="margin-left: 24px;">
|
<div class="row" style="margin-left: 24px;">
|
||||||
<div class="col s12 l3">
|
<div class="col s12 l3">
|
||||||
<label>
|
<label>
|
||||||
<input {% if user.has_profile_privacy_setting('SHOW_EMAIL') %}checked{% endif %} class="profile-privacy-setting-checkbox" data-profile-privacy-setting-name="ProfileShowEmail" {% 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="SHOW_EMAIL" {% if not user.is_public %}disabled{% endif %} type="checkbox">
|
||||||
<span>Email</span>
|
<span>Email</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col s12 l3">
|
<div class="col s12 l3">
|
||||||
<label>
|
<label>
|
||||||
<input {% if user.has_profile_privacy_setting('SHOW_LAST_SEEN') %}checked{% endif %} class="profile-privacy-setting-checkbox" data-profile-privacy-setting-name="ProfileShowLastSeen" {% 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="SHOW_LAST_SEEN" {% if not user.is_public %}disabled{% endif %} type="checkbox">
|
||||||
<span>Last seen</span>
|
<span>Last seen</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col s12 l3">
|
<div class="col s12 l3">
|
||||||
<label>
|
<label>
|
||||||
<input {% if user.has_profile_privacy_setting('SHOW_MEMBER_SINCE') %}checked{% endif %} class="profile-privacy-setting-checkbox" data-profile-privacy-setting-name="ProfileShowMemberSince" {% 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="SHOW_MEMBER_SINCE" {% if not user.is_public %}disabled{% endif %} type="checkbox">
|
||||||
<span>Member since</span>
|
<span>Member since</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@ -74,7 +74,7 @@
|
|||||||
<form method="POST">
|
<form method="POST">
|
||||||
{{ update_profile_information_form.hidden_tag() }}
|
{{ update_profile_information_form.hidden_tag() }}
|
||||||
{{ wtf.render_field(update_profile_information_form.full_name, material_icon='badge') }}
|
{{ wtf.render_field(update_profile_information_form.full_name, material_icon='badge') }}
|
||||||
{{ wtf.render_field(update_profile_information_form.about_me, material_icon='description') }}
|
{{ wtf.render_field(update_profile_information_form.about_me, material_icon='description', id='about-me-textfield') }}
|
||||||
{{ wtf.render_field(update_profile_information_form.website, material_icon='laptop') }}
|
{{ wtf.render_field(update_profile_information_form.website, material_icon='laptop') }}
|
||||||
{{ wtf.render_field(update_profile_information_form.organization, material_icon='business') }}
|
{{ wtf.render_field(update_profile_information_form.organization, material_icon='business') }}
|
||||||
{{ wtf.render_field(update_profile_information_form.location, material_icon='location_on') }}
|
{{ wtf.render_field(update_profile_information_form.location, material_icon='location_on') }}
|
||||||
@ -172,6 +172,8 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% block admin_settings %}{% endblock admin_settings %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock page_content %}
|
{% endblock page_content %}
|
||||||
@ -250,28 +252,28 @@ for (let collapsibleElement of document.querySelectorAll('.collapsible.no-autoin
|
|||||||
// #region Profile Privacy settings
|
// #region Profile Privacy settings
|
||||||
let profileIsPublicSwitchElement = document.querySelector('#profile-is-public-switch');
|
let profileIsPublicSwitchElement = document.querySelector('#profile-is-public-switch');
|
||||||
let profilePrivacySettingCheckboxElements = document.querySelectorAll('.profile-privacy-setting-checkbox');
|
let profilePrivacySettingCheckboxElements = document.querySelectorAll('.profile-privacy-setting-checkbox');
|
||||||
profileIsPublicSwitchElement.addEventListener('change', async (event) => {
|
profileIsPublicSwitchElement.addEventListener('change', (event) => {
|
||||||
const newEnabled = profileIsPublicSwitchElement.checked;
|
let newEnabled = profileIsPublicSwitchElement.checked;
|
||||||
try {
|
nopaque.requests.users.entity.settings.profilePrivacy.update({{ user.hashid|tojson }}, 'is-public', newEnabled)
|
||||||
const message = await app.settings.updateProfileIsPublic(newEnabled);
|
.then(
|
||||||
for (let profilePrivacySettingCheckboxElement of profilePrivacySettingCheckboxElements) {
|
(response) => {
|
||||||
profilePrivacySettingCheckboxElement.disabled = !newEnabled;
|
for (let profilePrivacySettingCheckboxElement of document.querySelectorAll('.profile-privacy-setting-checkbox')) {
|
||||||
}
|
profilePrivacySettingCheckboxElement.disabled = !newEnabled;
|
||||||
app.ui.flash(message);
|
}
|
||||||
} catch (e) {
|
},
|
||||||
profileIsPublicSwitchElement.checked = !newEnabled;
|
(response) => {
|
||||||
app.ui.flash(e.message, 'error');
|
profileIsPublicSwitchElement.checked = !newEnabled;
|
||||||
}
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
for (let profilePrivacySettingCheckboxElement of profilePrivacySettingCheckboxElements) {
|
for (let profilePrivacySettingCheckboxElement of profilePrivacySettingCheckboxElements) {
|
||||||
profilePrivacySettingCheckboxElement.addEventListener('change', async (event) => {
|
profilePrivacySettingCheckboxElement.addEventListener('change', (event) => {
|
||||||
const newEnabled = profilePrivacySettingCheckboxElement.checked;
|
let newEnabled = profilePrivacySettingCheckboxElement.checked;
|
||||||
const valueName = profilePrivacySettingCheckboxElement.dataset.profilePrivacySettingName;
|
let valueName = profilePrivacySettingCheckboxElement.dataset.profilePrivacySettingName;
|
||||||
try {
|
nopaque.requests.users.entity.settings.profilePrivacy.update({{ user.hashid|tojson }}, valueName, newEnabled)
|
||||||
app.settings[`update${valueName}`](newEnabled)
|
.catch((response) => {
|
||||||
} catch (error) {
|
|
||||||
profilePrivacySettingCheckboxElement.checked = !newEnabled;
|
profilePrivacySettingCheckboxElement.checked = !newEnabled;
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// #endregion Profile Privacy settings
|
// #endregion Profile Privacy settings
|
@ -7,8 +7,6 @@
|
|||||||
{{ render_integer_field(field, *args, **kwargs) }}
|
{{ render_integer_field(field, *args, **kwargs) }}
|
||||||
{% elif field.type == 'MultipleFileField' %}
|
{% elif field.type == 'MultipleFileField' %}
|
||||||
{{ render_multiple_file_field(field, *args, **kwargs) }}
|
{{ render_multiple_file_field(field, *args, **kwargs) }}
|
||||||
{% elif field.type == 'StringField' %}
|
|
||||||
{{ render_string_field(field, *args, **kwargs) }}
|
|
||||||
{% elif field.type == 'SubmitField' %}
|
{% elif field.type == 'SubmitField' %}
|
||||||
{{ render_submit_field(field, *args, **kwargs) }}
|
{{ render_submit_field(field, *args, **kwargs) }}
|
||||||
{% elif field.type == 'TextAreaField' %}
|
{% elif field.type == 'TextAreaField' %}
|
||||||
@ -22,7 +20,7 @@
|
|||||||
{% macro render_boolean_field(field) %}
|
{% macro render_boolean_field(field) %}
|
||||||
<div>
|
<div>
|
||||||
<label>
|
<label>
|
||||||
{{ field(*args, **kwargs) }}
|
<input id="{{ field.id }}" name="{{ field.name }}" type="checkbox">
|
||||||
<span>{{ field.label.text }}</span>
|
<span>{{ field.label.text }}</span>
|
||||||
{% for error in field.errors %}
|
{% for error in field.errors %}
|
||||||
<span class="helper-text error-color-text">{{ error }}</span>
|
<span class="helper-text error-color-text">{{ error }}</span>
|
||||||
@ -38,7 +36,25 @@
|
|||||||
<div class="file-field input-field">
|
<div class="file-field input-field">
|
||||||
<div class="btn">
|
<div class="btn">
|
||||||
<span>{{ field.label.text }}</span>
|
<span>{{ field.label.text }}</span>
|
||||||
{{ field(*args, **kwargs) }}
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<div class="file-path-wrapper">
|
<div class="file-path-wrapper">
|
||||||
<input class="file-path validate" type="text" placeholder="{{ placeholder }}">
|
<input class="file-path validate" type="text" placeholder="{{ placeholder }}">
|
||||||
@ -51,29 +67,12 @@
|
|||||||
|
|
||||||
|
|
||||||
{% macro render_integer_field(field) %}
|
{% macro render_integer_field(field) %}
|
||||||
{% set classes = kwargs.pop('class_', '').split(' ') %}
|
<div class="input-field">
|
||||||
|
{% if 'material_icon' in kwargs %}
|
||||||
{% if 'validate' not in classes %}
|
<i class="material-icons prefix">{{ kwargs.pop('material_icon') }}</i>
|
||||||
{% set _ = classes.append('validate') %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<input class="validate" id="{{ field.id }}" name="{{ field.name }}" type="number">
|
||||||
{% set _ = kwargs.update({'class_': ' '.join(classes)}) %}
|
<label for="{{ field.id }}">{{ field.label.text }}</label>
|
||||||
|
|
||||||
{{ render_generic_field(field, *args, **kwargs) }}
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
|
|
||||||
{% macro render_multiple_file_field(field) %}
|
|
||||||
{% set placeholder = kwargs.pop('placeholder', '') %}
|
|
||||||
|
|
||||||
<div class="file-field input-field">
|
|
||||||
<div class="btn">
|
|
||||||
<span>{{ field.label.text }}</span>
|
|
||||||
{{ field(*args, **kwargs) }}
|
|
||||||
</div>
|
|
||||||
<div class="file-path-wrapper">
|
|
||||||
<input class="file-path validate" type="text" placeholder="{{ placeholder }}">
|
|
||||||
</div>
|
|
||||||
{% for error in field.errors %}
|
{% for error in field.errors %}
|
||||||
<span class="helper-text error-color-text">{{ error }}</span>
|
<span class="helper-text error-color-text">{{ error }}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -81,21 +80,8 @@
|
|||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
{% macro render_string_field(field) %}
|
|
||||||
{% set classes = kwargs.pop('class_', '').split(' ') %}
|
|
||||||
|
|
||||||
{% if 'validate' not in classes %}
|
|
||||||
{% set _ = classes.append('validate') %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% set _ = kwargs.update({'class_': ' '.join(classes)}) %}
|
|
||||||
|
|
||||||
{{ render_generic_field(field, *args, **kwargs) }}
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
|
|
||||||
{% macro render_submit_field(field) %}
|
{% macro render_submit_field(field) %}
|
||||||
<button class="btn waves-effect waves-light" id="{{ field.id }}" name="{{ field.name }}" type="submit" value="Submit">
|
<button class="btn waves-effect waves-light" id="{{ field.id }}" name="{{ field.name }}" type="submit">
|
||||||
{{ field.label.text }}
|
{{ field.label.text }}
|
||||||
{% if 'material_icon' in kwargs %}
|
{% if 'material_icon' in kwargs %}
|
||||||
<i class="material-icons right">{{ kwargs.pop('material_icon') }}</i>
|
<i class="material-icons right">{{ kwargs.pop('material_icon') }}</i>
|
||||||
@ -105,23 +91,26 @@
|
|||||||
|
|
||||||
|
|
||||||
{% macro render_text_area_field(field) %}
|
{% macro render_text_area_field(field) %}
|
||||||
{% set classes = kwargs.pop('class_', '').split(' ') %}
|
<div class="input-field">
|
||||||
|
{% if 'material_icon' in kwargs %}
|
||||||
{% if 'materialize-textarea' not in classes %}
|
<i class="material-icons prefix">{{ kwargs.pop('material_icon') }}</i>
|
||||||
{% set _ = classes.append('materialize-textarea') %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<textarea class="materialize-textarea validate" id="{{ field.id }}" name="{{ field.name }}"></textarea>
|
||||||
{% if 'validate' not in classes %}
|
<label for="{{ field.id }}">{{ field.label.text }}</label>
|
||||||
{% set _ = classes.append('validate') %}
|
{% for error in field.errors %}
|
||||||
{% endif %}
|
<span class="helper-text error-color-text">{{ error }}</span>
|
||||||
|
{% endfor %}
|
||||||
{% set _ = kwargs.update({'class_': ' '.join(classes)}) %}
|
</div>
|
||||||
|
|
||||||
{{ render_generic_field(field, *args, **kwargs) }}
|
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
{% macro render_generic_field(field) %}
|
{% macro render_generic_field(field) %}
|
||||||
|
{% set classes_ = kwargs.pop('class_', '').split(' ') %}
|
||||||
|
{% if 'validate' not in classes_ %}
|
||||||
|
{% set _ = classes_.append('validate') %}
|
||||||
|
{% endif %}
|
||||||
|
{% set _ = kwargs.update({'class_': ' '.join(classes_)}) %}
|
||||||
|
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
{% if 'material_icon' in kwargs %}
|
{% if 'material_icon' in kwargs %}
|
||||||
<i class="material-icons prefix">{{ kwargs.pop('material_icon') }}</i>
|
<i class="material-icons prefix">{{ kwargs.pop('material_icon') }}</i>
|
||||||
|
@ -14,7 +14,6 @@ docker==7.0.0
|
|||||||
email_validator==2.1.1
|
email_validator==2.1.1
|
||||||
eventlet==0.34.2
|
eventlet==0.34.2
|
||||||
Flask==2.3.3
|
Flask==2.3.3
|
||||||
Flask-Admin==1.6.1
|
|
||||||
Flask-APScheduler==1.13.1
|
Flask-APScheduler==1.13.1
|
||||||
Flask-Assets==2.1.0
|
Flask-Assets==2.1.0
|
||||||
Flask-Hashids==1.0.3
|
Flask-Hashids==1.0.3
|
||||||
|
@ -4,7 +4,6 @@ dnspython==2.5.0
|
|||||||
docker
|
docker
|
||||||
eventlet==0.34.2
|
eventlet==0.34.2
|
||||||
Flask==2.3.3
|
Flask==2.3.3
|
||||||
Flask-Admin==1.6.1
|
|
||||||
Flask-APScheduler
|
Flask-APScheduler
|
||||||
Flask-Assets
|
Flask-Assets
|
||||||
Flask-Hashids
|
Flask-Hashids
|
||||||
|
Loading…
x
Reference in New Issue
Block a user