mirror of
https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
synced 2026-05-29 18:10:39 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c709e65d0 | |||
| 71c0ddf515 | |||
| 5c395d1e06 |
+2
-2
@@ -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
|
|
||||||
|
|||||||
+6
-4
@@ -35,17 +35,19 @@ ENV PATH="${NOPAQUE_PYTHON3_VENV_PATH}/bin:${PATH}"
|
|||||||
|
|
||||||
|
|
||||||
# Install Python dependencies
|
# Install Python dependencies
|
||||||
COPY --chown=nopaque:nopaque requirements.freezed.txt requirements.freezed.txt
|
COPY --chown=nopaque:nopaque requirements.txt requirements.txt
|
||||||
RUN python3 -m pip install --requirement requirements.freezed.txt \
|
RUN python3 -m pip install --requirement requirements.txt \
|
||||||
&& rm requirements.freezed.txt
|
&& rm requirements.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 boot.sh config.py wsgi.py ./
|
COPY --chown=nopaque:nopaque .flaskenv boot.sh config.py nopaque.py requirements.txt ./
|
||||||
|
|
||||||
RUN mkdir logs
|
RUN mkdir logs
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+20
-11
@@ -1,12 +1,13 @@
|
|||||||
# from apifairy import APIFairy
|
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_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
|
||||||
from flask_migrate import Migrate
|
from flask_migrate import Migrate
|
||||||
from flask_paranoid import Paranoid
|
from flask_paranoid import Paranoid
|
||||||
from flask_socketio import SocketIO
|
from flask_socketio import SocketIO
|
||||||
@@ -14,15 +15,16 @@ from flask_sqlalchemy import SQLAlchemy
|
|||||||
from flask_hashids import Hashids
|
from flask_hashids import Hashids
|
||||||
|
|
||||||
|
|
||||||
# apifairy = APIFairy()
|
apifairy = APIFairy()
|
||||||
assets = Environment()
|
assets = Environment()
|
||||||
|
breadcrumbs = Breadcrumbs()
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
docker_client = DockerClient()
|
docker_client = DockerClient()
|
||||||
hashids = Hashids()
|
hashids = Hashids()
|
||||||
login = LoginManager()
|
login = LoginManager()
|
||||||
login.login_view = 'auth.login'
|
login.login_view = 'auth.login'
|
||||||
login.login_message = 'Please log in to access this page.'
|
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()
|
||||||
@@ -31,9 +33,6 @@ scheduler = APScheduler()
|
|||||||
socketio = SocketIO()
|
socketio = SocketIO()
|
||||||
|
|
||||||
|
|
||||||
# TODO: Create export for lemmatized corpora
|
|
||||||
|
|
||||||
|
|
||||||
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 (WSGI Application) object. '''
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
@@ -45,12 +44,13 @@ def create_app(config: Config = Config) -> Flask:
|
|||||||
registry=app.config['NOPAQUE_DOCKER_REGISTRY']
|
registry=app.config['NOPAQUE_DOCKER_REGISTRY']
|
||||||
)
|
)
|
||||||
|
|
||||||
# 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)
|
||||||
# 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)
|
||||||
@@ -61,19 +61,23 @@ def create_app(config: Config = Config) -> Flask:
|
|||||||
register_event_listeners()
|
register_event_listeners()
|
||||||
|
|
||||||
from .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 .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 .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 .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 .corpora import bp as corpora_blueprint
|
||||||
from .corpora.cqi_over_sio import CQiNamespace
|
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'))
|
socketio.on_namespace(CQiNamespace('/cqi_over_sio'))
|
||||||
|
|
||||||
@@ -81,18 +85,23 @@ def create_app(config: Config = Config) -> Flask:
|
|||||||
app.register_blueprint(errors_bp)
|
app.register_blueprint(errors_bp)
|
||||||
|
|
||||||
from .jobs import bp as jobs_blueprint
|
from .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 .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 .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 .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 .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 .workshops import bp as workshops_blueprint
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
from flask_login import current_user
|
|
||||||
from flask_socketio import disconnect, Namespace
|
|
||||||
from app import db, hashids
|
|
||||||
from app.extensions.flask_socketio_extras import 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()
|
|
||||||
|
|
||||||
|
|
||||||
@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.decorators import content_negotiation
|
|
||||||
from app import db
|
from app import db
|
||||||
|
from app.decorators import content_negotiation
|
||||||
from app.models import User
|
from app.models import User
|
||||||
from . import bp
|
from . import bp
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
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.users.settings.forms import (
|
||||||
@@ -10,9 +11,14 @@ 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',
|
||||||
@@ -21,6 +27,7 @@ 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(
|
||||||
@@ -31,6 +38,7 @@ 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(
|
||||||
@@ -41,6 +49,7 @@ 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()
|
||||||
@@ -53,6 +62,7 @@ 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)
|
||||||
|
|||||||
+3
-3
@@ -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 .auth import auth_error_responses, token_auth
|
|
||||||
from .schemas import EmptySchema, JobSchema, SpaCyNLPPipelineJobSchema, TesseractOCRPipelineJobSchema, TesseractOCRPipelineModelSchema
|
from .schemas import EmptySchema, JobSchema, SpaCyNLPPipelineJobSchema, TesseractOCRPipelineJobSchema, TesseractOCRPipelineModelSchema
|
||||||
|
from .auth import auth_error_responses, token_auth
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
+5
-5
@@ -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.email import create_message, send
|
|
||||||
from app import db
|
from app import db
|
||||||
|
from app.email import create_message, send
|
||||||
from app.models import User
|
from app.models import User
|
||||||
from .auth import auth_error_responses, token_auth
|
|
||||||
from .schemas import EmptySchema, UserSchema
|
from .schemas import EmptySchema, UserSchema
|
||||||
|
from .auth import auth_error_responses, token_auth
|
||||||
|
|
||||||
|
|
||||||
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,4 +1,5 @@
|
|||||||
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
|
||||||
@@ -29,6 +30,7 @@ def before_request():
|
|||||||
|
|
||||||
|
|
||||||
@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'))
|
||||||
@@ -65,6 +67,7 @@ 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'))
|
||||||
@@ -95,6 +98,7 @@ 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:
|
||||||
@@ -137,6 +141,7 @@ 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'))
|
||||||
@@ -166,6 +171,7 @@ 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,7 +1,9 @@
|
|||||||
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'))
|
||||||
|
|||||||
@@ -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,4 +1,5 @@
|
|||||||
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
|
||||||
@@ -7,9 +8,13 @@ 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',
|
||||||
@@ -18,6 +23,7 @@ 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():
|
||||||
@@ -51,9 +57,10 @@ 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():
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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,4 +1,5 @@
|
|||||||
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
|
||||||
@@ -7,9 +8,13 @@ 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',
|
||||||
@@ -18,6 +23,7 @@ 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():
|
||||||
@@ -50,9 +56,10 @@ 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():
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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,11 @@
|
|||||||
from datetime import datetime
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
from app import db
|
||||||
|
from app.models import User, Corpus, CorpusFile
|
||||||
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List
|
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:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from inspect import signature
|
|||||||
from threading import Lock
|
from threading import Lock
|
||||||
from typing import Callable, Dict, List, Optional
|
from typing import Callable, Dict, List, Optional
|
||||||
from app import db, docker_client, hashids, socketio
|
from app import db, docker_client, hashids, socketio
|
||||||
from app.extensions.flask_socketio_extras import login_required
|
from app.decorators import socketio_login_required
|
||||||
from app.models import Corpus, CorpusStatus
|
from app.models import Corpus, CorpusStatus
|
||||||
from . import extensions
|
from . import extensions
|
||||||
|
|
||||||
@@ -87,11 +87,11 @@ CQI_API_FUNCTION_NAMES: List[str] = [
|
|||||||
|
|
||||||
|
|
||||||
class CQiNamespace(Namespace):
|
class CQiNamespace(Namespace):
|
||||||
@login_required
|
@socketio_login_required
|
||||||
def on_connect(self):
|
def on_connect(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@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: Optional[Corpus] = Corpus.query.get(db_corpus_id)
|
||||||
@@ -99,7 +99,7 @@ class CQiNamespace(Namespace):
|
|||||||
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,
|
||||||
@@ -134,7 +134,7 @@ class CQiNamespace(Namespace):
|
|||||||
}
|
}
|
||||||
return {'code': 200, 'msg': 'OK'}
|
return {'code': 200, 'msg': 'OK'}
|
||||||
|
|
||||||
@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']
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_socketio import join_room
|
from flask_socketio import join_room
|
||||||
from app import hashids, socketio
|
from app import hashids, socketio
|
||||||
from app.extensions.flask_socketio_extras import login_required
|
from app.decorators import socketio_login_required
|
||||||
from app.models import Corpus
|
from app.models import Corpus
|
||||||
|
|
||||||
|
|
||||||
@socketio.on('GET /corpora/<corpus_id>')
|
@socketio.on('GET /corpora/<corpus_id>')
|
||||||
@login_required
|
@socketio_login_required
|
||||||
def get_corpus(corpus_hashid):
|
def get_corpus(corpus_hashid):
|
||||||
corpus_id = hashids.decode(corpus_hashid)
|
corpus_id = hashids.decode(corpus_hashid)
|
||||||
corpus = Corpus.query.get(corpus_id)
|
corpus = Corpus.query.get(corpus_id)
|
||||||
@@ -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 {
|
||||||
@@ -29,7 +29,7 @@ def get_corpus(corpus_hashid):
|
|||||||
|
|
||||||
|
|
||||||
@socketio.on('SUBSCRIBE /corpora/<corpus_id>')
|
@socketio.on('SUBSCRIBE /corpora/<corpus_id>')
|
||||||
@login_required
|
@socketio_login_required
|
||||||
def subscribe_corpus(corpus_hashid):
|
def subscribe_corpus(corpus_hashid):
|
||||||
corpus_id = hashids.decode(corpus_hashid)
|
corpus_id = hashids.decode(corpus_hashid)
|
||||||
corpus = Corpus.query.get(corpus_id)
|
corpus = Corpus.query.get(corpus_id)
|
||||||
@@ -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 current_app
|
from flask import abort, current_app
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from app.decorators import content_negotiation
|
|
||||||
from app import db
|
from app import db
|
||||||
|
from app.decorators import content_negotiation
|
||||||
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,19 +6,24 @@ 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)
|
||||||
@@ -60,6 +65,7 @@ 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()
|
||||||
@@ -88,6 +94,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,
|
||||||
download_name=corpus_file.filename,
|
attachment_filename=corpus_file.filename,
|
||||||
mimetype=corpus_file.mimetype
|
mimetype=corpus_file.mimetype
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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')
|
||||||
|
|||||||
+13
-2
@@ -1,4 +1,5 @@
|
|||||||
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 (
|
||||||
@@ -10,14 +11,20 @@ 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():
|
||||||
@@ -40,6 +47,7 @@ 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()
|
||||||
@@ -47,13 +55,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,
|
||||||
@@ -79,6 +87,7 @@ 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(
|
||||||
@@ -99,11 +108,13 @@ 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)
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -22,6 +22,31 @@ def admin_required(f):
|
|||||||
return permission_required(Permission.ADMINISTRATE)(f)
|
return permission_required(Permission.ADMINISTRATE)(f)
|
||||||
|
|
||||||
|
|
||||||
|
def socketio_login_required(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
else:
|
||||||
|
return {'code': 401, 'msg': 'Unauthorized'}
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
|
def socketio_permission_required(permission):
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
if not current_user.can(permission):
|
||||||
|
return {'code': 403, 'msg': 'Forbidden'}
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def socketio_admin_required(f):
|
||||||
|
return socketio_permission_required(Permission.ADMINISTRATE)(f)
|
||||||
|
|
||||||
|
|
||||||
def background(f):
|
def background(f):
|
||||||
'''
|
'''
|
||||||
' This decorator executes a function in a Thread.
|
' This decorator executes a function in a Thread.
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
from .container_column import ContainerColumn
|
||||||
|
from .int_enum_column import IntEnumColumn
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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)
|
||||||
-20
@@ -1,26 +1,6 @@
|
|||||||
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 +0,0 @@
|
|||||||
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .decorators import login_required
|
|
||||||
from .decorators import permission_required
|
|
||||||
from .decorators import admin_required
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
from flask_login import current_user
|
|
||||||
from functools import wraps
|
|
||||||
from app.models import Permission as UserPermission
|
|
||||||
|
|
||||||
|
|
||||||
def login_required(f):
|
|
||||||
@wraps(f)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
if current_user.is_authenticated:
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
return {'code': 401, 'body': 'Unauthorized'}
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
def permission_required(permission):
|
|
||||||
def decorator(f):
|
|
||||||
@wraps(f)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
if not current_user.can(permission):
|
|
||||||
return {'code': 403, 'body': 'Forbidden'}
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
return wrapper
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
def admin_required(f):
|
|
||||||
return permission_required(UserPermission.ADMINISTRATE)(f)
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
from .types import ContainerColumn
|
|
||||||
from .types import IntEnumColumn
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
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"'}}
|
||||||
|
|||||||
+9
-5
@@ -5,20 +5,24 @@ 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 corpora():
|
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',
|
||||||
@@ -30,13 +34,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,
|
||||||
download_name=job_input.filename,
|
attachment_filename=job_input.filename,
|
||||||
mimetype=job_input.mimetype
|
mimetype=job_input.mimetype
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -44,12 +48,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,
|
||||||
download_name=job_result.filename,
|
attachment_filename=job_result.filename,
|
||||||
mimetype=job_result.mimetype
|
mimetype=job_result.mimetype
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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,11 +1,14 @@
|
|||||||
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.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():
|
||||||
@@ -24,6 +27,7 @@ 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',
|
||||||
@@ -32,6 +36,7 @@ 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(
|
||||||
@@ -41,6 +46,7 @@ def dashboard():
|
|||||||
|
|
||||||
|
|
||||||
@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',
|
||||||
@@ -49,6 +55,7 @@ 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',
|
||||||
@@ -57,6 +64,7 @@ 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',
|
||||||
@@ -65,6 +73,7 @@ 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
-140
@@ -1,7 +1,3 @@
|
|||||||
from enum import Enum
|
|
||||||
from flask_login import AnonymousUserMixin
|
|
||||||
from app import db, login, mail, socketio
|
|
||||||
from app.email import create_message
|
|
||||||
from .avatar import *
|
from .avatar import *
|
||||||
from .corpus_file import *
|
from .corpus_file import *
|
||||||
from .corpus_follower_association import *
|
from .corpus_follower_association import *
|
||||||
@@ -15,142 +11,7 @@ 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
|
||||||
|
|
||||||
@db.event.listens_for(Corpus, 'after_delete')
|
|
||||||
@db.event.listens_for(CorpusFile, 'after_delete')
|
|
||||||
@db.event.listens_for(Job, 'after_delete')
|
|
||||||
@db.event.listens_for(JobInput, 'after_delete')
|
|
||||||
@db.event.listens_for(JobResult, 'after_delete')
|
|
||||||
@db.event.listens_for(SpaCyNLPPipelineModel, 'after_delete')
|
|
||||||
@db.event.listens_for(TesseractOCRPipelineModel, 'after_delete')
|
|
||||||
def resource_after_delete(mapper, connection, resource):
|
|
||||||
print('[START] resource_after_delete')
|
|
||||||
jsonpatch = [
|
|
||||||
{
|
|
||||||
'op': 'remove',
|
|
||||||
'path': resource.jsonpatch_path
|
|
||||||
}
|
|
||||||
]
|
|
||||||
room = f'/users/{resource.user_hashid}'
|
|
||||||
print('[EMIT] PATCH', jsonpatch)
|
|
||||||
socketio.emit('PATCH', jsonpatch, room=room)
|
|
||||||
print('[END] resource_after_delete')
|
|
||||||
|
|
||||||
|
|
||||||
@db.event.listens_for(CorpusFollowerAssociation, 'after_delete')
|
|
||||||
def cfa_after_delete_handler(mapper, connection, cfa):
|
|
||||||
jsonpatch_path = f'/users/{cfa.corpus.user.hashid}/corpora/{cfa.corpus.hashid}/corpus_follower_associations/{cfa.hashid}'
|
|
||||||
jsonpatch = [
|
|
||||||
{
|
|
||||||
'op': 'remove',
|
|
||||||
'path': jsonpatch_path
|
|
||||||
}
|
|
||||||
]
|
|
||||||
room = f'/users/{cfa.corpus.user.hashid}'
|
|
||||||
socketio.emit('PATCH', jsonpatch, room=room)
|
|
||||||
|
|
||||||
|
|
||||||
@db.event.listens_for(Corpus, 'after_insert')
|
|
||||||
@db.event.listens_for(CorpusFile, 'after_insert')
|
|
||||||
@db.event.listens_for(Job, 'after_insert')
|
|
||||||
@db.event.listens_for(JobInput, 'after_insert')
|
|
||||||
@db.event.listens_for(JobResult, 'after_insert')
|
|
||||||
@db.event.listens_for(SpaCyNLPPipelineModel, 'after_insert')
|
|
||||||
@db.event.listens_for(TesseractOCRPipelineModel, 'after_insert')
|
|
||||||
def resource_after_insert_handler(mapper, connection, resource):
|
|
||||||
jsonpatch_value = resource.to_json_serializeable()
|
|
||||||
for attr in mapper.relationships:
|
|
||||||
jsonpatch_value[attr.key] = {}
|
|
||||||
jsonpatch = [
|
|
||||||
{
|
|
||||||
'op': 'add',
|
|
||||||
'path': resource.jsonpatch_path,
|
|
||||||
'value': jsonpatch_value
|
|
||||||
}
|
|
||||||
]
|
|
||||||
room = f'/users/{resource.user_hashid}'
|
|
||||||
socketio.emit('PATCH', jsonpatch, room=room)
|
|
||||||
|
|
||||||
|
|
||||||
@db.event.listens_for(CorpusFollowerAssociation, 'after_insert')
|
|
||||||
def cfa_after_insert_handler(mapper, connection, cfa):
|
|
||||||
jsonpatch_value = cfa.to_json_serializeable()
|
|
||||||
jsonpatch_path = f'/users/{cfa.corpus.user.hashid}/corpora/{cfa.corpus.hashid}/corpus_follower_associations/{cfa.hashid}'
|
|
||||||
jsonpatch = [
|
|
||||||
{
|
|
||||||
'op': 'add',
|
|
||||||
'path': jsonpatch_path,
|
|
||||||
'value': jsonpatch_value
|
|
||||||
}
|
|
||||||
]
|
|
||||||
room = f'/users/{cfa.corpus.user.hashid}'
|
|
||||||
socketio.emit('PATCH', jsonpatch, room=room)
|
|
||||||
|
|
||||||
|
|
||||||
@db.event.listens_for(Corpus, 'after_update')
|
|
||||||
@db.event.listens_for(CorpusFile, 'after_update')
|
|
||||||
@db.event.listens_for(Job, 'after_update')
|
|
||||||
@db.event.listens_for(JobInput, 'after_update')
|
|
||||||
@db.event.listens_for(JobResult, 'after_update')
|
|
||||||
@db.event.listens_for(SpaCyNLPPipelineModel, 'after_update')
|
|
||||||
@db.event.listens_for(TesseractOCRPipelineModel, 'after_update')
|
|
||||||
def resource_after_update_handler(mapper, connection, resource):
|
|
||||||
jsonpatch = []
|
|
||||||
for attr in db.inspect(resource).attrs:
|
|
||||||
if attr.key in mapper.relationships:
|
|
||||||
continue
|
|
||||||
if not attr.load_history().has_changes():
|
|
||||||
continue
|
|
||||||
jsonpatch_path = f'{resource.jsonpatch_path}/{attr.key}'
|
|
||||||
if isinstance(attr.value, datetime):
|
|
||||||
jsonpatch_value = f'{attr.value.isoformat()}Z'
|
|
||||||
elif isinstance(attr.value, Enum):
|
|
||||||
jsonpatch_value = attr.value.name
|
|
||||||
else:
|
|
||||||
jsonpatch_value = attr.value
|
|
||||||
jsonpatch.append(
|
|
||||||
{
|
|
||||||
'op': 'replace',
|
|
||||||
'path': jsonpatch_path,
|
|
||||||
'value': jsonpatch_value
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if jsonpatch:
|
|
||||||
room = f'/users/{resource.user_hashid}'
|
|
||||||
socketio.emit('PATCH', jsonpatch, room=room)
|
|
||||||
|
|
||||||
|
|
||||||
@db.event.listens_for(Job, 'after_update')
|
|
||||||
def job_after_update_handler(mapper, connection, job):
|
|
||||||
for attr in db.inspect(job).attrs:
|
|
||||||
if attr.key != 'status':
|
|
||||||
continue
|
|
||||||
if not attr.load_history().has_changes():
|
|
||||||
return
|
|
||||||
if job.user.setting_job_status_mail_notification_level == UserSettingJobStatusMailNotificationLevel.NONE:
|
|
||||||
return
|
|
||||||
if job.user.setting_job_status_mail_notification_level == UserSettingJobStatusMailNotificationLevel.END:
|
|
||||||
if job.status not in [JobStatus.COMPLETED, JobStatus.FAILED]:
|
|
||||||
return
|
|
||||||
msg = create_message(
|
|
||||||
job.user.email,
|
|
||||||
f'Status update for your Job "{job.title}"',
|
|
||||||
'tasks/email/notification',
|
|
||||||
job=job
|
|
||||||
)
|
|
||||||
mail.send(msg)
|
|
||||||
|
|
||||||
|
|
||||||
class AnonymousUser(AnonymousUserMixin):
|
|
||||||
def can(self, permissions):
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_administrator(self):
|
|
||||||
return False
|
|
||||||
|
|
||||||
login.anonymous_user = AnonymousUser
|
|
||||||
|
|
||||||
|
|
||||||
@login.user_loader
|
@login.user_loader
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import shutil
|
|||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from app import db
|
from app import db
|
||||||
from app.converters.vrt import normalize_vrt_file
|
from app.converters.vrt import normalize_vrt_file
|
||||||
from app.extensions.sqlalchemy_extras import IntEnumColumn
|
from app.ext.flask_sqlalchemy import IntEnumColumn
|
||||||
from .corpus_follower_association import CorpusFollowerAssociation
|
from .corpus_follower_association import CorpusFollowerAssociation
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ 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.extensions.sqlalchemy_extras import ContainerColumn, IntEnumColumn
|
from app.ext.flask_sqlalchemy import ContainerColumn, IntEnumColumn
|
||||||
|
|
||||||
|
|
||||||
class JobStatus(IntEnum):
|
class JobStatus(IntEnum):
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from pathlib import Path
|
|||||||
import requests
|
import requests
|
||||||
import yaml
|
import yaml
|
||||||
from app import db
|
from app import db
|
||||||
from app.extensions.sqlalchemy_extras import ContainerColumn
|
from app.ext.flask_sqlalchemy import ContainerColumn
|
||||||
from .file_mixin import FileMixin
|
from .file_mixin import FileMixin
|
||||||
from .user import User
|
from .user import User
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from pathlib import Path
|
|||||||
import requests
|
import requests
|
||||||
import yaml
|
import yaml
|
||||||
from app import db
|
from app import db
|
||||||
from app.extensions.sqlalchemy_extras import ContainerColumn
|
from app.ext.flask_sqlalchemy import ContainerColumn
|
||||||
from .file_mixin import FileMixin
|
from .file_mixin import FileMixin
|
||||||
from .user import User
|
from .user import User
|
||||||
|
|
||||||
|
|||||||
+5
-7
@@ -12,7 +12,7 @@ import re
|
|||||||
import secrets
|
import secrets
|
||||||
import shutil
|
import shutil
|
||||||
from app import db, hashids
|
from app import db, hashids
|
||||||
from app.extensions.sqlalchemy_extras import IntEnumColumn
|
from app.ext.flask_sqlalchemy import IntEnumColumn
|
||||||
from .corpus import Corpus
|
from .corpus import Corpus
|
||||||
from .corpus_follower_association import CorpusFollowerAssociation
|
from .corpus_follower_association import CorpusFollowerAssociation
|
||||||
from .corpus_follower_role import CorpusFollowerRole
|
from .corpus_follower_role import CorpusFollowerRole
|
||||||
@@ -132,10 +132,6 @@ class User(HashidMixin, UserMixin, db.Model):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<User {self.username}>'
|
return f'<User {self.username}>'
|
||||||
|
|
||||||
@property
|
|
||||||
def is_administrator(self):
|
|
||||||
return self.can(Permission.ADMINISTRATE)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def jsonpatch_path(self):
|
def jsonpatch_path(self):
|
||||||
return f'/users/{self.hashid}'
|
return f'/users/{self.hashid}'
|
||||||
@@ -146,8 +142,7 @@ class User(HashidMixin, UserMixin, db.Model):
|
|||||||
|
|
||||||
@password.setter
|
@password.setter
|
||||||
def password(self, password):
|
def password(self, password):
|
||||||
#pbkdf2:sha256
|
self.password_hash = generate_password_hash(password)
|
||||||
self.password_hash = generate_password_hash(password, method='pbkdf2')
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path(self) -> Path:
|
def path(self) -> Path:
|
||||||
@@ -299,6 +294,9 @@ class User(HashidMixin, UserMixin, db.Model):
|
|||||||
algorithm='HS256'
|
algorithm='HS256'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def is_administrator(self):
|
||||||
|
return self.can(Permission.ADMINISTRATE)
|
||||||
|
|
||||||
def ping(self):
|
def ping(self):
|
||||||
self.last_seen = datetime.utcnow()
|
self.last_seen = datetime.utcnow()
|
||||||
|
|
||||||
|
|||||||
+16
-5
@@ -1,10 +1,12 @@
|
|||||||
from flask import abort, current_app, flash, redirect, render_template, request, url_for
|
from flask import abort, current_app, flash, Markup, 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
|
||||||
@@ -19,11 +21,13 @@ 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]
|
||||||
@@ -53,7 +57,7 @@ def file_setup_pipeline():
|
|||||||
abort(500)
|
abort(500)
|
||||||
job.status = JobStatus.SUBMITTED
|
job.status = JobStatus.SUBMITTED
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
message = f'Job "<a href="{job.url}">{job.title}</a>" created'
|
message = Markup(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(
|
||||||
@@ -64,12 +68,15 @@ 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():
|
||||||
@@ -96,7 +103,7 @@ def tesseract_ocr_pipeline():
|
|||||||
abort(500)
|
abort(500)
|
||||||
job.status = JobStatus.SUBMITTED
|
job.status = JobStatus.SUBMITTED
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
message = f'Job "<a href="{job.url}">{job.title}</a>" created'
|
message = Markup(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 = [
|
||||||
@@ -107,6 +114,7 @@ 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
|
||||||
@@ -114,6 +122,7 @@ 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)
|
||||||
@@ -159,7 +168,7 @@ def transkribus_htr_pipeline():
|
|||||||
abort(500)
|
abort(500)
|
||||||
job.status = JobStatus.SUBMITTED
|
job.status = JobStatus.SUBMITTED
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
message = f'Job "<a href="{job.url}">{job.title}</a>" created'
|
message = Markup(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(
|
||||||
@@ -171,6 +180,7 @@ 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]
|
||||||
@@ -204,7 +214,7 @@ def spacy_nlp_pipeline():
|
|||||||
abort(500)
|
abort(500)
|
||||||
job.status = JobStatus.SUBMITTED
|
job.status = JobStatus.SUBMITTED
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
message = f'Job "<a href="{job.url}">{job.title}</a>" created'
|
message = Markup(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(
|
||||||
@@ -217,6 +227,7 @@ 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,10 +1,12 @@
|
|||||||
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.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)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* Spacing
|
* Spacing
|
||||||
*/
|
*/
|
||||||
$spacing-shortcuts: ("margin": "m", "padding": "p");
|
$spacing-shortcuts: ("margin": "mg", "padding": "pd");
|
||||||
$spacing-directions: ("top": "t", "right": "r", "bottom": "b", "left": "l");
|
$spacing-directions: ("top": "t", "right": "r", "bottom": "b", "left": "l");
|
||||||
$spacing-values: ("0": 0, "1": 0.25rem, "2": 0.5rem, "3": 0.75rem, "4": 1rem, "5": 1.5rem, "6": 3rem, "auto": auto);
|
$spacing-values: ("0": 0, "1": 0.25rem, "2": 0.5rem, "3": 0.75rem, "4": 1rem, "5": 1.5rem, "6": 3rem, "auto": auto);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* The sidenav-fixed class is used which causes the sidenav to be fixed and open
|
||||||
|
* on large screens and hides to the regular functionality on smaller screens.
|
||||||
|
* In order to prevent the sidenav to overlap the content, the content (in our
|
||||||
|
* case header, main and footer) gets an offset equal to the width of the
|
||||||
|
* sidenav.
|
||||||
|
*/
|
||||||
|
@media only screen and (min-width : 993px) {
|
||||||
|
header, main, footer {padding-left: 300px;}
|
||||||
|
.modal:not(.bottom-sheet) {left: 300px;}
|
||||||
|
.navbar-fixed > nav {width: calc(100% - 300px)}
|
||||||
|
}
|
||||||
+1
-1
@@ -9,8 +9,8 @@
|
|||||||
*/
|
*/
|
||||||
body {
|
body {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 100vh;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
@@ -2,13 +2,9 @@
|
|||||||
font-family: 'Nopaque Icons';
|
font-family: 'Nopaque Icons';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
/* For IE6-8 */
|
|
||||||
/* src: url("../font/NopaqueIcons-Regular.eot"); */
|
|
||||||
src: local('nopaque Icons'),
|
src: local('nopaque Icons'),
|
||||||
local('NopaqueIcons-Regular'),
|
local('NopaqueIcons-Regular'),
|
||||||
url("../font/NopaqueIcons-Regular.woff2") format('woff2'),
|
url(../fonts/nopaque_icons/NopaqueIcons-Regular.otf) format('opentype');
|
||||||
url("../font/NopaqueIcons-Regular.woff") format('woff'),
|
|
||||||
url("../font/nopaque_icons/NopaqueIcons-Regular.otf") format('opentype');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nopaque-icons {
|
.nopaque-icons {
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
.scroll {overflow: auto;}
|
/* Change navbar height bacause an extended and fixed navbar is used */
|
||||||
|
.navbar-fixed {
|
||||||
.clearfix::after {
|
height: 112px;
|
||||||
display: block;
|
|
||||||
content: "";
|
|
||||||
clear: both;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Change placholdertext color of file uplaod fields */
|
/* Change placholdertext color of file uplaod fields */
|
||||||
|
|||||||
-22
@@ -1,22 +0,0 @@
|
|||||||
(The MIT License)
|
|
||||||
|
|
||||||
Copyright (c) 2013, 2014, 2020 Joachim Wester
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining
|
|
||||||
a copy of this software and associated documentation files (the
|
|
||||||
'Software'), to deal in the Software without restriction, including
|
|
||||||
without limitation the rights to use, copy, modify, merge, publish,
|
|
||||||
distribute, sublicense, and/or sell copies of the Software, and to
|
|
||||||
permit persons to whom the Software is furnished to do so, subject to
|
|
||||||
the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be
|
|
||||||
included in all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
||||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
||||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
||||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
||||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
3.1.1
|
|
||||||
@@ -1,928 +0,0 @@
|
|||||||
/*! fast-json-patch, version: 3.1.1 */
|
|
||||||
var jsonpatch =
|
|
||||||
/******/ (function(modules) { // webpackBootstrap
|
|
||||||
/******/ // The module cache
|
|
||||||
/******/ var installedModules = {};
|
|
||||||
/******/
|
|
||||||
/******/ // The require function
|
|
||||||
/******/ function __webpack_require__(moduleId) {
|
|
||||||
/******/
|
|
||||||
/******/ // Check if module is in cache
|
|
||||||
/******/ if(installedModules[moduleId]) {
|
|
||||||
/******/ return installedModules[moduleId].exports;
|
|
||||||
/******/ }
|
|
||||||
/******/ // Create a new module (and put it into the cache)
|
|
||||||
/******/ var module = installedModules[moduleId] = {
|
|
||||||
/******/ i: moduleId,
|
|
||||||
/******/ l: false,
|
|
||||||
/******/ exports: {}
|
|
||||||
/******/ };
|
|
||||||
/******/
|
|
||||||
/******/ // Execute the module function
|
|
||||||
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
|
||||||
/******/
|
|
||||||
/******/ // Flag the module as loaded
|
|
||||||
/******/ module.l = true;
|
|
||||||
/******/
|
|
||||||
/******/ // Return the exports of the module
|
|
||||||
/******/ return module.exports;
|
|
||||||
/******/ }
|
|
||||||
/******/
|
|
||||||
/******/
|
|
||||||
/******/ // expose the modules object (__webpack_modules__)
|
|
||||||
/******/ __webpack_require__.m = modules;
|
|
||||||
/******/
|
|
||||||
/******/ // expose the module cache
|
|
||||||
/******/ __webpack_require__.c = installedModules;
|
|
||||||
/******/
|
|
||||||
/******/ // define getter function for harmony exports
|
|
||||||
/******/ __webpack_require__.d = function(exports, name, getter) {
|
|
||||||
/******/ if(!__webpack_require__.o(exports, name)) {
|
|
||||||
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
|
|
||||||
/******/ }
|
|
||||||
/******/ };
|
|
||||||
/******/
|
|
||||||
/******/ // define __esModule on exports
|
|
||||||
/******/ __webpack_require__.r = function(exports) {
|
|
||||||
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
|
|
||||||
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
||||||
/******/ }
|
|
||||||
/******/ Object.defineProperty(exports, '__esModule', { value: true });
|
|
||||||
/******/ };
|
|
||||||
/******/
|
|
||||||
/******/ // create a fake namespace object
|
|
||||||
/******/ // mode & 1: value is a module id, require it
|
|
||||||
/******/ // mode & 2: merge all properties of value into the ns
|
|
||||||
/******/ // mode & 4: return value when already ns object
|
|
||||||
/******/ // mode & 8|1: behave like require
|
|
||||||
/******/ __webpack_require__.t = function(value, mode) {
|
|
||||||
/******/ if(mode & 1) value = __webpack_require__(value);
|
|
||||||
/******/ if(mode & 8) return value;
|
|
||||||
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
|
|
||||||
/******/ var ns = Object.create(null);
|
|
||||||
/******/ __webpack_require__.r(ns);
|
|
||||||
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
|
|
||||||
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
|
|
||||||
/******/ return ns;
|
|
||||||
/******/ };
|
|
||||||
/******/
|
|
||||||
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
|
||||||
/******/ __webpack_require__.n = function(module) {
|
|
||||||
/******/ var getter = module && module.__esModule ?
|
|
||||||
/******/ function getDefault() { return module['default']; } :
|
|
||||||
/******/ function getModuleExports() { return module; };
|
|
||||||
/******/ __webpack_require__.d(getter, 'a', getter);
|
|
||||||
/******/ return getter;
|
|
||||||
/******/ };
|
|
||||||
/******/
|
|
||||||
/******/ // Object.prototype.hasOwnProperty.call
|
|
||||||
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
|
|
||||||
/******/
|
|
||||||
/******/ // __webpack_public_path__
|
|
||||||
/******/ __webpack_require__.p = "";
|
|
||||||
/******/
|
|
||||||
/******/
|
|
||||||
/******/ // Load entry module and return exports
|
|
||||||
/******/ return __webpack_require__(__webpack_require__.s = 2);
|
|
||||||
/******/ })
|
|
||||||
/************************************************************************/
|
|
||||||
/******/ ([
|
|
||||||
/* 0 */
|
|
||||||
/***/ (function(module, exports) {
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* https://github.com/Starcounter-Jack/JSON-Patch
|
|
||||||
* (c) 2017-2022 Joachim Wester
|
|
||||||
* MIT licensed
|
|
||||||
*/
|
|
||||||
var __extends = (this && this.__extends) || (function () {
|
|
||||||
var extendStatics = function (d, b) {
|
|
||||||
extendStatics = Object.setPrototypeOf ||
|
|
||||||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
|
||||||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
|
|
||||||
return extendStatics(d, b);
|
|
||||||
};
|
|
||||||
return function (d, b) {
|
|
||||||
extendStatics(d, b);
|
|
||||||
function __() { this.constructor = d; }
|
|
||||||
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
var _hasOwnProperty = Object.prototype.hasOwnProperty;
|
|
||||||
function hasOwnProperty(obj, key) {
|
|
||||||
return _hasOwnProperty.call(obj, key);
|
|
||||||
}
|
|
||||||
exports.hasOwnProperty = hasOwnProperty;
|
|
||||||
function _objectKeys(obj) {
|
|
||||||
if (Array.isArray(obj)) {
|
|
||||||
var keys_1 = new Array(obj.length);
|
|
||||||
for (var k = 0; k < keys_1.length; k++) {
|
|
||||||
keys_1[k] = "" + k;
|
|
||||||
}
|
|
||||||
return keys_1;
|
|
||||||
}
|
|
||||||
if (Object.keys) {
|
|
||||||
return Object.keys(obj);
|
|
||||||
}
|
|
||||||
var keys = [];
|
|
||||||
for (var i in obj) {
|
|
||||||
if (hasOwnProperty(obj, i)) {
|
|
||||||
keys.push(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return keys;
|
|
||||||
}
|
|
||||||
exports._objectKeys = _objectKeys;
|
|
||||||
;
|
|
||||||
/**
|
|
||||||
* Deeply clone the object.
|
|
||||||
* https://jsperf.com/deep-copy-vs-json-stringify-json-parse/25 (recursiveDeepCopy)
|
|
||||||
* @param {any} obj value to clone
|
|
||||||
* @return {any} cloned obj
|
|
||||||
*/
|
|
||||||
function _deepClone(obj) {
|
|
||||||
switch (typeof obj) {
|
|
||||||
case "object":
|
|
||||||
return JSON.parse(JSON.stringify(obj)); //Faster than ES5 clone - http://jsperf.com/deep-cloning-of-objects/5
|
|
||||||
case "undefined":
|
|
||||||
return null; //this is how JSON.stringify behaves for array items
|
|
||||||
default:
|
|
||||||
return obj; //no need to clone primitives
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exports._deepClone = _deepClone;
|
|
||||||
//3x faster than cached /^\d+$/.test(str)
|
|
||||||
function isInteger(str) {
|
|
||||||
var i = 0;
|
|
||||||
var len = str.length;
|
|
||||||
var charCode;
|
|
||||||
while (i < len) {
|
|
||||||
charCode = str.charCodeAt(i);
|
|
||||||
if (charCode >= 48 && charCode <= 57) {
|
|
||||||
i++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
exports.isInteger = isInteger;
|
|
||||||
/**
|
|
||||||
* Escapes a json pointer path
|
|
||||||
* @param path The raw pointer
|
|
||||||
* @return the Escaped path
|
|
||||||
*/
|
|
||||||
function escapePathComponent(path) {
|
|
||||||
if (path.indexOf('/') === -1 && path.indexOf('~') === -1)
|
|
||||||
return path;
|
|
||||||
return path.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
||||||
}
|
|
||||||
exports.escapePathComponent = escapePathComponent;
|
|
||||||
/**
|
|
||||||
* Unescapes a json pointer path
|
|
||||||
* @param path The escaped pointer
|
|
||||||
* @return The unescaped path
|
|
||||||
*/
|
|
||||||
function unescapePathComponent(path) {
|
|
||||||
return path.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
||||||
}
|
|
||||||
exports.unescapePathComponent = unescapePathComponent;
|
|
||||||
function _getPathRecursive(root, obj) {
|
|
||||||
var found;
|
|
||||||
for (var key in root) {
|
|
||||||
if (hasOwnProperty(root, key)) {
|
|
||||||
if (root[key] === obj) {
|
|
||||||
return escapePathComponent(key) + '/';
|
|
||||||
}
|
|
||||||
else if (typeof root[key] === 'object') {
|
|
||||||
found = _getPathRecursive(root[key], obj);
|
|
||||||
if (found != '') {
|
|
||||||
return escapePathComponent(key) + '/' + found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
exports._getPathRecursive = _getPathRecursive;
|
|
||||||
function getPath(root, obj) {
|
|
||||||
if (root === obj) {
|
|
||||||
return '/';
|
|
||||||
}
|
|
||||||
var path = _getPathRecursive(root, obj);
|
|
||||||
if (path === '') {
|
|
||||||
throw new Error("Object not found in root");
|
|
||||||
}
|
|
||||||
return "/" + path;
|
|
||||||
}
|
|
||||||
exports.getPath = getPath;
|
|
||||||
/**
|
|
||||||
* Recursively checks whether an object has any undefined values inside.
|
|
||||||
*/
|
|
||||||
function hasUndefined(obj) {
|
|
||||||
if (obj === undefined) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (obj) {
|
|
||||||
if (Array.isArray(obj)) {
|
|
||||||
for (var i_1 = 0, len = obj.length; i_1 < len; i_1++) {
|
|
||||||
if (hasUndefined(obj[i_1])) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (typeof obj === "object") {
|
|
||||||
var objKeys = _objectKeys(obj);
|
|
||||||
var objKeysLength = objKeys.length;
|
|
||||||
for (var i = 0; i < objKeysLength; i++) {
|
|
||||||
if (hasUndefined(obj[objKeys[i]])) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
exports.hasUndefined = hasUndefined;
|
|
||||||
function patchErrorMessageFormatter(message, args) {
|
|
||||||
var messageParts = [message];
|
|
||||||
for (var key in args) {
|
|
||||||
var value = typeof args[key] === 'object' ? JSON.stringify(args[key], null, 2) : args[key]; // pretty print
|
|
||||||
if (typeof value !== 'undefined') {
|
|
||||||
messageParts.push(key + ": " + value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return messageParts.join('\n');
|
|
||||||
}
|
|
||||||
var PatchError = /** @class */ (function (_super) {
|
|
||||||
__extends(PatchError, _super);
|
|
||||||
function PatchError(message, name, index, operation, tree) {
|
|
||||||
var _newTarget = this.constructor;
|
|
||||||
var _this = _super.call(this, patchErrorMessageFormatter(message, { name: name, index: index, operation: operation, tree: tree })) || this;
|
|
||||||
_this.name = name;
|
|
||||||
_this.index = index;
|
|
||||||
_this.operation = operation;
|
|
||||||
_this.tree = tree;
|
|
||||||
Object.setPrototypeOf(_this, _newTarget.prototype); // restore prototype chain, see https://stackoverflow.com/a/48342359
|
|
||||||
_this.message = patchErrorMessageFormatter(message, { name: name, index: index, operation: operation, tree: tree });
|
|
||||||
return _this;
|
|
||||||
}
|
|
||||||
return PatchError;
|
|
||||||
}(Error));
|
|
||||||
exports.PatchError = PatchError;
|
|
||||||
|
|
||||||
|
|
||||||
/***/ }),
|
|
||||||
/* 1 */
|
|
||||||
/***/ (function(module, exports, __webpack_require__) {
|
|
||||||
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
var helpers_js_1 = __webpack_require__(0);
|
|
||||||
exports.JsonPatchError = helpers_js_1.PatchError;
|
|
||||||
exports.deepClone = helpers_js_1._deepClone;
|
|
||||||
/* We use a Javascript hash to store each
|
|
||||||
function. Each hash entry (property) uses
|
|
||||||
the operation identifiers specified in rfc6902.
|
|
||||||
In this way, we can map each patch operation
|
|
||||||
to its dedicated function in efficient way.
|
|
||||||
*/
|
|
||||||
/* The operations applicable to an object */
|
|
||||||
var objOps = {
|
|
||||||
add: function (obj, key, document) {
|
|
||||||
obj[key] = this.value;
|
|
||||||
return { newDocument: document };
|
|
||||||
},
|
|
||||||
remove: function (obj, key, document) {
|
|
||||||
var removed = obj[key];
|
|
||||||
delete obj[key];
|
|
||||||
return { newDocument: document, removed: removed };
|
|
||||||
},
|
|
||||||
replace: function (obj, key, document) {
|
|
||||||
var removed = obj[key];
|
|
||||||
obj[key] = this.value;
|
|
||||||
return { newDocument: document, removed: removed };
|
|
||||||
},
|
|
||||||
move: function (obj, key, document) {
|
|
||||||
/* in case move target overwrites an existing value,
|
|
||||||
return the removed value, this can be taxing performance-wise,
|
|
||||||
and is potentially unneeded */
|
|
||||||
var removed = getValueByPointer(document, this.path);
|
|
||||||
if (removed) {
|
|
||||||
removed = helpers_js_1._deepClone(removed);
|
|
||||||
}
|
|
||||||
var originalValue = applyOperation(document, { op: "remove", path: this.from }).removed;
|
|
||||||
applyOperation(document, { op: "add", path: this.path, value: originalValue });
|
|
||||||
return { newDocument: document, removed: removed };
|
|
||||||
},
|
|
||||||
copy: function (obj, key, document) {
|
|
||||||
var valueToCopy = getValueByPointer(document, this.from);
|
|
||||||
// enforce copy by value so further operations don't affect source (see issue #177)
|
|
||||||
applyOperation(document, { op: "add", path: this.path, value: helpers_js_1._deepClone(valueToCopy) });
|
|
||||||
return { newDocument: document };
|
|
||||||
},
|
|
||||||
test: function (obj, key, document) {
|
|
||||||
return { newDocument: document, test: _areEquals(obj[key], this.value) };
|
|
||||||
},
|
|
||||||
_get: function (obj, key, document) {
|
|
||||||
this.value = obj[key];
|
|
||||||
return { newDocument: document };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
/* The operations applicable to an array. Many are the same as for the object */
|
|
||||||
var arrOps = {
|
|
||||||
add: function (arr, i, document) {
|
|
||||||
if (helpers_js_1.isInteger(i)) {
|
|
||||||
arr.splice(i, 0, this.value);
|
|
||||||
}
|
|
||||||
else { // array props
|
|
||||||
arr[i] = this.value;
|
|
||||||
}
|
|
||||||
// this may be needed when using '-' in an array
|
|
||||||
return { newDocument: document, index: i };
|
|
||||||
},
|
|
||||||
remove: function (arr, i, document) {
|
|
||||||
var removedList = arr.splice(i, 1);
|
|
||||||
return { newDocument: document, removed: removedList[0] };
|
|
||||||
},
|
|
||||||
replace: function (arr, i, document) {
|
|
||||||
var removed = arr[i];
|
|
||||||
arr[i] = this.value;
|
|
||||||
return { newDocument: document, removed: removed };
|
|
||||||
},
|
|
||||||
move: objOps.move,
|
|
||||||
copy: objOps.copy,
|
|
||||||
test: objOps.test,
|
|
||||||
_get: objOps._get
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Retrieves a value from a JSON document by a JSON pointer.
|
|
||||||
* Returns the value.
|
|
||||||
*
|
|
||||||
* @param document The document to get the value from
|
|
||||||
* @param pointer an escaped JSON pointer
|
|
||||||
* @return The retrieved value
|
|
||||||
*/
|
|
||||||
function getValueByPointer(document, pointer) {
|
|
||||||
if (pointer == '') {
|
|
||||||
return document;
|
|
||||||
}
|
|
||||||
var getOriginalDestination = { op: "_get", path: pointer };
|
|
||||||
applyOperation(document, getOriginalDestination);
|
|
||||||
return getOriginalDestination.value;
|
|
||||||
}
|
|
||||||
exports.getValueByPointer = getValueByPointer;
|
|
||||||
/**
|
|
||||||
* Apply a single JSON Patch Operation on a JSON document.
|
|
||||||
* Returns the {newDocument, result} of the operation.
|
|
||||||
* It modifies the `document` and `operation` objects - it gets the values by reference.
|
|
||||||
* If you would like to avoid touching your values, clone them:
|
|
||||||
* `jsonpatch.applyOperation(document, jsonpatch._deepClone(operation))`.
|
|
||||||
*
|
|
||||||
* @param document The document to patch
|
|
||||||
* @param operation The operation to apply
|
|
||||||
* @param validateOperation `false` is without validation, `true` to use default jsonpatch's validation, or you can pass a `validateOperation` callback to be used for validation.
|
|
||||||
* @param mutateDocument Whether to mutate the original document or clone it before applying
|
|
||||||
* @param banPrototypeModifications Whether to ban modifications to `__proto__`, defaults to `true`.
|
|
||||||
* @return `{newDocument, result}` after the operation
|
|
||||||
*/
|
|
||||||
function applyOperation(document, operation, validateOperation, mutateDocument, banPrototypeModifications, index) {
|
|
||||||
if (validateOperation === void 0) { validateOperation = false; }
|
|
||||||
if (mutateDocument === void 0) { mutateDocument = true; }
|
|
||||||
if (banPrototypeModifications === void 0) { banPrototypeModifications = true; }
|
|
||||||
if (index === void 0) { index = 0; }
|
|
||||||
if (validateOperation) {
|
|
||||||
if (typeof validateOperation == 'function') {
|
|
||||||
validateOperation(operation, 0, document, operation.path);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
validator(operation, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* ROOT OPERATIONS */
|
|
||||||
if (operation.path === "") {
|
|
||||||
var returnValue = { newDocument: document };
|
|
||||||
if (operation.op === 'add') {
|
|
||||||
returnValue.newDocument = operation.value;
|
|
||||||
return returnValue;
|
|
||||||
}
|
|
||||||
else if (operation.op === 'replace') {
|
|
||||||
returnValue.newDocument = operation.value;
|
|
||||||
returnValue.removed = document; //document we removed
|
|
||||||
return returnValue;
|
|
||||||
}
|
|
||||||
else if (operation.op === 'move' || operation.op === 'copy') { // it's a move or copy to root
|
|
||||||
returnValue.newDocument = getValueByPointer(document, operation.from); // get the value by json-pointer in `from` field
|
|
||||||
if (operation.op === 'move') { // report removed item
|
|
||||||
returnValue.removed = document;
|
|
||||||
}
|
|
||||||
return returnValue;
|
|
||||||
}
|
|
||||||
else if (operation.op === 'test') {
|
|
||||||
returnValue.test = _areEquals(document, operation.value);
|
|
||||||
if (returnValue.test === false) {
|
|
||||||
throw new exports.JsonPatchError("Test operation failed", 'TEST_OPERATION_FAILED', index, operation, document);
|
|
||||||
}
|
|
||||||
returnValue.newDocument = document;
|
|
||||||
return returnValue;
|
|
||||||
}
|
|
||||||
else if (operation.op === 'remove') { // a remove on root
|
|
||||||
returnValue.removed = document;
|
|
||||||
returnValue.newDocument = null;
|
|
||||||
return returnValue;
|
|
||||||
}
|
|
||||||
else if (operation.op === '_get') {
|
|
||||||
operation.value = document;
|
|
||||||
return returnValue;
|
|
||||||
}
|
|
||||||
else { /* bad operation */
|
|
||||||
if (validateOperation) {
|
|
||||||
throw new exports.JsonPatchError('Operation `op` property is not one of operations defined in RFC-6902', 'OPERATION_OP_INVALID', index, operation, document);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return returnValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} /* END ROOT OPERATIONS */
|
|
||||||
else {
|
|
||||||
if (!mutateDocument) {
|
|
||||||
document = helpers_js_1._deepClone(document);
|
|
||||||
}
|
|
||||||
var path = operation.path || "";
|
|
||||||
var keys = path.split('/');
|
|
||||||
var obj = document;
|
|
||||||
var t = 1; //skip empty element - http://jsperf.com/to-shift-or-not-to-shift
|
|
||||||
var len = keys.length;
|
|
||||||
var existingPathFragment = undefined;
|
|
||||||
var key = void 0;
|
|
||||||
var validateFunction = void 0;
|
|
||||||
if (typeof validateOperation == 'function') {
|
|
||||||
validateFunction = validateOperation;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
validateFunction = validator;
|
|
||||||
}
|
|
||||||
while (true) {
|
|
||||||
key = keys[t];
|
|
||||||
if (key && key.indexOf('~') != -1) {
|
|
||||||
key = helpers_js_1.unescapePathComponent(key);
|
|
||||||
}
|
|
||||||
if (banPrototypeModifications &&
|
|
||||||
(key == '__proto__' ||
|
|
||||||
(key == 'prototype' && t > 0 && keys[t - 1] == 'constructor'))) {
|
|
||||||
throw new TypeError('JSON-Patch: modifying `__proto__` or `constructor/prototype` prop is banned for security reasons, if this was on purpose, please set `banPrototypeModifications` flag false and pass it to this function. More info in fast-json-patch README');
|
|
||||||
}
|
|
||||||
if (validateOperation) {
|
|
||||||
if (existingPathFragment === undefined) {
|
|
||||||
if (obj[key] === undefined) {
|
|
||||||
existingPathFragment = keys.slice(0, t).join('/');
|
|
||||||
}
|
|
||||||
else if (t == len - 1) {
|
|
||||||
existingPathFragment = operation.path;
|
|
||||||
}
|
|
||||||
if (existingPathFragment !== undefined) {
|
|
||||||
validateFunction(operation, 0, document, existingPathFragment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
t++;
|
|
||||||
if (Array.isArray(obj)) {
|
|
||||||
if (key === '-') {
|
|
||||||
key = obj.length;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (validateOperation && !helpers_js_1.isInteger(key)) {
|
|
||||||
throw new exports.JsonPatchError("Expected an unsigned base-10 integer value, making the new referenced value the array element with the zero-based index", "OPERATION_PATH_ILLEGAL_ARRAY_INDEX", index, operation, document);
|
|
||||||
} // only parse key when it's an integer for `arr.prop` to work
|
|
||||||
else if (helpers_js_1.isInteger(key)) {
|
|
||||||
key = ~~key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (t >= len) {
|
|
||||||
if (validateOperation && operation.op === "add" && key > obj.length) {
|
|
||||||
throw new exports.JsonPatchError("The specified index MUST NOT be greater than the number of elements in the array", "OPERATION_VALUE_OUT_OF_BOUNDS", index, operation, document);
|
|
||||||
}
|
|
||||||
var returnValue = arrOps[operation.op].call(operation, obj, key, document); // Apply patch
|
|
||||||
if (returnValue.test === false) {
|
|
||||||
throw new exports.JsonPatchError("Test operation failed", 'TEST_OPERATION_FAILED', index, operation, document);
|
|
||||||
}
|
|
||||||
return returnValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (t >= len) {
|
|
||||||
var returnValue = objOps[operation.op].call(operation, obj, key, document); // Apply patch
|
|
||||||
if (returnValue.test === false) {
|
|
||||||
throw new exports.JsonPatchError("Test operation failed", 'TEST_OPERATION_FAILED', index, operation, document);
|
|
||||||
}
|
|
||||||
return returnValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
obj = obj[key];
|
|
||||||
// If we have more keys in the path, but the next value isn't a non-null object,
|
|
||||||
// throw an OPERATION_PATH_UNRESOLVABLE error instead of iterating again.
|
|
||||||
if (validateOperation && t < len && (!obj || typeof obj !== "object")) {
|
|
||||||
throw new exports.JsonPatchError('Cannot perform operation at the desired path', 'OPERATION_PATH_UNRESOLVABLE', index, operation, document);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exports.applyOperation = applyOperation;
|
|
||||||
/**
|
|
||||||
* Apply a full JSON Patch array on a JSON document.
|
|
||||||
* Returns the {newDocument, result} of the patch.
|
|
||||||
* It modifies the `document` object and `patch` - it gets the values by reference.
|
|
||||||
* If you would like to avoid touching your values, clone them:
|
|
||||||
* `jsonpatch.applyPatch(document, jsonpatch._deepClone(patch))`.
|
|
||||||
*
|
|
||||||
* @param document The document to patch
|
|
||||||
* @param patch The patch to apply
|
|
||||||
* @param validateOperation `false` is without validation, `true` to use default jsonpatch's validation, or you can pass a `validateOperation` callback to be used for validation.
|
|
||||||
* @param mutateDocument Whether to mutate the original document or clone it before applying
|
|
||||||
* @param banPrototypeModifications Whether to ban modifications to `__proto__`, defaults to `true`.
|
|
||||||
* @return An array of `{newDocument, result}` after the patch
|
|
||||||
*/
|
|
||||||
function applyPatch(document, patch, validateOperation, mutateDocument, banPrototypeModifications) {
|
|
||||||
if (mutateDocument === void 0) { mutateDocument = true; }
|
|
||||||
if (banPrototypeModifications === void 0) { banPrototypeModifications = true; }
|
|
||||||
if (validateOperation) {
|
|
||||||
if (!Array.isArray(patch)) {
|
|
||||||
throw new exports.JsonPatchError('Patch sequence must be an array', 'SEQUENCE_NOT_AN_ARRAY');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!mutateDocument) {
|
|
||||||
document = helpers_js_1._deepClone(document);
|
|
||||||
}
|
|
||||||
var results = new Array(patch.length);
|
|
||||||
for (var i = 0, length_1 = patch.length; i < length_1; i++) {
|
|
||||||
// we don't need to pass mutateDocument argument because if it was true, we already deep cloned the object, we'll just pass `true`
|
|
||||||
results[i] = applyOperation(document, patch[i], validateOperation, true, banPrototypeModifications, i);
|
|
||||||
document = results[i].newDocument; // in case root was replaced
|
|
||||||
}
|
|
||||||
results.newDocument = document;
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
exports.applyPatch = applyPatch;
|
|
||||||
/**
|
|
||||||
* Apply a single JSON Patch Operation on a JSON document.
|
|
||||||
* Returns the updated document.
|
|
||||||
* Suitable as a reducer.
|
|
||||||
*
|
|
||||||
* @param document The document to patch
|
|
||||||
* @param operation The operation to apply
|
|
||||||
* @return The updated document
|
|
||||||
*/
|
|
||||||
function applyReducer(document, operation, index) {
|
|
||||||
var operationResult = applyOperation(document, operation);
|
|
||||||
if (operationResult.test === false) { // failed test
|
|
||||||
throw new exports.JsonPatchError("Test operation failed", 'TEST_OPERATION_FAILED', index, operation, document);
|
|
||||||
}
|
|
||||||
return operationResult.newDocument;
|
|
||||||
}
|
|
||||||
exports.applyReducer = applyReducer;
|
|
||||||
/**
|
|
||||||
* Validates a single operation. Called from `jsonpatch.validate`. Throws `JsonPatchError` in case of an error.
|
|
||||||
* @param {object} operation - operation object (patch)
|
|
||||||
* @param {number} index - index of operation in the sequence
|
|
||||||
* @param {object} [document] - object where the operation is supposed to be applied
|
|
||||||
* @param {string} [existingPathFragment] - comes along with `document`
|
|
||||||
*/
|
|
||||||
function validator(operation, index, document, existingPathFragment) {
|
|
||||||
if (typeof operation !== 'object' || operation === null || Array.isArray(operation)) {
|
|
||||||
throw new exports.JsonPatchError('Operation is not an object', 'OPERATION_NOT_AN_OBJECT', index, operation, document);
|
|
||||||
}
|
|
||||||
else if (!objOps[operation.op]) {
|
|
||||||
throw new exports.JsonPatchError('Operation `op` property is not one of operations defined in RFC-6902', 'OPERATION_OP_INVALID', index, operation, document);
|
|
||||||
}
|
|
||||||
else if (typeof operation.path !== 'string') {
|
|
||||||
throw new exports.JsonPatchError('Operation `path` property is not a string', 'OPERATION_PATH_INVALID', index, operation, document);
|
|
||||||
}
|
|
||||||
else if (operation.path.indexOf('/') !== 0 && operation.path.length > 0) {
|
|
||||||
// paths that aren't empty string should start with "/"
|
|
||||||
throw new exports.JsonPatchError('Operation `path` property must start with "/"', 'OPERATION_PATH_INVALID', index, operation, document);
|
|
||||||
}
|
|
||||||
else if ((operation.op === 'move' || operation.op === 'copy') && typeof operation.from !== 'string') {
|
|
||||||
throw new exports.JsonPatchError('Operation `from` property is not present (applicable in `move` and `copy` operations)', 'OPERATION_FROM_REQUIRED', index, operation, document);
|
|
||||||
}
|
|
||||||
else if ((operation.op === 'add' || operation.op === 'replace' || operation.op === 'test') && operation.value === undefined) {
|
|
||||||
throw new exports.JsonPatchError('Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)', 'OPERATION_VALUE_REQUIRED', index, operation, document);
|
|
||||||
}
|
|
||||||
else if ((operation.op === 'add' || operation.op === 'replace' || operation.op === 'test') && helpers_js_1.hasUndefined(operation.value)) {
|
|
||||||
throw new exports.JsonPatchError('Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)', 'OPERATION_VALUE_CANNOT_CONTAIN_UNDEFINED', index, operation, document);
|
|
||||||
}
|
|
||||||
else if (document) {
|
|
||||||
if (operation.op == "add") {
|
|
||||||
var pathLen = operation.path.split("/").length;
|
|
||||||
var existingPathLen = existingPathFragment.split("/").length;
|
|
||||||
if (pathLen !== existingPathLen + 1 && pathLen !== existingPathLen) {
|
|
||||||
throw new exports.JsonPatchError('Cannot perform an `add` operation at the desired path', 'OPERATION_PATH_CANNOT_ADD', index, operation, document);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (operation.op === 'replace' || operation.op === 'remove' || operation.op === '_get') {
|
|
||||||
if (operation.path !== existingPathFragment) {
|
|
||||||
throw new exports.JsonPatchError('Cannot perform the operation at a path that does not exist', 'OPERATION_PATH_UNRESOLVABLE', index, operation, document);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (operation.op === 'move' || operation.op === 'copy') {
|
|
||||||
var existingValue = { op: "_get", path: operation.from, value: undefined };
|
|
||||||
var error = validate([existingValue], document);
|
|
||||||
if (error && error.name === 'OPERATION_PATH_UNRESOLVABLE') {
|
|
||||||
throw new exports.JsonPatchError('Cannot perform the operation from a path that does not exist', 'OPERATION_FROM_UNRESOLVABLE', index, operation, document);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exports.validator = validator;
|
|
||||||
/**
|
|
||||||
* Validates a sequence of operations. If `document` parameter is provided, the sequence is additionally validated against the object document.
|
|
||||||
* If error is encountered, returns a JsonPatchError object
|
|
||||||
* @param sequence
|
|
||||||
* @param document
|
|
||||||
* @returns {JsonPatchError|undefined}
|
|
||||||
*/
|
|
||||||
function validate(sequence, document, externalValidator) {
|
|
||||||
try {
|
|
||||||
if (!Array.isArray(sequence)) {
|
|
||||||
throw new exports.JsonPatchError('Patch sequence must be an array', 'SEQUENCE_NOT_AN_ARRAY');
|
|
||||||
}
|
|
||||||
if (document) {
|
|
||||||
//clone document and sequence so that we can safely try applying operations
|
|
||||||
applyPatch(helpers_js_1._deepClone(document), helpers_js_1._deepClone(sequence), externalValidator || true);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
externalValidator = externalValidator || validator;
|
|
||||||
for (var i = 0; i < sequence.length; i++) {
|
|
||||||
externalValidator(sequence[i], i, document, undefined);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
if (e instanceof exports.JsonPatchError) {
|
|
||||||
return e;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exports.validate = validate;
|
|
||||||
// based on https://github.com/epoberezkin/fast-deep-equal
|
|
||||||
// MIT License
|
|
||||||
// Copyright (c) 2017 Evgeny Poberezkin
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
function _areEquals(a, b) {
|
|
||||||
if (a === b)
|
|
||||||
return true;
|
|
||||||
if (a && b && typeof a == 'object' && typeof b == 'object') {
|
|
||||||
var arrA = Array.isArray(a), arrB = Array.isArray(b), i, length, key;
|
|
||||||
if (arrA && arrB) {
|
|
||||||
length = a.length;
|
|
||||||
if (length != b.length)
|
|
||||||
return false;
|
|
||||||
for (i = length; i-- !== 0;)
|
|
||||||
if (!_areEquals(a[i], b[i]))
|
|
||||||
return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (arrA != arrB)
|
|
||||||
return false;
|
|
||||||
var keys = Object.keys(a);
|
|
||||||
length = keys.length;
|
|
||||||
if (length !== Object.keys(b).length)
|
|
||||||
return false;
|
|
||||||
for (i = length; i-- !== 0;)
|
|
||||||
if (!b.hasOwnProperty(keys[i]))
|
|
||||||
return false;
|
|
||||||
for (i = length; i-- !== 0;) {
|
|
||||||
key = keys[i];
|
|
||||||
if (!_areEquals(a[key], b[key]))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return a !== a && b !== b;
|
|
||||||
}
|
|
||||||
exports._areEquals = _areEquals;
|
|
||||||
;
|
|
||||||
|
|
||||||
|
|
||||||
/***/ }),
|
|
||||||
/* 2 */
|
|
||||||
/***/ (function(module, exports, __webpack_require__) {
|
|
||||||
|
|
||||||
var core = __webpack_require__(1);
|
|
||||||
Object.assign(exports, core);
|
|
||||||
|
|
||||||
var duplex = __webpack_require__(3);
|
|
||||||
Object.assign(exports, duplex);
|
|
||||||
|
|
||||||
var helpers = __webpack_require__(0);
|
|
||||||
exports.JsonPatchError = helpers.PatchError;
|
|
||||||
exports.deepClone = helpers._deepClone;
|
|
||||||
exports.escapePathComponent = helpers.escapePathComponent;
|
|
||||||
exports.unescapePathComponent = helpers.unescapePathComponent;
|
|
||||||
|
|
||||||
|
|
||||||
/***/ }),
|
|
||||||
/* 3 */
|
|
||||||
/***/ (function(module, exports, __webpack_require__) {
|
|
||||||
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
/*!
|
|
||||||
* https://github.com/Starcounter-Jack/JSON-Patch
|
|
||||||
* (c) 2017-2021 Joachim Wester
|
|
||||||
* MIT license
|
|
||||||
*/
|
|
||||||
var helpers_js_1 = __webpack_require__(0);
|
|
||||||
var core_js_1 = __webpack_require__(1);
|
|
||||||
var beforeDict = new WeakMap();
|
|
||||||
var Mirror = /** @class */ (function () {
|
|
||||||
function Mirror(obj) {
|
|
||||||
this.observers = new Map();
|
|
||||||
this.obj = obj;
|
|
||||||
}
|
|
||||||
return Mirror;
|
|
||||||
}());
|
|
||||||
var ObserverInfo = /** @class */ (function () {
|
|
||||||
function ObserverInfo(callback, observer) {
|
|
||||||
this.callback = callback;
|
|
||||||
this.observer = observer;
|
|
||||||
}
|
|
||||||
return ObserverInfo;
|
|
||||||
}());
|
|
||||||
function getMirror(obj) {
|
|
||||||
return beforeDict.get(obj);
|
|
||||||
}
|
|
||||||
function getObserverFromMirror(mirror, callback) {
|
|
||||||
return mirror.observers.get(callback);
|
|
||||||
}
|
|
||||||
function removeObserverFromMirror(mirror, observer) {
|
|
||||||
mirror.observers.delete(observer.callback);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Detach an observer from an object
|
|
||||||
*/
|
|
||||||
function unobserve(root, observer) {
|
|
||||||
observer.unobserve();
|
|
||||||
}
|
|
||||||
exports.unobserve = unobserve;
|
|
||||||
/**
|
|
||||||
* Observes changes made to an object, which can then be retrieved using generate
|
|
||||||
*/
|
|
||||||
function observe(obj, callback) {
|
|
||||||
var patches = [];
|
|
||||||
var observer;
|
|
||||||
var mirror = getMirror(obj);
|
|
||||||
if (!mirror) {
|
|
||||||
mirror = new Mirror(obj);
|
|
||||||
beforeDict.set(obj, mirror);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
var observerInfo = getObserverFromMirror(mirror, callback);
|
|
||||||
observer = observerInfo && observerInfo.observer;
|
|
||||||
}
|
|
||||||
if (observer) {
|
|
||||||
return observer;
|
|
||||||
}
|
|
||||||
observer = {};
|
|
||||||
mirror.value = helpers_js_1._deepClone(obj);
|
|
||||||
if (callback) {
|
|
||||||
observer.callback = callback;
|
|
||||||
observer.next = null;
|
|
||||||
var dirtyCheck = function () {
|
|
||||||
generate(observer);
|
|
||||||
};
|
|
||||||
var fastCheck = function () {
|
|
||||||
clearTimeout(observer.next);
|
|
||||||
observer.next = setTimeout(dirtyCheck);
|
|
||||||
};
|
|
||||||
if (typeof window !== 'undefined') { //not Node
|
|
||||||
window.addEventListener('mouseup', fastCheck);
|
|
||||||
window.addEventListener('keyup', fastCheck);
|
|
||||||
window.addEventListener('mousedown', fastCheck);
|
|
||||||
window.addEventListener('keydown', fastCheck);
|
|
||||||
window.addEventListener('change', fastCheck);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
observer.patches = patches;
|
|
||||||
observer.object = obj;
|
|
||||||
observer.unobserve = function () {
|
|
||||||
generate(observer);
|
|
||||||
clearTimeout(observer.next);
|
|
||||||
removeObserverFromMirror(mirror, observer);
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.removeEventListener('mouseup', fastCheck);
|
|
||||||
window.removeEventListener('keyup', fastCheck);
|
|
||||||
window.removeEventListener('mousedown', fastCheck);
|
|
||||||
window.removeEventListener('keydown', fastCheck);
|
|
||||||
window.removeEventListener('change', fastCheck);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
mirror.observers.set(callback, new ObserverInfo(callback, observer));
|
|
||||||
return observer;
|
|
||||||
}
|
|
||||||
exports.observe = observe;
|
|
||||||
/**
|
|
||||||
* Generate an array of patches from an observer
|
|
||||||
*/
|
|
||||||
function generate(observer, invertible) {
|
|
||||||
if (invertible === void 0) { invertible = false; }
|
|
||||||
var mirror = beforeDict.get(observer.object);
|
|
||||||
_generate(mirror.value, observer.object, observer.patches, "", invertible);
|
|
||||||
if (observer.patches.length) {
|
|
||||||
core_js_1.applyPatch(mirror.value, observer.patches);
|
|
||||||
}
|
|
||||||
var temp = observer.patches;
|
|
||||||
if (temp.length > 0) {
|
|
||||||
observer.patches = [];
|
|
||||||
if (observer.callback) {
|
|
||||||
observer.callback(temp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return temp;
|
|
||||||
}
|
|
||||||
exports.generate = generate;
|
|
||||||
// Dirty check if obj is different from mirror, generate patches and update mirror
|
|
||||||
function _generate(mirror, obj, patches, path, invertible) {
|
|
||||||
if (obj === mirror) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (typeof obj.toJSON === "function") {
|
|
||||||
obj = obj.toJSON();
|
|
||||||
}
|
|
||||||
var newKeys = helpers_js_1._objectKeys(obj);
|
|
||||||
var oldKeys = helpers_js_1._objectKeys(mirror);
|
|
||||||
var changed = false;
|
|
||||||
var deleted = false;
|
|
||||||
//if ever "move" operation is implemented here, make sure this test runs OK: "should not generate the same patch twice (move)"
|
|
||||||
for (var t = oldKeys.length - 1; t >= 0; t--) {
|
|
||||||
var key = oldKeys[t];
|
|
||||||
var oldVal = mirror[key];
|
|
||||||
if (helpers_js_1.hasOwnProperty(obj, key) && !(obj[key] === undefined && oldVal !== undefined && Array.isArray(obj) === false)) {
|
|
||||||
var newVal = obj[key];
|
|
||||||
if (typeof oldVal == "object" && oldVal != null && typeof newVal == "object" && newVal != null && Array.isArray(oldVal) === Array.isArray(newVal)) {
|
|
||||||
_generate(oldVal, newVal, patches, path + "/" + helpers_js_1.escapePathComponent(key), invertible);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (oldVal !== newVal) {
|
|
||||||
changed = true;
|
|
||||||
if (invertible) {
|
|
||||||
patches.push({ op: "test", path: path + "/" + helpers_js_1.escapePathComponent(key), value: helpers_js_1._deepClone(oldVal) });
|
|
||||||
}
|
|
||||||
patches.push({ op: "replace", path: path + "/" + helpers_js_1.escapePathComponent(key), value: helpers_js_1._deepClone(newVal) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (Array.isArray(mirror) === Array.isArray(obj)) {
|
|
||||||
if (invertible) {
|
|
||||||
patches.push({ op: "test", path: path + "/" + helpers_js_1.escapePathComponent(key), value: helpers_js_1._deepClone(oldVal) });
|
|
||||||
}
|
|
||||||
patches.push({ op: "remove", path: path + "/" + helpers_js_1.escapePathComponent(key) });
|
|
||||||
deleted = true; // property has been deleted
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (invertible) {
|
|
||||||
patches.push({ op: "test", path: path, value: mirror });
|
|
||||||
}
|
|
||||||
patches.push({ op: "replace", path: path, value: obj });
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!deleted && newKeys.length == oldKeys.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (var t = 0; t < newKeys.length; t++) {
|
|
||||||
var key = newKeys[t];
|
|
||||||
if (!helpers_js_1.hasOwnProperty(mirror, key) && obj[key] !== undefined) {
|
|
||||||
patches.push({ op: "add", path: path + "/" + helpers_js_1.escapePathComponent(key), value: helpers_js_1._deepClone(obj[key]) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Create an array of patches from the differences in two objects
|
|
||||||
*/
|
|
||||||
function compare(tree1, tree2, invertible) {
|
|
||||||
if (invertible === void 0) { invertible = false; }
|
|
||||||
var patches = [];
|
|
||||||
_generate(tree1, tree2, patches, '', invertible);
|
|
||||||
return patches;
|
|
||||||
}
|
|
||||||
exports.compare = compare;
|
|
||||||
|
|
||||||
|
|
||||||
/***/ })
|
|
||||||
/******/ ]);
|
|
||||||
File diff suppressed because one or more lines are too long
Vendored
-21
@@ -1,21 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2011-2018 Jonny Strömberg, jonnystromberg.com
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
2.3.1
|
|
||||||
-2020
File diff suppressed because it is too large
Load Diff
-1
File diff suppressed because one or more lines are too long
-2
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"list.min.js","sources":["webpack://List/list.min.js"],"mappings":"AAAA","sourceRoot":""}
|
|
||||||
-202
@@ -1,202 +0,0 @@
|
|||||||
|
|
||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright [yyyy] [name of copyright owner]
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
https://github.com/google/material-design-icons/tree/f7bd4f25f3764883717c09a1fd867f560c9a9581
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
@font-face {
|
|
||||||
font-family: "Material Icons";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
/* For IE6-8 */
|
|
||||||
/* src: url("../font/MaterialIcons-Regular.eot"); */
|
|
||||||
src: local("Material Icons"),
|
|
||||||
local("MaterialIcons-Regular"),
|
|
||||||
/* url("../font/MaterialIcons-Regular.woff2") format('woff2'), */
|
|
||||||
/* url("../font/MaterialIcons-Regular.woff") format('woff'), */
|
|
||||||
url("../font/MaterialIcons-Regular.ttf") format("truetype");
|
|
||||||
}
|
|
||||||
|
|
||||||
.material-icons {
|
|
||||||
font-family: 'Material Icons';
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
font-size: 24px; /* Preferred icon size */
|
|
||||||
display: inline-block;
|
|
||||||
line-height: 1;
|
|
||||||
text-transform: none;
|
|
||||||
letter-spacing: normal;
|
|
||||||
word-wrap: normal;
|
|
||||||
white-space: nowrap;
|
|
||||||
direction: ltr;
|
|
||||||
|
|
||||||
/* Support for all WebKit browsers. */
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
/* Support for Safari and Chrome. */
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
|
|
||||||
/* Support for Firefox. */
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
|
|
||||||
/* Support for IE. */
|
|
||||||
font-feature-settings: 'liga';
|
|
||||||
}
|
|
||||||
-2235
File diff suppressed because it is too large
Load Diff
Binary file not shown.
-2195
File diff suppressed because it is too large
Load Diff
BIN
Binary file not shown.
-2200
File diff suppressed because it is too large
Load Diff
BIN
Binary file not shown.
-2200
File diff suppressed because it is too large
Load Diff
BIN
Binary file not shown.
-2238
File diff suppressed because it is too large
Load Diff
BIN
Binary file not shown.
-21
@@ -1,21 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2014-2018 Materialize
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
1.0.0
|
|
||||||
-9067
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
-12374
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Vendored
-21
@@ -1,21 +0,0 @@
|
|||||||
(The MIT License)
|
|
||||||
|
|
||||||
Copyright (C) 2014-2017 by Vitaly Puzrin and Andrei Tuputcyn
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
2.1.0
|
|
||||||
-6688
File diff suppressed because it is too large
Load Diff
-2
File diff suppressed because one or more lines are too long
-6877
File diff suppressed because it is too large
Load Diff
Vendored
-6896
File diff suppressed because it is too large
Load Diff
-2
File diff suppressed because one or more lines are too long
-3924
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
-4126
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
-3229
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
-3239
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
-21
@@ -1,21 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2021 Plotly, Inc
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
2.31.0
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user