mirror of
https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
synced 2025-07-06 12:43:18 +00:00
Compare commits
40 Commits
access-pip
...
551b928dca
Author | SHA1 | Date | |
---|---|---|---|
551b928dca | |||
eeb5a280b3 | |||
5fc3015bf1 | |||
5f05cedf5e | |||
aabea234fe | |||
492fdc9d28 | |||
02e6c7c16c | |||
c7ca674b2f | |||
81c6f32a35 | |||
94548ac30c | |||
158190de1a | |||
13e4d461c7 | |||
e51dcafa6f | |||
f79c6d48b2 | |||
5ee9edef9f | |||
f1ccda6ad7 | |||
a65b1ff578 | |||
fe0fcb0e10 | |||
32fa632961 | |||
562b8d5ce0 | |||
cbd0a41bce | |||
c68286e010 | |||
4a29a52f2a | |||
991810cff5 | |||
6025a4a606 | |||
e1cfd394fa | |||
882987ba68 | |||
a03b5918d9 | |||
43b38b2216 | |||
543276d766 | |||
485a0155c6 | |||
c29c50feb9 | |||
c191e7bd4a | |||
8f960cf359 | |||
ccf484c9bc | |||
d0d2a8abd6 | |||
03876f6a39 | |||
cdf6f9fcfd | |||
268da220d2 | |||
84e1755a57 |
@ -5,9 +5,9 @@
|
|||||||
!app
|
!app
|
||||||
!migrations
|
!migrations
|
||||||
!tests
|
!tests
|
||||||
!.flaskenv
|
|
||||||
!boot.sh
|
!boot.sh
|
||||||
!config.py
|
!config.py
|
||||||
!docker-nopaque-entrypoint.sh
|
!docker-nopaque-entrypoint.sh
|
||||||
!nopaque.py
|
|
||||||
!requirements.txt
|
!requirements.txt
|
||||||
|
!requirements.freezed.txt
|
||||||
|
!wsgi.py
|
||||||
|
22
.env.tpl
22
.env.tpl
@ -1,32 +1,20 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# Variables for use in Docker Compose YAML files #
|
# Environment variables used by Docker Compose config files. #
|
||||||
##############################################################################
|
##############################################################################
|
||||||
# HINT: Use this bash command `id -u`
|
# HINT: Use this bash command `id -u`
|
||||||
# NOTE: 0 (= root user) is not allowed
|
# NOTE: 0 (= root user) is not allowed
|
||||||
HOST_UID=
|
HOST_UID=
|
||||||
|
|
||||||
# HINT: Use this bash command `id -g`
|
# HINT: Use this bash command `id -g`
|
||||||
|
# NOTE: 0 (= root group) is not allowed
|
||||||
HOST_GID=
|
HOST_GID=
|
||||||
|
|
||||||
# HINT: Use this bash command `getent group docker | cut -d: -f3`
|
# HINT: Use this bash command `getent group docker | cut -d: -f3`
|
||||||
HOST_DOCKER_GID=
|
HOST_DOCKER_GID=
|
||||||
|
|
||||||
# DEFAULT: nopaque
|
# DEFAULT: nopaque
|
||||||
# DOCKER_DEFAULT_NETWORK_NAME=
|
NOPAQUE_DOCKER_NETWORK_NAME=nopaque
|
||||||
|
|
||||||
# DEFAULT: ./volumes/db/data
|
|
||||||
# NOTE: Use `.` as <project-basedir>
|
|
||||||
# DOCKER_DB_SERVICE_DATA_VOLUME_SOURCE_PATH=
|
|
||||||
|
|
||||||
# DEFAULT: ./volumes/mq/data
|
|
||||||
# NOTE: Use `.` as <project-basedir>
|
|
||||||
# DOCKER_MQ_SERVICE_DATA_VOLUME_SOURCE_PATH=
|
|
||||||
|
|
||||||
# NOTE: This must be a network share and it must be available on all
|
# NOTE: This must be a network share and it must be available on all
|
||||||
# Docker Swarm nodes, mounted to the same path with the same
|
# Docker Swarm nodes, mounted to the same path.
|
||||||
# user and group ownership.
|
HOST_NOPAQUE_DATA_PATH=/mnt/nopaque
|
||||||
DOCKER_NOPAQUE_SERVICE_DATA_VOLUME_SOURCE_PATH=
|
|
||||||
|
|
||||||
# DEFAULT: ./volumes/nopaque/logs
|
|
||||||
# NOTE: Use `.` as <project-basedir>
|
|
||||||
# DOCKER_NOPAQUE_SERVICE_LOGS_VOLUME_SOURCE_PATH=.
|
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,8 +2,6 @@
|
|||||||
app/static/gen/
|
app/static/gen/
|
||||||
volumes/
|
volumes/
|
||||||
docker-compose.override.yml
|
docker-compose.override.yml
|
||||||
logs/
|
|
||||||
!logs/dummy
|
|
||||||
*.env
|
*.env
|
||||||
|
|
||||||
*.pjentsch-testing
|
*.pjentsch-testing
|
||||||
|
18
.vscode/settings.json
vendored
18
.vscode/settings.json
vendored
@ -1,19 +1,7 @@
|
|||||||
{
|
{
|
||||||
"editor.rulers": [79],
|
"editor.rulers": [79],
|
||||||
|
"editor.tabSize": 2,
|
||||||
"files.insertFinalNewline": true,
|
"files.insertFinalNewline": true,
|
||||||
"[css]": {
|
"files.trimFinalNewlines": true,
|
||||||
"editor.tabSize": 2
|
"files.trimTrailingWhitespace": true
|
||||||
},
|
|
||||||
"[html]": {
|
|
||||||
"editor.tabSize": 2
|
|
||||||
},
|
|
||||||
"[javascript]": {
|
|
||||||
"editor.tabSize": 2
|
|
||||||
},
|
|
||||||
"[jinja-html]": {
|
|
||||||
"editor.tabSize": 2
|
|
||||||
},
|
|
||||||
"[scss]": {
|
|
||||||
"editor.tabSize": 2
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
11
Dockerfile
11
Dockerfile
@ -35,20 +35,17 @@ ENV PATH="${NOPAQUE_PYTHON3_VENV_PATH}/bin:${PATH}"
|
|||||||
|
|
||||||
|
|
||||||
# Install Python dependencies
|
# Install Python dependencies
|
||||||
COPY --chown=nopaque:nopaque requirements.txt requirements.txt
|
COPY --chown=nopaque:nopaque requirements.freezed.txt requirements.freezed.txt
|
||||||
RUN python3 -m pip install --requirement requirements.txt \
|
RUN python3 -m pip install --requirement requirements.freezed.txt \
|
||||||
&& rm requirements.txt
|
&& rm requirements.freezed.txt
|
||||||
|
|
||||||
|
|
||||||
# Install the application
|
# Install the application
|
||||||
COPY docker-nopaque-entrypoint.sh /usr/local/bin/
|
COPY docker-nopaque-entrypoint.sh /usr/local/bin/
|
||||||
|
|
||||||
COPY --chown=nopaque:nopaque app app
|
COPY --chown=nopaque:nopaque app app
|
||||||
COPY --chown=nopaque:nopaque migrations migrations
|
COPY --chown=nopaque:nopaque migrations migrations
|
||||||
COPY --chown=nopaque:nopaque tests tests
|
COPY --chown=nopaque:nopaque tests tests
|
||||||
COPY --chown=nopaque:nopaque .flaskenv boot.sh config.py nopaque.py requirements.txt ./
|
COPY --chown=nopaque:nopaque boot.sh config.py wsgi.py ./
|
||||||
|
|
||||||
RUN mkdir logs
|
|
||||||
|
|
||||||
|
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
|
@ -35,7 +35,7 @@ username@hostname:~$ sudo mount --types cifs --options gid=${USER},password=nopa
|
|||||||
# Clone the nopaque repository
|
# Clone the nopaque repository
|
||||||
username@hostname:~$ git clone https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
|
username@hostname:~$ git clone https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
|
||||||
# Create data directories
|
# Create data directories
|
||||||
username@hostname:~$ mkdir data/{db,logs,mq}
|
username@hostname:~$ mkdir volumes/{db,mq}
|
||||||
username@hostname:~$ cp db.env.tpl db.env
|
username@hostname:~$ cp db.env.tpl db.env
|
||||||
username@hostname:~$ cp .env.tpl .env
|
username@hostname:~$ cp .env.tpl .env
|
||||||
# Fill out the variables within these files.
|
# Fill out the variables within these files.
|
||||||
|
113
app/__init__.py
113
app/__init__.py
@ -2,9 +2,9 @@ from apifairy import APIFairy
|
|||||||
from config import Config
|
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_apscheduler import APScheduler
|
from flask_apscheduler import APScheduler
|
||||||
from flask_assets import Environment
|
from flask_assets import Environment
|
||||||
from flask_breadcrumbs import Breadcrumbs, default_breadcrumb_root
|
|
||||||
from flask_login import LoginManager
|
from flask_login import LoginManager
|
||||||
from flask_mail import Mail
|
from flask_mail import Mail
|
||||||
from flask_marshmallow import Marshmallow
|
from flask_marshmallow import Marshmallow
|
||||||
@ -13,98 +13,139 @@ from flask_paranoid import Paranoid
|
|||||||
from flask_socketio import SocketIO
|
from flask_socketio import SocketIO
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from flask_hashids import Hashids
|
from flask_hashids import Hashids
|
||||||
|
from logging import Formatter, StreamHandler
|
||||||
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
|
|
||||||
|
|
||||||
|
docker_client = DockerClient.from_env()
|
||||||
|
|
||||||
apifairy = APIFairy()
|
apifairy = APIFairy()
|
||||||
assets = Environment()
|
assets = Environment()
|
||||||
breadcrumbs = Breadcrumbs()
|
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
docker_client = DockerClient()
|
|
||||||
hashids = Hashids()
|
hashids = Hashids()
|
||||||
login = LoginManager()
|
login = LoginManager()
|
||||||
login.login_view = 'auth.login'
|
|
||||||
login.login_message = 'Please log in to access this page.'
|
|
||||||
ma = Marshmallow()
|
ma = Marshmallow()
|
||||||
mail = Mail()
|
mail = Mail()
|
||||||
migrate = Migrate(compare_type=True)
|
migrate = Migrate(compare_type=True)
|
||||||
paranoid = Paranoid()
|
paranoid = Paranoid()
|
||||||
paranoid.redirect_view = '/'
|
|
||||||
scheduler = APScheduler()
|
scheduler = APScheduler()
|
||||||
socketio = SocketIO()
|
socketio = SocketIO()
|
||||||
|
|
||||||
|
|
||||||
def create_app(config: Config = Config) -> Flask:
|
def create_app(config: Config = Config) -> Flask:
|
||||||
''' Creates an initialized Flask (WSGI Application) object. '''
|
''' Creates an initialized Flask object. '''
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config.from_object(config)
|
app.config.from_object(config)
|
||||||
config.init_app(app)
|
|
||||||
|
# region Logging
|
||||||
|
log_formatter = Formatter(
|
||||||
|
fmt=app.config['NOPAQUE_LOG_FORMAT'],
|
||||||
|
datefmt=app.config['NOPAQUE_LOG_DATE_FORMAT']
|
||||||
|
)
|
||||||
|
|
||||||
|
log_handler = StreamHandler()
|
||||||
|
log_handler.setFormatter(log_formatter)
|
||||||
|
log_handler.setLevel(app.config['NOPAQUE_LOG_LEVEL'])
|
||||||
|
|
||||||
|
app.logger.setLevel('DEBUG')
|
||||||
|
app.logger.removeHandler(default_handler)
|
||||||
|
app.logger.addHandler(log_handler)
|
||||||
|
# endregion Logging
|
||||||
|
|
||||||
|
# region Middlewares
|
||||||
|
if app.config['NOPAQUE_PROXY_FIX_ENABLED']:
|
||||||
|
app.wsgi_app = ProxyFix(
|
||||||
|
app.wsgi_app,
|
||||||
|
x_for=app.config['NOPAQUE_PROXY_FIX_X_FOR'],
|
||||||
|
x_host=app.config['NOPAQUE_PROXY_FIX_X_HOST'],
|
||||||
|
x_port=app.config['NOPAQUE_PROXY_FIX_X_PORT'],
|
||||||
|
x_prefix=app.config['NOPAQUE_PROXY_FIX_X_PREFIX'],
|
||||||
|
x_proto=app.config['NOPAQUE_PROXY_FIX_X_PROTO']
|
||||||
|
)
|
||||||
|
# endregion Middlewares
|
||||||
|
|
||||||
|
# region Extensions
|
||||||
docker_client.login(
|
docker_client.login(
|
||||||
app.config['NOPAQUE_DOCKER_REGISTRY_USERNAME'],
|
app.config['NOPAQUE_DOCKER_REGISTRY_USERNAME'],
|
||||||
password=app.config['NOPAQUE_DOCKER_REGISTRY_PASSWORD'],
|
password=app.config['NOPAQUE_DOCKER_REGISTRY_PASSWORD'],
|
||||||
registry=app.config['NOPAQUE_DOCKER_REGISTRY']
|
registry=app.config['NOPAQUE_DOCKER_REGISTRY']
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .models import AnonymousUser, User
|
||||||
|
|
||||||
apifairy.init_app(app)
|
apifairy.init_app(app)
|
||||||
assets.init_app(app)
|
assets.init_app(app)
|
||||||
breadcrumbs.init_app(app)
|
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
hashids.init_app(app)
|
hashids.init_app(app)
|
||||||
login.init_app(app)
|
login.init_app(app)
|
||||||
|
login.anonymous_user = AnonymousUser
|
||||||
|
login.login_view = 'auth.login'
|
||||||
|
login.user_loader(lambda user_id: User.query.get(int(user_id)))
|
||||||
ma.init_app(app)
|
ma.init_app(app)
|
||||||
mail.init_app(app)
|
mail.init_app(app)
|
||||||
migrate.init_app(app, db)
|
migrate.init_app(app, db)
|
||||||
paranoid.init_app(app)
|
paranoid.init_app(app)
|
||||||
|
paranoid.redirect_view = '/'
|
||||||
scheduler.init_app(app)
|
scheduler.init_app(app)
|
||||||
socketio.init_app(app, message_queue=app.config['NOPAQUE_SOCKETIO_MESSAGE_QUEUE_URI']) # noqa
|
socketio.init_app(app, message_queue=app.config['NOPAQUE_SOCKETIO_MESSAGE_QUEUE_URI'])
|
||||||
|
# endregion Extensions
|
||||||
|
|
||||||
from .models.event_listeners import register_event_listeners
|
# region Blueprints
|
||||||
register_event_listeners()
|
from .blueprints.admin import bp as admin_blueprint
|
||||||
|
|
||||||
from .admin import bp as admin_blueprint
|
|
||||||
default_breadcrumb_root(admin_blueprint, '.admin')
|
|
||||||
app.register_blueprint(admin_blueprint, url_prefix='/admin')
|
app.register_blueprint(admin_blueprint, url_prefix='/admin')
|
||||||
|
|
||||||
from .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')
|
||||||
|
|
||||||
from .auth import bp as auth_blueprint
|
from .blueprints.auth import bp as auth_blueprint
|
||||||
default_breadcrumb_root(auth_blueprint, '.')
|
|
||||||
app.register_blueprint(auth_blueprint)
|
app.register_blueprint(auth_blueprint)
|
||||||
|
|
||||||
from .contributions import bp as contributions_blueprint
|
from .blueprints.contributions import bp as contributions_blueprint
|
||||||
default_breadcrumb_root(contributions_blueprint, '.contributions')
|
|
||||||
app.register_blueprint(contributions_blueprint, url_prefix='/contributions')
|
app.register_blueprint(contributions_blueprint, url_prefix='/contributions')
|
||||||
|
|
||||||
from .corpora import bp as corpora_blueprint
|
from .blueprints.corpora import bp as corpora_blueprint
|
||||||
from .corpora.cqi_over_sio import CQiNamespace
|
|
||||||
default_breadcrumb_root(corpora_blueprint, '.corpora')
|
|
||||||
app.register_blueprint(corpora_blueprint, cli_group='corpus', url_prefix='/corpora')
|
app.register_blueprint(corpora_blueprint, cli_group='corpus', url_prefix='/corpora')
|
||||||
socketio.on_namespace(CQiNamespace('/cqi_over_sio'))
|
|
||||||
|
|
||||||
from .errors import bp as errors_bp
|
from .blueprints.errors import bp as errors_bp
|
||||||
app.register_blueprint(errors_bp)
|
app.register_blueprint(errors_bp)
|
||||||
|
|
||||||
from .jobs import bp as jobs_blueprint
|
from .blueprints.jobs import bp as jobs_blueprint
|
||||||
default_breadcrumb_root(jobs_blueprint, '.jobs')
|
|
||||||
app.register_blueprint(jobs_blueprint, url_prefix='/jobs')
|
app.register_blueprint(jobs_blueprint, url_prefix='/jobs')
|
||||||
|
|
||||||
from .main import bp as main_blueprint
|
from .blueprints.main import bp as main_blueprint
|
||||||
default_breadcrumb_root(main_blueprint, '.')
|
|
||||||
app.register_blueprint(main_blueprint, cli_group=None)
|
app.register_blueprint(main_blueprint, cli_group=None)
|
||||||
|
|
||||||
from .services import bp as services_blueprint
|
from .blueprints.services import bp as services_blueprint
|
||||||
default_breadcrumb_root(services_blueprint, '.services')
|
|
||||||
app.register_blueprint(services_blueprint, url_prefix='/services')
|
app.register_blueprint(services_blueprint, url_prefix='/services')
|
||||||
|
|
||||||
from .settings import bp as settings_blueprint
|
from .blueprints.settings import bp as settings_blueprint
|
||||||
default_breadcrumb_root(settings_blueprint, '.settings')
|
|
||||||
app.register_blueprint(settings_blueprint, url_prefix='/settings')
|
app.register_blueprint(settings_blueprint, url_prefix='/settings')
|
||||||
|
|
||||||
from .users import bp as users_blueprint
|
from .blueprints.users import bp as users_blueprint
|
||||||
default_breadcrumb_root(users_blueprint, '.users')
|
|
||||||
app.register_blueprint(users_blueprint, cli_group='user', url_prefix='/users')
|
app.register_blueprint(users_blueprint, cli_group='user', url_prefix='/users')
|
||||||
|
|
||||||
from .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')
|
||||||
|
# endregion Blueprints
|
||||||
|
|
||||||
|
# region SocketIO Namespaces
|
||||||
|
from .blueprints.corpora.cqi_over_sio import CQiOverSocketIO
|
||||||
|
socketio.on_namespace(CQiOverSocketIO('/cqi_over_sio'))
|
||||||
|
# endregion SocketIO Namespaces
|
||||||
|
|
||||||
|
# region Database event Listeners
|
||||||
|
from .models.event_listeners import register_event_listeners
|
||||||
|
register_event_listeners()
|
||||||
|
# endregion Database event Listeners
|
||||||
|
|
||||||
|
# region Add scheduler jobs
|
||||||
|
if app.config['NOPAQUE_IS_PRIMARY_INSTANCE']:
|
||||||
|
from .jobs import handle_corpora
|
||||||
|
scheduler.add_job('handle_corpora', handle_corpora, seconds=3, trigger='interval')
|
||||||
|
|
||||||
|
from .jobs import handle_jobs
|
||||||
|
scheduler.add_job('handle_jobs', handle_jobs, seconds=3, trigger='interval')
|
||||||
|
# endregion Add scheduler jobs
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
49
app/blueprints/admin/events.py
Normal file
49
app/blueprints/admin/events.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
from flask_login import current_user
|
||||||
|
from flask_socketio import disconnect, Namespace
|
||||||
|
from app import db, hashids
|
||||||
|
from app.decorators import socketio_admin_required
|
||||||
|
from app.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class AdminNamespace(Namespace):
|
||||||
|
def on_connect(self):
|
||||||
|
# Check if the user is authenticated and is an administrator
|
||||||
|
if not (current_user.is_authenticated and current_user.is_administrator):
|
||||||
|
disconnect()
|
||||||
|
|
||||||
|
|
||||||
|
@socketio_admin_required
|
||||||
|
def on_set_user_confirmed(self, user_hashid: str, confirmed_value: bool):
|
||||||
|
# Decode the user hashid
|
||||||
|
user_id = hashids.decode(user_hashid)
|
||||||
|
|
||||||
|
# Validate user_id
|
||||||
|
if not isinstance(user_id, int):
|
||||||
|
return {
|
||||||
|
'code': 400,
|
||||||
|
'body': 'user_id is invalid'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate confirmed_value
|
||||||
|
if not isinstance(confirmed_value, bool):
|
||||||
|
return {
|
||||||
|
'code': 400,
|
||||||
|
'body': 'confirmed_value is invalid'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Load user from database
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
if user is None:
|
||||||
|
return {
|
||||||
|
'code': 404,
|
||||||
|
'body': 'User not found'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update user confirmed status
|
||||||
|
user.confirmed = confirmed_value
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'code': 200,
|
||||||
|
'body': f'User "{user.username}" is now {"confirmed" if confirmed_value else "unconfirmed"}'
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
from flask import abort, request
|
from flask import abort, request
|
||||||
from app import db
|
|
||||||
from app.decorators import content_negotiation
|
from app.decorators import content_negotiation
|
||||||
|
from app import db
|
||||||
from app.models import User
|
from app.models import User
|
||||||
from . import bp
|
from . import bp
|
||||||
|
|
@ -1,8 +1,7 @@
|
|||||||
from flask import abort, flash, redirect, render_template, url_for
|
from flask import abort, flash, redirect, render_template, url_for
|
||||||
from flask_breadcrumbs import register_breadcrumb
|
|
||||||
from app import db, hashids
|
from app import db, hashids
|
||||||
from app.models import Avatar, Corpus, Role, User
|
from app.models import Avatar, Corpus, Role, User
|
||||||
from app.users.settings.forms import (
|
from app.blueprints.users.settings.forms import (
|
||||||
UpdateAvatarForm,
|
UpdateAvatarForm,
|
||||||
UpdatePasswordForm,
|
UpdatePasswordForm,
|
||||||
UpdateNotificationsForm,
|
UpdateNotificationsForm,
|
||||||
@ -11,14 +10,9 @@ from app.users.settings.forms import (
|
|||||||
)
|
)
|
||||||
from . import bp
|
from . import bp
|
||||||
from .forms import UpdateUserForm
|
from .forms import UpdateUserForm
|
||||||
from app.users.utils import (
|
|
||||||
user_endpoint_arguments_constructor as user_eac,
|
|
||||||
user_dynamic_list_constructor as user_dlc
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('')
|
@bp.route('')
|
||||||
@register_breadcrumb(bp, '.', '<i class="material-icons left">admin_panel_settings</i>Administration')
|
|
||||||
def admin():
|
def admin():
|
||||||
return render_template(
|
return render_template(
|
||||||
'admin/admin.html.j2',
|
'admin/admin.html.j2',
|
||||||
@ -27,7 +21,6 @@ def admin():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/corpora')
|
@bp.route('/corpora')
|
||||||
@register_breadcrumb(bp, '.corpora', 'Corpora')
|
|
||||||
def corpora():
|
def corpora():
|
||||||
corpora = Corpus.query.all()
|
corpora = Corpus.query.all()
|
||||||
return render_template(
|
return render_template(
|
||||||
@ -38,7 +31,6 @@ def corpora():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/users')
|
@bp.route('/users')
|
||||||
@register_breadcrumb(bp, '.users', '<i class="material-icons left">group</i>Users')
|
|
||||||
def users():
|
def users():
|
||||||
users = User.query.all()
|
users = User.query.all()
|
||||||
return render_template(
|
return render_template(
|
||||||
@ -49,7 +41,6 @@ def users():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/users/<hashid:user_id>')
|
@bp.route('/users/<hashid:user_id>')
|
||||||
@register_breadcrumb(bp, '.users.entity', '', dynamic_list_constructor=user_dlc)
|
|
||||||
def user(user_id):
|
def user(user_id):
|
||||||
user = User.query.get_or_404(user_id)
|
user = User.query.get_or_404(user_id)
|
||||||
corpora = Corpus.query.filter(Corpus.user == user).all()
|
corpora = Corpus.query.filter(Corpus.user == user).all()
|
||||||
@ -62,7 +53,6 @@ def user(user_id):
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/users/<hashid:user_id>/settings', methods=['GET', 'POST'])
|
@bp.route('/users/<hashid:user_id>/settings', methods=['GET', 'POST'])
|
||||||
@register_breadcrumb(bp, '.users.entity.settings', '<i class="material-icons left">settings</i>Settings')
|
|
||||||
def user_settings(user_id):
|
def user_settings(user_id):
|
||||||
user = User.query.get_or_404(user_id)
|
user = User.query.get_or_404(user_id)
|
||||||
update_account_information_form = UpdateAccountInformationForm(user)
|
update_account_information_form = UpdateAccountInformationForm(user)
|
@ -5,8 +5,8 @@ from flask import abort, Blueprint
|
|||||||
from werkzeug.exceptions import InternalServerError
|
from werkzeug.exceptions import InternalServerError
|
||||||
from app import db, hashids
|
from app import db, hashids
|
||||||
from app.models import Job, JobInput, JobStatus, TesseractOCRPipelineModel
|
from app.models import Job, JobInput, JobStatus, TesseractOCRPipelineModel
|
||||||
from .schemas import EmptySchema, JobSchema, SpaCyNLPPipelineJobSchema, TesseractOCRPipelineJobSchema, TesseractOCRPipelineModelSchema
|
|
||||||
from .auth import auth_error_responses, token_auth
|
from .auth import auth_error_responses, token_auth
|
||||||
|
from .schemas import EmptySchema, JobSchema, SpaCyNLPPipelineJobSchema, TesseractOCRPipelineJobSchema, TesseractOCRPipelineModelSchema
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint('jobs', __name__)
|
bp = Blueprint('jobs', __name__)
|
||||||
@ -77,7 +77,7 @@ def delete_job(job_id):
|
|||||||
job = Job.query.get(job_id)
|
job = Job.query.get(job_id)
|
||||||
if job is None:
|
if job is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
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)
|
||||||
try:
|
try:
|
||||||
job.delete()
|
job.delete()
|
||||||
@ -97,6 +97,6 @@ def get_job(job_id):
|
|||||||
job = Job.query.get(job_id)
|
job = Job.query.get(job_id)
|
||||||
if job is None:
|
if job is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
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 job
|
return job
|
@ -10,7 +10,7 @@ from app.models import (
|
|||||||
User,
|
User,
|
||||||
UserSettingJobStatusMailNotificationLevel
|
UserSettingJobStatusMailNotificationLevel
|
||||||
)
|
)
|
||||||
from app.services import SERVICES
|
from app.blueprints.services import SERVICES
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -3,11 +3,11 @@ from apifairy import authenticate, body, response
|
|||||||
from apifairy.decorators import other_responses
|
from apifairy.decorators import other_responses
|
||||||
from flask import abort, Blueprint
|
from flask import abort, Blueprint
|
||||||
from werkzeug.exceptions import InternalServerError
|
from werkzeug.exceptions import InternalServerError
|
||||||
from app import db
|
|
||||||
from app.email import create_message, send
|
from app.email import create_message, send
|
||||||
|
from app import db
|
||||||
from app.models import User
|
from app.models import User
|
||||||
from .schemas import EmptySchema, UserSchema
|
|
||||||
from .auth import auth_error_responses, token_auth
|
from .auth import auth_error_responses, token_auth
|
||||||
|
from .schemas import EmptySchema, UserSchema
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint('users', __name__)
|
bp = Blueprint('users', __name__)
|
||||||
@ -60,7 +60,7 @@ def delete_user(user_id):
|
|||||||
user = User.query.get(user_id)
|
user = User.query.get(user_id)
|
||||||
if user is None:
|
if user is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
if not (user == current_user or current_user.is_administrator()):
|
if not (user == current_user or current_user.is_administrator):
|
||||||
abort(403)
|
abort(403)
|
||||||
user.delete()
|
user.delete()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@ -78,7 +78,7 @@ def get_user(user_id):
|
|||||||
user = User.query.get(user_id)
|
user = User.query.get(user_id)
|
||||||
if user is None:
|
if user is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
if not (user == current_user or current_user.is_administrator()):
|
if not (user == current_user or current_user.is_administrator):
|
||||||
abort(403)
|
abort(403)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
@ -94,6 +94,6 @@ def get_user_by_username(username):
|
|||||||
user = User.query.filter(User.username == username).first()
|
user = User.query.filter(User.username == username).first()
|
||||||
if user is None:
|
if user is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
if not (user == current_user or current_user.is_administrator()):
|
if not (user == current_user or current_user.is_administrator):
|
||||||
abort(403)
|
abort(403)
|
||||||
return user
|
return user
|
@ -1,5 +1,4 @@
|
|||||||
from flask import abort, flash, redirect, render_template, request, url_for
|
from flask import abort, flash, redirect, render_template, request, url_for
|
||||||
from flask_breadcrumbs import register_breadcrumb
|
|
||||||
from flask_login import current_user, login_user, login_required, logout_user
|
from flask_login import current_user, login_user, login_required, logout_user
|
||||||
from app import db
|
from app import db
|
||||||
from app.email import create_message, send
|
from app.email import create_message, send
|
||||||
@ -19,7 +18,9 @@ def before_request():
|
|||||||
Checks if a user is unconfirmed when visiting specific sites. Redirects to
|
Checks if a user is unconfirmed when visiting specific sites. Redirects to
|
||||||
unconfirmed view if user is unconfirmed.
|
unconfirmed view if user is unconfirmed.
|
||||||
"""
|
"""
|
||||||
if current_user.is_authenticated:
|
if not current_user.is_authenticated:
|
||||||
|
return
|
||||||
|
|
||||||
current_user.ping()
|
current_user.ping()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
if (not current_user.confirmed
|
if (not current_user.confirmed
|
||||||
@ -27,10 +28,11 @@ def before_request():
|
|||||||
and request.blueprint != 'auth'
|
and request.blueprint != 'auth'
|
||||||
and request.endpoint != 'static'):
|
and request.endpoint != 'static'):
|
||||||
return redirect(url_for('auth.unconfirmed'))
|
return redirect(url_for('auth.unconfirmed'))
|
||||||
|
if not current_user.terms_of_use_accepted:
|
||||||
|
return redirect(url_for('main.terms_of_use'))
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/register', methods=['GET', 'POST'])
|
@bp.route('/register', methods=['GET', 'POST'])
|
||||||
@register_breadcrumb(bp, '.register', 'Register')
|
|
||||||
def register():
|
def register():
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
return redirect(url_for('main.dashboard'))
|
return redirect(url_for('main.dashboard'))
|
||||||
@ -67,7 +69,6 @@ def register():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/login', methods=['GET', 'POST'])
|
@bp.route('/login', methods=['GET', 'POST'])
|
||||||
@register_breadcrumb(bp, '.login', 'Login')
|
|
||||||
def login():
|
def login():
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
return redirect(url_for('main.dashboard'))
|
return redirect(url_for('main.dashboard'))
|
||||||
@ -98,7 +99,6 @@ def logout():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/unconfirmed')
|
@bp.route('/unconfirmed')
|
||||||
@register_breadcrumb(bp, '.unconfirmed', 'Unconfirmed')
|
|
||||||
@login_required
|
@login_required
|
||||||
def unconfirmed():
|
def unconfirmed():
|
||||||
if current_user.confirmed:
|
if current_user.confirmed:
|
||||||
@ -141,7 +141,6 @@ def confirm(token):
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/reset-password-request', methods=['GET', 'POST'])
|
@bp.route('/reset-password-request', methods=['GET', 'POST'])
|
||||||
@register_breadcrumb(bp, '.reset_password_request', 'Password Reset')
|
|
||||||
def reset_password_request():
|
def reset_password_request():
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
return redirect(url_for('main.dashboard'))
|
return redirect(url_for('main.dashboard'))
|
||||||
@ -171,7 +170,6 @@ def reset_password_request():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/reset-password/<token>', methods=['GET', 'POST'])
|
@bp.route('/reset-password/<token>', methods=['GET', 'POST'])
|
||||||
@register_breadcrumb(bp, '.reset_password', 'Password Reset')
|
|
||||||
def reset_password(token):
|
def reset_password(token):
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
return redirect(url_for('main.dashboard'))
|
return redirect(url_for('main.dashboard'))
|
@ -1,9 +1,7 @@
|
|||||||
from flask import redirect, url_for
|
from flask import redirect, url_for
|
||||||
from flask_breadcrumbs import register_breadcrumb
|
|
||||||
from . import bp
|
from . import bp
|
||||||
|
|
||||||
|
|
||||||
@bp.route('')
|
@bp.route('')
|
||||||
@register_breadcrumb(bp, '.', '<i class="material-icons left">new_label</i>My Contributions')
|
|
||||||
def contributions():
|
def contributions():
|
||||||
return redirect(url_for('main.dashboard', _anchor='contributions'))
|
return redirect(url_for('main.dashboard', _anchor='contributions'))
|
@ -1,7 +1,7 @@
|
|||||||
from flask_wtf.file import FileField, FileRequired
|
from flask_wtf.file import FileField, FileRequired
|
||||||
from wtforms import StringField, ValidationError
|
from wtforms import StringField, ValidationError
|
||||||
from wtforms.validators import InputRequired, Length
|
from wtforms.validators import InputRequired, Length
|
||||||
from app.services import SERVICES
|
from app.blueprints.services import SERVICES
|
||||||
from ..forms import ContributionBaseForm, UpdateContributionBaseForm
|
from ..forms import ContributionBaseForm, UpdateContributionBaseForm
|
||||||
|
|
||||||
|
|
@ -4,7 +4,7 @@ from threading import Thread
|
|||||||
from app import db
|
from app import db
|
||||||
from app.decorators import content_negotiation, permission_required
|
from app.decorators import content_negotiation, permission_required
|
||||||
from app.models import SpaCyNLPPipelineModel
|
from app.models import SpaCyNLPPipelineModel
|
||||||
from .. import bp
|
from . import bp
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/spacy-nlp-pipeline-models/<hashid:spacy_nlp_pipeline_model_id>', methods=['DELETE'])
|
@bp.route('/spacy-nlp-pipeline-models/<hashid:spacy_nlp_pipeline_model_id>', methods=['DELETE'])
|
||||||
@ -17,7 +17,7 @@ def delete_spacy_model(spacy_nlp_pipeline_model_id):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
snpm = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id)
|
snpm = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id)
|
||||||
if not (snpm.user == current_user or current_user.is_administrator()):
|
if not (snpm.user == current_user or current_user.is_administrator):
|
||||||
abort(403)
|
abort(403)
|
||||||
thread = Thread(
|
thread = Thread(
|
||||||
target=_delete_spacy_model,
|
target=_delete_spacy_model,
|
||||||
@ -39,7 +39,7 @@ def update_spacy_nlp_pipeline_model_is_public(spacy_nlp_pipeline_model_id):
|
|||||||
if not isinstance(is_public, bool):
|
if not isinstance(is_public, bool):
|
||||||
abort(400)
|
abort(400)
|
||||||
snpm = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id)
|
snpm = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id)
|
||||||
if not (snpm.user == current_user or current_user.is_administrator()):
|
if not (snpm.user == current_user or current_user.is_administrator):
|
||||||
abort(403)
|
abort(403)
|
||||||
snpm.is_public = is_public
|
snpm.is_public = is_public
|
||||||
db.session.commit()
|
db.session.commit()
|
@ -1,5 +1,4 @@
|
|||||||
from flask import abort, flash, redirect, render_template, url_for
|
from flask import abort, flash, redirect, render_template, url_for
|
||||||
from flask_breadcrumbs import register_breadcrumb
|
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import SpaCyNLPPipelineModel
|
from app.models import SpaCyNLPPipelineModel
|
||||||
@ -8,13 +7,9 @@ from .forms import (
|
|||||||
CreateSpaCyNLPPipelineModelForm,
|
CreateSpaCyNLPPipelineModelForm,
|
||||||
UpdateSpaCyNLPPipelineModelForm
|
UpdateSpaCyNLPPipelineModelForm
|
||||||
)
|
)
|
||||||
from .utils import (
|
|
||||||
spacy_nlp_pipeline_model_dlc as spacy_nlp_pipeline_model_dlc
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/spacy-nlp-pipeline-models')
|
@bp.route('/spacy-nlp-pipeline-models')
|
||||||
@register_breadcrumb(bp, '.spacy_nlp_pipeline_models', 'SpaCy NLP Pipeline Models')
|
|
||||||
def spacy_nlp_pipeline_models():
|
def spacy_nlp_pipeline_models():
|
||||||
return render_template(
|
return render_template(
|
||||||
'contributions/spacy_nlp_pipeline_models/spacy_nlp_pipeline_models.html.j2',
|
'contributions/spacy_nlp_pipeline_models/spacy_nlp_pipeline_models.html.j2',
|
||||||
@ -23,7 +18,6 @@ def spacy_nlp_pipeline_models():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/spacy-nlp-pipeline-models/create', methods=['GET', 'POST'])
|
@bp.route('/spacy-nlp-pipeline-models/create', methods=['GET', 'POST'])
|
||||||
@register_breadcrumb(bp, '.spacy_nlp_pipeline_models.create', 'Create')
|
|
||||||
def create_spacy_nlp_pipeline_model():
|
def create_spacy_nlp_pipeline_model():
|
||||||
form = CreateSpaCyNLPPipelineModelForm()
|
form = CreateSpaCyNLPPipelineModelForm()
|
||||||
if form.is_submitted():
|
if form.is_submitted():
|
||||||
@ -57,10 +51,9 @@ def create_spacy_nlp_pipeline_model():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/spacy-nlp-pipeline-models/<hashid:spacy_nlp_pipeline_model_id>', methods=['GET', 'POST'])
|
@bp.route('/spacy-nlp-pipeline-models/<hashid:spacy_nlp_pipeline_model_id>', methods=['GET', 'POST'])
|
||||||
@register_breadcrumb(bp, '.spacy_nlp_pipeline_models.entity', '', dynamic_list_constructor=spacy_nlp_pipeline_model_dlc)
|
|
||||||
def spacy_nlp_pipeline_model(spacy_nlp_pipeline_model_id):
|
def spacy_nlp_pipeline_model(spacy_nlp_pipeline_model_id):
|
||||||
snpm = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id)
|
snpm = SpaCyNLPPipelineModel.query.get_or_404(spacy_nlp_pipeline_model_id)
|
||||||
if not (snpm.user == current_user or current_user.is_administrator()):
|
if not (snpm.user == current_user or current_user.is_administrator):
|
||||||
abort(403)
|
abort(403)
|
||||||
form = UpdateSpaCyNLPPipelineModelForm(data=snpm.to_json_serializeable())
|
form = UpdateSpaCyNLPPipelineModelForm(data=snpm.to_json_serializeable())
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
@ -1,6 +1,6 @@
|
|||||||
from flask_wtf.file import FileField, FileRequired
|
from flask_wtf.file import FileField, FileRequired
|
||||||
from wtforms import ValidationError
|
from wtforms import ValidationError
|
||||||
from app.services import SERVICES
|
from app.blueprints.services import SERVICES
|
||||||
from ..forms import ContributionBaseForm, UpdateContributionBaseForm
|
from ..forms import ContributionBaseForm, UpdateContributionBaseForm
|
||||||
|
|
||||||
|
|
@ -17,7 +17,7 @@ def delete_tesseract_model(tesseract_ocr_pipeline_model_id):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
topm = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id)
|
topm = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id)
|
||||||
if not (topm.user == current_user or current_user.is_administrator()):
|
if not (topm.user == current_user or current_user.is_administrator):
|
||||||
abort(403)
|
abort(403)
|
||||||
thread = Thread(
|
thread = Thread(
|
||||||
target=_delete_tesseract_ocr_pipeline_model,
|
target=_delete_tesseract_ocr_pipeline_model,
|
||||||
@ -39,7 +39,7 @@ def update_tesseract_ocr_pipeline_model_is_public(tesseract_ocr_pipeline_model_i
|
|||||||
if not isinstance(is_public, bool):
|
if not isinstance(is_public, bool):
|
||||||
abort(400)
|
abort(400)
|
||||||
topm = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id)
|
topm = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id)
|
||||||
if not (topm.user == current_user or current_user.is_administrator()):
|
if not (topm.user == current_user or current_user.is_administrator):
|
||||||
abort(403)
|
abort(403)
|
||||||
topm.is_public = is_public
|
topm.is_public = is_public
|
||||||
db.session.commit()
|
db.session.commit()
|
@ -1,5 +1,4 @@
|
|||||||
from flask import abort, flash, redirect, render_template, url_for
|
from flask import abort, flash, redirect, render_template, url_for
|
||||||
from flask_breadcrumbs import register_breadcrumb
|
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import TesseractOCRPipelineModel
|
from app.models import TesseractOCRPipelineModel
|
||||||
@ -8,13 +7,9 @@ from .forms import (
|
|||||||
CreateTesseractOCRPipelineModelForm,
|
CreateTesseractOCRPipelineModelForm,
|
||||||
UpdateTesseractOCRPipelineModelForm
|
UpdateTesseractOCRPipelineModelForm
|
||||||
)
|
)
|
||||||
from .utils import (
|
|
||||||
tesseract_ocr_pipeline_model_dlc as tesseract_ocr_pipeline_model_dlc
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/tesseract-ocr-pipeline-models')
|
@bp.route('/tesseract-ocr-pipeline-models')
|
||||||
@register_breadcrumb(bp, '.tesseract_ocr_pipeline_models', 'Tesseract OCR Pipeline Models')
|
|
||||||
def tesseract_ocr_pipeline_models():
|
def tesseract_ocr_pipeline_models():
|
||||||
return render_template(
|
return render_template(
|
||||||
'contributions/tesseract_ocr_pipeline_models/tesseract_ocr_pipeline_models.html.j2',
|
'contributions/tesseract_ocr_pipeline_models/tesseract_ocr_pipeline_models.html.j2',
|
||||||
@ -23,7 +18,6 @@ def tesseract_ocr_pipeline_models():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/tesseract-ocr-pipeline-models/create', methods=['GET', 'POST'])
|
@bp.route('/tesseract-ocr-pipeline-models/create', methods=['GET', 'POST'])
|
||||||
@register_breadcrumb(bp, '.tesseract_ocr_pipeline_models.create', 'Create')
|
|
||||||
def create_tesseract_ocr_pipeline_model():
|
def create_tesseract_ocr_pipeline_model():
|
||||||
form = CreateTesseractOCRPipelineModelForm()
|
form = CreateTesseractOCRPipelineModelForm()
|
||||||
if form.is_submitted():
|
if form.is_submitted():
|
||||||
@ -56,10 +50,9 @@ def create_tesseract_ocr_pipeline_model():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/tesseract-ocr-pipeline-models/<hashid:tesseract_ocr_pipeline_model_id>', methods=['GET', 'POST'])
|
@bp.route('/tesseract-ocr-pipeline-models/<hashid:tesseract_ocr_pipeline_model_id>', methods=['GET', 'POST'])
|
||||||
@register_breadcrumb(bp, '.tesseract_ocr_pipeline_models.entity', '', dynamic_list_constructor=tesseract_ocr_pipeline_model_dlc)
|
|
||||||
def tesseract_ocr_pipeline_model(tesseract_ocr_pipeline_model_id):
|
def tesseract_ocr_pipeline_model(tesseract_ocr_pipeline_model_id):
|
||||||
topm = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id)
|
topm = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id)
|
||||||
if not (topm.user == current_user or current_user.is_administrator()):
|
if not (topm.user == current_user or current_user.is_administrator):
|
||||||
abort(403)
|
abort(403)
|
||||||
form = UpdateTesseractOCRPipelineModelForm(data=topm.to_json_serializeable())
|
form = UpdateTesseractOCRPipelineModelForm(data=topm.to_json_serializeable())
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
@ -7,7 +7,7 @@ from flask_login import current_user
|
|||||||
from flask_socketio import Namespace
|
from flask_socketio import Namespace
|
||||||
from inspect import signature
|
from inspect import signature
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
from typing import Callable, Dict, List, Optional
|
from typing import Callable
|
||||||
from app import db, docker_client, hashids, socketio
|
from app import db, docker_client, hashids, socketio
|
||||||
from app.decorators import socketio_login_required
|
from app.decorators import socketio_login_required
|
||||||
from app.models import Corpus, CorpusStatus
|
from app.models import Corpus, CorpusStatus
|
||||||
@ -19,7 +19,7 @@ This package tunnels the Corpus Query interface (CQi) protocol through
|
|||||||
Socket.IO (SIO) by tunneling CQi API calls through an event called "exec".
|
Socket.IO (SIO) by tunneling CQi API calls through an event called "exec".
|
||||||
|
|
||||||
Basic concept:
|
Basic concept:
|
||||||
1. A client connects to the "/cqi_over_sio" namespace.
|
1. A client connects to the namespace.
|
||||||
2. The client emits the "init" event and provides a corpus id for the corpus
|
2. The client emits the "init" event and provides a corpus id for the corpus
|
||||||
that should be analysed in this session.
|
that should be analysed in this session.
|
||||||
1.1 The analysis session counter of the corpus is incremented.
|
1.1 The analysis session counter of the corpus is incremented.
|
||||||
@ -28,17 +28,17 @@ Basic concept:
|
|||||||
1.4 Connect the CQiClient to the server.
|
1.4 Connect the CQiClient to the server.
|
||||||
1.5 Save the CQiClient, the Lock and the corpus id in the session for
|
1.5 Save the CQiClient, the Lock and the corpus id in the session for
|
||||||
subsequential use.
|
subsequential use.
|
||||||
2. The client emits the "exec" event provides the name of a CQi API function
|
3. The client emits "exec" events, within which it provides the name of a CQi
|
||||||
arguments (optional).
|
API function and the corresponding arguments.
|
||||||
- The event "exec" handler will execute the function, make sure that the
|
3.1 The "exec" event handler will execute the function, make sure that
|
||||||
result is serializable and returns the result back to the client.
|
the result is serializable and returns the result back to the client.
|
||||||
4. Wait for more events
|
4. The client disconnects from the namespace
|
||||||
5. The client disconnects from the "/cqi_over_sio" namespace
|
4.1 The analysis session counter of the corpus is decremented.
|
||||||
1.1 The analysis session counter of the corpus is decremented.
|
4.2 The CQiClient and (Mutex) Lock belonging to it are teared down.
|
||||||
1.2 The CQiClient and (Mutex) Lock belonging to it are teared down.
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
CQI_API_FUNCTION_NAMES: List[str] = [
|
|
||||||
|
CQI_API_FUNCTION_NAMES: list[str] = [
|
||||||
'ask_feature_cl_2_3',
|
'ask_feature_cl_2_3',
|
||||||
'ask_feature_cqi_1_0',
|
'ask_feature_cqi_1_0',
|
||||||
'ask_feature_cqp_2_3',
|
'ask_feature_cqp_2_3',
|
||||||
@ -86,7 +86,7 @@ CQI_API_FUNCTION_NAMES: List[str] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class CQiNamespace(Namespace):
|
class CQiOverSocketIO(Namespace):
|
||||||
@socketio_login_required
|
@socketio_login_required
|
||||||
def on_connect(self):
|
def on_connect(self):
|
||||||
pass
|
pass
|
||||||
@ -94,12 +94,12 @@ class CQiNamespace(Namespace):
|
|||||||
@socketio_login_required
|
@socketio_login_required
|
||||||
def on_init(self, db_corpus_hashid: str):
|
def on_init(self, db_corpus_hashid: str):
|
||||||
db_corpus_id: int = hashids.decode(db_corpus_hashid)
|
db_corpus_id: int = hashids.decode(db_corpus_hashid)
|
||||||
db_corpus: Optional[Corpus] = Corpus.query.get(db_corpus_id)
|
db_corpus: Corpus | None = Corpus.query.get(db_corpus_id)
|
||||||
if db_corpus is None:
|
if db_corpus is None:
|
||||||
return {'code': 404, 'msg': 'Not Found'}
|
return {'code': 404, 'msg': 'Not Found'}
|
||||||
if not (db_corpus.user == current_user
|
if not (db_corpus.user == current_user
|
||||||
or current_user.is_following_corpus(db_corpus)
|
or current_user.is_following_corpus(db_corpus)
|
||||||
or current_user.is_administrator()):
|
or current_user.is_administrator):
|
||||||
return {'code': 403, 'msg': 'Forbidden'}
|
return {'code': 403, 'msg': 'Forbidden'}
|
||||||
if db_corpus.status not in [
|
if db_corpus.status not in [
|
||||||
CorpusStatus.BUILT,
|
CorpusStatus.BUILT,
|
||||||
@ -135,7 +135,7 @@ class CQiNamespace(Namespace):
|
|||||||
return {'code': 200, 'msg': 'OK'}
|
return {'code': 200, 'msg': 'OK'}
|
||||||
|
|
||||||
@socketio_login_required
|
@socketio_login_required
|
||||||
def on_exec(self, fn_name: str, fn_args: Dict = {}):
|
def on_exec(self, fn_name: str, fn_args: dict = {}):
|
||||||
try:
|
try:
|
||||||
cqi_client: CQiClient = session['cqi_over_sio']['cqi_client']
|
cqi_client: CQiClient = session['cqi_over_sio']['cqi_client']
|
||||||
cqi_client_lock: Lock = session['cqi_over_sio']['cqi_client_lock']
|
cqi_client_lock: Lock = session['cqi_over_sio']['cqi_client_lock']
|
||||||
@ -199,7 +199,7 @@ class CQiNamespace(Namespace):
|
|||||||
except (BrokenPipeError, CQiException):
|
except (BrokenPipeError, CQiException):
|
||||||
pass
|
pass
|
||||||
cqi_client_lock.release()
|
cqi_client_lock.release()
|
||||||
db_corpus: Optional[Corpus] = Corpus.query.get(db_corpus_id)
|
db_corpus: Corpus | None = Corpus.query.get(db_corpus_id)
|
||||||
if db_corpus is None:
|
if db_corpus is None:
|
||||||
return
|
return
|
||||||
db_corpus.num_analysis_sessions = Corpus.num_analysis_sessions - 1
|
db_corpus.num_analysis_sessions = Corpus.num_analysis_sessions - 1
|
@ -8,7 +8,6 @@ from cqi.models.attributes import (
|
|||||||
)
|
)
|
||||||
from cqi.status import StatusOk as CQiStatusOk
|
from cqi.status import StatusOk as CQiStatusOk
|
||||||
from flask import session
|
from flask import session
|
||||||
from typing import Dict, List
|
|
||||||
import gzip
|
import gzip
|
||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
@ -17,7 +16,7 @@ from app.models import Corpus
|
|||||||
from .utils import lookups_by_cpos, partial_export_subcorpus, export_subcorpus
|
from .utils import lookups_by_cpos, partial_export_subcorpus, export_subcorpus
|
||||||
|
|
||||||
|
|
||||||
CQI_EXTENSION_FUNCTION_NAMES: List[str] = [
|
CQI_EXTENSION_FUNCTION_NAMES: list[str] = [
|
||||||
'ext_corpus_update_db',
|
'ext_corpus_update_db',
|
||||||
'ext_corpus_static_data',
|
'ext_corpus_static_data',
|
||||||
'ext_corpus_paginate_corpus',
|
'ext_corpus_paginate_corpus',
|
||||||
@ -37,7 +36,7 @@ def ext_corpus_update_db(corpus: str) -> CQiStatusOk:
|
|||||||
return CQiStatusOk()
|
return CQiStatusOk()
|
||||||
|
|
||||||
|
|
||||||
def ext_corpus_static_data(corpus: str) -> Dict:
|
def ext_corpus_static_data(corpus: str) -> dict:
|
||||||
db_corpus_id: int = session['cqi_over_sio']['db_corpus_id']
|
db_corpus_id: int = session['cqi_over_sio']['db_corpus_id']
|
||||||
db_corpus: Corpus = Corpus.query.get(db_corpus_id)
|
db_corpus: Corpus = Corpus.query.get(db_corpus_id)
|
||||||
|
|
||||||
@ -48,8 +47,8 @@ def ext_corpus_static_data(corpus: str) -> Dict:
|
|||||||
|
|
||||||
cqi_client: CQiClient = session['cqi_over_sio']['cqi_client']
|
cqi_client: CQiClient = session['cqi_over_sio']['cqi_client']
|
||||||
cqi_corpus: CQiCorpus = cqi_client.corpora.get(corpus)
|
cqi_corpus: CQiCorpus = cqi_client.corpora.get(corpus)
|
||||||
cqi_p_attrs: List[CQiPositionalAttribute] = cqi_corpus.positional_attributes.list()
|
cqi_p_attrs: list[CQiPositionalAttribute] = cqi_corpus.positional_attributes.list()
|
||||||
cqi_s_attrs: List[CQiStructuralAttribute] = cqi_corpus.structural_attributes.list()
|
cqi_s_attrs: list[CQiStructuralAttribute] = cqi_corpus.structural_attributes.list()
|
||||||
|
|
||||||
static_data = {
|
static_data = {
|
||||||
'corpus': {
|
'corpus': {
|
||||||
@ -64,19 +63,19 @@ def ext_corpus_static_data(corpus: str) -> Dict:
|
|||||||
for p_attr in cqi_p_attrs:
|
for p_attr in cqi_p_attrs:
|
||||||
print(f'corpus.freqs.{p_attr.name}')
|
print(f'corpus.freqs.{p_attr.name}')
|
||||||
static_data['corpus']['freqs'][p_attr.name] = []
|
static_data['corpus']['freqs'][p_attr.name] = []
|
||||||
p_attr_id_list: List[int] = list(range(p_attr.lexicon_size))
|
p_attr_id_list: list[int] = list(range(p_attr.lexicon_size))
|
||||||
static_data['corpus']['freqs'][p_attr.name].extend(p_attr.freqs_by_ids(p_attr_id_list))
|
static_data['corpus']['freqs'][p_attr.name].extend(p_attr.freqs_by_ids(p_attr_id_list))
|
||||||
del p_attr_id_list
|
del p_attr_id_list
|
||||||
|
|
||||||
print(f'p_attrs.{p_attr.name}')
|
print(f'p_attrs.{p_attr.name}')
|
||||||
static_data['p_attrs'][p_attr.name] = []
|
static_data['p_attrs'][p_attr.name] = []
|
||||||
cpos_list: List[int] = list(range(cqi_corpus.size))
|
cpos_list: list[int] = list(range(cqi_corpus.size))
|
||||||
static_data['p_attrs'][p_attr.name].extend(p_attr.ids_by_cpos(cpos_list))
|
static_data['p_attrs'][p_attr.name].extend(p_attr.ids_by_cpos(cpos_list))
|
||||||
del cpos_list
|
del cpos_list
|
||||||
|
|
||||||
print(f'values.p_attrs.{p_attr.name}')
|
print(f'values.p_attrs.{p_attr.name}')
|
||||||
static_data['values']['p_attrs'][p_attr.name] = []
|
static_data['values']['p_attrs'][p_attr.name] = []
|
||||||
p_attr_id_list: List[int] = list(range(p_attr.lexicon_size))
|
p_attr_id_list: list[int] = list(range(p_attr.lexicon_size))
|
||||||
static_data['values']['p_attrs'][p_attr.name].extend(p_attr.values_by_ids(p_attr_id_list))
|
static_data['values']['p_attrs'][p_attr.name].extend(p_attr.values_by_ids(p_attr_id_list))
|
||||||
del p_attr_id_list
|
del p_attr_id_list
|
||||||
|
|
||||||
@ -127,23 +126,23 @@ def ext_corpus_static_data(corpus: str) -> Dict:
|
|||||||
print(f's_attrs.{s_attr.name}.lexicon.{id}.bounds')
|
print(f's_attrs.{s_attr.name}.lexicon.{id}.bounds')
|
||||||
static_data['s_attrs'][s_attr.name]['lexicon'][id]['bounds'] = [lbound, rbound]
|
static_data['s_attrs'][s_attr.name]['lexicon'][id]['bounds'] = [lbound, rbound]
|
||||||
static_data['s_attrs'][s_attr.name]['lexicon'][id]['freqs'] = {}
|
static_data['s_attrs'][s_attr.name]['lexicon'][id]['freqs'] = {}
|
||||||
cpos_list: List[int] = list(range(lbound, rbound + 1))
|
cpos_list: list[int] = list(range(lbound, rbound + 1))
|
||||||
for p_attr in cqi_p_attrs:
|
for p_attr in cqi_p_attrs:
|
||||||
p_attr_ids: List[int] = []
|
p_attr_ids: list[int] = []
|
||||||
p_attr_ids.extend(p_attr.ids_by_cpos(cpos_list))
|
p_attr_ids.extend(p_attr.ids_by_cpos(cpos_list))
|
||||||
print(f's_attrs.{s_attr.name}.lexicon.{id}.freqs.{p_attr.name}')
|
print(f's_attrs.{s_attr.name}.lexicon.{id}.freqs.{p_attr.name}')
|
||||||
static_data['s_attrs'][s_attr.name]['lexicon'][id]['freqs'][p_attr.name] = dict(Counter(p_attr_ids))
|
static_data['s_attrs'][s_attr.name]['lexicon'][id]['freqs'][p_attr.name] = dict(Counter(p_attr_ids))
|
||||||
del p_attr_ids
|
del p_attr_ids
|
||||||
del cpos_list
|
del cpos_list
|
||||||
|
|
||||||
sub_s_attrs: List[CQiStructuralAttribute] = cqi_corpus.structural_attributes.list(filters={'part_of': s_attr})
|
sub_s_attrs: list[CQiStructuralAttribute] = cqi_corpus.structural_attributes.list(filters={'part_of': s_attr})
|
||||||
print(f's_attrs.{s_attr.name}.values')
|
print(f's_attrs.{s_attr.name}.values')
|
||||||
static_data['s_attrs'][s_attr.name]['values'] = [
|
static_data['s_attrs'][s_attr.name]['values'] = [
|
||||||
sub_s_attr.name[(len(s_attr.name) + 1):]
|
sub_s_attr.name[(len(s_attr.name) + 1):]
|
||||||
for sub_s_attr in sub_s_attrs
|
for sub_s_attr in sub_s_attrs
|
||||||
]
|
]
|
||||||
s_attr_id_list: List[int] = list(range(s_attr.size))
|
s_attr_id_list: list[int] = list(range(s_attr.size))
|
||||||
sub_s_attr_values: List[str] = []
|
sub_s_attr_values: list[str] = []
|
||||||
for sub_s_attr in sub_s_attrs:
|
for sub_s_attr in sub_s_attrs:
|
||||||
tmp = []
|
tmp = []
|
||||||
tmp.extend(sub_s_attr.values_by_ids(s_attr_id_list))
|
tmp.extend(sub_s_attr.values_by_ids(s_attr_id_list))
|
||||||
@ -173,7 +172,7 @@ def ext_corpus_paginate_corpus(
|
|||||||
corpus: str,
|
corpus: str,
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
per_page: int = 20
|
per_page: int = 20
|
||||||
) -> Dict:
|
) -> dict:
|
||||||
cqi_client: CQiClient = session['cqi_over_sio']['cqi_client']
|
cqi_client: CQiClient = session['cqi_over_sio']['cqi_client']
|
||||||
cqi_corpus = cqi_client.corpora.get(corpus)
|
cqi_corpus = cqi_client.corpora.get(corpus)
|
||||||
# Sanity checks
|
# Sanity checks
|
||||||
@ -219,7 +218,7 @@ def ext_cqp_paginate_subcorpus(
|
|||||||
context: int = 50,
|
context: int = 50,
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
per_page: int = 20
|
per_page: int = 20
|
||||||
) -> Dict:
|
) -> dict:
|
||||||
corpus_name, subcorpus_name = subcorpus.split(':', 1)
|
corpus_name, subcorpus_name = subcorpus.split(':', 1)
|
||||||
cqi_client: CQiClient = session['cqi_over_sio']['cqi_client']
|
cqi_client: CQiClient = session['cqi_over_sio']['cqi_client']
|
||||||
cqi_corpus = cqi_client.corpora.get(corpus_name)
|
cqi_corpus = cqi_client.corpora.get(corpus_name)
|
||||||
@ -266,7 +265,7 @@ def ext_cqp_partial_export_subcorpus(
|
|||||||
subcorpus: str,
|
subcorpus: str,
|
||||||
match_id_list: list,
|
match_id_list: list,
|
||||||
context: int = 50
|
context: int = 50
|
||||||
) -> Dict:
|
) -> dict:
|
||||||
corpus_name, subcorpus_name = subcorpus.split(':', 1)
|
corpus_name, subcorpus_name = subcorpus.split(':', 1)
|
||||||
cqi_client: CQiClient = session['cqi_over_sio']['cqi_client']
|
cqi_client: CQiClient = session['cqi_over_sio']['cqi_client']
|
||||||
cqi_corpus = cqi_client.corpora.get(corpus_name)
|
cqi_corpus = cqi_client.corpora.get(corpus_name)
|
||||||
@ -278,7 +277,7 @@ def ext_cqp_partial_export_subcorpus(
|
|||||||
def ext_cqp_export_subcorpus(
|
def ext_cqp_export_subcorpus(
|
||||||
subcorpus: str,
|
subcorpus: str,
|
||||||
context: int = 50
|
context: int = 50
|
||||||
) -> Dict:
|
) -> dict:
|
||||||
corpus_name, subcorpus_name = subcorpus.split(':', 1)
|
corpus_name, subcorpus_name = subcorpus.split(':', 1)
|
||||||
cqi_client: CQiClient = session['cqi_over_sio']['cqi_client']
|
cqi_client: CQiClient = session['cqi_over_sio']['cqi_client']
|
||||||
cqi_corpus = cqi_client.corpora.get(corpus_name)
|
cqi_corpus = cqi_client.corpora.get(corpus_name)
|
@ -1,13 +1,12 @@
|
|||||||
from cqi.models.corpora import Corpus as CQiCorpus
|
from cqi.models.corpora import Corpus as CQiCorpus
|
||||||
from cqi.models.subcorpora import Subcorpus as CQiSubcorpus
|
from cqi.models.subcorpora import Subcorpus as CQiSubcorpus
|
||||||
from typing import Dict, List
|
|
||||||
|
|
||||||
|
|
||||||
def lookups_by_cpos(corpus: CQiCorpus, cpos_list: List[int]) -> Dict:
|
def lookups_by_cpos(corpus: CQiCorpus, cpos_list: list[int]) -> dict:
|
||||||
lookups = {}
|
lookups = {}
|
||||||
lookups['cpos_lookup'] = {cpos: {} for cpos in cpos_list}
|
lookups['cpos_lookup'] = {cpos: {} for cpos in cpos_list}
|
||||||
for attr in corpus.positional_attributes.list():
|
for attr in corpus.positional_attributes.list():
|
||||||
cpos_attr_values: List[str] = attr.values_by_cpos(cpos_list)
|
cpos_attr_values: list[str] = attr.values_by_cpos(cpos_list)
|
||||||
for i, cpos in enumerate(cpos_list):
|
for i, cpos in enumerate(cpos_list):
|
||||||
lookups['cpos_lookup'][cpos][attr.name] = cpos_attr_values[i]
|
lookups['cpos_lookup'][cpos][attr.name] = cpos_attr_values[i]
|
||||||
for attr in corpus.structural_attributes.list():
|
for attr in corpus.structural_attributes.list():
|
||||||
@ -15,7 +14,7 @@ def lookups_by_cpos(corpus: CQiCorpus, cpos_list: List[int]) -> Dict:
|
|||||||
# attr.has_values == False
|
# attr.has_values == False
|
||||||
if attr.has_values:
|
if attr.has_values:
|
||||||
continue
|
continue
|
||||||
cpos_attr_ids: List[int] = attr.ids_by_cpos(cpos_list)
|
cpos_attr_ids: list[int] = attr.ids_by_cpos(cpos_list)
|
||||||
for i, cpos in enumerate(cpos_list):
|
for i, cpos in enumerate(cpos_list):
|
||||||
if cpos_attr_ids[i] == -1:
|
if cpos_attr_ids[i] == -1:
|
||||||
continue
|
continue
|
||||||
@ -39,9 +38,9 @@ def lookups_by_cpos(corpus: CQiCorpus, cpos_list: List[int]) -> Dict:
|
|||||||
|
|
||||||
def partial_export_subcorpus(
|
def partial_export_subcorpus(
|
||||||
subcorpus: CQiSubcorpus,
|
subcorpus: CQiSubcorpus,
|
||||||
match_id_list: List[int],
|
match_id_list: list[int],
|
||||||
context: int = 25
|
context: int = 25
|
||||||
) -> Dict:
|
) -> dict:
|
||||||
if subcorpus.size == 0:
|
if subcorpus.size == 0:
|
||||||
return {"matches": []}
|
return {"matches": []}
|
||||||
match_boundaries = []
|
match_boundaries = []
|
||||||
@ -91,7 +90,7 @@ def export_subcorpus(
|
|||||||
context: int = 25,
|
context: int = 25,
|
||||||
cutoff: float = float('inf'),
|
cutoff: float = float('inf'),
|
||||||
offset: int = 0
|
offset: int = 0
|
||||||
) -> Dict:
|
) -> dict:
|
||||||
if subcorpus.size == 0:
|
if subcorpus.size == 0:
|
||||||
return {"matches": []}
|
return {"matches": []}
|
||||||
first_match = max(0, offset)
|
first_match = max(0, offset)
|
@ -10,7 +10,7 @@ def corpus_follower_permission_required(*permissions):
|
|||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
corpus_id = kwargs.get('corpus_id')
|
corpus_id = kwargs.get('corpus_id')
|
||||||
corpus = Corpus.query.get_or_404(corpus_id)
|
corpus = Corpus.query.get_or_404(corpus_id)
|
||||||
if not (corpus.user == current_user or current_user.is_administrator()):
|
if not (corpus.user == current_user or current_user.is_administrator):
|
||||||
cfa = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=current_user.id).first()
|
cfa = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=current_user.id).first()
|
||||||
if cfa is None:
|
if cfa is None:
|
||||||
abort(403)
|
abort(403)
|
||||||
@ -26,7 +26,7 @@ def corpus_owner_or_admin_required(f):
|
|||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
corpus_id = kwargs.get('corpus_id')
|
corpus_id = kwargs.get('corpus_id')
|
||||||
corpus = Corpus.query.get_or_404(corpus_id)
|
corpus = Corpus.query.get_or_404(corpus_id)
|
||||||
if not (corpus.user == current_user or current_user.is_administrator()):
|
if not (corpus.user == current_user or current_user.is_administrator):
|
||||||
abort(403)
|
abort(403)
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
return decorated_function
|
return decorated_function
|
@ -15,7 +15,7 @@ def get_corpus(corpus_hashid):
|
|||||||
if not (
|
if not (
|
||||||
corpus.is_public
|
corpus.is_public
|
||||||
or corpus.user == current_user
|
or corpus.user == current_user
|
||||||
or current_user.is_administrator()
|
or current_user.is_administrator
|
||||||
):
|
):
|
||||||
return {'options': {'status': 403, 'statusText': 'Forbidden'}}
|
return {'options': {'status': 403, 'statusText': 'Forbidden'}}
|
||||||
return {
|
return {
|
||||||
@ -38,7 +38,7 @@ def subscribe_corpus(corpus_hashid):
|
|||||||
if not (
|
if not (
|
||||||
corpus.is_public
|
corpus.is_public
|
||||||
or corpus.user == current_user
|
or corpus.user == current_user
|
||||||
or current_user.is_administrator()
|
or current_user.is_administrator
|
||||||
):
|
):
|
||||||
return {'options': {'status': 403, 'statusText': 'Forbidden'}}
|
return {'options': {'status': 403, 'statusText': 'Forbidden'}}
|
||||||
join_room(f'/corpora/{corpus.hashid}')
|
join_room(f'/corpora/{corpus.hashid}')
|
@ -1,7 +1,7 @@
|
|||||||
from flask import abort, current_app
|
from flask import current_app
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from app import db
|
|
||||||
from app.decorators import content_negotiation
|
from app.decorators import content_negotiation
|
||||||
|
from app import db
|
||||||
from app.models import CorpusFile
|
from app.models import CorpusFile
|
||||||
from ..decorators import corpus_follower_permission_required
|
from ..decorators import corpus_follower_permission_required
|
||||||
from . import bp
|
from . import bp
|
@ -6,24 +6,19 @@ from flask import (
|
|||||||
send_from_directory,
|
send_from_directory,
|
||||||
url_for
|
url_for
|
||||||
)
|
)
|
||||||
from flask_breadcrumbs import register_breadcrumb
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import Corpus, CorpusFile, CorpusStatus
|
from app.models import Corpus, CorpusFile, CorpusStatus
|
||||||
from ..decorators import corpus_follower_permission_required
|
from ..decorators import corpus_follower_permission_required
|
||||||
from ..utils import corpus_endpoint_arguments_constructor as corpus_eac
|
|
||||||
from . import bp
|
from . import bp
|
||||||
from .forms import CreateCorpusFileForm, UpdateCorpusFileForm
|
from .forms import CreateCorpusFileForm, UpdateCorpusFileForm
|
||||||
from .utils import corpus_file_dynamic_list_constructor as corpus_file_dlc
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:corpus_id>/files')
|
@bp.route('/<hashid:corpus_id>/files')
|
||||||
@register_breadcrumb(bp, '.entity.files', 'Files', endpoint_arguments_constructor=corpus_eac)
|
|
||||||
def corpus_files(corpus_id):
|
def corpus_files(corpus_id):
|
||||||
return redirect(url_for('.corpus', _anchor='files', corpus_id=corpus_id))
|
return redirect(url_for('.corpus', _anchor='files', corpus_id=corpus_id))
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:corpus_id>/files/create', methods=['GET', 'POST'])
|
@bp.route('/<hashid:corpus_id>/files/create', methods=['GET', 'POST'])
|
||||||
@register_breadcrumb(bp, '.entity.files.create', 'Create', endpoint_arguments_constructor=corpus_eac)
|
|
||||||
@corpus_follower_permission_required('MANAGE_FILES')
|
@corpus_follower_permission_required('MANAGE_FILES')
|
||||||
def create_corpus_file(corpus_id):
|
def create_corpus_file(corpus_id):
|
||||||
corpus = Corpus.query.get_or_404(corpus_id)
|
corpus = Corpus.query.get_or_404(corpus_id)
|
||||||
@ -65,7 +60,6 @@ def create_corpus_file(corpus_id):
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>', methods=['GET', 'POST'])
|
@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>', methods=['GET', 'POST'])
|
||||||
@register_breadcrumb(bp, '.entity.files.entity', '', dynamic_list_constructor=corpus_file_dlc)
|
|
||||||
@corpus_follower_permission_required('MANAGE_FILES')
|
@corpus_follower_permission_required('MANAGE_FILES')
|
||||||
def corpus_file(corpus_id, corpus_file_id):
|
def corpus_file(corpus_id, corpus_file_id):
|
||||||
corpus_file = CorpusFile.query.filter_by(corpus_id=corpus_id, id=corpus_file_id).first_or_404()
|
corpus_file = CorpusFile.query.filter_by(corpus_id=corpus_id, id=corpus_file_id).first_or_404()
|
||||||
@ -94,6 +88,6 @@ def download_corpus_file(corpus_id, corpus_file_id):
|
|||||||
corpus_file.path.parent,
|
corpus_file.path.parent,
|
||||||
corpus_file.path.name,
|
corpus_file.path.name,
|
||||||
as_attachment=True,
|
as_attachment=True,
|
||||||
attachment_filename=corpus_file.filename,
|
download_name=corpus_file.filename,
|
||||||
mimetype=corpus_file.mimetype
|
mimetype=corpus_file.mimetype
|
||||||
)
|
)
|
@ -58,7 +58,7 @@ def delete_corpus_follower(corpus_id, follower_id):
|
|||||||
current_user.id == follower_id
|
current_user.id == follower_id
|
||||||
or current_user == cfa.corpus.user
|
or current_user == cfa.corpus.user
|
||||||
or CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=current_user.id).first().role.has_permission('MANAGE_FOLLOWERS')
|
or CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=current_user.id).first().role.has_permission('MANAGE_FOLLOWERS')
|
||||||
or current_user.is_administrator()):
|
or current_user.is_administrator):
|
||||||
abort(403)
|
abort(403)
|
||||||
if current_user.id == follower_id:
|
if current_user.id == follower_id:
|
||||||
flash(f'You are no longer following "{cfa.corpus.title}"', 'corpus')
|
flash(f'You are no longer following "{cfa.corpus.title}"', 'corpus')
|
@ -1,5 +1,4 @@
|
|||||||
from flask import abort, flash, redirect, render_template, url_for
|
from flask import abort, flash, redirect, render_template, url_for
|
||||||
from flask_breadcrumbs import register_breadcrumb
|
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import (
|
from app.models import (
|
||||||
@ -11,20 +10,14 @@ from app.models import (
|
|||||||
from . import bp
|
from . import bp
|
||||||
from .decorators import corpus_follower_permission_required
|
from .decorators import corpus_follower_permission_required
|
||||||
from .forms import CreateCorpusForm
|
from .forms import CreateCorpusForm
|
||||||
from .utils import (
|
|
||||||
corpus_endpoint_arguments_constructor as corpus_eac,
|
|
||||||
corpus_dynamic_list_constructor as corpus_dlc
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('')
|
@bp.route('')
|
||||||
@register_breadcrumb(bp, '.', '<i class="nopaque-icons left">I</i>My Corpora')
|
|
||||||
def corpora():
|
def corpora():
|
||||||
return redirect(url_for('main.dashboard', _anchor='corpora'))
|
return redirect(url_for('main.dashboard', _anchor='corpora'))
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/create', methods=['GET', 'POST'])
|
@bp.route('/create', methods=['GET', 'POST'])
|
||||||
@register_breadcrumb(bp, '.create', 'Create')
|
|
||||||
def create_corpus():
|
def create_corpus():
|
||||||
form = CreateCorpusForm()
|
form = CreateCorpusForm()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
@ -47,7 +40,6 @@ def create_corpus():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:corpus_id>')
|
@bp.route('/<hashid:corpus_id>')
|
||||||
@register_breadcrumb(bp, '.entity', '', dynamic_list_constructor=corpus_dlc)
|
|
||||||
def corpus(corpus_id):
|
def corpus(corpus_id):
|
||||||
corpus = Corpus.query.get_or_404(corpus_id)
|
corpus = Corpus.query.get_or_404(corpus_id)
|
||||||
cfrs = CorpusFollowerRole.query.all()
|
cfrs = CorpusFollowerRole.query.all()
|
||||||
@ -55,13 +47,13 @@ def corpus(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()
|
users = User.query.filter(User.is_public == True, User.id != current_user.id, User.id != corpus.user.id, User.role_id < 4).all()
|
||||||
cfa = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=current_user.id).first()
|
cfa = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=current_user.id).first()
|
||||||
if cfa is None:
|
if 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()
|
||||||
else:
|
else:
|
||||||
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():
|
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,
|
||||||
@ -87,7 +79,6 @@ def corpus(corpus_id):
|
|||||||
|
|
||||||
@bp.route('/<hashid:corpus_id>/analysis')
|
@bp.route('/<hashid:corpus_id>/analysis')
|
||||||
@corpus_follower_permission_required('VIEW')
|
@corpus_follower_permission_required('VIEW')
|
||||||
@register_breadcrumb(bp, '.entity.analysis', 'Analysis', endpoint_arguments_constructor=corpus_eac)
|
|
||||||
def analysis(corpus_id):
|
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(
|
||||||
@ -108,13 +99,11 @@ def follow_corpus(corpus_id, token):
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/import', methods=['GET', 'POST'])
|
@bp.route('/import', methods=['GET', 'POST'])
|
||||||
@register_breadcrumb(bp, '.import', 'Import')
|
|
||||||
def import_corpus():
|
def import_corpus():
|
||||||
abort(503)
|
abort(503)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:corpus_id>/export')
|
@bp.route('/<hashid:corpus_id>/export')
|
||||||
@corpus_follower_permission_required('VIEW')
|
@corpus_follower_permission_required('VIEW')
|
||||||
@register_breadcrumb(bp, '.entity.export', 'Export', endpoint_arguments_constructor=corpus_eac)
|
|
||||||
def export_corpus(corpus_id):
|
def export_corpus(corpus_id):
|
||||||
abort(503)
|
abort(503)
|
18
app/blueprints/jobs/__init__.py
Normal file
18
app/blueprints/jobs/__init__.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
from flask_login import login_required
|
||||||
|
|
||||||
|
|
||||||
|
bp = Blueprint('jobs', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.before_request
|
||||||
|
@login_required
|
||||||
|
def before_request():
|
||||||
|
'''
|
||||||
|
Ensures that the routes in this package can only be visited by users that
|
||||||
|
are logged in.
|
||||||
|
'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
from . import routes, json_routes
|
138
app/blueprints/jobs/events.py
Normal file
138
app/blueprints/jobs/events.py
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
from flask import current_app
|
||||||
|
from flask_login import current_user
|
||||||
|
from flask_socketio import Namespace
|
||||||
|
from app import db, hashids, socketio
|
||||||
|
from app.extensions.flask_socketio import admin_required, login_required
|
||||||
|
from app.models import Job, JobStatus
|
||||||
|
|
||||||
|
|
||||||
|
class JobsNamespace(Namespace):
|
||||||
|
@login_required
|
||||||
|
def on_delete(self, job_hashid: str):
|
||||||
|
# Decode the job hashid
|
||||||
|
job_id = hashids.decode(job_hashid)
|
||||||
|
|
||||||
|
# Validate job_id
|
||||||
|
if not isinstance(job_id, int):
|
||||||
|
return {
|
||||||
|
'code': 400,
|
||||||
|
'body': 'job_id is invalid'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Load job from database
|
||||||
|
job = Job.query.get(job_id)
|
||||||
|
if job is None:
|
||||||
|
return {
|
||||||
|
'code': 404,
|
||||||
|
'body': 'Job not found'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if the current user is allowed to delete the job
|
||||||
|
if not (job.user == current_user or current_user.is_administrator):
|
||||||
|
return {
|
||||||
|
'code': 403,
|
||||||
|
'body': 'Forbidden'
|
||||||
|
}
|
||||||
|
|
||||||
|
# TODO: This should be a method in the Job model
|
||||||
|
def _delete_job(app, job_id):
|
||||||
|
with app.app_context():
|
||||||
|
job = Job.query.get(job_id)
|
||||||
|
job.delete()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Delete the job in a background task
|
||||||
|
socketio.start_background_task(
|
||||||
|
target=_delete_job,
|
||||||
|
app=current_app._get_current_object(),
|
||||||
|
job_id=job_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'code': 202,
|
||||||
|
'body': f'Job "{job.title}" marked for deletion'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@admin_required
|
||||||
|
def on_get_log(self, job_hashid: str):
|
||||||
|
# Decode the job hashid
|
||||||
|
job_id = hashids.decode(job_hashid)
|
||||||
|
|
||||||
|
# Validate job_id
|
||||||
|
if not isinstance(job_id, int):
|
||||||
|
return {
|
||||||
|
'code': 400,
|
||||||
|
'body': 'job_id is invalid'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Load job from database
|
||||||
|
job = Job.query.get(job_id)
|
||||||
|
if job is None:
|
||||||
|
return {
|
||||||
|
'code': 404,
|
||||||
|
'body': 'Job not found'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if the job is already processed
|
||||||
|
if job.status not in [JobStatus.COMPLETED, JobStatus.FAILED]:
|
||||||
|
return {
|
||||||
|
'code': 409,
|
||||||
|
'body': 'Job is not done processing'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read the log file
|
||||||
|
with open(job.path / 'pipeline_data' / 'logs' / 'pyflow_log.txt') as log_file:
|
||||||
|
job_log = log_file.read()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'code': 200,
|
||||||
|
'body': job_log
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def on_restart(self, job_hashid: str):
|
||||||
|
# Decode the job hashid
|
||||||
|
job_id = hashids.decode(job_hashid)
|
||||||
|
|
||||||
|
# Validate job_id
|
||||||
|
if not isinstance(job_id, int):
|
||||||
|
return {
|
||||||
|
'code': 400,
|
||||||
|
'body': 'job_id is invalid'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Load job from database
|
||||||
|
job = Job.query.get(job_id)
|
||||||
|
if job is None:
|
||||||
|
return {
|
||||||
|
'code': 404,
|
||||||
|
'body': 'Job not found'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if the current user is allowed to restart the job
|
||||||
|
if not (job.user == current_user or current_user.is_administrator):
|
||||||
|
return {
|
||||||
|
'code': 403,
|
||||||
|
'body': 'Forbidden'
|
||||||
|
}
|
||||||
|
|
||||||
|
# TODO: This should be a method in the Job model
|
||||||
|
def _restart_job(app, job_id):
|
||||||
|
with app.app_context():
|
||||||
|
job = Job.query.get(job_id)
|
||||||
|
job.restart()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Restart the job in a background task
|
||||||
|
socketio.start_background_task(
|
||||||
|
target=_restart_job,
|
||||||
|
app=current_app._get_current_object(),
|
||||||
|
job_id=job_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'code': 202,
|
||||||
|
'body': f'Job "{job.title}" restarted'
|
||||||
|
}
|
@ -17,7 +17,7 @@ def delete_job(job_id):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
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)
|
||||||
thread = Thread(
|
thread = Thread(
|
||||||
target=_delete_job,
|
target=_delete_job,
|
||||||
@ -56,7 +56,7 @@ def restart_job(job_id):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
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)
|
||||||
if job.status == JobStatus.FAILED:
|
if job.status == JobStatus.FAILED:
|
||||||
response = {'errors': {'message': 'Job status is not "failed"'}}
|
response = {'errors': {'message': 'Job status is not "failed"'}}
|
@ -5,24 +5,20 @@ from flask import (
|
|||||||
send_from_directory,
|
send_from_directory,
|
||||||
url_for
|
url_for
|
||||||
)
|
)
|
||||||
from flask_breadcrumbs import register_breadcrumb
|
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from app.models import Job, JobInput, JobResult
|
from app.models import Job, JobInput, JobResult
|
||||||
from . import bp
|
from . import bp
|
||||||
from .utils import job_dynamic_list_constructor as job_dlc
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('')
|
@bp.route('')
|
||||||
@register_breadcrumb(bp, '.', '<i class="nopaque-icons left">J</i>My Jobs')
|
def jobs():
|
||||||
def corpora():
|
|
||||||
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>')
|
||||||
@register_breadcrumb(bp, '.entity', '', dynamic_list_constructor=job_dlc)
|
|
||||||
def job(job_id):
|
def job(job_id):
|
||||||
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',
|
||||||
@ -34,13 +30,13 @@ def job(job_id):
|
|||||||
@bp.route('/<hashid:job_id>/inputs/<hashid:job_input_id>/download')
|
@bp.route('/<hashid:job_id>/inputs/<hashid:job_input_id>/download')
|
||||||
def download_job_input(job_id, job_input_id):
|
def download_job_input(job_id, job_input_id):
|
||||||
job_input = JobInput.query.filter_by(job_id=job_id, id=job_input_id).first_or_404()
|
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()):
|
if not (job_input.job.user == current_user or current_user.is_administrator):
|
||||||
abort(403)
|
abort(403)
|
||||||
return send_from_directory(
|
return send_from_directory(
|
||||||
job_input.path.parent,
|
job_input.path.parent,
|
||||||
job_input.path.name,
|
job_input.path.name,
|
||||||
as_attachment=True,
|
as_attachment=True,
|
||||||
attachment_filename=job_input.filename,
|
download_name=job_input.filename,
|
||||||
mimetype=job_input.mimetype
|
mimetype=job_input.mimetype
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -48,12 +44,12 @@ def download_job_input(job_id, job_input_id):
|
|||||||
@bp.route('/<hashid:job_id>/results/<hashid:job_result_id>/download')
|
@bp.route('/<hashid:job_id>/results/<hashid:job_result_id>/download')
|
||||||
def download_job_result(job_id, job_result_id):
|
def download_job_result(job_id, job_result_id):
|
||||||
job_result = JobResult.query.filter_by(job_id=job_id, id=job_result_id).first_or_404()
|
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()):
|
if not (job_result.job.user == current_user or current_user.is_administrator):
|
||||||
abort(403)
|
abort(403)
|
||||||
return send_from_directory(
|
return send_from_directory(
|
||||||
job_result.path.parent,
|
job_result.path.parent,
|
||||||
job_result.path.name,
|
job_result.path.name,
|
||||||
as_attachment=True,
|
as_attachment=True,
|
||||||
attachment_filename=job_result.filename,
|
download_name=job_result.filename,
|
||||||
mimetype=job_result.mimetype
|
mimetype=job_result.mimetype
|
||||||
)
|
)
|
@ -1,8 +1,9 @@
|
|||||||
from flask import current_app
|
from flask import current_app
|
||||||
from flask_migrate import upgrade
|
from flask_migrate import upgrade
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List
|
from app import db
|
||||||
from app.models import (
|
from app.models import (
|
||||||
|
Corpus,
|
||||||
CorpusFollowerRole,
|
CorpusFollowerRole,
|
||||||
Role,
|
Role,
|
||||||
SpaCyNLPPipelineModel,
|
SpaCyNLPPipelineModel,
|
||||||
@ -15,10 +16,10 @@ from . import bp
|
|||||||
@bp.cli.command('deploy')
|
@bp.cli.command('deploy')
|
||||||
def deploy():
|
def deploy():
|
||||||
''' Run deployment tasks. '''
|
''' Run deployment tasks. '''
|
||||||
# Make default directories
|
|
||||||
print('Make default directories')
|
print('Make default directories')
|
||||||
base_dir = current_app.config['NOPAQUE_DATA_DIR']
|
base_dir = current_app.config['NOPAQUE_DATA_DIR']
|
||||||
default_dirs: List[Path] = [
|
default_dirs: list[Path] = [
|
||||||
base_dir / 'tmp',
|
base_dir / 'tmp',
|
||||||
base_dir / 'users'
|
base_dir / 'users'
|
||||||
]
|
]
|
||||||
@ -28,11 +29,9 @@ def deploy():
|
|||||||
if not default_dir.is_dir():
|
if not default_dir.is_dir():
|
||||||
raise NotADirectoryError(f'{default_dir} is not a directory')
|
raise NotADirectoryError(f'{default_dir} is not a directory')
|
||||||
|
|
||||||
# migrate database to latest revision
|
|
||||||
print('Migrate database to latest revision')
|
print('Migrate database to latest revision')
|
||||||
upgrade()
|
upgrade()
|
||||||
|
|
||||||
# Insert/Update default database values
|
|
||||||
print('Insert/Update default Roles')
|
print('Insert/Update default Roles')
|
||||||
Role.insert_defaults()
|
Role.insert_defaults()
|
||||||
print('Insert/Update default Users')
|
print('Insert/Update default Users')
|
||||||
@ -44,4 +43,9 @@ def deploy():
|
|||||||
print('Insert/Update default TesseractOCRPipelineModels')
|
print('Insert/Update default TesseractOCRPipelineModels')
|
||||||
TesseractOCRPipelineModel.insert_defaults()
|
TesseractOCRPipelineModel.insert_defaults()
|
||||||
|
|
||||||
|
print('Stop running analysis sessions')
|
||||||
|
for corpus in Corpus.query.filter(Corpus.num_analysis_sessions > 0).all():
|
||||||
|
corpus.num_analysis_sessions = 0
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
# TODO: Implement checks for if the nopaque network exists
|
# TODO: Implement checks for if the nopaque network exists
|
@ -1,14 +1,11 @@
|
|||||||
from flask import flash, redirect, render_template, url_for
|
from flask import flash, redirect, render_template, url_for
|
||||||
from flask_breadcrumbs import register_breadcrumb
|
|
||||||
from flask_login import current_user, login_required, login_user
|
from flask_login import current_user, login_required, login_user
|
||||||
from app.auth.forms import LoginForm
|
from app.blueprints.auth.forms import LoginForm
|
||||||
from app.models import Corpus, User
|
from app.models import Corpus, User
|
||||||
from sqlalchemy import or_
|
|
||||||
from . import bp
|
from . import bp
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/', methods=['GET', 'POST'])
|
@bp.route('/', methods=['GET', 'POST'])
|
||||||
@register_breadcrumb(bp, '.', '<i class="material-icons">home</i>')
|
|
||||||
def index():
|
def index():
|
||||||
form = LoginForm()
|
form = LoginForm()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
@ -27,7 +24,6 @@ def index():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/faq')
|
@bp.route('/faq')
|
||||||
@register_breadcrumb(bp, '.faq', 'Frequently Asked Questions')
|
|
||||||
def faq():
|
def faq():
|
||||||
return render_template(
|
return render_template(
|
||||||
'main/faq.html.j2',
|
'main/faq.html.j2',
|
||||||
@ -36,7 +32,6 @@ def faq():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/dashboard')
|
@bp.route('/dashboard')
|
||||||
@register_breadcrumb(bp, '.dashboard', '<i class="material-icons left">dashboard</i>Dashboard')
|
|
||||||
@login_required
|
@login_required
|
||||||
def dashboard():
|
def dashboard():
|
||||||
return render_template(
|
return render_template(
|
||||||
@ -45,8 +40,15 @@ def dashboard():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/manual')
|
||||||
|
def manual():
|
||||||
|
return render_template(
|
||||||
|
'main/manual.html.j2',
|
||||||
|
title='Manual'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/news')
|
@bp.route('/news')
|
||||||
@register_breadcrumb(bp, '.news', '<i class="material-icons left">email</i>News')
|
|
||||||
def news():
|
def news():
|
||||||
return render_template(
|
return render_template(
|
||||||
'main/news.html.j2',
|
'main/news.html.j2',
|
||||||
@ -55,7 +57,6 @@ def news():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/privacy_policy')
|
@bp.route('/privacy_policy')
|
||||||
@register_breadcrumb(bp, '.privacy_policy', 'Private statement (GDPR)')
|
|
||||||
def privacy_policy():
|
def privacy_policy():
|
||||||
return render_template(
|
return render_template(
|
||||||
'main/privacy_policy.html.j2',
|
'main/privacy_policy.html.j2',
|
||||||
@ -64,7 +65,6 @@ def privacy_policy():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/terms_of_use')
|
@bp.route('/terms_of_use')
|
||||||
@register_breadcrumb(bp, '.terms_of_use', 'Terms of Use')
|
|
||||||
def terms_of_use():
|
def terms_of_use():
|
||||||
return render_template(
|
return render_template(
|
||||||
'main/terms_of_use.html.j2',
|
'main/terms_of_use.html.j2',
|
||||||
@ -73,7 +73,6 @@ def terms_of_use():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/social-area')
|
@bp.route('/social-area')
|
||||||
@register_breadcrumb(bp, '.social_area', '<i class="material-icons left">group</i>Social Area')
|
|
||||||
@login_required
|
@login_required
|
||||||
def social_area():
|
def social_area():
|
||||||
print('test')
|
print('test')
|
@ -1,12 +1,10 @@
|
|||||||
from flask import abort, current_app, flash, Markup, redirect, render_template, request, url_for
|
from flask import abort, current_app, flash, redirect, render_template, request, url_for
|
||||||
from flask_breadcrumbs import register_breadcrumb
|
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
import requests
|
import requests
|
||||||
from app import db, hashids
|
from app import db, hashids
|
||||||
from app.models import (
|
from app.models import (
|
||||||
Job,
|
Job,
|
||||||
JobInput,
|
JobInput,
|
||||||
JobResult,
|
|
||||||
JobStatus,
|
JobStatus,
|
||||||
TesseractOCRPipelineModel,
|
TesseractOCRPipelineModel,
|
||||||
SpaCyNLPPipelineModel
|
SpaCyNLPPipelineModel
|
||||||
@ -21,13 +19,11 @@ from .forms import (
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/services')
|
@bp.route('/services')
|
||||||
@register_breadcrumb(bp, '.', 'Services')
|
|
||||||
def services():
|
def services():
|
||||||
return redirect(url_for('main.dashboard'))
|
return redirect(url_for('main.dashboard'))
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/file-setup-pipeline', methods=['GET', 'POST'])
|
@bp.route('/file-setup-pipeline', methods=['GET', 'POST'])
|
||||||
@register_breadcrumb(bp, '.file_setup_pipeline', '<i class="nopaque-icons service-icons left" data-service="file-setup-pipeline"></i>File Setup')
|
|
||||||
def file_setup_pipeline():
|
def file_setup_pipeline():
|
||||||
service = 'file-setup-pipeline'
|
service = 'file-setup-pipeline'
|
||||||
service_manifest = SERVICES[service]
|
service_manifest = SERVICES[service]
|
||||||
@ -57,7 +53,7 @@ def file_setup_pipeline():
|
|||||||
abort(500)
|
abort(500)
|
||||||
job.status = JobStatus.SUBMITTED
|
job.status = JobStatus.SUBMITTED
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
message = Markup(f'Job "<a href="{job.url}">{job.title}</a>" created')
|
message = f'Job "<a href="{job.url}">{job.title}</a>" created'
|
||||||
flash(message, 'job')
|
flash(message, 'job')
|
||||||
return {}, 201, {'Location': job.url}
|
return {}, 201, {'Location': job.url}
|
||||||
return render_template(
|
return render_template(
|
||||||
@ -68,15 +64,12 @@ def file_setup_pipeline():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/tesseract-ocr-pipeline', methods=['GET', 'POST'])
|
@bp.route('/tesseract-ocr-pipeline', methods=['GET', 'POST'])
|
||||||
@register_breadcrumb(bp, '.tesseract_ocr_pipeline', '<i class="nopaque-icons service-icons left" data-service="tesseract-ocr-pipeline"></i>Tesseract OCR Pipeline')
|
|
||||||
def tesseract_ocr_pipeline():
|
def tesseract_ocr_pipeline():
|
||||||
service_name = 'tesseract-ocr-pipeline'
|
service_name = 'tesseract-ocr-pipeline'
|
||||||
service_manifest = SERVICES[service_name]
|
service_manifest = SERVICES[service_name]
|
||||||
version = request.args.get('version', service_manifest['latest_version'])
|
version = request.args.get('version', service_manifest['latest_version'])
|
||||||
if version not in service_manifest['versions']:
|
if version not in service_manifest['versions']:
|
||||||
abort(404)
|
abort(404)
|
||||||
job_results = JobResult.query.all()
|
|
||||||
choosable_job_ids = [job_result.job.hashid for job_result in job_results if job_result.job.service == "file-setup-pipeline" and job_result.filename.endswith('.pdf')]
|
|
||||||
form = CreateTesseractOCRPipelineJobForm(prefix='create-job-form', version=version)
|
form = CreateTesseractOCRPipelineJobForm(prefix='create-job-form', version=version)
|
||||||
if form.is_submitted():
|
if form.is_submitted():
|
||||||
if not form.validate():
|
if not form.validate():
|
||||||
@ -103,7 +96,7 @@ def tesseract_ocr_pipeline():
|
|||||||
abort(500)
|
abort(500)
|
||||||
job.status = JobStatus.SUBMITTED
|
job.status = JobStatus.SUBMITTED
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
message = Markup(f'Job "<a href="{job.url}">{job.title}</a>" created')
|
message = f'Job "<a href="{job.url}">{job.title}</a>" created'
|
||||||
flash(message, 'job')
|
flash(message, 'job')
|
||||||
return {}, 201, {'Location': job.url}
|
return {}, 201, {'Location': job.url}
|
||||||
tesseract_ocr_pipeline_models = [
|
tesseract_ocr_pipeline_models = [
|
||||||
@ -114,7 +107,6 @@ def tesseract_ocr_pipeline():
|
|||||||
return render_template(
|
return render_template(
|
||||||
'services/tesseract_ocr_pipeline.html.j2',
|
'services/tesseract_ocr_pipeline.html.j2',
|
||||||
title=service_manifest['name'],
|
title=service_manifest['name'],
|
||||||
choosable_job_ids=choosable_job_ids,
|
|
||||||
form=form,
|
form=form,
|
||||||
tesseract_ocr_pipeline_models=tesseract_ocr_pipeline_models,
|
tesseract_ocr_pipeline_models=tesseract_ocr_pipeline_models,
|
||||||
user_tesseract_ocr_pipeline_models_count=user_tesseract_ocr_pipeline_models_count
|
user_tesseract_ocr_pipeline_models_count=user_tesseract_ocr_pipeline_models_count
|
||||||
@ -122,7 +114,6 @@ def tesseract_ocr_pipeline():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/transkribus-htr-pipeline', methods=['GET', 'POST'])
|
@bp.route('/transkribus-htr-pipeline', methods=['GET', 'POST'])
|
||||||
@register_breadcrumb(bp, '.transkribus_htr_pipeline', '<i class="nopaque-icons service-icons left" data-service="transkribus-htr-pipeline"></i>Transkribus HTR Pipeline')
|
|
||||||
def transkribus_htr_pipeline():
|
def transkribus_htr_pipeline():
|
||||||
if not current_app.config.get('NOPAQUE_TRANSKRIBUS_ENABLED'):
|
if not current_app.config.get('NOPAQUE_TRANSKRIBUS_ENABLED'):
|
||||||
abort(404)
|
abort(404)
|
||||||
@ -168,7 +159,7 @@ def transkribus_htr_pipeline():
|
|||||||
abort(500)
|
abort(500)
|
||||||
job.status = JobStatus.SUBMITTED
|
job.status = JobStatus.SUBMITTED
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
message = Markup(f'Job "<a href="{job.url}">{job.title}</a>" created')
|
message = f'Job "<a href="{job.url}">{job.title}</a>" created'
|
||||||
flash(message, 'job')
|
flash(message, 'job')
|
||||||
return {}, 201, {'Location': job.url}
|
return {}, 201, {'Location': job.url}
|
||||||
return render_template(
|
return render_template(
|
||||||
@ -180,7 +171,6 @@ def transkribus_htr_pipeline():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/spacy-nlp-pipeline', methods=['GET', 'POST'])
|
@bp.route('/spacy-nlp-pipeline', methods=['GET', 'POST'])
|
||||||
@register_breadcrumb(bp, '.spacy_nlp_pipeline', '<i class="nopaque-icons service-icons left" data-service="spacy-nlp-pipeline"></i>SpaCy NLP Pipeline')
|
|
||||||
def spacy_nlp_pipeline():
|
def spacy_nlp_pipeline():
|
||||||
service = 'spacy-nlp-pipeline'
|
service = 'spacy-nlp-pipeline'
|
||||||
service_manifest = SERVICES[service]
|
service_manifest = SERVICES[service]
|
||||||
@ -214,7 +204,7 @@ def spacy_nlp_pipeline():
|
|||||||
abort(500)
|
abort(500)
|
||||||
job.status = JobStatus.SUBMITTED
|
job.status = JobStatus.SUBMITTED
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
message = Markup(f'Job "<a href="{job.url}">{job.title}</a>" created')
|
message = f'Job "<a href="{job.url}">{job.title}</a>" created'
|
||||||
flash(message, 'job')
|
flash(message, 'job')
|
||||||
return {}, 201, {'Location': job.url}
|
return {}, 201, {'Location': job.url}
|
||||||
return render_template(
|
return render_template(
|
||||||
@ -227,7 +217,6 @@ def spacy_nlp_pipeline():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/corpus-analysis')
|
@bp.route('/corpus-analysis')
|
||||||
@register_breadcrumb(bp, '.corpus_analysis', '<i class="nopaque-icons service-icons left" data-service="corpus-analysis"></i>Corpus Analysis')
|
|
||||||
def corpus_analysis():
|
def corpus_analysis():
|
||||||
return render_template(
|
return render_template(
|
||||||
'services/corpus_analysis.html.j2',
|
'services/corpus_analysis.html.j2',
|
@ -1,12 +1,10 @@
|
|||||||
from flask import g, url_for
|
from flask import g, url_for
|
||||||
from flask_breadcrumbs import register_breadcrumb
|
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from app.users.settings.routes import settings as settings_route
|
from app.blueprints.users.settings.routes import settings as settings_route
|
||||||
from . import bp
|
from . import bp
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/settings', methods=['GET', 'POST'])
|
@bp.route('/settings', methods=['GET', 'POST'])
|
||||||
@register_breadcrumb(bp, '.', '<i class="material-icons left">settings</i>Settings')
|
|
||||||
def settings():
|
def settings():
|
||||||
g._nopaque_redirect_location_on_post = url_for('.settings')
|
g._nopaque_redirect_location_on_post = url_for('.settings')
|
||||||
return settings_route(current_user.id)
|
return settings_route(current_user.id)
|
@ -12,7 +12,7 @@ def get_user(user_hashid):
|
|||||||
user = User.query.get(user_id)
|
user = User.query.get(user_id)
|
||||||
if user is None:
|
if user is None:
|
||||||
return {'status': 404, 'statusText': 'Not found'}
|
return {'status': 404, 'statusText': 'Not found'}
|
||||||
if not (user == current_user or current_user.is_administrator()):
|
if not (user == current_user or current_user.is_administrator):
|
||||||
return {'status': 403, 'statusText': 'Forbidden'}
|
return {'status': 403, 'statusText': 'Forbidden'}
|
||||||
return {
|
return {
|
||||||
'body': user.to_json_serializeable(backrefs=True, relationships=True),
|
'body': user.to_json_serializeable(backrefs=True, relationships=True),
|
||||||
@ -28,7 +28,7 @@ def subscribe_user(user_hashid):
|
|||||||
user = User.query.get(user_id)
|
user = User.query.get(user_id)
|
||||||
if user is None:
|
if user is None:
|
||||||
return {'status': 404, 'statusText': 'Not found'}
|
return {'status': 404, 'statusText': 'Not found'}
|
||||||
if not (user == current_user or current_user.is_administrator()):
|
if not (user == current_user or current_user.is_administrator):
|
||||||
return {'status': 403, 'statusText': 'Forbidden'}
|
return {'status': 403, 'statusText': 'Forbidden'}
|
||||||
join_room(f'/users/{user.hashid}')
|
join_room(f'/users/{user.hashid}')
|
||||||
return {'status': 200, 'statusText': 'OK'}
|
return {'status': 200, 'statusText': 'OK'}
|
||||||
@ -41,7 +41,7 @@ def unsubscribe_user(user_hashid):
|
|||||||
user = User.query.get(user_id)
|
user = User.query.get(user_id)
|
||||||
if user is None:
|
if user is None:
|
||||||
return {'status': 404, 'statusText': 'Not found'}
|
return {'status': 404, 'statusText': 'Not found'}
|
||||||
if not (user == current_user or current_user.is_administrator()):
|
if not (user == current_user or current_user.is_administrator):
|
||||||
return {'status': 403, 'statusText': 'Forbidden'}
|
return {'status': 403, 'statusText': 'Forbidden'}
|
||||||
leave_room(f'/users/{user.hashid}')
|
leave_room(f'/users/{user.hashid}')
|
||||||
return {'status': 200, 'statusText': 'OK'}
|
return {'status': 200, 'statusText': 'OK'}
|
@ -17,7 +17,7 @@ def delete_user(user_id):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
user = User.query.get_or_404(user_id)
|
user = User.query.get_or_404(user_id)
|
||||||
if not (user == current_user or current_user.is_administrator()):
|
if not (user == current_user or current_user.is_administrator):
|
||||||
abort(403)
|
abort(403)
|
||||||
thread = Thread(
|
thread = Thread(
|
||||||
target=_delete_user,
|
target=_delete_user,
|
||||||
@ -44,7 +44,7 @@ def delete_user_avatar(user_id):
|
|||||||
user = User.query.get_or_404(user_id)
|
user = User.query.get_or_404(user_id)
|
||||||
if user.avatar is None:
|
if user.avatar is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
if not (user == current_user or current_user.is_administrator()):
|
if not (user == current_user or current_user.is_administrator):
|
||||||
abort(403)
|
abort(403)
|
||||||
thread = Thread(
|
thread = Thread(
|
||||||
target=_delete_avatar,
|
target=_delete_avatar,
|
@ -1,5 +1,5 @@
|
|||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_socketio import join_room, leave_room
|
from flask_socketio import join_room
|
||||||
from app import hashids, socketio
|
from app import hashids, socketio
|
||||||
from app.decorators import socketio_admin_required, socketio_login_required
|
from app.decorators import socketio_admin_required, socketio_login_required
|
||||||
from app.models import User
|
from app.models import User
|
||||||
@ -33,7 +33,7 @@ def get_user(user_hashid):
|
|||||||
user = User.query.get(user_id)
|
user = User.query.get(user_id)
|
||||||
if user is None:
|
if user is None:
|
||||||
return {'options': {'status': 404, 'statusText': 'Not found'}}
|
return {'options': {'status': 404, 'statusText': 'Not found'}}
|
||||||
if not (user == current_user or current_user.is_administrator()):
|
if not (user == current_user or current_user.is_administrator):
|
||||||
return {'options': {'status': 403, 'statusText': 'Forbidden'}}
|
return {'options': {'status': 403, 'statusText': 'Forbidden'}}
|
||||||
return {
|
return {
|
||||||
'body': user.to_json_serializable(),
|
'body': user.to_json_serializable(),
|
||||||
@ -52,7 +52,7 @@ def subscribe_user(user_hashid):
|
|||||||
user = User.query.get(user_id)
|
user = User.query.get(user_id)
|
||||||
if user is None:
|
if user is None:
|
||||||
return {'options': {'status': 404, 'statusText': 'Not found'}}
|
return {'options': {'status': 404, 'statusText': 'Not found'}}
|
||||||
if not (user == current_user or current_user.is_administrator()):
|
if not (user == current_user or current_user.is_administrator):
|
||||||
return {'options': {'status': 403, 'statusText': 'Forbidden'}}
|
return {'options': {'status': 403, 'statusText': 'Forbidden'}}
|
||||||
join_room(f'/users/{user.hashid}')
|
join_room(f'/users/{user.hashid}')
|
||||||
return {'options': {'status': 200, 'statusText': 'OK'}}
|
return {'options': {'status': 200, 'statusText': 'OK'}}
|
||||||
@ -89,7 +89,7 @@ def get_user(user_hashid):
|
|||||||
user = User.query.filter_by(id=user_id, is_public=True).first()
|
user = User.query.filter_by(id=user_id, is_public=True).first()
|
||||||
if user is None:
|
if user is None:
|
||||||
return {'options': {'status': 404, 'statusText': 'Not found'}}
|
return {'options': {'status': 404, 'statusText': 'Not found'}}
|
||||||
if not (user == current_user or current_user.is_administrator()):
|
if not (user == current_user or current_user.is_administrator):
|
||||||
return {'options': {'status': 403, 'statusText': 'Forbidden'}}
|
return {'options': {'status': 403, 'statusText': 'Forbidden'}}
|
||||||
return {
|
return {
|
||||||
'body': user.to_json_serializable(filter_by_privacy_settings=True),
|
'body': user.to_json_serializable(filter_by_privacy_settings=True),
|
||||||
@ -108,7 +108,7 @@ def subscribe_user(user_hashid):
|
|||||||
user = User.query.filter_by(id=user_id, is_public=True).first()
|
user = User.query.filter_by(id=user_id, is_public=True).first()
|
||||||
if user is None:
|
if user is None:
|
||||||
return {'options': {'status': 404, 'statusText': 'Not found'}}
|
return {'options': {'status': 404, 'statusText': 'Not found'}}
|
||||||
if not (user == current_user or current_user.is_administrator()):
|
if not (user == current_user or current_user.is_administrator):
|
||||||
return {'options': {'status': 403, 'statusText': 'Forbidden'}}
|
return {'options': {'status': 403, 'statusText': 'Forbidden'}}
|
||||||
join_room(f'/public_users/{user.hashid}')
|
join_room(f'/public_users/{user.hashid}')
|
||||||
return {'options': {'status': 200, 'statusText': 'OK'}}
|
return {'options': {'status': 200, 'statusText': 'OK'}}
|
@ -5,24 +5,20 @@ from flask import (
|
|||||||
send_from_directory,
|
send_from_directory,
|
||||||
url_for
|
url_for
|
||||||
)
|
)
|
||||||
from flask_breadcrumbs import register_breadcrumb
|
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from app.models import User
|
from app.models import User
|
||||||
from . import bp
|
from . import bp
|
||||||
from .utils import user_dynamic_list_constructor as user_dlc
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('')
|
@bp.route('')
|
||||||
@register_breadcrumb(bp, '.', '<i class="material-icons left">group</i>Users')
|
|
||||||
def users():
|
def users():
|
||||||
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>')
|
||||||
@register_breadcrumb(bp, '.entity', '', dynamic_list_constructor=user_dlc)
|
|
||||||
def user(user_id):
|
def user(user_id):
|
||||||
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)
|
||||||
return render_template(
|
return render_template(
|
||||||
'users/user.html.j2',
|
'users/user.html.j2',
|
||||||
@ -34,7 +30,7 @@ def user(user_id):
|
|||||||
@bp.route('/<hashid:user_id>/avatar')
|
@bp.route('/<hashid:user_id>/avatar')
|
||||||
def user_avatar(user_id):
|
def user_avatar(user_id):
|
||||||
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'))
|
||||||
@ -42,6 +38,6 @@ def user_avatar(user_id):
|
|||||||
user.avatar.path.parent,
|
user.avatar.path.parent,
|
||||||
user.avatar.path.name,
|
user.avatar.path.name,
|
||||||
as_attachment=True,
|
as_attachment=True,
|
||||||
attachment_filename=user.avatar.filename,
|
download_name=user.avatar.filename,
|
||||||
mimetype=user.avatar.mimetype
|
mimetype=user.avatar.mimetype
|
||||||
)
|
)
|
@ -1,6 +1,5 @@
|
|||||||
from flask_login import current_user
|
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from flask_wtf.file import FileField, FileRequired
|
from flask_wtf.file import FileField, FileRequired, FileSize
|
||||||
from wtforms import (
|
from wtforms import (
|
||||||
PasswordField,
|
PasswordField,
|
||||||
SelectField,
|
SelectField,
|
||||||
@ -17,7 +16,6 @@ from wtforms.validators import (
|
|||||||
Regexp
|
Regexp
|
||||||
)
|
)
|
||||||
from app.models import User, UserSettingJobStatusMailNotificationLevel
|
from app.models import User, UserSettingJobStatusMailNotificationLevel
|
||||||
from app.wtforms.validators import FileSize
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateAccountInformationForm(FlaskForm):
|
class UpdateAccountInformationForm(FlaskForm):
|
||||||
@ -100,7 +98,7 @@ class UpdateProfileInformationForm(FlaskForm):
|
|||||||
|
|
||||||
|
|
||||||
class UpdateAvatarForm(FlaskForm):
|
class UpdateAvatarForm(FlaskForm):
|
||||||
avatar = FileField('File', validators=[FileRequired(), FileSize(2)])
|
avatar = FileField('File', validators=[FileRequired(), FileSize(2_000_000)])
|
||||||
submit = SubmitField()
|
submit = SubmitField()
|
||||||
|
|
||||||
def validate_avatar(self, field):
|
def validate_avatar(self, field):
|
@ -10,7 +10,7 @@ from . import bp
|
|||||||
@content_negotiation(consumes='application/json', produces='application/json')
|
@content_negotiation(consumes='application/json', produces='application/json')
|
||||||
def update_user_profile_privacy_setting_is_public(user_id):
|
def update_user_profile_privacy_setting_is_public(user_id):
|
||||||
user = User.query.get_or_404(user_id)
|
user = User.query.get_or_404(user_id)
|
||||||
if not (user == current_user or current_user.is_administrator()):
|
if not (user == current_user or current_user.is_administrator):
|
||||||
abort(403)
|
abort(403)
|
||||||
enabled = request.json
|
enabled = request.json
|
||||||
if not isinstance(enabled, bool):
|
if not isinstance(enabled, bool):
|
||||||
@ -32,7 +32,7 @@ def update_user_profile_privacy_settings(user_id, profile_privacy_setting_name):
|
|||||||
profile_privacy_setting = ProfilePrivacySettings[profile_privacy_setting_name]
|
profile_privacy_setting = ProfilePrivacySettings[profile_privacy_setting_name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
abort(404)
|
abort(404)
|
||||||
if not (user == current_user or current_user.is_administrator()):
|
if not (user == current_user or current_user.is_administrator):
|
||||||
abort(403)
|
abort(403)
|
||||||
enabled = request.json
|
enabled = request.json
|
||||||
if not isinstance(enabled, bool):
|
if not isinstance(enabled, bool):
|
@ -1,9 +1,7 @@
|
|||||||
from flask import abort, flash, g, redirect, render_template, url_for
|
from flask import abort, flash, g, redirect, render_template, url_for
|
||||||
from flask_breadcrumbs import register_breadcrumb
|
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import Avatar, User
|
from app.models import Avatar, User
|
||||||
from ..utils import user_endpoint_arguments_constructor as user_eac
|
|
||||||
from . import bp
|
from . import bp
|
||||||
from .forms import (
|
from .forms import (
|
||||||
UpdateAvatarForm,
|
UpdateAvatarForm,
|
||||||
@ -15,10 +13,9 @@ from .forms import (
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/<hashid:user_id>/settings', methods=['GET', 'POST'])
|
@bp.route('/<hashid:user_id>/settings', methods=['GET', 'POST'])
|
||||||
@register_breadcrumb(bp, '.entity.settings', '<i class="material-icons left">settings</i>Settings', endpoint_arguments_constructor=user_eac)
|
|
||||||
def settings(user_id):
|
def settings(user_id):
|
||||||
user = User.query.get_or_404(user_id)
|
user = User.query.get_or_404(user_id)
|
||||||
if not (user == current_user or current_user.is_administrator()):
|
if not (user == current_user or current_user.is_administrator):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
redirect_location_on_post = g.pop(
|
redirect_location_on_post = g.pop(
|
@ -1,16 +1,13 @@
|
|||||||
from flask import redirect, render_template, url_for
|
from flask import redirect, render_template, url_for
|
||||||
from flask_breadcrumbs import register_breadcrumb
|
|
||||||
from . import bp
|
from . import bp
|
||||||
|
|
||||||
|
|
||||||
@bp.route('')
|
@bp.route('')
|
||||||
@register_breadcrumb(bp, '.', '<i class="material-icons left">business_center</i>Workshops')
|
|
||||||
def workshops():
|
def workshops():
|
||||||
return redirect(url_for('main.dashboard'))
|
return redirect(url_for('main.dashboard'))
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/fgho_sommerschule_2023')
|
@bp.route('/fgho_sommerschule_2023')
|
||||||
@register_breadcrumb(bp, '.fgho_sommerschule_2023', 'FGHO Sommerschule 2023')
|
|
||||||
def fgho_sommerschule_2023():
|
def fgho_sommerschule_2023():
|
||||||
return render_template(
|
return render_template(
|
||||||
'workshops/fgho_sommerschule_2023.html.j2',
|
'workshops/fgho_sommerschule_2023.html.j2',
|
@ -1,13 +0,0 @@
|
|||||||
from flask import request, url_for
|
|
||||||
from app.models import SpaCyNLPPipelineModel
|
|
||||||
|
|
||||||
|
|
||||||
def spacy_nlp_pipeline_model_dlc():
|
|
||||||
snpm_id = request.view_args['spacy_nlp_pipeline_model_id']
|
|
||||||
snpm = SpaCyNLPPipelineModel.query.get_or_404(snpm_id)
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
'text': f'{snpm.title} {snpm.version}',
|
|
||||||
'url': url_for('.spacy_nlp_pipeline_model', spacy_nlp_pipeline_model_id=snpm_id)
|
|
||||||
}
|
|
||||||
]
|
|
@ -1,13 +0,0 @@
|
|||||||
from flask import request, url_for
|
|
||||||
from app.models import TesseractOCRPipelineModel
|
|
||||||
|
|
||||||
|
|
||||||
def tesseract_ocr_pipeline_model_dlc():
|
|
||||||
topm_id = request.view_args['tesseract_ocr_pipeline_model_id']
|
|
||||||
topm = TesseractOCRPipelineModel.query.get_or_404(topm_id)
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
'text': f'{topm.title} {topm.version}',
|
|
||||||
'url': url_for('.tesseract_ocr_pipeline_model', tesseract_ocr_pipeline_model_id=topm_id)
|
|
||||||
}
|
|
||||||
]
|
|
@ -1,11 +1,10 @@
|
|||||||
from flask import current_app
|
|
||||||
from app import db
|
|
||||||
from app.models import User, Corpus, CorpusFile
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from flask import current_app
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List
|
|
||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
|
from app import db
|
||||||
|
from app.models import User, Corpus, CorpusFile
|
||||||
|
|
||||||
|
|
||||||
class SandpaperConverter:
|
class SandpaperConverter:
|
||||||
@ -15,7 +14,7 @@ class SandpaperConverter:
|
|||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
with self.json_db_file.open('r') as f:
|
with self.json_db_file.open('r') as f:
|
||||||
json_db: List[Dict] = json.load(f)
|
json_db: list[dict] = json.load(f)
|
||||||
|
|
||||||
for json_user in json_db:
|
for json_user in json_db:
|
||||||
if not json_user['confirmed']:
|
if not json_user['confirmed']:
|
||||||
@ -26,7 +25,7 @@ class SandpaperConverter:
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
def convert_user(self, json_user: Dict, user_dir: Path):
|
def convert_user(self, json_user: dict, user_dir: Path):
|
||||||
current_app.logger.info(f'Create User {json_user["username"]}...')
|
current_app.logger.info(f'Create User {json_user["username"]}...')
|
||||||
try:
|
try:
|
||||||
user = User.create(
|
user = User.create(
|
||||||
@ -48,7 +47,7 @@ class SandpaperConverter:
|
|||||||
current_app.logger.info('Done')
|
current_app.logger.info('Done')
|
||||||
|
|
||||||
|
|
||||||
def convert_corpus(self, json_corpus: Dict, user: User, corpus_dir: Path):
|
def convert_corpus(self, json_corpus: dict, user: User, corpus_dir: Path):
|
||||||
current_app.logger.info(f'Create Corpus {json_corpus["title"]}...')
|
current_app.logger.info(f'Create Corpus {json_corpus["title"]}...')
|
||||||
try:
|
try:
|
||||||
corpus = Corpus.create(
|
corpus = Corpus.create(
|
||||||
@ -64,7 +63,7 @@ class SandpaperConverter:
|
|||||||
current_app.logger.info('Done')
|
current_app.logger.info('Done')
|
||||||
|
|
||||||
|
|
||||||
def convert_corpus_file(self, json_corpus_file: Dict, corpus: Corpus, corpus_dir: Path):
|
def convert_corpus_file(self, json_corpus_file: dict, corpus: Corpus, corpus_dir: Path):
|
||||||
current_app.logger.info(f'Create CorpusFile {json_corpus_file["title"]}...')
|
current_app.logger.info(f'Create CorpusFile {json_corpus_file["title"]}...')
|
||||||
corpus_file = CorpusFile(
|
corpus_file = CorpusFile(
|
||||||
corpus=corpus,
|
corpus=corpus,
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
from flask import request, url_for
|
|
||||||
from app.models import CorpusFile
|
|
||||||
from ..utils import corpus_endpoint_arguments_constructor as corpus_eac
|
|
||||||
|
|
||||||
|
|
||||||
def corpus_file_dynamic_list_constructor():
|
|
||||||
corpus_id = request.view_args['corpus_id']
|
|
||||||
corpus_file_id = request.view_args['corpus_file_id']
|
|
||||||
corpus_file = CorpusFile.query.filter_by(corpus_id=corpus_id, id=corpus_file_id).first_or_404()
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
'text': f'{corpus_file.author}: {corpus_file.title} ({corpus_file.publishing_year})',
|
|
||||||
'url': url_for('.corpus_file', corpus_id=corpus_id, corpus_file_id=corpus_file_id)
|
|
||||||
}
|
|
||||||
]
|
|
@ -1,17 +0,0 @@
|
|||||||
from flask import request, url_for
|
|
||||||
from app.models import Corpus
|
|
||||||
|
|
||||||
|
|
||||||
def corpus_endpoint_arguments_constructor():
|
|
||||||
return {'corpus_id': request.view_args['corpus_id']}
|
|
||||||
|
|
||||||
|
|
||||||
def corpus_dynamic_list_constructor():
|
|
||||||
corpus_id = request.view_args['corpus_id']
|
|
||||||
corpus = Corpus.query.get_or_404(corpus_id)
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
'text': f'<i class="material-icons left">book</i>{corpus.title}',
|
|
||||||
'url': url_for('.corpus', corpus_id=corpus_id)
|
|
||||||
}
|
|
||||||
]
|
|
@ -1,11 +0,0 @@
|
|||||||
from app import db
|
|
||||||
from flask import Flask
|
|
||||||
from .corpus_utils import check_corpora
|
|
||||||
from .job_utils import check_jobs
|
|
||||||
|
|
||||||
|
|
||||||
def daemon(app: Flask):
|
|
||||||
with app.app_context():
|
|
||||||
check_corpora()
|
|
||||||
check_jobs()
|
|
||||||
db.session.commit()
|
|
@ -1,8 +1,7 @@
|
|||||||
from flask import abort, current_app, request
|
from flask import abort, request
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from threading import Thread
|
from typing import Optional
|
||||||
from typing import List, Union
|
|
||||||
from werkzeug.exceptions import NotAcceptable
|
from werkzeug.exceptions import NotAcceptable
|
||||||
from app.models import Permission
|
from app.models import Permission
|
||||||
|
|
||||||
@ -24,22 +23,21 @@ def admin_required(f):
|
|||||||
|
|
||||||
def socketio_login_required(f):
|
def socketio_login_required(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
else:
|
return {'code': 401, 'body': 'Unauthorized'}
|
||||||
return {'code': 401, 'msg': 'Unauthorized'}
|
return wrapper
|
||||||
return decorated_function
|
|
||||||
|
|
||||||
|
|
||||||
def socketio_permission_required(permission):
|
def socketio_permission_required(permission):
|
||||||
def decorator(f):
|
def decorator(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
if not current_user.can(permission):
|
if not current_user.can(permission):
|
||||||
return {'code': 403, 'msg': 'Forbidden'}
|
return {'code': 403, 'body': 'Forbidden'}
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
return decorated_function
|
return wrapper
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
@ -47,27 +45,9 @@ def socketio_admin_required(f):
|
|||||||
return socketio_permission_required(Permission.ADMINISTRATE)(f)
|
return socketio_permission_required(Permission.ADMINISTRATE)(f)
|
||||||
|
|
||||||
|
|
||||||
def background(f):
|
|
||||||
'''
|
|
||||||
' This decorator executes a function in a Thread.
|
|
||||||
' Decorated functions need to be executed within a code block where an
|
|
||||||
' app context exists.
|
|
||||||
'
|
|
||||||
' NOTE: An app object is passed as a keyword argument to the decorated
|
|
||||||
' function.
|
|
||||||
'''
|
|
||||||
@wraps(f)
|
|
||||||
def wrapped(*args, **kwargs):
|
|
||||||
kwargs['app'] = current_app._get_current_object()
|
|
||||||
thread = Thread(target=f, args=args, kwargs=kwargs)
|
|
||||||
thread.start()
|
|
||||||
return thread
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def content_negotiation(
|
def content_negotiation(
|
||||||
produces: Union[str, List[str], None] = None,
|
produces: Optional[str | list[str]] = None,
|
||||||
consumes: Union[str, List[str], None] = None
|
consumes: Optional[str | list[str]] = None
|
||||||
):
|
):
|
||||||
def decorator(f):
|
def decorator(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
|
31
app/email.py
31
app/email.py
@ -1,25 +1,32 @@
|
|||||||
from flask import current_app, render_template
|
from flask import current_app, Flask, render_template
|
||||||
from flask_mail import Message
|
from flask_mail import Message
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from app import mail
|
from app import mail
|
||||||
|
|
||||||
|
|
||||||
def create_message(recipient, subject, template, **kwargs):
|
def create_message(
|
||||||
subject_prefix: str = current_app.config['NOPAQUE_MAIL_SUBJECT_PREFIX']
|
recipient: str,
|
||||||
msg: Message = Message(
|
subject: str,
|
||||||
body=render_template(f'{template}.txt.j2', **kwargs),
|
template: str,
|
||||||
html=render_template(f'{template}.html.j2', **kwargs),
|
**context
|
||||||
|
) -> Message:
|
||||||
|
message = Message(
|
||||||
|
body=render_template(f'{template}.txt.j2', **context),
|
||||||
|
html=render_template(f'{template}.html.j2', **context),
|
||||||
recipients=[recipient],
|
recipients=[recipient],
|
||||||
subject=f'{subject_prefix} {subject}'
|
subject=f'[nopaque] {subject}'
|
||||||
)
|
)
|
||||||
return msg
|
return message
|
||||||
|
|
||||||
|
|
||||||
def send(msg, *args, **kwargs):
|
def send(message: Message) -> Thread:
|
||||||
def _send(app, msg):
|
def _send(app: Flask, message: Message):
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
mail.send(msg)
|
mail.send(message)
|
||||||
|
|
||||||
thread = Thread(target=_send, args=[current_app._get_current_object(), msg])
|
thread = Thread(
|
||||||
|
target=_send,
|
||||||
|
args=[current_app._get_current_object(), message]
|
||||||
|
)
|
||||||
thread.start()
|
thread.start()
|
||||||
return thread
|
return thread
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
from .container_column import ContainerColumn
|
|
||||||
from .int_enum_column import IntEnumColumn
|
|
@ -1,21 +0,0 @@
|
|||||||
import json
|
|
||||||
from app import db
|
|
||||||
|
|
||||||
|
|
||||||
class ContainerColumn(db.TypeDecorator):
|
|
||||||
impl = db.String
|
|
||||||
|
|
||||||
def __init__(self, container_type, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.container_type = container_type
|
|
||||||
|
|
||||||
def process_bind_param(self, value, dialect):
|
|
||||||
if isinstance(value, self.container_type):
|
|
||||||
return json.dumps(value)
|
|
||||||
elif isinstance(value, str) and isinstance(json.loads(value), self.container_type):
|
|
||||||
return value
|
|
||||||
else:
|
|
||||||
return TypeError()
|
|
||||||
|
|
||||||
def process_result_value(self, value, dialect):
|
|
||||||
return json.loads(value)
|
|
2
app/extensions/sqlalchemy_extras/__init__.py
Normal file
2
app/extensions/sqlalchemy_extras/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from .types import ContainerColumn
|
||||||
|
from .types import IntEnumColumn
|
@ -1,6 +1,26 @@
|
|||||||
|
import json
|
||||||
from app import db
|
from app import db
|
||||||
|
|
||||||
|
|
||||||
|
class ContainerColumn(db.TypeDecorator):
|
||||||
|
impl = db.String
|
||||||
|
|
||||||
|
def __init__(self, container_type, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.container_type = container_type
|
||||||
|
|
||||||
|
def process_bind_param(self, value, dialect):
|
||||||
|
if isinstance(value, self.container_type):
|
||||||
|
return json.dumps(value)
|
||||||
|
elif isinstance(value, str) and isinstance(json.loads(value), self.container_type):
|
||||||
|
return value
|
||||||
|
else:
|
||||||
|
return TypeError()
|
||||||
|
|
||||||
|
def process_result_value(self, value, dialect):
|
||||||
|
return json.loads(value)
|
||||||
|
|
||||||
|
|
||||||
class IntEnumColumn(db.TypeDecorator):
|
class IntEnumColumn(db.TypeDecorator):
|
||||||
impl = db.Integer
|
impl = db.Integer
|
||||||
|
|
@ -1,18 +1,2 @@
|
|||||||
from flask import Blueprint
|
from .handle_corpora import job as handle_corpora
|
||||||
from flask_login import login_required
|
from .handle_jobs import job as handle_jobs
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint('jobs', __name__)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.before_request
|
|
||||||
@login_required
|
|
||||||
def before_request():
|
|
||||||
'''
|
|
||||||
Ensures that the routes in this package can only be visited by users that
|
|
||||||
are logged in.
|
|
||||||
'''
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
from . import routes, json_routes
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from app import docker_client
|
from app import db, docker_client, scheduler
|
||||||
from app.models import Corpus, CorpusStatus
|
from app.models import Corpus, CorpusStatus
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
import docker
|
import docker
|
||||||
@ -6,7 +6,11 @@ import os
|
|||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
|
|
||||||
def check_corpora():
|
def job():
|
||||||
|
with scheduler.app.app_context():
|
||||||
|
_handle_corpora()
|
||||||
|
|
||||||
|
def _handle_corpora():
|
||||||
corpora = Corpus.query.all()
|
corpora = Corpus.query.all()
|
||||||
for corpus in [x for x in corpora if x.status == CorpusStatus.SUBMITTED]:
|
for corpus in [x for x in corpora if x.status == CorpusStatus.SUBMITTED]:
|
||||||
_create_build_corpus_service(corpus)
|
_create_build_corpus_service(corpus)
|
||||||
@ -22,6 +26,7 @@ def check_corpora():
|
|||||||
_create_cqpserver_container(corpus)
|
_create_cqpserver_container(corpus)
|
||||||
for corpus in [x for x in corpora if x.status == CorpusStatus.CANCELING_ANALYSIS_SESSION]:
|
for corpus in [x for x in corpora if x.status == CorpusStatus.CANCELING_ANALYSIS_SESSION]:
|
||||||
_remove_cqpserver_container(corpus)
|
_remove_cqpserver_container(corpus)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def _create_build_corpus_service(corpus):
|
def _create_build_corpus_service(corpus):
|
||||||
''' # Docker service settings # '''
|
''' # Docker service settings # '''
|
@ -1,4 +1,4 @@
|
|||||||
from app import db, docker_client, hashids
|
from app import db, docker_client, hashids, scheduler
|
||||||
from app.models import (
|
from app.models import (
|
||||||
Job,
|
Job,
|
||||||
JobResult,
|
JobResult,
|
||||||
@ -15,7 +15,11 @@ import os
|
|||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
|
|
||||||
def check_jobs():
|
def job():
|
||||||
|
with scheduler.app.app_context():
|
||||||
|
_handle_jobs()
|
||||||
|
|
||||||
|
def _handle_jobs():
|
||||||
jobs = Job.query.all()
|
jobs = Job.query.all()
|
||||||
for job in [x for x in jobs if x.status == JobStatus.SUBMITTED]:
|
for job in [x for x in jobs if x.status == JobStatus.SUBMITTED]:
|
||||||
_create_job_service(job)
|
_create_job_service(job)
|
||||||
@ -23,6 +27,7 @@ def check_jobs():
|
|||||||
_checkout_job_service(job)
|
_checkout_job_service(job)
|
||||||
for job in [x for x in jobs if x.status == JobStatus.CANCELING]:
|
for job in [x for x in jobs if x.status == JobStatus.CANCELING]:
|
||||||
_remove_job_service(job)
|
_remove_job_service(job)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def _create_job_service(job):
|
def _create_job_service(job):
|
||||||
''' # Docker service settings # '''
|
''' # Docker service settings # '''
|
@ -1,13 +0,0 @@
|
|||||||
from flask import request, url_for
|
|
||||||
from app.models import Job
|
|
||||||
|
|
||||||
|
|
||||||
def job_dynamic_list_constructor():
|
|
||||||
job_id = request.view_args['job_id']
|
|
||||||
job = Job.query.get_or_404(job_id)
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
'text': f'<i class="nopaque-icons left service-icons" data-service="{job.service}"></i>{job.title}',
|
|
||||||
'url': url_for('.job', job_id=job_id)
|
|
||||||
}
|
|
||||||
]
|
|
@ -1,3 +1,4 @@
|
|||||||
|
from .anonymous_user import *
|
||||||
from .avatar import *
|
from .avatar import *
|
||||||
from .corpus_file import *
|
from .corpus_file import *
|
||||||
from .corpus_follower_association import *
|
from .corpus_follower_association import *
|
||||||
@ -11,9 +12,3 @@ from .spacy_nlp_pipeline_model import *
|
|||||||
from .tesseract_ocr_pipeline_model import *
|
from .tesseract_ocr_pipeline_model import *
|
||||||
from .token import *
|
from .token import *
|
||||||
from .user import *
|
from .user import *
|
||||||
from app import login
|
|
||||||
|
|
||||||
|
|
||||||
@login.user_loader
|
|
||||||
def load_user(user_id):
|
|
||||||
return User.query.get(int(user_id))
|
|
||||||
|
10
app/models/anonymous_user.py
Normal file
10
app/models/anonymous_user.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from flask_login import AnonymousUserMixin
|
||||||
|
|
||||||
|
|
||||||
|
class AnonymousUser(AnonymousUserMixin):
|
||||||
|
def can(self, permissions):
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_administrator(self):
|
||||||
|
return False
|
@ -3,13 +3,12 @@ from enum import IntEnum
|
|||||||
from flask import current_app, url_for
|
from flask import current_app, url_for
|
||||||
from flask_hashids import HashidMixin
|
from flask_hashids import HashidMixin
|
||||||
from sqlalchemy.ext.associationproxy import association_proxy
|
from sqlalchemy.ext.associationproxy import association_proxy
|
||||||
from typing import Union
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
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.ext.flask_sqlalchemy import IntEnumColumn
|
from app.extensions.sqlalchemy_extras import IntEnumColumn
|
||||||
from .corpus_follower_association import CorpusFollowerAssociation
|
from .corpus_follower_association import CorpusFollowerAssociation
|
||||||
|
|
||||||
|
|
||||||
@ -25,7 +24,7 @@ class CorpusStatus(IntEnum):
|
|||||||
CANCELING_ANALYSIS_SESSION = 9
|
CANCELING_ANALYSIS_SESSION = 9
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get(corpus_status: Union['CorpusStatus', int, str]) -> 'CorpusStatus':
|
def get(corpus_status: 'CorpusStatus | int | str') -> 'CorpusStatus':
|
||||||
if isinstance(corpus_status, CorpusStatus):
|
if isinstance(corpus_status, CorpusStatus):
|
||||||
return corpus_status
|
return corpus_status
|
||||||
if isinstance(corpus_status, int):
|
if isinstance(corpus_status, int):
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
from flask_hashids import HashidMixin
|
from flask_hashids import HashidMixin
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from typing import Union
|
|
||||||
from app import db
|
from app import db
|
||||||
|
|
||||||
|
|
||||||
@ -11,7 +10,7 @@ class CorpusFollowerPermission(IntEnum):
|
|||||||
MANAGE_CORPUS = 8
|
MANAGE_CORPUS = 8
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get(corpus_follower_permission: Union['CorpusFollowerPermission', int, str]) -> 'CorpusFollowerPermission':
|
def get(corpus_follower_permission: 'CorpusFollowerPermission | int | str') -> 'CorpusFollowerPermission':
|
||||||
if isinstance(corpus_follower_permission, CorpusFollowerPermission):
|
if isinstance(corpus_follower_permission, CorpusFollowerPermission):
|
||||||
return corpus_follower_permission
|
return corpus_follower_permission
|
||||||
if isinstance(corpus_follower_permission, int):
|
if isinstance(corpus_follower_permission, int):
|
||||||
@ -38,16 +37,16 @@ class CorpusFollowerRole(HashidMixin, db.Model):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<CorpusFollowerRole {self.name}>'
|
return f'<CorpusFollowerRole {self.name}>'
|
||||||
|
|
||||||
def has_permission(self, permission: Union[CorpusFollowerPermission, int, str]):
|
def has_permission(self, permission: CorpusFollowerPermission | int | str):
|
||||||
perm = CorpusFollowerPermission.get(permission)
|
perm = CorpusFollowerPermission.get(permission)
|
||||||
return self.permissions & perm.value == perm.value
|
return self.permissions & perm.value == perm.value
|
||||||
|
|
||||||
def add_permission(self, permission: Union[CorpusFollowerPermission, int, str]):
|
def add_permission(self, permission: CorpusFollowerPermission | int | str):
|
||||||
perm = CorpusFollowerPermission.get(permission)
|
perm = CorpusFollowerPermission.get(permission)
|
||||||
if not self.has_permission(perm):
|
if not self.has_permission(perm):
|
||||||
self.permissions += perm.value
|
self.permissions += perm.value
|
||||||
|
|
||||||
def remove_permission(self, permission: Union[CorpusFollowerPermission, int, str]):
|
def remove_permission(self, permission: CorpusFollowerPermission | int | str):
|
||||||
perm = CorpusFollowerPermission.get(permission)
|
perm = CorpusFollowerPermission.get(permission)
|
||||||
if self.has_permission(perm):
|
if self.has_permission(perm):
|
||||||
self.permissions -= perm.value
|
self.permissions -= perm.value
|
||||||
|
@ -3,11 +3,10 @@ from enum import IntEnum
|
|||||||
from flask import current_app, url_for
|
from flask import current_app, url_for
|
||||||
from flask_hashids import HashidMixin
|
from flask_hashids import HashidMixin
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from typing import Union
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
from app import db
|
from app import db
|
||||||
from app.ext.flask_sqlalchemy import ContainerColumn, IntEnumColumn
|
from app.extensions.sqlalchemy_extras import ContainerColumn, IntEnumColumn
|
||||||
|
|
||||||
|
|
||||||
class JobStatus(IntEnum):
|
class JobStatus(IntEnum):
|
||||||
@ -21,7 +20,7 @@ class JobStatus(IntEnum):
|
|||||||
FAILED = 8
|
FAILED = 8
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get(job_status: Union['JobStatus', int, str]) -> 'JobStatus':
|
def get(job_status: 'JobStatus | int | str') -> 'JobStatus':
|
||||||
if isinstance(job_status, JobStatus):
|
if isinstance(job_status, JobStatus):
|
||||||
return job_status
|
return job_status
|
||||||
if isinstance(job_status, int):
|
if isinstance(job_status, int):
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from flask_hashids import HashidMixin
|
from flask_hashids import HashidMixin
|
||||||
from typing import Union
|
|
||||||
from app import db
|
from app import db
|
||||||
|
|
||||||
|
|
||||||
@ -14,7 +13,7 @@ class Permission(IntEnum):
|
|||||||
USE_API = 4
|
USE_API = 4
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get(permission: Union['Permission', int, str]) -> 'Permission':
|
def get(permission: 'Permission | int | str') -> 'Permission':
|
||||||
if isinstance(permission, Permission):
|
if isinstance(permission, Permission):
|
||||||
return permission
|
return permission
|
||||||
if isinstance(permission, int):
|
if isinstance(permission, int):
|
||||||
@ -38,16 +37,16 @@ class Role(HashidMixin, db.Model):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<Role {self.name}>'
|
return f'<Role {self.name}>'
|
||||||
|
|
||||||
def has_permission(self, permission: Union[Permission, int, str]):
|
def has_permission(self, permission: Permission | int | str):
|
||||||
p = Permission.get(permission)
|
p = Permission.get(permission)
|
||||||
return self.permissions & p.value == p.value
|
return self.permissions & p.value == p.value
|
||||||
|
|
||||||
def add_permission(self, permission: Union[Permission, int, str]):
|
def add_permission(self, permission: Permission | int | str):
|
||||||
p = Permission.get(permission)
|
p = Permission.get(permission)
|
||||||
if not self.has_permission(p):
|
if not self.has_permission(p):
|
||||||
self.permissions += p.value
|
self.permissions += p.value
|
||||||
|
|
||||||
def remove_permission(self, permission: Union[Permission, int, str]):
|
def remove_permission(self, permission: Permission | int | str):
|
||||||
p = Permission.get(permission)
|
p = Permission.get(permission)
|
||||||
if self.has_permission(p):
|
if self.has_permission(p):
|
||||||
self.permissions -= p.value
|
self.permissions -= p.value
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user