diff --git a/teraserver/python/.env.vscode b/teraserver/python/.env.vscode new file mode 100644 index 000000000..74e866aae --- /dev/null +++ b/teraserver/python/.env.vscode @@ -0,0 +1 @@ +PYTHONPATH=${workspaceFolder} diff --git a/teraserver/python/.vscode/launch.json b/teraserver/python/.vscode/launch.json new file mode 100644 index 000000000..8b0ce7aee --- /dev/null +++ b/teraserver/python/.vscode/launch.json @@ -0,0 +1,33 @@ +{ + // Utilisez IntelliSense pour en savoir plus sur les attributs possibles. + // Pointez pour afficher la description des attributs existants. + // Pour plus d'informations, visitez : https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: OpenTera Server TeraServer.py", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/TeraServer.py", + "console": "integratedTerminal", + "env": { + "PYTHONPATH": "${workspaceFolder}" + } + }, + { + "name": "Python Debugger: Attach", + "type": "debugpy", + "request": "attach", + "connect": { + "host": "localhost", + "port": 5688 + }, + "pathMappings": [ + { + "localRoot": "${workspaceFolder}", // Maps C:\Users\user1\project1 + "remoteRoot": "/root/opentera/teraserver/python" // To current working directory ~/project1 + } + ] + } + ] +} diff --git a/teraserver/python/.vscode/settings.json b/teraserver/python/.vscode/settings.json new file mode 100644 index 000000000..3fdc29e40 --- /dev/null +++ b/teraserver/python/.vscode/settings.json @@ -0,0 +1,13 @@ + +{ + "python.testing.unittestArgs": [ + "-v", + "-s", + "${workspaceFolder}", + "-p", + "test_*.py" + ], + "python.testing.pytestEnabled": false, + "python.testing.unittestEnabled": true, + "python.envFile": "${workspaceFolder}/.env.vscode" +} diff --git a/teraserver/python/alembic/versions/60f5b2ed8b5a_assets_table_rework.py b/teraserver/python/alembic/versions/60f5b2ed8b5a_assets_table_rework.py index ba6c28442..1adc5c9f7 100644 --- a/teraserver/python/alembic/versions/60f5b2ed8b5a_assets_table_rework.py +++ b/teraserver/python/alembic/versions/60f5b2ed8b5a_assets_table_rework.py @@ -18,7 +18,7 @@ def upgrade(): # Change t_assets column asset_type to string - integers values should be converted directly in Postgresql - op.alter_column(table_name='t_assets', column_name='asset_type', type_=sa.String) + op.alter_column(table_name='t_assets', column_name='asset_type', type=sa.String) # Change all current values to "application/octet-stream" since that is what we have right now op.execute("UPDATE t_assets SET asset_type=\'application/octet-stream\'") diff --git a/teraserver/python/alembic/versions/65a42f6ee567_soft_delete_upgrade.py b/teraserver/python/alembic/versions/65a42f6ee567_soft_delete_upgrade.py index 0caf54f55..0656d22a6 100644 --- a/teraserver/python/alembic/versions/65a42f6ee567_soft_delete_upgrade.py +++ b/teraserver/python/alembic/versions/65a42f6ee567_soft_delete_upgrade.py @@ -18,7 +18,7 @@ def upgrade(): # Remove site_name unique constraint on t_sites - op.drop_constraint(constraint_name='t_sites_site_name_key', table_name='t_sites', type_='unique') + op.drop_constraint(constraint_name='t_sites_site_name_key', table_name='t_sites', type='unique') # TeraSessionParticipants.id_session add ondelete='cascade' op.drop_constraint(constraint_name='t_sessions_participants_id_session_fkey', table_name='t_sessions_participants', diff --git a/teraserver/python/alembic/versions/89343f5c95b9_allow_2fa_login.py b/teraserver/python/alembic/versions/89343f5c95b9_allow_2fa_login.py new file mode 100644 index 000000000..a113bed6f --- /dev/null +++ b/teraserver/python/alembic/versions/89343f5c95b9_allow_2fa_login.py @@ -0,0 +1,50 @@ +"""allow 2fa login + +Revision ID: 89343f5c95b9 +Revises: 09764faa2d57 +Create Date: 2024-09-05 14:49:04.781595 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '89343f5c95b9' +down_revision = '09764faa2d57' +branch_labels = None +depends_on = None + + +def upgrade(): + # Add 2fa_enabled column to t_users table + op.add_column(table_name='t_users', column=sa.Column('user_2fa_enabled', + sa.Boolean, nullable=False, server_default=str(False))) + + # Add 2fa_otp_enabled column to t_users table + op.add_column(table_name='t_users', column=sa.Column('user_2fa_otp_enabled', + sa.Boolean, nullable=False, server_default=str(False))) + + # Add 2fa_email_enabled_column to t_users table + # Will user user_email as 2fa email + op.add_column(table_name='t_users', column=sa.Column('user_2fa_email_enabled', + sa.Boolean, nullable=False, server_default=str(False))) + + # Add 2fa_otp_secret column to t_users table + # Secrets will be generated with pytop.random_base32() + op.add_column(table_name='t_users', column=sa.Column('user_2fa_otp_secret', + sa.String(32), nullable=True)) + + # Add a force_password_change column to t_users table + op.add_column(table_name='t_users', column=sa.Column('user_force_password_change', + sa.Boolean, nullable=False, server_default=str(False))) + + +def downgrade(): + # Remove columns + op.drop_column('t_users', 'user_2fa_enabled') + op.drop_column('t_users', 'user_2fa_otp_enabled') + op.drop_column('t_users', 'user_2fa_email_enabled') + op.drop_column('t_users', 'user_2fa_otp_secret') + op.drop_column('t_users', 'user_force_password_change') + diff --git a/teraserver/python/alembic/versions/README.md b/teraserver/python/alembic/versions/README.md index e7265c312..096fe54be 100644 --- a/teraserver/python/alembic/versions/README.md +++ b/teraserver/python/alembic/versions/README.md @@ -6,6 +6,16 @@ alembic revision -m "create account table" ``` +## Changes for next version (Sept 5 2024) + +### TeraServer +**Modified t_users table** +* Add column user_2fa_enabled (Boolean, default=False) +* Add column user_2fa_otp_enabled (Boolean, default=False) +* Add column user_2fa_email_enabled (Boolean, default=False) +* Add column user_2fa_otp_secret (String(32), nullable=True) +* Add column user_force_password_change (Boolean, default=False) + ## Changes for next version (Feb 6 2023) ### TeraServer diff --git a/teraserver/python/alembic/versions/c58727df3ac2_add_site_2fa_required_column_to_tera_.py b/teraserver/python/alembic/versions/c58727df3ac2_add_site_2fa_required_column_to_tera_.py new file mode 100644 index 000000000..8b7d3f824 --- /dev/null +++ b/teraserver/python/alembic/versions/c58727df3ac2_add_site_2fa_required_column_to_tera_.py @@ -0,0 +1,26 @@ +"""add_site_2fa_required_column_to_tera_site + +Revision ID: c58727df3ac2 +Revises: 89343f5c95b9 +Create Date: 2024-09-30 13:58:38.839824 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'c58727df3ac2' +down_revision = '89343f5c95b9' +branch_labels = None +depends_on = None + + +def upgrade(): + # Add site_2fa_required column to t_sites table + op.add_column(table_name='t_sites', column=sa.Column('site_2fa_required', + sa.Boolean, nullable=False, server_default=str(False))) + +def downgrade(): + # Remove columns + op.drop_column('t_sites', 'site_2fa_required') diff --git a/teraserver/python/env/requirements.txt b/teraserver/python/env/requirements.txt index a6b51afdd..63620809a 100644 --- a/teraserver/python/env/requirements.txt +++ b/teraserver/python/env/requirements.txt @@ -1,41 +1,43 @@ pypiwin32==223; sys_platform == 'win32' -Twisted==24.3.0 -treq==23.11.0 -cryptography==42.0.5 -autobahn==23.6.2 -SQLAlchemy==2.0.28 +Twisted==24.7.0 +treq==24.9.1 +cryptography==43.0.1 +autobahn==24.4.2 +SQLAlchemy==2.0.35 sqlalchemy-schemadisplay==2.0 -pydot==2.0.0 +pydot==3.0.1 psycopg2-binary==2.9.9 -Flask==3.0.2 +Flask==3.0.3 Flask-SQLAlchemy==3.1.1 Flask-Login==0.6.3 Flask-Login-Multi==0.1.2 Flask-HTTPAuth==4.8.0 -Flask-SocketIO==5.3.6 -Flask-Session==0.6.0 +Flask-SocketIO==5.3.7 +Flask-Session==0.8.0 flask-restx==1.3.0 -Flask-Security==3.0.0 +Flask-Security==5.5.2 Flask-Babel==4.0.0 Flask-BabelEx==0.9.4 -Flask-Migrate==4.0.5 +Flask-Migrate==4.0.7 flask-swagger-ui==4.11.1 -Flask-Limiter==3.5.1 -Flask-Mail==0.9.1 +Flask-Limiter==3.8.0 +Flask-Mail==0.10.0 Flask-Principal==0.4.0 -redis==5.0.2 +redis==5.0.8 txredisapi==1.4.10 passlib==1.7.4 -bcrypt==4.1.2 +bcrypt==4.2.0 WTForms==3.1.2 -pyOpenSSL==24.0.0 +pyOpenSSL==24.2.1 service-identity==24.1.0 -PyJWT==2.8.0 +PyJWT==2.9.0 pylzma==0.5.0 bz2file==0.98 python-slugify==8.0.4 -websocket-client==1.7.0 -pytest==8.0.2 -Jinja2==3.1.3 +websocket-client==1.8.0 +pytest==8.3.3 +Jinja2==3.1.4 ua-parser==0.18.0 - +pyotp==2.9.0 +pyqrcode==1.2.1 +pypng==0.20220715.0 diff --git a/teraserver/python/modules/DatabaseModule/DBManager.py b/teraserver/python/modules/DatabaseModule/DBManager.py index db26b2858..517099b0e 100755 --- a/teraserver/python/modules/DatabaseModule/DBManager.py +++ b/teraserver/python/modules/DatabaseModule/DBManager.py @@ -1,11 +1,12 @@ +from sqlite3 import Connection as SQLite3Connection +import datetime +import json + from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import event, inspect +from sqlalchemy import event, inspect, update from sqlalchemy.engine import Engine from sqlalchemy.engine.reflection import Inspector -from sqlite3 import Connection as SQLite3Connection - from twisted.internet import task, reactor -import datetime import opentera.messages.python as messages # Must include all Database objects here to be properly initialized and created if needed @@ -43,8 +44,10 @@ from opentera.db.models.TeraTestTypeSite import TeraTestTypeSite from opentera.db.models.TeraTestTypeProject import TeraTestTypeProject from opentera.db.models.TeraTest import TeraTest -from opentera.db.models.TeraSessionDevices import TeraSessionDevices +# from opentera.db.models.TeraSessionDevices import TeraSessionDevices from opentera.db.Base import BaseModel +from opentera.db.models import EventNameClassMap + from opentera.config.ConfigManager import ConfigManager from modules.FlaskModule.FlaskModule import flask_app @@ -54,7 +57,6 @@ from modules.DatabaseModule.DBManagerTeraDeviceAccess import DBManagerTeraDeviceAccess from modules.DatabaseModule.DBManagerTeraParticipantAccess import DBManagerTeraParticipantAccess from modules.DatabaseModule.DBManagerTeraServiceAccess import DBManagerTeraServiceAccess - # Alembic from alembic.config import Config from alembic import command @@ -92,17 +94,137 @@ def start_cleanup_task(self) -> task: return task.deferLater(reactor, seconds_to_midnight, self.cleanup_database) # return task.deferLater(reactor, 5, self.cleanup_database) + def setup_events_for_2fa_sites(self): + """ + We need to validate that 2FA is enabled for all users in the site when the flag is set. + This can occur on multiple occasions : when the site is created, updated and also when user + groups are modified. + """ + @event.listens_for(TeraSite, 'after_update') + @event.listens_for(TeraSite, 'after_insert') + def site_updated_or_inserted(mapper, connection, target: TeraSite): + # Check if 2FA is enabled for this site + if target and target.site_2fa_required: + # Get all users that have access to this site + users = TeraServiceAccess.query.join(TeraServiceRole, TeraServiceAccess.id_service_role == TeraServiceRole.id_service_role) \ + .join(TeraUserUserGroup, TeraServiceAccess.id_user_group == TeraUserUserGroup.id_user_group) \ + .join(TeraUser, TeraUserUserGroup.id_user == TeraUser.id_user) \ + .join(TeraSite, TeraServiceRole.id_site == TeraSite.id_site) \ + .filter(TeraSite.id_site == target.id_site) \ + .with_entities(TeraUser).all() # Return the user information only + + # Enable 2FA for all standard users found + for user in users: + connection.execute( + update(TeraUser) + .where(TeraUser.id_user == user.id_user) + .values(user_2fa_enabled=True) + ) + + # Enable 2FA for all superadmins + connection.execute( + update(TeraUser) + .where(TeraUser.user_superadmin == bool(True)) + .values(user_2fa_enabled=True) + ) + + @event.listens_for(TeraUserGroup, 'after_update') + @event.listens_for(TeraUserGroup, 'after_insert') + def user_group_updated_or_inserted(mapper, connection, target: TeraUserGroup): + + # Check if 2FA is enabled for a related site in a single sql query + if target: + # Get users from the group that have access to a site with 2FA enabled + users = TeraUser.query.join(TeraUserUserGroup, TeraUser.id_user == TeraUserUserGroup.id_user) \ + .join(TeraServiceAccess, TeraUserUserGroup.id_user_group == TeraServiceAccess.id_user_group) \ + .join(TeraServiceRole, TeraServiceAccess.id_service_role == TeraServiceRole.id_service_role) \ + .join(TeraSite, TeraServiceRole.id_site == TeraSite.id_site) \ + .filter(TeraUserUserGroup.id_user_group == target.id_user_group) \ + .filter(TeraSite.site_2fa_required == bool(True)) \ + .with_entities(TeraUser).all() # Return the user information only + + # Enable 2FA for all users found + for user in users: + connection.execute( + update(TeraUser) + .where(TeraUser.id_user == user.id_user) + .values(user_2fa_enabled=True) + ) + + @event.listens_for(TeraUserUserGroup, 'after_update') + @event.listens_for(TeraUserUserGroup, 'after_insert') + def user_user_group_updated_or_inserted(mapper, connection, target: TeraUserUserGroup): + # If the user in the usergroup has access to a site with 2FA enabled, enable 2FA for the user + if target: + sites = TeraServiceAccess.query.join(TeraServiceRole, TeraServiceAccess.id_service_role == + TeraServiceRole.id_service_role) \ + .join(TeraSite, TeraServiceRole.id_site == TeraSite.id_site) \ + .filter(TeraServiceAccess.id_user_group == target.id_user_group) \ + .with_entities(TeraSite).all() # Return the site information only + + for site in sites: + if site.site_2fa_required: + # Perform single update for user + connection.execute( + update(TeraUser) + .where(TeraUser.id_user == target.id_user) + .values(user_2fa_enabled=True) + ) + break + + @event.listens_for(TeraUser, 'after_update') + @event.listens_for(TeraUser, 'after_insert') + def user_updated_or_inserted(mapper, connection, target: TeraUser): + # Check if 2FA is enabled for a related site through user groups + if target: + sites = [] + if target.user_superadmin: + # Superadmin has access to all sites, so we need to verify if any of them have 2FA enabled + sites = TeraSite.query.filter(TeraSite.site_2fa_required == bool(True)).all() + else: + # Standard user need to verify sites through user groups + sites = TeraServiceAccess.query.join(TeraUserUserGroup, TeraServiceAccess.id_user_group == TeraUserUserGroup.id_user_group) \ + .join(TeraServiceRole, TeraServiceAccess.id_service_role == TeraServiceRole.id_service_role) \ + .join(TeraSite, TeraServiceRole.id_site == TeraSite.id_site) \ + .filter(TeraUserUserGroup.id_user == target.id_user) \ + .with_entities(TeraSite).all() # Return the site information only + + if not sites: + # User is not in any user group related to a 2FA site + # If 2FA is disabled, make sure other 2FA fields are reset + if not target.user_2fa_enabled: + connection.execute( + update(TeraUser) + .where(TeraUser.id_user == target.id_user) + .values( + user_2fa_otp_enabled=False, + user_2fa_otp_secret=None, + user_2fa_email_enabled=False + ) + ) + else: + for site in sites: + if site.site_2fa_required: + # Perform single update for user + connection.execute( + update(TeraUser) + .where(TeraUser.id_user == target.id_user) + .values(user_2fa_enabled=True) + ) + break + + def setup_events_for_class(self, cls, event_name): - import json + """ + Setup events for a specific class. This will allow to send events through redis when a specific + event occurs on a specific class. This is useful to trace changes in the database. + The list of classes that produce events is defined in the EventNameClassMap in opentera/db/models/__init__.py. + :param cls: Class to setup events for + :param event_name: Name of the event + """ @event.listens_for(cls, 'after_update') def base_model_updated(mapper, connection, target): - # Handle soft deletion - # if getattr(target, 'soft_delete', None): - # if target.deleted_at: - # # Updated target with a deleted date - trigger the deleted handler instead - # base_model_deleted(mapper, connection, target) - # return json_update_event = target.to_json_update_event() if json_update_event: database_event = messages.DatabaseEvent() @@ -122,7 +244,6 @@ def base_model_updated(mapper, connection, target): # Send the event before we delete, so we can trace it... @event.listens_for(cls, 'after_delete') def base_model_deleted(mapper, connection, target): - # print(mapper, connection, target, event_name) json_delete_event = target.to_json_delete_event() if json_delete_event: database_event = messages.DatabaseEvent() @@ -141,7 +262,6 @@ def base_model_deleted(mapper, connection, target): @event.listens_for(cls, 'after_insert') def base_model_inserted(mapper, connection, target): - # print(mapper, connection, target, event_name) json_create_event = target.to_json_create_event() if json_create_event: database_event = messages.DatabaseEvent() @@ -275,12 +395,14 @@ def create_defaults(self, config: ConfigManager, test=False): TeraTest.create_defaults(test) def setup_events(self): - # TODO Add events that need to be sent through redis - # TODO Useful to specify event name, always get_model_name() ? + """ + Called after the database is opened. This will setup events for all classes that need to be monitored. + """ + for event_name, model_class in EventNameClassMap.items(): + self.setup_events_for_class(model_class, event_name) - from opentera.db.models import EventNameClassMap - for name in EventNameClassMap: - self.setup_events_for_class(EventNameClassMap[name], name) + # Setup events for 2FA sites + self.setup_events_for_2fa_sites() def open(self, echo=False): @@ -431,88 +553,6 @@ def _set_sqlite_pragma(dbapi_connection, connection_record): cursor.execute("PRAGMA foreign_keys=ON;") cursor.close() -# @event.listens_for(db.session, 'after_flush') -# def receive_after_flush(session, flush_context): -# from modules.Globals import db_man -# import json -# -# if db_man: -# events = list() -# # Updated objects -# for obj in session.dirty: -# # json_update_event = obj.to_json_update_event() -# # if json_update_event: -# # database_event = messages.DatabaseEvent() -# # database_event.type = messages.DatabaseEvent.DB_UPDATE -# # database_event.object_type = str(obj.get_model_name()) -# # database_event.object_value = json.dumps(json_update_event) -# # events.append(database_event) -# -# if isinstance(obj, TeraUser): -# new_event = messages.UserEvent() -# new_event.user_uuid = str(obj.user_uuid) -# new_event.type = messages.UserEvent.USER_UPDATED -# events.append(new_event) -# -# # Inserted objects -# for obj in session.new: -# # database_event = messages.DatabaseEvent() -# # database_event.type = messages.DatabaseEvent.DB_CREATE -# # database_event.object_type = str(obj.get_model_name()) -# # database_event.object_value = json.dumps(obj.to_json()) -# # events.append(database_event) -# -# if isinstance(obj, TeraUser): -# new_event = messages.UserEvent() -# new_event.user_uuid = str(obj.user_uuid) -# new_event.type = messages.UserEvent.USER_ADDED -# events.append(new_event) -# -# # Deleted objects -# for obj in session.deleted: -# # database_event = messages.DatabaseEvent() -# # database_event.type = messages.DatabaseEvent.DB_DELETE -# # database_event.object_type = str(obj.get_model_name()) -# # database_event.object_value = json.dumps(obj.to_json()) -# # events.append(database_event) -# -# if isinstance(obj, TeraUser): -# new_event = messages.UserEvent() -# new_event.user_uuid = str(obj.user_uuid) -# new_event.type = messages.UserEvent.USER_DELETED -# events.append(new_event) -# -# # Create event message -# if len(events) > 0: -# tera_message = db_man.create_event_message( -# create_module_event_topic_from_name(ModuleNames.DATABASE_MODULE_NAME)) -# any_events = list() -# for db_event in events: -# any_message = messages.Any() -# any_message.Pack(db_event) -# tera_message.events.append(any_message) -# -# db_man.publish(create_module_event_topic_from_name(ModuleNames.DATABASE_MODULE_NAME), -# tera_message.SerializeToString()) - -# @event.listens_for(TeraUser, 'after_update') -# def user_updated(mapper, connection, target): -# from modules.Globals import db_man -# # Publish event message -# # Advertise that we have a new user -# tera_message = db_man.create_event_message(create_module_event_topic_from_name(ModuleNames.DATABASE_MODULE_NAME)) -# user_event = messages.UserEvent() -# user_event.user_uuid = str(target.user_uuid) -# user_event.type = messages.UserEvent.USER_UPDATED -# # Need to use Any container -# any_message = messages.Any() -# any_message.Pack(user_event) -# tera_message.events.extend([any_message]) -# -# # Publish -# db_man.publish(create_module_event_topic_from_name(ModuleNames.DATABASE_MODULE_NAME), -# tera_message.SerializeToString()) - if __name__ == '__main__': with flask_app.app_context(): @@ -522,9 +562,7 @@ def _set_sqlite_pragma(dbapi_connection, connection_record): print(manager) manager.open_local(dict(), echo=True, ram=True) manager.create_defaults(config, test=True) - user = TeraUser() - user.query.all() + user_instance = TeraUser() + user_instance.query.all() test = TeraUser.query.all() print(test) - - diff --git a/teraserver/python/modules/FlaskModule/API/device/DeviceLogin.py b/teraserver/python/modules/FlaskModule/API/device/DeviceLogin.py index f6316493b..0b6652c21 100644 --- a/teraserver/python/modules/FlaskModule/API/device/DeviceLogin.py +++ b/teraserver/python/modules/FlaskModule/API/device/DeviceLogin.py @@ -3,7 +3,6 @@ from modules.LoginModule.LoginModule import LoginModule, current_device from modules.DatabaseModule.DBManager import DBManager from modules.FlaskModule.FlaskModule import device_api_ns as api -from opentera.db.models.TeraDevice import TeraDevice from opentera.redis.RedisRPCClient import RedisRPCClient from opentera.modules.BaseModule import ModuleNames from opentera.utils.UserAgentParser import UserAgentParser @@ -30,6 +29,9 @@ def __init__(self, _api, *args, **kwargs): @api.expect(get_parser) @LoginModule.device_token_or_certificate_required def get(self): + """ + Device login + """ # Redis key is handled in LoginModule server_name = self.module.config.server_config['hostname'] port = self.module.config.server_config['port'] diff --git a/teraserver/python/modules/FlaskModule/API/device/DeviceLogout.py b/teraserver/python/modules/FlaskModule/API/device/DeviceLogout.py index 3912707db..052f90e83 100644 --- a/teraserver/python/modules/FlaskModule/API/device/DeviceLogout.py +++ b/teraserver/python/modules/FlaskModule/API/device/DeviceLogout.py @@ -1,4 +1,4 @@ -from flask import jsonify, session +from flask import session from flask_restx import Resource from flask_babel import gettext from modules.LoginModule.LoginModule import LoginModule, current_device @@ -7,7 +7,7 @@ # Parser definition(s) get_parser = api.parser() -get_parser.add_argument('token', type=str, help='Secret Token') +get_parser.add_argument('token', type=str, help='Access Token') class DeviceLogout(Resource): @@ -22,6 +22,9 @@ def __init__(self, _api, *args, **kwargs): @api.expect(get_parser) @LoginModule.device_token_or_certificate_required def get(self): + """ + Device logout + """ if current_device: logout_user() session.clear() diff --git a/teraserver/python/modules/FlaskModule/API/device/DeviceQueryAssets.py b/teraserver/python/modules/FlaskModule/API/device/DeviceQueryAssets.py index c2ebb674d..1d1ac8319 100644 --- a/teraserver/python/modules/FlaskModule/API/device/DeviceQueryAssets.py +++ b/teraserver/python/modules/FlaskModule/API/device/DeviceQueryAssets.py @@ -15,7 +15,6 @@ get_parser.add_argument('with_urls', type=inputs.boolean, help='Also include assets infos and download-upload url') get_parser.add_argument('with_only_token', type=inputs.boolean, help='Only includes the access token. ' 'Will ignore with_urls if specified.') -get_parser.add_argument('token', type=str, help='Secret Token') class DeviceQueryAssets(Resource): @@ -27,10 +26,14 @@ def __init__(self, _api, *args, **kwargs): @api.doc(description='Get device assets based specified session or asset ID or, if no parameters, get all assets', responses={200: 'Success', - 403: 'Device doesn\'t have access to the specified asset'}) + 403: 'Device doesn\'t have access to the specified asset'}, + params={'token': 'Access token'}) @api.expect(get_parser) @LoginModule.device_token_or_certificate_required def get(self): + """ + Get device assets + """ args = get_parser.parse_args() device_access = DBManager.deviceAccess(current_device) diff --git a/teraserver/python/modules/FlaskModule/API/device/DeviceQueryDevices.py b/teraserver/python/modules/FlaskModule/API/device/DeviceQueryDevices.py index 871b0e114..3609912cd 100644 --- a/teraserver/python/modules/FlaskModule/API/device/DeviceQueryDevices.py +++ b/teraserver/python/modules/FlaskModule/API/device/DeviceQueryDevices.py @@ -1,5 +1,5 @@ -from flask import jsonify, session, request -from flask_restx import Resource, reqparse +from flask import request +from flask_restx import Resource from flask_babel import gettext from sqlalchemy import exc @@ -25,11 +25,14 @@ def __init__(self, _api, *args, **kwargs): 500: 'Required parameter is missing', 501: 'Not implemented', 403: 'Logged device doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(get_parser) @LoginModule.device_token_or_certificate_required def get(self): - args = get_parser.parse_args() + """ + Get connected device information + """ + # args = get_parser.parse_args() # Reply device information response = {'device_info': current_device.to_json(minimal=True)} @@ -62,9 +65,12 @@ def get(self): 403: 'Logged device can\'t update the specified device', 400: 'Badly formed JSON or missing fields(id_device) in the JSON body', 500: 'Internal error occurred when saving device'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @LoginModule.device_token_or_certificate_required def post(self): + """ + Update current device information + """ if 'device' not in request.json: return gettext('Missing device schema'), 400 diff --git a/teraserver/python/modules/FlaskModule/API/device/DeviceQueryParticipants.py b/teraserver/python/modules/FlaskModule/API/device/DeviceQueryParticipants.py index 0f925b7e2..f5994fdcf 100644 --- a/teraserver/python/modules/FlaskModule/API/device/DeviceQueryParticipants.py +++ b/teraserver/python/modules/FlaskModule/API/device/DeviceQueryParticipants.py @@ -1,10 +1,7 @@ -from flask import jsonify, session, request -from flask_restx import Resource, reqparse +from flask_restx import Resource from flask_babel import gettext from modules.LoginModule.LoginModule import LoginModule, current_device -from modules.DatabaseModule.DBManager import DBManager from modules.FlaskModule.FlaskModule import device_api_ns as api -from opentera.db.models.TeraDevice import TeraDevice # Parser definition(s) get_parser = api.parser() @@ -22,11 +19,14 @@ def __init__(self, _api, *args, **kwargs): 500: 'Required parameter is missing', 501: 'Not implemented', 403: 'Logged device doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(get_parser) @LoginModule.device_token_or_certificate_required def get(self): - args = get_parser.parse_args() + """ + Get device associated participants information + """ + # args = get_parser.parse_args() # Device must have device_onlineable flag if current_device and current_device.device_onlineable: diff --git a/teraserver/python/modules/FlaskModule/API/device/DeviceQuerySessionEvents.py b/teraserver/python/modules/FlaskModule/API/device/DeviceQuerySessionEvents.py index 706e53370..02543aaf7 100644 --- a/teraserver/python/modules/FlaskModule/API/device/DeviceQuerySessionEvents.py +++ b/teraserver/python/modules/FlaskModule/API/device/DeviceQuerySessionEvents.py @@ -1,12 +1,11 @@ -from flask import jsonify, request, session -from flask_restx import Resource, reqparse +from flask import request +from flask_restx import Resource from flask_babel import gettext from opentera.db.models.TeraSessionEvent import TeraSessionEvent from modules.LoginModule.LoginModule import LoginModule, current_device from modules.DatabaseModule.DBManager import DBManager from sqlalchemy import exc from modules.FlaskModule.FlaskModule import device_api_ns as api -from opentera.db.models.TeraDevice import TeraDevice # Parser definition(s) get_parser = api.parser() @@ -24,10 +23,13 @@ def __init__(self, _api, *args, **kwargs): @api.doc(description='Get session events', responses={403: 'Forbidden for security reasons.'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(get_parser) @LoginModule.device_token_or_certificate_required def get(self): + """ + Get events for a specific session + """ return gettext('Forbidden for security reasons'), 403 @api.doc(description='Update/Create session events', @@ -36,10 +38,13 @@ def get(self): 500: 'Internal server error', 501: 'Not implemented', 403: 'Logged device doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(post_parser) @LoginModule.device_token_or_certificate_required def post(self): + """ + Create / update session events + """ device_access = DBManager.deviceAccess(current_device) # Using request.json instead of parser, since parser messes up the json! @@ -94,4 +99,7 @@ def post(self): @LoginModule.device_token_or_certificate_required def delete(self): + """ + Delete session events + """ return gettext('Forbidden for security reasons'), 403 diff --git a/teraserver/python/modules/FlaskModule/API/device/DeviceQuerySessions.py b/teraserver/python/modules/FlaskModule/API/device/DeviceQuerySessions.py index f36df815a..977cd9940 100644 --- a/teraserver/python/modules/FlaskModule/API/device/DeviceQuerySessions.py +++ b/teraserver/python/modules/FlaskModule/API/device/DeviceQuerySessions.py @@ -1,4 +1,4 @@ -from flask import jsonify, session, request +from flask import jsonify, request from flask_restx import Resource, inputs from flask_babel import gettext from opentera.db.models.TeraSession import TeraSession @@ -7,7 +7,6 @@ from modules.LoginModule.LoginModule import LoginModule, current_device from sqlalchemy import exc from modules.FlaskModule.FlaskModule import device_api_ns as api -from opentera.db.models.TeraDevice import TeraDevice import datetime # Parser definition(s) @@ -65,10 +64,13 @@ def __init__(self, _api, *args, **kwargs): @api.doc(description='Get session', responses={403: 'Forbidden for security reasons.'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(get_parser) @LoginModule.device_token_or_certificate_required def get(self): + """ + Query device sessions + """ return gettext('Forbidden for security reasons'), 403 @api.doc(description='Update/Create session', @@ -77,10 +79,13 @@ def get(self): 500: 'Internal server error', 501: 'Not implemented', 403: 'Logged device doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(session_schema) @LoginModule.device_token_or_certificate_required def post(self): + """ + Update / create a session + """ # args = post_parser.parse_args() # Using request.json instead of parser, since parser messes up the json! if 'session' not in request.json: @@ -190,4 +195,7 @@ def post(self): @LoginModule.device_token_or_certificate_required def delete(self): + """ + Delete a session + """ return gettext('Forbidden for security reasons'), 403 diff --git a/teraserver/python/modules/FlaskModule/API/device/DeviceQueryStatus.py b/teraserver/python/modules/FlaskModule/API/device/DeviceQueryStatus.py index 4f34c6097..fd3d9289a 100644 --- a/teraserver/python/modules/FlaskModule/API/device/DeviceQueryStatus.py +++ b/teraserver/python/modules/FlaskModule/API/device/DeviceQueryStatus.py @@ -1,9 +1,8 @@ -from flask import jsonify, session, request -from flask_restx import Resource, reqparse +from flask import request +from flask_restx import Resource from modules.LoginModule.LoginModule import LoginModule, current_device from flask_babel import gettext from modules.FlaskModule.FlaskModule import device_api_ns as api -from opentera.db.models.TeraDevice import TeraDevice from opentera.redis.RedisRPCClient import RedisRPCClient from opentera.modules.BaseModule import ModuleNames import json @@ -38,10 +37,13 @@ def __init__(self, _api, *args, **kwargs): 500: 'Required parameter is missing', 501: 'Not implemented', 403: 'Logged device doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(status_schema) @LoginModule.device_token_or_certificate_required def post(self): + """ + Update current device status + """ # status_schema.validate(request.json) # This should not be required since schema should be validated first. if 'status' not in request.json or 'timestamp' not in request.json: diff --git a/teraserver/python/modules/FlaskModule/API/device/DeviceRegister.py b/teraserver/python/modules/FlaskModule/API/device/DeviceRegister.py index 7b40596c9..e8e7f95ad 100644 --- a/teraserver/python/modules/FlaskModule/API/device/DeviceRegister.py +++ b/teraserver/python/modules/FlaskModule/API/device/DeviceRegister.py @@ -66,6 +66,9 @@ def __init__(self, _api, *args, **kwargs): 401: 'Unauthorized - provided registration key is invalid'}) @api.expect(api_parser) def get(self): + """ + Register a new device in the server (token based) + """ args = api_parser.parse_args(strict=True) # Check if provided registration key is ok @@ -92,6 +95,9 @@ def get(self): 400: 'Missing or invalid parameter', 401: 'Unauthorized - provided registration key is invalid'}) def post(self): + """ + Register a new device in the server (certificate based) + """ args = api_parser.parse_args(strict=True) # Check if provided registration key is ok diff --git a/teraserver/python/modules/FlaskModule/API/participant/ParticipantLogin.py b/teraserver/python/modules/FlaskModule/API/participant/ParticipantLogin.py index 0902b5b7e..28fb42815 100644 --- a/teraserver/python/modules/FlaskModule/API/participant/ParticipantLogin.py +++ b/teraserver/python/modules/FlaskModule/API/participant/ParticipantLogin.py @@ -37,10 +37,13 @@ def __init__(self, _api, *args, **kwargs): 500: 'Required parameter is missing', 501: 'Not implemented.', 403: 'Logged user doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(get_parser) @participant_multi_auth.login_required(role='limited') def get(self): + """ + Login a participant with username / password + """ if current_participant: args = get_parser.parse_args() diff --git a/teraserver/python/modules/FlaskModule/API/participant/ParticipantLogout.py b/teraserver/python/modules/FlaskModule/API/participant/ParticipantLogout.py index cf7a96819..416b71306 100644 --- a/teraserver/python/modules/FlaskModule/API/participant/ParticipantLogout.py +++ b/teraserver/python/modules/FlaskModule/API/participant/ParticipantLogout.py @@ -1,6 +1,6 @@ -from flask import jsonify, session, request +from flask import session, request from flask_login import logout_user -from flask_restx import Resource, reqparse, fields +from flask_restx import Resource from flask_babel import gettext from modules.LoginModule.LoginModule import participant_multi_auth, current_participant, LoginModule from modules.FlaskModule.FlaskModule import participant_api_ns as api @@ -22,10 +22,13 @@ def __init__(self, _api, *args, **kwargs): 500: 'Required parameter is missing', 501: 'Not implemented.', 403: 'Logged user doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(get_parser) @participant_multi_auth.login_required def get(self): + """ + Participant logout + """ if current_participant: logout_user() session.clear() diff --git a/teraserver/python/modules/FlaskModule/API/participant/ParticipantQueryAssets.py b/teraserver/python/modules/FlaskModule/API/participant/ParticipantQueryAssets.py index 16dc80c15..b8144b5b7 100644 --- a/teraserver/python/modules/FlaskModule/API/participant/ParticipantQueryAssets.py +++ b/teraserver/python/modules/FlaskModule/API/participant/ParticipantQueryAssets.py @@ -1,10 +1,9 @@ -from flask import session, request +from flask import request from flask_restx import Resource, inputs from flask_babel import gettext from modules.LoginModule.LoginModule import participant_multi_auth, current_participant from modules.DatabaseModule.DBManager import DBManager from modules.FlaskModule.FlaskModule import device_api_ns as api -from opentera.db.models.TeraParticipant import TeraParticipant from opentera.db.models.TeraAsset import TeraAsset from opentera.redis.RedisVars import RedisVars @@ -29,10 +28,13 @@ def __init__(self, _api, *args, **kwargs): @api.doc(description='Get participant assets based on the ID or, if no parameters, get all assets', responses={200: 'Success', 403: 'Participant doesn\'t have access to the specified asset'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(get_parser) @participant_multi_auth.login_required(role='limited') def get(self): + """ + Get participant assets + """ args = get_parser.parse_args() participant_access = DBManager.participantAccess(current_participant) diff --git a/teraserver/python/modules/FlaskModule/API/participant/ParticipantQueryDevices.py b/teraserver/python/modules/FlaskModule/API/participant/ParticipantQueryDevices.py index 8944a8186..c41566d3d 100644 --- a/teraserver/python/modules/FlaskModule/API/participant/ParticipantQueryDevices.py +++ b/teraserver/python/modules/FlaskModule/API/participant/ParticipantQueryDevices.py @@ -1,9 +1,6 @@ -from flask import session from flask_restx import Resource, inputs -from flask_babel import gettext from modules.LoginModule.LoginModule import participant_multi_auth, current_participant from modules.FlaskModule.FlaskModule import participant_api_ns as api -from opentera.db.models.TeraParticipant import TeraParticipant from modules.DatabaseModule.DBManager import DBManager # Parser definition(s) @@ -26,11 +23,13 @@ def __init__(self, _api, *args, **kwargs): responses={200: 'Success', 500: 'Required parameter is missing', 501: 'Not implemented.', - 403: 'Logged user doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + 403: 'Logged user doesn\'t have permission to access the requested data'}) @api.expect(get_parser) @participant_multi_auth.login_required(role='full') def get(self): + """ + Get associated participant devices + """ participant_access = DBManager.participantAccess(current_participant) args = get_parser.parse_args(strict=True) diff --git a/teraserver/python/modules/FlaskModule/API/participant/ParticipantQueryParticipants.py b/teraserver/python/modules/FlaskModule/API/participant/ParticipantQueryParticipants.py index 70020880c..d7ee630ae 100644 --- a/teraserver/python/modules/FlaskModule/API/participant/ParticipantQueryParticipants.py +++ b/teraserver/python/modules/FlaskModule/API/participant/ParticipantQueryParticipants.py @@ -1,9 +1,7 @@ -from flask import session from flask_restx import Resource, inputs from flask_babel import gettext from modules.LoginModule.LoginModule import participant_multi_auth, current_participant from modules.FlaskModule.FlaskModule import participant_api_ns as api -from opentera.db.models.TeraParticipant import TeraParticipant from modules.DatabaseModule.DBManager import DBManager # Parser definition(s) @@ -26,10 +24,13 @@ def __init__(self, _api, *args, **kwargs): 500: 'Required parameter is missing', 501: 'Not implemented.', 403: 'Logged user doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(get_parser) @participant_multi_auth.login_required(role='limited') def get(self): + """ + Get current participant informations + """ participant_access = DBManager.participantAccess(current_participant) args = get_parser.parse_args(strict=True) @@ -47,9 +48,11 @@ def get(self): responses={200: 'Success - To be documented', 500: 'Required parameter is missing', 501: 'Not implemented.', - 403: 'Logged user doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + 403: 'Logged user doesn\'t have permission to access the requested data'}) @api.expect(post_parser) @participant_multi_auth.login_required(role='full') def post(self): + """ + Update current participant informations + """ return gettext('Not implemented'), 501 diff --git a/teraserver/python/modules/FlaskModule/API/participant/ParticipantQuerySessions.py b/teraserver/python/modules/FlaskModule/API/participant/ParticipantQuerySessions.py index de80ae446..fbf98eb76 100644 --- a/teraserver/python/modules/FlaskModule/API/participant/ParticipantQuerySessions.py +++ b/teraserver/python/modules/FlaskModule/API/participant/ParticipantQuerySessions.py @@ -75,9 +75,11 @@ def __init__(self, _api, *args, **kwargs): 400: 'Bad request', 500: 'Required parameter is missing', 501: 'Not implemented.', - 403: 'Logged user doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + 403: 'Logged user doesn\'t have permission to access the requested data'}) def get(self): + """ + Get participant sessions + """ participant_access = DBManager.participantAccess(current_participant) args = get_parser.parse_args(strict=True) @@ -105,10 +107,13 @@ def get(self): 500: 'Internal server error', 501: 'Not implemented', 403: 'Logged participant doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(session_schema) @participant_multi_auth.login_required(role='limited') def post(self): + """ + Create / update a session + """ # args = post_parser.parse_args() # Using request.json instead of parser, since parser messes up the json! if 'session' not in request.json: diff --git a/teraserver/python/modules/FlaskModule/API/participant/ParticipantRefreshToken.py b/teraserver/python/modules/FlaskModule/API/participant/ParticipantRefreshToken.py index 79bf760ae..bf9f0a2b5 100644 --- a/teraserver/python/modules/FlaskModule/API/participant/ParticipantRefreshToken.py +++ b/teraserver/python/modules/FlaskModule/API/participant/ParticipantRefreshToken.py @@ -1,10 +1,9 @@ -from flask import session, request +from flask import request from flask_restx import Resource from modules.LoginModule.LoginModule import participant_token_auth, current_participant from modules.FlaskModule.FlaskModule import participant_api_ns as api from modules.LoginModule.LoginModule import LoginModule from opentera.redis.RedisVars import RedisVars -from opentera.db.models.TeraParticipant import TeraParticipant # Parser definition(s) get_parser = api.parser() @@ -19,11 +18,13 @@ def __init__(self, _api, *args, **kwargs): @api.doc(description='Refresh token, old token needs to be passed in request headers.', responses={200: 'Success', - 500: 'Server error'}, - params={'token': 'Secret token'}) + 500: 'Server error'}) @api.expect(get_parser) @participant_token_auth.login_required(role='full') def get(self): + """ + Refresh participant dynamic token + """ # If we have made it this far, token passed in headers was valid. # Get user token key from redis diff --git a/teraserver/python/modules/FlaskModule/API/service/ServiceQueryAccess.py b/teraserver/python/modules/FlaskModule/API/service/ServiceQueryAccess.py index 16da654f5..fb846588e 100644 --- a/teraserver/python/modules/FlaskModule/API/service/ServiceQueryAccess.py +++ b/teraserver/python/modules/FlaskModule/API/service/ServiceQueryAccess.py @@ -1,5 +1,4 @@ -from flask import request -from flask_restx import Resource, reqparse, inputs +from flask_restx import Resource, inputs from flask_babel import gettext from modules.LoginModule.LoginModule import LoginModule, current_service from modules.FlaskModule.FlaskModule import service_api_ns as api @@ -41,10 +40,13 @@ def __init__(self, _api, *args, **kwargs): 500: 'Required parameter is missing', 501: 'Not implemented.', 403: 'Service doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(get_parser) @LoginModule.service_token_or_certificate_required def get(self): + """ + Get current service access + """ service_access = DBManager.serviceAccess(current_service) args = get_parser.parse_args() diff --git a/teraserver/python/modules/FlaskModule/API/service/ServiceQueryAssets.py b/teraserver/python/modules/FlaskModule/API/service/ServiceQueryAssets.py index 4d53f379c..ea5d4c98e 100644 --- a/teraserver/python/modules/FlaskModule/API/service/ServiceQueryAssets.py +++ b/teraserver/python/modules/FlaskModule/API/service/ServiceQueryAssets.py @@ -1,5 +1,5 @@ from flask import request -from flask_restx import Resource, reqparse, inputs +from flask_restx import Resource, inputs from flask_babel import gettext from modules.LoginModule.LoginModule import LoginModule, current_service from modules.FlaskModule.FlaskModule import service_api_ns as api @@ -48,10 +48,13 @@ def __init__(self, _api, *args, **kwargs): 500: 'Required parameter is missing', 501: 'Not implemented.', 403: 'Service doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(get_parser) @LoginModule.service_token_or_certificate_required def get(self): + """ + Get assets + """ service_access = DBManager.serviceAccess(current_service) args = get_parser.parse_args() @@ -151,10 +154,13 @@ def get(self): 400: 'Bad request - wrong or missing parameters in query', 500: 'Required parameter is missing', 403: 'Service doesn\'t have permission to post that asset'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(post_parser) @LoginModule.service_token_or_certificate_required def post(self): + """ + Create / update an asset + """ args = post_parser.parse_args() service_access = DBManager.serviceAccess(current_service) @@ -254,10 +260,13 @@ def post(self): responses={200: 'Success', 403: 'Service can\'t delete asset', 500: 'Database error.'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(delete_parser) @LoginModule.service_token_or_certificate_required def delete(self): + """ + Delete a specific asset + """ service_access = DBManager.serviceAccess(current_service) parser = delete_parser diff --git a/teraserver/python/modules/FlaskModule/API/service/ServiceQueryDevices.py b/teraserver/python/modules/FlaskModule/API/service/ServiceQueryDevices.py index 6e378faf7..b1c4d395f 100644 --- a/teraserver/python/modules/FlaskModule/API/service/ServiceQueryDevices.py +++ b/teraserver/python/modules/FlaskModule/API/service/ServiceQueryDevices.py @@ -4,11 +4,6 @@ from modules.LoginModule.LoginModule import LoginModule from modules.FlaskModule.FlaskModule import service_api_ns as api from opentera.db.models.TeraDevice import TeraDevice -from opentera.db.models.TeraDeviceType import TeraDeviceType -from opentera.db.models.TeraDeviceSubType import TeraDeviceSubType - -import uuid -from datetime import datetime # Parser definition(s) get_parser = api.parser() @@ -40,10 +35,13 @@ def __init__(self, _api, *args, **kwargs): 500: 'Required parameter is missing', 501: 'Not implemented.', 403: 'Service doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(get_parser) @LoginModule.service_token_or_certificate_required def get(self): + """ + Query device information + """ args = get_parser.parse_args() # args['device_uuid'] Will be None if not specified in args if args['device_uuid']: @@ -77,10 +75,13 @@ def get(self): 500: 'Required parameter is missing', 501: 'Not implemented.', 403: 'Service doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(device_schema, validate=True) @LoginModule.service_token_or_certificate_required def post(self): + """ + Create / update device + """ # args = post_parser.parse_args() # Using request.json instead of parser, since parser messes up the json! if 'device' not in request.json: diff --git a/teraserver/python/modules/FlaskModule/API/service/ServiceQueryDisconnect.py b/teraserver/python/modules/FlaskModule/API/service/ServiceQueryDisconnect.py index 5daab2304..763b89478 100644 --- a/teraserver/python/modules/FlaskModule/API/service/ServiceQueryDisconnect.py +++ b/teraserver/python/modules/FlaskModule/API/service/ServiceQueryDisconnect.py @@ -1,5 +1,4 @@ -from flask import request -from flask_restx import Resource, inputs +from flask_restx import Resource from flask_babel import gettext from modules.LoginModule.LoginModule import LoginModule, current_service from modules.FlaskModule.FlaskModule import service_api_ns as api @@ -33,10 +32,13 @@ def __init__(self, _api, *args, **kwargs): 403: 'Forbidden access. Please check that the service has access to' ' the requested id/uuid.', 500: 'Database error'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(get_parser) @LoginModule.service_token_or_certificate_required def get(self): + """ + Forcefully disconnect a user, participant or device + """ args = get_parser.parse_args() service_access: DBManagerTeraServiceAccess = DBManager.serviceAccess(current_service) diff --git a/teraserver/python/modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py b/teraserver/python/modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py index 27b5a4fbd..21364dae6 100644 --- a/teraserver/python/modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py +++ b/teraserver/python/modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py @@ -35,10 +35,13 @@ def __init__(self, _api, *args, **kwargs): 500: 'Required parameter is missing', 501: 'Not implemented.', 403: 'Service doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(get_parser) @LoginModule.service_token_or_certificate_required def get(self): + """ + Get participant groups + """ # Get service access manager, that allows to check for access service_access = DBManager.serviceAccess(current_service) @@ -80,10 +83,13 @@ def get(self): 500: 'Required parameter is missing', 501: 'Not implemented.', 403: 'Logged user doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(post_schema) @LoginModule.service_token_or_certificate_required def post(self): + """ + Create / update participant group + """ # Parse arguments args = post_parser.parse_args() @@ -163,10 +169,13 @@ def post(self): responses={200: 'Success', 403: 'Logged user doesn\'t have permission to access the requested data', 500: 'Database error.'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(delete_parser) @LoginModule.service_token_or_certificate_required def delete(self): + """ + Delete participant group + """ # Parse arguments args = delete_parser.parse_args() id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/service/ServiceQueryParticipants.py b/teraserver/python/modules/FlaskModule/API/service/ServiceQueryParticipants.py index 6b7e8f0f1..4f952a8b9 100644 --- a/teraserver/python/modules/FlaskModule/API/service/ServiceQueryParticipants.py +++ b/teraserver/python/modules/FlaskModule/API/service/ServiceQueryParticipants.py @@ -58,10 +58,13 @@ def __init__(self, _api, *args, **kwargs): 500: 'Required parameter is missing', 501: 'Not implemented.', 403: 'Service doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(get_parser) @LoginModule.service_token_or_certificate_required def get(self): + """ + Get participant + """ args = get_parser.parse_args() service_access = DBManager.serviceAccess(current_service) @@ -108,10 +111,13 @@ def get(self): 500: 'Required parameter is missing', 501: 'Not implemented.', 403: 'Logged user doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(participant_schema) @LoginModule.service_token_or_certificate_required def post(self): + """ + Create / update a participant + """ args = post_parser.parse_args() # Using request.json instead of parser, since parser messes up the json! diff --git a/teraserver/python/modules/FlaskModule/API/service/ServiceQueryProjects.py b/teraserver/python/modules/FlaskModule/API/service/ServiceQueryProjects.py index 3c51fb3b6..72ffd6213 100644 --- a/teraserver/python/modules/FlaskModule/API/service/ServiceQueryProjects.py +++ b/teraserver/python/modules/FlaskModule/API/service/ServiceQueryProjects.py @@ -35,10 +35,13 @@ def __init__(self, _api, *args, **kwargs): 500: 'Required parameter is missing', 501: 'Not implemented.', 403: 'Service doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(get_parser) @LoginModule.service_token_or_certificate_required def get(self): + """ + Get projects + """ args = get_parser.parse_args() service_access = DBManager.serviceAccess(current_service) @@ -77,10 +80,13 @@ def get(self): 403: 'Logged service can\'t create/update the specified project', 400: 'Badly formed JSON or missing fields(id_site or id_project) in the JSON body', 500: 'Internal error occured when saving project'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(post_schema) @LoginModule.service_token_or_certificate_required def post(self): + """ + Create / update projects + """ service_access = DBManager.serviceAccess(current_service) # Using request.json instead of parser, since parser messes up the json! if 'project' not in request.json: @@ -145,10 +151,13 @@ def post(self): responses={200: 'Success', 403: 'Logged service can\'t delete project (service not associated to)', 500: 'Database error.'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(delete_parser) @LoginModule.service_token_or_certificate_required def delete(self): + """ + Delete project + """ service_access = DBManager.serviceAccess(current_service) args = delete_parser.parse_args() id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/service/ServiceQueryRoles.py b/teraserver/python/modules/FlaskModule/API/service/ServiceQueryRoles.py index a2f38f315..f99bc3403 100644 --- a/teraserver/python/modules/FlaskModule/API/service/ServiceQueryRoles.py +++ b/teraserver/python/modules/FlaskModule/API/service/ServiceQueryRoles.py @@ -27,10 +27,13 @@ def __init__(self, _api, *args, **kwargs): @api.doc(description='Get service roles for that service', responses={200: 'Success - returns list of roles', 500: 'Database error'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(get_parser) @LoginModule.service_token_or_certificate_required def get(self): + """ + Get service roles for the current service + """ args = get_parser.parse_args() roles = TeraServiceRole.get_service_roles(service_id=current_service.id_service) roles_list = [] @@ -43,10 +46,13 @@ def get(self): responses={200: 'Success', 400: 'Badly formed JSON or missing fields in the JSON body', 500: 'Internal error when saving roles'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(post_schema) @LoginModule.service_token_or_certificate_required def post(self): + """ + Create / update service roles for the current service + """ # Using request.json instead of parser, since parser messes up the json! if 'service_role' not in request.json: return gettext('Missing service_role field'), 400 @@ -99,10 +105,13 @@ def post(self): responses={200: 'Success', 403: 'Logged service can\'t delete role (not related to that service)', 500: 'Database error.'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(delete_parser) @LoginModule.service_token_or_certificate_required def delete(self): + """ + Delete a specific service role + """ args = delete_parser.parse_args() id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/service/ServiceQueryServiceAccess.py b/teraserver/python/modules/FlaskModule/API/service/ServiceQueryServiceAccess.py index bf434ab01..661af3427 100644 --- a/teraserver/python/modules/FlaskModule/API/service/ServiceQueryServiceAccess.py +++ b/teraserver/python/modules/FlaskModule/API/service/ServiceQueryServiceAccess.py @@ -36,10 +36,13 @@ def __init__(self, _api, *args, **kwargs): responses={200: 'Success - returns list of access roles', 400: 'Required parameter is missing (must have at least one id)', 500: 'Error when getting association'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(get_parser) @LoginModule.service_token_or_certificate_required def get(self): + """ + Get access roles for a specific item + """ service_access: DBManagerTeraServiceAccess = DBManager.serviceAccess(current_service) args = get_parser.parse_args() @@ -72,10 +75,13 @@ def get(self): 403: 'Logged service can\'t modify association (only self access can be modified)', 400: 'Badly formed JSON or missing fields in the JSON body', 500: 'Internal error occurred when saving association'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(post_schema) @LoginModule.service_token_or_certificate_required def post(self): + """ + Create / update service - access association + """ service_access: DBManagerTeraServiceAccess = DBManager.serviceAccess(current_service) # Using request.json instead of parser, since parser messes up the json! @@ -177,10 +183,13 @@ def post(self): responses={200: 'Success', 403: 'Logged user can\'t delete association (not related to this service)', 500: 'Association not found or database error.'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(delete_parser) @LoginModule.service_token_or_certificate_required def delete(self): + """ + Delete a specific service access + """ service_access: DBManagerTeraServiceAccess = DBManager.serviceAccess(current_service) args = delete_parser.parse_args() id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/service/ServiceQueryServices.py b/teraserver/python/modules/FlaskModule/API/service/ServiceQueryServices.py index 50d6b0a0e..e453d25a8 100644 --- a/teraserver/python/modules/FlaskModule/API/service/ServiceQueryServices.py +++ b/teraserver/python/modules/FlaskModule/API/service/ServiceQueryServices.py @@ -30,6 +30,9 @@ def __init__(self, _api, *args, **kwargs): @api.expect(get_parser) @LoginModule.service_token_or_certificate_required def get(self): + """ + Get service information + """ args = get_parser.parse_args() services = [] diff --git a/teraserver/python/modules/FlaskModule/API/service/ServiceQuerySessionEvents.py b/teraserver/python/modules/FlaskModule/API/service/ServiceQuerySessionEvents.py index afc33a518..38dfa396a 100644 --- a/teraserver/python/modules/FlaskModule/API/service/ServiceQuerySessionEvents.py +++ b/teraserver/python/modules/FlaskModule/API/service/ServiceQuerySessionEvents.py @@ -1,9 +1,8 @@ -from flask import jsonify, session, request +from flask import request from flask_restx import Resource, reqparse from flask_babel import gettext from modules.LoginModule.LoginModule import LoginModule, current_service from modules.FlaskModule.FlaskModule import service_api_ns as api -from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraSessionEvent import TeraSessionEvent from opentera.db.models.TeraSession import TeraSession from modules.DatabaseModule.DBManager import DBManager @@ -34,10 +33,13 @@ def __init__(self, _api, *args, **kwargs): 400: 'Required parameter is missing (id_session)', 403: 'Service doesn\'t have permission to access the requested data', 500: 'Database error'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(get_parser) @LoginModule.service_token_or_certificate_required def get(self): + """ + Get events for a session + """ args = get_parser.parse_args() sessions_events = [] @@ -67,10 +69,13 @@ def get(self): 403: 'Logged user can\'t create/update the specified event', 400: 'Badly formed JSON or missing fields(id_session_event or id_session) in the JSON body', 500: 'Internal error when saving device'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(post_schema) @LoginModule.service_token_or_certificate_required def post(self): + """ + Create / update session events + """ # Using request.json instead of parser, since parser messes up the json! if 'session_event' not in request.json: return gettext('Missing session_event field'), 400 diff --git a/teraserver/python/modules/FlaskModule/API/service/ServiceQuerySessionTypes.py b/teraserver/python/modules/FlaskModule/API/service/ServiceQuerySessionTypes.py index 07e0034a8..82bb3967e 100644 --- a/teraserver/python/modules/FlaskModule/API/service/ServiceQuerySessionTypes.py +++ b/teraserver/python/modules/FlaskModule/API/service/ServiceQuerySessionTypes.py @@ -27,10 +27,13 @@ def __init__(self, _api, *args, **kwargs): 500: 'Required parameter is missing', 501: 'Not implemented.', 403: 'Service doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(get_parser) @LoginModule.service_token_or_certificate_required def get(self): + """ + Get session types associated to that service + """ args = get_parser.parse_args() service_access = DBManager.serviceAccess(current_service) diff --git a/teraserver/python/modules/FlaskModule/API/service/ServiceQuerySessions.py b/teraserver/python/modules/FlaskModule/API/service/ServiceQuerySessions.py index 64a9cc582..b45452c0d 100644 --- a/teraserver/python/modules/FlaskModule/API/service/ServiceQuerySessions.py +++ b/teraserver/python/modules/FlaskModule/API/service/ServiceQuerySessions.py @@ -1,5 +1,5 @@ from flask import request -from flask_restx import Resource, inputs # , reqparse +from flask_restx import Resource, inputs from flask_babel import gettext from modules.LoginModule.LoginModule import LoginModule, current_service from modules.FlaskModule.FlaskModule import service_api_ns as api @@ -52,10 +52,13 @@ def __init__(self, _api, *args, **kwargs): 500: 'Required parameter is missing', 501: 'Not implemented.', 403: 'Service doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(get_parser) @LoginModule.service_token_or_certificate_required def get(self): + """ + Get sessions + """ args = get_parser.parse_args() service_access = DBManager.serviceAccess(current_service) @@ -128,10 +131,13 @@ def get(self): 400: 'Badly formed JSON or missing fields(session, id_session, session_participants_ids and/or ' 'session_users_ids[for new sessions]) in the JSON body', 500: 'Internal error when saving session'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(post_schema) @LoginModule.service_token_or_certificate_required def post(self): + """ + Create / update session + """ if 'session' not in request.json: return gettext('Missing session'), 400 diff --git a/teraserver/python/modules/FlaskModule/API/service/ServiceQuerySiteProjectAccessRoles.py b/teraserver/python/modules/FlaskModule/API/service/ServiceQuerySiteProjectAccessRoles.py index c71359594..cd688ea6b 100644 --- a/teraserver/python/modules/FlaskModule/API/service/ServiceQuerySiteProjectAccessRoles.py +++ b/teraserver/python/modules/FlaskModule/API/service/ServiceQuerySiteProjectAccessRoles.py @@ -24,10 +24,13 @@ def __init__(self, _api, *args, **kwargs): 500: 'Required parameter is missing', 501: 'Not implemented.', 403: 'Service doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(get_parser) @LoginModule.service_token_or_certificate_required def get(self): + """ + Get access roles for a specific user and/or project/site + """ args = get_parser.parse_args(strict=True) service_access = DBManager.serviceAccess(current_service) diff --git a/teraserver/python/modules/FlaskModule/API/service/ServiceQuerySites.py b/teraserver/python/modules/FlaskModule/API/service/ServiceQuerySites.py index 0714bfd40..25a09c3d2 100644 --- a/teraserver/python/modules/FlaskModule/API/service/ServiceQuerySites.py +++ b/teraserver/python/modules/FlaskModule/API/service/ServiceQuerySites.py @@ -23,10 +23,13 @@ def __init__(self, _api, *args, **kwargs): 500: 'Required parameter is missing', 501: 'Not implemented.', 403: 'Service doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(get_parser) @LoginModule.service_token_or_certificate_required def get(self): + """ + Get sites + """ args = get_parser.parse_args(strict=True) service_access = DBManager.serviceAccess(current_service) diff --git a/teraserver/python/modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py b/teraserver/python/modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py index 9847e29a3..7ca1b8477 100644 --- a/teraserver/python/modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py +++ b/teraserver/python/modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py @@ -42,10 +42,13 @@ def __init__(self, _api, *args, **kwargs): responses={200: 'Success - returns list of test-types - projects association', 400: 'Required parameter is missing (must have at least one id)', 500: 'Error when getting association'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(get_parser) @LoginModule.service_token_or_certificate_required def get(self): + """ + Get test types associated with a project + """ service_access = DBManager.serviceAccess(current_service) args = get_parser.parse_args() @@ -102,10 +105,13 @@ def get(self): 403: 'Logged service can\'t modify association (not associated to project or test type)', 400: 'Badly formed JSON or missing fields in the JSON body', 500: 'Internal error occurred when saving association'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(post_schema) @LoginModule.service_token_or_certificate_required def post(self): + """ + Create / update test types -> project association + """ service_access = DBManager.serviceAccess(current_service) accessible_projects_ids = service_access.get_accessible_projects_ids(admin_only=True) @@ -238,10 +244,13 @@ def post(self): responses={200: 'Success', 403: 'Logged service can\'t delete association (no access to test-type or project)', 400: 'Association not found (invalid id?)'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(delete_parser) @LoginModule.service_token_or_certificate_required def delete(self): + """ + Delete a specific test type -> project association + """ service_access = DBManager.serviceAccess(current_service) args = delete_parser.parse_args() id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/service/ServiceQueryTestTypes.py b/teraserver/python/modules/FlaskModule/API/service/ServiceQueryTestTypes.py index ecbeaf6b6..55a497c5f 100644 --- a/teraserver/python/modules/FlaskModule/API/service/ServiceQueryTestTypes.py +++ b/teraserver/python/modules/FlaskModule/API/service/ServiceQueryTestTypes.py @@ -39,10 +39,13 @@ def __init__(self, _api, *args, **kwargs): 500: 'Required parameter is missing', 501: 'Not implemented.', 403: 'Service doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(get_parser) @LoginModule.service_token_or_certificate_required def get(self): + """ + Get test types + """ args = get_parser.parse_args(strict=True) service_access = DBManager.serviceAccess(current_service) @@ -78,10 +81,13 @@ def get(self): 403: 'Service can\'t create/update the specified test type', 400: 'Badly formed JSON or missing field in the JSON body', 500: 'Internal error when saving test type'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(post_schema) @LoginModule.service_token_or_certificate_required def post(self): + """ + Create / update test types + """ # Using request.json instead of parser, since parser messes up the json! if 'test_type' not in request.json: return gettext('Missing test_type'), 400 @@ -139,10 +145,13 @@ def post(self): responses={200: 'Success', 403: 'Service can\'t delete test type', 500: 'Database error.'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(delete_parser) @LoginModule.service_token_or_certificate_required def delete(self): + """ + Delete a test type + """ args = delete_parser.parse_args() uuid_todel = args['uuid'] diff --git a/teraserver/python/modules/FlaskModule/API/service/ServiceQueryTests.py b/teraserver/python/modules/FlaskModule/API/service/ServiceQueryTests.py index 400e1cb64..7a76e7084 100644 --- a/teraserver/python/modules/FlaskModule/API/service/ServiceQueryTests.py +++ b/teraserver/python/modules/FlaskModule/API/service/ServiceQueryTests.py @@ -1,5 +1,5 @@ from flask import request -from flask_restx import Resource, reqparse, inputs +from flask_restx import Resource, inputs from flask_babel import gettext from modules.LoginModule.LoginModule import LoginModule, current_service from modules.FlaskModule.FlaskModule import service_api_ns as api @@ -44,10 +44,13 @@ def __init__(self, _api, *args, **kwargs): 500: 'Required parameter is missing', 501: 'Not implemented.', 403: 'Service doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(get_parser) @LoginModule.service_token_or_certificate_required def get(self): + """ + Get tests + """ service_access = DBManager.serviceAccess(current_service) args = get_parser.parse_args() @@ -120,10 +123,13 @@ def get(self): 400: 'Bad request - wrong or missing parameters in query', 500: 'Required parameter is missing', 403: 'Service doesn\'t have permission to post that test'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(post_parser) @LoginModule.service_token_or_certificate_required def post(self): + """ + Create / update test + """ service_access = DBManager.serviceAccess(current_service) # Using request.json instead of parser, since parser messes up the json! @@ -249,10 +255,13 @@ def post(self): responses={200: 'Success', 403: 'Service can\'t delete test', 500: 'Database error.'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(delete_parser) @LoginModule.service_token_or_certificate_required def delete(self): + """ + Delete specific test + """ service_access = DBManager.serviceAccess(current_service) args = delete_parser.parse_args() uuid_todel = args['uuid'] diff --git a/teraserver/python/modules/FlaskModule/API/service/ServiceQueryUserGroups.py b/teraserver/python/modules/FlaskModule/API/service/ServiceQueryUserGroups.py index faf0455f8..d2d67e22a 100644 --- a/teraserver/python/modules/FlaskModule/API/service/ServiceQueryUserGroups.py +++ b/teraserver/python/modules/FlaskModule/API/service/ServiceQueryUserGroups.py @@ -8,7 +8,6 @@ from opentera.db.models.TeraUserGroup import TeraUserGroup from flask_babel import gettext from modules.DatabaseModule.DBManager import DBManager, DBManagerTeraServiceAccess -import modules.Globals as Globals # Parser definition(s) get_parser = api.parser() @@ -34,10 +33,13 @@ def __init__(self, _api, *args, **kwargs): @api.doc(description='Get user group information. If no id specified, returns all accessible users groups', responses={200: 'Success', 500: 'Database error'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(get_parser) @LoginModule.service_token_or_certificate_required def get(self): + """ + Get usergroups + """ service_access: DBManagerTeraServiceAccess = DBManager.serviceAccess(current_service) args = get_parser.parse_args() user_groups = [] @@ -71,10 +73,13 @@ def get(self): 403: 'Logged service can\'t create/update the specified user group', 400: 'Badly formed JSON or missing field(id_user_group) in the JSON body', 500: 'Internal error when saving user group'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(post_schema) @LoginModule.service_token_or_certificate_required def post(self): + """ + Create / update usergroup + """ service_access: DBManagerTeraServiceAccess = DBManager.serviceAccess(current_service) if 'user_group' not in request.json: @@ -193,10 +198,13 @@ def post(self): responses={200: 'Success', 403: 'Service can\'t delete user group (no access to it)', 500: 'Database error.'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(delete_parser) @LoginModule.service_token_or_certificate_required def delete(self): + """ + Delete a specific usergroup + """ service_access: DBManagerTeraServiceAccess = DBManager.serviceAccess(current_service) args = delete_parser.parse_args() id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/service/ServiceQueryUsers.py b/teraserver/python/modules/FlaskModule/API/service/ServiceQueryUsers.py index 5af700fc0..7743ef5b2 100644 --- a/teraserver/python/modules/FlaskModule/API/service/ServiceQueryUsers.py +++ b/teraserver/python/modules/FlaskModule/API/service/ServiceQueryUsers.py @@ -23,10 +23,13 @@ def __init__(self, _api, *args, **kwargs): 500: 'Required parameter is missing', 501: 'Not implemented.', 403: 'Service doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(get_parser) @LoginModule.service_token_or_certificate_required def get(self): + """ + Get specific user information + """ args = get_parser.parse_args() # args['user_id'] Will be None if not specified in args diff --git a/teraserver/python/modules/FlaskModule/API/service/ServiceSessionManager.py b/teraserver/python/modules/FlaskModule/API/service/ServiceSessionManager.py index 8acee8dd8..89735f4e9 100644 --- a/teraserver/python/modules/FlaskModule/API/service/ServiceSessionManager.py +++ b/teraserver/python/modules/FlaskModule/API/service/ServiceSessionManager.py @@ -1,8 +1,7 @@ -from flask import session, request +from flask import request from flask_restx import Resource from modules.LoginModule.LoginModule import LoginModule, current_service from modules.FlaskModule.FlaskModule import user_api_ns as api -from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraService import TeraService from opentera.db.models.TeraSession import TeraSession from flask_babel import gettext @@ -99,10 +98,13 @@ def __init__(self, _api, *args, **kwargs): 500: 'Internal server error', 501: 'Not implemented', 403: 'Service doesn\'t have enough permission'}, - params={'token': 'Secret token'}) + params={'token': 'Access token'}) @api.expect(session_manager_schema) @LoginModule.service_token_or_certificate_required def post(self): + """ + Starts / stop a session related to a service + """ args = post_parser.parse_args() service_access = DBManager.serviceAccess(current_service) diff --git a/teraserver/python/modules/FlaskModule/API/user/UserLogin.py b/teraserver/python/modules/FlaskModule/API/user/UserLogin.py index 596c2ca89..dc4efd240 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserLogin.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserLogin.py @@ -1,157 +1,117 @@ -from flask import session, request -from flask_restx import Resource, reqparse, inputs +from flask_restx import inputs from flask_babel import gettext -from modules.LoginModule.LoginModule import user_http_auth, LoginModule, current_user +from modules.LoginModule.LoginModule import current_user, user_http_login_auth from modules.FlaskModule.FlaskModule import user_api_ns as api -from opentera.redis.RedisRPCClient import RedisRPCClient -from opentera.modules.BaseModule import ModuleNames -from opentera.utils.UserAgentParser import UserAgentParser +from modules.FlaskModule.API.user.UserLoginBase import UserLoginBase +from modules.FlaskModule.API.user.UserLoginBase import OutdatedClientVersionError, \ + UserAlreadyLoggedInError, TooMany2FALoginAttemptsError -import opentera.messages.python as messages -from opentera.redis.RedisVars import RedisVars -from opentera.db.models.TeraUser import TeraUser -# model = api.model('Login', { -# 'websocket_url': fields.String, -# 'user_uuid': fields.String, -# 'user_token': fields.String -# }) -# Parser definition(s) get_parser = api.parser() -get_parser.add_argument('with_websocket', type=inputs.boolean, help='If set, requires that a websocket url is returned.' - 'If not possible to do so, return a 403 error.') +get_parser.add_argument('with_websocket', type=inputs.boolean, default=False, + help='If set, requires that a websocket url is returned.' + 'If not possible to do so, return a 403 error.') +post_parser = api.parser() +post_parser.add_argument('with_websocket', type=inputs.boolean, default=False, + help='If set, requires that a websocket url is returned.' + 'If not possible to do so, return a 403 error.') -class UserLogin(Resource): +class UserLogin(UserLoginBase): + """ + UserLogin Resource. + """ def __init__(self, _api, *args, **kwargs): - Resource.__init__(self, _api, *args, **kwargs) - self.module = kwargs.get('flaskModule', None) - self.test = kwargs.get('test', False) - - @api.doc(description='Login to the server using HTTP Basic Authentification (HTTPAuth)') + UserLoginBase.__init__(self, _api, *args, **kwargs) + + def _common_login_response(self, parser): + try: + # Validate args + args = parser.parse_args(strict=True) + response = {} + + version_info = self._verify_client_version() + if version_info: + response.update(version_info) + + # User needs to change password? + if current_user.user_force_password_change: + response['message'] = gettext('Password change required for this user.') + response['reason'] = 'password_change' + response['redirect_url'] = self._generate_password_change_url() + + # 2FA enabled? Client will need to proceed to 2FA login step first + if current_user.user_2fa_enabled and not current_user.user_force_password_change: + + # If user had too many 2FA login failures, stop login process + self._verify_2fa_login_attempts(current_user.user_uuid) + + if current_user.user_2fa_otp_enabled and current_user.user_2fa_otp_secret: + response['message'] = gettext('2FA required for this user.') + response['reason'] = '2fa' + response['redirect_url'] = self._generate_2fa_verification_url() + else: + response['message'] = gettext('2FA enabled but OTP not set for this user.' + 'Please setup 2FA.') + response['reason'] = '2fa_setup' + response['redirect_url'] = self._generate_2fa_setup_url() + else: + # Standard Login without 2FA. Check if user is already logged in. + if args['with_websocket']: + self._verify_user_already_logged_in() + response['websocket_url'] = self._generate_websocket_url() + + # Generate user token + response['user_uuid'] = current_user.user_uuid + response['user_token'] = self._generate_user_token() + + self._send_login_success_message() + + except OutdatedClientVersionError as e: + self._user_logout() + + return { + 'version_latest': e.version_latest, + 'current_version': e.current_version, + 'version_error': e.version_error, + 'message': gettext('Client major version too old, not accepting login')}, 426 +# except InvalidClientVersionError as e: +# # Invalid client version, will not be handled for now +# pass + except UserAlreadyLoggedInError as e: + self._user_logout() + return str(e), 403 + except TooMany2FALoginAttemptsError as e: + self._user_logout() + return str(e), 403 + except Exception as e: + # Something went wrong, logout user + self._user_logout() + raise e + else: + # Everything went well, return response + return response, 200 + + + @api.doc(description='Login to the server using HTTP Basic Authentication (HTTPAuth)', + security='basicAuth') @api.expect(get_parser) - @user_http_auth.login_required + @user_http_login_auth.login_required def get(self): - parser = get_parser - args = parser.parse_args() - - # Redis key is handled in LoginModule - servername = self.module.config.server_config['hostname'] - port = self.module.config.server_config['port'] - if 'X_EXTERNALSERVER' in request.headers: - servername = request.headers['X_EXTERNALSERVER'] - - if 'X_EXTERNALPORT' in request.headers: - port = request.headers['X_EXTERNALPORT'] - - websocket_url = None - - # Get user token key from redis - token_key = self.module.redisGet(RedisVars.RedisVar_UserTokenAPIKey) - - # Get login informations for log - login_infos = UserAgentParser.parse_request_for_login_infos(request) - - # Verify if user already logged in - online_users = [] - if not self.test: - rpc = RedisRPCClient(self.module.config.redis_config) - online_users = rpc.call(ModuleNames.USER_MANAGER_MODULE_NAME.value, 'online_users') - - if current_user.user_uuid not in online_users: - websocket_url = "wss://" + servername + ":" + str(port) + "/wss/user?id=" + session['_id'] - # print('Login - setting key with expiration in 60s', session['_id'], session['_user_id']) - self.module.redisSet(session['_id'], session['_user_id'], ex=60) - elif args['with_websocket']: - # User is online and a websocket is required - self.module.logger.send_login_event(sender=self.module.module_name, - level=messages.LogEvent.LOGLEVEL_ERROR, - login_type=messages.LoginEvent.LOGIN_TYPE_PASSWORD, - login_status= - messages.LoginEvent.LOGIN_STATUS_FAILED_WITH_ALREADY_LOGGED_IN, - client_name=login_infos['client_name'], - client_version=login_infos['client_version'], - client_ip=login_infos['client_ip'], - os_name=login_infos['os_name'], - os_version=login_infos['os_version'], - user_uuid=current_user.user_uuid, - server_endpoint=login_infos['server_endpoint']) - - return gettext('User already logged in.'), 403 - - current_user.update_last_online() - user_token = current_user.get_token(token_key) - - # Return reply as json object - reply = {"user_uuid": session['_user_id'], - "user_token": user_token} - if websocket_url: - reply["websocket_url"] = websocket_url - - # Verify client version (optional for now) - # And add info to reply - if 'X-Client-Name' in request.headers and 'X-Client-Version' in request.headers: - try: - # Extract information - client_name = request.headers['X-Client-Name'] - client_version = request.headers['X-Client-Version'] - - client_version_parts = client_version.split('.') - - # Load known version from database. - from opentera.utils.TeraVersions import TeraVersions - versions = TeraVersions() - versions.load_from_db() - - # Verify if we have client information in DB - client_info = versions.get_client_version_with_name(client_name) - if client_info: - # We have something stored for this client, let's verify version numbers - # For now, we still allow login even when version mismatch - # Reply full version information - reply['version_latest'] = client_info.to_dict() - if client_info.version != client_version: - reply['version_error'] = gettext('Client version mismatch') - # If major version mismatch, kill client, first part of the version - stored_client_version_parts = client_info.version.split('.') - if len(stored_client_version_parts) and len(client_version_parts): - if stored_client_version_parts[0] != client_version_parts[0]: - # return 426 = upgrade required - self.module.logger.send_login_event(sender=self.module.module_name, - level=messages.LogEvent.LOGLEVEL_ERROR, - login_type=messages.LoginEvent.LOGIN_TYPE_PASSWORD, - login_status= - messages.LoginEvent.LOGIN_STATUS_UNKNOWN, - client_name=login_infos['client_name'], - client_version=login_infos['client_version'], - client_ip=login_infos['client_ip'], - os_name=login_infos['os_name'], - os_version=login_infos['os_version'], - user_uuid=current_user.user_uuid, - server_endpoint=login_infos['server_endpoint'], - message=gettext('Client version mismatch')) - - return gettext('Client major version too old, not accepting login'), 426 - # else: - # return gettext('Invalid client name :') + client_name, 403 - except BaseException as e: - self.module.logger.log_error(self.module.module_name, - UserLogin.__name__, - 'get', 500, 'Invalid client version handler', str(e)) - return gettext('Invalid client version handler') + str(e), 500 - - self.module.logger.send_login_event(sender=self.module.module_name, - level=messages.LogEvent.LOGLEVEL_INFO, - login_type=messages.LoginEvent.LOGIN_TYPE_PASSWORD, - login_status=messages.LoginEvent.LOGIN_STATUS_SUCCESS, - client_name=login_infos['client_name'], - client_version=login_infos['client_version'], - client_ip=login_infos['client_ip'], - os_name=login_infos['os_name'], - os_version=login_infos['os_version'], - user_uuid=current_user.user_uuid, - server_endpoint=login_infos['server_endpoint']) - - return reply + """ + Login to the server using HTTP Basic Authentication + """ + return self._common_login_response(get_parser) + + + @api.doc(description='Login to the server using HTTP Basic Authentication (HTTPAuth)', + security='basicAuth') + @api.expect(post_parser) + @user_http_login_auth.login_required + def post(self): + """ + Login to the server using HTTP Basic Authentication + """ + return self._common_login_response(post_parser) diff --git a/teraserver/python/modules/FlaskModule/API/user/UserLogin2FA.py b/teraserver/python/modules/FlaskModule/API/user/UserLogin2FA.py new file mode 100644 index 000000000..1feae83ce --- /dev/null +++ b/teraserver/python/modules/FlaskModule/API/user/UserLogin2FA.py @@ -0,0 +1,136 @@ +from flask_restx import inputs +from flask_babel import gettext +import pyotp +from modules.LoginModule.LoginModule import LoginModule, current_user +from modules.FlaskModule.FlaskModule import user_api_ns as api +from modules.FlaskModule.API.user.UserLoginBase import UserLoginBase +from modules.FlaskModule.API.user.UserLoginBase import OutdatedClientVersionError, \ + UserAlreadyLoggedInError, TooMany2FALoginAttemptsError +import opentera.messages.python as messages +from opentera.redis.RedisVars import RedisVars + + +# Get parser +get_parser = api.parser() +get_parser.add_argument('otp_code', type=str, required=True, help='2FA otp code') +get_parser.add_argument('with_websocket', type=inputs.boolean, + help='If set, requires that a websocket url is returned.' + 'If not possible to do so, return a 403 error.', + default=False) + +# Post parser +post_parser = api.parser() +post_parser.add_argument('otp_code', type=str, required=True, help='2FA otp code') +post_parser.add_argument('with_websocket', type=inputs.boolean, + help='If set, requires that a websocket url is returned.' + 'If not possible to do so, return a 403 error.', + default=False) + + +class UserLogin2FA(UserLoginBase): + """ + UserLogin2FA endpoint resource. + """ + + def __init__(self, _api, *args, **kwargs): + UserLoginBase.__init__(self, _api, *args, **kwargs) + + # TODO Move this to UserLoginBase ? + def _common_2fa_login_response(self, parser): + try: + # Validate args + args = parser.parse_args(strict=True) + response = {} + + # Current user is logged in with HTTPAuth, or session + # Let's verify if 2FA is enabled and if OTP is valid + if not current_user.user_2fa_enabled: + self._user_logout() + message = gettext('User does not have 2FA enabled') + self._send_login_failure_message(messages.LoginEvent.LOGIN_STATUS_UNKNOWN, message) + return message, 403 + if not current_user.user_2fa_otp_enabled or not current_user.user_2fa_otp_secret: + self._user_logout() + message = gettext('User does not have 2FA OTP enabled or secret set') + self._send_login_failure_message(messages.LoginEvent.LOGIN_STATUS_UNKNOWN, message) + return message, 403 + + # Verify OTP + attempts_key_2fa = RedisVars.RedisVar_User2FALoginAttemptKey + current_user.user_uuid + totp = pyotp.TOTP(current_user.user_2fa_otp_secret) + + # Increment attempts + attempts = self.module.redisGet(attempts_key_2fa) + if attempts: + attempts = int(attempts) + 1 + else: + attempts = 1 + + # Store attempts in the last 15 minutes + self.module.redisSet(attempts_key_2fa, attempts, ex=900) + + if not totp.verify(args['otp_code'], valid_window=1): + self._verify_2fa_login_attempts(current_user.user_uuid) + message = gettext('Invalid OTP code') + self._send_login_failure_message(messages.LoginEvent.LOGIN_STATUS_UNKNOWN, message) + return message, 401 + + # Clear attempts + self.module.redisDelete(attempts_key_2fa) + + # OTP validation completed, proceed with standard login + version_info = self._verify_client_version() + if version_info: + response.update(version_info) + + if args['with_websocket']: + self._verify_user_already_logged_in() + response['websocket_url'] = self._generate_websocket_url() + + # Generate user token + response['user_uuid'] = current_user.user_uuid + response['user_token'] = self._generate_user_token() + + except OutdatedClientVersionError as e: + self._user_logout() + + return { + 'version_latest': e.version_latest, + 'current_version': e.current_version, + 'version_error': e.version_error, + 'message': gettext('Client major version too old, not accepting login')}, 426 +# except InvalidClientVersionError as e: +# # Invalid client version, will not be handled for now +# pass + except UserAlreadyLoggedInError as e: + self._user_logout() + return str(e), 403 + except TooMany2FALoginAttemptsError as e: + self._user_logout() + return str(e), 403 + except Exception as e: + # Something went wrong, logout user + self._user_logout() + raise e + else: + # Everything went well, return response + self._send_login_success_message() + return response, 200 + + @api.doc(description='Login to the server using Session Authentication and 2FA') + @api.expect(get_parser, validate=True) + @LoginModule.user_session_required + def get(self): + """ + Login to the server using Session Authentication and 2FA + """ + return self._common_2fa_login_response(get_parser) + + @api.doc(description='Login to the server using Session Authentication and 2FA') + @api.expect(post_parser, validate=True) + @LoginModule.user_session_required + def post(self): + """ + Login to the server using Session Authentication and 2FA + """ + return self._common_2fa_login_response(post_parser) diff --git a/teraserver/python/modules/FlaskModule/API/user/UserLoginBase.py b/teraserver/python/modules/FlaskModule/API/user/UserLoginBase.py new file mode 100644 index 000000000..3984abf49 --- /dev/null +++ b/teraserver/python/modules/FlaskModule/API/user/UserLoginBase.py @@ -0,0 +1,232 @@ +from flask import session, request +from flask_login import logout_user +from flask_restx import Resource +from flask_babel import gettext +from modules.LoginModule.LoginModule import current_user +from opentera.redis.RedisRPCClient import RedisRPCClient +from opentera.modules.BaseModule import ModuleNames +from opentera.utils.UserAgentParser import UserAgentParser +from opentera.utils.TeraVersions import TeraVersions +import opentera.messages.python as messages +from opentera.redis.RedisVars import RedisVars + + +class OutdatedClientVersionError(Exception): + """ + Raised when the client version is too old. + """ + def __init__(self, message, version_latest=None, current_version=None, version_error=None): + super().__init__(message) + self.version_latest = version_latest + self.current_version = current_version + self.version_error = version_error + + +class InvalidClientVersionError(Exception): + """ + Raised when the client version is invalid. + """ + def __init__(self, message): + super().__init__(message) + +class UserAlreadyLoggedInError(Exception): + """ + Raised when the user is already logged in. + """ + def __init__(self, message): + super().__init__(message) + +class TooMany2FALoginAttemptsError(Exception): + """ + Raised when the user has too many 2FA login attempts. + """ + def __init__(self, message): + super().__init__(message) + + +class UserLoginBase(Resource): + """ + UserLoginBase for all Login resources. + """ + + def __init__(self, _api, *args, **kwargs): + Resource.__init__(self, _api, *args, **kwargs) + self.module = kwargs.get('flaskModule', None) + self.test = kwargs.get('test', False) + if self.module: + self.servername = self.module.config.server_config['hostname'] + self.port = self.module.config.server_config['port'] + else: + self.servername = 'localhost' + self.port = 40075 + + # Setup servername and port from headers if available + # This will happen when the server is behind a reverse proxy + if 'X_EXTERNALSERVER' in request.headers: + self.servername = request.headers['X_EXTERNALSERVER'] + if 'X_EXTERNALPORT' in request.headers: + self.port = request.headers['X_EXTERNALPORT'] + + def _verify_user_already_logged_in(self) -> None: + online_users = [] + if not self.test: + rpc = RedisRPCClient(self.module.config.redis_config) + online_users = rpc.call(ModuleNames.USER_MANAGER_MODULE_NAME.value, 'online_users') + + if current_user.user_uuid in online_users: + user_agent_info = UserAgentParser.parse_request_for_login_infos(request) + self.module.logger.send_login_event(sender=self.module.module_name, + level=messages.LogEvent.LOGLEVEL_ERROR, + login_type=messages.LoginEvent.LOGIN_TYPE_PASSWORD, + login_status= + messages.LoginEvent.LOGIN_STATUS_FAILED_WITH_ALREADY_LOGGED_IN, + client_name=user_agent_info['client_name'], + client_version=user_agent_info['client_version'], + client_ip=user_agent_info['client_ip'], + os_name=user_agent_info['os_name'], + os_version=user_agent_info['os_version'], + user_uuid=current_user.user_uuid, + server_endpoint=user_agent_info['server_endpoint'], + message=gettext('User already logged in :' + + current_user.user_username)) + raise UserAlreadyLoggedInError(gettext('User already logged in.')) + + + def _verify_2fa_login_attempts(self, user_uuid: str) -> None: + attempts_key_2fa = RedisVars.RedisVar_User2FALoginAttemptKey + user_uuid + attempts = self.module.redisGet(attempts_key_2fa) + if attempts is not None: + attempts = int(attempts) + if attempts >= 5: + message = gettext('Too many 2FA attempts. Please wait and try again.') + self._send_login_failure_message( + messages.LoginEvent.LOGIN_STATUS_FAILED_WITH_MAX_ATTEMPTS_REACHED, message) + raise TooMany2FALoginAttemptsError(message) + + def _verify_client_version(self) -> dict | None: + reply = {} + + # Extract login information + user_agent_info = UserAgentParser.parse_request_for_login_infos(request) + + # Extract information + if 'X-Client-Name' not in request.headers or 'X-Client-Version' not in request.headers: + # raise InvalidClientVersionError(gettext('Client information missing')) + return None + + client_name = request.headers['X-Client-Name'] + client_version = request.headers['X-Client-Version'] + + client_version_parts = client_version.split('.') + + # Load known version from database. + versions = TeraVersions() + versions.load_from_db() + + # Verify if we have client information in DB + client_info = versions.get_client_version_with_name(client_name) + if client_info: + # We have something stored for this client, let's verify version numbers + # For now, we still allow login even when version mismatch + # Reply full version information + reply = {'version_latest': client_info.to_dict()} + if client_info.version != client_version: + reply['version_error'] = gettext('Client major version mismatch') + # If major version mismatch, kill client, first part of the version + stored_client_version_parts = client_info.version.split('.') + if len(stored_client_version_parts) and len(client_version_parts): + if stored_client_version_parts[0] != client_version_parts[0]: + self.module.logger.send_login_event(sender=self.module.module_name, + level=messages.LogEvent.LOGLEVEL_ERROR, + login_type=messages.LoginEvent.LOGIN_TYPE_PASSWORD, + login_status= + messages.LoginEvent.LOGIN_STATUS_UNKNOWN, + client_name=user_agent_info['client_name'], + client_version=user_agent_info['client_version'], + client_ip=user_agent_info['client_ip'], + os_name=user_agent_info['os_name'], + os_version=user_agent_info['os_version'], + user_uuid=current_user.user_uuid, + server_endpoint=user_agent_info['server_endpoint'], + message=gettext('Client version mismatch')) + + raise OutdatedClientVersionError( + gettext('Client major version too old, not accepting login'), + version_latest=reply['version_latest'], + current_version=client_version_parts, + version_error=reply['version_error']) + else: + self.module.logger.send_login_event(sender=self.module.module_name, + level=messages.LogEvent.LOGLEVEL_ERROR, + login_type=messages.LoginEvent.LOGIN_TYPE_PASSWORD, + login_status= + messages.LoginEvent.LOGIN_STATUS_UNKNOWN, + client_name=user_agent_info['client_name'], + client_version=user_agent_info['client_version'], + client_ip=user_agent_info['client_ip'], + os_name=user_agent_info['os_name'], + os_version=user_agent_info['os_version'], + user_uuid=current_user.user_uuid, + server_endpoint=user_agent_info['server_endpoint'], + message=gettext('Unknown client name :') + client_name) + # For now, simply log the error, this will allow unknown clients to login + # raise InvalidClientVersionError(gettext('Invalid client name :') + client_name) + return reply + + def _generate_websocket_url(self) -> str: + websocket_url = f"wss://{self.servername}:{str(self.port)}/wss/user?id={session['_id']}" + # The key is set with an expiration of 60s, will be verified when + # the websocket is opened in the TwistedModule + self.module.redisSet(session['_id'], session['_user_id'], ex=60) + return websocket_url + + def _generate_user_token(self) -> str: + token_key = self.module.redisGet(RedisVars.RedisVar_UserTokenAPIKey) + return current_user.get_token(token_key) + + def _generate_2fa_verification_url(self) -> str: + return "/login_validate_2fa" + + def _generate_2fa_setup_url(self) -> str: + return "/login_setup_2fa" + + def _generate_login_url(self) -> str: + return "/login" + + def _generate_password_change_url(self) -> str: + return "/login_change_password" + + def _user_logout(self): + logout_user() + session.clear() + + def _send_login_success_message(self, message: str = ''): + user_agent_info = UserAgentParser.parse_request_for_login_infos(request) + self.module.logger.send_login_event(sender=self.module.module_name, + level=messages.LogEvent.LOGLEVEL_INFO, + login_type=messages.LoginEvent.LOGIN_TYPE_PASSWORD, + login_status=messages.LoginEvent.LOGIN_STATUS_SUCCESS, + client_name=user_agent_info['client_name'], + client_version=user_agent_info['client_version'], + client_ip=user_agent_info['client_ip'], + os_name=user_agent_info['os_name'], + os_version=user_agent_info['os_version'], + user_uuid=current_user.user_uuid, + server_endpoint=user_agent_info['server_endpoint'], + message=message) + + def _send_login_failure_message(self, + status: messages.LoginEvent.LoginStatus, message:str = ''): + user_agent_info = UserAgentParser.parse_request_for_login_infos(request) + self.module.logger.send_login_event(sender=self.module.module_name, + level=messages.LogEvent.LOGLEVEL_ERROR, + login_type=messages.LoginEvent.LOGIN_TYPE_PASSWORD, + login_status=status, + client_name=user_agent_info['client_name'], + client_version=user_agent_info['client_version'], + client_ip=user_agent_info['client_ip'], + os_name=user_agent_info['os_name'], + os_version=user_agent_info['os_version'], + user_uuid=current_user.user_uuid, + server_endpoint=user_agent_info['server_endpoint'], + message=message) diff --git a/teraserver/python/modules/FlaskModule/API/user/UserLoginChangePassword.py b/teraserver/python/modules/FlaskModule/API/user/UserLoginChangePassword.py new file mode 100644 index 000000000..c4b58e879 --- /dev/null +++ b/teraserver/python/modules/FlaskModule/API/user/UserLoginChangePassword.py @@ -0,0 +1,53 @@ +from modules.FlaskModule.API.user.UserLoginBase import UserLoginBase +from modules.FlaskModule.FlaskModule import user_api_ns as api +from modules.LoginModule.LoginModule import LoginModule, current_user +from opentera.db.models.TeraUser import TeraUser, UserPasswordInsecure, UserNewPasswordSameAsOld +from modules.FlaskModule.FlaskUtils import FlaskUtils + +from flask_babel import gettext +from flask import redirect + +post_parser = api.parser() +post_parser.add_argument('new_password', type=str, required=True, help='New password for the user') +post_parser.add_argument('confirm_password', type=str, required=True, help='Password confirmation for the user') + +class UserLoginChangePassword(UserLoginBase): + """ + UserLoginChangePassword endpoint resource. + """ + + @api.doc(description='Change password for the user. This API will only work if forced change is required on login. ' + 'Otherwise, use the standard \'api/user\' endpoint.') + @api.expect(post_parser, validate=True) + @LoginModule.user_session_required + def post(self): + """ + Change password for a user on login (forced change) + """ + try: + args = post_parser.parse_args(strict=True) + new_password = args['new_password'] + confirm_password = args['confirm_password'] + + # Validate if new password and confirm password are the same + if new_password != confirm_password: + return gettext('New password and confirm password do not match'), 400 + + if not current_user.user_force_password_change: + return gettext('User not required to change password'), 400 + + # Change password, will be encrypted + # Will also reset force password change flag + try: + TeraUser.update(current_user.id_user, {'user_password': new_password, + 'user_force_password_change': False}) + except UserPasswordInsecure as e: + return FlaskUtils.get_password_weaknesses_text(e.weaknesses, '
'), 400 + except UserNewPasswordSameAsOld: + return gettext('New password same as old password'), 400 + + return 200 + except Exception as e: + # Something went wrong, logout user + self._user_logout() + raise e diff --git a/teraserver/python/modules/FlaskModule/API/user/UserLoginSetup2FA.py b/teraserver/python/modules/FlaskModule/API/user/UserLoginSetup2FA.py new file mode 100644 index 000000000..133caa6f8 --- /dev/null +++ b/teraserver/python/modules/FlaskModule/API/user/UserLoginSetup2FA.py @@ -0,0 +1,171 @@ +from flask_restx import inputs +from flask_babel import gettext +import pyotp +import pyqrcode +from modules.LoginModule.LoginModule import LoginModule, current_user +from modules.FlaskModule.FlaskModule import user_api_ns as api +from modules.FlaskModule.API.user.UserLoginBase import UserLoginBase +from modules.FlaskModule.API.user.UserLoginBase import OutdatedClientVersionError, \ + UserAlreadyLoggedInError, TooMany2FALoginAttemptsError +from opentera.db.models.TeraUser import TeraUser +import opentera.messages.python as messages + + +# Get parser +get_parser = api.parser() + +# Post parser +post_parser = api.parser() +post_parser.add_argument('otp_secret', type=str, required=True, help='OTP Secret for the user.') +post_parser.add_argument('with_email_enabled', type=inputs.boolean, + help='Enable email notifications for 2FA', default=False) +post_parser.add_argument('otp_code', type=str, required=True, help='OTP code for validation on setup') + + +class UserLoginSetup2FA(UserLoginBase): + """ + UserLogin2FA endpoint resource. + """ + + def __init__(self, _api, *args, **kwargs): + UserLoginBase.__init__(self, _api, *args, **kwargs) + + @api.doc(description='Generate a new 2FA secret and QR Code for the user') + @api.expect(get_parser, validate=True) + @LoginModule.user_session_required + def get(self): + """ + Generate a new 2FA secret for the user. Will be enabled on post. + """ + try: + # Validate args (should not have any) + get_parser.parse_args(strict=True) + response = {} + + # Current user is logged in with HTTPAuth, or session + # Let's verify if 2FA is enabled and if OTP is valid + if not current_user.user_2fa_enabled: + self._user_logout() + return gettext('User does not have 2FA enabled'), 403 + + if current_user.user_2fa_otp_secret: + self._user_logout() + return gettext('User already has 2FA OTP secret set'), 403 + + # Verify if user has tried too many times to login with 2FA + # This should not happen here, but just in case + self._verify_2fa_login_attempts(current_user.user_uuid) + + # Generate new secret + secret = pyotp.random_base32() + + # Generate OTP URI for QR Code + totp = pyotp.TOTP(secret) + + # Get the server name in the config + server_name = self.module.config.server_config['name'] + otp_uri = totp.provisioning_uri(current_user.user_username, + issuer_name=f'OpenTera-{server_name}') + + # Generate QR Code with otp_uri + qr_code = pyqrcode.create(otp_uri) + + # Generate image as base64 + qr_code_base64 = qr_code.png_as_base64_str(scale=5) + + response['qr_code'] = qr_code_base64 + response['otp_secret'] = secret + + except OutdatedClientVersionError as e: + self._user_logout() + return { + 'version_latest': e.version_latest, + 'current_version': e.current_version, + 'version_error': e.version_error, + 'message': gettext('Client major version too old, not accepting login')}, 426 +# except InvalidClientVersionError as e: +# # Invalid client version, will not be handled for now +# pass + except UserAlreadyLoggedInError as e: + self._user_logout() + return str(e), 403 + except TooMany2FALoginAttemptsError as e: + self._user_logout() + return str(e), 403 + except Exception as e: + # Something went wrong, logout user + self._user_logout() + raise e + else: + # Everything went well, return response + return response, 200 + + + @api.doc(description='Enable 2FA for the user') + @api.expect(post_parser, validate=True) + @LoginModule.user_session_required + def post(self): + """ + Enable 2FA for the user. Will use the OTP secret generated in the GET method. + """ + try: + args = post_parser.parse_args(strict=True) + response = {} + + # Current user is logged in with HTTPAuth, or session + # Let's verify if 2FA is enabled and if OTP is valid + if not current_user.user_2fa_enabled: + self._user_logout() + return gettext('User does not have 2FA enabled'), 403 + + if current_user.user_2fa_otp_secret: + self._user_logout() + return gettext('User already has 2FA OTP secret set'), 403 + + # Verify if user has tried too many times to login with 2FA + # This should not happen here, but just in case + self._verify_2fa_login_attempts(current_user.user_uuid) + + # Verify OTP code if present + if args['otp_code']: + totp = pyotp.TOTP(args['otp_secret']) + if not totp.verify(args['otp_code'], valid_window=1): + message = gettext('Invalid OTP code') + self._send_login_failure_message(messages.LoginEvent.LOGIN_STATUS_UNKNOWN, message) + return message, 401 + + data = {'user_2fa_enabled': True, + 'user_2fa_otp_enabled': True, + 'user_2fa_otp_secret': args['otp_secret'], + 'user_2fa_email_enabled': args['with_email_enabled']} + + # Save user to db + TeraUser.update(current_user.id_user, data) + + # Redirect to 2FA validation page + response['message'] = gettext('2FA enabled for this user.') + response['redirect_url'] = self._generate_2fa_verification_url() + "?code=" + args['otp_code'] + + except OutdatedClientVersionError as e: + self._user_logout() + return { + 'version_latest': e.version_latest, + 'current_version': e.current_version, + 'version_error': e.version_error, + 'message': gettext('Client major version too old, not accepting login')}, 426 +# except InvalidClientVersionError as e: +# # Invalid client version, will not be handled for now +# pass + except UserAlreadyLoggedInError as e: + self._user_logout() + return str(e), 403 + except TooMany2FALoginAttemptsError as e: + self._user_logout() + return str(e), 403 + except Exception as e: + # Something went wrong, logout user + self._user_logout() + raise e + else: + # Everything went well, return response + return response, 200 diff --git a/teraserver/python/modules/FlaskModule/API/user/UserLogout.py b/teraserver/python/modules/FlaskModule/API/user/UserLogout.py index 8ffaad438..ae2a8279f 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserLogout.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserLogout.py @@ -1,5 +1,5 @@ from flask_login import logout_user -from flask_restx import Resource, reqparse +from flask_restx import Resource from flask_babel import gettext from flask import session, request from modules.FlaskModule.FlaskModule import user_api_ns as api @@ -15,12 +15,14 @@ def __init__(self, _api, *args, **kwargs): self.module = kwargs.get('flaskModule', None) self.test = kwargs.get('test', False) - @api.doc(description='Logout from the server', params={'token': 'Secret token'}) + @api.doc(description='Logout from the server') @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Logout the user + """ if current_user: - print('logout user') logout_user() session.clear() self.module.send_user_disconnect_module_message(current_user.user_uuid) diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryAssets.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryAssets.py index 8d980e611..d144c2c84 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryAssets.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryAssets.py @@ -1,9 +1,8 @@ -from flask import session, request +from flask import request from flask_restx import Resource, inputs from flask_babel import gettext from modules.LoginModule.LoginModule import user_multi_auth, current_user from modules.FlaskModule.FlaskModule import user_api_ns as api -from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraAsset import TeraAsset from opentera.db.models.TeraService import TeraService @@ -46,11 +45,13 @@ def __init__(self, _api, *args, **kwargs): @api.doc(description='Get asset information. Only one of the ID parameter is supported at once', responses={200: 'Success - returns list of assets', 400: 'Required parameter is missing', - 403: 'Logged user doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + 403: 'Logged user doesn\'t have permission to access the requested data'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get asset information + """ user_access = DBManager.userAccess(current_user) args = get_parser.parse_args() @@ -166,18 +167,22 @@ def get(self): # else: return assets_list - @api.doc(description='Delete asset.', - responses={501: 'Unable to update asset information from here'}, - params={'token': 'Secret token'}) + @api.doc(description='Update asset information', + responses={501: 'Unable to update asset information from here'}) @user_multi_auth.login_required def post(self): + """ + Update asset information + """ return gettext('Asset information update and creation must be done directly into a service (such as ' 'Filetransfer service)'), 501 - @api.doc(description='Delete asset.', - responses={501: 'Unable to delete asset information from here'}, - params={'token': 'Secret token'}) + @api.doc(description='Delete asset information', + responses={501: 'Unable to delete asset information from here'}) @user_multi_auth.login_required def delete(self): + """ + Delete asset information + """ return gettext('Asset information deletion must be done directly into a service (such as ' 'Filetransfer service)'), 501 diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryAssetsArchive.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryAssetsArchive.py index 2997c022b..866a27d06 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryAssetsArchive.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryAssetsArchive.py @@ -4,17 +4,14 @@ import json import datetime import threading -import zipfile -from io import BytesIO -from flask import session, request, Response -from flask_restx import Resource, inputs +from flask import request +from flask_restx import Resource from flask_babel import gettext from modules.LoginModule.LoginModule import user_multi_auth, current_user from modules.FlaskModule.FlaskModule import user_api_ns as api from modules.FlaskModule import FlaskModule -from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraAsset import TeraAsset from opentera.db.models.TeraService import TeraService from opentera.db.models.TeraProject import TeraProject @@ -46,11 +43,13 @@ def __init__(self, _api, *args, **kwargs): @api.doc(description='Get asset archive. Only one of the ID parameter is supported at once.', responses={200: 'Success - returns list of assets', 400: 'Required parameter is missing', - 403: 'Logged user doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + 403: 'Logged user doesn\'t have permission to access the requested data'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get asset archive + """ # Get parameters args = get_parser.parse_args() user_access = DBManager.userAccess(current_user) diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryDeviceParticipants.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryDeviceParticipants.py index 92855b9cc..12ff91980 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryDeviceParticipants.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryDeviceParticipants.py @@ -1,8 +1,7 @@ -from flask import jsonify, session, request +from flask import jsonify, request from flask_restx import Resource, reqparse, fields, inputs from modules.LoginModule.LoginModule import user_multi_auth, current_user from modules.FlaskModule.FlaskModule import user_api_ns as api -from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraDeviceParticipant import TeraDeviceParticipant from opentera.db.models.TeraParticipant import TeraParticipant from opentera.db.models.TeraDeviceProject import TeraDeviceProject @@ -55,11 +54,13 @@ def __init__(self, _api, *args, **kwargs): ' at once.', responses={200: 'Success - returns list of devices - participants association', 400: 'Required parameter is missing (must have at least one id)', - 500: 'Error occurred when loading devices for participant'}, - params={'token': 'Secret token'}) + 500: 'Error occurred when loading devices for participant'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get devices assigned to a participant + """ user_access = DBManager.userAccess(current_user) args = get_parser.parse_args() @@ -107,11 +108,13 @@ def get(self): responses={200: 'Success', 403: 'Logged user can\'t modify device association', 400: 'Badly formed JSON or missing fields(id_participant or id_device) in the JSON body', - 500: 'Internal error occured when saving device association'}, - params={'token': 'Secret token'}) + 500: 'Internal error occured when saving device association'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): + """ + Assign / remove devices from a participant + """ user_access = DBManager.userAccess(current_user) # Using request.json instead of parser, since parser messes up the json! @@ -187,11 +190,13 @@ def post(self): @api.doc(description='Delete a specific device-participant association.', responses={200: 'Success', 403: 'Logged user can\'t delete device association', - 500: 'Device-participant association not found or database error.'}, - params={'token': 'Secret token'}) + 500: 'Device-participant association not found or database error.'}) @api.expect(delete_parser) @user_multi_auth.login_required def delete(self): + """ + Remove a specific device - participant association + """ user_access = DBManager.userAccess(current_user) args = delete_parser.parse_args() diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryDeviceProjects.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryDeviceProjects.py index 72ca569a8..7e88a0504 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryDeviceProjects.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryDeviceProjects.py @@ -1,8 +1,7 @@ -from flask import jsonify, session, request -from flask_restx import Resource, reqparse, inputs +from flask import request +from flask_restx import Resource, inputs from modules.LoginModule.LoginModule import user_multi_auth, current_user from modules.FlaskModule.FlaskModule import user_api_ns as api -from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraDeviceProject import TeraDeviceProject from opentera.db.models.TeraDeviceSite import TeraDeviceSite from opentera.db.models.TeraDevice import TeraDevice @@ -51,11 +50,13 @@ def __init__(self, _api, *args, **kwargs): 'supported at once.', responses={200: 'Success - returns list of devices - project association', 400: 'Required parameter is missing (must have at least one id)', - 500: 'Error occurred when loading devices for projects'}, - params={'token': 'Secret token'}) + 500: 'Error occurred when loading devices for projects'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get devices associated with a project + """ user_access = DBManager.userAccess(current_user) args = get_parser.parse_args() @@ -127,11 +128,13 @@ def get(self): 403: 'Logged user can\'t modify device association - the user isn\'t admin ' 'of the project or current user can\'t access the device.', 400: 'Badly formed JSON or missing fields(id_project or id_device) in the JSON body', - 500: 'Internal error occured when saving device association'}, - params={'token': 'Secret token'}) + 500: 'Internal error occured when saving device association'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): + """ + Create/update devices associated with a project + """ user_access = DBManager.userAccess(current_user) # Using request.json instead of parser, since parser messes up the json! @@ -281,11 +284,13 @@ def post(self): responses={200: 'Success', 403: 'Logged user can\'t delete device association (no admin access to project or one of the ' 'device\'s site)', - 500: 'Device-project association not found or database error.'}, - params={'token': 'Secret token'}) + 500: 'Device-project association not found or database error.'}) @api.expect(delete_parser) @user_multi_auth.login_required def delete(self): + """ + Delete a specific device-project association + """ user_access = DBManager.userAccess(current_user) args = delete_parser.parse_args() id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryDeviceSites.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryDeviceSites.py index 4bb04dfc9..7c5b37d9b 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryDeviceSites.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryDeviceSites.py @@ -1,8 +1,7 @@ -from flask import jsonify, session, request +from flask import request from flask_restx import Resource, reqparse, inputs from modules.LoginModule.LoginModule import user_multi_auth, current_user from modules.FlaskModule.FlaskModule import user_api_ns as api -from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraDevice import TeraDevice from opentera.db.models.TeraSite import TeraSite from opentera.db.models.TeraDeviceSite import TeraDeviceSite @@ -47,11 +46,13 @@ def __init__(self, _api, *args, **kwargs): ' at once.', responses={200: 'Success - returns list of devices - sites association', 400: 'Required parameter is missing (must have at least one id)', - 500: 'Error occurred when loading devices for sites'}, - params={'token': 'Secret token'}) + 500: 'Error occurred when loading devices for sites'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get devices associated to a site + """ user_access = DBManager.userAccess(current_user) args = get_parser.parse_args() @@ -112,11 +113,13 @@ def get(self): responses={200: 'Success', 403: 'Logged user can\'t modify device association', 400: 'Badly formed JSON or missing fields(id_site or id_device) in the JSON body', - 500: 'Internal error occured when saving device association'}, - params={'token': 'Secret token'}) + 500: 'Internal error occured when saving device association'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): + """ + Create/update devices association with a site + """ # Only super admins can change service - site associations if not current_user.user_superadmin: return gettext('Forbidden'), 403 @@ -236,11 +239,13 @@ def post(self): @api.doc(description='Delete a specific device-site association.', responses={200: 'Success', 403: 'Logged user can\'t delete device association (no admin access to site)', - 500: 'Device-site association not found or database error.'}, - params={'token': 'Secret token'}) + 500: 'Device-site association not found or database error.'}) @api.expect(delete_parser) @user_multi_auth.login_required def delete(self): + """ + Delete a specific device-site association + """ user_access = DBManager.userAccess(current_user) args = delete_parser.parse_args() diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py index 03463e129..d77a31156 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py @@ -1,9 +1,8 @@ -from flask import session, request +from flask import request from flask_restx import Resource, reqparse, inputs from flask_babel import gettext from modules.LoginModule.LoginModule import user_multi_auth, current_user from modules.FlaskModule.FlaskModule import user_api_ns as api -from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraDeviceSubType import TeraDeviceSubType from sqlalchemy.exc import InvalidRequestError from sqlalchemy import exc @@ -36,11 +35,13 @@ def __init__(self, _api, *args, **kwargs): 400: 'No parameters specified at least one id must be used', 403: 'Forbidden access to the device type specified. Please check that the user has access to a' ' session type containing that device type.', - 500: 'Database error'}, - params={'token': 'Secret token'}) + 500: 'Database error'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get devices subtypes + """ user_access = DBManager.userAccess(current_user) args = get_parser.parse_args() has_list = args.pop('list') @@ -92,11 +93,13 @@ def get(self): 403: 'Logged user can\'t create/update the specified device subtype', 400: 'Badly formed JSON or missing fields(id_device_subtype or id_device_type) in the JSON ' 'body', - 500: 'Internal error occured when saving device subtype'}, - params={'token': 'Secret token'}) + 500: 'Internal error occured when saving device subtype'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): + """ + Create / update devices subtypes + """ user_access = DBManager.userAccess(current_user) if 'device_subtype' not in request.json: return gettext('Missing device_subtype'), 400 @@ -151,11 +154,13 @@ def post(self): @api.doc(description='Delete a specific device subtype', responses={200: 'Success', 403: 'Logged user can\'t delete device subtype (can delete if site admin)', - 500: 'Device subtype not found or database error.'}, - params={'token': 'Secret token'}) + 500: 'Device subtype not found or database error.'}) @api.expect(delete_parser) @user_multi_auth.login_required def delete(self): + """ + Delete a specific device subtype + """ user_access = DBManager.userAccess(current_user) args = delete_parser.parse_args() id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryDeviceTypes.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryDeviceTypes.py index 6c439a5d6..a075bf032 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryDeviceTypes.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryDeviceTypes.py @@ -1,12 +1,10 @@ -from flask import session, request +from flask import request from flask_babel import gettext from flask_restx import Resource, reqparse, inputs from sqlalchemy import exc from sqlalchemy.exc import InvalidRequestError -from opentera.db.models import TeraDeviceType from opentera.db.models.TeraDeviceType import TeraDeviceType -from opentera.db.models.TeraUser import TeraUser from modules.DatabaseModule.DBManager import DBManager from modules.FlaskModule.FlaskModule import user_api_ns as api from modules.LoginModule.LoginModule import user_multi_auth, current_user @@ -40,11 +38,13 @@ def __init__(self, _api, *args, **kwargs): 400: 'No parameters specified at least one id must be used', 403: 'Forbidden access to the device type specified. Please check that the user has access to a' ' session type containing that device type.', - 500: 'Database error'}, - params={'token': 'Secret token'}) + 500: 'Database error'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get devices types + """ user_access = DBManager.userAccess(current_user) args = get_parser.parse_args() device_type = [] @@ -84,12 +84,14 @@ def get(self): 403: 'Logged user can\'t create/update the specified device type', 400: 'Badly formed JSON or missing fields(id_device_name or id_device_type) in the JSON ' 'body', - 500: 'Internal error occured when saving device type'}, - params={'token': 'Secret token'}) + 500: 'Internal error occured when saving device type'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): - user_access = DBManager.userAccess(current_user) + """ + Create / update devices types + """ + # user_access = DBManager.userAccess(current_user) if 'device_type' not in request.json: return gettext('Missing device type'), 400 @@ -144,11 +146,13 @@ def post(self): responses={200: 'Success', 403: 'Logged user can\'t delete device type (can delete if site admin)', 500: 'Device type not found or database error.', - 501: 'Tried to delete with 2 parameters'}, - params={'token': 'Secret token'}) + 501: 'Tried to delete with 2 parameters'}) @api.expect(delete_parser) @user_multi_auth.login_required def delete(self): + """ + Delete a specific device type + """ user_access = DBManager.userAccess(current_user) args = delete_parser.parse_args() # To accommodate the 'delete_with_http_auth' function which uses id as args, the id_device_type is rename as id diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryDevices.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryDevices.py index 740685e9a..d0d4cf583 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryDevices.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryDevices.py @@ -1,8 +1,7 @@ -from flask import jsonify, session, request +from flask import jsonify, request from flask_restx import Resource, reqparse, inputs from modules.LoginModule.LoginModule import user_multi_auth, current_user from modules.FlaskModule.FlaskModule import user_api_ns as api -from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraDevice import TeraDevice from opentera.db.models.TeraProject import TeraProject from opentera.db.models.TeraSite import TeraSite @@ -75,11 +74,13 @@ def __init__(self, _api, *args, **kwargs): responses={200: 'Success - returns list of devices', 400: 'User Error : Too Many IDs', 403: 'Forbidden access', - 500: 'Database error'}, - params={'token': 'Secret token'}) + 500: 'Database error'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get device information + """ user_access = DBManager.userAccess(current_user) args = get_parser.parse_args() @@ -242,11 +243,13 @@ def get(self): responses={200: 'Success', 403: 'Logged user can\'t create/update the specified device', 400: 'Badly formed JSON or missing fields(id_device) in the JSON body', - 500: 'Internal error occurred when saving device'}, - params={'token': 'Secret token'}) + 500: 'Internal error occurred when saving device'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): + """ + Create / update devices + """ user_access = DBManager.userAccess(current_user) # Using request.json instead of parser, since parser messes up the json! if 'device' not in request.json: @@ -395,11 +398,13 @@ def post(self): responses={200: 'Success', 400: 'Wrong ID/ No ID', 403: 'Logged user can\'t delete device (can delete if superadmin)', - 500: 'Device not found or database error.'}, - params={'token': 'Secret token'}) + 500: 'Device not found or database error.'}) @api.expect(delete_parser) @user_multi_auth.login_required def delete(self): + """ + Delete a specific device + """ user_access = DBManager.userAccess(current_user) args = delete_parser.parse_args() id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryDisconnect.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryDisconnect.py index b3a1e9a72..4a30a210b 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryDisconnect.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryDisconnect.py @@ -1,5 +1,4 @@ -from flask import jsonify, session, request -from flask_restx import Resource, reqparse, inputs +from flask_restx import Resource from modules.FlaskModule.FlaskModule import user_api_ns as api from modules.LoginModule.LoginModule import user_multi_auth, current_user from opentera.db.models.TeraUser import TeraUser @@ -33,11 +32,13 @@ def __init__(self, _api, *args, **kwargs): 400: 'No parameters specified, at least one id / uuid must be used', 403: 'Forbidden access. Please check that the user has access to' ' the requested id/uuid.', - 500: 'Database error'}, - params={'token': 'Secret token'}) + 500: 'Database error'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Force disconnect a specific user / participant / device from server + """ args = get_parser.parse_args() user_access: DBManagerTeraUserAccess = DBManager.userAccess(current_user) diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryForms.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryForms.py index 5e21fa09d..0b91c3fdf 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryForms.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryForms.py @@ -1,9 +1,6 @@ -from flask import jsonify, session -from flask_restx import Resource, reqparse from modules.LoginModule.LoginModule import user_multi_auth, current_user from modules.FlaskModule.FlaskModule import user_api_ns as api from modules.DatabaseModule.DBManager import DBManager -from flask_babel import gettext from opentera.db.models.TeraSessionType import TeraSessionType from opentera.db.models.TeraParticipantGroup import TeraParticipantGroup @@ -27,7 +24,11 @@ from opentera.forms.TeraTestTypeForm import TeraTestTypeForm from opentera.redis.RedisRPCClient import RedisRPCClient + import json +from flask_babel import gettext +from flask_restx import Resource +from flask import request get_parser = api.parser() get_parser.add_argument(name='type', type=str, help='Data type of the required form. Currently, the ' @@ -66,17 +67,29 @@ def __init__(self, _api, *args, **kwargs): @api.doc(description='Get json description of standard input form for the specified data type.', responses={200: 'Success', 400: 'Missing required parameter', - 500: 'Unknown or unsupported data type'}, - params={'token': 'Secret token'}) + 500: 'Unknown or unsupported data type'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get json description of form to display to edit a specific data type + """ args = get_parser.parse_args() user_access = DBManager.userAccess(current_user) - # if args['type'] == 'user_profile': - # return jsonify(TeraUserForm.get_user_profile_form()) # If we have no arguments, return error + show_2fa_fields = True + if 'X-Client-Name' in request.headers and 'X-Client-Version' in request.headers: + client_name = request.headers['X-Client-Name'] + client_version = request.headers['X-Client-Version'] + client_version_parts = client_version.split('.') + if client_name == 'OpenTeraPlus': + # TODO: Remove when all OpenTeraPlus clients are updated at least to 1.3.0 version + if len(client_version_parts) >= 2: + if int(client_version_parts[1]) < 3 and int(client_version_parts[0]) == 1: + show_2fa_fields = False + else: + show_2fa_fields = False if 'type' not in args: return gettext('Missing type'), 400 @@ -85,10 +98,10 @@ def get(self): return gettext('Missing arguments'), 400 if args['type'] == 'user': - return TeraUserForm.get_user_form() + return TeraUserForm.get_user_form(show_2fa_fields=show_2fa_fields) if args['type'] == 'site': - return TeraSiteForm.get_site_form() + return TeraSiteForm.get_site_form(show_2fa_fields=show_2fa_fields) if args['type'] == 'device': return TeraDeviceForm.get_device_form() diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryOnlineDevices.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryOnlineDevices.py index ec7ff8eb5..a0daefb47 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryOnlineDevices.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryOnlineDevices.py @@ -1,10 +1,8 @@ -from flask import session from flask_restx import Resource from flask_babel import gettext from modules.LoginModule.LoginModule import user_multi_auth, current_user from modules.FlaskModule.FlaskModule import user_api_ns as api from sqlalchemy.exc import InvalidRequestError -from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraDevice import TeraDevice from opentera.redis.RedisRPCClient import RedisRPCClient from opentera.modules.BaseModule import ModuleNames @@ -20,12 +18,14 @@ def __init__(self, _api, *args, **kwargs): self.test = kwargs.get('test', False) @api.doc(description='Get online devices uuids.', - responses={200: 'Success'}, - params={'token': 'Secret token'}) + responses={200: 'Success'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): - args = get_parser.parse_args() + """ + Get online devices + """ + # args = get_parser.parse_args() user_access = DBManager.userAccess(current_user) try: diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryOnlineParticipants.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryOnlineParticipants.py index c645852e9..9c8bca756 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryOnlineParticipants.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryOnlineParticipants.py @@ -1,10 +1,8 @@ -from flask import session from flask_restx import Resource, inputs from flask_babel import gettext from modules.LoginModule.LoginModule import user_multi_auth, current_user from modules.FlaskModule.FlaskModule import user_api_ns as api from sqlalchemy.exc import InvalidRequestError -from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraParticipant import TeraParticipant from opentera.redis.RedisRPCClient import RedisRPCClient from opentera.modules.BaseModule import ModuleNames @@ -27,6 +25,9 @@ def __init__(self, _api, *args, **kwargs): @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get online participants + """ args = get_parser.parse_args() user_access = DBManager.userAccess(current_user) diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryOnlineUsers.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryOnlineUsers.py index e49c9c097..0286a95b6 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryOnlineUsers.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryOnlineUsers.py @@ -1,4 +1,3 @@ -from flask import session from flask_restx import Resource from flask_babel import gettext from modules.LoginModule.LoginModule import user_multi_auth, current_user @@ -24,7 +23,10 @@ def __init__(self, _api, *args, **kwargs): @api.expect(get_parser) @user_multi_auth.login_required def get(self): - args = get_parser.parse_args() + """ + Get online users + """ + # args = get_parser.parse_args() user_access = DBManager.userAccess(current_user) try: diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryParticipantGroup.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryParticipantGroup.py index 63909a3fd..be3542f69 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryParticipantGroup.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryParticipantGroup.py @@ -1,8 +1,7 @@ -from flask import jsonify, session, request +from flask import jsonify, request from flask_restx import Resource, reqparse, inputs from modules.LoginModule.LoginModule import user_multi_auth, current_user from modules.FlaskModule.FlaskModule import user_api_ns as api -from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraParticipantGroup import TeraParticipantGroup from modules.DatabaseModule.DBManager import DBManager from sqlalchemy.exc import InvalidRequestError @@ -36,11 +35,13 @@ def __init__(self, _api, *args, **kwargs): @api.doc(description='Get participant groups information. Only one of the ID parameter is supported at once. ' 'If no ID is specified, returns all accessible groups for the logged user', responses={200: 'Success - returns list of participant groups', - 500: 'Database error'}, - params={'token': 'Secret token'}) + 500: 'Database error'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get participant groups + """ user_access = DBManager.userAccess(current_user) args = get_parser.parse_args() @@ -82,11 +83,13 @@ def get(self): responses={200: 'Success', 403: 'Logged user can\'t create/update the specified device', 400: 'Badly formed JSON or missing fields(id_participant_group or id_project) in the JSON body', - 500: 'Internal error occurred when saving device'}, - params={'token': 'Secret token'}) + 500: 'Internal error occurred when saving device'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): + """ + Create / update participant groups + """ user_access = DBManager.userAccess(current_user) # Using request.json instead of parser, since parser messes up the json! if 'group' not in request.json and 'participant_group' not in request.json: @@ -141,11 +144,13 @@ def post(self): @api.doc(description='Delete a specific participant group', responses={200: 'Success', 403: 'Logged user can\'t delete participant group (only project admin can delete)', - 500: 'Database error.'}, - params={'token': 'Secret token'}) + 500: 'Database error.'}) @api.expect(delete_parser) @user_multi_auth.login_required def delete(self): + """ + Delete a participant group + """ user_access = DBManager.userAccess(current_user) args = delete_parser.parse_args() diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryParticipants.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryParticipants.py index 04e556b29..683d49384 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryParticipants.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryParticipants.py @@ -57,11 +57,13 @@ def __init__(self, _api, *args, **kwargs): @api.doc(description='Get participants information. Only one of the ID parameter is supported and required at once', responses={200: 'Success - returns list of participants', 400: 'No parameters specified at least one id must be used', - 500: 'Database error'}, - params={'token': 'Secret token'}) + 500: 'Database error'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get participants + """ user_access = DBManager.userAccess(current_user) args = get_parser.parse_args() @@ -208,11 +210,13 @@ def get(self): 403: 'Logged user can\'t create/update the specified participant', 400: 'Badly formed JSON or missing fields(id_participant or id_project/id_group [only one of ' 'them]) in the JSON body, or mismatch between id_project and participant group project', - 500: 'Internal error when saving participant'}, - params={'token': 'Secret token'}) + 500: 'Internal error when saving participant'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): + """ + Create / update participant + """ user_access = DBManager.userAccess(current_user) # Using request.json instead of parser, since parser messes up the json! if 'participant' not in request.json: @@ -345,11 +349,13 @@ def post(self): @api.doc(description='Delete a specific participant', responses={200: 'Success', 403: 'Logged user can\'t delete participant (only project admin can delete)', - 500: 'Database error.'}, - params={'token': 'Secret token'}) + 500: 'Database error.'}) @api.expect(delete_parser) @user_multi_auth.login_required def delete(self): + """ + Delete a participant + """ user_access = DBManager.userAccess(current_user) args = delete_parser.parse_args() id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryProjectAccess.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryProjectAccess.py index 9d31ad885..cd42dcb79 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryProjectAccess.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryProjectAccess.py @@ -1,10 +1,9 @@ -from flask import jsonify, session, request +from flask import jsonify, request from flask_restx import Resource, reqparse, inputs from flask_babel import gettext from sqlalchemy import exc from modules.LoginModule.LoginModule import user_multi_auth, current_user from modules.FlaskModule.FlaskModule import user_api_ns as api -from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraServiceAccess import TeraServiceAccess from opentera.db.models.TeraServiceRole import TeraServiceRole from opentera.db.models.TeraProject import TeraProject @@ -67,11 +66,13 @@ def __init__(self, _api, *args, **kwargs): @api.doc(description='Get user roles for projects. Only one ID parameter required and supported at once.', responses={200: 'Success - returns list of users roles in projects', 400: 'Required parameter is missing (must have at least one id)', - 500: 'Error occured when loading project roles'}, - params={'token': 'Secret token'}) + 500: 'Error occured when loading project roles'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get roles for users / user groupes in a project + """ user_access = DBManager.userAccess(current_user) args = get_parser.parse_args() @@ -173,15 +174,17 @@ def get(self): # No access, but still fine return [], 200 - @api.doc(description='Create/update project access for an user.', + @api.doc(description='Create/update project access for an user / usergroup.', responses={200: 'Success', 403: 'Logged user can\'t modify this project or user access (project admin access required)', 400: 'Badly formed JSON or missing fields(id_user_group or id_project) in the JSON body', - 500: 'Database error'}, - params={'token': 'Secret token'}) + 500: 'Database error'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): + """ + Create / update project roles for users / usergroups + """ user_access = DBManager.userAccess(current_user) # Using request.json instead of parser, since parser messes up the json! json_projects = request.json['project_access'] @@ -256,11 +259,13 @@ def post(self): responses={200: 'Success', 403: 'Logged user can\'t delete project access(only user who is admin in that project can ' 'remove it)', - 500: 'Database error.'}, - params={'token': 'Secret token'}) + 500: 'Database error.'}) @api.expect(delete_parser) @user_multi_auth.login_required def delete(self): + """ + Delete specific user / usergroup role in a project + """ user_access = DBManager.userAccess(current_user) args = delete_parser.parse_args() id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryProjects.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryProjects.py index f1d1670d4..a73a4ee42 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryProjects.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryProjects.py @@ -43,11 +43,13 @@ def __init__(self, _api, *args, **kwargs): @api.doc(description='Get projects information. Only one of the ID parameter is supported and required at once', responses={200: 'Success - returns list of participants', - 500: 'Database error'}, - params={'token': 'Secret token'}) + 500: 'Database error'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get projects + """ user_access = DBManager.userAccess(current_user) args = get_parser.parse_args() @@ -121,11 +123,13 @@ def get(self): responses={200: 'Success', 403: 'Logged user can\'t create/update the specified project', 400: 'Badly formed JSON or missing fields(id_site or id_project) in the JSON body', - 500: 'Internal error occured when saving project'}, - params={'token': 'Secret token'}) + 500: 'Internal error occured when saving project'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): + """ + Create / update project + """ user_access = DBManager.userAccess(current_user) # Using request.json instead of parser, since parser messes up the json! if 'project' not in request.json: @@ -247,11 +251,13 @@ def post(self): @api.doc(description='Delete a specific project', responses={200: 'Success', 403: 'Logged user can\'t delete project (only site admin can delete)', - 500: 'Database error.'}, - params={'token': 'Secret token'}) + 500: 'Database error.'}) @api.expect(delete_parser) @user_multi_auth.login_required def delete(self): + """ + Delete project + """ user_access = DBManager.userAccess(current_user) args = delete_parser.parse_args() id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryServerSettings.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryServerSettings.py index ac10146cf..e35a11086 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryServerSettings.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryServerSettings.py @@ -19,11 +19,13 @@ def __init__(self, _api, *args, **kwargs): @api.doc(description='Get server setting key', responses={200: 'Success - returns setting value', - 401: 'Logged user doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + 401: 'Logged user doesn\'t have permission to access the requested data'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get server settings + """ # As soon as we are authorized, we can output the server versions args = get_parser.parse_args() diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryServiceAccess.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryServiceAccess.py index 285bfc628..f33cd9652 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryServiceAccess.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryServiceAccess.py @@ -1,5 +1,5 @@ -from flask import jsonify, request -from flask_restx import Resource, reqparse, inputs +from flask import request +from flask_restx import Resource, reqparse from modules.LoginModule.LoginModule import user_multi_auth, current_user from modules.FlaskModule.FlaskModule import user_api_ns as api from opentera.db.models.TeraServiceAccess import TeraServiceAccess @@ -39,11 +39,13 @@ def __init__(self, _api, *args, **kwargs): 'supported at once.', responses={200: 'Success - returns list of access roles', 400: 'Required parameter is missing (must have at least one id)', - 500: 'Error when getting association'}, - params={'token': 'Secret token'}) + 500: 'Error when getting association'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get service access roles for a specific item + """ user_access = DBManager.userAccess(current_user) args = get_parser.parse_args() @@ -88,11 +90,13 @@ def get(self): responses={200: 'Success', 403: 'Logged user can\'t modify association (only site admin can modify association)', 400: 'Badly formed JSON or missing fields(id_project or id_service) in the JSON body', - 500: 'Internal error occurred when saving association'}, - params={'token': 'Secret token'}) + 500: 'Internal error occurred when saving association'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): + """ + Create / update service - access association + """ user_access = DBManager.userAccess(current_user) # Using request.json instead of parser, since parser messes up the json! @@ -194,11 +198,13 @@ def post(self): @api.doc(description='Delete a specific service access.', responses={200: 'Success', 403: 'Logged user can\'t delete association (not admin of the associated elements)', - 500: 'Association not found or database error.'}, - params={'token': 'Secret token'}) + 500: 'Association not found or database error.'}) @api.expect(delete_parser) @user_multi_auth.login_required def delete(self): + """ + Delete a specific service access for an item + """ user_access = DBManager.userAccess(current_user) args = delete_parser.parse_args() id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryServiceAccessToken.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryServiceAccessToken.py index e38ae3e01..22abc3fa3 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryServiceAccessToken.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryServiceAccessToken.py @@ -23,11 +23,13 @@ def __init__(self, _api, *args, **kwargs): @api.doc(description='Get access token for a specific service.', responses={200: 'Success - returns access token', 400: 'Required parameter is missing (must have at least one id)', - 500: 'Error when getting association'}, - params={'token': 'Secret token'}) + 500: 'Error when getting association'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get service access token + """ user_access = DBManager.userAccess(current_user) args = get_parser.parse_args() diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryServiceConfigs.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryServiceConfigs.py index efeeba66e..18a06b8c8 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryServiceConfigs.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryServiceConfigs.py @@ -1,8 +1,7 @@ -from flask import jsonify, session, request +from flask import session, request from flask_restx import Resource, reqparse, inputs from modules.LoginModule.LoginModule import user_multi_auth, current_user from modules.FlaskModule.FlaskModule import user_api_ns as api -from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraServiceConfig import TeraServiceConfig from modules.DatabaseModule.DBManager import DBManager from sqlalchemy.exc import InvalidRequestError @@ -47,11 +46,13 @@ def __init__(self, _api, *args, **kwargs): 'config the current user.', responses={200: 'Success - returns list of configurations', 400: 'No parameters specified - id_service is at least required', - 500: 'Database error'}, - params={'token': 'Secret token'}) + 500: 'Database error'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get specific service configuration for an item + """ user_access = DBManager.userAccess(current_user) args = get_parser.parse_args() @@ -110,11 +111,13 @@ def get(self): 403: 'Logged user can\'t create/update the specified session', 400: 'Badly formed JSON or missing fields(service_config, id_service_config, id_service) in the' ' JSON body', - 500: 'Internal error when saving service config'}, - params={'token': 'Secret token'}) + 500: 'Internal error when saving service config'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): + """ + Create / update service configuration for an item + """ user_access = DBManager.userAccess(current_user) # Using request.json instead of parser, since parser messes up the json! if 'service_config' not in request.json: @@ -220,15 +223,17 @@ def post(self): else: return [update_config.to_json()] - @api.doc(description='Delete a specific session', + @api.doc(description='Delete a specific service configuration', responses={200: 'Success', 403: 'Logged user can\'t delete config (must have admin access to the related object - user,' 'device or participant, or be its own config)', - 500: 'Database error.'}, - params={'token': 'Secret token'}) + 500: 'Database error.'}) @api.expect(delete_parser) @user_multi_auth.login_required def delete(self): + """ + Delete a specific service configuration for an item + """ user_access = DBManager.userAccess(current_user) args = delete_parser.parse_args() id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryServiceProjects.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryServiceProjects.py index 512cdc317..3f0456f28 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryServiceProjects.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryServiceProjects.py @@ -49,11 +49,13 @@ def __init__(self, _api, *args, **kwargs): 'supported at once.', responses={200: 'Success - returns list of services - projects association', 400: 'Required parameter is missing (must have at least one id)', - 500: 'Error when getting association'}, - params={'token': 'Secret token'}) + 500: 'Error when getting association'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get services associated with a project + """ user_access = DBManager.userAccess(current_user) args = get_parser.parse_args() @@ -128,11 +130,13 @@ def get(self): responses={200: 'Success', 403: 'Logged user can\'t modify association (only site admin can modify association)', 400: 'Badly formed JSON or missing fields(id_project or id_service) in the JSON body', - 500: 'Internal error occurred when saving association'}, - params={'token': 'Secret token'}) + 500: 'Internal error occurred when saving association'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): + """ + Create / update service-project association + """ user_access = DBManager.userAccess(current_user) # Using request.json instead of parser, since parser messes up the json! @@ -293,11 +297,13 @@ def post(self): @api.doc(description='Delete a specific service - project association.', responses={200: 'Success', 403: 'Logged user can\'t delete association (not site admin of the associated project)', - 500: 'Association not found or database error.'}, - params={'token': 'Secret token'}) + 500: 'Association not found or database error.'}) @api.expect(delete_parser) @user_multi_auth.login_required def delete(self): + """ + Delete a specific service - project association + """ user_access = DBManager.userAccess(current_user) args = delete_parser.parse_args() id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryServiceRoles.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryServiceRoles.py index 067247996..7d9fa30e2 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryServiceRoles.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryServiceRoles.py @@ -33,11 +33,13 @@ def __init__(self, _api, *args, **kwargs): @api.doc(description='Get service roles for either a specific service or for all available services.', responses={200: 'Success - returns list of service roles', - 500: 'Error when getting roles'}, - params={'token': 'Secret token'}) + 500: 'Error when getting roles'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get service roles + """ user_access = DBManager.userAccess(current_user) args = get_parser.parse_args() @@ -70,11 +72,13 @@ def get(self): responses={200: 'Success', 403: 'Logged user can\'t modify service role (only super admin)', 400: 'Badly formed JSON or missing fields(id_service or id_service_role) in the JSON body', - 500: 'Internal error occurred when saving association'}, - params={'token': 'Secret token'}) + 500: 'Internal error occurred when saving association'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): + """ + Create / update service roles + """ if not current_user.user_superadmin: return gettext('Forbidden'), 403 @@ -123,11 +127,13 @@ def post(self): @api.doc(description='Delete a specific service role.', responses={200: 'Success', 403: 'Logged user can\'t delete role (not super admin)', - 500: 'Database error.'}, - params={'token': 'Secret token'}) + 500: 'Database error.'}) @api.expect(delete_parser) @user_multi_auth.login_required def delete(self): + """ + Delete service role + """ user_access = DBManager.userAccess(current_user) args = delete_parser.parse_args() id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryServiceSites.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryServiceSites.py index 55d487092..c04599937 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryServiceSites.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryServiceSites.py @@ -1,4 +1,4 @@ -from flask import jsonify, request +from flask import request from flask_restx import Resource, reqparse, inputs from modules.LoginModule.LoginModule import user_multi_auth, current_user from modules.FlaskModule.FlaskModule import user_api_ns as api @@ -46,11 +46,13 @@ def __init__(self, _api, *args, **kwargs): 'supported at once.', responses={200: 'Success - returns list of services - sites association', 400: 'Required parameter is missing (must have at least one id)', - 500: 'Error when getting association'}, - params={'token': 'Secret token'}) + 500: 'Error when getting association'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get services associated with a site + """ user_access = DBManager.userAccess(current_user) args = get_parser.parse_args() @@ -117,12 +119,14 @@ def get(self): responses={200: 'Success', 403: 'Logged user can\'t modify association (only super admin can modify association)', 400: 'Badly formed JSON or missing fields(id_project or id_service) in the JSON body', - 500: 'Internal error occurred when saving association'}, - params={'token': 'Secret token'}) + 500: 'Internal error occurred when saving association'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): - user_access = DBManager.userAccess(current_user) + """ + Create / update service-site association + """ + # user_access = DBManager.userAccess(current_user) # Only super admins can change service - site associations if not current_user.user_superadmin: @@ -248,12 +252,14 @@ def post(self): @api.doc(description='Delete a specific service - site association.', responses={200: 'Success', 403: 'Logged user can\'t delete association (only super admins can)', - 500: 'Association not found or database error.'}, - params={'token': 'Secret token'}) + 500: 'Association not found or database error.'}) @api.expect(delete_parser) @user_multi_auth.login_required def delete(self): - user_access = DBManager.userAccess(current_user) + """ + Delete service-site association + """ + # user_access = DBManager.userAccess(current_user) if not current_user.user_superadmin: return gettext('Forbidden'), 403 diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryServices.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryServices.py index 2c830618b..dbe855131 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryServices.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryServices.py @@ -1,10 +1,9 @@ -from flask import jsonify, session, request +from flask import jsonify, request from flask_restx import Resource, reqparse, inputs from modules.LoginModule.LoginModule import user_multi_auth, current_user from modules.FlaskModule.FlaskModule import user_api_ns as api from sqlalchemy.exc import InvalidRequestError from sqlalchemy import exc -from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraService import TeraService from opentera.db.models.TeraServiceRole import TeraServiceRole from modules.DatabaseModule.DBManager import DBManager @@ -41,11 +40,13 @@ def __init__(self, _api, *args, **kwargs): @api.doc(description='Get services information. Only one of the ID parameter is supported and required at once.', responses={200: 'Success - returns list of services', - 500: 'Database error'}, - params={'token': 'Secret token'}) + 500: 'Database error'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get services + """ user_access = DBManager.userAccess(current_user) args = get_parser.parse_args() @@ -109,11 +110,13 @@ def get(self): responses={200: 'Success', 403: 'Logged user can\'t create/update the specified service', 400: 'Badly formed JSON or missing fields(id_service) in the JSON body', - 500: 'Internal error occured when saving service'}, - params={'token': 'Secret token'}) + 500: 'Internal error occured when saving service'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): + """ + Create / update services + """ user_access = DBManager.userAccess(current_user) # Check if user is a super admin @@ -217,11 +220,13 @@ def post(self): 400: 'Service doesn\'t exists', 403: 'Logged user can\'t delete service (only super admins can delete) or service is a system ' 'service', - 500: 'Database error.'}, - params={'token': 'Secret token'}) + 500: 'Database error.'}) @api.expect(delete_parser) @user_multi_auth.login_required def delete(self): + """ + Delete service + """ user_access = DBManager.userAccess(current_user) args = delete_parser.parse_args() id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQuerySessionEvents.py b/teraserver/python/modules/FlaskModule/API/user/UserQuerySessionEvents.py index 76ca8aa2d..b2270df8d 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQuerySessionEvents.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQuerySessionEvents.py @@ -1,9 +1,8 @@ -from flask import jsonify, session, request +from flask import jsonify, request from flask_restx import Resource, reqparse from flask_babel import gettext from modules.LoginModule.LoginModule import user_multi_auth, current_user from modules.FlaskModule.FlaskModule import user_api_ns as api -from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraSessionEvent import TeraSessionEvent from modules.DatabaseModule.DBManager import DBManager from sqlalchemy.exc import InvalidRequestError @@ -32,11 +31,13 @@ def __init__(self, _api, *args, **kwargs): @api.doc(description='Get events for a specific session', responses={200: 'Success - returns list of events', 400: 'Required parameter is missing (id_session)', - 500: 'Database error'}, - params={'token': 'Secret token'}) + 500: 'Database error'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get events for a session + """ user_access = DBManager.userAccess(current_user) args = get_parser.parse_args() @@ -66,11 +67,13 @@ def get(self): responses={200: 'Success', 403: 'Logged user can\'t create/update the specified event', 400: 'Badly formed JSON or missing fields(id_session_event or id_session) in the JSON body', - 500: 'Internal error when saving device'}, - params={'token': 'Secret token'}) + 500: 'Internal error when saving device'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): + """ + Create / update session events + """ user_access = DBManager.userAccess(current_user) # Using request.json instead of parser, since parser messes up the json! if 'session_event' not in request.json: @@ -126,11 +129,13 @@ def post(self): @api.doc(description='Delete a specific session event', responses={200: 'Success', 403: 'Logged user can\'t delete event (no access to that session)', - 500: 'Database error.'}, - params={'token': 'Secret token'}) + 500: 'Database error.'}) @api.expect(delete_parser) @user_multi_auth.login_required def delete(self): + """ + Delete a session event + """ user_access = DBManager.userAccess(current_user) args = delete_parser.parse_args() id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py b/teraserver/python/modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py index 83ec36855..d04273929 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py @@ -44,15 +44,17 @@ def __init__(self, _api, *args, **kwargs): self.module = kwargs.get('flaskModule', None) self.test = kwargs.get('test', False) - @api.doc(description='Get devices types that are associated with a project. Only one "ID" parameter required and ' + @api.doc(description='Get session types that are associated with a project. Only one "ID" parameter required and ' 'supported at once.', responses={200: 'Success - returns list of devices-types - projects association', 400: 'Required parameter is missing (must have at least one id)', - 500: 'Error when getting association'}, - params={'token': 'Secret token'}) + 500: 'Error when getting association'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get session types associated with a project + """ user_access = DBManager.userAccess(current_user) args = get_parser.parse_args() @@ -115,11 +117,13 @@ def get(self): 403: 'Logged user can\'t modify association (session type must be accessible from project ' 'access)', 400: 'Badly formed JSON or missing fields(id_project or id_session_type) in the JSON body', - 500: 'Internal error occurred when saving association'}, - params={'token': 'Secret token'}) + 500: 'Internal error occurred when saving association'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): + """ + Create / update session types for a project + """ user_access = DBManager.userAccess(current_user) accessible_projects_ids = user_access.get_accessible_projects_ids(admin_only=True) @@ -255,11 +259,13 @@ def post(self): @api.doc(description='Delete a specific session-type - project association.', responses={200: 'Success', 403: 'Logged user can\'t delete association (no access to session-type or project)', - 400: 'Association not found (invalid id?)'}, - params={'token': 'Secret token'}) + 400: 'Association not found (invalid id?)'}) @api.expect(delete_parser) @user_multi_auth.login_required def delete(self): + """ + Delete specific session type - project association + """ user_access = DBManager.userAccess(current_user) args = delete_parser.parse_args() id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQuerySessionTypeSites.py b/teraserver/python/modules/FlaskModule/API/user/UserQuerySessionTypeSites.py index d43e0b572..490753761 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQuerySessionTypeSites.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQuerySessionTypeSites.py @@ -45,11 +45,13 @@ def __init__(self, _api, *args, **kwargs): ' at once.', responses={200: 'Success - returns list of session types - sites association', 400: 'Required parameter is missing (must have at least one id)', - 500: 'Error occurred when loading devices for sites'}, - params={'token': 'Secret token'}) + 500: 'Error occurred when loading devices for sites'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get session types associated with a site + """ user_access = DBManager.userAccess(current_user) args = get_parser.parse_args() @@ -104,11 +106,13 @@ def get(self): responses={200: 'Success', 403: 'Logged user can\'t modify device association', 400: 'Badly formed JSON or missing fields(id_site or id_device) in the JSON body', - 500: 'Internal error occured when saving device association'}, - params={'token': 'Secret token'}) + 500: 'Internal error occured when saving device association'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): + """ + Create / update session types associated with a site + """ user_access = DBManager.userAccess(current_user) # Only super admins can change session type - site associations @@ -248,11 +252,13 @@ def post(self): @api.doc(description='Delete a specific session type-site association.', responses={200: 'Success', 403: 'Logged user can\'t delete association (no admin access to site)', - 500: 'Session type - site association not found or database error.'}, - params={'token': 'Secret token'}) + 500: 'Session type - site association not found or database error.'}) @api.expect(delete_parser) @user_multi_auth.login_required def delete(self): + """ + Delete a specific session type - site association + """ user_access = DBManager.userAccess(current_user) args = delete_parser.parse_args() id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQuerySessionTypes.py b/teraserver/python/modules/FlaskModule/API/user/UserQuerySessionTypes.py index df9c1b2e9..80ddf24b5 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQuerySessionTypes.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQuerySessionTypes.py @@ -1,4 +1,4 @@ -from flask import session, request +from flask import request from flask_restx import Resource, reqparse, inputs from modules.LoginModule.LoginModule import user_multi_auth, current_user from modules.FlaskModule.FlaskModule import user_api_ns as api @@ -37,11 +37,13 @@ def __init__(self, _api, *args, **kwargs): @api.doc(description='Get session type information. If no id_session_type specified, returns all available ' 'session types', responses={200: 'Success - returns list of session types', - 500: 'Database error'}, - params={'token': 'Secret token'}) + 500: 'Database error'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get session type + """ user_access = DBManager.userAccess(current_user) args = get_parser.parse_args() @@ -84,11 +86,13 @@ def get(self): responses={200: 'Success', 403: 'Logged user can\'t create/update the specified session type', 400: 'Badly formed JSON or missing field(id_session_type) in the JSON body', - 500: 'Internal error when saving session type'}, - params={'token': 'Secret token'}) + 500: 'Internal error when saving session type'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): + """ + Create / update session type + """ user_access = DBManager.userAccess(current_user) # Using request.json instead of parser, since parser messes up the json! if 'session_type' not in request.json: @@ -302,6 +306,9 @@ def post(self): @api.expect(delete_parser) @user_multi_auth.login_required def delete(self): + """ + Delete session type + """ user_access = DBManager.userAccess(current_user) args = delete_parser.parse_args() id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQuerySessions.py b/teraserver/python/modules/FlaskModule/API/user/UserQuerySessions.py index f6c4d6aa4..dea617259 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQuerySessions.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQuerySessions.py @@ -1,4 +1,4 @@ -from flask import jsonify, session, request +from flask import jsonify, request from flask_restx import Resource, reqparse, inputs from modules.LoginModule.LoginModule import user_multi_auth, current_user from modules.FlaskModule.FlaskModule import user_api_ns as api @@ -45,11 +45,13 @@ def __init__(self, _api, *args, **kwargs): @api.doc(description='Get sessions information. Only one of the ID parameter is supported and required at once', responses={200: 'Success - returns list of sessions', 400: 'No parameters specified at least one id must be used', - 500: 'Database error'}, - params={'token': 'Secret token'}) + 500: 'Database error'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get session + """ user_access = DBManager.userAccess(current_user) args = get_parser.parse_args() @@ -109,11 +111,13 @@ def get(self): 403: 'Logged user can\'t create/update the specified session', 400: 'Badly formed JSON or missing fields(session, id_session, session_participants_ids and/or ' 'session_users_ids[for new sessions]) in the JSON body', - 500: 'Internal error when saving session'}, - params={'token': 'Secret token'}) + 500: 'Internal error when saving session'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): + """ + Create / update session + """ user_access = DBManager.userAccess(current_user) # Using request.json instead of parser, since parser messes up the json! if 'session' not in request.json: @@ -225,11 +229,13 @@ def post(self): responses={200: 'Success', 403: 'Logged user can\'t delete session (must have access to all participants and users in the ' 'session to delete)', - 500: 'Database error.'}, - params={'token': 'Secret token'}) + 500: 'Database error.'}) @api.expect(delete_parser) @user_multi_auth.login_required def delete(self): + """ + Delete session + """ user_access = DBManager.userAccess(current_user) args = delete_parser.parse_args() id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQuerySiteAccess.py b/teraserver/python/modules/FlaskModule/API/user/UserQuerySiteAccess.py index 45d21a8ed..c8890c632 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQuerySiteAccess.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQuerySiteAccess.py @@ -1,10 +1,9 @@ -from flask import jsonify, session, request +from flask import request from flask_restx import Resource, reqparse, inputs from flask_babel import gettext from sqlalchemy import exc from modules.LoginModule.LoginModule import user_multi_auth, current_user from modules.FlaskModule.FlaskModule import user_api_ns as api -from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraServiceAccess import TeraServiceAccess from opentera.db.models.TeraSite import TeraSite from opentera.db.models.TeraServiceRole import TeraServiceRole @@ -66,11 +65,13 @@ def __init__(self, _api, *args, **kwargs): @api.doc(description='Get user roles for sites. Only one parameter required and supported at once.', responses={200: 'Success - returns list of users roles in sites', 400: 'Required parameter is missing (must have at least one id)', - 500: 'Error occurred when loading sites roles'}, - params={'token': 'Secret token'}) + 500: 'Error occurred when loading sites roles'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get access role to site for user / usergroup + """ user_access = DBManager.userAccess(current_user) args = get_parser.parse_args() @@ -160,11 +161,13 @@ def get(self): responses={200: 'Success', 403: 'Logged user can\'t modify this site or user access (site admin access required)', 400: 'Badly formed JSON or missing fields(id_user or id_site) in the JSON body', - 500: 'Database error'}, - params={'token': 'Secret token'}) + 500: 'Database error'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): + """ + Create / update site access for an user / usergroup + """ user_access = DBManager.userAccess(current_user) # Using request.json instead of parser, since parser messes up the json! json_sites = request.json['site_access'] @@ -257,11 +260,13 @@ def post(self): @api.doc(description='Delete a specific site access', responses={200: 'Success', 403: 'Logged user can\'t delete site access(only user who is admin in that site can remove it)', - 500: 'Database error.'}, - params={'token': 'Secret token'}) + 500: 'Database error.'}) @api.expect(delete_parser) @user_multi_auth.login_required def delete(self): + """ + Delete a specific site access + """ user_access = DBManager.userAccess(current_user) args = delete_parser.parse_args() id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQuerySites.py b/teraserver/python/modules/FlaskModule/API/user/UserQuerySites.py index e8de48c5b..7d48f485a 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQuerySites.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQuerySites.py @@ -1,4 +1,4 @@ -from flask import jsonify, session, request +from flask import jsonify, request from flask_restx import Resource, reqparse from sqlalchemy import exc from modules.LoginModule.LoginModule import user_multi_auth, current_user @@ -35,11 +35,13 @@ def __init__(self, _api, *args, **kwargs): @api.doc(description='Get site information. Only one of the ID parameter is supported and required at once', responses={200: 'Success - returns list of sites', - 500: 'Database error'}, - params={'token': 'Secret token'}) + 500: 'Database error'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get site + """ user_access = DBManager.userAccess(current_user) args = get_parser.parse_args() @@ -102,11 +104,13 @@ def get(self): responses={200: 'Success', 403: 'Logged user can\'t create/update the specified site', 400: 'Badly formed JSON or missing field(id_site) in the JSON body', - 500: 'Internal error when saving site'}, - params={'token': 'Secret token'}) + 500: 'Internal error when saving site'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): + """ + Create / update site + """ user_access = DBManager.userAccess(current_user) if 'site' not in request.json: return gettext('Missing site'), 400 @@ -123,6 +127,7 @@ def post(self): json_site['id_site'] > 0: return gettext('Forbidden'), 403 + # Only superuser can create a site if json_site['id_site'] == 0 and not current_user.user_superadmin: return gettext('Forbidden'), 403 @@ -162,11 +167,13 @@ def post(self): @api.doc(description='Delete a specific site', responses={200: 'Success', 403: 'Logged user can\'t delete site (only super admin can delete)', - 500: 'Database error.'}, - params={'token': 'Secret token'}) + 500: 'Database error.'}) @api.expect(delete_parser) @user_multi_auth.login_required def delete(self): + """ + Delete a site + """ args = delete_parser.parse_args() id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryStats.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryStats.py index b51f3b71b..8da3e5f60 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryStats.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryStats.py @@ -39,11 +39,13 @@ def __init__(self, _api, *args, **kwargs): @api.doc(description='Get stats for the specified item.', responses={200: 'Success', 400: 'Missing parameter - one id must be specified.', - 500: 'Database error'}, - params={'token': 'Secret token'}) + 500: 'Database error'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get stats for the specified item + """ args = get_parser.parse_args() user_access = DBManager.userAccess(current_user) @@ -238,7 +240,6 @@ def get_site_stats(user_access: DBManagerTeraUserAccess, item_id: int, with_part @staticmethod def get_project_stats(user_access: DBManagerTeraUserAccess, item_id: int, with_parts: bool) -> dict: - from opentera.db.models.TeraSessionParticipants import TeraSessionParticipants from opentera.db.models.TeraProject import TeraProject project_users = user_access.query_users_for_project(project_id=item_id) project_users_enabled = user_access.query_users_for_project(project_id=item_id, enabled_only=True) diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryTestType.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryTestType.py index fe4e96f97..2d7f8dabc 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryTestType.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryTestType.py @@ -1,4 +1,4 @@ -from flask import session, request +from flask import request from flask_restx import Resource, reqparse, inputs from modules.LoginModule.LoginModule import user_multi_auth, current_user from modules.FlaskModule.FlaskModule import user_api_ns as api @@ -41,11 +41,13 @@ def __init__(self, _api, *args, **kwargs): @api.doc(description='Get test type information. If no id_test_type specified, returns all available test types', responses={200: 'Success - returns list of test types', - 500: 'Database error'}, - params={'token': 'Secret token'}) + 500: 'Database error'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get test types + """ user_access = DBManager.userAccess(current_user) args = get_parser.parse_args() @@ -117,11 +119,13 @@ def get(self): responses={200: 'Success', 403: 'Logged user can\'t create/update the specified test type', 400: 'Badly formed JSON or missing field in the JSON body', - 500: 'Internal error when saving test type'}, - params={'token': 'Secret token'}) + 500: 'Internal error when saving test type'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): + """ + Create / update test types + """ user_access = DBManager.userAccess(current_user) # Using request.json instead of parser, since parser messes up the json! if 'test_type' not in request.json: @@ -311,11 +315,13 @@ def post(self): responses={200: 'Success', 403: 'Logged user can\'t delete test type (no admin access to project related to that type ' 'or tests of that type exists in the system somewhere)', - 500: 'Database error.'}, - params={'token': 'Secret token'}) + 500: 'Database error.'}) @api.expect(delete_parser) @user_multi_auth.login_required def delete(self): + """ + Delete test type + """ user_access = DBManager.userAccess(current_user) args = delete_parser.parse_args() id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryTestTypeProjects.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryTestTypeProjects.py index ccb645058..7181f2ce5 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryTestTypeProjects.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryTestTypeProjects.py @@ -48,11 +48,13 @@ def __init__(self, _api, *args, **kwargs): 'supported at once.', responses={200: 'Success - returns list of test-types - projects association', 400: 'Required parameter is missing (must have at least one id)', - 500: 'Error when getting association'}, - params={'token': 'Secret token'}) + 500: 'Error when getting association'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get test types - project association + """ user_access = DBManager.userAccess(current_user) args = get_parser.parse_args() @@ -113,11 +115,13 @@ def get(self): responses={200: 'Success', 403: 'Logged user can\'t modify association (project admin access required)', 400: 'Badly formed JSON or missing fields in the JSON body', - 500: 'Internal error occurred when saving association'}, - params={'token': 'Secret token'}) + 500: 'Internal error occurred when saving association'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): + """ + Create / update test-type -> project association + """ user_access = DBManager.userAccess(current_user) accessible_projects_ids = user_access.get_accessible_projects_ids(admin_only=True) @@ -256,11 +260,13 @@ def post(self): @api.doc(description='Delete a specific test-type - project association.', responses={200: 'Success', 403: 'Logged user can\'t delete association (no access to test-type or project)', - 400: 'Association not found (invalid id?)'}, - params={'token': 'Secret token'}) + 400: 'Association not found (invalid id?)'}) @api.expect(delete_parser) @user_multi_auth.login_required def delete(self): + """ + Delete a specific test type - project association + """ user_access = DBManager.userAccess(current_user) args = delete_parser.parse_args() id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryTestTypeSites.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryTestTypeSites.py index 2339a5dca..f39521fa7 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryTestTypeSites.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryTestTypeSites.py @@ -42,15 +42,17 @@ def __init__(self, _api, *args, **kwargs): self.module = kwargs.get('flaskModule', None) self.test = kwargs.get('test', False) - @api.doc(description='Get session types that are related to a site. Only one "ID" parameter required and supported' + @api.doc(description='Get test types that are related to a site. Only one "ID" parameter required and supported' ' at once.', responses={200: 'Success - returns list of session types - sites association', 400: 'Required parameter is missing (must have at least one id)', - 500: 'Error occured when loading devices for sites'}, - params={'token': 'Secret token'}) + 500: 'Error occured when loading devices for sites'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get test types associated to a site + """ user_access = DBManager.userAccess(current_user) args = get_parser.parse_args() @@ -102,11 +104,13 @@ def get(self): responses={200: 'Success', 403: 'Logged user can\'t modify association', 400: 'Badly formed JSON or missing fields(id_site or id_test_type) in the JSON body', - 500: 'Internal error occurred when saving device association'}, - params={'token': 'Secret token'}) + 500: 'Internal error occurred when saving device association'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): + """ + Create / update test types associated to a site + """ user_access = DBManager.userAccess(current_user) # Only super admins can change session type - site associations @@ -237,11 +241,13 @@ def post(self): @api.doc(description='Delete a specific test type-site association.', responses={200: 'Success', 403: 'Logged user can\'t delete association (no admin access to site)', - 500: 'Session type - site association not found or database error.'}, - params={'token': 'Secret token'}) + 500: 'Session type - site association not found or database error.'}) @api.expect(delete_parser) @user_multi_auth.login_required def delete(self): + """ + Delete specific test type - site association + """ user_access = DBManager.userAccess(current_user) args = delete_parser.parse_args() id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryTests.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryTests.py index dc4a0d3b8..4edbf3c88 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryTests.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryTests.py @@ -1,4 +1,4 @@ -from flask import session, request +from flask import request from flask_restx import Resource, inputs from flask_babel import gettext from modules.LoginModule.LoginModule import user_multi_auth, current_user @@ -41,11 +41,13 @@ def __init__(self, _api, *args, **kwargs): @api.doc(description='Get test information. Only one of the ID parameter is supported at once', responses={200: 'Success - returns list of assets', 400: 'Required parameter is missing', - 403: 'Logged user doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + 403: 'Logged user doesn\'t have permission to access the requested data'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get test information + """ user_access = DBManager.userAccess(current_user) args = get_parser.parse_args() @@ -115,11 +117,13 @@ def get(self): return tests_list @api.doc(description='Delete test.', - responses={501: 'Unable to update test from here - use service!'}, - params={'token': 'Secret token'}) + responses={501: 'Unable to update test from here - use service!'}) @api.expect(post_parser) @user_multi_auth.login_required def post(self): + """ + Create / update test + """ return gettext('Test information update and creation must be done directly into a service (such as ' 'Test service)'), 501 @@ -131,6 +135,9 @@ def post(self): @api.expect(delete_parser) @user_multi_auth.login_required def delete(self): + """ + Delete test + """ user_access = DBManager.userAccess(current_user) args = delete_parser.parse_args(strict=True) id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryUndelete.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryUndelete.py index a28fd6e21..fbe09992b 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryUndelete.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryUndelete.py @@ -21,11 +21,13 @@ def __init__(self, _api, *args, **kwargs): 400: 'Required parameter is missing', 401: 'Requested item not found or is undeletable', 403: 'Access level insufficient to access that API or the item to undelete', - 500: 'Database error'}, - params={'token': 'Secret token'}) + 500: 'Database error'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Undelete an item + """ if not current_user.user_superadmin: return gettext('No access to this API'), 403 diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryUserGroups.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryUserGroups.py index 500f4dfd5..0a0e85bd8 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryUserGroups.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryUserGroups.py @@ -1,9 +1,8 @@ -from flask import jsonify, session, request +from flask import jsonify, request from flask_restx import Resource, reqparse, inputs from sqlalchemy import exc from modules.LoginModule.LoginModule import user_multi_auth, current_user from modules.FlaskModule.FlaskModule import user_api_ns as api -from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraServiceAccess import TeraServiceAccess from opentera.db.models.TeraServiceRole import TeraServiceRole from opentera.db.models.TeraUserGroup import TeraUserGroup @@ -72,11 +71,13 @@ def get_sites_roles_json(user_access, user_group_id: int): @api.doc(description='Get user group information. If no id specified, returns all accessible users groups', responses={200: 'Success', - 500: 'Database error'}, - params={'token': 'Secret token'}) + 500: 'Database error'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get usergroup + """ args = get_parser.parse_args() user_access = DBManager.userAccess(current_user) @@ -121,11 +122,13 @@ def get(self): responses={200: 'Success', 403: 'Logged user can\'t create/update the specified user group', 400: 'Badly formed JSON or missing field(id_user_group) in the JSON body', - 500: 'Internal error when saving user group'}, - params={'token': 'Secret token'}) + 500: 'Internal error when saving user group'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): + """ + Create / update usergroup + """ user_access = DBManager.userAccess(current_user) if 'user_group' not in request.json: @@ -274,11 +277,13 @@ def post(self): responses={200: 'Success', 403: 'Logged user can\'t delete user group (only a site admin that includes that user group in ' 'their site can delete)', - 500: 'Database error.'}, - params={'token': 'Secret token'}) + 500: 'Database error.'}) @api.expect(delete_parser) @user_multi_auth.login_required def delete(self): + """ + Delete an usergroup + """ user_access = DBManager.userAccess(current_user) args = delete_parser.parse_args() id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryUserPreferences.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryUserPreferences.py index b8fec30a5..b1da1ff60 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryUserPreferences.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryUserPreferences.py @@ -1,9 +1,8 @@ -from flask import session, request -from flask_restx import Resource, reqparse, inputs +from flask import request +from flask_restx import Resource from flask_babel import gettext from modules.LoginModule.LoginModule import user_multi_auth, current_user from modules.FlaskModule.FlaskModule import user_api_ns as api -from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraUserPreference import TeraUserPreference from sqlalchemy.exc import InvalidRequestError from sqlalchemy import exc @@ -31,11 +30,13 @@ def __init__(self, _api, *args, **kwargs): responses={200: 'Success - returns list of user preferences', 400: 'Missing parameter or bad app_tag', 403: 'Forbidden access to that user.', - 500: 'Database error'}, - params={'token': 'Secret token'}) + 500: 'Database error'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get user preferences (for a specific app) + """ user_access = DBManager.userAccess(current_user) args = get_parser.parse_args() @@ -72,11 +73,13 @@ def get(self): responses={200: 'Success', 403: 'Logged user can\'t create/update the user linked to that preference', 400: 'Badly formed JSON or missing fields(app_tag) in the JSON body', - 500: 'Internal error occurred when saving user preference'}, - params={'token': 'Secret token'}) + 500: 'Internal error occurred when saving user preference'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): + """ + Create / update user preferences + """ user_access = DBManager.userAccess(current_user) # Using request.json instead of parser, since parser messes up the json! json_user_pref = request.json['user_preference'] diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryUserUserGroups.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryUserUserGroups.py index 1f8660788..cb17256f9 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryUserUserGroups.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryUserUserGroups.py @@ -1,4 +1,4 @@ -from flask import jsonify, session, request +from flask import request from flask_restx import Resource, reqparse, inputs from sqlalchemy import exc from modules.LoginModule.LoginModule import user_multi_auth, current_user @@ -35,11 +35,13 @@ def __init__(self, _api, *args, **kwargs): @api.doc(description='Get user - user group information. At least one "id" field must be specified', responses={200: 'Success', - 500: 'Database error'}, - params={'token': 'Secret token'}) + 500: 'Database error'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get user usergroups + """ args = get_parser.parse_args() user_access = DBManager.userAccess(current_user) @@ -78,11 +80,13 @@ def get(self): responses={200: 'Success', 403: 'Logged user can\'t create/update the specified user group', 400: 'Badly formed JSON or missing field(id_user_group) in the JSON body', - 500: 'Internal error when saving user group'}, - params={'token': 'Secret token'}) + 500: 'Internal error when saving user group'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): + """ + Create / update user's usergroups + """ user_access = DBManager.userAccess(current_user) if not 'user_user_group' in request.json: @@ -143,11 +147,13 @@ def post(self): responses={200: 'Success', 403: 'Logged user can\'t delete user group (only a site admin that includes that user group in ' 'their site can delete)', - 500: 'Database error.'}, - params={'token': 'Secret token'}) + 500: 'Database error.'}) @api.expect(delete_parser) @user_multi_auth.login_required def delete(self): + """ + Delete a specific user-usergroup assocation + """ user_access = DBManager.userAccess(current_user) args = delete_parser.parse_args() id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryUsers.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryUsers.py index 93ccf3073..0e42a4eb6 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryUsers.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryUsers.py @@ -3,13 +3,15 @@ from sqlalchemy import exc from modules.LoginModule.LoginModule import user_multi_auth, current_user from modules.FlaskModule.FlaskModule import user_api_ns as api -from opentera.db.models.TeraUser import TeraUser +from opentera.db.models.TeraUser import TeraUser, UserPasswordInsecure, UserNewPasswordSameAsOld from opentera.db.models.TeraUserGroup import TeraUserGroup from flask_babel import gettext from modules.DatabaseModule.DBManager import DBManager from opentera.redis.RedisRPCClient import RedisRPCClient from opentera.modules.BaseModule import ModuleNames +from modules.FlaskModule.FlaskUtils import FlaskUtils + # Parser definition(s) get_parser = api.parser() get_parser.add_argument('id', type=int, help='ID of the user to query') @@ -43,13 +45,32 @@ def __init__(self, _api, *args, **kwargs): self.module = kwargs.get('flaskModule', None) self.test = kwargs.get('test', False) + @staticmethod + def get_password_weaknesses_text(weaknesses: list) -> str: + text_list = [] + for weakness in weaknesses: + if weakness == UserPasswordInsecure.PasswordWeaknesses.NO_SPECIAL: + text_list.append(gettext('Password missing special character')) + if weakness == UserPasswordInsecure.PasswordWeaknesses.NO_NUMERIC: + text_list.append(gettext('Password missing numeric character')) + if weakness == UserPasswordInsecure.PasswordWeaknesses.BAD_LENGTH: + text_list.append(gettext('Password not long enough')) + if weakness == UserPasswordInsecure.PasswordWeaknesses.NO_LOWER_CASE: + text_list.append(gettext('Password missing lower case letter')) + if weakness == UserPasswordInsecure.PasswordWeaknesses.NO_UPPER_CASE: + text_list.append(gettext('Password missing upper case letter')) + + return ",".join(text for text in text_list) + @api.doc(description='Get user information. If no id specified, returns all accessible users', responses={200: 'Success', - 500: 'Database error'}, - params={'token': 'Secret token'}) + 500: 'Database error'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get users + """ args = get_parser.parse_args() user_access = DBManager.userAccess(current_user) @@ -147,14 +168,6 @@ def get(self): return jsonify(users_list) return [], 200 - # try: - # users = TeraUser.query_data(my_args) - # users_list = [] - # for user in users: - # users_list.append(user.to_json()) - # return jsonify(users_list) - # except InvalidRequestError: - # return '', 500 @api.doc(description='Create / update user. id_user must be set to "0" to create a new user. User can be modified ' 'if: current user is super admin or user is part of a project which the current user is admin.' @@ -165,11 +178,13 @@ def get(self): 400: 'Badly formed JSON or missing field(id_user or missing password when new user) in the ' 'JSON body', 409: 'Username is already taken', - 500: 'Internal error when saving user'}, - params={'token': 'Secret token'}) + 500: 'Internal error when saving user'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): + """ + Create / update an user + """ user_access = DBManager.userAccess(current_user) if 'user' not in request.json: @@ -238,6 +253,12 @@ def post(self): UserQueryUsers.__name__, 'post', 500, 'Database error', str(e)) return gettext('Database error'), 500 + except UserPasswordInsecure as e: + return (gettext('Password not strong enough') + ': ' + + FlaskUtils.get_password_weaknesses_text(e.weaknesses), 400) + except UserNewPasswordSameAsOld: + return gettext('New password same as old password'), 400 + else: # New user, check if password is set # if 'user_password' not in json_user: @@ -270,6 +291,9 @@ def post(self): UserQueryUsers.__name__, 'post', 500, 'Database error', str(e)) return gettext('Database error'), 500 + except UserPasswordInsecure as e: + return (gettext('Password not strong enough') + ': ' + + FlaskUtils.get_password_weaknesses_text(e.weaknesses), 400) update_user = TeraUser.get_user_by_id(json_user['id_user']) @@ -299,34 +323,18 @@ def post(self): update_user.commit() - # Check if there's some user groups for the updated user that we need to delete - # id_groups_to_delete = set([group.id_user_group for group in update_user.user_user_groups])\ - # .difference(user_user_groups_ids) - # - # for id_to_del in id_groups_to_delete: - # uug_to_del = TeraUserUserGroup.query_user_user_group_for_user_user_group(user_id=update_user.id_user, - # user_group_id=id_to_del) - # TeraUserUserGroup.delete(id_todel=uug_to_del.id_user_user_group) - # - # # Update / insert user groups - # for user_group in user_user_groups: - # if not TeraUserUserGroup.query_user_user_group_for_user_user_group(user_id=update_user.id_user, - # user_group_id= - # user_group['id_user_group']): - # # Group not already associated - associates! - # TeraUserUserGroup.insert_user_user_group(id_user_group=user_group['id_user_group'], - # id_user=update_user.id_user) - return [update_user.to_json()] @api.doc(description='Delete a specific user', responses={200: 'Success', 403: 'Logged user can\'t delete user (only super admin can delete)', - 500: 'Database error.'}, - params={'token': 'Secret token'}) + 500: 'Database error.'}) @api.expect(delete_parser) @user_multi_auth.login_required def delete(self): + """ + Delete an user + """ user_access = DBManager.userAccess(current_user) args = delete_parser.parse_args() id_todel = args['id'] diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryVersions.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryVersions.py index 121522475..cf88672f2 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryVersions.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryVersions.py @@ -27,13 +27,15 @@ def __init__(self, _api, *args, **kwargs): @api.doc(description='Get server versions', responses={200: 'Success - returns versions information', 400: 'Required parameter is missing', - 403: 'Logged user doesn\'t have permission to access the requested data'}, - params={'token': 'Secret token'}) + 403: 'Logged user doesn\'t have permission to access the requested data'}) @api.expect(get_parser) @user_multi_auth.login_required def get(self): + """ + Get server versions + """ # As soon as we are authorized, we can output the server versions - args = get_parser.parse_args() + # args = get_parser.parse_args() current_settings = TeraServerSettings.get_server_setting_value(TeraServerSettings.ServerVersions) if not current_settings: @@ -45,11 +47,13 @@ def get(self): responses={200: 'Success - asset posted', 500: 'Database error occurred', 403: 'Logged user doesn\'t have permission to delete the requested asset (must be an user of' - 'the related project)'}, - params={'token': 'Secret token'}) + 'the related project)'}) @api.expect(post_schema) @user_multi_auth.login_required def post(self): + """ + Update server versions + """ # Only superuser can change the versions settings # Only some fields can be changed. if current_user.user_superadmin: diff --git a/teraserver/python/modules/FlaskModule/API/user/UserRefreshToken.py b/teraserver/python/modules/FlaskModule/API/user/UserRefreshToken.py index 6edf0e94d..e699365fd 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserRefreshToken.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserRefreshToken.py @@ -1,6 +1,5 @@ from flask import session, request -from flask_restx import Resource, reqparse, inputs -from flask_babel import gettext +from flask_restx import Resource, inputs from modules.LoginModule.LoginModule import user_token_auth, current_user from modules.FlaskModule.FlaskModule import user_api_ns as api from modules.LoginModule.LoginModule import LoginModule @@ -23,11 +22,13 @@ def __init__(self, _api, *args, **kwargs): @api.doc(description='Refresh token, old token needs to be passed in request headers.', responses={200: 'Success', - 500: 'Server error'}, - params={'token': 'Secret token'}) + 500: 'Server error'}) @api.expect(get_parser) @user_token_auth.login_required def get(self): + """ + Refresh token for current user + """ # If we have made it this far, token passed in headers was valid. # Get user token key from redis token_key = self.module.redisGet(RedisVars.RedisVar_UserTokenAPIKey) diff --git a/teraserver/python/modules/FlaskModule/API/user/UserSessionManager.py b/teraserver/python/modules/FlaskModule/API/user/UserSessionManager.py index ad9c498a5..3f98bb4e8 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserSessionManager.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserSessionManager.py @@ -1,8 +1,7 @@ -from flask import session, request +from flask import request from flask_restx import Resource from modules.LoginModule.LoginModule import user_multi_auth, current_user from modules.FlaskModule.FlaskModule import user_api_ns as api -from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraService import TeraService from opentera.db.models.TeraSession import TeraSession from flask_babel import gettext @@ -89,11 +88,13 @@ def __init__(self, _api, *args, **kwargs): 400: 'Required parameter is missing', 500: 'Internal server error', 501: 'Not implemented', - 403: 'Logged user doesn\'t have enough permission'}, - params={'token': 'Secret token'}) + 403: 'Logged user doesn\'t have enough permission'}) @api.expect(session_manager_schema) @user_multi_auth.login_required def post(self): + """ + Starts / stop a session related to a service + """ args = post_parser.parse_args() user_access = DBManager.userAccess(current_user) diff --git a/teraserver/python/modules/FlaskModule/FlaskModule.py b/teraserver/python/modules/FlaskModule/FlaskModule.py index 9b2d8465c..6c1d1b86c 100755 --- a/teraserver/python/modules/FlaskModule/FlaskModule.py +++ b/teraserver/python/modules/FlaskModule/FlaskModule.py @@ -7,6 +7,8 @@ from opentera.db.models.TeraServerSettings import TeraServerSettings from opentera.OpenTeraServerVersion import opentera_server_version_string import redis +import datetime + from modules.Globals import opentera_doc_url @@ -37,16 +39,14 @@ def get_timezone(): # API authorizations = { - 'HTTPAuth': { - 'type': 'basic', - 'in': 'header' + 'basicAuth': { + 'type': 'basic' }, - 'Token Authentication': { + 'tokenAuth': { 'type': 'apiKey', 'in': 'header', 'name': 'Authorization', - 'default': 'OpenTera', - 'bearerFormat': 'JWT' + 'description': 'Enter token with the `OpenTera` prefix, e.g. "OpenTera 12345"' } } @@ -66,7 +66,7 @@ def specs_url(self): # if doc is set to False, documentation is disabled api = CustomAPI(flask_app, version=opentera_server_version_string, title='OpenTeraServer API', description='TeraServer API Documentation', doc=opentera_doc_url, prefix='/api', - authorizations=authorizations) + authorizations=authorizations, security='basicAuth') # Namespaces user_api_ns = api.namespace('user', description='API for user calls') @@ -97,7 +97,9 @@ def __init__(self, config: ConfigManager, test_mode=False): flask_app.config.update({'SESSION_REDIS': redis_url}) flask_app.config.update({'BABEL_DEFAULT_LOCALE': 'fr'}) flask_app.config.update({'SESSION_COOKIE_SECURE': True}) + flask_app.config.update({'SESSION_COOKIE_SAMESITE': 'Strict'}) flask_app.config.update({'PROPAGATE_EXCEPTIONS': flask_app.debug}) + flask_app.config.update({'PERMANENT_SESSION_LIFETIME': datetime.timedelta(minutes=5)}) # TODO set upload folder in config # TODO remove this configuration, it is not useful? flask_app.config.update({'UPLOAD_FOLDER': 'uploads'}) @@ -105,7 +107,7 @@ def __init__(self, config: ConfigManager, test_mode=False): # Not sure. # flask_app.config.update({'BABEL_DEFAULT_TIMEZONE': 'UTC'}) - self.session = Session(flask_app) + # self.session = Session(flask_app) # Init API FlaskModule.init_user_api(self, user_api_ns) @@ -138,6 +140,9 @@ def init_user_api(module: object, namespace: Namespace, additional_args: dict = # Users... from modules.FlaskModule.API.user.UserLogin import UserLogin + from modules.FlaskModule.API.user.UserLogin2FA import UserLogin2FA + from modules.FlaskModule.API.user.UserLoginSetup2FA import UserLoginSetup2FA + from modules.FlaskModule.API.user.UserLoginChangePassword import UserLoginChangePassword from modules.FlaskModule.API.user.UserLogout import UserLogout from modules.FlaskModule.API.user.UserQueryUsers import UserQueryUsers from modules.FlaskModule.API.user.UserQueryUserPreferences import UserQueryUserPreferences @@ -200,6 +205,9 @@ def init_user_api(module: object, namespace: Namespace, additional_args: dict = namespace.add_resource(UserQueryForms, '/forms', resource_class_kwargs=kwargs) namespace.add_resource(UserQueryParticipantGroup, '/groups', resource_class_kwargs=kwargs) namespace.add_resource(UserLogin, '/login', resource_class_kwargs=kwargs) + namespace.add_resource(UserLogin2FA, '/login/2fa', resource_class_kwargs=kwargs) + namespace.add_resource(UserLoginSetup2FA, '/login/setup_2fa', resource_class_kwargs=kwargs) + namespace.add_resource(UserLoginChangePassword, '/login/change_password', resource_class_kwargs=kwargs) namespace.add_resource(UserLogout, '/logout', resource_class_kwargs=kwargs) namespace.add_resource(UserQueryParticipants, '/participants', resource_class_kwargs=kwargs) namespace.add_resource(UserQueryOnlineParticipants, '/participants/online', resource_class_kwargs=kwargs) @@ -351,6 +359,10 @@ def init_test_api(module: object, namespace: Namespace, additional_args: dict = def init_views(self): from modules.FlaskModule.Views.About import About from modules.FlaskModule.Views.DisabledDoc import DisabledDoc + from modules.FlaskModule.Views.LoginView import LoginView + from modules.FlaskModule.Views.LoginChangePasswordView import LoginChangePasswordView + from modules.FlaskModule.Views.LoginSetup2FAView import LoginSetup2FAView + from modules.FlaskModule.Views.LoginValidate2FAView import LoginValidate2FAView # Default arguments args = [] @@ -359,18 +371,31 @@ def init_views(self): # About flask_app.add_url_rule('/about', view_func=About.as_view('about', *args, **kwargs)) + # Login + flask_app.add_url_rule('/login', view_func=LoginView.as_view('login', *args, **kwargs)) + flask_app.add_url_rule('/login_change_password', view_func=LoginChangePasswordView.as_view( + 'login_change_password', *args, **kwargs)) + flask_app.add_url_rule('/login_setup_2fa', view_func=LoginSetup2FAView.as_view( + 'login_setup_2fa', *args, **kwargs)) + flask_app.add_url_rule('/login_validate_2fa', view_func=LoginValidate2FAView.as_view( + 'login_validate_2fa', *args, **kwargs)) + if not self.config.server_config['enable_docs']: # Disabled docs view flask_app.add_url_rule('/doc', view_func=DisabledDoc.as_view('doc', *args, **kwargs)) @flask_app.after_request -def apply_caching(response): +def post_process_request(response): # This is required to expose the backend API to rendered webpages from other sources, such as services response.headers["Access-Control-Allow-Origin"] = "*" response.headers["Access-Control-Allow-Headers"] = "*" response.headers["Access-Control-Allow-Methods"] = "*" + # Remove WWW-Authenticate from header to prevent browsers to prevent an authentication pop-up + if response.status_code == 401 and 'WWW-Authenticate' in response.headers: + del response.headers['WWW-Authenticate'] + # Request processing time import time print(f"Process time: {(time.time() - g.start_time)*1000} ms") diff --git a/teraserver/python/modules/FlaskModule/FlaskUtils.py b/teraserver/python/modules/FlaskModule/FlaskUtils.py new file mode 100644 index 000000000..3736d994d --- /dev/null +++ b/teraserver/python/modules/FlaskModule/FlaskUtils.py @@ -0,0 +1,23 @@ +from flask_babel import gettext +from opentera.db.models.TeraUser import UserPasswordInsecure + + +class FlaskUtils: + + @staticmethod + def get_password_weaknesses_text(weaknesses: list, separator=',') -> str: + from flask_babel import gettext + text_list = [] + for weakness in weaknesses: + if weakness == UserPasswordInsecure.PasswordWeaknesses.NO_SPECIAL: + text_list.append(gettext('Password missing special character')) + if weakness == UserPasswordInsecure.PasswordWeaknesses.NO_NUMERIC: + text_list.append(gettext('Password missing numeric character')) + if weakness == UserPasswordInsecure.PasswordWeaknesses.BAD_LENGTH: + text_list.append(gettext('Password not long enough (10 characters)')) + if weakness == UserPasswordInsecure.PasswordWeaknesses.NO_LOWER_CASE: + text_list.append(gettext('Password missing lower case letter')) + if weakness == UserPasswordInsecure.PasswordWeaknesses.NO_UPPER_CASE: + text_list.append(gettext('Password missing upper case letter')) + + return separator.join(text for text in text_list) diff --git a/teraserver/python/modules/FlaskModule/Views/About.py b/teraserver/python/modules/FlaskModule/Views/About.py index 238e86987..cc5de3e11 100644 --- a/teraserver/python/modules/FlaskModule/Views/About.py +++ b/teraserver/python/modules/FlaskModule/Views/About.py @@ -8,6 +8,7 @@ class About(MethodView): def __init__(self, *args, **kwargs): self.flaskModule = kwargs.get('flaskModule', None) + self.test = kwargs.get('test', False) # Anybody can view this? # @user_multi_auth.login_required diff --git a/teraserver/python/modules/FlaskModule/Views/DisabledDoc.py b/teraserver/python/modules/FlaskModule/Views/DisabledDoc.py index 561a738c7..f22bcc22d 100644 --- a/teraserver/python/modules/FlaskModule/Views/DisabledDoc.py +++ b/teraserver/python/modules/FlaskModule/Views/DisabledDoc.py @@ -7,6 +7,7 @@ class DisabledDoc(MethodView): def __init__(self, *args, **kwargs): self.flaskModule = kwargs.get('flaskModule', None) + self.test = kwargs.get('test', False) # Anybody can view this? # @user_multi_auth.login_required diff --git a/teraserver/python/modules/FlaskModule/Views/LoginChangePasswordView.py b/teraserver/python/modules/FlaskModule/Views/LoginChangePasswordView.py new file mode 100644 index 000000000..13893422e --- /dev/null +++ b/teraserver/python/modules/FlaskModule/Views/LoginChangePasswordView.py @@ -0,0 +1,71 @@ +from flask.views import MethodView +from flask import render_template, request, redirect, url_for +from flask_login import logout_user +from opentera.utils.TeraVersions import TeraVersions +from opentera.db.models.TeraUser import TeraUser, UserPasswordInsecure +from flask_babel import gettext +from modules.LoginModule.LoginModule import current_user, LoginModule +from modules.FlaskModule.FlaskUtils import FlaskUtils + +class LoginChangePasswordView(MethodView): + + def __init__(self, *args, **kwargs): + self.flaskModule = kwargs.get('flaskModule', None) + self.test = kwargs.get('test', False) + + @LoginModule.user_session_required + def get(self): + hostname = self.flaskModule.config.server_config['hostname'] + port = self.flaskModule.config.server_config['port'] + + if 'X_EXTERNALSERVER' in request.headers: + hostname = request.headers['X_EXTERNALSERVER'] + + if 'X_EXTERNALPORT' in request.headers: + port = request.headers['X_EXTERNALPORT'] + + versions = TeraVersions() + versions.load_from_db() + + return render_template('login_change_password.html', hostname=hostname, port=port, + server_version=versions.version_string, username=current_user.user_username) + + @LoginModule.user_session_required + def post(self): + + # Verify if form is complete + if 'new_password' not in request.form or 'confirm_password' not in request.form: + return gettext('Missing information'), 400 + + # Get form information + # old_password = request.form['old_password'] + new_password = request.form['new_password'] + confirm_password = request.form['confirm_password'] + + # Validate if new password and confirm password are the same + if new_password != confirm_password: + logout_user() + return gettext('New password and confirm password do not match'), 400 + + # Validate that new password is different from current + if TeraUser.verify_password(current_user.user_username, new_password) is not None: + # logout_user() + return gettext('New password must be different from current'), 400 + + + # Validate if old password is correct + # if TeraUser.verify_password(current_user.user_username, old_password) is None: + # logout_user() + # return gettext('Invalid old password'), 400 + + # Change password, will be encrypted + # Will also reset force password change flag + try: + TeraUser.update(current_user.id_user, {'user_password': new_password, + 'user_force_password_change': False }) + except UserPasswordInsecure as e: + return FlaskUtils.get_password_weaknesses_text(e.weaknesses, '
'), 400 + + # logout_user() + + return redirect(url_for('login')) diff --git a/teraserver/python/modules/FlaskModule/Views/LoginSetup2FAView.py b/teraserver/python/modules/FlaskModule/Views/LoginSetup2FAView.py new file mode 100644 index 000000000..e1d4ae138 --- /dev/null +++ b/teraserver/python/modules/FlaskModule/Views/LoginSetup2FAView.py @@ -0,0 +1,43 @@ +from flask.views import MethodView +from flask import render_template, request, redirect, url_for, session +from opentera.utils.TeraVersions import TeraVersions +from modules.LoginModule.LoginModule import current_user, LoginModule +from opentera.db.models.TeraUser import TeraUser +import pyotp +import pyqrcode +from flask_babel import gettext + + +class LoginSetup2FAView(MethodView): + + def __init__(self, *args, **kwargs): + self.flaskModule = kwargs.get('flaskModule', None) + self.test = kwargs.get('test', False) + + @LoginModule.user_session_required + def get(self): + """ + GET method for the login enable 2FA page. This page is displayed when a user logs in and has 2FA disabled. + User must be authenticated to access this page. User will need to set 2FA to continue. + """ + + # Verify if user is authenticated, should be stored in session + # Return to login page + if not current_user: + return redirect(url_for('login')) + + hostname = self.flaskModule.config.server_config['hostname'] + port = self.flaskModule.config.server_config['port'] + + if 'X_EXTERNALSERVER' in request.headers: + hostname = request.headers['X_EXTERNALSERVER'] + + if 'X_EXTERNALPORT' in request.headers: + port = request.headers['X_EXTERNALPORT'] + + versions = TeraVersions() + versions.load_from_db() + + return render_template('login_setup_2fa.html', hostname=hostname, port=port, + server_version=versions.version_string, + user_has_email=current_user.user_email is not None and current_user.user_email != "") diff --git a/teraserver/python/modules/FlaskModule/Views/LoginValidate2FAView.py b/teraserver/python/modules/FlaskModule/Views/LoginValidate2FAView.py new file mode 100644 index 000000000..23c97c191 --- /dev/null +++ b/teraserver/python/modules/FlaskModule/Views/LoginValidate2FAView.py @@ -0,0 +1,46 @@ +from flask.views import MethodView +from flask import render_template, request, redirect, url_for, session, jsonify +from opentera.utils.TeraVersions import TeraVersions +from modules.LoginModule.LoginModule import current_user, LoginModule +from opentera.db.models.TeraUser import TeraUser +from flask_babel import gettext +from opentera.utils.UserAgentParser import UserAgentParser + +import opentera.messages.python as messages +from opentera.redis.RedisVars import RedisVars +from opentera.redis.RedisRPCClient import RedisRPCClient +from opentera.modules.BaseModule import ModuleNames + + +class LoginValidate2FAView(MethodView): + + def __init__(self, *args, **kwargs): + self.flaskModule = kwargs.get('flaskModule', None) + self.test = kwargs.get('test', False) + + @LoginModule.user_session_required + def get(self): + """ + GET method for the login enable 2FA page. This page is displayed when a user logs in and has 2FA disabled. + User must be authenticated to access this page. User will need to set 2FA to continue. + """ + + # Verify if user is authenticated, should be stored in session + # Return to login page + if not current_user: + return redirect(url_for('login')) + + hostname = self.flaskModule.config.server_config['hostname'] + port = self.flaskModule.config.server_config['port'] + + if 'X_EXTERNALSERVER' in request.headers: + hostname = request.headers['X_EXTERNALSERVER'] + + if 'X_EXTERNALPORT' in request.headers: + port = request.headers['X_EXTERNALPORT'] + + versions = TeraVersions() + versions.load_from_db() + + return render_template('login_validate_2fa.html', hostname=hostname, port=port, + server_version=versions.version_string) diff --git a/teraserver/python/modules/FlaskModule/Views/LoginView.py b/teraserver/python/modules/FlaskModule/Views/LoginView.py new file mode 100644 index 000000000..8f16e004c --- /dev/null +++ b/teraserver/python/modules/FlaskModule/Views/LoginView.py @@ -0,0 +1,28 @@ +from flask.views import MethodView +from flask import render_template, request +from opentera.utils.TeraVersions import TeraVersions + + +class LoginView(MethodView): + + def __init__(self, *args, **kwargs): + self.flaskModule = kwargs.get('flaskModule', None) + self.test = kwargs.get('test', False) + + def get(self): + hostname = self.flaskModule.config.server_config['hostname'] + port = self.flaskModule.config.server_config['port'] + + if 'X_EXTERNALSERVER' in request.headers: + hostname = request.headers['X_EXTERNALSERVER'] + + if 'X_EXTERNALPORT' in request.headers: + port = request.headers['X_EXTERNALPORT'] + + show_logo = 'no_logo' not in request.args + + versions = TeraVersions() + versions.load_from_db() + + return render_template('login.html', hostname=hostname, port=port, + server_version=versions.version_string, show_logo=show_logo) diff --git a/teraserver/python/modules/LoginModule/LoginModule.py b/teraserver/python/modules/LoginModule/LoginModule.py index a974d19be..4f0f741c5 100755 --- a/teraserver/python/modules/LoginModule/LoginModule.py +++ b/teraserver/python/modules/LoginModule/LoginModule.py @@ -16,11 +16,11 @@ import datetime import redis -from flask import request, g +from flask import request, g, session, abort from flask_babel import gettext from werkzeug.local import LocalProxy from flask_restx import reqparse -from functools import wraps +from functools import wraps, partial from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth, MultiAuth, Authorization @@ -43,6 +43,7 @@ # Authentication schemes for users user_http_auth = HTTPBasicAuth(realm='user') +user_http_login_auth = HTTPBasicAuth(realm='user') user_token_auth = HTTPTokenAuth("OpenTera") user_multi_auth = MultiAuth(user_http_auth, user_token_auth) @@ -117,19 +118,23 @@ def setup_login_manager(self): # Cookie based configuration self.app.config.update({'REMEMBER_COOKIE_NAME': 'OpenTera', - 'REMEMBER_COOKIE_DURATION': 14, + 'REMEMBER_COOKIE_DURATION': datetime.timedelta(minutes=30), 'REMEMBER_COOKIE_SECURE': True, - 'USE_PERMANENT_SESSION': True, - 'PERMANENT_SESSION_LIFETIME': datetime.timedelta(minutes=1), - 'REMEMBER_COOKIE_REFRESH_EACH_REQUEST': True}) + 'REMEMBER_COOKIE_SAMESITE': 'Strict', + # 'PERMANENT_SESSION_LIFETIME': datetime.timedelta(minutes=1), + # 'PERMANENT_SESSION_LIFETIME': datetime.timedelta(minutes=5), + 'REMEMBER_COOKIE_REFRESH_EACH_REQUEST': True + }) # Setup user loader function self.login_manager.user_loader(self.load_user) # Setup verify password function for users - user_http_auth.verify_password(self.user_verify_password) + user_http_auth.verify_password(partial(self.user_verify_password, is_login=False)) + user_http_login_auth.verify_password(partial(self.user_verify_password, is_login=True)) user_token_auth.verify_token(self.user_verify_token) user_http_auth.error_handler(self.auth_error) + user_http_login_auth.error_handler(self.auth_error) user_token_auth.error_handler(self.auth_error) # Setup verify password function for participants @@ -162,8 +167,8 @@ def load_user(self, user_id): return None - def user_verify_password(self, username, password): - # print('LoginModule - user_verify_password ', username) + def user_verify_password(self, username, password, is_login): + # print('LoginModule - user_verify_password', username) tentative_user = TeraUser.get_user_by_username(username) if not tentative_user: # self.logger.log_warning(self.module_name, 'Invalid username', username) @@ -207,15 +212,24 @@ def user_verify_password(self, username, password): logged_user = TeraUser.verify_password(username=username, password=password, user=tentative_user) if logged_user and logged_user.is_active(): + + if not is_login: + if logged_user.user_force_password_change: + # Prevent API access if password change was requested for that user + abort(401, gettext('Unauthorized - User must login first to change password')) + if logged_user.user_2fa_enabled: + # Prevent API access with username/password if 2FA is enabled + abort(401, gettext('Unauthorized - 2FA is enabled, must login first and use token')) + g.current_user = logged_user - # print('user_verify_password, found user: ', current_user) - # current_user.update_last_online() + # print('user_verify_password, found user:', current_user) + current_user.update_last_online() # Clear attempts counter self.redisDelete(attempts_key) - login_user(current_user, remember=True) + login_user(current_user, remember=False) # print('Setting key with expiration in 60s', session['_id'], session['_user_id']) # self.redisSet(session['_id'], session['_user_id'], ex=60) return True @@ -321,7 +335,7 @@ def user_verify_token(self, token_value): # TODO: Validate if user is also online? if current_user and current_user.is_active(): # current_user.update_last_online() - login_user(current_user, remember=True) + login_user(current_user, remember=False) return True login_infos = UserAgentParser.parse_request_for_login_infos(request) @@ -341,7 +355,7 @@ def user_verify_token(self, token_value): return False def participant_verify_password(self, username, password): - # print('LoginModule - participant_verify_password for ', username) + # print('LoginModule - participant_verify_password for', username) tentative_participant = TeraParticipant.get_participant_by_username(username) if not tentative_participant: @@ -391,7 +405,7 @@ def participant_verify_password(self, username, password): # print('participant_verify_password, found participant: ', current_participant) # current_participant.update_last_online() - login_user(current_participant, remember=True) + login_user(current_participant, remember=False) # Flag that participant has full API access g.current_participant.fullAccess = True @@ -445,7 +459,7 @@ def participant_verify_token(self, token_value): if current_participant and current_participant.is_active(): # current_participant.update_last_online() g.current_participant.fullAccess = False - login_user(current_participant, remember=True) + login_user(current_participant, remember=False) return True # Second attempt, validate dynamic token @@ -534,7 +548,7 @@ def participant_verify_token(self, token_value): # Flag that participant has full API access g.current_participant.fullAccess = True # current_participant.update_last_online() - login_user(current_participant, remember=True) + login_user(current_participant, remember=False) return True login_infos = UserAgentParser.parse_request_for_login_infos(request) @@ -604,7 +618,7 @@ def decorated(*args, **kwargs): # Device must be found and enabled if current_device: if current_device.device_enabled: - login_user(current_device, remember=True) + login_user(current_device, remember=False) return f(*args, **kwargs) else: login_infos = UserAgentParser.parse_request_for_login_infos(request) @@ -637,7 +651,7 @@ def decorated(*args, **kwargs): if current_device: if current_device.device_enabled: # Returns the function if authenticated with token - login_user(current_device, remember=True) + login_user(current_device, remember=False) return f(*args, **kwargs) else: login_infos = UserAgentParser.parse_request_for_login_infos(request) @@ -664,7 +678,7 @@ def decorated(*args, **kwargs): # Device must be found and enabled if current_device and current_device.device_enabled: # Returns the function if authenticated with token - login_user(current_device, remember=True) + login_user(current_device, remember=False) return f(*args, **kwargs) # Any other case, do not call function since no valid auth found. @@ -796,3 +810,25 @@ def decorated(*args, **kwargs): return gettext('Unauthorized'), 401 return decorated + + @staticmethod + def user_session_required(f): + """ + Use this decorator if a user session is required. A session is created when a user logs in. The session contains + the user UUID. + """ + @wraps(f) + def decorated(*args, **kwargs): + if '_user_id' in session: + # Verify if we have a valid user + user = TeraUser.get_user_by_uuid(session['_user_id']) + if user and user.user_enabled: + g.current_user = user + return f(*args, **kwargs) + else: + return gettext('Unauthorized'), 401 + else: + return gettext('Unauthorized'), 401 + + return decorated + diff --git a/teraserver/python/opentera/db/models/TeraSessionType.py b/teraserver/python/opentera/db/models/TeraSessionType.py index b48e87690..02ef55993 100644 --- a/teraserver/python/opentera/db/models/TeraSessionType.py +++ b/teraserver/python/opentera/db/models/TeraSessionType.py @@ -1,7 +1,7 @@ from opentera.db.Base import BaseModel from opentera.db.SoftDeleteMixin import SoftDeleteMixin from opentera.db.models.TeraSession import TeraSession -from sqlalchemy import Column, ForeignKey, Integer, String, Sequence, Boolean, TIMESTAMP +from sqlalchemy import Column, ForeignKey, Integer, String, Sequence, Boolean from sqlalchemy.orm import relationship from sqlalchemy.exc import IntegrityError from enum import Enum, unique diff --git a/teraserver/python/opentera/db/models/TeraSite.py b/teraserver/python/opentera/db/models/TeraSite.py index a6804a249..13bbd89d4 100644 --- a/teraserver/python/opentera/db/models/TeraSite.py +++ b/teraserver/python/opentera/db/models/TeraSite.py @@ -1,14 +1,14 @@ -from opentera.db.Base import BaseModel -from opentera.db.SoftDeleteMixin import SoftDeleteMixin -from sqlalchemy import Column, Integer, String, Sequence +from sqlalchemy import Column, Integer, Boolean, String, Sequence from sqlalchemy.orm import relationship from sqlalchemy.exc import IntegrityError - +from opentera.db.Base import BaseModel +from opentera.db.SoftDeleteMixin import SoftDeleteMixin class TeraSite(BaseModel, SoftDeleteMixin): __tablename__ = 't_sites' id_site = Column(Integer, Sequence('id_site_sequence'), primary_key=True, autoincrement=True) site_name = Column(String, nullable=False) + site_2fa_required = Column(Boolean, nullable=False, default=False) site_devices = relationship("TeraDevice", secondary="t_devices_sites", back_populates="device_sites") site_projects = relationship("TeraProject", cascade="delete", passive_deletes=True, @@ -22,6 +22,8 @@ class TeraSite(BaseModel, SoftDeleteMixin): site_tests_types = relationship("TeraTestType", secondary="t_tests_types_sites", viewonly=True) + + def to_json(self, ignore_fields=None, minimal=False): if ignore_fields is None: ignore_fields = [] diff --git a/teraserver/python/opentera/db/models/TeraUser.py b/teraserver/python/opentera/db/models/TeraUser.py index ab60555cc..c85c4f5bd 100755 --- a/teraserver/python/opentera/db/models/TeraUser.py +++ b/teraserver/python/opentera/db/models/TeraUser.py @@ -13,11 +13,14 @@ from passlib.hash import bcrypt +from enum import Enum, unique import uuid import datetime import json import time import jwt +import re +import pyotp # Generator for jti @@ -46,6 +49,12 @@ class TeraUser(BaseModel, SoftDeleteMixin): user_notes = Column(String, nullable=True) user_lastonline = Column(TIMESTAMP(timezone=True), nullable=True) user_superadmin = Column(Boolean, nullable=False, default=False) + # Fields added for 2FA + user_2fa_enabled = Column(Boolean, nullable=False, default=False) + user_2fa_otp_enabled = Column(Boolean, nullable=False, default=False) + user_2fa_email_enabled = Column(Boolean, nullable=False, default=False) + user_2fa_otp_secret = Column(String, nullable=True) + user_force_password_change = Column(Boolean, nullable=False, default=False) # user_sites_access = relationship('TeraSiteAccess', cascade="all,delete") # user_projects_access = relationship("TeraProjectAccess", cascade="all,delete") @@ -68,10 +77,11 @@ def to_json(self, ignore_fields=None, minimal=False): if ignore_fields is None: ignore_fields = [] ignore_fields.extend(['authenticated', 'user_password', 'user_user_groups', - 'user_sessions']) + 'user_sessions', 'user_2fa_otp_secret']) if minimal: ignore_fields.extend(['user_username', 'user_email', 'user_profile', 'user_notes', 'user_lastonline', - 'user_superadmin']) + 'user_superadmin' 'user_2fa_enabled', 'user_2fa_otp_enabled', + 'user_2fa_email_enabled', 'user_force_password_change']) rval = super().to_json(ignore_fields=ignore_fields) rval['user_name'] = self.get_fullname() return rval @@ -121,6 +131,27 @@ def get_token(self, token_key: str, expiration: int = 3600): return jwt.encode(payload, token_key, algorithm='HS256') + def enable_2fa_otp(self) -> bool: + if self.user_2fa_enabled and self.user_2fa_otp_enabled and self.user_2fa_otp_secret: + return False + + self.user_2fa_enabled = True + self.user_2fa_otp_enabled = True + self.user_2fa_email_enabled = False + self.user_2fa_otp_secret = pyotp.random_base32() + return True + + def verify_2fa(self, code: str) -> bool: + if not self.user_2fa_enabled: + return False + + if self.user_2fa_otp_enabled: + # Default is 6 digits with interval of 30 seconds + totp = pyotp.TOTP(self.user_2fa_otp_secret) + return totp.verify(code, valid_window=1) + + return False + def get_service_access_dict(self): service_access = {} @@ -297,8 +328,20 @@ def update(cls, id_user: int, values: dict): # Remove the password field is present and if empty if 'user_password' in values: if values['user_password'] == '': - del values['user_password'] + del values['user_password'] # Don't change password if empty else: + # Check password strength + password_errors = TeraUser.validate_password_strength(str(values['user_password'])) + if len(password_errors) > 0: + raise UserPasswordInsecure("User password insufficient strength", password_errors) + + # Check that old password != new password + current_user = TeraUser.get_user_by_id(id_user) + if current_user: + if TeraUser.verify_password('', values['user_password'], current_user): + # Same password as before + raise UserNewPasswordSameAsOld("New password same as old") + # Forcing password to string values['user_password'] = TeraUser.encrypt_password(str(values['user_password'])) @@ -315,8 +358,12 @@ def update(cls, id_user: int, values: dict): @classmethod def insert(cls, user): + # Check password strength + password_errors = TeraUser.validate_password_strength(str(user.user_password)) + if len(password_errors) > 0: + raise UserPasswordInsecure("User password insufficient strength", password_errors) + # Encrypts password - # Forcing password to string user.user_password = TeraUser.encrypt_password(str(user.user_password)) # Generate UUID @@ -346,6 +393,27 @@ def delete_check_integrity(self, with_deleted: bool = False) -> IntegrityError | return None + @staticmethod + def validate_password_strength(password: str) -> list: + errors = [] + + if len(password) < 10: + errors.append(UserPasswordInsecure.PasswordWeaknesses.BAD_LENGTH) + + if re.search(r"\d", password) is None: + errors.append(UserPasswordInsecure.PasswordWeaknesses.NO_NUMERIC) + + if re.search(r"[A-Z]", password) is None: + errors.append(UserPasswordInsecure.PasswordWeaknesses.NO_UPPER_CASE) + + if re.search(r"[a-z]", password) is None: + errors.append(UserPasswordInsecure.PasswordWeaknesses.NO_LOWER_CASE) + + if re.search(r"[ !#$%&'()*+,-./[\\\]^_`{|}~"+r'"]', password) is None: + errors.append(UserPasswordInsecure.PasswordWeaknesses.NO_SPECIAL) + + return errors + @staticmethod def create_defaults(test=False): # Admin @@ -355,6 +423,8 @@ def create_defaults(test=False): admin.user_lastname = "Admin" admin.user_profile = "" admin.user_password = TeraUser.encrypt_password("admin") + # Force reset password for admin on first login + admin.user_force_password_change = not test admin.user_superadmin = True admin.user_username = "admin" admin.user_uuid = str(uuid.uuid4()) @@ -430,3 +500,30 @@ def create_defaults(test=False): def get_undelete_cascade_relations(self) -> list: return ['user_service_config'] + + +class UserPasswordInsecure(Exception): + """ + Raised when the user password doesn't meet minimal requirements + """ + + @unique + class PasswordWeaknesses(Enum): + BAD_LENGTH = 1 + NO_LOWER_CASE = 2 + NO_UPPER_CASE = 3 + NO_NUMERIC = 4 + NO_SPECIAL = 5 + + def __init__(self, message, weaknesses: list): + super().__init__(message) + self.weaknesses = weaknesses + + +class UserNewPasswordSameAsOld(Exception): + """ + Raised when the new password is equal to the old one + """ + def __init__(self, message): + super().__init__(message) + diff --git a/teraserver/python/opentera/db/models/__init__.py b/teraserver/python/opentera/db/models/__init__.py index c0d5f08e3..e1bdb0e6d 100644 --- a/teraserver/python/opentera/db/models/__init__.py +++ b/teraserver/python/opentera/db/models/__init__.py @@ -33,10 +33,10 @@ from .TeraUserGroup import TeraUserGroup from .TeraUserUserGroup import TeraUserUserGroup from .TeraUserPreference import TeraUserPreference -from .TeraUserUserGroup import TeraUserUserGroup + """ - A map containing the event name and class, useful for event filtering. + A map containing the event name and class, useful for event filtering. Insert only useful events here. """ EventNameClassMap = { diff --git a/teraserver/python/opentera/forms/TeraSiteForm.py b/teraserver/python/opentera/forms/TeraSiteForm.py index edbdf1a40..5074db0ec 100644 --- a/teraserver/python/opentera/forms/TeraSiteForm.py +++ b/teraserver/python/opentera/forms/TeraSiteForm.py @@ -5,8 +5,9 @@ class TeraSiteForm: @staticmethod - def get_site_form(): + def get_site_form(**kwargs): form = TeraForm("site") + show_2fa_fields = kwargs.get('show_2fa_fields', True) # Sections section = TeraFormSection("informations", gettext("Information")) @@ -15,6 +16,9 @@ def get_site_form(): # Items section.add_item(TeraFormItem("id_site", gettext("Site ID"), "hidden", True)) section.add_item(TeraFormItem("site_name", gettext("Site Name"), "text", True)) + if show_2fa_fields: + section.add_item(TeraFormItem("site_2fa_required", gettext("Users Require 2FA"), "boolean", + False, item_default=False)) section.add_item(TeraFormItem("site_role", gettext("Site Role"), "hidden", False)) return form.to_dict() diff --git a/teraserver/python/opentera/forms/TeraUserForm.py b/teraserver/python/opentera/forms/TeraUserForm.py index 7811b35c8..68113a295 100644 --- a/teraserver/python/opentera/forms/TeraUserForm.py +++ b/teraserver/python/opentera/forms/TeraUserForm.py @@ -6,8 +6,9 @@ class TeraUserForm: @staticmethod - def get_user_form(): + def get_user_form(**kwargs): form = TeraForm("user") + show_2fa_fields = kwargs.get('show_2fa_fields', True) # Sections section = TeraFormSection("informations", gettext("Information")) @@ -18,16 +19,34 @@ def get_user_form(): section.add_item(TeraFormItem("user_uuid", gettext("User UUID"), "hidden")) section.add_item(TeraFormItem("user_name", gettext("User Full Name"), "hidden")) section.add_item(TeraFormItem("user_username", gettext("Username"), "text", True)) - section.add_item(TeraFormItem("user_enabled", gettext("User Enabled"), "boolean", True, item_default=True)) + section.add_item(TeraFormItem("user_enabled", gettext("User Enabled"), "boolean", + True, item_default=True)) + + if show_2fa_fields: + section.add_item(TeraFormItem("user_force_password_change", gettext("Force password change"), + "boolean", False, item_default=False)) + section.add_item(TeraFormItem("user_2fa_enabled", gettext("2FA Enabled"), "boolean", + False, item_default=False)) + + section.add_item(TeraFormItem("user_2fa_otp_enabled", gettext("2FA OTP Enabled"), "boolean", + False, item_default=False, + item_condition=TeraFormItemCondition("user_2fa_enabled", "=", True))) + section.add_item(TeraFormItem("user_2fa_email_enabled", gettext("2FA Email Enabled"), "hidden", + False, item_default=False, + # item_condition = TeraFormItemCondition("user_2fa_enabled", "=", True) + )) + + # section.add_item(TeraFormItem("user_2fa_otp_secret", gettext("OTP Secret"), "hidden")) section.add_item(TeraFormItem("user_firstname", gettext("First Name"), "text", True)) section.add_item(TeraFormItem("user_lastname", gettext("Last Name"), "text", True)) section.add_item(TeraFormItem("user_email", gettext("Email"), "text")) section.add_item( TeraFormItem("user_password", gettext("Password"), "password", item_options={"confirm": True})) section.add_item(TeraFormItem("user_superadmin", gettext("User Is Super Administrator"), "boolean", True)) - section.add_item(TeraFormItem("user_notes", gettext("Notes"), "longtext")) - section.add_item(TeraFormItem("user_profile", gettext("Profile"), "hidden")) section.add_item(TeraFormItem("user_lastonline", gettext("Last Connection"), "datetime", item_options={"readonly": True})) + section.add_item(TeraFormItem("user_notes", gettext("Notes"), "longtext")) + section.add_item(TeraFormItem("user_profile", gettext("Profile"), "hidden")) + return form.to_dict() diff --git a/teraserver/python/opentera/redis/RedisVars.py b/teraserver/python/opentera/redis/RedisVars.py index b51e73b2c..4c114ef83 100644 --- a/teraserver/python/opentera/redis/RedisVars.py +++ b/teraserver/python/opentera/redis/RedisVars.py @@ -25,6 +25,9 @@ class RedisVars: # User login attempt counter prefix RedisVar_UserLoginAttemptKey = "UserLoginAttempts." + # User 2FA attempt counter prefix + RedisVar_User2FALoginAttemptKey = "User2FALoginAttempts." + # Participant login attempt counter prefix RedisVar_ParticipantLoginAttemptKey = "ParticipantLoginAttempts." diff --git a/teraserver/python/services/FileTransferService/translations/en/LC_MESSAGES/filetransferservice.po b/teraserver/python/services/FileTransferService/translations/en/LC_MESSAGES/filetransferservice.po index 5033c2106..fab8143f7 100644 --- a/teraserver/python/services/FileTransferService/translations/en/LC_MESSAGES/filetransferservice.po +++ b/teraserver/python/services/FileTransferService/translations/en/LC_MESSAGES/filetransferservice.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-05-13 11:02-0400\n" +"POT-Creation-Date: 2024-10-07 15:05-0400\n" "PO-Revision-Date: 2021-01-19 16:16-0500\n" "Last-Translator: FULL NAME \n" "Language: en\n" @@ -16,7 +16,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.15.0\n" +"Generated-By: Babel 2.16.0\n" #: API/QueryArchiveFile.py:53 API/QueryArchiveFile.py:109 #: API/QueryArchiveFile.py:172 API/QueryArchiveFileInfos.py:64 diff --git a/teraserver/python/services/FileTransferService/translations/fr/LC_MESSAGES/filetransferservice.po b/teraserver/python/services/FileTransferService/translations/fr/LC_MESSAGES/filetransferservice.po index 29598999f..8cd9c6fe3 100644 --- a/teraserver/python/services/FileTransferService/translations/fr/LC_MESSAGES/filetransferservice.po +++ b/teraserver/python/services/FileTransferService/translations/fr/LC_MESSAGES/filetransferservice.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-05-13 11:02-0400\n" +"POT-Creation-Date: 2024-10-07 15:05-0400\n" "PO-Revision-Date: 2023-02-28 08:22-0500\n" "Last-Translator: \n" "Language: fr\n" @@ -16,7 +16,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.15.0\n" +"Generated-By: Babel 2.16.0\n" #: API/QueryArchiveFile.py:53 API/QueryArchiveFile.py:109 #: API/QueryArchiveFile.py:172 API/QueryArchiveFileInfos.py:64 diff --git a/teraserver/python/services/LoggingService/translations/en/LC_MESSAGES/loggingservice.po b/teraserver/python/services/LoggingService/translations/en/LC_MESSAGES/loggingservice.po index 4e9dde9d2..86c7e823e 100644 --- a/teraserver/python/services/LoggingService/translations/en/LC_MESSAGES/loggingservice.po +++ b/teraserver/python/services/LoggingService/translations/en/LC_MESSAGES/loggingservice.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-05-13 11:02-0400\n" +"POT-Creation-Date: 2024-10-07 15:05-0400\n" "PO-Revision-Date: 2023-01-26 13:29-0500\n" "Last-Translator: FULL NAME \n" "Language: en\n" @@ -16,7 +16,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.15.0\n" +"Generated-By: Babel 2.16.0\n" #: API/QueryLogEntries.py:86 API/QueryLoginEntries.py:163 msgid "Database error: " diff --git a/teraserver/python/services/LoggingService/translations/fr/LC_MESSAGES/loggingservice.po b/teraserver/python/services/LoggingService/translations/fr/LC_MESSAGES/loggingservice.po index 05233069f..389878668 100644 --- a/teraserver/python/services/LoggingService/translations/fr/LC_MESSAGES/loggingservice.po +++ b/teraserver/python/services/LoggingService/translations/fr/LC_MESSAGES/loggingservice.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-05-13 11:02-0400\n" +"POT-Creation-Date: 2024-10-07 15:05-0400\n" "PO-Revision-Date: 2023-02-28 08:10-0500\n" "Last-Translator: \n" "Language: fr\n" @@ -16,7 +16,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.15.0\n" +"Generated-By: Babel 2.16.0\n" #: API/QueryLogEntries.py:86 API/QueryLoginEntries.py:163 msgid "Database error: " diff --git a/teraserver/python/services/VideoRehabService/translations/en/LC_MESSAGES/videorehabservice.po b/teraserver/python/services/VideoRehabService/translations/en/LC_MESSAGES/videorehabservice.po index 6d0d959f6..3374100e4 100644 --- a/teraserver/python/services/VideoRehabService/translations/en/LC_MESSAGES/videorehabservice.po +++ b/teraserver/python/services/VideoRehabService/translations/en/LC_MESSAGES/videorehabservice.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-05-13 11:02-0400\n" +"POT-Creation-Date: 2024-10-07 15:05-0400\n" "PO-Revision-Date: 2021-01-19 16:16-0500\n" "Last-Translator: FULL NAME \n" "Language: en\n" @@ -16,7 +16,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.15.0\n" +"Generated-By: Babel 2.16.0\n" #: VideoRehabService.py:44 msgid "General configuration" diff --git a/teraserver/python/services/VideoRehabService/translations/fr/LC_MESSAGES/videorehabservice.po b/teraserver/python/services/VideoRehabService/translations/fr/LC_MESSAGES/videorehabservice.po index d3e5bc032..2c75d6a80 100644 --- a/teraserver/python/services/VideoRehabService/translations/fr/LC_MESSAGES/videorehabservice.po +++ b/teraserver/python/services/VideoRehabService/translations/fr/LC_MESSAGES/videorehabservice.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-05-13 11:02-0400\n" +"POT-Creation-Date: 2024-10-07 15:05-0400\n" "PO-Revision-Date: 2023-05-23 14:29-0400\n" "Last-Translator: \n" "Language: fr\n" @@ -16,7 +16,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.15.0\n" +"Generated-By: Babel 2.16.0\n" #: VideoRehabService.py:44 msgid "General configuration" diff --git a/teraserver/python/static/bootstrap/css/bootstrap.min.css b/teraserver/python/static/bootstrap/css/bootstrap.min.css index 83a71b1f5..39934146f 100644 --- a/teraserver/python/static/bootstrap/css/bootstrap.min.css +++ b/teraserver/python/static/bootstrap/css/bootstrap.min.css @@ -1,7 +1,6 @@ -/*! - * Bootstrap v4.6.2 (https://getbootstrap.com/) - * Copyright 2011-2022 The Bootstrap Authors - * Copyright 2011-2022 Twitter, Inc. +@charset "UTF-8";/*! + * Bootstrap v5.3.3 (https://getbootstrap.com/) + * Copyright 2011-2024 The Bootstrap Authors * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([class]){color:inherit;text-decoration:none}a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit;text-align:-webkit-match-parent}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:.875em;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-sm-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-sm-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-md-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-md-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-md-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-md-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-md-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-md-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-lg-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-lg-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-xl-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-xl-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;margin-bottom:1rem;color:#212529}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{color:#212529;background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#7abaff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#b3b7bb}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#8fd19e}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#86cfda}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#ffdf7e}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#ed969e}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#fbfcfc}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#95999c}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#343a40;border-color:#454d55}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#343a40}.table-dark td,.table-dark th,.table-dark thead th{border-color:#454d55}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}input[type=date].form-control,input[type=datetime-local].form-control,input[type=month].form-control,input[type=time].form-control{-webkit-appearance:none;-moz-appearance:none;appearance:none}select.form-control:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;font-size:1rem;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#28a745}.valid-tooltip{position:absolute;top:100%;left:0;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,.9);border-radius:.25rem}.form-row>.col>.valid-tooltip,.form-row>[class*=col-]>.valid-tooltip{left:5px}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#28a745;padding-right:calc(1.5em + .75rem)!important;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.was-validated select.form-control:valid,select.form-control.is-valid{padding-right:3rem!important;background-position:right 1.5rem center}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-valid,.was-validated .custom-select:valid{border-color:#28a745;padding-right:calc(.75em + 2.3125rem)!important;background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right .75rem center/8px 10px no-repeat,#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem) no-repeat}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{border-color:#28a745}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{border-color:#34ce57;background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label::before{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;left:0;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.form-row>.col>.invalid-tooltip,.form-row>[class*=col-]>.invalid-tooltip{left:5px}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem)!important;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.was-validated select.form-control:invalid,select.form-control.is-invalid{padding-right:3rem!important;background-position:right 1.5rem center}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{border-color:#dc3545;padding-right:calc(.75em + 2.3125rem)!important;background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right .75rem center/8px 10px no-repeat,#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem) no-repeat}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{border-color:#dc3545}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{border-color:#e4606d;background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label::before{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;-ms-flex-negative:0;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#212529;text-align:center;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529;text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}.btn:not(:disabled):not(.disabled){cursor:pointer}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#0069d9;border-color:#0062cc;box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{color:#fff;background-color:#5a6268;border-color:#545b62;box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#218838;border-color:#1e7e34;box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#138496;border-color:#117a8b;box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{color:#212529;background-color:#e0a800;border-color:#d39e00;box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c82333;border-color:#bd2130;box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{color:#212529;background-color:#e2e6ea;border-color:#dae0e5;box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{color:#fff;background-color:#23272b;border-color:#1d2124;box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-outline-primary{color:#007bff;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;text-decoration:none}.btn-link:hover{color:#0056b3;text-decoration:underline}.btn-link.focus,.btn-link:focus{text-decoration:underline}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.width{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.width{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;min-width:0;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group:not(.has-validation)>.custom-file:not(:last-child) .custom-file-label,.input-group:not(.has-validation)>.custom-file:not(:last-child) .custom-file-label::after,.input-group:not(.has-validation)>.custom-select:not(:last-child),.input-group:not(.has-validation)>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.custom-file:nth-last-child(n+3) .custom-file-label,.input-group.has-validation>.custom-file:nth-last-child(n+3) .custom-file-label::after,.input-group.has-validation>.custom-select:nth-last-child(n+3),.input-group.has-validation>.form-control:nth-last-child(n+3){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-append,.input-group-prepend{display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(1.5em + 1rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.5em + .5rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group.has-validation>.input-group-append:nth-last-child(n+3)>.btn,.input-group.has-validation>.input-group-append:nth-last-child(n+3)>.input-group-text,.input-group:not(.has-validation)>.input-group-append:not(:last-child)>.btn,.input-group:not(.has-validation)>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;z-index:1;display:block;min-height:1.5rem;padding-left:1.5rem;-webkit-print-color-adjust:exact;color-adjust:exact;print-color-adjust:exact}.custom-control-inline{display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;left:0;z-index:-1;width:1rem;height:1.25rem;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;border-color:#007bff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:focus:not(:checked)~.custom-control-label::before{border-color:#80bdff}.custom-control-input:not(:disabled):active~.custom-control-label::before{color:#fff;background-color:#b3d7ff;border-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label,.custom-control-input[disabled]~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before,.custom-control-input[disabled]~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:1px solid #adb5bd}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:50%/50% 50% no-repeat}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{border-color:#007bff;background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label::after{background-color:#fff;-webkit-transform:translateX(.75rem);transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right .75rem center/8px 10px no-repeat;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{display:none}.custom-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + .75rem + 2px);margin:0;overflow:hidden;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:disabled~.custom-file-label,.custom-file-input[disabled]~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;overflow:hidden;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:1.4rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{-moz-transition:none;transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#007bff;border:0;border-radius:1rem;-ms-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{-ms-transition:none;transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;background-color:transparent;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{background:0 0;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item,.nav-fill>.nav-link{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar .container,.navbar .container-fluid,.navbar .container-lg,.navbar .container-md,.navbar .container-sm,.navbar .container-xl{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:50%/100% 100% no-repeat}.navbar-nav-scroll{max-height:75vh;overflow-y:auto}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;min-height:1px;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{-ms-flex-negative:0;flex-shrink:0;width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{-ms-flex:1 0 0%;flex:1 0 0%;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion{overflow-anchor:none}.accordion>.card{overflow:hidden}.accordion>.card:not(:last-of-type){border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:not(:first-of-type){border-top-left-radius:0;border-top-right-radius:0}.accordion>.card>.card-header{border-radius:0;margin-bottom:-1px}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:3;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}a.badge-primary:focus,a.badge-primary:hover{color:#fff;background-color:#0062cc}a.badge-primary.focus,a.badge-primary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.badge-secondary{color:#fff;background-color:#6c757d}a.badge-secondary:focus,a.badge-secondary:hover{color:#fff;background-color:#545b62}a.badge-secondary.focus,a.badge-secondary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.badge-success{color:#fff;background-color:#28a745}a.badge-success:focus,a.badge-success:hover{color:#fff;background-color:#1e7e34}a.badge-success.focus,a.badge-success:focus{outline:0;box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.badge-info{color:#fff;background-color:#17a2b8}a.badge-info:focus,a.badge-info:hover{color:#fff;background-color:#117a8b}a.badge-info.focus,a.badge-info:focus{outline:0;box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.badge-warning{color:#212529;background-color:#ffc107}a.badge-warning:focus,a.badge-warning:hover{color:#212529;background-color:#d39e00}a.badge-warning.focus,a.badge-warning:focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.badge-danger{color:#fff;background-color:#dc3545}a.badge-danger:focus,a.badge-danger:hover{color:#fff;background-color:#bd2130}a.badge-danger.focus,a.badge-danger:focus{outline:0;box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.badge-light{color:#212529;background-color:#f8f9fa}a.badge-light:focus,a.badge-light:hover{color:#212529;background-color:#dae0e5}a.badge-light.focus,a.badge-light:focus{outline:0;box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.badge-dark{color:#fff;background-color:#343a40}a.badge-dark:focus,a.badge-dark:hover{color:#fff;background-color:#1d2124}a.badge-dark.focus,a.badge-dark:focus{outline:0;box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;z-index:2;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;line-height:0;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{opacity:.75}button.close{padding:0;background-color:transparent;border:0}a.close.disabled{pointer-events:none}.toast{-ms-flex-preferred-size:350px;flex-basis:350px;max-width:350px;font-size:.875rem;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .25rem .75rem rgba(0,0,0,.1);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.25rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-50px);transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal.modal-static .modal-dialog{-webkit-transform:scale(1.02);transform:scale(1.02)}.modal-dialog-scrollable{display:-ms-flexbox;display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{-ms-flex-negative:0;flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);height:-webkit-min-content;height:-moz-min-content;height:min-content;content:""}.modal-dialog-centered.modal-dialog-scrollable{-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem);height:-webkit-min-content;height:-moz-min-content;height:min-content}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top]>.arrow,.bs-popover-top>.arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=top]>.arrow::before,.bs-popover-top>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right]>.arrow,.bs-popover-right>.arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right]>.arrow::before,.bs-popover-right>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom]>.arrow,.bs-popover-bottom>.arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=bottom]>.arrow::before,.bs-popover-bottom>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left]>.arrow,.bs-popover-left>.arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left]>.arrow::before,.bs-popover-left>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{-ms-touch-action:pan-y;touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){-webkit-transform:translateX(100%);transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){-webkit-transform:translateX(-100%);transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;-webkit-transform:none;transform:none}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:50%/100% 100% no-repeat}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid currentcolor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1;-webkit-transform:none;transform:none}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1;-webkit-transform:none;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentcolor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded-sm{border-radius:.2rem!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-lg{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;-ms-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;-ms-user-select:none!important;user-select:none!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace!important}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0056b3!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#494f54!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#19692c!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#0f6674!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#ba8b00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#a71d2a!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#cbd3da!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#121416!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none!important}.text-break{word-break:break-word!important;word-wrap:break-word!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}} + */:root,[data-bs-theme=light]{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-primary-text-emphasis:#052c65;--bs-secondary-text-emphasis:#2b2f32;--bs-success-text-emphasis:#0a3622;--bs-info-text-emphasis:#055160;--bs-warning-text-emphasis:#664d03;--bs-danger-text-emphasis:#58151c;--bs-light-text-emphasis:#495057;--bs-dark-text-emphasis:#495057;--bs-primary-bg-subtle:#cfe2ff;--bs-secondary-bg-subtle:#e2e3e5;--bs-success-bg-subtle:#d1e7dd;--bs-info-bg-subtle:#cff4fc;--bs-warning-bg-subtle:#fff3cd;--bs-danger-bg-subtle:#f8d7da;--bs-light-bg-subtle:#fcfcfd;--bs-dark-bg-subtle:#ced4da;--bs-primary-border-subtle:#9ec5fe;--bs-secondary-border-subtle:#c4c8cb;--bs-success-border-subtle:#a3cfbb;--bs-info-border-subtle:#9eeaf9;--bs-warning-border-subtle:#ffe69c;--bs-danger-border-subtle:#f1aeb5;--bs-light-border-subtle:#e9ecef;--bs-dark-border-subtle:#adb5bd;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-color-rgb:33,37,41;--bs-body-bg:#fff;--bs-body-bg-rgb:255,255,255;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(33, 37, 41, 0.75);--bs-secondary-color-rgb:33,37,41;--bs-secondary-bg:#e9ecef;--bs-secondary-bg-rgb:233,236,239;--bs-tertiary-color:rgba(33, 37, 41, 0.5);--bs-tertiary-color-rgb:33,37,41;--bs-tertiary-bg:#f8f9fa;--bs-tertiary-bg-rgb:248,249,250;--bs-heading-color:inherit;--bs-link-color:#0d6efd;--bs-link-color-rgb:13,110,253;--bs-link-decoration:underline;--bs-link-hover-color:#0a58ca;--bs-link-hover-color-rgb:10,88,202;--bs-code-color:#d63384;--bs-highlight-color:#212529;--bs-highlight-bg:#fff3cd;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(13, 110, 253, 0.25);--bs-form-valid-color:#198754;--bs-form-valid-border-color:#198754;--bs-form-invalid-color:#dc3545;--bs-form-invalid-border-color:#dc3545}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#dee2e6;--bs-body-color-rgb:222,226,230;--bs-body-bg:#212529;--bs-body-bg-rgb:33,37,41;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255,255,255;--bs-secondary-color:rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb:222,226,230;--bs-secondary-bg:#343a40;--bs-secondary-bg-rgb:52,58,64;--bs-tertiary-color:rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb:222,226,230;--bs-tertiary-bg:#2b3035;--bs-tertiary-bg-rgb:43,48,53;--bs-primary-text-emphasis:#6ea8fe;--bs-secondary-text-emphasis:#a7acb1;--bs-success-text-emphasis:#75b798;--bs-info-text-emphasis:#6edff6;--bs-warning-text-emphasis:#ffda6a;--bs-danger-text-emphasis:#ea868f;--bs-light-text-emphasis:#f8f9fa;--bs-dark-text-emphasis:#dee2e6;--bs-primary-bg-subtle:#031633;--bs-secondary-bg-subtle:#161719;--bs-success-bg-subtle:#051b11;--bs-info-bg-subtle:#032830;--bs-warning-bg-subtle:#332701;--bs-danger-bg-subtle:#2c0b0e;--bs-light-bg-subtle:#343a40;--bs-dark-bg-subtle:#1a1d20;--bs-primary-border-subtle:#084298;--bs-secondary-border-subtle:#41464b;--bs-success-border-subtle:#0f5132;--bs-info-border-subtle:#087990;--bs-warning-border-subtle:#997404;--bs-danger-border-subtle:#842029;--bs-light-border-subtle:#495057;--bs-dark-border-subtle:#343a40;--bs-heading-color:inherit;--bs-link-color:#6ea8fe;--bs-link-hover-color:#8bb9fe;--bs-link-color-rgb:110,168,254;--bs-link-hover-color-rgb:139,185,254;--bs-code-color:#e685b5;--bs-highlight-color:#dee2e6;--bs-highlight-bg:#664d03;--bs-border-color:#495057;--bs-border-color-translucent:rgba(255, 255, 255, 0.15);--bs-form-valid-color:#75b798;--bs-form-valid-border-color:#75b798;--bs-form-invalid-color:#ea868f;--bs-form-invalid-border-color:#ea868f}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:var(--bs-border-width) solid;opacity:.25}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2;color:var(--bs-heading-color)}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.1875em;color:var(--bs-highlight-color);background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1));text-decoration:underline}a:hover{--bs-link-color-rgb:var(--bs-link-hover-color-rgb)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-secondary-color);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:var(--bs-body-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:var(--bs-secondary-color)}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--bs-gutter-x:1.5rem;--bs-gutter-y:0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}:root{--bs-breakpoint-xs:0;--bs-breakpoint-sm:576px;--bs-breakpoint-md:768px;--bs-breakpoint-lg:992px;--bs-breakpoint-xl:1200px;--bs-breakpoint-xxl:1400px}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.66666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.66666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.66666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.66666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.66666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.66666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-color-type:initial;--bs-table-bg-type:initial;--bs-table-color-state:initial;--bs-table-bg-state:initial;--bs-table-color:var(--bs-emphasis-color);--bs-table-bg:var(--bs-body-bg);--bs-table-border-color:var(--bs-border-color);--bs-table-accent-bg:transparent;--bs-table-striped-color:var(--bs-emphasis-color);--bs-table-striped-bg:rgba(var(--bs-emphasis-color-rgb), 0.05);--bs-table-active-color:var(--bs-emphasis-color);--bs-table-active-bg:rgba(var(--bs-emphasis-color-rgb), 0.1);--bs-table-hover-color:var(--bs-emphasis-color);--bs-table-hover-bg:rgba(var(--bs-emphasis-color-rgb), 0.075);width:100%;margin-bottom:1rem;vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem .5rem;color:var(--bs-table-color-state,var(--bs-table-color-type,var(--bs-table-color)));background-color:var(--bs-table-bg);border-bottom-width:var(--bs-border-width);box-shadow:inset 0 0 0 9999px var(--bs-table-bg-state,var(--bs-table-bg-type,var(--bs-table-accent-bg)))}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:calc(var(--bs-border-width) * 2) solid currentcolor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:var(--bs-border-width) 0}.table-bordered>:not(caption)>*>*{border-width:0 var(--bs-border-width)}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-striped-columns>:not(caption)>tr>:nth-child(2n){--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-active{--bs-table-color-state:var(--bs-table-active-color);--bs-table-bg-state:var(--bs-table-active-bg)}.table-hover>tbody>tr:hover>*{--bs-table-color-state:var(--bs-table-hover-color);--bs-table-bg-state:var(--bs-table-hover-bg)}.table-primary{--bs-table-color:#000;--bs-table-bg:#cfe2ff;--bs-table-border-color:#a6b5cc;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color:#000;--bs-table-bg:#e2e3e5;--bs-table-border-color:#b5b6b7;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color:#000;--bs-table-bg:#d1e7dd;--bs-table-border-color:#a7b9b1;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color:#000;--bs-table-bg:#cff4fc;--bs-table-border-color:#a6c3ca;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color:#000;--bs-table-bg:#fff3cd;--bs-table-border-color:#ccc2a4;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color:#000;--bs-table-bg:#f8d7da;--bs-table-border-color:#c6acae;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color:#000;--bs-table-bg:#f8f9fa;--bs-table-border-color:#c6c7c8;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color:#fff;--bs-table-bg:#212529;--bs-table-border-color:#4d5154;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + var(--bs-border-width));padding-bottom:calc(.375rem + var(--bs-border-width));margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + var(--bs-border-width));padding-bottom:calc(.5rem + var(--bs-border-width));font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + var(--bs-border-width));padding-bottom:calc(.25rem + var(--bs-border-width));font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:var(--bs-secondary-color)}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-clip:padding-box;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:var(--bs-body-color);background-color:var(--bs-body-bg);border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{min-width:85px;height:1.5em;margin:0}.form-control::-webkit-datetime-edit{display:block;padding:0}.form-control::-moz-placeholder{color:var(--bs-secondary-color);opacity:1}.form-control::placeholder{color:var(--bs-secondary-color);opacity:1}.form-control:disabled{background-color:var(--bs-secondary-bg);opacity:1}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:var(--bs-secondary-bg)}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:var(--bs-secondary-bg)}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:var(--bs-body-color);background-color:transparent;border:solid transparent;border-width:var(--bs-border-width) 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2));padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2))}textarea.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}textarea.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-control-color{width:3rem;height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2));padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color::-webkit-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color.form-control-sm{height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}.form-control-color.form-control-lg{height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-image:var(--bs-form-select-bg-img),var(--bs-form-select-bg-icon,none);background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:var(--bs-secondary-bg)}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 var(--bs-body-color)}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}[data-bs-theme=dark] .form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e")}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-reverse{padding-right:1.5em;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-1.5em;margin-left:0}.form-check-input{--bs-form-check-bg:var(--bs-body-bg);flex-shrink:0;width:1em;height:1em;margin-top:.25em;vertical-align:top;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-form-check-bg);background-image:var(--bs-form-check-bg-image);background-repeat:no-repeat;background-position:center;background-size:contain;border:var(--bs-border-width) solid var(--bs-border-color);-webkit-print-color-adjust:exact;color-adjust:exact;print-color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{cursor:default;opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");width:2em;margin-left:-2.5em;background-image:var(--bs-form-switch-bg);background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e")}.form-range{width:100%;height:1.5rem;padding:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;-webkit-appearance:none;appearance:none;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-secondary-bg);border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;-moz-appearance:none;appearance:none;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-secondary-bg);border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:var(--bs-secondary-color)}.form-range:disabled::-moz-range-thumb{background-color:var(--bs-secondary-color)}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + calc(var(--bs-border-width) * 2));min-height:calc(3.5rem + calc(var(--bs-border-width) * 2));line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;z-index:2;height:100%;padding:1rem .75rem;overflow:hidden;text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:var(--bs-border-width) solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem .75rem}.form-floating>.form-control-plaintext::-moz-placeholder,.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control-plaintext::placeholder,.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control-plaintext:not(:-moz-placeholder-shown),.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown),.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:-webkit-autofill,.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label,.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:not(:-moz-placeholder-shown)~label::after{position:absolute;inset:1rem 0.375rem;z-index:-1;height:1.5em;content:"";background-color:var(--bs-body-bg);border-radius:var(--bs-border-radius)}.form-floating>.form-control-plaintext~label::after,.form-floating>.form-control:focus~label::after,.form-floating>.form-control:not(:placeholder-shown)~label::after,.form-floating>.form-select~label::after{position:absolute;inset:1rem 0.375rem;z-index:-1;height:1.5em;content:"";background-color:var(--bs-body-bg);border-radius:var(--bs-border-radius)}.form-floating>.form-control:-webkit-autofill~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label{border-width:var(--bs-border-width) 0}.form-floating>.form-control:disabled~label,.form-floating>:disabled~label{color:#6c757d}.form-floating>.form-control:disabled~label::after,.form-floating>:disabled~label::after{background-color:var(--bs-secondary-bg)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-floating,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-floating:focus-within,.input-group>.form-select:focus{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);text-align:center;white-space:nowrap;background-color:var(--bs-tertiary-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius)}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-control,.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-select,.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-control,.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-select,.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:calc(var(--bs-border-width) * -1);border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.form-floating:not(:first-child)>.form-control,.input-group>.form-floating:not(:first-child)>.form-select{border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-valid-color)}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-success);border-radius:var(--bs-border-radius)}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:var(--bs-form-valid-border-color);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:var(--bs-form-valid-border-color)}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-control-color.is-valid,.was-validated .form-control-color:valid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:var(--bs-form-valid-border-color)}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:var(--bs-form-valid-color)}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:var(--bs-form-valid-color)}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-valid,.input-group>.form-floating:not(:focus-within).is-valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-control:not(:focus):valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.was-validated .input-group>.form-select:not(:focus):valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-invalid-color)}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-danger);border-radius:var(--bs-border-radius)}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:var(--bs-form-invalid-border-color);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:var(--bs-form-invalid-border-color)}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-control-color.is-invalid,.was-validated .form-control-color:invalid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:var(--bs-form-invalid-border-color)}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:var(--bs-form-invalid-color)}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:var(--bs-form-invalid-color)}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-invalid,.input-group>.form-floating:not(:focus-within).is-invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-control:not(:focus):invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.was-validated .input-group>.form-select:not(:focus):invalid{z-index:4}.btn{--bs-btn-padding-x:0.75rem;--bs-btn-padding-y:0.375rem;--bs-btn-font-family: ;--bs-btn-font-size:1rem;--bs-btn-font-weight:400;--bs-btn-line-height:1.5;--bs-btn-color:var(--bs-body-color);--bs-btn-bg:transparent;--bs-btn-border-width:var(--bs-border-width);--bs-btn-border-color:transparent;--bs-btn-border-radius:var(--bs-border-radius);--bs-btn-hover-border-color:transparent;--bs-btn-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.15),0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity:0.65;--bs-btn-focus-box-shadow:0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,.btn.active,.btn.show,.btn:first-child:active,:not(.btn-check)+.btn:active{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible,.btn:first-child:active:focus-visible,:not(.btn-check)+.btn:active:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked:focus-visible+.btn{box-shadow:var(--bs-btn-focus-box-shadow)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0b5ed7;--bs-btn-hover-border-color:#0a58ca;--bs-btn-focus-shadow-rgb:49,132,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0a58ca;--bs-btn-active-border-color:#0a53be;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#0d6efd;--bs-btn-disabled-border-color:#0d6efd}.btn-secondary{--bs-btn-color:#fff;--bs-btn-bg:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#5c636a;--bs-btn-hover-border-color:#565e64;--bs-btn-focus-shadow-rgb:130,138,145;--bs-btn-active-color:#fff;--bs-btn-active-bg:#565e64;--bs-btn-active-border-color:#51585e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#6c757d;--bs-btn-disabled-border-color:#6c757d}.btn-success{--bs-btn-color:#fff;--bs-btn-bg:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#157347;--bs-btn-hover-border-color:#146c43;--bs-btn-focus-shadow-rgb:60,153,110;--bs-btn-active-color:#fff;--bs-btn-active-bg:#146c43;--bs-btn-active-border-color:#13653f;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#198754;--bs-btn-disabled-border-color:#198754}.btn-info{--bs-btn-color:#000;--bs-btn-bg:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#31d2f2;--bs-btn-hover-border-color:#25cff2;--bs-btn-focus-shadow-rgb:11,172,204;--bs-btn-active-color:#000;--bs-btn-active-bg:#3dd5f3;--bs-btn-active-border-color:#25cff2;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#0dcaf0;--bs-btn-disabled-border-color:#0dcaf0}.btn-warning{--bs-btn-color:#000;--bs-btn-bg:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffca2c;--bs-btn-hover-border-color:#ffc720;--bs-btn-focus-shadow-rgb:217,164,6;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffcd39;--bs-btn-active-border-color:#ffc720;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#ffc107;--bs-btn-disabled-border-color:#ffc107}.btn-danger{--bs-btn-color:#fff;--bs-btn-bg:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#bb2d3b;--bs-btn-hover-border-color:#b02a37;--bs-btn-focus-shadow-rgb:225,83,97;--bs-btn-active-color:#fff;--bs-btn-active-bg:#b02a37;--bs-btn-active-border-color:#a52834;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#dc3545;--bs-btn-disabled-border-color:#dc3545}.btn-light{--bs-btn-color:#000;--bs-btn-bg:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#d3d4d5;--bs-btn-hover-border-color:#c6c7c8;--bs-btn-focus-shadow-rgb:211,212,213;--bs-btn-active-color:#000;--bs-btn-active-bg:#c6c7c8;--bs-btn-active-border-color:#babbbc;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#f8f9fa;--bs-btn-disabled-border-color:#f8f9fa}.btn-dark{--bs-btn-color:#fff;--bs-btn-bg:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#424649;--bs-btn-hover-border-color:#373b3e;--bs-btn-focus-shadow-rgb:66,70,73;--bs-btn-active-color:#fff;--bs-btn-active-bg:#4d5154;--bs-btn-active-border-color:#373b3e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#212529;--bs-btn-disabled-border-color:#212529}.btn-outline-primary{--bs-btn-color:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0d6efd;--bs-btn-hover-border-color:#0d6efd;--bs-btn-focus-shadow-rgb:13,110,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0d6efd;--bs-btn-active-border-color:#0d6efd;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0d6efd;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0d6efd;--bs-gradient:none}.btn-outline-secondary{--bs-btn-color:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6c757d;--bs-btn-hover-border-color:#6c757d;--bs-btn-focus-shadow-rgb:108,117,125;--bs-btn-active-color:#fff;--bs-btn-active-bg:#6c757d;--bs-btn-active-border-color:#6c757d;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#6c757d;--bs-gradient:none}.btn-outline-success{--bs-btn-color:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#198754;--bs-btn-hover-border-color:#198754;--bs-btn-focus-shadow-rgb:25,135,84;--bs-btn-active-color:#fff;--bs-btn-active-bg:#198754;--bs-btn-active-border-color:#198754;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#198754;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#198754;--bs-gradient:none}.btn-outline-info{--bs-btn-color:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#0dcaf0;--bs-btn-hover-border-color:#0dcaf0;--bs-btn-focus-shadow-rgb:13,202,240;--bs-btn-active-color:#000;--bs-btn-active-bg:#0dcaf0;--bs-btn-active-border-color:#0dcaf0;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0dcaf0;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0dcaf0;--bs-gradient:none}.btn-outline-warning{--bs-btn-color:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffc107;--bs-btn-hover-border-color:#ffc107;--bs-btn-focus-shadow-rgb:255,193,7;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffc107;--bs-btn-active-border-color:#ffc107;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#ffc107;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#ffc107;--bs-gradient:none}.btn-outline-danger{--bs-btn-color:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#dc3545;--bs-btn-hover-border-color:#dc3545;--bs-btn-focus-shadow-rgb:220,53,69;--bs-btn-active-color:#fff;--bs-btn-active-bg:#dc3545;--bs-btn-active-border-color:#dc3545;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#dc3545;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#dc3545;--bs-gradient:none}.btn-outline-light{--bs-btn-color:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#f8f9fa;--bs-btn-hover-border-color:#f8f9fa;--bs-btn-focus-shadow-rgb:248,249,250;--bs-btn-active-color:#000;--bs-btn-active-bg:#f8f9fa;--bs-btn-active-border-color:#f8f9fa;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#f8f9fa;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#f8f9fa;--bs-gradient:none}.btn-outline-dark{--bs-btn-color:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#212529;--bs-btn-hover-border-color:#212529;--bs-btn-focus-shadow-rgb:33,37,41;--bs-btn-active-color:#fff;--bs-btn-active-bg:#212529;--bs-btn-active-border-color:#212529;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#212529;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#212529;--bs-gradient:none}.btn-link{--bs-btn-font-weight:400;--bs-btn-color:var(--bs-link-color);--bs-btn-bg:transparent;--bs-btn-border-color:transparent;--bs-btn-hover-color:var(--bs-link-hover-color);--bs-btn-hover-border-color:transparent;--bs-btn-active-color:var(--bs-link-hover-color);--bs-btn-active-border-color:transparent;--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-border-color:transparent;--bs-btn-box-shadow:0 0 0 #000;--bs-btn-focus-shadow-rgb:49,132,253;text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-group-lg>.btn,.btn-lg{--bs-btn-padding-y:0.5rem;--bs-btn-padding-x:1rem;--bs-btn-font-size:1.25rem;--bs-btn-border-radius:var(--bs-border-radius-lg)}.btn-group-sm>.btn,.btn-sm{--bs-btn-padding-y:0.25rem;--bs-btn-padding-x:0.5rem;--bs-btn-font-size:0.875rem;--bs-btn-border-radius:var(--bs-border-radius-sm)}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropdown-center,.dropend,.dropstart,.dropup,.dropup-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{--bs-dropdown-zindex:1000;--bs-dropdown-min-width:10rem;--bs-dropdown-padding-x:0;--bs-dropdown-padding-y:0.5rem;--bs-dropdown-spacer:0.125rem;--bs-dropdown-font-size:1rem;--bs-dropdown-color:var(--bs-body-color);--bs-dropdown-bg:var(--bs-body-bg);--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-border-radius:var(--bs-border-radius);--bs-dropdown-border-width:var(--bs-border-width);--bs-dropdown-inner-border-radius:calc(var(--bs-border-radius) - var(--bs-border-width));--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-divider-margin-y:0.5rem;--bs-dropdown-box-shadow:var(--bs-box-shadow);--bs-dropdown-link-color:var(--bs-body-color);--bs-dropdown-link-hover-color:var(--bs-body-color);--bs-dropdown-link-hover-bg:var(--bs-tertiary-bg);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:var(--bs-tertiary-color);--bs-dropdown-item-padding-x:1rem;--bs-dropdown-item-padding-y:0.25rem;--bs-dropdown-header-color:#6c757d;--bs-dropdown-header-padding-x:1rem;--bs-dropdown-header-padding-y:0.5rem;position:absolute;z-index:var(--bs-dropdown-zindex);display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);border-radius:var(--bs-dropdown-border-radius)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--bs-dropdown-spacer)}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--bs-dropdown-spacer)}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--bs-dropdown-spacer)}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:var(--bs-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0;border-radius:var(--bs-dropdown-item-border-radius,0)}.dropdown-item:focus,.dropdown-item:hover{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:.875rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color:#dee2e6;--bs-dropdown-bg:#343a40;--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-box-shadow: ;--bs-dropdown-link-color:#dee2e6;--bs-dropdown-link-hover-color:#fff;--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-link-hover-bg:rgba(255, 255, 255, 0.15);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:#adb5bd;--bs-dropdown-header-color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group{border-radius:var(--bs-border-radius)}.btn-group>.btn-group:not(:first-child),.btn-group>:not(.btn-check:first-child)+.btn{margin-left:calc(var(--bs-border-width) * -1)}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:calc(var(--bs-border-width) * -1)}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--bs-nav-link-padding-x:1rem;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-link-color);--bs-nav-link-hover-color:var(--bs-link-hover-color);--bs-nav-link-disabled-color:var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;background:0 0;border:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:var(--bs-nav-link-hover-color)}.nav-link:focus-visible{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.nav-link.disabled,.nav-link:disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width:var(--bs-border-width);--bs-nav-tabs-border-color:var(--bs-border-color);--bs-nav-tabs-border-radius:var(--bs-border-radius);--bs-nav-tabs-link-hover-border-color:var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color);--bs-nav-tabs-link-active-color:var(--bs-emphasis-color);--bs-nav-tabs-link-active-bg:var(--bs-body-bg);--bs-nav-tabs-link-active-border-color:var(--bs-border-color) var(--bs-border-color) var(--bs-body-bg);border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--bs-nav-tabs-border-width));border:var(--bs-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1 * var(--bs-nav-tabs-border-width));border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--bs-nav-pills-border-radius:var(--bs-border-radius);--bs-nav-pills-link-active-color:#fff;--bs-nav-pills-link-active-bg:#0d6efd}.nav-pills .nav-link{border-radius:var(--bs-nav-pills-border-radius)}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-underline{--bs-nav-underline-gap:1rem;--bs-nav-underline-border-width:0.125rem;--bs-nav-underline-link-active-color:var(--bs-emphasis-color);gap:var(--bs-nav-underline-gap)}.nav-underline .nav-link{padding-right:0;padding-left:0;border-bottom:var(--bs-nav-underline-border-width) solid transparent}.nav-underline .nav-link:focus,.nav-underline .nav-link:hover{border-bottom-color:currentcolor}.nav-underline .nav-link.active,.nav-underline .show>.nav-link{font-weight:700;color:var(--bs-nav-underline-link-active-color);border-bottom-color:currentcolor}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x:0;--bs-navbar-padding-y:0.5rem;--bs-navbar-color:rgba(var(--bs-emphasis-color-rgb), 0.65);--bs-navbar-hover-color:rgba(var(--bs-emphasis-color-rgb), 0.8);--bs-navbar-disabled-color:rgba(var(--bs-emphasis-color-rgb), 0.3);--bs-navbar-active-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-padding-y:0.3125rem;--bs-navbar-brand-margin-end:1rem;--bs-navbar-brand-font-size:1.25rem;--bs-navbar-brand-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-hover-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-nav-link-padding-x:0.5rem;--bs-navbar-toggler-padding-y:0.25rem;--bs-navbar-toggler-padding-x:0.75rem;--bs-navbar-toggler-font-size:1.25rem;--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color:rgba(var(--bs-emphasis-color-rgb), 0.15);--bs-navbar-toggler-border-radius:var(--bs-border-radius);--bs-navbar-toggler-focus-width:0.25rem;--bs-navbar-toggler-transition:box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x:0;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-navbar-color);--bs-nav-link-hover-color:var(--bs-navbar-hover-color);--bs-nav-link-disabled-color:var(--bs-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .nav-link.show{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:focus,.navbar-text a:hover{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:transparent;border:var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);border-radius:var(--bs-navbar-toggler-border-radius);transition:var(--bs-navbar-toggler-transition)}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-dark,.navbar[data-bs-theme=dark]{--bs-navbar-color:rgba(255, 255, 255, 0.55);--bs-navbar-hover-color:rgba(255, 255, 255, 0.75);--bs-navbar-disabled-color:rgba(255, 255, 255, 0.25);--bs-navbar-active-color:#fff;--bs-navbar-brand-color:#fff;--bs-navbar-brand-hover-color:#fff;--bs-navbar-toggler-border-color:rgba(255, 255, 255, 0.1);--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}[data-bs-theme=dark] .navbar-toggler-icon{--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y:1rem;--bs-card-spacer-x:1rem;--bs-card-title-spacer-y:0.5rem;--bs-card-title-color: ;--bs-card-subtitle-color: ;--bs-card-border-width:var(--bs-border-width);--bs-card-border-color:var(--bs-border-color-translucent);--bs-card-border-radius:var(--bs-border-radius);--bs-card-box-shadow: ;--bs-card-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-card-cap-padding-y:0.5rem;--bs-card-cap-padding-x:1rem;--bs-card-cap-bg:rgba(var(--bs-body-color-rgb), 0.03);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg:var(--bs-body-bg);--bs-card-img-overlay-padding:1rem;--bs-card-group-margin:0.75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);color:var(--bs-body-color);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y);color:var(--bs-card-title-color)}.card-subtitle{margin-top:calc(-.5 * var(--bs-card-title-spacer-y));margin-bottom:0;color:var(--bs-card-subtitle-color)}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header:first-child{border-radius:var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer:last-child{border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)}.card-header-tabs{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-bottom:calc(-1 * var(--bs-card-cap-padding-y));margin-left:calc(-.5 * var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-left:calc(-.5 * var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding);border-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion{--bs-accordion-color:var(--bs-body-color);--bs-accordion-bg:var(--bs-body-bg);--bs-accordion-transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out,border-radius 0.15s ease;--bs-accordion-border-color:var(--bs-border-color);--bs-accordion-border-width:var(--bs-border-width);--bs-accordion-border-radius:var(--bs-border-radius);--bs-accordion-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-accordion-btn-padding-x:1.25rem;--bs-accordion-btn-padding-y:1rem;--bs-accordion-btn-color:var(--bs-body-color);--bs-accordion-btn-bg:var(--bs-accordion-bg);--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23212529' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width:1.25rem;--bs-accordion-btn-icon-transform:rotate(-180deg);--bs-accordion-btn-icon-transition:transform 0.2s ease-in-out;--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23052c65' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");--bs-accordion-btn-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-accordion-body-padding-x:1.25rem;--bs-accordion-body-padding-y:1rem;--bs-accordion-active-color:var(--bs-primary-text-emphasis);--bs-accordion-active-bg:var(--bs-primary-bg-subtle)}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);font-size:1rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;border-radius:0;overflow-anchor:none;transition:var(--bs-accordion-transition)}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width);transition:var(--bs-accordion-btn-icon-transition)}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.accordion-item:first-of-type{border-top-left-radius:var(--bs-accordion-border-radius);border-top-right-radius:var(--bs-accordion-border-radius)}.accordion-item:first-of-type>.accordion-header .accordion-button{border-top-left-radius:var(--bs-accordion-inner-border-radius);border-top-right-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-item:last-of-type>.accordion-header .accordion-button.collapsed{border-bottom-right-radius:var(--bs-accordion-inner-border-radius);border-bottom-left-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:last-of-type>.accordion-collapse{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-body{padding:var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x)}.accordion-flush>.accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush>.accordion-item:first-child{border-top:0}.accordion-flush>.accordion-item:last-child{border-bottom:0}.accordion-flush>.accordion-item>.accordion-header .accordion-button,.accordion-flush>.accordion-item>.accordion-header .accordion-button.collapsed{border-radius:0}.accordion-flush>.accordion-item>.accordion-collapse{border-radius:0}[data-bs-theme=dark] .accordion-button::after{--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.breadcrumb{--bs-breadcrumb-padding-x:0;--bs-breadcrumb-padding-y:0;--bs-breadcrumb-margin-bottom:1rem;--bs-breadcrumb-bg: ;--bs-breadcrumb-border-radius: ;--bs-breadcrumb-divider-color:var(--bs-secondary-color);--bs-breadcrumb-item-padding-x:0.5rem;--bs-breadcrumb-item-active-color:var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg);border-radius:var(--bs-breadcrumb-border-radius)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x:0.75rem;--bs-pagination-padding-y:0.375rem;--bs-pagination-font-size:1rem;--bs-pagination-color:var(--bs-link-color);--bs-pagination-bg:var(--bs-body-bg);--bs-pagination-border-width:var(--bs-border-width);--bs-pagination-border-color:var(--bs-border-color);--bs-pagination-border-radius:var(--bs-border-radius);--bs-pagination-hover-color:var(--bs-link-hover-color);--bs-pagination-hover-bg:var(--bs-tertiary-bg);--bs-pagination-hover-border-color:var(--bs-border-color);--bs-pagination-focus-color:var(--bs-link-hover-color);--bs-pagination-focus-bg:var(--bs-secondary-bg);--bs-pagination-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-pagination-active-color:#fff;--bs-pagination-active-bg:#0d6efd;--bs-pagination-active-border-color:#0d6efd;--bs-pagination-disabled-color:var(--bs-secondary-color);--bs-pagination-disabled-bg:var(--bs-secondary-bg);--bs-pagination-disabled-border-color:var(--bs-border-color);display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.active>.page-link,.page-link.active{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.disabled>.page-link,.page-link.disabled{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:calc(var(--bs-border-width) * -1)}.page-item:first-child .page-link{border-top-left-radius:var(--bs-pagination-border-radius);border-bottom-left-radius:var(--bs-pagination-border-radius)}.page-item:last-child .page-link{border-top-right-radius:var(--bs-pagination-border-radius);border-bottom-right-radius:var(--bs-pagination-border-radius)}.pagination-lg{--bs-pagination-padding-x:1.5rem;--bs-pagination-padding-y:0.75rem;--bs-pagination-font-size:1.25rem;--bs-pagination-border-radius:var(--bs-border-radius-lg)}.pagination-sm{--bs-pagination-padding-x:0.5rem;--bs-pagination-padding-y:0.25rem;--bs-pagination-font-size:0.875rem;--bs-pagination-border-radius:var(--bs-border-radius-sm)}.badge{--bs-badge-padding-x:0.65em;--bs-badge-padding-y:0.35em;--bs-badge-font-size:0.75em;--bs-badge-font-weight:700;--bs-badge-color:#fff;--bs-badge-border-radius:var(--bs-border-radius);display:inline-block;padding:var(--bs-badge-padding-y) var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:var(--bs-badge-border-radius)}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg:transparent;--bs-alert-padding-x:1rem;--bs-alert-padding-y:1rem;--bs-alert-margin-bottom:1rem;--bs-alert-color:inherit;--bs-alert-border-color:transparent;--bs-alert-border:var(--bs-border-width) solid var(--bs-alert-border-color);--bs-alert-border-radius:var(--bs-border-radius);--bs-alert-link-color:inherit;position:relative;padding:var(--bs-alert-padding-y) var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border);border-radius:var(--bs-alert-border-radius)}.alert-heading{color:inherit}.alert-link{font-weight:700;color:var(--bs-alert-link-color)}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{--bs-alert-color:var(--bs-primary-text-emphasis);--bs-alert-bg:var(--bs-primary-bg-subtle);--bs-alert-border-color:var(--bs-primary-border-subtle);--bs-alert-link-color:var(--bs-primary-text-emphasis)}.alert-secondary{--bs-alert-color:var(--bs-secondary-text-emphasis);--bs-alert-bg:var(--bs-secondary-bg-subtle);--bs-alert-border-color:var(--bs-secondary-border-subtle);--bs-alert-link-color:var(--bs-secondary-text-emphasis)}.alert-success{--bs-alert-color:var(--bs-success-text-emphasis);--bs-alert-bg:var(--bs-success-bg-subtle);--bs-alert-border-color:var(--bs-success-border-subtle);--bs-alert-link-color:var(--bs-success-text-emphasis)}.alert-info{--bs-alert-color:var(--bs-info-text-emphasis);--bs-alert-bg:var(--bs-info-bg-subtle);--bs-alert-border-color:var(--bs-info-border-subtle);--bs-alert-link-color:var(--bs-info-text-emphasis)}.alert-warning{--bs-alert-color:var(--bs-warning-text-emphasis);--bs-alert-bg:var(--bs-warning-bg-subtle);--bs-alert-border-color:var(--bs-warning-border-subtle);--bs-alert-link-color:var(--bs-warning-text-emphasis)}.alert-danger{--bs-alert-color:var(--bs-danger-text-emphasis);--bs-alert-bg:var(--bs-danger-bg-subtle);--bs-alert-border-color:var(--bs-danger-border-subtle);--bs-alert-link-color:var(--bs-danger-text-emphasis)}.alert-light{--bs-alert-color:var(--bs-light-text-emphasis);--bs-alert-bg:var(--bs-light-bg-subtle);--bs-alert-border-color:var(--bs-light-border-subtle);--bs-alert-link-color:var(--bs-light-text-emphasis)}.alert-dark{--bs-alert-color:var(--bs-dark-text-emphasis);--bs-alert-bg:var(--bs-dark-bg-subtle);--bs-alert-border-color:var(--bs-dark-border-subtle);--bs-alert-link-color:var(--bs-dark-text-emphasis)}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress,.progress-stacked{--bs-progress-height:1rem;--bs-progress-font-size:0.75rem;--bs-progress-bg:var(--bs-secondary-bg);--bs-progress-border-radius:var(--bs-border-radius);--bs-progress-box-shadow:var(--bs-box-shadow-inset);--bs-progress-bar-color:#fff;--bs-progress-bar-bg:#0d6efd;--bs-progress-bar-transition:width 0.6s ease;display:flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg);border-radius:var(--bs-progress-border-radius)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg);transition:var(--bs-progress-bar-transition)}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:var(--bs-progress-height) var(--bs-progress-height)}.progress-stacked>.progress{overflow:visible}.progress-stacked>.progress>.progress-bar{width:100%}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.list-group{--bs-list-group-color:var(--bs-body-color);--bs-list-group-bg:var(--bs-body-bg);--bs-list-group-border-color:var(--bs-border-color);--bs-list-group-border-width:var(--bs-border-width);--bs-list-group-border-radius:var(--bs-border-radius);--bs-list-group-item-padding-x:1rem;--bs-list-group-item-padding-y:0.5rem;--bs-list-group-action-color:var(--bs-secondary-color);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-tertiary-bg);--bs-list-group-action-active-color:var(--bs-body-color);--bs-list-group-action-active-bg:var(--bs-secondary-bg);--bs-list-group-disabled-color:var(--bs-secondary-color);--bs-list-group-disabled-bg:var(--bs-body-bg);--bs-list-group-active-color:#fff;--bs-list-group-active-bg:#0d6efd;--bs-list-group-active-border-color:#0d6efd;display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:var(--bs-list-group-border-radius)}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1 * var(--bs-list-group-border-width));border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{--bs-list-group-color:var(--bs-primary-text-emphasis);--bs-list-group-bg:var(--bs-primary-bg-subtle);--bs-list-group-border-color:var(--bs-primary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-primary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-primary-border-subtle);--bs-list-group-active-color:var(--bs-primary-bg-subtle);--bs-list-group-active-bg:var(--bs-primary-text-emphasis);--bs-list-group-active-border-color:var(--bs-primary-text-emphasis)}.list-group-item-secondary{--bs-list-group-color:var(--bs-secondary-text-emphasis);--bs-list-group-bg:var(--bs-secondary-bg-subtle);--bs-list-group-border-color:var(--bs-secondary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-secondary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-secondary-border-subtle);--bs-list-group-active-color:var(--bs-secondary-bg-subtle);--bs-list-group-active-bg:var(--bs-secondary-text-emphasis);--bs-list-group-active-border-color:var(--bs-secondary-text-emphasis)}.list-group-item-success{--bs-list-group-color:var(--bs-success-text-emphasis);--bs-list-group-bg:var(--bs-success-bg-subtle);--bs-list-group-border-color:var(--bs-success-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-success-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-success-border-subtle);--bs-list-group-active-color:var(--bs-success-bg-subtle);--bs-list-group-active-bg:var(--bs-success-text-emphasis);--bs-list-group-active-border-color:var(--bs-success-text-emphasis)}.list-group-item-info{--bs-list-group-color:var(--bs-info-text-emphasis);--bs-list-group-bg:var(--bs-info-bg-subtle);--bs-list-group-border-color:var(--bs-info-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-info-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-info-border-subtle);--bs-list-group-active-color:var(--bs-info-bg-subtle);--bs-list-group-active-bg:var(--bs-info-text-emphasis);--bs-list-group-active-border-color:var(--bs-info-text-emphasis)}.list-group-item-warning{--bs-list-group-color:var(--bs-warning-text-emphasis);--bs-list-group-bg:var(--bs-warning-bg-subtle);--bs-list-group-border-color:var(--bs-warning-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-warning-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-warning-border-subtle);--bs-list-group-active-color:var(--bs-warning-bg-subtle);--bs-list-group-active-bg:var(--bs-warning-text-emphasis);--bs-list-group-active-border-color:var(--bs-warning-text-emphasis)}.list-group-item-danger{--bs-list-group-color:var(--bs-danger-text-emphasis);--bs-list-group-bg:var(--bs-danger-bg-subtle);--bs-list-group-border-color:var(--bs-danger-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-danger-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-danger-border-subtle);--bs-list-group-active-color:var(--bs-danger-bg-subtle);--bs-list-group-active-bg:var(--bs-danger-text-emphasis);--bs-list-group-active-border-color:var(--bs-danger-text-emphasis)}.list-group-item-light{--bs-list-group-color:var(--bs-light-text-emphasis);--bs-list-group-bg:var(--bs-light-bg-subtle);--bs-list-group-border-color:var(--bs-light-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-light-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-light-border-subtle);--bs-list-group-active-color:var(--bs-light-bg-subtle);--bs-list-group-active-bg:var(--bs-light-text-emphasis);--bs-list-group-active-border-color:var(--bs-light-text-emphasis)}.list-group-item-dark{--bs-list-group-color:var(--bs-dark-text-emphasis);--bs-list-group-bg:var(--bs-dark-bg-subtle);--bs-list-group-border-color:var(--bs-dark-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-dark-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-dark-border-subtle);--bs-list-group-active-color:var(--bs-dark-bg-subtle);--bs-list-group-active-bg:var(--bs-dark-text-emphasis);--bs-list-group-active-border-color:var(--bs-dark-text-emphasis)}.btn-close{--bs-btn-close-color:#000;--bs-btn-close-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e");--bs-btn-close-opacity:0.5;--bs-btn-close-hover-opacity:0.75;--bs-btn-close-focus-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-btn-close-focus-opacity:1;--bs-btn-close-disabled-opacity:0.25;--bs-btn-close-white-filter:invert(1) grayscale(100%) brightness(200%);box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:var(--bs-btn-close-color);background:transparent var(--bs-btn-close-bg) center/1em auto no-repeat;border:0;border-radius:.375rem;opacity:var(--bs-btn-close-opacity)}.btn-close:hover{color:var(--bs-btn-close-color);text-decoration:none;opacity:var(--bs-btn-close-hover-opacity)}.btn-close:focus{outline:0;box-shadow:var(--bs-btn-close-focus-shadow);opacity:var(--bs-btn-close-focus-opacity)}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:var(--bs-btn-close-disabled-opacity)}.btn-close-white{filter:var(--bs-btn-close-white-filter)}[data-bs-theme=dark] .btn-close{filter:var(--bs-btn-close-white-filter)}.toast{--bs-toast-zindex:1090;--bs-toast-padding-x:0.75rem;--bs-toast-padding-y:0.5rem;--bs-toast-spacing:1.5rem;--bs-toast-max-width:350px;--bs-toast-font-size:0.875rem;--bs-toast-color: ;--bs-toast-bg:rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-border-width:var(--bs-border-width);--bs-toast-border-color:var(--bs-border-color-translucent);--bs-toast-border-radius:var(--bs-border-radius);--bs-toast-box-shadow:var(--bs-box-shadow);--bs-toast-header-color:var(--bs-secondary-color);--bs-toast-header-bg:rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-header-border-color:var(--bs-border-color-translucent);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width) solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow);border-radius:var(--bs-toast-border-radius)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--bs-toast-zindex:1090;position:absolute;z-index:var(--bs-toast-zindex);width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--bs-toast-padding-y) var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);border-top-left-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));border-top-right-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width))}.toast-header .btn-close{margin-right:calc(-.5 * var(--bs-toast-padding-x));margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex:1055;--bs-modal-width:500px;--bs-modal-padding:1rem;--bs-modal-margin:0.5rem;--bs-modal-color: ;--bs-modal-bg:var(--bs-body-bg);--bs-modal-border-color:var(--bs-border-color-translucent);--bs-modal-border-width:var(--bs-border-width);--bs-modal-border-radius:var(--bs-border-radius-lg);--bs-modal-box-shadow:var(--bs-box-shadow-sm);--bs-modal-inner-border-radius:calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));--bs-modal-header-padding-x:1rem;--bs-modal-header-padding-y:1rem;--bs-modal-header-padding:1rem 1rem;--bs-modal-header-border-color:var(--bs-border-color);--bs-modal-header-border-width:var(--bs-border-width);--bs-modal-title-line-height:1.5;--bs-modal-footer-gap:0.5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color:var(--bs-border-color);--bs-modal-footer-border-width:var(--bs-border-width);position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin) * 2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - var(--bs-modal-margin) * 2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);border-radius:var(--bs-modal-border-radius);outline:0}.modal-backdrop{--bs-backdrop-zindex:1050;--bs-backdrop-bg:#000;--bs-backdrop-opacity:0.5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);border-top-left-radius:var(--bs-modal-inner-border-radius);border-top-right-radius:var(--bs-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y) * .5) calc(var(--bs-modal-header-padding-x) * .5);margin:calc(-.5 * var(--bs-modal-header-padding-y)) calc(-.5 * var(--bs-modal-header-padding-x)) calc(-.5 * var(--bs-modal-header-padding-y)) auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);border-bottom-right-radius:var(--bs-modal-inner-border-radius);border-bottom-left-radius:var(--bs-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap) * .5)}@media (min-width:576px){.modal{--bs-modal-margin:1.75rem;--bs-modal-box-shadow:var(--bs-box-shadow)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{--bs-modal-width:800px}}@media (min-width:1200px){.modal-xl{--bs-modal-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-footer,.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-footer,.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-footer,.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-footer,.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-footer,.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-footer,.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex:1080;--bs-tooltip-max-width:200px;--bs-tooltip-padding-x:0.5rem;--bs-tooltip-padding-y:0.25rem;--bs-tooltip-margin: ;--bs-tooltip-font-size:0.875rem;--bs-tooltip-color:var(--bs-body-bg);--bs-tooltip-bg:var(--bs-emphasis-color);--bs-tooltip-border-radius:var(--bs-border-radius);--bs-tooltip-opacity:0.9;--bs-tooltip-arrow-width:0.8rem;--bs-tooltip-arrow-height:0.4rem;z-index:var(--bs-tooltip-zindex);display:block;margin:var(--bs-tooltip-margin);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) 0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg);border-radius:var(--bs-tooltip-border-radius)}.popover{--bs-popover-zindex:1070;--bs-popover-max-width:276px;--bs-popover-font-size:0.875rem;--bs-popover-bg:var(--bs-body-bg);--bs-popover-border-width:var(--bs-border-width);--bs-popover-border-color:var(--bs-border-color-translucent);--bs-popover-border-radius:var(--bs-border-radius-lg);--bs-popover-inner-border-radius:calc(var(--bs-border-radius-lg) - var(--bs-border-width));--bs-popover-box-shadow:var(--bs-box-shadow);--bs-popover-header-padding-x:1rem;--bs-popover-header-padding-y:0.5rem;--bs-popover-header-font-size:1rem;--bs-popover-header-color:inherit;--bs-popover-header-bg:var(--bs-secondary-bg);--bs-popover-body-padding-x:1rem;--bs-popover-body-padding-y:1rem;--bs-popover-body-color:var(--bs-body-color);--bs-popover-arrow-width:1rem;--bs-popover-arrow-height:0.5rem;--bs-popover-arrow-border:var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-radius:var(--bs-popover-border-radius)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid;border-width:0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-top>.popover-arrow::before{border-width:var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-end>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::before{border-width:0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:var(--bs-popover-arrow-width);margin-left:calc(-.5 * var(--bs-popover-arrow-width));content:"";border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-header-bg)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-start>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) 0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-top-left-radius:var(--bs-popover-inner-border-radius);border-top-right-radius:var(--bs-popover-inner-border-radius)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}[data-bs-theme=dark] .carousel .carousel-control-next-icon,[data-bs-theme=dark] .carousel .carousel-control-prev-icon,[data-bs-theme=dark].carousel .carousel-control-next-icon,[data-bs-theme=dark].carousel .carousel-control-prev-icon{filter:invert(1) grayscale(100)}[data-bs-theme=dark] .carousel .carousel-indicators [data-bs-target],[data-bs-theme=dark].carousel .carousel-indicators [data-bs-target]{background-color:#000}[data-bs-theme=dark] .carousel .carousel-caption,[data-bs-theme=dark].carousel .carousel-caption{color:#000}.spinner-border,.spinner-grow{display:inline-block;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-border-width:0.25em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-border;border:var(--bs-spinner-border-width) solid currentcolor;border-right-color:transparent}.spinner-border-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem;--bs-spinner-border-width:0.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed:1.5s}}.offcanvas,.offcanvas-lg,.offcanvas-md,.offcanvas-sm,.offcanvas-xl,.offcanvas-xxl{--bs-offcanvas-zindex:1045;--bs-offcanvas-width:400px;--bs-offcanvas-height:30vh;--bs-offcanvas-padding-x:1rem;--bs-offcanvas-padding-y:1rem;--bs-offcanvas-color:var(--bs-body-color);--bs-offcanvas-bg:var(--bs-body-bg);--bs-offcanvas-border-width:var(--bs-border-width);--bs-offcanvas-border-color:var(--bs-border-color-translucent);--bs-offcanvas-box-shadow:var(--bs-box-shadow-sm);--bs-offcanvas-transition:transform 0.3s ease-in-out;--bs-offcanvas-title-line-height:1.5}@media (max-width:575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:575.98px) and (prefers-reduced-motion:reduce){.offcanvas-sm{transition:none}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-sm.show:not(.hiding),.offcanvas-sm.showing{transform:none}.offcanvas-sm.hiding,.offcanvas-sm.show,.offcanvas-sm.showing{visibility:visible}}@media (min-width:576px){.offcanvas-sm{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:767.98px) and (prefers-reduced-motion:reduce){.offcanvas-md{transition:none}}@media (max-width:767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-md.show:not(.hiding),.offcanvas-md.showing{transform:none}.offcanvas-md.hiding,.offcanvas-md.show,.offcanvas-md.showing{visibility:visible}}@media (min-width:768px){.offcanvas-md{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:991.98px) and (prefers-reduced-motion:reduce){.offcanvas-lg{transition:none}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-lg.show:not(.hiding),.offcanvas-lg.showing{transform:none}.offcanvas-lg.hiding,.offcanvas-lg.show,.offcanvas-lg.showing{visibility:visible}}@media (min-width:992px){.offcanvas-lg{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:1199.98px) and (prefers-reduced-motion:reduce){.offcanvas-xl{transition:none}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xl.show:not(.hiding),.offcanvas-xl.showing{transform:none}.offcanvas-xl.hiding,.offcanvas-xl.show,.offcanvas-xl.showing{visibility:visible}}@media (min-width:1200px){.offcanvas-xl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:1399.98px) and (prefers-reduced-motion:reduce){.offcanvas-xxl{transition:none}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xxl.show:not(.hiding),.offcanvas-xxl.showing{transform:none}.offcanvas-xxl.hiding,.offcanvas-xxl.show,.offcanvas-xxl.showing{visibility:visible}}@media (min-width:1400px){.offcanvas-xxl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.show:not(.hiding),.offcanvas.showing{transform:none}.offcanvas.hiding,.offcanvas.show,.offcanvas.showing{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y) * .5) calc(var(--bs-offcanvas-padding-x) * .5);margin:calc(-.5 * var(--bs-offcanvas-padding-y)) calc(-.5 * var(--bs-offcanvas-padding-x)) calc(-.5 * var(--bs-offcanvas-padding-y)) auto}.offcanvas-title{margin-bottom:0;line-height:var(--bs-offcanvas-title-line-height)}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.text-bg-primary{color:#fff!important;background-color:RGBA(var(--bs-primary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-secondary{color:#fff!important;background-color:RGBA(var(--bs-secondary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-success{color:#fff!important;background-color:RGBA(var(--bs-success-rgb),var(--bs-bg-opacity,1))!important}.text-bg-info{color:#000!important;background-color:RGBA(var(--bs-info-rgb),var(--bs-bg-opacity,1))!important}.text-bg-warning{color:#000!important;background-color:RGBA(var(--bs-warning-rgb),var(--bs-bg-opacity,1))!important}.text-bg-danger{color:#fff!important;background-color:RGBA(var(--bs-danger-rgb),var(--bs-bg-opacity,1))!important}.text-bg-light{color:#000!important;background-color:RGBA(var(--bs-light-rgb),var(--bs-bg-opacity,1))!important}.text-bg-dark{color:#fff!important;background-color:RGBA(var(--bs-dark-rgb),var(--bs-bg-opacity,1))!important}.link-primary{color:RGBA(var(--bs-primary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important}.link-primary:focus,.link-primary:hover{color:RGBA(10,88,202,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(10,88,202,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(10,88,202,var(--bs-link-underline-opacity,1))!important}.link-secondary{color:RGBA(var(--bs-secondary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important}.link-secondary:focus,.link-secondary:hover{color:RGBA(86,94,100,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(86,94,100,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(86,94,100,var(--bs-link-underline-opacity,1))!important}.link-success{color:RGBA(var(--bs-success-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important}.link-success:focus,.link-success:hover{color:RGBA(20,108,67,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(20,108,67,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(20,108,67,var(--bs-link-underline-opacity,1))!important}.link-info{color:RGBA(var(--bs-info-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important}.link-info:focus,.link-info:hover{color:RGBA(61,213,243,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(61,213,243,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(61,213,243,var(--bs-link-underline-opacity,1))!important}.link-warning{color:RGBA(var(--bs-warning-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important}.link-warning:focus,.link-warning:hover{color:RGBA(255,205,57,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(255,205,57,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(255,205,57,var(--bs-link-underline-opacity,1))!important}.link-danger{color:RGBA(var(--bs-danger-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important}.link-danger:focus,.link-danger:hover{color:RGBA(176,42,55,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(176,42,55,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(176,42,55,var(--bs-link-underline-opacity,1))!important}.link-light{color:RGBA(var(--bs-light-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important}.link-light:focus,.link-light:hover{color:RGBA(249,250,251,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(249,250,251,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(249,250,251,var(--bs-link-underline-opacity,1))!important}.link-dark{color:RGBA(var(--bs-dark-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important}.link-dark:focus,.link-dark:hover{color:RGBA(26,30,33,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(26,30,33,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(26,30,33,var(--bs-link-underline-opacity,1))!important}.link-body-emphasis{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-body-emphasis:focus,.link-body-emphasis:hover{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,.75))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,0.75))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,0.75))!important}.focus-ring:focus{outline:0;box-shadow:var(--bs-focus-ring-x,0) var(--bs-focus-ring-y,0) var(--bs-focus-ring-blur,0) var(--bs-focus-ring-width) var(--bs-focus-ring-color)}.icon-link{display:inline-flex;gap:.375rem;align-items:center;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,0.5));text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,0.5));text-underline-offset:0.25em;-webkit-backface-visibility:hidden;backface-visibility:hidden}.icon-link>.bi{flex-shrink:0;width:1em;height:1em;fill:currentcolor;transition:.2s ease-in-out transform}@media (prefers-reduced-motion:reduce){.icon-link>.bi{transition:none}}.icon-link-hover:focus-visible>.bi,.icon-link-hover:hover>.bi{transform:var(--bs-icon-link-transform,translate3d(.25em,0,0))}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption),.visually-hidden:not(caption){position:absolute!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:var(--bs-border-width);min-height:1em;background-color:currentcolor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.object-fit-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-none{-o-object-fit:none!important;object-fit:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.overflow-x-auto{overflow-x:auto!important}.overflow-x-hidden{overflow-x:hidden!important}.overflow-x-visible{overflow-x:visible!important}.overflow-x-scroll{overflow-x:scroll!important}.overflow-y-auto{overflow-y:auto!important}.overflow-y-hidden{overflow-y:hidden!important}.overflow-y-visible{overflow-y:visible!important}.overflow-y-scroll{overflow-y:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-inline-grid{display:inline-grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:var(--bs-box-shadow)!important}.shadow-sm{box-shadow:var(--bs-box-shadow-sm)!important}.shadow-lg{box-shadow:var(--bs-box-shadow-lg)!important}.shadow-none{box-shadow:none!important}.focus-ring-primary{--bs-focus-ring-color:rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-secondary{--bs-focus-ring-color:rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-success{--bs-focus-ring-color:rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity))}.focus-ring-info{--bs-focus-ring-color:rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity))}.focus-ring-warning{--bs-focus-ring-color:rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity))}.focus-ring-danger{--bs-focus-ring-color:rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity))}.focus-ring-light{--bs-focus-ring-color:rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity))}.focus-ring-dark{--bs-focus-ring-color:rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity))}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-0{border:0!important}.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-top-0{border-top:0!important}.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-start-0{border-left:0!important}.border-primary{--bs-border-opacity:1;border-color:rgba(var(--bs-primary-rgb),var(--bs-border-opacity))!important}.border-secondary{--bs-border-opacity:1;border-color:rgba(var(--bs-secondary-rgb),var(--bs-border-opacity))!important}.border-success{--bs-border-opacity:1;border-color:rgba(var(--bs-success-rgb),var(--bs-border-opacity))!important}.border-info{--bs-border-opacity:1;border-color:rgba(var(--bs-info-rgb),var(--bs-border-opacity))!important}.border-warning{--bs-border-opacity:1;border-color:rgba(var(--bs-warning-rgb),var(--bs-border-opacity))!important}.border-danger{--bs-border-opacity:1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important}.border-light{--bs-border-opacity:1;border-color:rgba(var(--bs-light-rgb),var(--bs-border-opacity))!important}.border-dark{--bs-border-opacity:1;border-color:rgba(var(--bs-dark-rgb),var(--bs-border-opacity))!important}.border-black{--bs-border-opacity:1;border-color:rgba(var(--bs-black-rgb),var(--bs-border-opacity))!important}.border-white{--bs-border-opacity:1;border-color:rgba(var(--bs-white-rgb),var(--bs-border-opacity))!important}.border-primary-subtle{border-color:var(--bs-primary-border-subtle)!important}.border-secondary-subtle{border-color:var(--bs-secondary-border-subtle)!important}.border-success-subtle{border-color:var(--bs-success-border-subtle)!important}.border-info-subtle{border-color:var(--bs-info-border-subtle)!important}.border-warning-subtle{border-color:var(--bs-warning-border-subtle)!important}.border-danger-subtle{border-color:var(--bs-danger-border-subtle)!important}.border-light-subtle{border-color:var(--bs-light-border-subtle)!important}.border-dark-subtle{border-color:var(--bs-dark-border-subtle)!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.border-opacity-10{--bs-border-opacity:0.1}.border-opacity-25{--bs-border-opacity:0.25}.border-opacity-50{--bs-border-opacity:0.5}.border-opacity-75{--bs-border-opacity:0.75}.border-opacity-100{--bs-border-opacity:1}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.row-gap-0{row-gap:0!important}.row-gap-1{row-gap:.25rem!important}.row-gap-2{row-gap:.5rem!important}.row-gap-3{row-gap:1rem!important}.row-gap-4{row-gap:1.5rem!important}.row-gap-5{row-gap:3rem!important}.column-gap-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-lighter{font-weight:lighter!important}.fw-light{font-weight:300!important}.fw-normal{font-weight:400!important}.fw-medium{font-weight:500!important}.fw-semibold{font-weight:600!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-body-secondary{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-body-tertiary{--bs-text-opacity:1;color:var(--bs-tertiary-color)!important}.text-body-emphasis{--bs-text-opacity:1;color:var(--bs-emphasis-color)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.text-primary-emphasis{color:var(--bs-primary-text-emphasis)!important}.text-secondary-emphasis{color:var(--bs-secondary-text-emphasis)!important}.text-success-emphasis{color:var(--bs-success-text-emphasis)!important}.text-info-emphasis{color:var(--bs-info-text-emphasis)!important}.text-warning-emphasis{color:var(--bs-warning-text-emphasis)!important}.text-danger-emphasis{color:var(--bs-danger-text-emphasis)!important}.text-light-emphasis{color:var(--bs-light-text-emphasis)!important}.text-dark-emphasis{color:var(--bs-dark-text-emphasis)!important}.link-opacity-10{--bs-link-opacity:0.1}.link-opacity-10-hover:hover{--bs-link-opacity:0.1}.link-opacity-25{--bs-link-opacity:0.25}.link-opacity-25-hover:hover{--bs-link-opacity:0.25}.link-opacity-50{--bs-link-opacity:0.5}.link-opacity-50-hover:hover{--bs-link-opacity:0.5}.link-opacity-75{--bs-link-opacity:0.75}.link-opacity-75-hover:hover{--bs-link-opacity:0.75}.link-opacity-100{--bs-link-opacity:1}.link-opacity-100-hover:hover{--bs-link-opacity:1}.link-offset-1{text-underline-offset:0.125em!important}.link-offset-1-hover:hover{text-underline-offset:0.125em!important}.link-offset-2{text-underline-offset:0.25em!important}.link-offset-2-hover:hover{text-underline-offset:0.25em!important}.link-offset-3{text-underline-offset:0.375em!important}.link-offset-3-hover:hover{text-underline-offset:0.375em!important}.link-underline-primary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-secondary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-success{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important}.link-underline-info{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important}.link-underline-warning{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important}.link-underline-danger{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important}.link-underline-light{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important}.link-underline-dark{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important}.link-underline{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-underline-opacity-0{--bs-link-underline-opacity:0}.link-underline-opacity-0-hover:hover{--bs-link-underline-opacity:0}.link-underline-opacity-10{--bs-link-underline-opacity:0.1}.link-underline-opacity-10-hover:hover{--bs-link-underline-opacity:0.1}.link-underline-opacity-25{--bs-link-underline-opacity:0.25}.link-underline-opacity-25-hover:hover{--bs-link-underline-opacity:0.25}.link-underline-opacity-50{--bs-link-underline-opacity:0.5}.link-underline-opacity-50-hover:hover{--bs-link-underline-opacity:0.5}.link-underline-opacity-75{--bs-link-underline-opacity:0.75}.link-underline-opacity-75-hover:hover{--bs-link-underline-opacity:0.75}.link-underline-opacity-100{--bs-link-underline-opacity:1}.link-underline-opacity-100-hover:hover{--bs-link-underline-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-body-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-bg-rgb),var(--bs-bg-opacity))!important}.bg-body-tertiary{--bs-bg-opacity:1;background-color:rgba(var(--bs-tertiary-bg-rgb),var(--bs-bg-opacity))!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-primary-subtle{background-color:var(--bs-primary-bg-subtle)!important}.bg-secondary-subtle{background-color:var(--bs-secondary-bg-subtle)!important}.bg-success-subtle{background-color:var(--bs-success-bg-subtle)!important}.bg-info-subtle{background-color:var(--bs-info-bg-subtle)!important}.bg-warning-subtle{background-color:var(--bs-warning-bg-subtle)!important}.bg-danger-subtle{background-color:var(--bs-danger-bg-subtle)!important}.bg-light-subtle{background-color:var(--bs-light-bg-subtle)!important}.bg-dark-subtle{background-color:var(--bs-dark-bg-subtle)!important}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:var(--bs-border-radius)!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:var(--bs-border-radius-sm)!important}.rounded-2{border-radius:var(--bs-border-radius)!important}.rounded-3{border-radius:var(--bs-border-radius-lg)!important}.rounded-4{border-radius:var(--bs-border-radius-xl)!important}.rounded-5{border-radius:var(--bs-border-radius-xxl)!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:var(--bs-border-radius-pill)!important}.rounded-top{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-0{border-top-left-radius:0!important;border-top-right-radius:0!important}.rounded-top-1{border-top-left-radius:var(--bs-border-radius-sm)!important;border-top-right-radius:var(--bs-border-radius-sm)!important}.rounded-top-2{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-3{border-top-left-radius:var(--bs-border-radius-lg)!important;border-top-right-radius:var(--bs-border-radius-lg)!important}.rounded-top-4{border-top-left-radius:var(--bs-border-radius-xl)!important;border-top-right-radius:var(--bs-border-radius-xl)!important}.rounded-top-5{border-top-left-radius:var(--bs-border-radius-xxl)!important;border-top-right-radius:var(--bs-border-radius-xxl)!important}.rounded-top-circle{border-top-left-radius:50%!important;border-top-right-radius:50%!important}.rounded-top-pill{border-top-left-radius:var(--bs-border-radius-pill)!important;border-top-right-radius:var(--bs-border-radius-pill)!important}.rounded-end{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-0{border-top-right-radius:0!important;border-bottom-right-radius:0!important}.rounded-end-1{border-top-right-radius:var(--bs-border-radius-sm)!important;border-bottom-right-radius:var(--bs-border-radius-sm)!important}.rounded-end-2{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-3{border-top-right-radius:var(--bs-border-radius-lg)!important;border-bottom-right-radius:var(--bs-border-radius-lg)!important}.rounded-end-4{border-top-right-radius:var(--bs-border-radius-xl)!important;border-bottom-right-radius:var(--bs-border-radius-xl)!important}.rounded-end-5{border-top-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-right-radius:var(--bs-border-radius-xxl)!important}.rounded-end-circle{border-top-right-radius:50%!important;border-bottom-right-radius:50%!important}.rounded-end-pill{border-top-right-radius:var(--bs-border-radius-pill)!important;border-bottom-right-radius:var(--bs-border-radius-pill)!important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-0{border-bottom-right-radius:0!important;border-bottom-left-radius:0!important}.rounded-bottom-1{border-bottom-right-radius:var(--bs-border-radius-sm)!important;border-bottom-left-radius:var(--bs-border-radius-sm)!important}.rounded-bottom-2{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-3{border-bottom-right-radius:var(--bs-border-radius-lg)!important;border-bottom-left-radius:var(--bs-border-radius-lg)!important}.rounded-bottom-4{border-bottom-right-radius:var(--bs-border-radius-xl)!important;border-bottom-left-radius:var(--bs-border-radius-xl)!important}.rounded-bottom-5{border-bottom-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-left-radius:var(--bs-border-radius-xxl)!important}.rounded-bottom-circle{border-bottom-right-radius:50%!important;border-bottom-left-radius:50%!important}.rounded-bottom-pill{border-bottom-right-radius:var(--bs-border-radius-pill)!important;border-bottom-left-radius:var(--bs-border-radius-pill)!important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-0{border-bottom-left-radius:0!important;border-top-left-radius:0!important}.rounded-start-1{border-bottom-left-radius:var(--bs-border-radius-sm)!important;border-top-left-radius:var(--bs-border-radius-sm)!important}.rounded-start-2{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-3{border-bottom-left-radius:var(--bs-border-radius-lg)!important;border-top-left-radius:var(--bs-border-radius-lg)!important}.rounded-start-4{border-bottom-left-radius:var(--bs-border-radius-xl)!important;border-top-left-radius:var(--bs-border-radius-xl)!important}.rounded-start-5{border-bottom-left-radius:var(--bs-border-radius-xxl)!important;border-top-left-radius:var(--bs-border-radius-xxl)!important}.rounded-start-circle{border-bottom-left-radius:50%!important;border-top-left-radius:50%!important}.rounded-start-pill{border-bottom-left-radius:var(--bs-border-radius-pill)!important;border-top-left-radius:var(--bs-border-radius-pill)!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}.z-n1{z-index:-1!important}.z-0{z-index:0!important}.z-1{z-index:1!important}.z-2{z-index:2!important}.z-3{z-index:3!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.object-fit-sm-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-sm-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-sm-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-sm-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-sm-none{-o-object-fit:none!important;object-fit:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-inline-grid{display:inline-grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.row-gap-sm-0{row-gap:0!important}.row-gap-sm-1{row-gap:.25rem!important}.row-gap-sm-2{row-gap:.5rem!important}.row-gap-sm-3{row-gap:1rem!important}.row-gap-sm-4{row-gap:1.5rem!important}.row-gap-sm-5{row-gap:3rem!important}.column-gap-sm-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-sm-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-sm-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-sm-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-sm-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-sm-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.object-fit-md-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-md-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-md-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-md-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-md-none{-o-object-fit:none!important;object-fit:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-inline-grid{display:inline-grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.row-gap-md-0{row-gap:0!important}.row-gap-md-1{row-gap:.25rem!important}.row-gap-md-2{row-gap:.5rem!important}.row-gap-md-3{row-gap:1rem!important}.row-gap-md-4{row-gap:1.5rem!important}.row-gap-md-5{row-gap:3rem!important}.column-gap-md-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-md-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-md-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-md-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-md-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-md-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.object-fit-lg-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-lg-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-lg-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-lg-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-lg-none{-o-object-fit:none!important;object-fit:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-inline-grid{display:inline-grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.row-gap-lg-0{row-gap:0!important}.row-gap-lg-1{row-gap:.25rem!important}.row-gap-lg-2{row-gap:.5rem!important}.row-gap-lg-3{row-gap:1rem!important}.row-gap-lg-4{row-gap:1.5rem!important}.row-gap-lg-5{row-gap:3rem!important}.column-gap-lg-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-lg-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-lg-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-lg-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-lg-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-lg-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.object-fit-xl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xl-none{-o-object-fit:none!important;object-fit:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-inline-grid{display:inline-grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.row-gap-xl-0{row-gap:0!important}.row-gap-xl-1{row-gap:.25rem!important}.row-gap-xl-2{row-gap:.5rem!important}.row-gap-xl-3{row-gap:1rem!important}.row-gap-xl-4{row-gap:1.5rem!important}.row-gap-xl-5{row-gap:3rem!important}.column-gap-xl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xl-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.object-fit-xxl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xxl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xxl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xxl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xxl-none{-o-object-fit:none!important;object-fit:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-inline-grid{display:inline-grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.row-gap-xxl-0{row-gap:0!important}.row-gap-xxl-1{row-gap:.25rem!important}.row-gap-xxl-2{row-gap:.5rem!important}.row-gap-xxl-3{row-gap:1rem!important}.row-gap-xxl-4{row-gap:1.5rem!important}.row-gap-xxl-5{row-gap:3rem!important}.column-gap-xxl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xxl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xxl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xxl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xxl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xxl-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-inline-grid{display:inline-grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} /*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/teraserver/python/static/bootstrap/css/bootstrap.min.css.map b/teraserver/python/static/bootstrap/css/bootstrap.min.css.map index 4eb463791..90ce79873 100644 --- a/teraserver/python/static/bootstrap/css/bootstrap.min.css.map +++ b/teraserver/python/static/bootstrap/css/bootstrap.min.css.map @@ -1 +1 @@ -{"version":3,"sources":["../../scss/bootstrap.scss","../../scss/_root.scss","../../scss/_reboot.scss","dist/css/bootstrap.css","../../scss/vendor/_rfs.scss","bootstrap.css","../../scss/mixins/_hover.scss","../../scss/_type.scss","../../scss/mixins/_lists.scss","../../scss/_images.scss","../../scss/mixins/_image.scss","../../scss/mixins/_border-radius.scss","../../scss/_code.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/mixins/_breakpoints.scss","../../scss/mixins/_grid-framework.scss","../../scss/_tables.scss","../../scss/mixins/_table-row.scss","../../scss/_forms.scss","../../scss/mixins/_transition.scss","../../scss/mixins/_forms.scss","../../scss/mixins/_gradients.scss","../../scss/_buttons.scss","../../scss/mixins/_buttons.scss","../../scss/_transitions.scss","../../scss/_dropdown.scss","../../scss/mixins/_caret.scss","../../scss/mixins/_nav-divider.scss","../../scss/_button-group.scss","../../scss/_input-group.scss","../../scss/_custom-forms.scss","../../scss/_nav.scss","../../scss/_navbar.scss","../../scss/_card.scss","../../scss/_breadcrumb.scss","../../scss/_pagination.scss","../../scss/mixins/_pagination.scss","../../scss/_badge.scss","../../scss/mixins/_badge.scss","../../scss/_jumbotron.scss","../../scss/_alert.scss","../../scss/mixins/_alert.scss","../../scss/_progress.scss","../../scss/_media.scss","../../scss/_list-group.scss","../../scss/mixins/_list-group.scss","../../scss/_close.scss","../../scss/_toasts.scss","../../scss/_modal.scss","../../scss/_tooltip.scss","../../scss/mixins/_reset-text.scss","../../scss/_popover.scss","../../scss/_carousel.scss","../../scss/mixins/_clearfix.scss","../../scss/_spinners.scss","../../scss/utilities/_align.scss","../../scss/mixins/_background-variant.scss","../../scss/utilities/_background.scss","../../scss/utilities/_borders.scss","../../scss/utilities/_display.scss","../../scss/utilities/_embed.scss","../../scss/utilities/_flex.scss","../../scss/utilities/_float.scss","../../scss/utilities/_interactions.scss","../../scss/utilities/_overflow.scss","../../scss/utilities/_position.scss","../../scss/utilities/_screenreaders.scss","../../scss/mixins/_screen-reader.scss","../../scss/utilities/_shadows.scss","../../scss/utilities/_sizing.scss","../../scss/utilities/_spacing.scss","../../scss/utilities/_stretched-link.scss","../../scss/utilities/_text.scss","../../scss/mixins/_text-truncate.scss","../../scss/mixins/_text-emphasis.scss","../../scss/mixins/_text-hide.scss","../../scss/utilities/_visibility.scss","../../scss/_print.scss"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Bootstrap v4.6.2 (https://getbootstrap.com/)\n * Copyright 2011-2022 The Bootstrap Authors\n * Copyright 2011-2022 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"root\";\n@import \"reboot\";\n@import \"type\";\n@import \"images\";\n@import \"code\";\n@import \"grid\";\n@import \"tables\";\n@import \"forms\";\n@import \"buttons\";\n@import \"transitions\";\n@import \"dropdown\";\n@import \"button-group\";\n@import \"input-group\";\n@import \"custom-forms\";\n@import \"nav\";\n@import \"navbar\";\n@import \"card\";\n@import \"breadcrumb\";\n@import \"pagination\";\n@import \"badge\";\n@import \"jumbotron\";\n@import \"alert\";\n@import \"progress\";\n@import \"media\";\n@import \"list-group\";\n@import \"close\";\n@import \"toasts\";\n@import \"modal\";\n@import \"tooltip\";\n@import \"popover\";\n@import \"carousel\";\n@import \"spinners\";\n@import \"utilities\";\n@import \"print\";\n",":root {\n // Custom variable values only support SassScript inside `#{}`.\n @each $color, $value in $colors {\n --#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors {\n --#{$color}: #{$value};\n }\n\n @each $bp, $value in $grid-breakpoints {\n --breakpoint-#{$bp}: #{$value};\n }\n\n // Use `inspect` for lists so that quoted items keep the quotes.\n // See https://github.com/sass/sass/issues/2383#issuecomment-336349172\n --font-family-sans-serif: #{inspect($font-family-sans-serif)};\n --font-family-monospace: #{inspect($font-family-monospace)};\n}\n","// stylelint-disable declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// 1. Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n// 2. Change the default font family in all browsers.\n// 3. Correct the line height in all browsers.\n// 4. Prevent adjustments of font size after orientation changes in IE on Windows Phone and in iOS.\n// 5. Change the default tap highlight to be completely transparent in iOS.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box; // 1\n}\n\nhtml {\n font-family: sans-serif; // 2\n line-height: 1.15; // 3\n -webkit-text-size-adjust: 100%; // 4\n -webkit-tap-highlight-color: rgba($black, 0); // 5\n}\n\n// Shim for \"new\" HTML5 structural elements to display correctly (IE10, older browsers)\n// TODO: remove in v5\n// stylelint-disable-next-line selector-list-comma-newline-after\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Set an explicit initial text-align value so that we can later use\n// the `inherit` value on things like `` elements.\n\nbody {\n margin: 0; // 1\n font-family: $font-family-base;\n @include font-size($font-size-base);\n font-weight: $font-weight-base;\n line-height: $line-height-base;\n color: $body-color;\n text-align: left; // 3\n background-color: $body-bg; // 2\n}\n\n// Future-proof rule: in browsers that support :focus-visible, suppress the focus outline\n// on elements that programmatically receive focus but wouldn't normally show a visible\n// focus outline. In general, this would mean that the outline is only applied if the\n// interaction that led to the element receiving programmatic focus was a keyboard interaction,\n// or the browser has somehow determined that the user is primarily a keyboard user and/or\n// wants focus outlines to always be presented.\n//\n// See https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible\n// and https://developer.paciellogroup.com/blog/2018/03/focus-visible-and-backwards-compatibility/\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\n\n// Content grouping\n//\n// 1. Add the correct box sizing in Firefox.\n// 2. Show the overflow in Edge and IE.\n\nhr {\n box-sizing: content-box; // 1\n height: 0; // 1\n overflow: visible; // 2\n}\n\n\n//\n// Typography\n//\n\n// Remove top margins from headings\n//\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n// stylelint-disable-next-line selector-list-comma-newline-after\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: $headings-margin-bottom;\n}\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Remove the bottom border in Firefox 39-.\n// 5. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-original-title] { // 1\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 4\n text-decoration-skip-ink: none; // 5\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: $font-weight-bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n\nsmall {\n @include font-size(80%); // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n @include font-size(75%);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n\n @include hover() {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n color: inherit;\n text-decoration: none;\n\n @include hover() {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n//\n// Code\n//\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-monospace;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n}\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n // Disable auto-hiding scrollbar in IE & legacy Edge to avoid overlap,\n // making it impossible to interact with the content\n -ms-overflow-style: scrollbar;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg {\n // Workaround for the SVG overflow bug in IE10/11 is still required.\n // See https://github.com/twbs/bootstrap/issues/26878\n overflow: hidden;\n vertical-align: middle;\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $table-caption-color;\n text-align: left;\n caption-side: bottom;\n}\n\n// 1. Removes font-weight bold by inheriting\n// 2. Matches default `` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\n\n//\n// Forms\n//\n\nlabel {\n // Allow labels to use `margin` for spacing.\n display: inline-block;\n margin-bottom: $label-margin-bottom;\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24093\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // Remove the margin in Firefox and Safari\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible; // Show the overflow in Edge\n}\n\nbutton,\nselect {\n text-transform: none; // Remove the inheritance of text transform in Firefox\n}\n\n// Set the cursor for non-` + + +

+ + + + + + + + + + + + diff --git a/teraserver/python/templates/login_change_password.html b/teraserver/python/templates/login_change_password.html new file mode 100644 index 000000000..26b4f8d84 --- /dev/null +++ b/teraserver/python/templates/login_change_password.html @@ -0,0 +1,135 @@ + + + + + {{ gettext("OpenTera - Change Password") }} + + + + + + + + + + + + + + +
+
+
+
+
+
+ {{ gettext('Password successfully changed!') }}
+ {{ gettext('Redirecting to login screen...') }} +
+
+
+ +
+
+
{{ gettext('Password change required') }} - {{ username }}
+
5:00
+
+
+
+
+ +
+ +
+ + +
+ +
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+ + diff --git a/teraserver/python/templates/login_setup_2fa.html b/teraserver/python/templates/login_setup_2fa.html new file mode 100644 index 000000000..7f1c4962d --- /dev/null +++ b/teraserver/python/templates/login_setup_2fa.html @@ -0,0 +1,167 @@ + + + + + OpenTera Setup 2FA + + + + + + + + + + + + + + + + + + +
+
+

{{ gettext('You need to setup multi-factor authentication before continuing.') }}

+
+
+
+
+
+ +
+
+
5:00
+
+
+
+
+ +
+
+ +
+
    +
  1. {{ gettext('Scan the QR code with your authenticator app') }}
  2. +
  3. {{ gettext('Enter the generated code:') }}
    + +
  4. +
+ + + +
+
+ +
+ +
+
+ + + + +
+
+
+
+
+
+ + + + diff --git a/teraserver/python/templates/login_validate_2fa.html b/teraserver/python/templates/login_validate_2fa.html new file mode 100644 index 000000000..d976c4847 --- /dev/null +++ b/teraserver/python/templates/login_validate_2fa.html @@ -0,0 +1,181 @@ + + + + + OpenTera Login Page + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+ +
+
+
{{ gettext('Multi Factor Authentication') }}
+
5:00
+
+
+ +
+ +
+ +
+ + +
+ +
+
+
+ +
+
+ +
+ +
+
+
+
+
+ + \ No newline at end of file diff --git a/teraserver/python/tests/__init__.py b/teraserver/python/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/teraserver/python/tests/modules/FlaskModule/API/__init__.py b/teraserver/python/tests/modules/FlaskModule/API/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/teraserver/python/tests/modules/FlaskModule/API/device/__init__.py b/teraserver/python/tests/modules/FlaskModule/API/device/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceLogin.py b/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceLogin.py index 84f95952c..609a6c04f 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceLogin.py +++ b/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceLogin.py @@ -1,4 +1,4 @@ -from BaseDeviceAPITest import BaseDeviceAPITest +from tests.modules.FlaskModule.API.device.BaseDeviceAPITest import BaseDeviceAPITest from opentera.db.models.TeraDevice import TeraDevice diff --git a/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceLogout.py b/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceLogout.py index ee2f24d18..bee36e8ce 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceLogout.py +++ b/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceLogout.py @@ -1,4 +1,4 @@ -from BaseDeviceAPITest import BaseDeviceAPITest +from tests.modules.FlaskModule.API.device.BaseDeviceAPITest import BaseDeviceAPITest from opentera.db.models.TeraDevice import TeraDevice diff --git a/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceQueryAssets.py b/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceQueryAssets.py index 429b303f3..3513240bf 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceQueryAssets.py +++ b/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceQueryAssets.py @@ -1,5 +1,5 @@ from typing import List -from BaseDeviceAPITest import BaseDeviceAPITest +from tests.modules.FlaskModule.API.device.BaseDeviceAPITest import BaseDeviceAPITest from opentera.db.models.TeraDevice import TeraDevice from opentera.db.models.TeraAsset import TeraAsset from opentera.db.models.TeraSession import TeraSession diff --git a/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceQueryDevices.py b/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceQueryDevices.py index 9363b2bac..3e3e24a4d 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceQueryDevices.py +++ b/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceQueryDevices.py @@ -1,4 +1,4 @@ -from BaseDeviceAPITest import BaseDeviceAPITest +from tests.modules.FlaskModule.API.device.BaseDeviceAPITest import BaseDeviceAPITest from opentera.db.models.TeraDevice import TeraDevice diff --git a/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceQueryParticipants.py b/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceQueryParticipants.py index ec71c8712..6869a1730 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceQueryParticipants.py +++ b/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceQueryParticipants.py @@ -1,4 +1,4 @@ -from BaseDeviceAPITest import BaseDeviceAPITest +from tests.modules.FlaskModule.API.device.BaseDeviceAPITest import BaseDeviceAPITest from opentera.db.models.TeraDevice import TeraDevice diff --git a/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceQuerySessionEvents.py b/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceQuerySessionEvents.py index c2f7ade64..66e928596 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceQuerySessionEvents.py +++ b/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceQuerySessionEvents.py @@ -1,4 +1,4 @@ -from BaseDeviceAPITest import BaseDeviceAPITest +from tests.modules.FlaskModule.API.device.BaseDeviceAPITest import BaseDeviceAPITest from opentera.db.models.TeraDevice import TeraDevice from opentera.db.models.TeraSession import TeraSession from opentera.db.models.TeraSessionEvent import TeraSessionEvent diff --git a/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceQuerySessions.py b/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceQuerySessions.py index 358839fa3..9f0de6e2c 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceQuerySessions.py +++ b/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceQuerySessions.py @@ -1,4 +1,4 @@ -from BaseDeviceAPITest import BaseDeviceAPITest +from tests.modules.FlaskModule.API.device.BaseDeviceAPITest import BaseDeviceAPITest from opentera.db.models.TeraDevice import TeraDevice from opentera.db.models.TeraSession import TeraSession from modules.DatabaseModule.DBManagerTeraDeviceAccess import DBManagerTeraDeviceAccess diff --git a/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceQueryStatus.py b/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceQueryStatus.py index 26b3053f6..78fbdb3c7 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceQueryStatus.py +++ b/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceQueryStatus.py @@ -1,4 +1,4 @@ -from BaseDeviceAPITest import BaseDeviceAPITest +from tests.modules.FlaskModule.API.device.BaseDeviceAPITest import BaseDeviceAPITest from opentera.db.models.TeraDevice import TeraDevice from opentera.db.models.TeraSession import TeraSession from modules.DatabaseModule.DBManagerTeraDeviceAccess import DBManagerTeraDeviceAccess diff --git a/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceRegister.py b/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceRegister.py index c347e1038..cab8355f0 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceRegister.py +++ b/teraserver/python/tests/modules/FlaskModule/API/device/test_DeviceRegister.py @@ -1,4 +1,4 @@ -from BaseDeviceAPITest import BaseDeviceAPITest +from tests.modules.FlaskModule.API.device.BaseDeviceAPITest import BaseDeviceAPITest from opentera.db.models.TeraServerSettings import TeraServerSettings from opentera.db.models.TeraDevice import TeraDevice from opentera.db.models.TeraDeviceType import TeraDeviceType diff --git a/teraserver/python/tests/modules/FlaskModule/API/participant/__init__.py b/teraserver/python/tests/modules/FlaskModule/API/participant/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantLogin.py b/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantLogin.py index 766596019..d35aaeda5 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantLogin.py +++ b/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantLogin.py @@ -1,4 +1,4 @@ -from BaseParticipantAPITest import BaseParticipantAPITest +from tests.modules.FlaskModule.API.participant.BaseParticipantAPITest import BaseParticipantAPITest from opentera.db.models.TeraParticipant import TeraParticipant from opentera.db.models.TeraSessionType import TeraSessionType diff --git a/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantLogout.py b/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantLogout.py index cd1fcff4e..633e1c579 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantLogout.py +++ b/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantLogout.py @@ -1,4 +1,4 @@ -from BaseParticipantAPITest import BaseParticipantAPITest +from tests.modules.FlaskModule.API.participant.BaseParticipantAPITest import BaseParticipantAPITest from opentera.db.models.TeraParticipant import TeraParticipant diff --git a/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantQueryAssets.py b/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantQueryAssets.py index cdc4b7627..83fce2da2 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantQueryAssets.py +++ b/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantQueryAssets.py @@ -1,5 +1,5 @@ from typing import List -from BaseParticipantAPITest import BaseParticipantAPITest +from tests.modules.FlaskModule.API.participant.BaseParticipantAPITest import BaseParticipantAPITest from opentera.db.models.TeraAsset import TeraAsset from opentera.db.models.TeraParticipant import TeraParticipant from opentera.db.models.TeraSession import TeraSession diff --git a/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantQueryDevices.py b/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantQueryDevices.py index 1f7aaaf23..0aee1f29a 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantQueryDevices.py +++ b/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantQueryDevices.py @@ -1,4 +1,4 @@ -from BaseParticipantAPITest import BaseParticipantAPITest +from tests.modules.FlaskModule.API.participant.BaseParticipantAPITest import BaseParticipantAPITest class ParticipantQueryDevicesTest(BaseParticipantAPITest): diff --git a/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantQueryParticipants.py b/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantQueryParticipants.py index 7a33afd78..e1ade5e55 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantQueryParticipants.py +++ b/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantQueryParticipants.py @@ -1,4 +1,4 @@ -from BaseParticipantAPITest import BaseParticipantAPITest +from tests.modules.FlaskModule.API.participant.BaseParticipantAPITest import BaseParticipantAPITest class ParticipantQueryParticipantsTest(BaseParticipantAPITest): diff --git a/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantQuerySessions.py b/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantQuerySessions.py index b153ed800..2daf8a411 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantQuerySessions.py +++ b/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantQuerySessions.py @@ -1,5 +1,5 @@ from datetime import datetime, timedelta -from BaseParticipantAPITest import BaseParticipantAPITest +from tests.modules.FlaskModule.API.participant.BaseParticipantAPITest import BaseParticipantAPITest from modules.DatabaseModule.DBManagerTeraParticipantAccess import DBManagerTeraParticipantAccess from opentera.db.models.TeraParticipant import TeraParticipant from opentera.db.models.TeraSession import TeraSession diff --git a/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantRefreshToken.py b/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantRefreshToken.py index f9fefe4a2..c8d64d7d1 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantRefreshToken.py +++ b/teraserver/python/tests/modules/FlaskModule/API/participant/test_ParticipantRefreshToken.py @@ -1,4 +1,4 @@ -from BaseParticipantAPITest import BaseParticipantAPITest +from tests.modules.FlaskModule.API.participant.BaseParticipantAPITest import BaseParticipantAPITest class ParticipantRefreshTokenTest(BaseParticipantAPITest): diff --git a/teraserver/python/tests/modules/FlaskModule/API/service/__init__.py b/teraserver/python/tests/modules/FlaskModule/API/service/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryAccess.py b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryAccess.py index c2c368e32..f2d7b5723 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryAccess.py +++ b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryAccess.py @@ -1,4 +1,4 @@ -from BaseServiceAPITest import BaseServiceAPITest +from tests.modules.FlaskModule.API.service.BaseServiceAPITest import BaseServiceAPITest from opentera.db.models.TeraDevice import TeraDevice from opentera.db.models.TeraParticipant import TeraParticipant from opentera.db.models.TeraUser import TeraUser @@ -214,4 +214,3 @@ def test_get_endpoint_with_token_auth_all_params_admin_and_not_admin_for_device_ # TODO complete tests with devices access = DBManagerTeraDeviceAccess(device) - diff --git a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryAssets.py b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryAssets.py index 45367e339..de14650e4 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryAssets.py +++ b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryAssets.py @@ -1,4 +1,4 @@ -from BaseServiceAPITest import BaseServiceAPITest +from tests.modules.FlaskModule.API.service.BaseServiceAPITest import BaseServiceAPITest from modules.FlaskModule.FlaskModule import flask_app from datetime import datetime diff --git a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryDevices.py b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryDevices.py index 24ed98adf..76c1a01c2 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryDevices.py +++ b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryDevices.py @@ -1,6 +1,6 @@ from typing import List -from BaseServiceAPITest import BaseServiceAPITest +from tests.modules.FlaskModule.API.service.BaseServiceAPITest import BaseServiceAPITest from modules.FlaskModule.FlaskModule import flask_app from opentera.db.models.TeraDevice import TeraDevice from opentera.db.models.TeraDeviceSubType import TeraDeviceSubType diff --git a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryDisconnect.py b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryDisconnect.py index e0511aef6..ced6bd053 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryDisconnect.py +++ b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryDisconnect.py @@ -1,6 +1,6 @@ from typing import List -from BaseServiceAPITest import BaseServiceAPITest +from tests.modules.FlaskModule.API.service.BaseServiceAPITest import BaseServiceAPITest from opentera.db.models.TeraDevice import TeraDevice from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraParticipant import TeraParticipant diff --git a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryParticipantGroups.py b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryParticipantGroups.py index 4138754d5..651954ab2 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryParticipantGroups.py +++ b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryParticipantGroups.py @@ -1,4 +1,4 @@ -from BaseServiceAPITest import BaseServiceAPITest +from tests.modules.FlaskModule.API.service.BaseServiceAPITest import BaseServiceAPITest from opentera.db.models import TeraParticipantGroup @@ -213,4 +213,4 @@ def test_post_and_delete_endpoint_with_token(self): # Test case: Delete with no problem response = self._delete_with_service_token_auth(self.test_client, token=self.service_token, params={'id': 3}) - self.assertEqual(200, response.status_code, msg="Delete OK") \ No newline at end of file + self.assertEqual(200, response.status_code, msg="Delete OK") diff --git a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryParticipants.py b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryParticipants.py index 25e1789c6..2cb9473d6 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryParticipants.py +++ b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryParticipants.py @@ -1,6 +1,6 @@ from typing import List -from BaseServiceAPITest import BaseServiceAPITest +from tests.modules.FlaskModule.API.service.BaseServiceAPITest import BaseServiceAPITest from opentera.db.models.TeraParticipant import TeraParticipant from opentera.db.models.TeraProject import TeraProject diff --git a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryProjects.py b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryProjects.py index 235b1400e..d64653743 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryProjects.py +++ b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryProjects.py @@ -1,4 +1,4 @@ -from BaseServiceAPITest import BaseServiceAPITest +from tests.modules.FlaskModule.API.service.BaseServiceAPITest import BaseServiceAPITest from opentera.db.models.TeraProject import TeraProject from opentera.db.models.TeraSite import TeraSite from opentera.db.models.TeraServiceProject import TeraServiceProject diff --git a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryRoles.py b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryRoles.py index 621d09b29..6be36e9ae 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryRoles.py +++ b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryRoles.py @@ -1,4 +1,4 @@ -from BaseServiceAPITest import BaseServiceAPITest +from tests.modules.FlaskModule.API.service.BaseServiceAPITest import BaseServiceAPITest from opentera.db.models.TeraServiceRole import TeraServiceRole diff --git a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryServiceAccess.py b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryServiceAccess.py index c02583912..b7d1c7807 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryServiceAccess.py +++ b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryServiceAccess.py @@ -1,4 +1,4 @@ -from BaseServiceAPITest import BaseServiceAPITest +from tests.modules.FlaskModule.API.service.BaseServiceAPITest import BaseServiceAPITest from opentera.db.models.TeraServiceAccess import TeraServiceAccess from opentera.db.models.TeraServiceRole import TeraServiceRole @@ -151,4 +151,4 @@ def _checkJson(self, json_data, minimal=False): self.assertFalse(json_data.__contains__('service_name')) self.assertFalse(json_data.__contains__('service_key')) self.assertFalse(json_data.__contains__('id_service')) - self.assertFalse(json_data.__contains__('service_access_role_name')) \ No newline at end of file + self.assertFalse(json_data.__contains__('service_access_role_name')) diff --git a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryServices.py b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryServices.py index dc799a0aa..1fedd5d57 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryServices.py +++ b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryServices.py @@ -1,6 +1,6 @@ from typing import List -from BaseServiceAPITest import BaseServiceAPITest +from tests.modules.FlaskModule.API.service.BaseServiceAPITest import BaseServiceAPITest from modules.FlaskModule.FlaskModule import flask_app from opentera.db.models.TeraService import TeraService diff --git a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQuerySessionEvents.py b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQuerySessionEvents.py index a9e5f533e..5243f0aba 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQuerySessionEvents.py +++ b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQuerySessionEvents.py @@ -1,4 +1,4 @@ -from BaseServiceAPITest import BaseServiceAPITest +from tests.modules.FlaskModule.API.service.BaseServiceAPITest import BaseServiceAPITest from modules.FlaskModule.FlaskModule import flask_app diff --git a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQuerySessionTypes.py b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQuerySessionTypes.py index bf8725322..79e5df996 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQuerySessionTypes.py +++ b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQuerySessionTypes.py @@ -1,5 +1,5 @@ from typing import List -from BaseServiceAPITest import BaseServiceAPITest +from tests.modules.FlaskModule.API.service.BaseServiceAPITest import BaseServiceAPITest from modules.FlaskModule.FlaskModule import flask_app from opentera.db.models.TeraSessionType import TeraSessionType from opentera.db.models.TeraSessionTypeSite import TeraSessionTypeSite diff --git a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQuerySessions.py b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQuerySessions.py index 567d36753..51eb2d3b8 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQuerySessions.py +++ b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQuerySessions.py @@ -1,4 +1,4 @@ -from BaseServiceAPITest import BaseServiceAPITest +from tests.modules.FlaskModule.API.service.BaseServiceAPITest import BaseServiceAPITest from modules.FlaskModule.FlaskModule import flask_app from datetime import datetime, timedelta from opentera.db.models.TeraSession import TeraSession diff --git a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQuerySiteProjectAccessRoles.py b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQuerySiteProjectAccessRoles.py index 642b6dabe..ef9ccb269 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQuerySiteProjectAccessRoles.py +++ b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQuerySiteProjectAccessRoles.py @@ -1,6 +1,6 @@ from typing import List -from BaseServiceAPITest import BaseServiceAPITest +from tests.modules.FlaskModule.API.service.BaseServiceAPITest import BaseServiceAPITest from modules.FlaskModule.FlaskModule import flask_app from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraSite import TeraSite @@ -172,4 +172,4 @@ def test_get_endpoint_with_token_auth_and_project_user(self): role = user_access.get_user_project_role(user.id_user, project.id_project) self.assertEqual({'project_role': role['project_role']}, response.json) else: - self.assertEqual({'project_role': None}, response.json) \ No newline at end of file + self.assertEqual({'project_role': None}, response.json) diff --git a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQuerySites.py b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQuerySites.py index 364175613..d9430e662 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQuerySites.py +++ b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQuerySites.py @@ -1,6 +1,6 @@ from typing import List -from BaseServiceAPITest import BaseServiceAPITest +from tests.modules.FlaskModule.API.service.BaseServiceAPITest import BaseServiceAPITest from modules.FlaskModule.FlaskModule import flask_app from opentera.db.models.TeraSite import TeraSite from opentera.db.models.TeraUser import TeraUser diff --git a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryTestTypeProjects.py b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryTestTypeProjects.py index 3e9a20122..694b566c5 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryTestTypeProjects.py +++ b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryTestTypeProjects.py @@ -1,4 +1,4 @@ -from BaseServiceAPITest import BaseServiceAPITest +from tests.modules.FlaskModule.API.service.BaseServiceAPITest import BaseServiceAPITest from opentera.db.models.TeraTestTypeProject import TeraTestTypeProject from opentera.db.models.TeraTest import TeraTest from opentera.db.models.TeraSession import TeraSession diff --git a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryTestTypes.py b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryTestTypes.py index e7d631693..8bd73db09 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryTestTypes.py +++ b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryTestTypes.py @@ -1,5 +1,5 @@ from typing import List -from BaseServiceAPITest import BaseServiceAPITest +from tests.modules.FlaskModule.API.service.BaseServiceAPITest import BaseServiceAPITest from opentera.db.models.TeraTestType import TeraTestType from opentera.db.models.TeraTestTypeSite import TeraTestTypeSite from opentera.db.models.TeraTestTypeProject import TeraTestTypeProject @@ -214,4 +214,4 @@ def _checkJson(self, json_data, minimal=False): if not minimal: self.assertTrue(json_data.__contains__('test_type_description')) self.assertTrue(json_data.__contains__('test_type_service_key')) - self.assertTrue(json_data.__contains__('test_type_service_uuid')) \ No newline at end of file + self.assertTrue(json_data.__contains__('test_type_service_uuid')) diff --git a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryTests.py b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryTests.py index fdd4e1412..c2bac7663 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryTests.py +++ b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryTests.py @@ -1,4 +1,4 @@ -from BaseServiceAPITest import BaseServiceAPITest +from tests.modules.FlaskModule.API.service.BaseServiceAPITest import BaseServiceAPITest from modules.FlaskModule.FlaskModule import flask_app from datetime import datetime diff --git a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryUserGroups.py b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryUserGroups.py index 2d81f19c0..72776da5c 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryUserGroups.py +++ b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryUserGroups.py @@ -1,4 +1,4 @@ -from BaseServiceAPITest import BaseServiceAPITest +from tests.modules.FlaskModule.API.service.BaseServiceAPITest import BaseServiceAPITest from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraUserUserGroup import TeraUserUserGroup from opentera.db.models.TeraServiceAccess import TeraServiceAccess diff --git a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryUsers.py b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryUsers.py index 3dd307422..cc19e9ed0 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryUsers.py +++ b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceQueryUsers.py @@ -1,6 +1,6 @@ from typing import List -from BaseServiceAPITest import BaseServiceAPITest +from tests.modules.FlaskModule.API.service.BaseServiceAPITest import BaseServiceAPITest from opentera.db.models.TeraUser import TeraUser diff --git a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceSessionManager.py b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceSessionManager.py index c94bbd65f..bb86dd0db 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceSessionManager.py +++ b/teraserver/python/tests/modules/FlaskModule/API/service/test_ServiceSessionManager.py @@ -1,6 +1,6 @@ from typing import List -from BaseServiceAPITest import BaseServiceAPITest +from tests.modules.FlaskModule.API.service.BaseServiceAPITest import BaseServiceAPITest from modules.FlaskModule.FlaskModule import flask_app from opentera.db.models.TeraSessionType import TeraSessionType diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/BaseUserAPITest.py b/teraserver/python/tests/modules/FlaskModule/API/user/BaseUserAPITest.py index 27ea64407..10d0459d4 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/BaseUserAPITest.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/BaseUserAPITest.py @@ -151,65 +151,90 @@ def setup_redis_keys(self): TeraServerSettings.get_server_setting_value( TeraServerSettings.ServerParticipantTokenKey)) - def _get_with_user_token_auth(self, client: FlaskClient, token: str = '', params=None, endpoint=None): + def _get_with_user_token_auth(self, client: FlaskClient, token: str = '', params=None, endpoint=None, + opt_headers: dict = None): if params is None: params = {} if endpoint is None: endpoint = self.test_endpoint headers = {'Authorization': 'OpenTera ' + token} + if opt_headers is not None: + headers.update(opt_headers) + return client.get(endpoint, headers=headers, query_string=params) def _get_with_user_http_auth(self, client: FlaskClient, username: str = '', password: str = '', - params=None, endpoint=None): + params: dict = None, endpoint: str = None, opt_headers: dict = None): if params is None: - params = {} + params = {} if endpoint is None: endpoint = self.test_endpoint headers = {'Authorization': _basic_auth_str(username, password)} + if opt_headers is not None: + headers.update(opt_headers) + return client.get(endpoint, headers=headers, query_string=params) def _post_with_user_token_auth(self, client: FlaskClient, token: str = '', json: dict = {}, - params: dict = None, endpoint: str = None): + params: dict = None, endpoint: str = None, opt_headers: dict = None): if params is None: params = {} if endpoint is None: endpoint = self.test_endpoint headers = {'Authorization': 'OpenTera ' + token} + + if opt_headers is not None: + headers.update(opt_headers) + return client.post(endpoint, headers=headers, query_string=params, json=json) def _post_with_user_http_auth(self, client: FlaskClient, username: str = '', password: str = '', - json: dict = {}, params: dict = None, endpoint: str = None): + json: dict = {}, params: dict = None, endpoint: str = None, opt_headers: dict = None): if params is None: params = {} if endpoint is None: endpoint = self.test_endpoint headers = {'Authorization': _basic_auth_str(username, password)} + if opt_headers is not None: + headers.update(opt_headers) return client.post(endpoint, headers=headers, query_string=params, json=json) def _post_file_with_user_http_auth(self, client: FlaskClient, files: dict, username: str = '', password: str = '', - params: dict = None, endpoint: str = None): + params: dict = None, endpoint: str = None, opt_headers: dict = None): if params is None: params = {} if endpoint is None: endpoint = self.test_endpoint headers = {'Authorization': _basic_auth_str(username, password)} + + if opt_headers is not None: + headers.update(opt_headers) + return client.post(endpoint, headers=headers, query_string=params, data=files) def _delete_with_user_token_auth(self, client: FlaskClient, token: str = '', - params: dict = None, endpoint: str = None): + params: dict = None, endpoint: str = None, opt_headers: dict = None): if params is None: params = {} if endpoint is None: endpoint = self.test_endpoint headers = {'Authorization': 'OpenTera ' + token} + + if opt_headers is not None: + headers.update(opt_headers) + return client.delete(endpoint, headers=headers, query_string=params) def _delete_with_user_http_auth(self, client: FlaskClient, username: str = '', password: str = '', - params: dict = None, endpoint: str = None): + params: dict = None, endpoint: str = None, opt_headers: dict = None): if params is None: params = {} if endpoint is None: endpoint = self.test_endpoint headers = {'Authorization': _basic_auth_str(username, password)} + + if opt_headers is not None: + headers.update(opt_headers) + return client.delete(endpoint, headers=headers, query_string=params) diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/__init__.py b/teraserver/python/tests/modules/FlaskModule/API/user/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/_test_UserQueryServiceAccessToken.py b/teraserver/python/tests/modules/FlaskModule/API/user/_test_UserQueryServiceAccessToken.py index fceeb7138..ab97b91bc 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/_test_UserQueryServiceAccessToken.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/_test_UserQueryServiceAccessToken.py @@ -1,4 +1,4 @@ -from BaseUserAPITest import BaseUserAPITest +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest from opentera.db.models.TeraService import TeraService from opentera.db.models.TeraServiceRole import TeraServiceRole from opentera.db.models.TeraServiceProject import TeraServiceProject diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserLogin.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserLogin.py index 32ed14a2f..76fb490b5 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserLogin.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserLogin.py @@ -1,4 +1,4 @@ -from BaseUserAPITest import BaseUserAPITest +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest from opentera.db.models.TeraUser import TeraUser @@ -7,10 +7,39 @@ class UserLoginTest(BaseUserAPITest): def setUp(self): super().setUp() + # Create users with 2fa enabled + with self._flask_app.app_context(): + self.user1: dict = self._create_2fa_enabled_user('test_user_2fa_1', 'Password12345!', set_secret=True) + self.user2: dict = self._create_2fa_enabled_user('test_user_2fa_2', 'Password12345!', set_secret=False) def tearDown(self): + # Delete users with 2fa enabled + with self._flask_app.app_context(): + TeraUser.delete(self.user1['id_user'], hard_delete=True) + TeraUser.delete(self.user2['id_user'], hard_delete=True) super().tearDown() + def _create_2fa_enabled_user(self, username, password, set_secret:bool = True): + user = TeraUser() + user.id_user = 0 # New user + user.user_username = username + user.user_password = password + user.user_firstname = username + user.user_lastname = username + user.user_email = f"{username}@test.com" + user.user_enabled = True + user.user_profile = {} + if set_secret: + user.enable_2fa_otp() + else: + user.user_2fa_enabled = True + user.user_2fa_otp_enabled = False + user.user_2fa_otp_secret = None + + TeraUser.insert(user) + return user.to_json(minimal=False) + + def test_get_endpoint_no_auth(self): with self._flask_app.app_context(): response = self.test_client.get(self.test_endpoint) @@ -21,10 +50,12 @@ def test_get_endpoint_invalid_token_auth(self): response = self._get_with_user_token_auth(self.test_client, 'invalid') self.assertEqual(401, response.status_code) - def test_get_endpoint_login_admin_user_http_auth(self): + def test_get_endpoint_login_admin_user_http_auth_with_websocket(self): with self._flask_app.app_context(): # Using default participant information - response = self._get_with_user_http_auth(self.test_client, 'admin', 'admin') + response = self._get_with_user_http_auth(self.test_client, + 'admin', 'admin', + {'with_websocket': True}) self.assertEqual(200, response.status_code) self.assertEqual('application/json', response.headers['Content-Type']) @@ -35,6 +66,20 @@ def test_get_endpoint_login_admin_user_http_auth(self): self.assertTrue('user_uuid' in response.json) self.assertTrue('user_token' in response.json) + def test_get_endpoint_login_admin_user_http_auth_no_websocket(self): + with self._flask_app.app_context(): + # Using default participant information + response = self._get_with_user_http_auth(self.test_client, 'admin', 'admin') + + self.assertEqual(200, response.status_code) + self.assertEqual('application/json', response.headers['Content-Type']) + self.assertGreater(len(response.json), 0) + + # Validate fields in json response + self.assertTrue('user_uuid' in response.json) + self.assertTrue('user_token' in response.json) + self.assertFalse('websocket_url' in response.json) + def test_get_endpoint_login_admin_user_http_auth_then_token_auth(self): with self._flask_app.app_context(): # Using default participant information @@ -52,3 +97,33 @@ def test_get_endpoint_login_admin_user_http_auth_then_token_auth(self): # Not allowed for this endpoint response = self._get_with_user_token_auth(self.test_client, token=token) self.assertEqual(401, response.status_code) + + def test_get_endpoint_login_user1_2fa_already_setup(self): + with self._flask_app.app_context(): + + # Login should redirect to 2fa verification + response = self._get_with_user_http_auth(self.test_client, 'test_user_2fa_1', 'Password12345!') + self.assertEqual(200, response.status_code) + self.assertTrue('redirect_url' in response.json) + self.assertFalse('login_setup_2fa' in response.json['redirect_url']) + self.assertTrue('login_validate_2fa' in response.json['redirect_url']) + + # Answer should not provide login information + self.assertFalse('websocket_url' in response.json) + self.assertFalse('user_uuid' in response.json) + self.assertFalse('user_token' in response.json) + + def test_get_endpoint_login_user2_2fa_not_setup(self): + with self._flask_app.app_context(): + + # Login should redirect to 2fa verification + response = self._get_with_user_http_auth(self.test_client, 'test_user_2fa_2', 'Password12345!') + self.assertEqual(200, response.status_code) + self.assertTrue('redirect_url' in response.json) + self.assertTrue('login_setup_2fa' in response.json['redirect_url']) + self.assertFalse('login_validate_2fa' in response.json['redirect_url']) + + # Answer should not provide login information + self.assertFalse('websocket_url' in response.json) + self.assertFalse('user_uuid' in response.json) + self.assertFalse('user_token' in response.json) diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserLogin2FA.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserLogin2FA.py new file mode 100644 index 000000000..0f9a1a136 --- /dev/null +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserLogin2FA.py @@ -0,0 +1,227 @@ +import pyotp +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest +from opentera.db.models.TeraUser import TeraUser + + +class UserLogin2FATest(BaseUserAPITest): + test_endpoint = '/api/user/login/2fa' + + def setUp(self): + super().setUp() + # Create users with 2fa enabled + with self._flask_app.app_context(): + self.user1: dict = self._create_2fa_enabled_user('test_user_2fa_1', 'Password12345!', set_secret=True) + self.user2: dict = self._create_2fa_enabled_user('test_user_2fa_2', 'Password12345!', set_secret=False) + + def tearDown(self): + # Delete users with 2fa enabled + with self._flask_app.app_context(): + TeraUser.delete(self.user1['id_user'], hard_delete=True) + TeraUser.delete(self.user2['id_user'], hard_delete=True) + super().tearDown() + + def _create_2fa_enabled_user(self, username, password, set_secret:bool = True): + user = TeraUser() + user.id_user = 0 # New user + user.user_username = username + user.user_password = password + user.user_firstname = username + user.user_lastname = username + user.user_email = f"{username}@test.com" + user.user_enabled = True + user.user_profile = {} + if set_secret: + user.enable_2fa_otp() + else: + user.user_2fa_enabled = True + user.user_2fa_otp_enabled = False + user.user_2fa_otp_secret = None + + TeraUser.insert(user) + return user.to_json(minimal=False) + + def _login_user(self, username, password): + response = self._get_with_user_http_auth(self.test_client, username, password, endpoint='/api/user/login') + self.assertEqual(200, response.status_code) + self.assertEqual('application/json', response.headers['Content-Type']) + self.assertGreater(len(response.json), 0) + return response + + def test_get_endpoint_no_auth(self): + with self._flask_app.app_context(): + response = self.test_client.get(self.test_endpoint) + self.assertEqual(401, response.status_code) + + def test_get_endpoint_invalid_token_auth(self): + with self._flask_app.app_context(): + response = self._get_with_user_token_auth(self.test_client, 'invalid') + self.assertEqual(401, response.status_code) + + def test_get_endpoint_with_no_session(self): + with self._flask_app.app_context(): + response = self.test_client.get(self.test_endpoint) + self.assertEqual(401, response.status_code) + + def test_get_endpoint_with_admin_without_2fa_enabled(self): + with self._flask_app.app_context(): + user = TeraUser.get_user_by_username('admin') + self.assertIsNotNone(user) + self.assertFalse(user.user_2fa_enabled) + # Fist login + response = self._login_user('admin', 'admin') + self.assertEqual(200, response.status_code) + + # Now try to login with 2fa + response = self._get_with_user_http_auth(self.test_client, 'admin', 'admin', + params={'otp_code': '123456'}, + endpoint=self.test_endpoint) + self.assertEqual(403, response.status_code) + + def test_get_endpoint_login_user1_http_auth_no_code(self): + with self._flask_app.app_context(): + + # Fisrt login to create session + response = self._login_user('test_user_2fa_1', 'Password12345!') + self.assertEqual(200, response.status_code) + self.assertTrue('redirect_url' in response.json) + self.assertTrue('login_validate_2fa' in response.json['redirect_url']) + + # Using default admin information, http auth not used + response = self._get_with_user_http_auth(self.test_client) + self.assertEqual(400, response.status_code) + + def test_get_endpoint_login_user1_http_auth_invalid_code(self): + with self._flask_app.app_context(): + + # First login to create session + response = self._login_user('test_user_2fa_1', 'Password12345!') + self.assertEqual(200, response.status_code) + self.assertTrue('redirect_url' in response.json) + self.assertTrue('login_validate_2fa' in response.json['redirect_url']) + + # Then try to login with invalid code + response = self._get_with_user_http_auth(self.test_client, + params={'otp_code': '123456'}) + self.assertEqual(401, response.status_code) + + def test_get_endpoint_login_user1_http_auth_valid_code(self): + with self._flask_app.app_context(): + user = TeraUser.get_user_by_username('test_user_2fa_1') + self.assertIsNotNone(user.user_2fa_otp_secret) + + # First login to create session + response = self._login_user('test_user_2fa_1', 'Password12345!') + self.assertEqual(200, response.status_code) + self.assertTrue('redirect_url' in response.json) + self.assertTrue('login_validate_2fa' in response.json['redirect_url']) + self.assertFalse('login_setup_2fa' in response.json['redirect_url']) + + # Then try to login with valid code + totp = pyotp.TOTP(user.user_2fa_otp_secret) + response = self._get_with_user_http_auth(self.test_client, + params={'otp_code': totp.now()}) + self.assertEqual(200, response.status_code) + self.assertTrue('user_uuid' in response.json) + self.assertTrue('user_token' in response.json) + self.assertFalse('websocket_url'in response.json) + + def test_get_endpoint_login_user1_http_auth_valid_code_with_websocket(self): + with self._flask_app.app_context(): + user = TeraUser.get_user_by_username('test_user_2fa_1') + self.assertIsNotNone(user.user_2fa_otp_secret) + + # First login to create session + response = self._login_user('test_user_2fa_1', 'Password12345!') + self.assertEqual(200, response.status_code) + self.assertTrue('redirect_url' in response.json) + self.assertTrue('login_validate_2fa' in response.json['redirect_url']) + self.assertFalse('login_setup_2fa' in response.json['redirect_url']) + + # Then try to login with valid code + totp = pyotp.TOTP(user.user_2fa_otp_secret) + response = self._get_with_user_http_auth(self.test_client, + params={'otp_code': totp.now(), + 'with_websocket': True}) + self.assertEqual(200, response.status_code) + self.assertTrue('user_uuid' in response.json) + self.assertTrue('user_token' in response.json) + self.assertTrue('websocket_url'in response.json) + + def test_get_endpoint_login_user2_http_auth_invalid_code(self): + with self._flask_app.app_context(): + user = TeraUser.get_user_by_username('test_user_2fa_2') + self.assertIsNone(user.user_2fa_otp_secret) + + # First login to create session + response = self._login_user('test_user_2fa_2', 'Password12345!') + self.assertEqual(200, response.status_code) + self.assertTrue('redirect_url' in response.json) + self.assertFalse('login_validate_2fa' in response.json['redirect_url']) + self.assertTrue('login_setup_2fa' in response.json['redirect_url']) + + # Then try to login with invalid code + response = self._get_with_user_http_auth(self.test_client, + params={'otp_code': '123456'}) + self.assertEqual(403, response.status_code) + + def test_get_endpoint_login_user1_http_auth_valid_code_unknown_app_name(self): + with self._flask_app.app_context(): + user = TeraUser.get_user_by_username('test_user_2fa_1') + self.assertIsNotNone(user.user_2fa_otp_secret) + + # First login to create session + response = self._login_user('test_user_2fa_1', 'Password12345!') + self.assertEqual(200, response.status_code) + self.assertTrue('redirect_url' in response.json) + self.assertTrue('login_validate_2fa' in response.json['redirect_url']) + self.assertFalse('login_setup_2fa' in response.json['redirect_url']) + + # Then try to login with valid code + totp = pyotp.TOTP(user.user_2fa_otp_secret) + response = self._get_with_user_http_auth(self.test_client, + params={'otp_code': totp.now()}, + opt_headers={'X-Client-Name': 'test', 'X-Client-Version': '0.0.0'}) + self.assertEqual(200, response.status_code) + self.assertTrue('user_uuid' in response.json) + self.assertTrue('user_token' in response.json) + self.assertFalse('websocket_url'in response.json) + + def test_get_endpoint_login_user1_http_auth_valid_code_outdated_app(self): + with self._flask_app.app_context(): + user = TeraUser.get_user_by_username('test_user_2fa_1') + self.assertIsNotNone(user.user_2fa_otp_secret) + + # First login to create session + response = self._login_user('test_user_2fa_1', 'Password12345!') + self.assertEqual(200, response.status_code) + self.assertTrue('redirect_url' in response.json) + self.assertTrue('login_validate_2fa' in response.json['redirect_url']) + self.assertFalse('login_setup_2fa' in response.json['redirect_url']) + + # Then try to login with valid code + totp = pyotp.TOTP(user.user_2fa_otp_secret) + response = self._get_with_user_http_auth(self.test_client, + params={'otp_code': totp.now()}, + opt_headers={'X-Client-Name': 'OpenTeraPlus', + 'X-Client-Version': '0.0.0'}) + self.assertEqual(426, response.status_code) + + def test_get_endpoint_login_user1_http_auth_valid_code_valid_app(self): + with self._flask_app.app_context(): + user = TeraUser.get_user_by_username('test_user_2fa_1') + self.assertIsNotNone(user.user_2fa_otp_secret) + + # First login to create session + response = self._login_user('test_user_2fa_1', 'Password12345!') + self.assertEqual(200, response.status_code) + self.assertTrue('redirect_url' in response.json) + self.assertTrue('login_validate_2fa' in response.json['redirect_url']) + self.assertFalse('login_setup_2fa' in response.json['redirect_url']) + + # Then try to login with valid code + totp = pyotp.TOTP(user.user_2fa_otp_secret) + response = self._get_with_user_http_auth(self.test_client, + params={'otp_code': totp.now()}, + opt_headers={'X-Client-Name': 'OpenTeraPlus', + 'X-Client-Version': '1.0.0'}) + self.assertEqual(200, response.status_code) diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserLoginChangePassword.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserLoginChangePassword.py new file mode 100644 index 000000000..6346a86dd --- /dev/null +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserLoginChangePassword.py @@ -0,0 +1,179 @@ +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest +from opentera.db.models.TeraUser import TeraUser + + +class UserLoginChangePassword(BaseUserAPITest): + test_endpoint = '/api/user/login/change_password' + + def setUp(self): + super().setUp() + # Create users with password change needed + with self._flask_app.app_context(): + self.user1: dict = self._create_change_password_user('test_user_1', 'Password12345!') + + def tearDown(self): + # Delete users with 2fa enabled + with self._flask_app.app_context(): + TeraUser.delete(self.user1['id_user'], hard_delete=True) + super().tearDown() + + @staticmethod + def _create_change_password_user(username, password): + user = TeraUser() + user.id_user = 0 # New user + user.user_username = username + user.user_password = password + user.user_firstname = username + user.user_lastname = username + user.user_email = f"{username}@test.com" + user.user_enabled = True + user.user_profile = {} + user.user_force_password_change = True + + TeraUser.insert(user) + return user.to_json(minimal=False) + + def _login_user(self, username, password): + response = self._get_with_user_http_auth(self.test_client, username, password, endpoint='/api/user/login') + self.assertEqual(200, response.status_code) + self.assertEqual('application/json', response.headers['Content-Type']) + self.assertGreater(len(response.json), 0) + return response + + def test_post_endpoint_no_auth(self): + with self._flask_app.app_context(): + response = self.test_client.post(self.test_endpoint) + self.assertEqual(401, response.status_code) + + def test_post_endpoint_invalid_token_auth(self): + with self._flask_app.app_context(): + response = self._post_with_user_token_auth(self.test_client, 'invalid') + self.assertEqual(401, response.status_code) + + def test_post_endpoint_with_no_session(self): + with self._flask_app.app_context(): + response = self.test_client.post(self.test_endpoint) + self.assertEqual(401, response.status_code) + + def test_get_endpoint(self): + with self._flask_app.app_context(): + response = self.test_client.get(self.test_endpoint) + self.assertEqual(405, response.status_code) + + def test_post_password_without_force_required(self): + with self._flask_app.app_context(): + # First login to create session + response = self._login_user('admin', 'admin') + self.assertEqual(200, response.status_code) + self.assertFalse('redirect_url' in response.json) + + def test_post_password_change_mismatched(self): + with self._flask_app.app_context(): + # First login to create session + response = self._login_user('test_user_1', 'Password12345!') + self.assertEqual(200, response.status_code) + self.assertTrue('redirect_url' in response.json) + self.assertTrue('login_change_password' in response.json['redirect_url']) + + params = {'new_password': 'NewPassword12345!', + 'confirm_password': 'NotNewPassword12345!'} + response = self.test_client.post(self.test_endpoint, json=params) + self.assertEqual(400, response.status_code) + + def test_post_password_change_same_as_old(self): + with self._flask_app.app_context(): + # First login to create session + response = self._login_user('test_user_1', 'Password12345!') + self.assertEqual(200, response.status_code) + self.assertTrue('redirect_url' in response.json) + self.assertTrue('login_change_password' in response.json['redirect_url']) + + params = {'new_password': 'Password12345!', + 'confirm_password': 'Password12345!'} + response = self.test_client.post(self.test_endpoint, json=params) + self.assertEqual(400, response.status_code) + + def test_post_password_change_insecure(self): + with self._flask_app.app_context(): + # First login to create session + response = self._login_user('test_user_1', 'Password12345!') + self.assertEqual(200, response.status_code) + self.assertTrue('redirect_url' in response.json) + self.assertTrue('login_change_password' in response.json['redirect_url']) + + json_data = {'new_password': 'password', 'confirm_password': 'password'} + response = self.test_client.post(self.test_endpoint, json=json_data) + self.assertEqual(400, response.status_code, msg="Password not long enough") + + json_data['new_password'] = 'password12345!' + json_data['confirm_password'] = json_data['new_password'] + response = self.test_client.post(self.test_endpoint, json=json_data) + self.assertEqual(400, response.status_code, msg="Password without capital letters") + + json_data['new_password'] = 'PASSWORD12345!' + json_data['confirm_password'] = json_data['new_password'] + response = self.test_client.post(self.test_endpoint, json=json_data) + self.assertEqual(400, response.status_code, msg="Password without lower case letters") + + json_data['new_password'] = 'Password12345' + json_data['confirm_password'] = json_data['new_password'] + response = self.test_client.post(self.test_endpoint, json=json_data) + self.assertEqual(400, response.status_code, msg="Password without special characters") + + json_data['new_password'] = 'Password!!!!' + json_data['confirm_password'] = json_data['new_password'] + response = self.test_client.post(self.test_endpoint, json=json_data) + self.assertEqual(400, response.status_code, msg="Password without numbers") + + json_data['new_password'] = 'Password12345!!' + json_data['confirm_password'] = json_data['new_password'] + response = self.test_client.post(self.test_endpoint, json=json_data) + self.assertEqual(200, response.status_code, msg="Password OK") + + # Reset to original password + user = TeraUser.get_user_by_id(self.user1['id_user']) + user.user_force_password_change = True + user.db().session.commit() + + json_data['new_password'] = 'Password12345!' + json_data['confirm_password'] = json_data['new_password'] + response = self.test_client.post(self.test_endpoint, json=json_data) + self.assertEqual(200, response.status_code, msg="Password back to last") + + def test_post_password_change_not_required(self): + with self._flask_app.app_context(): + # First login to create session + response = self._login_user('test_user_1', 'Password12345!') + self.assertEqual(200, response.status_code) + self.assertTrue('redirect_url' in response.json) + self.assertTrue('login_change_password' in response.json['redirect_url']) + + user = TeraUser.get_user_by_id(self.user1['id_user']) + user.user_force_password_change = False + user.db().session.commit() + + json_data = {'new_password': 'Password12345!!', 'confirm_password': 'Password12345!!'} + response = self.test_client.post(self.test_endpoint, json=json_data) + self.assertEqual(400, response.status_code, msg="Password not required to be changed") + + user = TeraUser.get_user_by_id(self.user1['id_user']) + user.user_force_password_change = True + user.db().session.commit() + + def test_post_password_change_ok(self): + with self._flask_app.app_context(): + # First login to create session + response = self._login_user('test_user_1', 'Password12345!') + self.assertEqual(200, response.status_code) + self.assertTrue('redirect_url' in response.json) + self.assertTrue('login_change_password' in response.json['redirect_url']) + + json_data = {'new_password': 'Password12345!!', 'confirm_password': 'Password12345!!'} + response = self.test_client.post(self.test_endpoint, json=json_data) + self.assertEqual(200, response.status_code, msg="Password OK") + + # Reset to original password + user = TeraUser.get_user_by_id(self.user1['id_user']) + user.user_force_password_change = True + user.user_password = 'Password12345!' + user.db().session.commit() \ No newline at end of file diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserLoginSetup2FA.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserLoginSetup2FA.py new file mode 100644 index 000000000..21fb8873e --- /dev/null +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserLoginSetup2FA.py @@ -0,0 +1,186 @@ +import pyotp +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest +from opentera.db.models.TeraUser import TeraUser + + +class UserLoginSetup2FATest(BaseUserAPITest): + test_endpoint = '/api/user/login/setup_2fa' + + def setUp(self): + super().setUp() + # Create users with 2fa enabled + with self._flask_app.app_context(): + self.user1: dict = self._create_2fa_enabled_user('test_user_2fa_1', 'Password12345!', set_secret=True) + self.user2: dict = self._create_2fa_enabled_user('test_user_2fa_2', 'Password12345!', set_secret=False) + + def tearDown(self): + # Delete users with 2fa enabled + with self._flask_app.app_context(): + TeraUser.delete(self.user1['id_user'], hard_delete=True) + TeraUser.delete(self.user2['id_user'], hard_delete=True) + super().tearDown() + + + def _create_2fa_enabled_user(self, username, password, set_secret:bool = True): + user = TeraUser() + user.id_user = 0 # New user + user.user_username = username + user.user_password = password + user.user_firstname = username + user.user_lastname = username + user.user_email = f"{username}@test.com" + user.user_enabled = True + user.user_profile = {} + if set_secret: + user.enable_2fa_otp() + else: + user.user_2fa_enabled = True + user.user_2fa_otp_enabled = False + user.user_2fa_otp_secret = None + + TeraUser.insert(user) + return user.to_json(minimal=False) + + + def _login_user(self, username, password): + response = self._get_with_user_http_auth(self.test_client, username, + password, endpoint='/api/user/login') + self.assertEqual(200, response.status_code) + self.assertEqual('application/json', response.headers['Content-Type']) + self.assertGreater(len(response.json), 0) + return response + + def test_get_endpoint_invalid_token_auth(self): + with self._flask_app.app_context(): + response = self._get_with_user_token_auth(self.test_client, 'invalid') + self.assertEqual(401, response.status_code) + + def test_get_endpoint_with_no_session(self): + with self._flask_app.app_context(): + response = self.test_client.get(self.test_endpoint) + self.assertEqual(401, response.status_code) + + def test_get_endpoint_with_admin_without_2fa_enabled(self): + with self._flask_app.app_context(): + user = TeraUser.get_user_by_username('admin') + self.assertIsNotNone(user) + self.assertFalse(user.user_2fa_enabled) + # Fist login + response = self._login_user('admin', 'admin') + self.assertEqual(200, response.status_code) + + # Now try to setup 2fa + response = self._get_with_user_http_auth(self.test_client, 'admin', 'admin', + endpoint=self.test_endpoint) + + self.assertEqual(403, response.status_code) + + def test_get_endpoint_login_user1_2fa_already_setup(self): + with self._flask_app.app_context(): + + # First login to create session + response = self._login_user('test_user_2fa_1', 'Password12345!') + self.assertEqual(200, response.status_code) + self.assertTrue('redirect_url' in response.json) + self.assertFalse('login_setup_2fa' in response.json['redirect_url']) + + # Using default admin information, http auth not used + response = self._get_with_user_http_auth(self.test_client) + self.assertEqual(403, response.status_code) + + def test_get_endpoint_login_user2_http_auth_should_work_but_and_modify_user_after_post(self): + with self._flask_app.app_context(): + + # First login to create session + response = self._login_user('test_user_2fa_2', 'Password12345!') + self.assertEqual(200, response.status_code) + self.assertTrue('redirect_url' in response.json) + self.assertTrue('login_setup_2fa' in response.json['redirect_url']) + + # Then try to setup 2fa + response = self._get_with_user_http_auth(self.test_client) + self.assertEqual(200, response.status_code) + + self.assertTrue('qr_code' in response.json) + self.assertTrue('otp_secret' in response.json) + + user = TeraUser.get_user_by_username('test_user_2fa_2') + self.assertIsNotNone(user) + self.assertTrue(user.user_2fa_enabled) + self.assertFalse(user.user_2fa_otp_enabled) + self.assertIsNone(user.user_2fa_otp_secret) + + # Post will enable 2fa + otp_secret = response.json['otp_secret'] + otp_code = pyotp.TOTP(response.json['otp_secret']).now() + params = {'otp_secret': response.json['otp_secret'], 'otp_code': otp_code} + response = self.test_client.post(self.test_endpoint, json=params) + self.assertEqual(200, response.status_code) + + # Verify response + self.assertTrue('redirect_url' in response.json) + self.assertTrue('login_validate_2fa' in response.json['redirect_url']) + + # Reload user and verify 2fa is enabled properly + user = TeraUser.get_user_by_username('test_user_2fa_2') + self.assertIsNotNone(user) + self.assertTrue(user.user_2fa_enabled) + self.assertTrue(user.user_2fa_otp_enabled) + self.assertIsNotNone(user.user_2fa_otp_secret) + self.assertEqual(otp_secret, user.user_2fa_otp_secret) + + def test_get_endpoint_login_user2_http_auth_should_fail_after_post_with_wrong_code(self): + with self._flask_app.app_context(): + + # First login to create session + response = self._login_user('test_user_2fa_2', 'Password12345!') + self.assertEqual(200, response.status_code) + self.assertTrue('redirect_url' in response.json) + self.assertTrue('login_setup_2fa' in response.json['redirect_url']) + + # Then try to setup 2fa + response = self._get_with_user_http_auth(self.test_client) + self.assertEqual(200, response.status_code) + + self.assertTrue('qr_code' in response.json) + self.assertTrue('otp_secret' in response.json) + + user = TeraUser.get_user_by_username('test_user_2fa_2') + self.assertIsNotNone(user) + self.assertTrue(user.user_2fa_enabled) + self.assertFalse(user.user_2fa_otp_enabled) + self.assertIsNone(user.user_2fa_otp_secret) + + # Post will fail with wrong code + params = {'otp_secret': response.json['otp_secret'], 'otp_code': '123456'} + response = self.test_client.post(self.test_endpoint, json=params) + self.assertEqual(401, response.status_code) + + def test_get_endpoint_login_user2_http_auth_should_fail_after_post_with_wrong_secret(self): + with self._flask_app.app_context(): + + # First login to create session + response = self._login_user('test_user_2fa_2', 'Password12345!') + self.assertEqual(200, response.status_code) + self.assertTrue('redirect_url' in response.json) + self.assertTrue('login_setup_2fa' in response.json['redirect_url']) + + # Then try to setup 2fa + response = self._get_with_user_http_auth(self.test_client) + self.assertEqual(200, response.status_code) + + self.assertTrue('qr_code' in response.json) + self.assertTrue('otp_secret' in response.json) + + user = TeraUser.get_user_by_username('test_user_2fa_2') + self.assertIsNotNone(user) + self.assertTrue(user.user_2fa_enabled) + self.assertFalse(user.user_2fa_otp_enabled) + self.assertIsNone(user.user_2fa_otp_secret) + + # Post will fail with wrong secret + otp_secret = pyotp.random_base32() + otp_code = pyotp.TOTP(otp_secret).now() + params = {'otp_secret': response.json['otp_secret'], 'otp_code': otp_code} + response = self.test_client.post(self.test_endpoint, json=params) + self.assertEqual(401, response.status_code) diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserLogout.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserLogout.py index 2d5c37386..a9c3f3aa5 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserLogout.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserLogout.py @@ -1,4 +1,4 @@ -from BaseUserAPITest import BaseUserAPITest +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest from opentera.db.models.TeraUser import TeraUser diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryAssets.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryAssets.py index 8f16ce9b7..4bf8cab24 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryAssets.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryAssets.py @@ -1,4 +1,4 @@ -from BaseUserAPITest import BaseUserAPITest +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest from opentera.db.models.TeraAsset import TeraAsset @@ -66,7 +66,7 @@ def test_query_tera_server_assets_no_access(self): params=params) self.assertEqual(200, response.status_code) self.assertEqual(len(response.json), 0) - + def test_query_device_assets_as_admin(self): with self._flask_app.app_context(): params = {'id_device': 1, 'with_urls': True} diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryDeviceParticipants.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryDeviceParticipants.py index cd8d06645..f5f172ae9 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryDeviceParticipants.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryDeviceParticipants.py @@ -1,6 +1,6 @@ from typing import List -from BaseUserAPITest import BaseUserAPITest +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest from modules.FlaskModule.FlaskModule import flask_app from opentera.db.models.TeraDevice import TeraDevice from opentera.db.models.TeraParticipant import TeraParticipant diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryDeviceProjects.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryDeviceProjects.py index 799fc18aa..65b081151 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryDeviceProjects.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryDeviceProjects.py @@ -1,4 +1,4 @@ -from BaseUserAPITest import BaseUserAPITest +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest from opentera.db.models.TeraDeviceProject import TeraDeviceProject from opentera.db.models.TeraDeviceSite import TeraDeviceSite diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryDisconnect.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryDisconnect.py index 4318e492d..daa29886d 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryDisconnect.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryDisconnect.py @@ -1,4 +1,4 @@ -from BaseUserAPITest import BaseUserAPITest +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest from opentera.db.models.TeraDevice import TeraDevice from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraParticipant import TeraParticipant diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryForms.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryForms.py index 871afe33c..7e0fea2dd 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryForms.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryForms.py @@ -1,4 +1,4 @@ -from BaseUserAPITest import BaseUserAPITest +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest from opentera.db.models.TeraParticipantGroup import TeraParticipantGroup from opentera.db.models.TeraParticipant import TeraParticipant from opentera.db.models.TeraUser import TeraUser diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryParticipantGroup.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryParticipantGroup.py index 00a4df8c1..a124790e9 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryParticipantGroup.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryParticipantGroup.py @@ -1,4 +1,4 @@ -from BaseUserAPITest import BaseUserAPITest +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest from opentera.db.models.TeraParticipantGroup import TeraParticipantGroup from opentera.db.models.TeraParticipant import TeraParticipant from opentera.db.models.TeraSession import TeraSession diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryParticipants.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryParticipants.py index d26c9e8a4..73ca17a24 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryParticipants.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryParticipants.py @@ -1,4 +1,4 @@ -from BaseUserAPITest import BaseUserAPITest +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest from opentera.db.models.TeraParticipant import TeraParticipant from opentera.db.models.TeraProject import TeraProject from opentera.db.models.TeraParticipantGroup import TeraParticipantGroup diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryProjectAccess.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryProjectAccess.py index aeaf95d88..45fdaee30 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryProjectAccess.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryProjectAccess.py @@ -1,4 +1,4 @@ -from BaseUserAPITest import BaseUserAPITest +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest class UserQueryProjectAccessTest(BaseUserAPITest): @@ -165,7 +165,7 @@ def test_query_specific_user_group_admins_with_sites(self): def test_query_specific_user_group_by_users(self): with self._flask_app.app_context(): - response = self._get_with_user_http_auth(self.test_client, username='admin', password='admin', + response = self._get_with_user_http_auth(self.test_client, username='admin', password='admin', params={'id_user_group': 1, 'by_users': True}) self.assertEqual(200, response.status_code) self.assertTrue(response.is_json) @@ -243,7 +243,7 @@ def test_query_specific_user_group_by_users_with_projects_admins(self): def test_query_specific_user_group_by_users_with_projects_admins_with_sites(self): with self._flask_app.app_context(): params = {'id_user_group': 4, 'by_users': True, 'with_empty': True, 'admins': True, 'with_sites': True} - response = self._get_with_user_http_auth(self.test_client, username='admin', password='admin', + response = self._get_with_user_http_auth(self.test_client, username='admin', password='admin', params=params) self.assertEqual(200, response.status_code) self.assertTrue(response.is_json) @@ -291,7 +291,7 @@ def test_query_specific_user_group_with_projects_admins_with_sites(self): def test_query_specific_project(self): with self._flask_app.app_context(): - response = self._get_with_user_http_auth(self.test_client, username='admin', password='admin', + response = self._get_with_user_http_auth(self.test_client, username='admin', password='admin', params={'id_project': 1}) self.assertEqual(200, response.status_code) self.assertTrue(response.is_json) @@ -471,7 +471,7 @@ def test_query_specific_project_by_users_with_user_groups_admins_with_sites(self with self._flask_app.app_context(): params = {'id_project': 2, 'by_users': True, 'with_empty': True, 'admins': True, 'with_sites': True} - response = self._get_with_user_http_auth(self.test_client, username='admin', password='admin', + response = self._get_with_user_http_auth(self.test_client, username='admin', password='admin', params=params) self.assertEqual(200, response.status_code) self.assertTrue(response.is_json) diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryProjects.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryProjects.py index f896dc261..d94beab22 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryProjects.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryProjects.py @@ -1,4 +1,4 @@ -from BaseUserAPITest import BaseUserAPITest +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest from opentera.db.models.TeraProject import TeraProject from opentera.db.models.TeraServiceProject import TeraServiceProject from opentera.db.models.TeraUser import TeraUser diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryServerSettings.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryServerSettings.py index fb2fcb8b7..37a8ecf7d 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryServerSettings.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryServerSettings.py @@ -1,4 +1,4 @@ -from BaseUserAPITest import BaseUserAPITest +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest from opentera.db.models.TeraServerSettings import TeraServerSettings diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryServiceAccess.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryServiceAccess.py index fac5fa534..fb88362e4 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryServiceAccess.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryServiceAccess.py @@ -1,4 +1,4 @@ -from BaseUserAPITest import BaseUserAPITest +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraServiceAccess import TeraServiceAccess diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryServiceConfigs.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryServiceConfigs.py index 1a64e784e..1f3288607 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryServiceConfigs.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryServiceConfigs.py @@ -1,4 +1,4 @@ -from BaseUserAPITest import BaseUserAPITest +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest class UserQueryServiceConfigsTest(BaseUserAPITest): diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryServiceProjects.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryServiceProjects.py index 97337d4d2..02e8cf18c 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryServiceProjects.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryServiceProjects.py @@ -1,4 +1,4 @@ -from BaseUserAPITest import BaseUserAPITest +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest from opentera.db.models.TeraParticipant import TeraParticipant from opentera.db.models.TeraSite import TeraSite from opentera.db.models.TeraSessionTypeProject import TeraSessionTypeProject diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryServiceRoles.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryServiceRoles.py index 2cec8e421..a2a0faa87 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryServiceRoles.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryServiceRoles.py @@ -1,4 +1,4 @@ -from BaseUserAPITest import BaseUserAPITest +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest from opentera.db.models.TeraServiceRole import TeraServiceRole diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryServiceSites.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryServiceSites.py index 289e5ef84..6d6d1b3a3 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryServiceSites.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryServiceSites.py @@ -1,4 +1,4 @@ -from BaseUserAPITest import BaseUserAPITest +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest from opentera.db.models.TeraServiceSite import TeraServiceSite diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryServices.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryServices.py index b3e568542..e1cc68965 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryServices.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryServices.py @@ -1,4 +1,4 @@ -from BaseUserAPITest import BaseUserAPITest +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest from opentera.db.models.TeraServiceSite import TeraServiceSite from opentera.db.models.TeraServiceProject import TeraServiceProject from opentera.db.models.TeraSession import TeraSession diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQuerySessionEvents.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQuerySessionEvents.py index 0bdbfcaa5..9913ebb2c 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQuerySessionEvents.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQuerySessionEvents.py @@ -1,4 +1,4 @@ -from BaseUserAPITest import BaseUserAPITest +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest class UserQuerySessionEventsTest(BaseUserAPITest): diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQuerySessionTypeProject.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQuerySessionTypeProject.py index 4e0a9e6ae..7cc8bf291 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQuerySessionTypeProject.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQuerySessionTypeProject.py @@ -1,4 +1,4 @@ -from BaseUserAPITest import BaseUserAPITest +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest from opentera.db.models.TeraSessionType import TeraSessionType from opentera.db.models.TeraSessionTypeProject import TeraSessionTypeProject from opentera.db.models.TeraSession import TeraSession diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQuerySessionTypeSites.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQuerySessionTypeSites.py index 08342b292..bb1379573 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQuerySessionTypeSites.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQuerySessionTypeSites.py @@ -1,4 +1,4 @@ -from BaseUserAPITest import BaseUserAPITest +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest from opentera.db.models.TeraSessionType import TeraSessionType from opentera.db.models.TeraSessionTypeSite import TeraSessionTypeSite from opentera.db.models.TeraSessionTypeProject import TeraSessionTypeProject @@ -81,14 +81,14 @@ def test_query_site_as_admin(self): self.assertEqual(200, response.status_code) self.assertTrue(response.is_json) self.assertEqual(len(response.json), 0) - + params = {'id_site': 2} response = self._get_with_user_http_auth(self.test_client, username='admin', password='admin', params=params) self.assertEqual(200, response.status_code) self.assertTrue(response.is_json) self.assertEqual(len(response.json), 1) - + for data_item in response.json: self._checkJson(json_data=data_item) diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQuerySiteAccess.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQuerySiteAccess.py index 348d4535b..5ca1cd86f 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQuerySiteAccess.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQuerySiteAccess.py @@ -1,4 +1,4 @@ -from BaseUserAPITest import BaseUserAPITest +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest class UserQuerySiteAccessTest(BaseUserAPITest): diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQuerySites.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQuerySites.py index 05b5fda38..a86cf7f26 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQuerySites.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQuerySites.py @@ -1,4 +1,4 @@ -from BaseUserAPITest import BaseUserAPITest +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraProject import TeraProject from opentera.db.models.TeraParticipant import TeraParticipant @@ -216,6 +216,7 @@ def test_post_and_delete(self): def _checkJson(self, json_data, minimal=False): self.assertGreater(len(json_data), 0) - self.assertTrue(json_data.__contains__('id_site')) - self.assertTrue(json_data.__contains__('site_name')) - self.assertTrue(json_data.__contains__('site_role')) + self.assertTrue('id_site' in json_data) + self.assertTrue('site_name' in json_data) + self.assertTrue('site_role' in json_data) + self.assertTrue('site_2fa_required' in json_data) diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryTestTypeProjects.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryTestTypeProjects.py index f71888883..fdb26d374 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryTestTypeProjects.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryTestTypeProjects.py @@ -1,4 +1,4 @@ -from BaseUserAPITest import BaseUserAPITest +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest from opentera.db.models.TeraTestTypeProject import TeraTestTypeProject from opentera.db.models.TeraTest import TeraTest from opentera.db.models.TeraSession import TeraSession @@ -165,21 +165,21 @@ def test_query_list_as_admin(self): def test_query_project_as_user(self): with self._flask_app.app_context(): params = {'id_project': 10} - response = self._get_with_user_http_auth(self.test_client, username='user', password='user', + response = self._get_with_user_http_auth(self.test_client, username='user', password='user', params=params) self.assertEqual(200, response.status_code) self.assertTrue(response.is_json) self.assertEqual(0, len(response.json)) params = {'id_project': 1} - response = self._get_with_user_http_auth(self.test_client, username='user4', password='user4', + response = self._get_with_user_http_auth(self.test_client, username='user4', password='user4', params=params) self.assertEqual(200, response.status_code) self.assertTrue(response.is_json) self.assertEqual(0, len(response.json)) params = {'id_project': 1} - response = self._get_with_user_http_auth(self.test_client, username='user', password='user', + response = self._get_with_user_http_auth(self.test_client, username='user', password='user', params=params) self.assertEqual(200, response.status_code) self.assertTrue(response.is_json) diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryTestTypeSites.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryTestTypeSites.py index f840c2dfc..61f003dd2 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryTestTypeSites.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryTestTypeSites.py @@ -1,4 +1,4 @@ -from BaseUserAPITest import BaseUserAPITest +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest from opentera.db.models.TeraTestType import TeraTestType from opentera.db.models.TeraTestTypeSite import TeraTestTypeSite from opentera.db.models.TeraTestTypeProject import TeraTestTypeProject diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryUserGroups.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryUserGroups.py index fe9dc0a72..dcc64eb9a 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryUserGroups.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryUserGroups.py @@ -1,4 +1,4 @@ -from BaseUserAPITest import BaseUserAPITest +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest from opentera.db.models.TeraUserGroup import TeraUserGroup from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraServiceAccess import TeraServiceAccess diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryUsers.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryUsers.py index 255be07de..8e029bb99 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryUsers.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryUsers.py @@ -333,6 +333,96 @@ def test_query_self(self): self.assertTrue(data_item.__contains__('projects')) self.assertTrue(data_item.__contains__('sites')) + def test_password_strength(self): + with self._flask_app.app_context(): + json_data = { + 'user': { + 'id_user': 0, + 'user_username': 'new_test_user', + 'user_enabled': True, + 'user_firstname': 'Test', + 'user_lastname': 'Test', + 'user_profile': '', + 'user_user_groups': [{'id_user_group': 3}], + 'user_password': 'password' + } + } + response = self._post_with_user_http_auth(self.test_client, username='admin', password='admin', + json=json_data) + self.assertEqual(400, response.status_code, msg="Password not long enough") + + json_data['user']['user_password'] = 'password12345!' + response = self._post_with_user_http_auth(self.test_client, username='admin', password='admin', + json=json_data) + self.assertEqual(400, response.status_code, msg="Password without capital letters") + + json_data['user']['user_password'] = 'PASSWORD12345!' + response = self._post_with_user_http_auth(self.test_client, username='admin', password='admin', + json=json_data) + self.assertEqual(400, response.status_code, msg="Password without lower case letters") + + json_data['user']['user_password'] = 'Password12345' + response = self._post_with_user_http_auth(self.test_client, username='admin', password='admin', + json=json_data) + self.assertEqual(400, response.status_code, msg="Password without special characters") + + json_data['user']['user_password'] = 'Password!!!!' + response = self._post_with_user_http_auth(self.test_client, username='admin', password='admin', + json=json_data) + self.assertEqual(400, response.status_code, msg="Password without numbers") + + json_data['user']['user_password'] = 'Password12345!' + response = self._post_with_user_http_auth(self.test_client, username='admin', password='admin', + json=json_data) + self.assertEqual(200, response.status_code, msg="Password OK") + self.assertGreater(len(response.json), 0) + json_data = response.json[0] + self._checkJson(json_data) + current_id = json_data['id_user'] + + # Modify password + json_data = { + 'user': { + 'id_user': current_id, + 'user_password': 'password' + } + } + + response = self._post_with_user_http_auth(self.test_client, username='admin', password='admin', + json=json_data) + self.assertEqual(400, response.status_code, msg="Password not long enough") + + json_data['user']['user_password'] = 'password12345!' + response = self._post_with_user_http_auth(self.test_client, username='admin', password='admin', + json=json_data) + self.assertEqual(400, response.status_code, msg="Password without capital letters") + + json_data['user']['user_password'] = 'PASSWORD12345!' + response = self._post_with_user_http_auth(self.test_client, username='admin', password='admin', + json=json_data) + self.assertEqual(400, response.status_code, msg="Password without lower case letters") + + json_data['user']['user_password'] = 'Password12345' + response = self._post_with_user_http_auth(self.test_client, username='admin', password='admin', + json=json_data) + self.assertEqual(400, response.status_code, msg="Password without special characters") + + json_data['user']['user_password'] = 'Password!!!!' + response = self._post_with_user_http_auth(self.test_client, username='admin', password='admin', + json=json_data) + self.assertEqual(400, response.status_code, msg="Password without numbers") + + json_data['user']['user_password'] = 'Password12345!!' + response = self._post_with_user_http_auth(self.test_client, username='admin', password='admin', + json=json_data) + self.assertEqual(200, response.status_code, msg="Password OK") + + response = self._post_with_user_http_auth(self.test_client, username='admin', password='admin', + json=json_data) + self.assertEqual(400, response.status_code, msg="Password same as old") + + TeraUser.delete(current_id) + def test_post_and_delete(self): with self._flask_app.app_context(): json_data = { @@ -376,7 +466,7 @@ def test_post_and_delete(self): json=json_data) self.assertEqual(400, response.status_code, msg="Invalid password") - json_data['user']['user_password'] = 'testtest' + json_data['user']['user_password'] = 'testTest11*&' response = self._post_with_user_http_auth(self.test_client, username='admin', password='admin', json=json_data) self.assertEqual(response.status_code, 409, msg="Username unavailable") diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryVersions.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryVersions.py index 0ba73a424..d6602b3cc 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryVersions.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserQueryVersions.py @@ -1,4 +1,4 @@ -from BaseUserAPITest import BaseUserAPITest +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest from opentera.utils.TeraVersions import TeraVersions diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserRefreshToken.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserRefreshToken.py index a12427e98..a0558609c 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserRefreshToken.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserRefreshToken.py @@ -1,4 +1,4 @@ -from BaseUserAPITest import BaseUserAPITest +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest class UserRefreshTokenTest(BaseUserAPITest): diff --git a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserSessionManager.py b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserSessionManager.py index 4d099a737..791e0367b 100644 --- a/teraserver/python/tests/modules/FlaskModule/API/user/test_UserSessionManager.py +++ b/teraserver/python/tests/modules/FlaskModule/API/user/test_UserSessionManager.py @@ -1,4 +1,4 @@ -from BaseUserAPITest import BaseUserAPITest +from tests.modules.FlaskModule.API.user.BaseUserAPITest import BaseUserAPITest class UserSessionManagerTest(BaseUserAPITest): @@ -53,4 +53,4 @@ def test_delete_endpoint_invalid_http_auth(self): def test_delete_endpoint_invalid_token_auth(self): with self._flask_app.app_context(): response = self._delete_with_user_token_auth(self.test_client, token='invalid') - self.assertEqual(405, response.status_code) \ No newline at end of file + self.assertEqual(405, response.status_code) diff --git a/teraserver/python/tests/modules/FlaskModule/__init__.py b/teraserver/python/tests/modules/FlaskModule/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/teraserver/python/tests/modules/__init__.py b/teraserver/python/tests/modules/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/teraserver/python/tests/opentera/__init__.py b/teraserver/python/tests/opentera/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/teraserver/python/tests/opentera/db/__init__.py b/teraserver/python/tests/opentera/db/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/teraserver/python/tests/opentera/db/models/__init__.py b/teraserver/python/tests/opentera/db/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/teraserver/python/tests/opentera/db/models/test_TeraAsset.py b/teraserver/python/tests/opentera/db/models/test_TeraAsset.py index 4928760eb..ffac8deb5 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraAsset.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraAsset.py @@ -299,22 +299,22 @@ def test_hard_delete(self): def test_undelete(self): with self._flask_app.app_context(): # Create new participant - from test_TeraParticipant import TeraParticipantTest + from tests.opentera.db.models.test_TeraParticipant import TeraParticipantTest participant = TeraParticipantTest.new_test_participant(id_project=1) id_participant = participant.id_participant # Create new device - from test_TeraDevice import TeraDeviceTest + from tests.opentera.db.models.test_TeraDevice import TeraDeviceTest device = TeraDeviceTest.new_test_device() id_device = device.id_device # Create new user - from test_TeraUser import TeraUserTest + from tests.opentera.db.models.test_TeraUser import TeraUserTest user = TeraUserTest.new_test_user(user_name="asset_user") id_user = user.id_user # Create new session - from test_TeraSession import TeraSessionTest + from tests.opentera.db.models.test_TeraSession import TeraSessionTest ses = TeraSessionTest.new_test_session(participants=[participant], users=[user], devices=[device]) id_session = ses.id_session @@ -409,4 +409,3 @@ def new_test_asset(id_session: int, service_uuid: str, id_device: int | None = N asset.asset_type = 'application/test' TeraAsset.insert(asset) return asset - diff --git a/teraserver/python/tests/opentera/db/models/test_TeraDevice.py b/teraserver/python/tests/opentera/db/models/test_TeraDevice.py index 901ea1fb6..1957ea482 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraDevice.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraDevice.py @@ -115,23 +115,23 @@ def test_hard_delete(self): id_device = device.id_device # Assign device to site - from test_TeraDeviceSite import TeraDeviceSiteTest + from tests.opentera.db.models.test_TeraDeviceSite import TeraDeviceSiteTest device_site = TeraDeviceSiteTest.new_test_device_site(id_device=id_device, id_site=1) id_device_site = device_site.id_device_site # Assign device to project - from test_TeraDeviceProject import TeraDeviceProjectTest + from tests.opentera.db.models.test_TeraDeviceProject import TeraDeviceProjectTest device_project = TeraDeviceProjectTest.new_test_device_project(id_device=id_device, id_project=1) id_device_project = device_project.id_device_project # Assign device to participants - from test_TeraDeviceParticipant import TeraDeviceParticipantTest + from tests.opentera.db.models.test_TeraDeviceParticipant import TeraDeviceParticipantTest device_participant = TeraDeviceParticipantTest.new_test_device_participant(id_device=id_device, id_participant=1) id_device_participant = device_participant.id_device_participant # Assign device to sessions - from test_TeraSession import TeraSessionTest + from tests.opentera.db.models.test_TeraSession import TeraSessionTest device_session = TeraSessionTest.new_test_session(id_creator_device=id_device) id_session = device_session.id_session @@ -139,19 +139,19 @@ def test_hard_delete(self): id_session_invitee = device_session.id_session # Attach asset - from test_TeraAsset import TeraAssetTest + from tests.opentera.db.models.test_TeraAsset import TeraAssetTest asset = TeraAssetTest.new_test_asset(id_session=id_session, service_uuid=TeraService.get_openteraserver_service().service_uuid, id_device=id_device) id_asset = asset.id_asset # ... and test - from test_TeraTest import TeraTestTest + from tests.opentera.db.models.test_TeraTest import TeraTestTest test = TeraTestTest.new_test_test(id_session=id_session, id_device=id_device) id_test = test.id_test # Create service config for device - from test_TeraServiceConfig import TeraServiceConfigTest + from tests.opentera.db.models.test_TeraServiceConfig import TeraServiceConfigTest device_service_config = TeraServiceConfigTest.new_test_service_config(id_device=id_device, id_service=2) id_service_config = device_service_config.id_service_config @@ -203,23 +203,23 @@ def test_undelete(self): id_device = device.id_device # Assign device to site - from test_TeraDeviceSite import TeraDeviceSiteTest + from tests.opentera.db.models.test_TeraDeviceSite import TeraDeviceSiteTest device_site = TeraDeviceSiteTest.new_test_device_site(id_device=id_device, id_site=1) id_device_site = device_site.id_device_site # Assign device to project - from test_TeraDeviceProject import TeraDeviceProjectTest + from tests.opentera.db.models.test_TeraDeviceProject import TeraDeviceProjectTest device_project = TeraDeviceProjectTest.new_test_device_project(id_device=id_device, id_project=1) id_device_project = device_project.id_device_project # Assign device to participants - from test_TeraDeviceParticipant import TeraDeviceParticipantTest + from tests.opentera.db.models.test_TeraDeviceParticipant import TeraDeviceParticipantTest device_participant = TeraDeviceParticipantTest.new_test_device_participant(id_device=id_device, id_participant=1) id_device_participant = device_participant.id_device_participant # Assign device to sessions - from test_TeraSession import TeraSessionTest + from tests.opentera.db.models.test_TeraSession import TeraSessionTest device_session = TeraSessionTest.new_test_session(id_creator_device=id_device) id_session = device_session.id_session @@ -227,19 +227,19 @@ def test_undelete(self): id_session_invitee = device_session.id_session # Attach asset - from test_TeraAsset import TeraAssetTest + from tests.opentera.db.models.test_TeraAsset import TeraAssetTest asset = TeraAssetTest.new_test_asset(id_session=id_session, service_uuid=TeraService.get_openteraserver_service().service_uuid, id_device=id_device) id_asset = asset.id_asset # ... and test - from test_TeraTest import TeraTestTest + from tests.opentera.db.models.test_TeraTest import TeraTestTest test = TeraTestTest.new_test_test(id_session=id_session, id_device=id_device) id_test = test.id_test # Create service config for device - from test_TeraServiceConfig import TeraServiceConfigTest + from tests.opentera.db.models.test_TeraServiceConfig import TeraServiceConfigTest device_service_config = TeraServiceConfigTest.new_test_service_config(id_device=id_device, id_service=2) id_service_config = device_service_config.id_service_config diff --git a/teraserver/python/tests/opentera/db/models/test_TeraParticipant.py b/teraserver/python/tests/opentera/db/models/test_TeraParticipant.py index 20f03b586..3496cf72b 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraParticipant.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraParticipant.py @@ -68,7 +68,7 @@ def test_hard_delete(self): id_participant = participant.id_participant # Assign participant to sessions - from test_TeraSession import TeraSessionTest + from tests.opentera.db.models.test_TeraSession import TeraSessionTest part_session = TeraSessionTest.new_test_session(id_creator_participant=id_participant) id_session = part_session.id_session @@ -76,14 +76,14 @@ def test_hard_delete(self): id_session_invitee = part_session.id_session # Attach asset - from test_TeraAsset import TeraAssetTest + from tests.opentera.db.models.test_TeraAsset import TeraAssetTest asset = TeraAssetTest.new_test_asset(id_session=id_session, service_uuid=TeraService.get_openteraserver_service().service_uuid, id_participant=id_participant) id_asset = asset.id_asset # ... and test - from test_TeraTest import TeraTestTest + from tests.opentera.db.models.test_TeraTest import TeraTestTest test = TeraTestTest.new_test_test(id_session=id_session, id_participant=id_participant) id_test = test.id_test @@ -117,7 +117,7 @@ def test_hard_delete(self): def test_undelete(self): with self._flask_app.app_context(): # Create a new project - from test_TeraProject import TeraProjectTest + from tests.opentera.db.models.test_TeraProject import TeraProjectTest project = TeraProjectTest.new_test_project() id_project = project.id_project @@ -127,7 +127,7 @@ def test_undelete(self): id_participant = participant.id_participant # Assign participant to sessions - from test_TeraSession import TeraSessionTest + from tests.opentera.db.models.test_TeraSession import TeraSessionTest part_session = TeraSessionTest.new_test_session(id_creator_participant=id_participant) id_session = part_session.id_session @@ -135,19 +135,19 @@ def test_undelete(self): id_session_invitee = part_session.id_session # Attach asset - from test_TeraAsset import TeraAssetTest + from tests.opentera.db.models.test_TeraAsset import TeraAssetTest asset = TeraAssetTest.new_test_asset(id_session=id_session, service_uuid=TeraService.get_openteraserver_service().service_uuid, id_participant=id_participant) id_asset = asset.id_asset # ... and test - from test_TeraTest import TeraTestTest + from tests.opentera.db.models.test_TeraTest import TeraTestTest test = TeraTestTest.new_test_test(id_session=id_session, id_participant=id_participant) id_test = test.id_test # ... and service config - from test_TeraServiceConfig import TeraServiceConfigTest + from tests.opentera.db.models.test_TeraServiceConfig import TeraServiceConfigTest device_service_config = TeraServiceConfigTest.new_test_service_config(id_participant=id_participant, id_service=2) id_service_config = device_service_config.id_service_config @@ -177,7 +177,7 @@ def test_undelete(self): TeraParticipant.undelete(id_participant) # Create and associate participant group - from test_TeraParticipantGroup import TeraParticipantGroupTest + from tests.opentera.db.models.test_TeraParticipantGroup import TeraParticipantGroupTest group = TeraParticipantGroupTest.new_test_group(id_project) id_group = group.id_participant_group diff --git a/teraserver/python/tests/opentera/db/models/test_TeraParticipantGroup.py b/teraserver/python/tests/opentera/db/models/test_TeraParticipantGroup.py index b99384f8c..e9ba557f4 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraParticipantGroup.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraParticipantGroup.py @@ -40,7 +40,7 @@ def test_hard_delete(self): id_participant_group = group.id_participant_group # Create a new participant in that group - from test_TeraParticipant import TeraParticipantTest + from tests.opentera.db.models.test_TeraParticipant import TeraParticipantTest participant = TeraParticipantTest.new_test_participant(id_project=1, id_participant_group=id_participant_group) self.assertIsNotNone(participant.id_participant) @@ -66,7 +66,7 @@ def test_hard_delete(self): def test_undelete(self): with self._flask_app.app_context(): # Create a new project - from test_TeraProject import TeraProjectTest + from tests.opentera.db.models.test_TeraProject import TeraProjectTest project = TeraProjectTest.new_test_project(id_site=1) id_project = project.id_project @@ -76,7 +76,7 @@ def test_undelete(self): id_participant_group = group.id_participant_group # Create a new participant in that group - from test_TeraParticipant import TeraParticipantTest + from tests.opentera.db.models.test_TeraParticipant import TeraParticipantTest participant = TeraParticipantTest.new_test_participant(id_project=id_project, id_participant_group=id_participant_group) self.assertIsNotNone(participant.id_participant) diff --git a/teraserver/python/tests/opentera/db/models/test_TeraProject.py b/teraserver/python/tests/opentera/db/models/test_TeraProject.py index 2ad0e9689..bca7f206a 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraProject.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraProject.py @@ -106,7 +106,7 @@ def test_update_set_inactive(self): # Create participants participants = [] - from test_TeraParticipant import TeraParticipantTest + from tests.opentera.db.models.test_TeraParticipant import TeraParticipantTest for i in range(3): part = TeraParticipantTest.new_test_participant(id_project=new_project.id_project, enabled=True) participants.append(part) @@ -116,7 +116,7 @@ def test_update_set_inactive(self): # Associate devices devices = [] - from test_TeraDevice import TeraDeviceTest + from tests.opentera.db.models.test_TeraDevice import TeraDeviceTest for i in range(2): device = TeraDeviceTest.new_test_device() devices.append(device) @@ -163,7 +163,7 @@ def test_hard_delete(self): id_project = project.id_project # Create a new participant in that project - from test_TeraParticipant import TeraParticipantTest + from tests.opentera.db.models.test_TeraParticipant import TeraParticipantTest participant = TeraParticipantTest.new_test_participant(id_project=id_project) self.assertIsNotNone(participant.id_participant) id_participant = participant.id_participant @@ -188,7 +188,7 @@ def test_hard_delete(self): def test_undelete(self): with self._flask_app.app_context(): # Create site - from test_TeraSite import TeraSiteTest + from tests.opentera.db.models.test_TeraSite import TeraSiteTest site = TeraSiteTest.new_test_site() id_site = site.id_site @@ -198,7 +198,7 @@ def test_undelete(self): id_project = project.id_project # Create a new participant in that project - from test_TeraParticipant import TeraParticipantTest + from tests.opentera.db.models.test_TeraParticipant import TeraParticipantTest participant = TeraParticipantTest.new_test_participant(id_project=id_project) self.assertIsNotNone(participant.id_participant) id_participant = participant.id_participant @@ -243,12 +243,12 @@ def test_project_relationships_deletion_and_access(self): id_project = project.id_project # Create participant groups - from test_TeraParticipantGroup import TeraParticipantGroupTest + from tests.opentera.db.models.test_TeraParticipantGroup import TeraParticipantGroupTest group = TeraParticipantGroupTest.new_test_group(id_project=id_project) self.assertIsNotNone(group.id_participant_group) id_participant_group1 = group.id_participant_group - from test_TeraParticipant import TeraParticipantTest + from tests.opentera.db.models.test_TeraParticipant import TeraParticipantTest participant = TeraParticipantTest.new_test_participant(id_project=1, id_participant_group=id_participant_group1) self.assertIsNotNone(participant.id_participant) diff --git a/teraserver/python/tests/opentera/db/models/test_TeraService.py b/teraserver/python/tests/opentera/db/models/test_TeraService.py index f47d4e6dd..a518a66e3 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraService.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraService.py @@ -400,7 +400,7 @@ def test_hard_delete(self): id_service = service.id_service # Create a new site association for that service - from test_TeraServiceSite import TeraServiceSiteTest + from tests.opentera.db.models.test_TeraServiceSite import TeraServiceSiteTest site_service = TeraServiceSiteTest.new_test_service_site(id_site=1, id_service=id_service) self.assertIsNotNone(site_service.id_service_site) id_site_service = site_service.id_service_site @@ -430,7 +430,7 @@ def test_undelete(self): id_service = service.id_service # Create service roles - from test_TeraServiceRole import TeraServiceRoleTest + from tests.opentera.db.models.test_TeraServiceRole import TeraServiceRoleTest role = TeraServiceRoleTest.new_test_service_role(id_service=id_service, role_name='admin') id_role_admin = role.id_service_role @@ -438,12 +438,12 @@ def test_undelete(self): id_role_user = role.id_service_role # Create service sites association - from test_TeraServiceSite import TeraServiceSiteTest + from tests.opentera.db.models.test_TeraServiceSite import TeraServiceSiteTest service_site = TeraServiceSiteTest.new_test_service_site(id_site=1, id_service=id_service) id_service_site = service_site.id_service_site # Create service projects association - from test_TeraServiceProject import TeraServiceProjectTest + from tests.opentera.db.models.test_TeraServiceProject import TeraServiceProjectTest service_project = TeraServiceProjectTest.new_test_service_project(id_service=id_service, id_project=1) id_service_project = service_project.id_service_project diff --git a/teraserver/python/tests/opentera/db/models/test_TeraSession.py b/teraserver/python/tests/opentera/db/models/test_TeraSession.py index a06789632..eb38d898a 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraSession.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraSession.py @@ -19,8 +19,8 @@ def test_session_defaults(self): for session in TeraSession.query.all(): my_list = [session.id_creator_device, session.id_creator_participant, session.id_creator_service, session.id_creator_user] - # Only one not None - self.assertEqual(1, len([x for x in my_list if x is not None])) + # At least one creator should be set + self.assertTrue(any(my_list)) def test_session_new(self): from datetime import datetime @@ -67,7 +67,7 @@ def test_soft_delete(self): id_session = ses.id_session # Attach asset - from test_TeraAsset import TeraAssetTest + from tests.opentera.db.models.test_TeraAsset import TeraAssetTest asset = TeraAssetTest.new_test_asset(id_session=id_session, service_uuid=TeraService.get_openteraserver_service().service_uuid, id_device=1) @@ -115,14 +115,14 @@ def test_hard_delete(self): id_session = ses.id_session # Attach asset - from test_TeraAsset import TeraAssetTest + from tests.opentera.db.models.test_TeraAsset import TeraAssetTest asset = TeraAssetTest.new_test_asset(id_session=id_session, service_uuid=TeraService.get_openteraserver_service().service_uuid, id_device=1) id_asset = asset.id_asset # ... and test - from test_TeraTest import TeraTestTest + from tests.opentera.db.models.test_TeraTest import TeraTestTest test = TeraTestTest.new_test_test(id_session=id_session, id_participant=1) id_test = test.id_test @@ -149,19 +149,19 @@ def test_undelete(self): id_session = ses.id_session # Attach asset - from test_TeraAsset import TeraAssetTest + from tests.opentera.db.models.test_TeraAsset import TeraAssetTest asset = TeraAssetTest.new_test_asset(id_session=id_session, service_uuid=TeraService.get_openteraserver_service().service_uuid, id_device=1) id_asset = asset.id_asset # ... and test - from test_TeraTest import TeraTestTest + from tests.opentera.db.models.test_TeraTest import TeraTestTest test = TeraTestTest.new_test_test(id_session=id_session, id_participant=1) id_test = test.id_test # ... and event - from test_TeraSessionEvent import TeraSessionEventTest + from tests.opentera.db.models.test_TeraSessionEvent import TeraSessionEventTest event = TeraSessionEventTest.new_test_session_event(id_session=id_session, id_event_type=1) id_event = event.id_session_event diff --git a/teraserver/python/tests/opentera/db/models/test_TeraSessionType.py b/teraserver/python/tests/opentera/db/models/test_TeraSessionType.py index 0caf78540..1ef3d73c7 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraSessionType.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraSessionType.py @@ -38,7 +38,7 @@ def test_hard_delete(self): id_session_type = ses_type.id_session_type # Create a new session of that session type - from test_TeraSession import TeraSessionTest + from tests.opentera.db.models.test_TeraSession import TeraSessionTest ses = TeraSessionTest.new_test_session(id_session_type=id_session_type, id_creator_service=1) id_session = ses.id_session @@ -62,7 +62,7 @@ def test_hard_delete(self): def test_undelete(self): with self._flask_app.app_context(): # Create new service - from test_TeraService import TeraServiceTest + from tests.opentera.db.models.test_TeraService import TeraServiceTest service = TeraServiceTest.new_test_service('SessionTypeService') id_service = service.id_service @@ -71,17 +71,17 @@ def test_undelete(self): id_session_type = ses_type.id_session_type # Create a new session of that session type - from test_TeraSession import TeraSessionTest + from tests.opentera.db.models.test_TeraSession import TeraSessionTest ses = TeraSessionTest.new_test_session(id_session_type=id_session_type, id_creator_service=1) id_session = ses.id_session # Associate session type to site - from test_TeraSessionTypeSite import TeraSessionTypeSiteTest + from tests.opentera.db.models.test_TeraSessionTypeSite import TeraSessionTypeSiteTest ses_site = TeraSessionTypeSiteTest.new_test_session_type_site(id_site=1, id_session_type=id_session_type) id_session_type_site = ses_site.id_session_type_site # ... and project - from test_TeraSessionTypeProject import TeraSessionTypeProjectTest + from tests.opentera.db.models.test_TeraSessionTypeProject import TeraSessionTypeProjectTest ses_proj = TeraSessionTypeProjectTest.new_test_session_type_project(id_project=1, id_session_type=id_session_type) id_session_type_project = ses_proj.id_session_type_project diff --git a/teraserver/python/tests/opentera/db/models/test_TeraSite.py b/teraserver/python/tests/opentera/db/models/test_TeraSite.py index 91fd2763a..7d80da00e 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraSite.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraSite.py @@ -1,4 +1,3 @@ -from tests.opentera.db.models.BaseModelsTest import BaseModelsTest from sqlalchemy import exc from opentera.db.models.TeraSite import TeraSite from opentera.db.models.TeraProject import TeraProject @@ -10,7 +9,22 @@ from opentera.db.models.TeraSessionTypeSite import TeraSessionTypeSite from opentera.db.models.TeraTestTypeSite import TeraTestTypeSite from opentera.db.models.TeraDevice import TeraDevice +from opentera.db.models.TeraUser import TeraUser +from opentera.db.models.TeraUserGroup import TeraUserGroup +from opentera.db.models.TeraUserUserGroup import TeraUserUserGroup +from opentera.db.models.TeraServiceAccess import TeraServiceAccess +from opentera.db.models.TeraService import TeraService +from tests.opentera.db.models.BaseModelsTest import BaseModelsTest +from tests.opentera.db.models.test_TeraDevice import TeraDeviceTest +from tests.opentera.db.models.test_TeraProject import TeraProjectTest +from tests.opentera.db.models.test_TeraParticipant import TeraParticipantTest +from tests.opentera.db.models.test_TeraSession import TeraSessionTest +from tests.opentera.db.models.test_TeraDeviceSite import TeraDeviceSiteTest +from tests.opentera.db.models.test_TeraServiceSite import TeraServiceSiteTest +from tests.opentera.db.models.test_TeraServiceRole import TeraServiceRoleTest +from tests.opentera.db.models.test_TeraSessionTypeSite import TeraSessionTypeSiteTest +from tests.opentera.db.models.test_TeraTestTypeSite import TeraTestTypeSiteTest class TeraSiteTest(BaseModelsTest): @@ -31,11 +45,29 @@ def test_unique_args(self): self.db.session.add(same_site1) self.assertRaises(exc.IntegrityError, self.db.session.commit) + def test_site_2fa_required_default(self): + with self._flask_app.app_context(): + new_site = TeraSite() + self.assertFalse(new_site.site_2fa_required) + + def test_site_2fa_required_update(self): + with self._flask_app.app_context(): + new_site = TeraSiteTest.new_test_site(name='Site With 2FA') + new_site.site_2fa_required = True + self.assertTrue(new_site.site_2fa_required) + self.db.session.add(new_site) + self.db.session.commit() + id_site = new_site.id_site + self.db.session.rollback() + same_site = TeraSite.get_site_by_id(id_site) + self.assertTrue(same_site.site_2fa_required) + def test_to_json(self): with self._flask_app.app_context(): new_site = TeraSiteTest.new_test_site(name='Site Name') new_site_json = new_site.to_json() new_site_json_minimal = new_site.to_json(minimal=True) + self.assertEqual(new_site_json['site_2fa_required'], False) self.assertEqual(new_site_json['site_name'], 'Site Name') self.assertGreaterEqual(new_site_json['id_site'], 1) self.assertEqual(new_site_json_minimal['site_name'], 'Site Name') @@ -118,17 +150,14 @@ def test_hard_delete(self): site = TeraSiteTest.new_test_site() id_site = site.id_site - from test_TeraProject import TeraProjectTest project = TeraProjectTest.new_test_project(id_site=id_site) self.assertIsNotNone(project.id_project) id_project = project.id_project - from test_TeraParticipant import TeraParticipantTest participant = TeraParticipantTest.new_test_participant(id_project=id_project) self.assertIsNotNone(participant.id_participant) id_participant = participant.id_participant - from test_TeraSession import TeraSessionTest ses = TeraSessionTest.new_test_session(id_session_type=1, id_creator_participant=1, participants=[participant]) id_session = ses.id_session @@ -165,31 +194,25 @@ def test_undelete(self): id_site = site.id_site # Associate device - from test_TeraDevice import TeraDeviceTest device = TeraDeviceTest.new_test_device() id_device = device.id_device - from test_TeraDeviceSite import TeraDeviceSiteTest device = TeraDeviceSiteTest.new_test_device_site(id_device=id_device, id_site=id_site) id_device_site = device.id_device_site # ... and service - from test_TeraServiceSite import TeraServiceSiteTest service_site = TeraServiceSiteTest.new_test_service_site(id_site=id_site, id_service=3) id_service_site = service_site.id_service_site # ... and roles - from test_TeraServiceRole import TeraServiceRoleTest role = TeraServiceRoleTest.new_test_service_role(id_service=3, id_site=id_site, role_name='Test') id_role = role.id_service_role # ... and session type - from test_TeraSessionTypeSite import TeraSessionTypeSiteTest ses_type = TeraSessionTypeSiteTest.new_test_session_type_site(id_site=id_site, id_session_type=1) id_session_type = ses_type.id_session_type_site # ... and test type - from test_TeraTestTypeSite import TeraTestTypeSiteTest test_type = TeraTestTypeSiteTest.new_test_test_type_site(id_site=id_site, id_test_type=1) id_test_type = test_type.id_test_type_site @@ -215,11 +238,231 @@ def test_undelete(self): self.assertIsNotNone(TeraSessionTypeSite.get_session_type_site_by_id(id_session_type)) self.assertIsNotNone(TeraTestTypeSite.get_test_type_site_by_id(id_test_type)) + def test_2fa_required_site(self): + with self._flask_app.app_context(): + site = TeraSiteTest.new_test_site(name='2FA Site', site_2fa_required=True) + self.assertTrue(site.site_2fa_required) + self.db.session.add(site) + self.db.session.commit() + id_site = site.id_site + self.db.session.rollback() + same_site = TeraSite.get_site_by_id(id_site) + self.assertTrue(same_site.site_2fa_required) + TeraSiteTest.delete_site(site.id_site) + + def test_enable_2fa_in_site_should_enable_2fa_for_users(self): + with self._flask_app.app_context(): + site = TeraSiteTest.new_test_site(name='2FA Site', site_2fa_required=False) + self.assertIsNotNone(site) + group = TeraSiteTest.new_test_user_group('Test Group', site.id_site) + self.assertIsNotNone(group) + user1 = TeraSiteTest.new_test_user('test_user1', 'Password12345!', group.id_user_group) + self.assertIsNotNone(user1) + + user2 = TeraSiteTest.new_test_user('test_user2', 'Password12345!', None) + self.assertIsNotNone(user2) + + # Enable 2fa in site + site.site_2fa_required = True + self.db.session.add(site) + self.db.session.commit() + + # User should be updated automatically with 2fa if group is associated with site + self.assertTrue(user1.user_2fa_enabled) + # Else user should not be updated + self.assertFalse(user2.user_2fa_enabled) + # Delete everything + TeraSiteTest.delete_site(site.id_site) + TeraSiteTest.delete_user(user1.id_user) + TeraSiteTest.delete_user(user2.id_user) + TeraSiteTest.delete_user_group(group.id_user_group) + + + def test_disable_2fa_in_site_should_not_disable_2fa_for_users(self): + with self._flask_app.app_context(): + site = TeraSiteTest.new_test_site(name='2FA Site', site_2fa_required=True) + self.assertIsNotNone(site) + group = TeraSiteTest.new_test_user_group('Test Group', site.id_site) + self.assertIsNotNone(group) + user1 = TeraSiteTest.new_test_user('test_user1', 'Password12345!', group.id_user_group) + self.assertIsNotNone(user1) + + user2 = TeraSiteTest.new_test_user('test_user2', 'Password12345!', None) + self.assertIsNotNone(user2) + + # Site should have 2fa enabled + self.db.session.add(site) + self.db.session.commit() + + # User should have 2fa enabled if group have access to site + self.assertTrue(user1.user_2fa_enabled) + # Else user should not be updated + self.assertFalse(user2.user_2fa_enabled) + + # Disable 2fa in site + site.site_2fa_required = False + self.db.session.add(site) + self.db.session.commit() + + # User should still have 2fa enabled (not changed) + self.assertTrue(user1.user_2fa_enabled) + # Else user should not be updated + self.assertFalse(user2.user_2fa_enabled) + + # Delete everything + TeraSiteTest.delete_site(site.id_site) + TeraSiteTest.delete_user(user1.id_user) + TeraSiteTest.delete_user(user2.id_user) + TeraSiteTest.delete_user_group(group.id_user_group) + + def test_add_group_to_2fa_enabled_site_should_enable_2fa_for_all_users(self): + with self._flask_app.app_context(): + site = TeraSiteTest.new_test_site(name='2FA Site', site_2fa_required=True) + self.assertIsNotNone(site) + + # No group associated to the user + user1 = TeraSiteTest.new_test_user('test_user1', 'Password12345!', None) + self.assertIsNotNone(user1) + + user2 = TeraSiteTest.new_test_user('test_user2', 'Password12345!', None) + self.assertIsNotNone(user2) + + # Site should have 2fa enabled + self.db.session.add(site) + self.db.session.commit() + + # User should have 2fa enabled if group have access to site + self.assertFalse(user1.user_2fa_enabled) + self.assertFalse(user2.user_2fa_enabled) + + # Add group to site + group = TeraSiteTest.new_test_user_group('Test Group', site.id_site) + self.assertIsNotNone(group) + + # Add users to group + user_user_group = TeraUserUserGroup() + user_user_group.id_user = user1.id_user + user_user_group.id_user_group = group.id_user_group + TeraUserUserGroup.insert(user_user_group) + + user_user_group = TeraUserUserGroup() + user_user_group.id_user = user2.id_user + user_user_group.id_user_group = group.id_user_group + TeraUserUserGroup.insert(user_user_group) + + # User should have 2fa enabled if group have access to site + self.assertTrue(user1.user_2fa_enabled) + self.assertTrue(user2.user_2fa_enabled) + + # Delete everything + TeraSiteTest.delete_site(site.id_site) + TeraSiteTest.delete_user(user1.id_user) + TeraSiteTest.delete_user(user2.id_user) + TeraSiteTest.delete_user_group(group.id_user_group) + + def test_disable_2fa_for_user_in_a_2fa_site_should_not_change_2fa_enabled(self): + with self._flask_app.app_context(): + site = TeraSiteTest.new_test_site(name='2FA Site', site_2fa_required=True) + self.assertIsNotNone(site) + group = TeraSiteTest.new_test_user_group('Test Group', site.id_site) + self.assertIsNotNone(group) + user1 = TeraSiteTest.new_test_user('test_user1', 'Password12345!', group.id_user_group) + self.assertIsNotNone(user1) + + # Site should have 2fa enabled + self.db.session.add(site) + self.db.session.commit() + + # User should have 2fa enabled if group have access to site + self.assertTrue(user1.user_2fa_enabled) + + # Disable 2fa for user + user1.user_2fa_enabled = False + self.db.session.add(user1) + self.db.session.commit() + + self.assertTrue(user1.user_2fa_enabled) + + # Delete everything + TeraSiteTest.delete_site(site.id_site) + TeraSiteTest.delete_user(user1.id_user) + TeraSiteTest.delete_user_group(group.id_user_group) + + def test_enable_2fa_for_site_needs_to_enable_2fa_for_all_superadmins(self): + with self._flask_app.app_context(): + site = TeraSiteTest.new_test_site(name='2FA Site', site_2fa_required=True) + self.assertIsNotNone(site) + + # Query all superadmins + superadmins = TeraUser.query.filter(TeraUser.user_superadmin == True).all() + for superadmin in superadmins: + self.assertTrue(superadmin.user_2fa_enabled) + + @staticmethod - def new_test_site(name: str = 'Test Site') -> TeraSite: + def new_test_site(name: str = 'Test Site', site_2fa_required: bool = False) -> TeraSite: site = TeraSite() site.site_name = name + site.site_2fa_required = site_2fa_required TeraSite.insert(site) return site + @staticmethod + def new_test_user_group(name: str, id_site: int ) -> TeraUserGroup: + + # Create Service Role first + tera_server_service = TeraService.get_openteraserver_service() + + service_role = TeraServiceRole() + service_role.service_role_name = 'Test Site Role' + service_role.id_service = tera_server_service.id_service + service_role.id_site = id_site + TeraServiceRole.insert(service_role) + + # Create User Group + group: TeraUserGroup = TeraUserGroup() + group.user_group_name = name + TeraUserGroup.insert(group) + + # Update Service Access + service_access = TeraServiceAccess() + service_access.id_service_role = service_role.id_service_role + service_access.id_user_group = group.id_user_group + TeraServiceAccess.insert(service_access) + + return group + + @staticmethod + def new_test_user(username: str, password: str, id_user_group: int | None) -> TeraUser: + user = TeraUser() + user.user_username = username + user.user_password = password + user.user_firstname = username + user.user_lastname = username + user.user_email = f"{username}@test.com" + user.user_enabled = True + user.user_profile = {} + TeraUser.insert(user) + + # Update user group if not none + if id_user_group is not None: + user_user_group = TeraUserUserGroup() + user_user_group.id_user = user.id_user + user_user_group.id_user_group = id_user_group + TeraUserUserGroup.insert(user_user_group) + + return user + + + @staticmethod + def delete_site(id: int): + TeraSite.delete(id, hard_delete=True) + + @staticmethod + def delete_user_group(id: int): + TeraUserGroup.delete(id, hard_delete=True) + + @staticmethod + def delete_user(id: int): + TeraUser.delete(id, hard_delete=True) diff --git a/teraserver/python/tests/opentera/db/models/test_TeraTest.py b/teraserver/python/tests/opentera/db/models/test_TeraTest.py index 97c3ce652..aa3b87c56 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraTest.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraTest.py @@ -46,22 +46,22 @@ def test_hard_delete(self): def test_undelete(self): with self._flask_app.app_context(): # Create new participant - from test_TeraParticipant import TeraParticipantTest + from tests.opentera.db.models.test_TeraParticipant import TeraParticipantTest participant = TeraParticipantTest.new_test_participant(id_project=1) id_participant = participant.id_participant # Create new device - from test_TeraDevice import TeraDeviceTest + from tests.opentera.db.models.test_TeraDevice import TeraDeviceTest device = TeraDeviceTest.new_test_device() id_device = device.id_device # Create new user - from test_TeraUser import TeraUserTest + from tests.opentera.db.models.test_TeraUser import TeraUserTest user = TeraUserTest.new_test_user(user_name='test_testuser') id_user = user.id_user # Create new session - from test_TeraSession import TeraSessionTest + from tests.opentera.db.models.test_TeraSession import TeraSessionTest ses = TeraSessionTest.new_test_session(participants=[participant], users=[user], devices=[device]) id_session = ses.id_session diff --git a/teraserver/python/tests/opentera/db/models/test_TeraTestType.py b/teraserver/python/tests/opentera/db/models/test_TeraTestType.py index 32fedb4ff..345dcd3d7 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraTestType.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraTestType.py @@ -35,7 +35,7 @@ def test_hard_delete(self): test_type = TeraTestTypeTest.new_test_test_type() id_test_type = test_type.id_test_type - from test_TeraTest import TeraTestTest + from tests.opentera.db.models.test_TeraTest import TeraTestTest test = TeraTestTest.new_test_test(id_session=1, id_participant=1, id_test_type=id_test_type) self.assertIsNotNone(test.id_test) id_test = test.id_test @@ -63,18 +63,18 @@ def test_undelete(self): test_type = TeraTestTypeTest.new_test_test_type() id_test_type = test_type.id_test_type - from test_TeraTest import TeraTestTest + from tests.opentera.db.models.test_TeraTest import TeraTestTest test = TeraTestTest.new_test_test(id_session=1, id_participant=1, id_test_type=id_test_type) self.assertIsNotNone(test.id_test) id_test = test.id_test # Associate with site - from test_TeraTestTypeSite import TeraTestTypeSiteTest + from tests.opentera.db.models.test_TeraTestTypeSite import TeraTestTypeSiteTest tt_site = TeraTestTypeSiteTest.new_test_test_type_site(id_site=1, id_test_type=id_test_type) id_test_type_site = tt_site.id_test_type_site # ... and project - from test_TeraTestTypeProject import TeraTestTypeProjectTest + from tests.opentera.db.models.test_TeraTestTypeProject import TeraTestTypeProjectTest tt_project = TeraTestTypeProjectTest.new_test_test_type_project(id_project=1, id_test_type=id_test_type) id_test_type_project = tt_project.id_test_type_project diff --git a/teraserver/python/tests/opentera/db/models/test_TeraUser.py b/teraserver/python/tests/opentera/db/models/test_TeraUser.py index 4e4e134e3..865e10a09 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraUser.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraUser.py @@ -1,4 +1,9 @@ +import uuid +import jwt + + from modules.DatabaseModule.DBManager import DBManager + from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraSite import TeraSite from opentera.db.models.TeraProject import TeraProject @@ -8,10 +13,16 @@ from opentera.db.models.TeraService import TeraService from opentera.db.models.TeraServiceConfig import TeraServiceConfig from opentera.db.models.TeraUserUserGroup import TeraUserUserGroup -from tests.opentera.db.models.BaseModelsTest import BaseModelsTest -import uuid -import jwt +from opentera.db.models.TeraServiceRole import TeraServiceRole +from opentera.db.models.TeraUserGroup import TeraUserGroup +from opentera.db.models.TeraServiceAccess import TeraServiceAccess +from tests.opentera.db.models.BaseModelsTest import BaseModelsTest +from tests.opentera.db.models.test_TeraSession import TeraSessionTest +from tests.opentera.db.models.test_TeraAsset import TeraAssetTest +from tests.opentera.db.models.test_TeraTest import TeraTestTest +from tests.opentera.db.models.test_TeraUserUserGroup import TeraUserUserGroupTest +from tests.opentera.db.models.test_TeraServiceConfig import TeraServiceConfigTest class TeraUserTest(BaseModelsTest): @@ -98,7 +109,6 @@ def test_hard_delete(self): id_user = user.id_user # Assign user to sessions - from test_TeraSession import TeraSessionTest user_session = TeraSessionTest.new_test_session(id_creator_user=id_user) id_session = user_session.id_session @@ -106,14 +116,12 @@ def test_hard_delete(self): id_session_invitee = user_session.id_session # Attach asset - from test_TeraAsset import TeraAssetTest asset = TeraAssetTest.new_test_asset(id_session=id_session, service_uuid=TeraService.get_openteraserver_service().service_uuid, id_user=id_user) id_asset = asset.id_asset # ... and test - from test_TeraTest import TeraTestTest test = TeraTestTest.new_test_test(id_session=id_session, id_user=id_user) id_test = test.id_test @@ -152,12 +160,10 @@ def test_undelete(self): id_user = user.id_user # Assign to user group - from test_TeraUserUserGroup import TeraUserUserGroupTest uug = TeraUserUserGroupTest.new_test_user_usergroup(id_user=id_user, id_user_group=1) id_user_user_group = uug.id_user_user_group # Assign user to sessions - from test_TeraSession import TeraSessionTest user_session = TeraSessionTest.new_test_session(id_creator_user=id_user) id_session = user_session.id_session @@ -165,19 +171,16 @@ def test_undelete(self): id_session_invitee = user_session.id_session # Attach asset - from test_TeraAsset import TeraAssetTest asset = TeraAssetTest.new_test_asset(id_session=id_session, service_uuid=TeraService.get_openteraserver_service().service_uuid, id_user=id_user) id_asset = asset.id_asset # ... and test - from test_TeraTest import TeraTestTest test = TeraTestTest.new_test_test(id_session=id_session, id_user=id_user) id_test = test.id_test # ... and service config - from test_TeraServiceConfig import TeraServiceConfigTest service_conf = TeraServiceConfigTest.new_test_service_config(id_service=1, id_user=id_user) id_service_conf = service_conf.id_service_config @@ -227,12 +230,6 @@ def test_token_for_admin_should_have_empty_service_access(self): self.assertEqual(token_dict['service_access'], {}) # Should be empty def test_token_for_siteadmin_should_have_valid_service_access(self): - from opentera.db.models.TeraService import TeraService - from opentera.db.models.TeraServiceRole import TeraServiceRole - from opentera.db.models.TeraUserGroup import TeraUserGroup - from opentera.db.models.TeraServiceAccess import TeraServiceAccess - from opentera.db.models.TeraUserUserGroup import TeraUserUserGroup - with self._flask_app.app_context(): user = TeraUser.get_user_by_username('siteadmin') self.assertIsNotNone(user) @@ -282,6 +279,66 @@ def test_token_for_siteadmin_should_have_valid_service_access(self): # TeraServiceAccess.delete(service_access.id_service_access) # TeraServiceRole.delete(role.id_service_role) + def test_disable_2fa_on_2fa_enabled_user_should_reset_secret_email_and_otp_states(self): + with self._flask_app.app_context(): + user: TeraUser = TeraUserTest.new_test_user(user_name="user_2fa", user_groups=None) + self.assertIsNotNone(user) + self.assertFalse(user.user_2fa_enabled) + # Setup 2FA + user.enable_2fa_otp() + self.assertTrue(user.user_2fa_enabled) + self.assertIsNotNone(user.user_2fa_otp_secret) + self.assertTrue(user.user_2fa_otp_enabled) + # Commit user + self.db.session.add(user) + self.db.session.commit() + # Disable 2FA + user.user_2fa_enabled = False + self.db.session.add(user) + self.db.session.commit() + # Check + self.assertFalse(user.user_2fa_enabled) + self.assertIsNone(user.user_2fa_otp_secret) + self.assertFalse(user.user_2fa_otp_enabled) + self.assertFalse(user.user_2fa_email_enabled) + # Delete user + TeraUser.delete(user.id_user, hard_delete=True) + + + def test_change_superadmin_2fa_enabled_while_site_has_2fa_required_does_not_change_2fa_enabled(self): + with self._flask_app.app_context(): + sites = TeraSite.query.all() + for site in sites: + site.site_2fa_required = True + self.db.session.add(site) + self.db.session.commit() + + superadmins = TeraUser.query.filter_by(user_superadmin=True).all() + for superadmin in superadmins: + self.assertTrue(superadmin.user_2fa_enabled) + superadmin.user_2fa_enabled = False + self.db.session.add(superadmin) + self.db.session.commit() + + # Reverify flag + for superadmin in superadmins: + self.assertTrue(superadmin.user_2fa_enabled) + + # Reset site 2fa required + for site in sites: + site.site_2fa_required = False + self.db.session.add(site) + self.db.session.commit() + + #Reset superadmin 2fa enabled + for superadmin in superadmins: + superadmin.user_2fa_enabled = False + self.db.session.add(superadmin) + self.db.session.commit() + + for superadmin in superadmins: + self.assertFalse(superadmin.user_2fa_enabled) + @staticmethod def new_test_user(user_name: str, user_groups: list | None = None) -> TeraUser: user = TeraUser() diff --git a/teraserver/python/tests/opentera/db/models/test_TeraUserGroup.py b/teraserver/python/tests/opentera/db/models/test_TeraUserGroup.py index 761a26918..98decfe92 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraUserGroup.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraUserGroup.py @@ -253,7 +253,7 @@ def test_hard_delete(self): self.assertIsNotNone(ug.id_user_group) id_user_group = ug.id_user_group - from test_TeraUser import TeraUserTest + from tests.opentera.db.models.test_TeraUser import TeraUserTest user = TeraUserTest.new_test_user(user_name="user_ug_harddelete", user_groups=[ug]) self.assertIsNotNone(user.id_user) id_user = user.id_user @@ -286,13 +286,13 @@ def test_undelete(self): self.assertIsNotNone(ug.id_user_group) id_user_group = ug.id_user_group - from test_TeraUser import TeraUserTest + from tests.opentera.db.models.test_TeraUser import TeraUserTest user = TeraUserTest.new_test_user(user_name="user_ug_undelete", user_groups=[ug]) self.assertIsNotNone(user.id_user) id_user = user.id_user # Add service access - from test_TeraServiceAccess import TeraServiceAccessTest + from tests.opentera.db.models.test_TeraServiceAccess import TeraServiceAccessTest access = TeraServiceAccessTest.new_test_service_access(id_service_role=1, id_user_group=id_user_group) id_access = access.id_service_access diff --git a/teraserver/python/tests/services/FileTransferService/BaseFileTransferServiceAPITest.py b/teraserver/python/tests/services/FileTransferService/BaseFileTransferServiceAPITest.py index 0eca382fd..b43854c4f 100644 --- a/teraserver/python/tests/services/FileTransferService/BaseFileTransferServiceAPITest.py +++ b/teraserver/python/tests/services/FileTransferService/BaseFileTransferServiceAPITest.py @@ -8,7 +8,7 @@ import uuid import random from string import digits, ascii_lowercase, ascii_uppercase -from FakeFileTransferService import FakeFileTransferService +from tests.services.FileTransferService.FakeFileTransferService import FakeFileTransferService from opentera.services.ServiceAccessManager import ServiceAccessManager from opentera.db.models.TeraService import TeraService from modules.LoginModule.LoginModule import LoginModule diff --git a/teraserver/python/tests/services/FileTransferService/__init__.py b/teraserver/python/tests/services/FileTransferService/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/teraserver/python/tests/services/LoggingService/BaseLoggingServiceAPITest.py b/teraserver/python/tests/services/LoggingService/BaseLoggingServiceAPITest.py index 151e548f0..3cb6a5d1e 100644 --- a/teraserver/python/tests/services/LoggingService/BaseLoggingServiceAPITest.py +++ b/teraserver/python/tests/services/LoggingService/BaseLoggingServiceAPITest.py @@ -6,7 +6,7 @@ import uuid import random from string import digits, ascii_lowercase, ascii_uppercase -from FakeLoggingService import FakeLoggingService +from tests.services.LoggingService.FakeLoggingService import FakeLoggingService from opentera.services.ServiceAccessManager import ServiceAccessManager from requests.auth import _basic_auth_str diff --git a/teraserver/python/tests/services/LoggingService/__init__.py b/teraserver/python/tests/services/LoggingService/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/teraserver/python/tests/services/LoggingService/test_QueryLogEntries.py b/teraserver/python/tests/services/LoggingService/test_QueryLogEntries.py index 246965a0a..be4eca81c 100644 --- a/teraserver/python/tests/services/LoggingService/test_QueryLogEntries.py +++ b/teraserver/python/tests/services/LoggingService/test_QueryLogEntries.py @@ -1,4 +1,4 @@ -from BaseLoggingServiceAPITest import BaseLoggingServiceAPITest +from tests.services.LoggingService.BaseLoggingServiceAPITest import BaseLoggingServiceAPITest from services.LoggingService.libloggingservice.db.models.LogEntry import LogEntry from datetime import datetime, timedelta @@ -28,7 +28,7 @@ def test_get_endpoint_with_valid_token_and_admin(self): token = self._generate_fake_user_token(name='FakeUser', superadmin=True, expiration=3600) all_entries = [] - for i in range(50): + for _ in range(50): current_time = datetime.now() entry = self._create_log_entry(current_time, 1, 'test', 'test_message') self.assertIsNotNone(entry) @@ -52,14 +52,14 @@ def test_get_endpoint_with_valid_token_and_admin_with_start_end_dates(self): current_time = datetime.now() + timedelta(hours=1) tomorrow = current_time + timedelta(days=1) - for i in range(50): + for _ in range(50): entry = self._create_log_entry(current_time, 1, 'test', 'test_message') self.assertIsNotNone(entry) LogEntry.insert(entry) self.assertIsNotNone(entry.id_log_entry) all_entries.append(entry) - for i in range(50): + for _ in range(50): entry = self._create_log_entry(tomorrow, 1, 'test', 'test_message') self.assertIsNotNone(entry) LogEntry.insert(entry) @@ -91,7 +91,7 @@ def test_get_endpoint_with_valid_token_and_admin_with_offset(self): all_entries = [] current_time = datetime.now() - for i in range(50): + for _ in range(50): entry = self._create_log_entry(current_time, 1, 'test', 'test_message') self.assertIsNotNone(entry) LogEntry.insert(entry) diff --git a/teraserver/python/tests/services/LoggingService/test_QueryLoginEntries.py b/teraserver/python/tests/services/LoggingService/test_QueryLoginEntries.py index 4cb7bcf5c..1e2e2ef61 100644 --- a/teraserver/python/tests/services/LoggingService/test_QueryLoginEntries.py +++ b/teraserver/python/tests/services/LoggingService/test_QueryLoginEntries.py @@ -1,4 +1,4 @@ -from BaseLoggingServiceAPITest import BaseLoggingServiceAPITest +from tests.services.LoggingService.BaseLoggingServiceAPITest import BaseLoggingServiceAPITest from services.LoggingService.libloggingservice.db.models.LoginEntry import LoginEntry from opentera.services.ServiceAccessManager import ServiceAccessManager from opentera.services.TeraUserClient import TeraUserClient diff --git a/teraserver/python/tests/services/__init__.py b/teraserver/python/tests/services/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/teraserver/python/translations/en/LC_MESSAGES/messages.po b/teraserver/python/translations/en/LC_MESSAGES/messages.po index b94de54e9..685782d6b 100644 --- a/teraserver/python/translations/en/LC_MESSAGES/messages.po +++ b/teraserver/python/translations/en/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-05-13 11:02-0400\n" +"POT-Creation-Date: 2024-10-07 15:05-0400\n" "PO-Revision-Date: 2021-01-25 13:01-0500\n" "Last-Translator: \n" "Language: en\n" @@ -16,165 +16,191 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.15.0\n" +"Generated-By: Babel 2.16.0\n" -#: modules/FlaskModule/API/device/DeviceLogin.py:88 +#: modules/FlaskModule/API/user/UserQueryUsers.py:53 +#: modules/FlaskModule/FlaskUtils.py:13 +msgid "Password missing special character" +msgstr "" + +#: modules/FlaskModule/API/user/UserQueryUsers.py:55 +#: modules/FlaskModule/FlaskUtils.py:15 +msgid "Password missing numeric character" +msgstr "" + +#: modules/FlaskModule/FlaskUtils.py:17 +msgid "Password not long enough (10 characters)" +msgstr "" + +#: modules/FlaskModule/API/user/UserQueryUsers.py:59 +#: modules/FlaskModule/FlaskUtils.py:19 +msgid "Password missing lower case letter" +msgstr "" + +#: modules/FlaskModule/API/user/UserQueryUsers.py:61 +#: modules/FlaskModule/FlaskUtils.py:21 +msgid "Password missing upper case letter" +msgstr "" + +#: modules/FlaskModule/API/device/DeviceLogin.py:90 msgid "Unable to get online devices." msgstr "" -#: modules/FlaskModule/API/device/DeviceLogin.py:104 +#: modules/FlaskModule/API/device/DeviceLogin.py:106 msgid "Device already logged in." msgstr "" -#: modules/FlaskModule/API/device/DeviceLogout.py:29 +#: modules/FlaskModule/API/device/DeviceLogout.py:32 msgid "Device logged out." msgstr "" -#: modules/FlaskModule/API/device/DeviceLogout.py:31 +#: modules/FlaskModule/API/device/DeviceLogout.py:34 msgid "Device not logged in" msgstr "" -#: modules/FlaskModule/API/device/DeviceQueryAssets.py:39 +#: modules/FlaskModule/API/device/DeviceQueryAssets.py:42 +#: modules/FlaskModule/API/participant/ParticipantQueryAssets.py:48 msgid "No access to session" msgstr "" -#: modules/FlaskModule/API/device/DeviceQueryDevices.py:69 +#: modules/FlaskModule/API/device/DeviceQueryDevices.py:75 msgid "Missing device schema" msgstr "" -#: modules/FlaskModule/API/device/DeviceQueryDevices.py:78 +#: modules/FlaskModule/API/device/DeviceQueryDevices.py:84 msgid "Missing config" msgstr "" -#: modules/FlaskModule/API/device/DeviceQueryDevices.py:82 -#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:49 -#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:55 -#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:62 -#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:68 -#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:74 -#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:80 -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:52 -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:66 -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:121 -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:144 -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:179 -#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:72 -#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:79 -#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:89 -#: modules/FlaskModule/API/service/ServiceQueryProjects.py:52 -#: modules/FlaskModule/API/service/ServiceQueryProjects.py:57 -#: modules/FlaskModule/API/service/ServiceQueryProjects.py:99 -#: modules/FlaskModule/API/service/ServiceQueryProjects.py:106 -#: modules/FlaskModule/API/service/ServiceQueryProjects.py:158 -#: modules/FlaskModule/API/service/ServiceQueryRoles.py:112 -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:100 -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:104 -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:108 -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:116 -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:194 -#: modules/FlaskModule/API/service/ServiceQuerySessionEvents.py:86 +#: modules/FlaskModule/API/device/DeviceQueryDevices.py:88 +#: modules/FlaskModule/API/participant/ParticipantQueryAssets.py:44 +#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:51 +#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:57 +#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:64 +#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:70 +#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:76 +#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:82 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:55 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:69 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:127 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:150 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:188 +#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:75 +#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:82 +#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:92 +#: modules/FlaskModule/API/service/ServiceQueryProjects.py:55 +#: modules/FlaskModule/API/service/ServiceQueryProjects.py:60 +#: modules/FlaskModule/API/service/ServiceQueryProjects.py:105 +#: modules/FlaskModule/API/service/ServiceQueryProjects.py:112 +#: modules/FlaskModule/API/service/ServiceQueryProjects.py:167 +#: modules/FlaskModule/API/service/ServiceQueryRoles.py:121 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:106 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:110 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:114 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:122 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:203 #: modules/FlaskModule/API/service/ServiceQuerySessionEvents.py:91 -#: modules/FlaskModule/API/service/ServiceQuerySessions.py:66 -#: modules/FlaskModule/API/service/ServiceQuerySessions.py:72 -#: modules/FlaskModule/API/service/ServiceQuerySessions.py:76 -#: modules/FlaskModule/API/service/ServiceQuerySessions.py:84 -#: modules/FlaskModule/API/service/ServiceQuerySessions.py:90 -#: modules/FlaskModule/API/service/ServiceQuerySites.py:36 -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:121 -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:159 -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:193 -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:256 -#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:96 -#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:101 -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:50 -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:54 -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:106 -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:207 -#: modules/FlaskModule/API/user/UserQueryAssetsArchive.py:117 -#: modules/FlaskModule/API/user/UserQueryAssetsArchive.py:126 -#: modules/FlaskModule/API/user/UserQueryAssetsArchive.py:137 -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:301 -#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:122 -#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:257 -#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:112 -#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:165 -#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:105 -#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:191 -#: modules/FlaskModule/API/user/UserQueryDevices.py:293 -#: modules/FlaskModule/API/user/UserQueryDevices.py:297 -#: modules/FlaskModule/API/user/UserQueryDevices.py:417 -#: modules/FlaskModule/API/user/UserQueryDisconnect.py:52 -#: modules/FlaskModule/API/user/UserQueryDisconnect.py:60 -#: modules/FlaskModule/API/user/UserQueryDisconnect.py:66 -#: modules/FlaskModule/API/user/UserQueryDisconnect.py:72 -#: modules/FlaskModule/API/user/UserQueryDisconnect.py:78 -#: modules/FlaskModule/API/user/UserQueryDisconnect.py:84 -#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:107 -#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:159 -#: modules/FlaskModule/API/user/UserQueryParticipants.py:362 -#: modules/FlaskModule/API/user/UserQueryParticipants.py:365 -#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:204 -#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:274 -#: modules/FlaskModule/API/user/UserQueryProjects.py:151 -#: modules/FlaskModule/API/user/UserQueryProjects.py:156 -#: modules/FlaskModule/API/user/UserQueryProjects.py:264 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:117 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:122 +#: modules/FlaskModule/API/service/ServiceQuerySessionEvents.py:96 +#: modules/FlaskModule/API/service/ServiceQuerySessions.py:69 +#: modules/FlaskModule/API/service/ServiceQuerySessions.py:75 +#: modules/FlaskModule/API/service/ServiceQuerySessions.py:79 +#: modules/FlaskModule/API/service/ServiceQuerySessions.py:87 +#: modules/FlaskModule/API/service/ServiceQuerySessions.py:93 +#: modules/FlaskModule/API/service/ServiceQuerySites.py:39 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:127 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:165 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:199 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:265 +#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:102 +#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:107 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:52 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:56 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:111 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:215 +#: modules/FlaskModule/API/user/UserQueryAssetsArchive.py:116 +#: modules/FlaskModule/API/user/UserQueryAssetsArchive.py:125 +#: modules/FlaskModule/API/user/UserQueryAssetsArchive.py:136 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:306 +#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:125 +#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:262 +#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:115 +#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:170 +#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:107 +#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:195 +#: modules/FlaskModule/API/user/UserQueryDevices.py:296 +#: modules/FlaskModule/API/user/UserQueryDevices.py:300 +#: modules/FlaskModule/API/user/UserQueryDevices.py:422 +#: modules/FlaskModule/API/user/UserQueryDisconnect.py:53 +#: modules/FlaskModule/API/user/UserQueryDisconnect.py:61 +#: modules/FlaskModule/API/user/UserQueryDisconnect.py:67 +#: modules/FlaskModule/API/user/UserQueryDisconnect.py:73 +#: modules/FlaskModule/API/user/UserQueryDisconnect.py:79 +#: modules/FlaskModule/API/user/UserQueryDisconnect.py:85 +#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:110 +#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:164 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:368 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:371 +#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:207 +#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:279 +#: modules/FlaskModule/API/user/UserQueryProjects.py:155 +#: modules/FlaskModule/API/user/UserQueryProjects.py:160 +#: modules/FlaskModule/API/user/UserQueryProjects.py:270 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:121 #: modules/FlaskModule/API/user/UserQueryServiceAccess.py:126 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:134 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:213 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:217 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:221 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:225 -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:151 -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:155 -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:159 -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:165 -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:240 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:130 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:138 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:219 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:223 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:227 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:231 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:154 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:158 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:162 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:168 #: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:245 -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:249 -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:253 -#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:79 -#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:136 -#: modules/FlaskModule/API/user/UserQueryServiceSites.py:129 -#: modules/FlaskModule/API/user/UserQueryServiceSites.py:259 -#: modules/FlaskModule/API/user/UserQueryServices.py:121 -#: modules/FlaskModule/API/user/UserQueryServices.py:134 -#: modules/FlaskModule/API/user/UserQueryServices.py:231 -#: modules/FlaskModule/API/user/UserQueryServices.py:239 -#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:91 -#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:144 -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:204 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:203 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:267 -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:55 -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:107 -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:138 -#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:187 -#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:275 -#: modules/FlaskModule/API/user/UserQuerySites.py:124 -#: modules/FlaskModule/API/user/UserQuerySites.py:127 -#: modules/FlaskModule/API/user/UserQuerySites.py:176 -#: modules/FlaskModule/API/user/UserQueryStats.py:52 -#: modules/FlaskModule/API/user/UserQueryStats.py:57 -#: modules/FlaskModule/API/user/UserQueryStats.py:62 -#: modules/FlaskModule/API/user/UserQueryStats.py:68 -#: modules/FlaskModule/API/user/UserQueryStats.py:73 -#: modules/FlaskModule/API/user/UserQueryStats.py:81 -#: modules/FlaskModule/API/user/UserQueryStats.py:86 -#: modules/FlaskModule/API/user/UserQueryStats.py:91 -#: modules/FlaskModule/API/user/UserQueryTestType.py:62 -#: modules/FlaskModule/API/user/UserQueryTestType.py:169 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:205 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:193 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:256 -#: modules/FlaskModule/API/user/UserQueryTests.py:140 -#: modules/FlaskModule/API/user/UserQueryUserGroups.py:147 -#: modules/FlaskModule/API/user/UserQueryUserPreferences.py:47 -#: modules/FlaskModule/API/user/UserQueryUserPreferences.py:96 -#: modules/FlaskModule/API/user/UserQueryUsers.py:214 -#: modules/FlaskModule/API/user/UserQueryUsers.py:219 -#: modules/FlaskModule/API/user/UserQueryUsers.py:348 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:250 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:254 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:258 +#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:83 +#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:142 +#: modules/FlaskModule/API/user/UserQueryServiceSites.py:133 +#: modules/FlaskModule/API/user/UserQueryServiceSites.py:265 +#: modules/FlaskModule/API/user/UserQueryServices.py:124 +#: modules/FlaskModule/API/user/UserQueryServices.py:137 +#: modules/FlaskModule/API/user/UserQueryServices.py:236 +#: modules/FlaskModule/API/user/UserQueryServices.py:244 +#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:94 +#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:149 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:208 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:207 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:273 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:57 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:111 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:142 +#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:190 +#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:280 +#: modules/FlaskModule/API/user/UserQuerySites.py:128 +#: modules/FlaskModule/API/user/UserQuerySites.py:132 +#: modules/FlaskModule/API/user/UserQuerySites.py:183 +#: modules/FlaskModule/API/user/UserQueryStats.py:54 +#: modules/FlaskModule/API/user/UserQueryStats.py:59 +#: modules/FlaskModule/API/user/UserQueryStats.py:64 +#: modules/FlaskModule/API/user/UserQueryStats.py:70 +#: modules/FlaskModule/API/user/UserQueryStats.py:75 +#: modules/FlaskModule/API/user/UserQueryStats.py:83 +#: modules/FlaskModule/API/user/UserQueryStats.py:88 +#: modules/FlaskModule/API/user/UserQueryStats.py:93 +#: modules/FlaskModule/API/user/UserQueryTestType.py:64 +#: modules/FlaskModule/API/user/UserQueryTestType.py:173 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:209 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:197 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:262 +#: modules/FlaskModule/API/user/UserQueryTests.py:147 +#: modules/FlaskModule/API/user/UserQueryUserGroups.py:150 +#: modules/FlaskModule/API/user/UserQueryUserPreferences.py:48 +#: modules/FlaskModule/API/user/UserQueryUserPreferences.py:99 +#: modules/FlaskModule/API/user/UserQueryUsers.py:229 +#: modules/FlaskModule/API/user/UserQueryUsers.py:234 +#: modules/FlaskModule/API/user/UserQueryUsers.py:356 #: opentera/services/ServiceAccessManager.py:116 #: opentera/services/ServiceAccessManager.py:166 #: opentera/services/ServiceAccessManager.py:195 @@ -190,933 +216,1009 @@ msgstr "" msgid "Forbidden" msgstr "" -#: modules/FlaskModule/API/device/DeviceQueryDevices.py:93 -#: modules/FlaskModule/API/device/DeviceQuerySessionEvents.py:74 -#: modules/FlaskModule/API/device/DeviceQuerySessionEvents.py:89 -#: modules/FlaskModule/API/device/DeviceQuerySessions.py:157 -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:233 -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:247 -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:284 -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:136 -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:153 -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:201 -#: modules/FlaskModule/API/service/ServiceQueryProjects.py:116 -#: modules/FlaskModule/API/service/ServiceQueryProjects.py:138 -#: modules/FlaskModule/API/service/ServiceQueryProjects.py:176 -#: modules/FlaskModule/API/service/ServiceQueryRoles.py:73 -#: modules/FlaskModule/API/service/ServiceQueryRoles.py:94 -#: modules/FlaskModule/API/service/ServiceQueryRoles.py:123 -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:138 -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:150 -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:172 -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:205 -#: modules/FlaskModule/API/service/ServiceQuerySessionEvents.py:104 -#: modules/FlaskModule/API/service/ServiceQuerySessionEvents.py:119 -#: modules/FlaskModule/API/service/ServiceQuerySessions.py:185 -#: modules/FlaskModule/API/service/ServiceQuerySessions.py:218 -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:233 -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:274 -#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:111 -#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:132 -#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:166 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:231 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:242 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:277 -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:151 -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:168 -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:225 -#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:180 -#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:221 -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:276 -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:331 -#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:232 -#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:268 -#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:130 -#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:145 -#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:187 -#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:121 -#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:136 -#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:189 -#: modules/FlaskModule/API/user/UserQueryDevices.py:328 -#: modules/FlaskModule/API/user/UserQueryDevices.py:343 -#: modules/FlaskModule/API/user/UserQueryDevices.py:460 -#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:120 -#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:135 -#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:178 -#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:244 -#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:285 -#: modules/FlaskModule/API/user/UserQueryProjects.py:196 -#: modules/FlaskModule/API/user/UserQueryProjects.py:211 -#: modules/FlaskModule/API/user/UserQueryProjects.py:282 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:156 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:168 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:190 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:236 -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:208 -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:264 -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:287 -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:322 -#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:98 -#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:119 -#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:147 -#: modules/FlaskModule/API/user/UserQueryServiceSites.py:244 -#: modules/FlaskModule/API/user/UserQueryServiceSites.py:286 -#: modules/FlaskModule/API/user/UserQueryServices.py:162 -#: modules/FlaskModule/API/user/UserQueryServices.py:182 -#: modules/FlaskModule/API/user/UserQueryServices.py:259 -#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:104 -#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:119 -#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:155 -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:251 -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:291 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:244 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:286 -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:197 -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:212 -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:342 -#: modules/FlaskModule/API/user/UserQuerySessions.py:179 -#: modules/FlaskModule/API/user/UserQuerySessions.py:194 -#: modules/FlaskModule/API/user/UserQuerySessions.py:272 -#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:246 -#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:286 -#: modules/FlaskModule/API/user/UserQuerySites.py:140 -#: modules/FlaskModule/API/user/UserQuerySites.py:155 -#: modules/FlaskModule/API/user/UserQuerySites.py:194 -#: modules/FlaskModule/API/user/UserQueryTestType.py:222 -#: modules/FlaskModule/API/user/UserQueryTestType.py:237 -#: modules/FlaskModule/API/user/UserQueryTestType.py:345 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:252 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:293 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:233 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:274 -#: modules/FlaskModule/API/user/UserQueryTests.py:151 -#: modules/FlaskModule/API/user/UserQueryUserGroups.py:190 -#: modules/FlaskModule/API/user/UserQueryUserGroups.py:205 -#: modules/FlaskModule/API/user/UserQueryUserGroups.py:233 -#: modules/FlaskModule/API/user/UserQueryUserGroups.py:262 -#: modules/FlaskModule/API/user/UserQueryUserGroups.py:310 -#: modules/FlaskModule/API/user/UserQueryUserPreferences.py:111 -#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:138 -#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:174 -#: modules/FlaskModule/API/user/UserQueryUsers.py:240 -#: modules/FlaskModule/API/user/UserQueryUsers.py:272 -#: modules/FlaskModule/API/user/UserQueryUsers.py:385 +#: modules/FlaskModule/API/device/DeviceQueryDevices.py:99 +#: modules/FlaskModule/API/device/DeviceQuerySessionEvents.py:79 +#: modules/FlaskModule/API/device/DeviceQuerySessionEvents.py:94 +#: modules/FlaskModule/API/device/DeviceQuerySessions.py:162 +#: modules/FlaskModule/API/participant/ParticipantQuerySessions.py:188 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:239 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:253 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:293 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:142 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:159 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:210 +#: modules/FlaskModule/API/service/ServiceQueryProjects.py:122 +#: modules/FlaskModule/API/service/ServiceQueryProjects.py:144 +#: modules/FlaskModule/API/service/ServiceQueryProjects.py:185 +#: modules/FlaskModule/API/service/ServiceQueryRoles.py:79 +#: modules/FlaskModule/API/service/ServiceQueryRoles.py:100 +#: modules/FlaskModule/API/service/ServiceQueryRoles.py:132 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:144 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:156 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:178 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:214 +#: modules/FlaskModule/API/service/ServiceQuerySessionEvents.py:109 +#: modules/FlaskModule/API/service/ServiceQuerySessionEvents.py:124 +#: modules/FlaskModule/API/service/ServiceQuerySessions.py:191 +#: modules/FlaskModule/API/service/ServiceQuerySessions.py:224 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:239 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:283 +#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:117 +#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:138 +#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:175 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:237 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:248 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:286 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:156 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:173 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:233 +#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:183 +#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:226 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:279 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:336 +#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:235 +#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:273 +#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:133 +#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:148 +#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:192 +#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:123 +#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:138 +#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:193 +#: modules/FlaskModule/API/user/UserQueryDevices.py:331 +#: modules/FlaskModule/API/user/UserQueryDevices.py:346 +#: modules/FlaskModule/API/user/UserQueryDevices.py:465 +#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:123 +#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:138 +#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:183 +#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:247 +#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:290 +#: modules/FlaskModule/API/user/UserQueryProjects.py:200 +#: modules/FlaskModule/API/user/UserQueryProjects.py:215 +#: modules/FlaskModule/API/user/UserQueryProjects.py:288 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:160 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:172 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:194 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:242 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:211 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:269 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:291 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:328 +#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:102 +#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:123 +#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:153 +#: modules/FlaskModule/API/user/UserQueryServiceSites.py:248 +#: modules/FlaskModule/API/user/UserQueryServiceSites.py:292 +#: modules/FlaskModule/API/user/UserQueryServices.py:165 +#: modules/FlaskModule/API/user/UserQueryServices.py:185 +#: modules/FlaskModule/API/user/UserQueryServices.py:264 +#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:107 +#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:122 +#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:160 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:255 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:297 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:248 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:292 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:201 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:216 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:349 +#: modules/FlaskModule/API/user/UserQuerySessions.py:183 +#: modules/FlaskModule/API/user/UserQuerySessions.py:198 +#: modules/FlaskModule/API/user/UserQuerySessions.py:278 +#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:249 +#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:291 +#: modules/FlaskModule/API/user/UserQuerySites.py:145 +#: modules/FlaskModule/API/user/UserQuerySites.py:160 +#: modules/FlaskModule/API/user/UserQuerySites.py:201 +#: modules/FlaskModule/API/user/UserQueryTestType.py:226 +#: modules/FlaskModule/API/user/UserQueryTestType.py:241 +#: modules/FlaskModule/API/user/UserQueryTestType.py:351 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:256 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:299 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:237 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:280 +#: modules/FlaskModule/API/user/UserQueryTests.py:158 +#: modules/FlaskModule/API/user/UserQueryUserGroups.py:193 +#: modules/FlaskModule/API/user/UserQueryUserGroups.py:208 +#: modules/FlaskModule/API/user/UserQueryUserGroups.py:236 +#: modules/FlaskModule/API/user/UserQueryUserGroups.py:265 +#: modules/FlaskModule/API/user/UserQueryUserGroups.py:315 +#: modules/FlaskModule/API/user/UserQueryUserPreferences.py:114 +#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:142 +#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:180 +#: modules/FlaskModule/API/user/UserQueryUsers.py:255 +#: modules/FlaskModule/API/user/UserQueryUsers.py:293 +#: modules/FlaskModule/API/user/UserQueryUsers.py:393 msgid "Database error" msgstr "" #: modules/FlaskModule/API/device/DeviceQueryParticipants.py:41 -#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:82 -#: modules/FlaskModule/API/service/ServiceQueryProjects.py:72 -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:67 -#: modules/FlaskModule/API/service/ServiceQueryServices.py:76 -#: modules/FlaskModule/API/service/ServiceQuerySessionEvents.py:62 -#: modules/FlaskModule/API/service/ServiceQuerySessions.py:122 -#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:109 -#: modules/FlaskModule/API/user/UserQueryDisconnect.py:86 -#: modules/FlaskModule/API/user/UserQueryProjects.py:116 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:84 -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:123 -#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:67 -#: modules/FlaskModule/API/user/UserQueryServiceSites.py:112 -#: modules/FlaskModule/API/user/UserQueryServices.py:104 -#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:62 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:101 -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:79 -#: modules/FlaskModule/API/user/UserQuerySessions.py:103 -#: modules/FlaskModule/API/user/UserQuerySites.py:97 -#: modules/FlaskModule/API/user/UserQueryTestType.py:112 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:99 -#: modules/FlaskModule/API/user/UserQueryUserPreferences.py:67 +#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:84 +#: modules/FlaskModule/API/service/ServiceQueryProjects.py:75 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:70 +#: modules/FlaskModule/API/service/ServiceQueryServices.py:79 +#: modules/FlaskModule/API/service/ServiceQuerySessionEvents.py:64 +#: modules/FlaskModule/API/service/ServiceQuerySessions.py:125 +#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:110 +#: modules/FlaskModule/API/user/UserQueryDisconnect.py:87 +#: modules/FlaskModule/API/user/UserQueryProjects.py:118 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:86 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:125 +#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:69 +#: modules/FlaskModule/API/user/UserQueryServiceSites.py:114 +#: modules/FlaskModule/API/user/UserQueryServices.py:105 +#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:63 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:103 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:81 +#: modules/FlaskModule/API/user/UserQuerySessions.py:105 +#: modules/FlaskModule/API/user/UserQuerySites.py:99 +#: modules/FlaskModule/API/user/UserQueryTestType.py:114 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:101 +#: modules/FlaskModule/API/user/UserQueryUserPreferences.py:68 msgid "Invalid request" msgstr "" -#: modules/FlaskModule/API/device/DeviceQuerySessionEvents.py:31 -#: modules/FlaskModule/API/device/DeviceQuerySessionEvents.py:97 -#: modules/FlaskModule/API/device/DeviceQuerySessions.py:72 -#: modules/FlaskModule/API/device/DeviceQuerySessions.py:193 +#: modules/FlaskModule/API/device/DeviceQuerySessionEvents.py:33 +#: modules/FlaskModule/API/device/DeviceQuerySessionEvents.py:105 +#: modules/FlaskModule/API/device/DeviceQuerySessions.py:74 +#: modules/FlaskModule/API/device/DeviceQuerySessions.py:201 msgid "Forbidden for security reasons" msgstr "" -#: modules/FlaskModule/API/device/DeviceQuerySessionEvents.py:47 -#: modules/FlaskModule/API/device/DeviceQuerySessionEvents.py:53 -#: modules/FlaskModule/API/device/DeviceQueryStatus.py:48 -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:270 -#: modules/FlaskModule/API/service/ServiceQueryDevices.py:73 -#: modules/FlaskModule/API/service/ServiceQueryDevices.py:87 -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:76 -#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:104 -#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:119 -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:49 -#: modules/FlaskModule/API/service/ServiceQuerySessionEvents.py:46 -#: modules/FlaskModule/API/service/ServiceQuerySiteProjectAccessRoles.py:43 -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:55 -#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:152 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:263 -#: modules/FlaskModule/API/service/ServiceQueryUsers.py:38 -#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:70 -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:65 -#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:61 -#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:157 -#: modules/FlaskModule/API/user/UserQueryForms.py:85 -#: modules/FlaskModule/API/user/UserQueryParticipants.py:76 -#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:81 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:53 -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:63 -#: modules/FlaskModule/API/user/UserQueryServiceSites.py:60 -#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:46 -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:62 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:59 -#: modules/FlaskModule/API/user/UserQuerySessions.py:59 -#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:80 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:62 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:60 +#: modules/FlaskModule/API/device/DeviceQuerySessionEvents.py:52 +#: modules/FlaskModule/API/device/DeviceQuerySessionEvents.py:58 +#: modules/FlaskModule/API/device/DeviceQueryStatus.py:50 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:279 +#: modules/FlaskModule/API/service/ServiceQueryDevices.py:71 +#: modules/FlaskModule/API/service/ServiceQueryDevices.py:88 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:79 +#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:107 +#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:125 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:52 +#: modules/FlaskModule/API/service/ServiceQuerySessionEvents.py:48 +#: modules/FlaskModule/API/service/ServiceQuerySiteProjectAccessRoles.py:46 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:58 +#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:161 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:272 +#: modules/FlaskModule/API/service/ServiceQueryUsers.py:41 +#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:71 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:66 +#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:62 +#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:161 +#: modules/FlaskModule/API/user/UserQueryForms.py:98 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:78 +#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:82 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:55 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:65 +#: modules/FlaskModule/API/user/UserQueryServiceSites.py:62 +#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:47 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:64 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:61 +#: modules/FlaskModule/API/user/UserQuerySessions.py:61 +#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:81 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:64 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:62 msgid "Missing arguments" msgstr "" -#: modules/FlaskModule/API/device/DeviceQuerySessionEvents.py:61 -#: modules/FlaskModule/API/device/DeviceQuerySessions.py:137 -#: modules/LoginModule/LoginModule.py:584 -#: modules/LoginModule/LoginModule.py:684 -#: modules/LoginModule/LoginModule.py:750 -#: modules/LoginModule/LoginModule.py:777 -#: modules/LoginModule/LoginModule.py:796 +#: modules/FlaskModule/API/device/DeviceQuerySessionEvents.py:66 +#: modules/FlaskModule/API/device/DeviceQuerySessions.py:142 +#: modules/FlaskModule/API/participant/ParticipantQuerySessions.py:169 +#: modules/LoginModule/LoginModule.py:598 +#: modules/LoginModule/LoginModule.py:698 +#: modules/LoginModule/LoginModule.py:764 +#: modules/LoginModule/LoginModule.py:791 +#: modules/LoginModule/LoginModule.py:810 +#: modules/LoginModule/LoginModule.py:829 +#: modules/LoginModule/LoginModule.py:831 msgid "Unauthorized" msgstr "" -#: modules/FlaskModule/API/device/DeviceQuerySessions.py:87 -#: modules/FlaskModule/API/service/ServiceQuerySessions.py:136 -#: modules/FlaskModule/API/user/UserQuerySessions.py:120 +#: modules/FlaskModule/API/device/DeviceQuerySessions.py:92 +#: modules/FlaskModule/API/participant/ParticipantQuerySessions.py:120 +#: modules/FlaskModule/API/service/ServiceQuerySessions.py:142 +#: modules/FlaskModule/API/user/UserQuerySessions.py:124 msgid "Missing session" msgstr "" -#: modules/FlaskModule/API/device/DeviceQuerySessions.py:95 +#: modules/FlaskModule/API/device/DeviceQuerySessions.py:100 +#: modules/FlaskModule/API/participant/ParticipantQuerySessions.py:128 msgid "Missing id_session value" msgstr "" -#: modules/FlaskModule/API/device/DeviceQuerySessions.py:99 +#: modules/FlaskModule/API/device/DeviceQuerySessions.py:104 +#: modules/FlaskModule/API/participant/ParticipantQuerySessions.py:132 msgid "Missing id_session_type value" msgstr "" -#: modules/FlaskModule/API/device/DeviceQuerySessions.py:104 +#: modules/FlaskModule/API/device/DeviceQuerySessions.py:109 +#: modules/FlaskModule/API/participant/ParticipantQuerySessions.py:137 msgid "Missing session participants and/or users and/or devices" msgstr "" -#: modules/FlaskModule/API/device/DeviceQuerySessions.py:114 +#: modules/FlaskModule/API/device/DeviceQuerySessions.py:119 +#: modules/FlaskModule/API/participant/ParticipantQuerySessions.py:146 msgid "No access to session type" msgstr "" -#: modules/FlaskModule/API/device/DeviceQuerySessions.py:119 +#: modules/FlaskModule/API/device/DeviceQuerySessions.py:124 +#: modules/FlaskModule/API/participant/ParticipantQuerySessions.py:151 msgid "Missing argument 'session name'" msgstr "" -#: modules/FlaskModule/API/device/DeviceQuerySessions.py:121 +#: modules/FlaskModule/API/device/DeviceQuerySessions.py:126 +#: modules/FlaskModule/API/participant/ParticipantQuerySessions.py:153 msgid "Missing argument 'session_start_datetime'" msgstr "" -#: modules/FlaskModule/API/device/DeviceQuerySessions.py:170 -#: modules/FlaskModule/API/service/ServiceQueryAccess.py:130 +#: modules/FlaskModule/API/device/DeviceQuerySessions.py:175 +#: modules/FlaskModule/API/participant/ParticipantQuerySessions.py:201 +#: modules/FlaskModule/API/service/ServiceQueryAccess.py:132 msgid "Invalid participant uuid" msgstr "" -#: modules/FlaskModule/API/device/DeviceQueryStatus.py:67 +#: modules/FlaskModule/API/device/DeviceQueryStatus.py:69 msgid "Status update forbidden on offline device." msgstr "" -#: modules/FlaskModule/API/device/DeviceRegister.py:73 -#: modules/FlaskModule/API/device/DeviceRegister.py:99 +#: modules/FlaskModule/API/device/DeviceRegister.py:76 +#: modules/FlaskModule/API/device/DeviceRegister.py:105 msgid "Invalid registration key" msgstr "" -#: modules/FlaskModule/API/device/DeviceRegister.py:103 +#: modules/FlaskModule/API/device/DeviceRegister.py:109 msgid "Invalid content type" msgstr "" -#: modules/FlaskModule/API/device/DeviceRegister.py:137 +#: modules/FlaskModule/API/device/DeviceRegister.py:143 msgid "Invalid CSR signature" msgstr "" -#: modules/FlaskModule/API/participant/ParticipantLogin.py:92 +#: modules/FlaskModule/API/participant/ParticipantLogin.py:96 msgid "Participant already logged in." msgstr "" -#: modules/FlaskModule/API/participant/ParticipantLogin.py:126 +#: modules/FlaskModule/API/participant/ParticipantLogin.py:138 msgid "Missing current_participant" msgstr "" -#: modules/FlaskModule/API/participant/ParticipantLogout.py:41 +#: modules/FlaskModule/API/participant/ParticipantLogout.py:44 msgid "Participant logged out." msgstr "" -#: modules/FlaskModule/API/participant/ParticipantLogout.py:43 +#: modules/FlaskModule/API/participant/ParticipantLogout.py:46 msgid "Participant not logged in" msgstr "" -#: modules/FlaskModule/API/participant/ParticipantQueryParticipants.py:55 -#: modules/FlaskModule/API/participant/ParticipantQuerySessions.py:73 +#: modules/FlaskModule/API/participant/ParticipantQueryParticipants.py:58 msgid "Not implemented" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryAccess.py:57 -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:61 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:57 -#: modules/FlaskModule/API/user/UserQueryAssets.py:60 -#: modules/FlaskModule/API/user/UserQueryTests.py:54 +#: modules/FlaskModule/API/service/ServiceQueryAccess.py:59 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:64 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:60 +#: modules/FlaskModule/API/user/UserQueryAssets.py:61 +#: modules/FlaskModule/API/user/UserQueryTests.py:56 msgid "No arguments specified" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryAccess.py:67 +#: modules/FlaskModule/API/service/ServiceQueryAccess.py:69 msgid "Missing at least one from argument for uuids" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryAccess.py:77 +#: modules/FlaskModule/API/service/ServiceQueryAccess.py:79 msgid "Invalid user uuid" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryAccess.py:126 +#: modules/FlaskModule/API/service/ServiceQueryAccess.py:128 msgid "Participant cannot be admin" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryAccess.py:163 +#: modules/FlaskModule/API/service/ServiceQueryAccess.py:165 msgid "Device cannot be admin" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryAccess.py:167 +#: modules/FlaskModule/API/service/ServiceQueryAccess.py:169 msgid "Invalid device uuid" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:64 -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:90 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:60 -#: modules/FlaskModule/API/user/UserQueryAssets.py:63 -#: modules/FlaskModule/API/user/UserQueryAssets.py:91 -#: modules/FlaskModule/API/user/UserQueryTests.py:57 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:67 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:93 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:63 +#: modules/FlaskModule/API/user/UserQueryAssets.py:64 +#: modules/FlaskModule/API/user/UserQueryAssets.py:92 +#: modules/FlaskModule/API/user/UserQueryTests.py:59 msgid "Device access denied" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:68 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:64 -#: modules/FlaskModule/API/user/UserQueryAssets.py:67 -#: modules/FlaskModule/API/user/UserQueryTests.py:61 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:71 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:67 +#: modules/FlaskModule/API/user/UserQueryAssets.py:68 +#: modules/FlaskModule/API/user/UserQueryTests.py:63 msgid "Session access denied" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:72 -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:86 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:68 -#: modules/FlaskModule/API/user/UserQueryAssets.py:71 -#: modules/FlaskModule/API/user/UserQueryAssets.py:87 -#: modules/FlaskModule/API/user/UserQueryTests.py:65 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:75 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:89 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:71 +#: modules/FlaskModule/API/user/UserQueryAssets.py:72 +#: modules/FlaskModule/API/user/UserQueryAssets.py:88 +#: modules/FlaskModule/API/user/UserQueryTests.py:67 msgid "Participant access denied" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:76 -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:82 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:72 -#: modules/FlaskModule/API/user/UserQueryAssets.py:75 -#: modules/FlaskModule/API/user/UserQueryAssets.py:83 -#: modules/FlaskModule/API/user/UserQueryTests.py:69 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:79 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:85 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:75 +#: modules/FlaskModule/API/user/UserQueryAssets.py:76 +#: modules/FlaskModule/API/user/UserQueryAssets.py:84 +#: modules/FlaskModule/API/user/UserQueryTests.py:71 msgid "User access denied" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:97 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:80 -#: modules/FlaskModule/API/user/UserQueryAssets.py:100 -#: modules/FlaskModule/API/user/UserQueryTests.py:76 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:100 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:83 +#: modules/FlaskModule/API/user/UserQueryAssets.py:101 +#: modules/FlaskModule/API/user/UserQueryTests.py:78 msgid "Missing argument" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:163 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:169 msgid "Missing asset field" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:169 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:175 msgid "Missing id_asset field" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:173 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:141 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:179 #: modules/FlaskModule/API/service/ServiceQueryTests.py:147 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:153 msgid "Unknown session" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:176 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:182 msgid "Invalid asset type" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:179 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:185 msgid "Invalid asset name" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:189 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:195 msgid "Service can't create assets for that session" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:196 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:196 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:202 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:202 msgid "Invalid participant" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:204 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:204 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:210 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:210 msgid "Invalid user" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:212 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:212 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:218 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:218 msgid "Invalid device" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:273 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:282 msgid "Service can't delete assets for that session" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryDevices.py:93 +#: modules/FlaskModule/API/service/ServiceQueryDevices.py:94 msgid "Unknown device type" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryDevices.py:97 +#: modules/FlaskModule/API/service/ServiceQueryDevices.py:98 msgid "Unknown device subtype" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:84 -#: modules/FlaskModule/API/user/UserQueryDisconnect.py:88 +#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:86 +#: modules/FlaskModule/API/user/UserQueryDisconnect.py:89 msgid "Success" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:57 -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:191 -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:252 -#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:203 -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:296 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:209 -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:63 -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:308 -#: modules/FlaskModule/API/user/UserQueryServiceSites.py:266 -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:270 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:271 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:60 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:200 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:261 +#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:208 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:301 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:215 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:64 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:314 +#: modules/FlaskModule/API/user/UserQueryServiceSites.py:272 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:276 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:277 msgid "Not found" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:95 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:101 msgid "Missing participant_group" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:102 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:108 msgid "Missing id_participant_group" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:107 -#: modules/FlaskModule/API/service/ServiceQueryProjects.py:93 -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:180 -#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:198 -#: modules/FlaskModule/API/user/UserQueryProjects.py:138 -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:179 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:113 +#: modules/FlaskModule/API/service/ServiceQueryProjects.py:99 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:183 +#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:201 +#: modules/FlaskModule/API/user/UserQueryProjects.py:142 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:183 msgid "Missing id_project" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:114 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:120 msgid "Missing group name" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:184 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:193 msgid "The id_participant_group given was not found" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:190 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:199 msgid "Deletion impossible: Participant group still has participant(s)" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:125 +#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:131 msgid "Unknown project" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:128 +#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:134 msgid "Invalid participant name" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:131 +#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:137 msgid "Invalid participant email" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:155 -#: modules/FlaskModule/API/user/UserQueryParticipants.py:311 +#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:161 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:315 msgid "Can't insert participant: participant's project is disabled or invalid." msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:165 -#: modules/FlaskModule/API/user/UserQueryParticipants.py:284 +#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:171 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:288 msgid "Can't update participant: participant's project is disabled." msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryProjects.py:48 +#: modules/FlaskModule/API/service/ServiceQueryProjects.py:51 msgid "Missing parameter" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryProjects.py:87 -#: modules/FlaskModule/API/user/UserQueryProjects.py:132 +#: modules/FlaskModule/API/service/ServiceQueryProjects.py:93 +#: modules/FlaskModule/API/user/UserQueryProjects.py:136 msgid "Missing project" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryProjects.py:96 -#: modules/FlaskModule/API/user/UserQueryProjects.py:140 +#: modules/FlaskModule/API/service/ServiceQueryProjects.py:102 +#: modules/FlaskModule/API/user/UserQueryProjects.py:144 msgid "Missing id_site arguments" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryProjects.py:169 -#: modules/FlaskModule/API/user/UserQueryProjects.py:275 +#: modules/FlaskModule/API/service/ServiceQueryProjects.py:178 +#: modules/FlaskModule/API/user/UserQueryProjects.py:281 msgid "" "Can't delete project: please delete all participants with sessions before" " deleting." msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryRoles.py:52 +#: modules/FlaskModule/API/service/ServiceQueryRoles.py:58 msgid "Missing service_role field" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryRoles.py:58 -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:154 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:172 -#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:85 +#: modules/FlaskModule/API/service/ServiceQueryRoles.py:64 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:160 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:176 +#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:89 msgid "Missing id_service_role" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryRoles.py:81 -#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:119 -#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:106 +#: modules/FlaskModule/API/service/ServiceQueryRoles.py:87 +#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:125 +#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:110 msgid "Missing fields" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:87 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:104 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:93 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:108 msgid "Missing service_access" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:91 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:108 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:97 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:112 msgid "Missing id_service_access" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:95 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:112 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:101 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:116 msgid "Can't combine id_user_group, id_participant_group and id_device in request" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:114 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:132 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:120 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:136 msgid "Bad id_service_role" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:158 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:176 -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:192 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:164 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:180 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:195 msgid "Missing at least one id field" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryServices.py:38 +#: modules/FlaskModule/API/service/ServiceQueryServices.py:41 msgid "Missing service key, id or uuid" msgstr "" -#: modules/FlaskModule/API/service/ServiceQuerySessionEvents.py:76 -#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:77 +#: modules/FlaskModule/API/service/ServiceQuerySessionEvents.py:81 +#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:80 msgid "Missing session_event field" msgstr "" -#: modules/FlaskModule/API/service/ServiceQuerySessionEvents.py:82 -#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:83 +#: modules/FlaskModule/API/service/ServiceQuerySessionEvents.py:87 +#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:86 msgid "Missing id_session or id_session_event fields" msgstr "" -#: modules/FlaskModule/API/service/ServiceQuerySessions.py:95 +#: modules/FlaskModule/API/service/ServiceQuerySessions.py:98 msgid "Missing arguments: at least one id is required" msgstr "" -#: modules/FlaskModule/API/service/ServiceQuerySessions.py:142 -#: modules/FlaskModule/API/user/UserQuerySessions.py:126 +#: modules/FlaskModule/API/service/ServiceQuerySessions.py:148 +#: modules/FlaskModule/API/user/UserQuerySessions.py:130 msgid "Missing id_session" msgstr "" -#: modules/FlaskModule/API/service/ServiceQuerySessions.py:157 +#: modules/FlaskModule/API/service/ServiceQuerySessions.py:163 msgid "Service doesn't have access to at least one participant of that session." msgstr "" -#: modules/FlaskModule/API/service/ServiceQuerySessions.py:164 +#: modules/FlaskModule/API/service/ServiceQuerySessions.py:170 msgid "Service doesn't have access to at least one user of that session." msgstr "" -#: modules/FlaskModule/API/service/ServiceQuerySessions.py:171 +#: modules/FlaskModule/API/service/ServiceQuerySessions.py:177 msgid "Service doesn't have access to at least one device of that session." msgstr "" -#: modules/FlaskModule/API/service/ServiceQuerySessions.py:189 -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:129 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:121 -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:101 +#: modules/FlaskModule/API/service/ServiceQuerySessions.py:195 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:133 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:125 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:105 msgid "Missing id_session_type" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:115 -#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:93 -#: modules/FlaskModule/API/user/UserQueryTestType.py:134 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:127 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:119 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:121 +#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:99 +#: modules/FlaskModule/API/user/UserQueryTestType.py:138 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:131 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:123 msgid "Missing id_test_type" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:117 -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:143 -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:144 -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:131 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:129 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:123 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:146 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:148 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:135 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:133 msgid "Missing projects" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:129 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:141 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:135 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:145 msgid "Access denied to at least one project" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:145 -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:176 -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:266 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:157 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:188 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:285 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:151 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:182 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:275 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:161 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:192 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:291 msgid "" "Can't delete test type from project: please delete all tests of that type" " in the project before deleting." msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:152 -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:161 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:164 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:158 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:165 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:168 msgid "Missing project ID" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:154 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:166 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:152 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:160 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:170 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:156 msgid "Missing test types" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:185 -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:196 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:197 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:191 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:200 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:201 msgid "Unknown format" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:190 -#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:195 -#: modules/FlaskModule/API/user/UserQueryServiceSites.py:207 -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:201 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:200 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:202 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:190 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:196 +#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:198 +#: modules/FlaskModule/API/user/UserQueryServiceSites.py:211 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:205 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:204 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:206 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:194 msgid "Badly formatted request" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:87 -#: modules/FlaskModule/API/user/UserQueryTestType.py:128 +#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:93 +#: modules/FlaskModule/API/user/UserQueryTestType.py:132 msgid "Missing test_type" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:155 +#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:164 msgid "Test type not related to this service. Can't delete." msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryTests.py:131 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:137 msgid "Missing test field" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryTests.py:137 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:143 msgid "Missing id_test field" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryTests.py:154 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:160 msgid "Missing id_test_type field" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryTests.py:159 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:165 msgid "Invalid test type" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryTests.py:189 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:195 msgid "Service can't create tests for that session" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryTests.py:266 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:275 msgid "Service can't delete tests for that session" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:81 -#: modules/FlaskModule/API/user/UserQueryUserGroups.py:132 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:86 +#: modules/FlaskModule/API/user/UserQueryUserGroups.py:135 msgid "Missing user_group" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:88 -#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:196 -#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:179 -#: modules/FlaskModule/API/user/UserQueryUserGroups.py:139 -#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:102 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:93 +#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:199 +#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:182 +#: modules/FlaskModule/API/user/UserQueryUserGroups.py:142 +#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:106 msgid "Missing id_user_group" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:100 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:105 msgid "Missing service role name or id_service_role" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:113 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:118 msgid "Can't set access to service other than self" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:119 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:124 msgid "No access for at a least one project in the list" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:125 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:130 msgid "No access for at a least one site in the list" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:137 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:142 msgid "Bad role name for service" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:155 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:160 msgid "A new usergroup must have at least one service access" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:217 -#: modules/FlaskModule/API/user/UserQueryUserGroups.py:302 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:225 +#: modules/FlaskModule/API/user/UserQueryUserGroups.py:307 msgid "" "Can't delete user group: please delete all users part of that user group " "before deleting." msgstr "" -#: modules/FlaskModule/API/service/ServiceSessionManager.py:116 -#: modules/FlaskModule/API/user/UserSessionManager.py:107 +#: modules/FlaskModule/API/service/ServiceSessionManager.py:118 +#: modules/FlaskModule/API/user/UserSessionManager.py:108 msgid "Missing action" msgstr "" -#: modules/FlaskModule/API/service/ServiceSessionManager.py:131 -#: modules/FlaskModule/API/user/UserSessionManager.py:121 -#: modules/FlaskModule/API/user/UserSessionManager.py:134 +#: modules/FlaskModule/API/service/ServiceSessionManager.py:133 +#: modules/FlaskModule/API/user/UserSessionManager.py:122 +#: modules/FlaskModule/API/user/UserSessionManager.py:135 msgid "Invalid session" msgstr "" -#: modules/FlaskModule/API/service/ServiceSessionManager.py:139 +#: modules/FlaskModule/API/service/ServiceSessionManager.py:141 msgid "Service doesn't have access to that session" msgstr "" -#: modules/FlaskModule/API/service/ServiceSessionManager.py:160 -#: modules/FlaskModule/API/user/UserSessionManager.py:155 +#: modules/FlaskModule/API/service/ServiceSessionManager.py:162 +#: modules/FlaskModule/API/user/UserSessionManager.py:156 msgid "Missing required id_session_type for new sessions" msgstr "" -#: modules/FlaskModule/API/service/ServiceSessionManager.py:166 +#: modules/FlaskModule/API/service/ServiceSessionManager.py:168 msgid "Invalid session type" msgstr "" -#: modules/FlaskModule/API/service/ServiceSessionManager.py:201 -#: modules/FlaskModule/API/user/UserSessionManager.py:192 +#: modules/FlaskModule/API/service/ServiceSessionManager.py:203 +#: modules/FlaskModule/API/user/UserSessionManager.py:193 msgid "Service not found" msgstr "" -#: modules/FlaskModule/API/service/ServiceSessionManager.py:209 -#: modules/FlaskModule/API/user/UserSessionManager.py:197 +#: modules/FlaskModule/API/service/ServiceSessionManager.py:211 +#: modules/FlaskModule/API/user/UserSessionManager.py:198 msgid "Not implemented yet" msgstr "" -#: modules/FlaskModule/API/service/ServiceSessionManager.py:224 -#: modules/FlaskModule/API/user/UserSessionManager.py:213 +#: modules/FlaskModule/API/service/ServiceSessionManager.py:226 +#: modules/FlaskModule/API/user/UserSessionManager.py:214 msgid "No answer from service." msgstr "" -#: modules/FlaskModule/API/user/UserLogin.py:82 +#: modules/FlaskModule/API/user/UserLogin.py:41 +msgid "Password change required for this user." +msgstr "" + +#: modules/FlaskModule/API/user/UserLogin.py:52 +msgid "2FA required for this user." +msgstr "" + +#: modules/FlaskModule/API/user/UserLogin.py:56 +msgid "2FA enabled but OTP not set for this user.Please setup 2FA." +msgstr "" + +#: modules/FlaskModule/API/user/UserLogin.py:79 +#: modules/FlaskModule/API/user/UserLogin2FA.py:101 +#: modules/FlaskModule/API/user/UserLoginBase.py:154 +#: modules/FlaskModule/API/user/UserLoginSetup2FA.py:85 +#: modules/FlaskModule/API/user/UserLoginSetup2FA.py:155 +msgid "Client major version too old, not accepting login" +msgstr "" + +#: modules/FlaskModule/API/user/UserLogin2FA.py:49 +#: modules/FlaskModule/API/user/UserLoginSetup2FA.py:49 +#: modules/FlaskModule/API/user/UserLoginSetup2FA.py:119 +msgid "User does not have 2FA enabled" +msgstr "" + +#: modules/FlaskModule/API/user/UserLogin2FA.py:54 +msgid "User does not have 2FA OTP enabled or secret set" +msgstr "" + +#: modules/FlaskModule/API/user/UserLogin2FA.py:74 +#: modules/FlaskModule/API/user/UserLoginSetup2FA.py:133 +msgid "Invalid OTP code" +msgstr "" + +#: modules/FlaskModule/API/user/UserLoginBase.py:90 +msgid "User already logged in :" +msgstr "" + +#: modules/FlaskModule/API/user/UserLoginBase.py:92 msgid "User already logged in." msgstr "" -#: modules/FlaskModule/API/user/UserLogin.py:116 -#: modules/FlaskModule/API/user/UserLogin.py:134 +#: modules/FlaskModule/API/user/UserLoginBase.py:101 +msgid "Too many 2FA attempts. Please wait and try again." +msgstr "" + +#: modules/FlaskModule/API/user/UserLoginBase.py:134 +msgid "Client major version mismatch" +msgstr "" + +#: modules/FlaskModule/API/user/UserLoginBase.py:151 msgid "Client version mismatch" msgstr "" -#: modules/FlaskModule/API/user/UserLogin.py:136 -msgid "Client major version too old, not accepting login" +#: modules/FlaskModule/API/user/UserLoginBase.py:171 +msgid "Unknown client name :" msgstr "" -#: modules/FlaskModule/API/user/UserLogin.py:143 -msgid "Invalid client version handler" +#: modules/FlaskModule/API/user/UserLoginChangePassword.py:34 +#: modules/FlaskModule/Views/LoginChangePasswordView.py:48 +msgid "New password and confirm password do not match" msgstr "" -#: modules/FlaskModule/API/user/UserLogout.py:34 -msgid "User logged out." +#: modules/FlaskModule/API/user/UserLoginChangePassword.py:37 +msgid "User not required to change password" +msgstr "" + +#: modules/FlaskModule/API/user/UserLoginChangePassword.py:47 +#: modules/FlaskModule/API/user/UserQueryUsers.py:260 +msgid "New password same as old password" +msgstr "" + +#: modules/FlaskModule/API/user/UserLoginSetup2FA.py:53 +#: modules/FlaskModule/API/user/UserLoginSetup2FA.py:123 +msgid "User already has 2FA OTP secret set" +msgstr "" + +#: modules/FlaskModule/API/user/UserLoginSetup2FA.py:146 +msgid "2FA enabled for this user." msgstr "" #: modules/FlaskModule/API/user/UserLogout.py:36 +msgid "User logged out." +msgstr "" + +#: modules/FlaskModule/API/user/UserLogout.py:38 msgid "User not logged in" msgstr "" -#: modules/FlaskModule/API/user/UserQueryAssets.py:79 +#: modules/FlaskModule/API/user/UserQueryAssets.py:80 msgid "Service access denied" msgstr "" -#: modules/FlaskModule/API/user/UserQueryAssets.py:174 +#: modules/FlaskModule/API/user/UserQueryAssets.py:177 msgid "" "Asset information update and creation must be done directly into a " "service (such as Filetransfer service)" msgstr "" -#: modules/FlaskModule/API/user/UserQueryAssets.py:182 +#: modules/FlaskModule/API/user/UserQueryAssets.py:187 msgid "" "Asset information deletion must be done directly into a service (such as " "Filetransfer service)" msgstr "" -#: modules/FlaskModule/API/user/UserQueryAssetsArchive.py:89 +#: modules/FlaskModule/API/user/UserQueryAssetsArchive.py:88 msgid "Only one of the ID parameter is supported at once" msgstr "" -#: modules/FlaskModule/API/user/UserQueryAssetsArchive.py:152 +#: modules/FlaskModule/API/user/UserQueryAssetsArchive.py:151 msgid "Missing required parameter" msgstr "" -#: modules/FlaskModule/API/user/UserQueryAssetsArchive.py:189 +#: modules/FlaskModule/API/user/UserQueryAssetsArchive.py:188 msgid "Unable to create archive information from FileTransferService" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:128 -#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:207 +#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:131 +#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:212 msgid "User is not admin of the participant's project" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:131 -#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:210 +#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:134 +#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:215 msgid "Access denied to device" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:149 +#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:152 msgid "Device not assigned to project or participant" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:141 -#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:127 -#: modules/FlaskModule/API/user/UserQueryDevices.py:259 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:144 +#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:130 +#: modules/FlaskModule/API/user/UserQueryDevices.py:262 msgid "Missing id_device" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:157 -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:165 -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:188 -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:232 -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:188 -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:243 -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:135 -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:168 -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:273 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:127 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:165 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:133 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:171 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:275 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:125 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:157 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:160 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:168 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:191 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:235 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:192 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:247 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:139 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:172 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:279 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:131 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:169 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:137 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:175 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:281 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:129 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:161 msgid "Access denied" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:172 -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:208 -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:321 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:175 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:211 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:326 msgid "" "Can't delete device from project. Please remove all participants " "associated with the device or all sessions in the project referring to " "the device before deleting." msgstr "" -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:182 -#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:157 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:185 +#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:160 msgid "Missing devices" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:238 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:241 msgid "" "At least one device is not part of the allowed device for that project " "site" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:314 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:319 msgid "" "Can't delete device from project: please remove all participants with " "device before deleting." msgstr "" -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:317 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:322 msgid "" "Can't delete device from project: please remove all sessions in this " "project referring to that device before deleting." msgstr "" -#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:129 -#: modules/FlaskModule/API/user/UserQueryServiceSites.py:136 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:123 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:121 +#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:132 +#: modules/FlaskModule/API/user/UserQueryServiceSites.py:140 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:127 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:125 msgid "Missing sites" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:148 -#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:175 +#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:151 +#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:178 msgid "" "Can't delete device from site. Please remove all participants associated " "with the device or all sessions in the site referring to the device " "before deleting." msgstr "" -#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:155 -#: modules/FlaskModule/API/user/UserQueryServiceSites.py:164 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:158 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:150 +#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:158 +#: modules/FlaskModule/API/user/UserQueryServiceSites.py:168 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:162 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:154 msgid "Missing site ID" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:252 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:263 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:252 +#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:257 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:269 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:258 msgid "Bad parameter" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:50 +#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:51 msgid "Too Many IDs" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:58 +#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:59 msgid "No access to device subtype" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:64 +#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:65 msgid "No access to device type" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:102 +#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:105 msgid "Missing device_subtype" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:109 +#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:112 msgid "Missing id_device_subtype" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:121 +#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:124 msgid "Invalid device subtype" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:169 +#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:174 msgid "Device subtype not found" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:179 +#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:184 msgid "" "Can't delete device subtype: please delete all devices of that subtype " "before deleting." msgstr "" -#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:189 +#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:194 msgid "Device subtype successfully deleted" msgstr "" @@ -1126,655 +1228,680 @@ msgid "Unexisting ID/Forbidden access" msgstr "" #: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:79 -#: modules/FlaskModule/API/user/UserQueryParticipants.py:399 -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:182 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:405 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:185 msgid "Database Error" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:95 +#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:97 msgid "Missing device type" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:102 +#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:104 msgid "Missing id_device_type" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:113 +#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:115 msgid "Invalid device type" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:160 +#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:164 msgid "Tried to delete with 2 parameters" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:168 -#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:171 +#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:172 +#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:175 msgid "Device type not found" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:181 +#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:185 msgid "" "Can't delete device type: please delete all associated devices before " "deleting." msgstr "" -#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:193 +#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:197 msgid "Device type successfully deleted" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDevices.py:130 +#: modules/FlaskModule/API/user/UserQueryDevices.py:131 msgid "Too many IDs" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDevices.py:253 +#: modules/FlaskModule/API/user/UserQueryDevices.py:256 msgid "Missing device" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDevices.py:275 -#: modules/FlaskModule/API/user/UserQueryUserGroups.py:177 +#: modules/FlaskModule/API/user/UserQueryDevices.py:278 +#: modules/FlaskModule/API/user/UserQueryUserGroups.py:180 msgid "No site admin access for at a least one project in the list" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDevices.py:287 -#: modules/FlaskModule/API/user/UserQueryUserGroups.py:158 +#: modules/FlaskModule/API/user/UserQueryDevices.py:290 +#: modules/FlaskModule/API/user/UserQueryUserGroups.py:161 msgid "No site admin access for at a least one site in the list" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDevices.py:412 -#: modules/FlaskModule/API/user/UserQueryUsers.py:343 +#: modules/FlaskModule/API/user/UserQueryDevices.py:417 +#: modules/FlaskModule/API/user/UserQueryUsers.py:351 msgid "Invalid id" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDevices.py:436 +#: modules/FlaskModule/API/user/UserQueryDevices.py:441 msgid "" "Can't delete device: please delete all participants association before " "deleting." msgstr "" -#: modules/FlaskModule/API/user/UserQueryDevices.py:439 +#: modules/FlaskModule/API/user/UserQueryDevices.py:444 msgid "" "Can't delete device: please remove all sessions referring to that device " "before deleting." msgstr "" -#: modules/FlaskModule/API/user/UserQueryDevices.py:442 +#: modules/FlaskModule/API/user/UserQueryDevices.py:447 msgid "" "Can't delete device: please remove all sessions created by that device " "before deleting." msgstr "" -#: modules/FlaskModule/API/user/UserQueryDevices.py:445 +#: modules/FlaskModule/API/user/UserQueryDevices.py:450 msgid "" "Can't delete device: please delete all assets created by that device " "before deleting." msgstr "" -#: modules/FlaskModule/API/user/UserQueryDevices.py:448 +#: modules/FlaskModule/API/user/UserQueryDevices.py:453 msgid "" "Can't delete device: please delete all tests created by that device " "before deleting." msgstr "" -#: modules/FlaskModule/API/user/UserQueryDevices.py:451 +#: modules/FlaskModule/API/user/UserQueryDevices.py:456 msgid "" "Can't delete device: please remove all related sessions, assets and tests" " before deleting." msgstr "" -#: modules/FlaskModule/API/user/UserQueryDevices.py:469 +#: modules/FlaskModule/API/user/UserQueryDevices.py:474 msgid "Device successfully deleted" msgstr "" -#: modules/FlaskModule/API/user/UserQueryDisconnect.py:49 -#: modules/FlaskModule/API/user/UserQueryDisconnect.py:57 +#: modules/FlaskModule/API/user/UserQueryDisconnect.py:50 +#: modules/FlaskModule/API/user/UserQueryDisconnect.py:58 msgid "Use Logout instead to disconnect current user" msgstr "" -#: modules/FlaskModule/API/user/UserQueryForms.py:82 +#: modules/FlaskModule/API/user/UserQueryForms.py:95 msgid "Missing type" msgstr "" -#: modules/FlaskModule/API/user/UserQueryForms.py:137 +#: modules/FlaskModule/API/user/UserQueryForms.py:150 msgid "Missing session type id" msgstr "" -#: modules/FlaskModule/API/user/UserQueryForms.py:148 +#: modules/FlaskModule/API/user/UserQueryForms.py:161 msgid "No reply from service while querying session type config" msgstr "" -#: modules/FlaskModule/API/user/UserQueryForms.py:182 +#: modules/FlaskModule/API/user/UserQueryForms.py:195 msgid "Invalid service specified" msgstr "" -#: modules/FlaskModule/API/user/UserQueryForms.py:195 +#: modules/FlaskModule/API/user/UserQueryForms.py:208 msgid "Unknown form type: " msgstr "" #: modules/FlaskModule/API/user/UserQueryOnlineDevices.py:60 -#: modules/FlaskModule/API/user/UserQueryOnlineParticipants.py:69 -#: modules/FlaskModule/API/user/UserQueryOnlineUsers.py:57 +#: modules/FlaskModule/API/user/UserQueryOnlineParticipants.py:70 +#: modules/FlaskModule/API/user/UserQueryOnlineUsers.py:59 msgid "Internal server error when making RPC call." msgstr "" -#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:93 +#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:96 msgid "Missing group" msgstr "" -#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:102 +#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:105 msgid "Missing id_participant_group or id_project" msgstr "" -#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:170 +#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:175 msgid "" "Can't delete participant group: please delete all sessions from all " "participants before deleting." msgstr "" -#: modules/FlaskModule/API/user/UserQueryParticipants.py:219 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:223 msgid "Missing participant" msgstr "" -#: modules/FlaskModule/API/user/UserQueryParticipants.py:225 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:229 msgid "Missing id_participant" msgstr "" -#: modules/FlaskModule/API/user/UserQueryParticipants.py:229 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:233 msgid "Missing id_project or id_participant_group" msgstr "" -#: modules/FlaskModule/API/user/UserQueryParticipants.py:244 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:248 msgid "No admin access to project" msgstr "" -#: modules/FlaskModule/API/user/UserQueryParticipants.py:252 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:256 msgid "No admin access to group" msgstr "" -#: modules/FlaskModule/API/user/UserQueryParticipants.py:261 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:265 msgid "Participant group not found." msgstr "" -#: modules/FlaskModule/API/user/UserQueryParticipants.py:265 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:269 msgid "Mismatch between id_project and group's project" msgstr "" -#: modules/FlaskModule/API/user/UserQueryParticipants.py:382 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:388 msgid "Can't delete participant: please remove all related sessions beforehand." msgstr "" -#: modules/FlaskModule/API/user/UserQueryParticipants.py:384 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:390 msgid "" "Can't delete participant: please remove all sessions created by this " "participant beforehand." msgstr "" -#: modules/FlaskModule/API/user/UserQueryParticipants.py:387 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:393 msgid "Can't delete participant: please remove all related assets beforehand." msgstr "" -#: modules/FlaskModule/API/user/UserQueryParticipants.py:389 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:395 msgid "Can't delete participant: please remove all related tests beforehand." msgstr "" -#: modules/FlaskModule/API/user/UserQueryParticipants.py:391 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:397 msgid "" "Can't delete participant: please remove all related sessions, assets and " "tests before deleting." msgstr "" -#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:200 -#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:183 +#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:203 +#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:186 msgid "Missing role name or id" msgstr "" -#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:231 +#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:234 msgid "Invalid role name or id for that project" msgstr "" -#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:270 +#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:275 msgid "No project access to delete." msgstr "" -#: modules/FlaskModule/API/user/UserQueryProjects.py:169 +#: modules/FlaskModule/API/user/UserQueryProjects.py:173 msgid "No access to a session type for at least one of it" msgstr "" -#: modules/FlaskModule/API/user/UserQueryProjects.py:181 +#: modules/FlaskModule/API/user/UserQueryProjects.py:185 msgid "At least one session type is not associated to the project site" msgstr "" -#: modules/FlaskModule/API/user/UserQueryServiceAccessToken.py:35 -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:189 -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:142 -#: modules/FlaskModule/API/user/UserQueryServiceSites.py:134 -#: modules/FlaskModule/API/user/UserQueryServices.py:128 +#: modules/FlaskModule/API/user/UserQueryServiceAccessToken.py:37 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:192 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:146 +#: modules/FlaskModule/API/user/UserQueryServiceSites.py:138 +#: modules/FlaskModule/API/user/UserQueryServices.py:131 msgid "Missing id_service" msgstr "" -#: modules/FlaskModule/API/user/UserQueryServiceAccessToken.py:38 +#: modules/FlaskModule/API/user/UserQueryServiceAccessToken.py:40 msgid "No access to specified service" msgstr "" -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:72 -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:146 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:73 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:149 msgid "Can't combine id_user, id_participant and id_device in request" msgstr "" -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:121 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:124 msgid "Missing service_config" msgstr "" -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:175 -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:198 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:178 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:201 msgid "Invalid config format provided" msgstr "" -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:170 -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:211 -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:339 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:174 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:215 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:345 msgid "" "Can't delete service-project: please remove all related sessions, assets " "and tests before deleting." msgstr "" -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:181 -#: modules/FlaskModule/API/user/UserQueryServiceSites.py:166 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:185 +#: modules/FlaskModule/API/user/UserQueryServiceSites.py:170 msgid "Missing services" msgstr "" -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:249 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:253 msgid "" "At least one service is not part of the allowed service for that project " "site" msgstr "" -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:311 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:317 msgid "Operation not completed" msgstr "" -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:332 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:338 msgid "" "Can't delete service-project: please remove all sessions involving a " "session type using this project beforehand." msgstr "" -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:335 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:341 msgid "Can't delete service-project: please remove all related assets beforehand." msgstr "" -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:337 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:343 msgid "Can't delete service-project: please remove all related tests beforehand." msgstr "" -#: modules/FlaskModule/API/user/UserQueryServiceSites.py:155 -#: modules/FlaskModule/API/user/UserQueryServiceSites.py:185 -#: modules/FlaskModule/API/user/UserQueryServiceSites.py:278 +#: modules/FlaskModule/API/user/UserQueryServiceSites.py:159 +#: modules/FlaskModule/API/user/UserQueryServiceSites.py:189 +#: modules/FlaskModule/API/user/UserQueryServiceSites.py:284 msgid "" "Can't delete service from site: please delete all sessions, assets and " "tests related to that service beforehand." msgstr "" -#: modules/FlaskModule/API/user/UserQueryServices.py:150 +#: modules/FlaskModule/API/user/UserQueryServices.py:153 msgid "OpenTera service can't be updated using this API" msgstr "" -#: modules/FlaskModule/API/user/UserQueryServices.py:164 -#: modules/FlaskModule/API/user/UserQueryServices.py:184 +#: modules/FlaskModule/API/user/UserQueryServices.py:167 +#: modules/FlaskModule/API/user/UserQueryServices.py:187 msgid "Invalid config json schema" msgstr "" -#: modules/FlaskModule/API/user/UserQueryServices.py:168 +#: modules/FlaskModule/API/user/UserQueryServices.py:171 msgid "Missing service_key" msgstr "" -#: modules/FlaskModule/API/user/UserQueryServices.py:236 +#: modules/FlaskModule/API/user/UserQueryServices.py:241 msgid "Invalid service" msgstr "" -#: modules/FlaskModule/API/user/UserQueryServices.py:251 +#: modules/FlaskModule/API/user/UserQueryServices.py:256 msgid "" "Can't delete service: please delete all sessions, assets and tests " "related to that service beforehand." msgstr "" -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:154 -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:186 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:158 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:190 msgid "" "Can't delete session type from project: please delete all sessions using " "that type in that project before deleting." msgstr "" -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:163 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:160 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:167 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:164 msgid "Missing session types" msgstr "" -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:212 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:216 msgid "At least one session type is not associated to the site of its project" msgstr "" -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:283 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:289 msgid "" "Can't delete session type from project: please delete all sessions of " "that type in the project before deleting." msgstr "" -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:150 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:185 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:278 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:154 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:189 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:284 msgid "" "Can't delete session type from site: please delete all sessions of that " "type in the site before deleting." msgstr "" -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:95 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:99 msgid "Missing session_type" msgstr "" -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:111 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:115 msgid "Missing site(s) to associate that session type to" msgstr "" -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:136 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:140 msgid "Missing id_service for session type of type service" msgstr "" -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:151 -#: modules/FlaskModule/API/user/UserQueryTestType.py:183 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:155 +#: modules/FlaskModule/API/user/UserQueryTestType.py:187 msgid "No site admin access for at least one site in the list" msgstr "" -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:161 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:165 msgid "At least one site isn't associated with the service of that session type" msgstr "" -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:178 -#: modules/FlaskModule/API/user/UserQueryTestType.py:206 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:182 +#: modules/FlaskModule/API/user/UserQueryTestType.py:210 msgid "No project admin access for at a least one project in the list" msgstr "" -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:264 -#: modules/FlaskModule/API/user/UserQueryTestType.py:286 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:268 +#: modules/FlaskModule/API/user/UserQueryTestType.py:290 msgid "Session type not associated to project site" msgstr "" -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:292 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:296 msgid "Session type has a a service not associated to its site" msgstr "" -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:316 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:323 msgid "Cannot delete because you are not admin in all projects." msgstr "" -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:321 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:328 msgid "Unable to delete - not admin in at least one project" msgstr "" -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:334 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:341 msgid "" "Can't delete session type: please delete all sessions with that type " "before deleting." msgstr "" -#: modules/FlaskModule/API/user/UserQuerySessions.py:135 +#: modules/FlaskModule/API/user/UserQuerySessions.py:139 msgid "Missing session participants and users" msgstr "" -#: modules/FlaskModule/API/user/UserQuerySessions.py:141 +#: modules/FlaskModule/API/user/UserQuerySessions.py:145 msgid "No access to session." msgstr "" -#: modules/FlaskModule/API/user/UserQuerySessions.py:156 -#: modules/FlaskModule/API/user/UserQuerySessions.py:247 +#: modules/FlaskModule/API/user/UserQuerySessions.py:160 +#: modules/FlaskModule/API/user/UserQuerySessions.py:253 msgid "User doesn't have access to at least one participant of that session." msgstr "" -#: modules/FlaskModule/API/user/UserQuerySessions.py:161 -#: modules/FlaskModule/API/user/UserQuerySessions.py:252 +#: modules/FlaskModule/API/user/UserQuerySessions.py:165 +#: modules/FlaskModule/API/user/UserQuerySessions.py:258 msgid "User doesn't have access to at least one user of that session." msgstr "" -#: modules/FlaskModule/API/user/UserQuerySessions.py:166 -#: modules/FlaskModule/API/user/UserQuerySessions.py:257 +#: modules/FlaskModule/API/user/UserQuerySessions.py:170 +#: modules/FlaskModule/API/user/UserQuerySessions.py:263 msgid "User doesn't have access to at least one device of that session." msgstr "" -#: modules/FlaskModule/API/user/UserQuerySessions.py:261 +#: modules/FlaskModule/API/user/UserQuerySessions.py:267 msgid "Session is in progress: can't delete that session." msgstr "" -#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:181 +#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:184 msgid "Missing id_site" msgstr "" -#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:230 +#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:233 msgid "Invalid role name or id for that site" msgstr "" -#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:271 +#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:276 msgid "No site access to delete" msgstr "" -#: modules/FlaskModule/API/user/UserQuerySites.py:112 +#: modules/FlaskModule/API/user/UserQuerySites.py:116 msgid "Missing site" msgstr "" -#: modules/FlaskModule/API/user/UserQuerySites.py:119 +#: modules/FlaskModule/API/user/UserQuerySites.py:123 msgid "Missing id_site field" msgstr "" -#: modules/FlaskModule/API/user/UserQuerySites.py:187 +#: modules/FlaskModule/API/user/UserQuerySites.py:194 msgid "" "Can't delete site: please delete all participants with sessions before " "deleting." msgstr "" -#: modules/FlaskModule/API/user/UserQueryStats.py:94 +#: modules/FlaskModule/API/user/UserQueryStats.py:96 msgid "Missing id argument" msgstr "" -#: modules/FlaskModule/API/user/UserQueryTestType.py:151 +#: modules/FlaskModule/API/user/UserQueryTestType.py:155 msgid "Missing project(s) to associate that test type to" msgstr "" -#: modules/FlaskModule/API/user/UserQueryTestType.py:192 +#: modules/FlaskModule/API/user/UserQueryTestType.py:196 msgid "At least one site isn't associated with the service of that test type" msgstr "" -#: modules/FlaskModule/API/user/UserQueryTestType.py:306 +#: modules/FlaskModule/API/user/UserQueryTestType.py:310 msgid "Test type has a a service not associated to its site" msgstr "" -#: modules/FlaskModule/API/user/UserQueryTestType.py:328 +#: modules/FlaskModule/API/user/UserQueryTestType.py:334 msgid "Unable to delete - not admin in the related test type service" msgstr "" -#: modules/FlaskModule/API/user/UserQueryTestType.py:338 +#: modules/FlaskModule/API/user/UserQueryTestType.py:344 msgid "" "Can't delete test type: please delete all tests of that type before " "deleting." msgstr "" -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:213 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:217 msgid "At least one test type is not associated to the site of its project" msgstr "" -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:143 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:176 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:266 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:147 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:180 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:272 msgid "" "Can't delete test type from site: please delete all tests of that type in" " the site before deleting." msgstr "" -#: modules/FlaskModule/API/user/UserQueryTests.py:123 +#: modules/FlaskModule/API/user/UserQueryTests.py:127 msgid "" "Test information update and creation must be done directly into a service" " (such as Test service)" msgstr "" -#: modules/FlaskModule/API/user/UserQueryUndelete.py:30 +#: modules/FlaskModule/API/user/UserQueryUndelete.py:32 msgid "No access to this API" msgstr "" -#: modules/FlaskModule/API/user/UserQueryUndelete.py:46 +#: modules/FlaskModule/API/user/UserQueryUndelete.py:48 msgid "Item to undelete not found" msgstr "" -#: modules/FlaskModule/API/user/UserQueryUndelete.py:49 +#: modules/FlaskModule/API/user/UserQueryUndelete.py:51 msgid "Item can't be undeleted" msgstr "" -#: modules/FlaskModule/API/user/UserQueryUndelete.py:52 +#: modules/FlaskModule/API/user/UserQueryUndelete.py:54 msgid "Item isn't deleted" msgstr "" -#: modules/FlaskModule/API/user/UserQueryUserGroups.py:142 +#: modules/FlaskModule/API/user/UserQueryUserGroups.py:145 msgid "Missing user group name" msgstr "" -#: modules/FlaskModule/API/user/UserQueryUserPreferences.py:89 +#: modules/FlaskModule/API/user/UserQueryUserPreferences.py:92 msgid "Missing app tag" msgstr "" -#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:57 +#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:59 msgid "At least one id must be specified" msgstr "" -#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:89 +#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:93 msgid "Missing user user group" msgstr "" -#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:100 -#: modules/FlaskModule/API/user/UserQueryUsers.py:183 +#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:104 +#: modules/FlaskModule/API/user/UserQueryUsers.py:198 msgid "Missing id_user" msgstr "" -#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:106 +#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:110 msgid "No access to specified user" msgstr "" -#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:108 +#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:112 msgid "No access to specified user group" msgstr "" -#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:113 +#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:117 msgid "Super admins can't be associated to an user group" msgstr "" -#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:158 +#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:164 msgid "Can't delete specified relationship" msgstr "" -#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:161 +#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:167 msgid "No access to relationship's user" msgstr "" -#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:163 +#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:169 msgid "No access to relationship's user group" msgstr "" -#: modules/FlaskModule/API/user/UserQueryUsers.py:176 +#: modules/FlaskModule/API/user/UserQueryUsers.py:57 +msgid "Password not long enough" +msgstr "" + +#: modules/FlaskModule/API/user/UserQueryUsers.py:191 msgid "Missing user" msgstr "" -#: modules/FlaskModule/API/user/UserQueryUsers.py:198 +#: modules/FlaskModule/API/user/UserQueryUsers.py:213 msgid "No access for at a least one user group in the list" msgstr "" -#: modules/FlaskModule/API/user/UserQueryUsers.py:229 +#: modules/FlaskModule/API/user/UserQueryUsers.py:244 msgid "Username can't be modified" msgstr "" -#: modules/FlaskModule/API/user/UserQueryUsers.py:250 +#: modules/FlaskModule/API/user/UserQueryUsers.py:257 +#: modules/FlaskModule/API/user/UserQueryUsers.py:295 +msgid "Password not strong enough" +msgstr "" + +#: modules/FlaskModule/API/user/UserQueryUsers.py:271 msgid "Missing required fields: " msgstr "" -#: modules/FlaskModule/API/user/UserQueryUsers.py:253 +#: modules/FlaskModule/API/user/UserQueryUsers.py:274 msgid "Invalid password" msgstr "" -#: modules/FlaskModule/API/user/UserQueryUsers.py:257 +#: modules/FlaskModule/API/user/UserQueryUsers.py:278 msgid "Username unavailable." msgstr "" -#: modules/FlaskModule/API/user/UserQueryUsers.py:336 +#: modules/FlaskModule/API/user/UserQueryUsers.py:344 msgid "Sorry, you can't delete yourself!" msgstr "" -#: modules/FlaskModule/API/user/UserQueryUsers.py:366 +#: modules/FlaskModule/API/user/UserQueryUsers.py:374 msgid "" "Can't delete user: please remove all sessions that this user is part of " "before deleting." msgstr "" -#: modules/FlaskModule/API/user/UserQueryUsers.py:369 +#: modules/FlaskModule/API/user/UserQueryUsers.py:377 msgid "" "Can't delete user: please remove all sessions created by this user before" " deleting." msgstr "" -#: modules/FlaskModule/API/user/UserQueryUsers.py:372 +#: modules/FlaskModule/API/user/UserQueryUsers.py:380 msgid "" "Can't delete user: please remove all tests created by this user before " "deleting." msgstr "" -#: modules/FlaskModule/API/user/UserQueryUsers.py:375 +#: modules/FlaskModule/API/user/UserQueryUsers.py:383 msgid "" "Can't delete user: please remove all assets created by this user before " "deleting." msgstr "" -#: modules/FlaskModule/API/user/UserQueryUsers.py:378 +#: modules/FlaskModule/API/user/UserQueryUsers.py:386 msgid "" "Can't delete user: please delete all assets created by this user before " "deleting." msgstr "" -#: modules/FlaskModule/API/user/UserQueryVersions.py:40 +#: modules/FlaskModule/API/user/UserQueryVersions.py:42 msgid "No version information found" msgstr "" -#: modules/FlaskModule/API/user/UserQueryVersions.py:71 +#: modules/FlaskModule/API/user/UserQueryVersions.py:75 msgid "Wrong ClientVersions" msgstr "" -#: modules/FlaskModule/API/user/UserQueryVersions.py:77 +#: modules/FlaskModule/API/user/UserQueryVersions.py:81 msgid "Not authorized" msgstr "" -#: modules/FlaskModule/API/user/UserSessionManager.py:129 +#: modules/FlaskModule/API/user/UserSessionManager.py:130 msgid "User doesn't have access to that session" msgstr "" -#: modules/FlaskModule/API/user/UserSessionManager.py:160 +#: modules/FlaskModule/API/user/UserSessionManager.py:161 msgid "User doesn't have access to that service." msgstr "" -#: modules/FlaskModule/API/user/UserSessionManager.py:165 +#: modules/FlaskModule/API/user/UserSessionManager.py:166 msgid "Missing parameters" msgstr "" -#: modules/FlaskModule/API/user/UserSessionManager.py:168 +#: modules/FlaskModule/API/user/UserSessionManager.py:169 msgid "Missing reply code in parameters" msgstr "" -#: modules/FlaskModule/API/user/UserSessionManager.py:181 -#: modules/FlaskModule/API/user/UserSessionManager.py:184 +#: modules/FlaskModule/API/user/UserSessionManager.py:182 +#: modules/FlaskModule/API/user/UserSessionManager.py:185 msgid "Invalid reply code" msgstr "" -#: modules/LoginModule/LoginModule.py:619 -#: modules/LoginModule/LoginModule.py:652 +#: modules/FlaskModule/Views/LoginChangePasswordView.py:38 +msgid "Missing information" +msgstr "" + +#: modules/FlaskModule/Views/LoginChangePasswordView.py:53 +msgid "New password must be different from current" +msgstr "" + +#: modules/LoginModule/LoginModule.py:219 +msgid "Unauthorized - User must login first to change password" +msgstr "" + +#: modules/LoginModule/LoginModule.py:222 +msgid "Unauthorized - 2FA is enabled, must login first and use token" +msgstr "" + +#: modules/LoginModule/LoginModule.py:633 +#: modules/LoginModule/LoginModule.py:666 msgid "Disabled device" msgstr "" -#: modules/LoginModule/LoginModule.py:629 +#: modules/LoginModule/LoginModule.py:643 msgid "Invalid token" msgstr "" -#: modules/LoginModule/LoginModule.py:729 +#: modules/LoginModule/LoginModule.py:743 msgid "Invalid Token" msgstr "" @@ -1845,7 +1972,7 @@ msgid "Device Onlineable?" msgstr "" #: opentera/forms/TeraDeviceForm.py:44 opentera/forms/TeraParticipantForm.py:50 -#: opentera/forms/TeraUserForm.py:30 +#: opentera/forms/TeraUserForm.py:46 msgid "Last Connection" msgstr "" @@ -1857,8 +1984,8 @@ msgstr "" #: opentera/forms/TeraProjectForm.py:18 #: opentera/forms/TeraServiceConfigForm.py:18 #: opentera/forms/TeraServiceForm.py:18 opentera/forms/TeraSessionForm.py:120 -#: opentera/forms/TeraSessionTypeForm.py:25 opentera/forms/TeraSiteForm.py:12 -#: opentera/forms/TeraUserForm.py:13 opentera/forms/TeraUserGroupForm.py:18 +#: opentera/forms/TeraSessionTypeForm.py:25 opentera/forms/TeraSiteForm.py:13 +#: opentera/forms/TeraUserForm.py:14 opentera/forms/TeraUserGroupForm.py:18 #: opentera/forms/TeraVersionsForm.py:18 msgid "Information" msgstr "" @@ -1876,7 +2003,7 @@ msgid "Device Configuration" msgstr "" #: opentera/forms/TeraDeviceForm.py:55 opentera/forms/TeraDeviceForm.py:57 -#: opentera/forms/TeraUserForm.py:28 +#: opentera/forms/TeraUserForm.py:48 msgid "Notes" msgstr "" @@ -1982,7 +2109,7 @@ msgstr "" msgid "Role" msgstr "" -#: opentera/forms/TeraProjectForm.py:28 opentera/forms/TeraSiteForm.py:17 +#: opentera/forms/TeraProjectForm.py:28 opentera/forms/TeraSiteForm.py:18 msgid "Site Name" msgstr "" @@ -2000,7 +2127,7 @@ msgstr "" msgid "Service Config ID" msgstr "" -#: opentera/forms/TeraServiceConfigForm.py:27 opentera/forms/TeraUserForm.py:17 +#: opentera/forms/TeraServiceConfigForm.py:27 opentera/forms/TeraUserForm.py:18 msgid "User ID" msgstr "" @@ -2044,11 +2171,13 @@ msgstr "" msgid "Port" msgstr "" -#: opentera/forms/TeraServiceConfigForm.py:71 opentera/forms/TeraUserForm.py:20 +#: opentera/forms/TeraServiceConfigForm.py:71 opentera/forms/TeraUserForm.py:21 +#: templates/login.html:125 msgid "Username" msgstr "" -#: opentera/forms/TeraServiceConfigForm.py:74 opentera/forms/TeraUserForm.py:26 +#: opentera/forms/TeraServiceConfigForm.py:74 opentera/forms/TeraUserForm.py:44 +#: templates/login.html:131 msgid "Password" msgstr "" @@ -2220,11 +2349,15 @@ msgstr "" msgid "Session Type Configuration" msgstr "" -#: opentera/forms/TeraSiteForm.py:16 +#: opentera/forms/TeraSiteForm.py:17 msgid "Site ID" msgstr "" -#: opentera/forms/TeraSiteForm.py:18 +#: opentera/forms/TeraSiteForm.py:20 +msgid "Users Require 2FA" +msgstr "" + +#: opentera/forms/TeraSiteForm.py:22 msgid "Site Role" msgstr "" @@ -2256,35 +2389,51 @@ msgstr "" msgid "Expose Web editor" msgstr "" -#: opentera/forms/TeraUserForm.py:18 +#: opentera/forms/TeraUserForm.py:19 msgid "User UUID" msgstr "" -#: opentera/forms/TeraUserForm.py:19 +#: opentera/forms/TeraUserForm.py:20 msgid "User Full Name" msgstr "" -#: opentera/forms/TeraUserForm.py:21 +#: opentera/forms/TeraUserForm.py:22 msgid "User Enabled" msgstr "" -#: opentera/forms/TeraUserForm.py:22 +#: opentera/forms/TeraUserForm.py:26 +msgid "Force password change" +msgstr "" + +#: opentera/forms/TeraUserForm.py:28 +msgid "2FA Enabled" +msgstr "" + +#: opentera/forms/TeraUserForm.py:31 +msgid "2FA OTP Enabled" +msgstr "" + +#: opentera/forms/TeraUserForm.py:34 +msgid "2FA Email Enabled" +msgstr "" + +#: opentera/forms/TeraUserForm.py:40 msgid "First Name" msgstr "" -#: opentera/forms/TeraUserForm.py:23 +#: opentera/forms/TeraUserForm.py:41 msgid "Last Name" msgstr "" -#: opentera/forms/TeraUserForm.py:24 +#: opentera/forms/TeraUserForm.py:42 msgid "Email" msgstr "" -#: opentera/forms/TeraUserForm.py:27 +#: opentera/forms/TeraUserForm.py:45 msgid "User Is Super Administrator" msgstr "" -#: opentera/forms/TeraUserForm.py:29 +#: opentera/forms/TeraUserForm.py:49 msgid "Profile" msgstr "" @@ -2313,7 +2462,6 @@ msgid "OpenTeraServer patch version number" msgstr "" #: opentera/forms/TeraVersionsForm.py:43 templates/about.html:26 -#: templates/disabled_doc.html:26 msgid "Versions" msgstr "" @@ -2433,22 +2581,119 @@ msgstr "" msgid "Latest version: " msgstr "" -#: templates/about.html:63 templates/disabled_doc.html:35 +#: templates/about.html:64 msgid "License" msgstr "" -#: templates/about.html:77 templates/disabled_doc.html:53 +#: templates/about.html:78 msgid "Authors" msgstr "" -#: templates/about.html:87 templates/disabled_doc.html:63 +#: templates/about.html:88 msgid "Contributors" msgstr "" -#: templates/disabled_doc.html:48 +#: templates/disabled_doc.html:27 msgid "Documentation is disabled!" msgstr "" +#: templates/login.html:5 +msgid "OpenTera Login Page" +msgstr "" + +#: templates/login.html:89 +msgid "Invalid username or password" +msgstr "" + +#: templates/login.html:139 +msgid "Login" +msgstr "" + +#: templates/login_change_password.html:5 +msgid "OpenTera - Change Password" +msgstr "" + +#: templates/login_change_password.html:96 +msgid "Password successfully changed!" +msgstr "" + +#: templates/login_change_password.html:97 +msgid "Redirecting to login screen..." +msgstr "" + +#: templates/login_change_password.html:104 +msgid "Password change required" +msgstr "" + +#: templates/login_change_password.html:110 +msgid "New Password" +msgstr "" + +#: templates/login_change_password.html:115 +msgid "Confirm Password" +msgstr "" + +#: templates/login_change_password.html:122 +msgid "Change Password" +msgstr "" + +#: templates/login_setup_2fa.html:107 +msgid "You need to setup multi-factor authentication before continuing." +msgstr "" + +#: templates/login_setup_2fa.html:121 +msgid "OTP configuration" +msgstr "" + +#: templates/login_setup_2fa.html:127 +msgid "Scan the QR code with your authenticator app" +msgstr "" + +#: templates/login_setup_2fa.html:128 +msgid "Enter the generated code:" +msgstr "" + +#: templates/login_setup_2fa.html:134 templates/login_setup_2fa.html:151 +#: templates/login_validate_2fa.html:162 +msgid "Validate" +msgstr "" + +#: templates/login_setup_2fa.html:144 +msgid "Use email for authentication" +msgstr "" + +#: templates/login_setup_2fa.html:149 +msgid "No email address configured. Please contact your administrator." +msgstr "" + +#: templates/login_validate_2fa.html:32 +msgid "Code validation timeout. Redirecting to login." +msgstr "" + +#: templates/login_validate_2fa.html:117 +msgid "Too many attempts - returning to login" +msgstr "" + +#: templates/login_validate_2fa.html:120 +msgid "Invalid code" +msgstr "" + +#: templates/login_validate_2fa.html:120 +msgid "attempts left" +msgstr "" + +#: templates/login_validate_2fa.html:144 +msgid "Multi Factor Authentication" +msgstr "" + +#: templates/login_validate_2fa.html:151 +msgid "Authentication code" +msgstr "" + +#: templates/login_validate_2fa.html:171 +msgid "Successfully authenticated" +msgstr "" + #~ msgid "Can't delete participant: please delete all sessions before deleting." #~ msgstr "" @@ -2476,3 +2721,12 @@ msgstr "" #~ msgid "Invalid client name :" #~ msgstr "" +#~ msgid "Invalid client version handler" +#~ msgstr "" + +#~ msgid "2FA enabled but OTP not set for this user. Please setup 2FA." +#~ msgstr "" + +#~ msgid "Invalid old password" +#~ msgstr "" + diff --git a/teraserver/python/translations/fr/LC_MESSAGES/messages.po b/teraserver/python/translations/fr/LC_MESSAGES/messages.po index cf5a3e2ef..399ede574 100644 --- a/teraserver/python/translations/fr/LC_MESSAGES/messages.po +++ b/teraserver/python/translations/fr/LC_MESSAGES/messages.po @@ -7,175 +7,201 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-05-13 11:02-0400\n" -"PO-Revision-Date: 2024-03-04 11:13-0500\n" +"POT-Creation-Date: 2024-10-07 15:05-0400\n" +"PO-Revision-Date: 2024-10-07 15:10-0400\n" "Last-Translator: \n" -"Language: fr\n" "Language-Team: fr \n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.15.0\n" - -#: modules/FlaskModule/API/device/DeviceLogin.py:88 +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"Generated-By: Babel 2.16.0\n" +"X-Generator: Poedit 3.5\n" + +#: modules/FlaskModule/API/user/UserQueryUsers.py:53 +#: modules/FlaskModule/FlaskUtils.py:13 +msgid "Password missing special character" +msgstr "Le mot de passe requiert un caractère spécial" + +#: modules/FlaskModule/API/user/UserQueryUsers.py:55 +#: modules/FlaskModule/FlaskUtils.py:15 +msgid "Password missing numeric character" +msgstr "Le mot de passe requiert au moins un chiffre" + +#: modules/FlaskModule/FlaskUtils.py:17 +msgid "Password not long enough (10 characters)" +msgstr "Le mot de passe n'est pas assez long (10 caractères min.)" + +#: modules/FlaskModule/API/user/UserQueryUsers.py:59 +#: modules/FlaskModule/FlaskUtils.py:19 +msgid "Password missing lower case letter" +msgstr "Le mot de passe requiert au moins une minuscule" + +#: modules/FlaskModule/API/user/UserQueryUsers.py:61 +#: modules/FlaskModule/FlaskUtils.py:21 +msgid "Password missing upper case letter" +msgstr "Le mot de passe requiert au moins une majuscule" + +#: modules/FlaskModule/API/device/DeviceLogin.py:90 msgid "Unable to get online devices." msgstr "Impossible d'obtenir les appareils connectés." -#: modules/FlaskModule/API/device/DeviceLogin.py:104 +#: modules/FlaskModule/API/device/DeviceLogin.py:106 msgid "Device already logged in." msgstr "L'appareil est déjà connecté." -#: modules/FlaskModule/API/device/DeviceLogout.py:29 +#: modules/FlaskModule/API/device/DeviceLogout.py:32 msgid "Device logged out." msgstr "Appareil déconnecté." -#: modules/FlaskModule/API/device/DeviceLogout.py:31 +#: modules/FlaskModule/API/device/DeviceLogout.py:34 msgid "Device not logged in" msgstr "L'appareil n'est pas connecté" -#: modules/FlaskModule/API/device/DeviceQueryAssets.py:39 -#, fuzzy +#: modules/FlaskModule/API/device/DeviceQueryAssets.py:42 +#: modules/FlaskModule/API/participant/ParticipantQueryAssets.py:48 msgid "No access to session" -msgstr "Aucun accès à la séance." +msgstr "Aucun accès à la séance" -#: modules/FlaskModule/API/device/DeviceQueryDevices.py:69 +#: modules/FlaskModule/API/device/DeviceQueryDevices.py:75 msgid "Missing device schema" msgstr "Schéma de l'appareil manquant" -#: modules/FlaskModule/API/device/DeviceQueryDevices.py:78 +#: modules/FlaskModule/API/device/DeviceQueryDevices.py:84 msgid "Missing config" msgstr "Configuration manquante" -#: modules/FlaskModule/API/device/DeviceQueryDevices.py:82 -#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:49 -#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:55 -#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:62 -#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:68 -#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:74 -#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:80 -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:52 -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:66 -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:121 -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:144 -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:179 -#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:72 -#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:79 -#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:89 -#: modules/FlaskModule/API/service/ServiceQueryProjects.py:52 -#: modules/FlaskModule/API/service/ServiceQueryProjects.py:57 -#: modules/FlaskModule/API/service/ServiceQueryProjects.py:99 -#: modules/FlaskModule/API/service/ServiceQueryProjects.py:106 -#: modules/FlaskModule/API/service/ServiceQueryProjects.py:158 -#: modules/FlaskModule/API/service/ServiceQueryRoles.py:112 -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:100 -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:104 -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:108 -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:116 -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:194 -#: modules/FlaskModule/API/service/ServiceQuerySessionEvents.py:86 +#: modules/FlaskModule/API/device/DeviceQueryDevices.py:88 +#: modules/FlaskModule/API/participant/ParticipantQueryAssets.py:44 +#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:51 +#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:57 +#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:64 +#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:70 +#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:76 +#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:82 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:55 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:69 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:127 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:150 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:188 +#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:75 +#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:82 +#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:92 +#: modules/FlaskModule/API/service/ServiceQueryProjects.py:55 +#: modules/FlaskModule/API/service/ServiceQueryProjects.py:60 +#: modules/FlaskModule/API/service/ServiceQueryProjects.py:105 +#: modules/FlaskModule/API/service/ServiceQueryProjects.py:112 +#: modules/FlaskModule/API/service/ServiceQueryProjects.py:167 +#: modules/FlaskModule/API/service/ServiceQueryRoles.py:121 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:106 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:110 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:114 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:122 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:203 #: modules/FlaskModule/API/service/ServiceQuerySessionEvents.py:91 -#: modules/FlaskModule/API/service/ServiceQuerySessions.py:66 -#: modules/FlaskModule/API/service/ServiceQuerySessions.py:72 -#: modules/FlaskModule/API/service/ServiceQuerySessions.py:76 -#: modules/FlaskModule/API/service/ServiceQuerySessions.py:84 -#: modules/FlaskModule/API/service/ServiceQuerySessions.py:90 -#: modules/FlaskModule/API/service/ServiceQuerySites.py:36 -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:121 -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:159 -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:193 -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:256 -#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:96 -#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:101 -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:50 -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:54 -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:106 -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:207 -#: modules/FlaskModule/API/user/UserQueryAssetsArchive.py:117 -#: modules/FlaskModule/API/user/UserQueryAssetsArchive.py:126 -#: modules/FlaskModule/API/user/UserQueryAssetsArchive.py:137 -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:301 -#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:122 -#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:257 -#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:112 -#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:165 -#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:105 -#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:191 -#: modules/FlaskModule/API/user/UserQueryDevices.py:293 -#: modules/FlaskModule/API/user/UserQueryDevices.py:297 -#: modules/FlaskModule/API/user/UserQueryDevices.py:417 -#: modules/FlaskModule/API/user/UserQueryDisconnect.py:52 -#: modules/FlaskModule/API/user/UserQueryDisconnect.py:60 -#: modules/FlaskModule/API/user/UserQueryDisconnect.py:66 -#: modules/FlaskModule/API/user/UserQueryDisconnect.py:72 -#: modules/FlaskModule/API/user/UserQueryDisconnect.py:78 -#: modules/FlaskModule/API/user/UserQueryDisconnect.py:84 -#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:107 -#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:159 -#: modules/FlaskModule/API/user/UserQueryParticipants.py:362 -#: modules/FlaskModule/API/user/UserQueryParticipants.py:365 -#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:204 -#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:274 -#: modules/FlaskModule/API/user/UserQueryProjects.py:151 -#: modules/FlaskModule/API/user/UserQueryProjects.py:156 -#: modules/FlaskModule/API/user/UserQueryProjects.py:264 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:117 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:122 +#: modules/FlaskModule/API/service/ServiceQuerySessionEvents.py:96 +#: modules/FlaskModule/API/service/ServiceQuerySessions.py:69 +#: modules/FlaskModule/API/service/ServiceQuerySessions.py:75 +#: modules/FlaskModule/API/service/ServiceQuerySessions.py:79 +#: modules/FlaskModule/API/service/ServiceQuerySessions.py:87 +#: modules/FlaskModule/API/service/ServiceQuerySessions.py:93 +#: modules/FlaskModule/API/service/ServiceQuerySites.py:39 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:127 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:165 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:199 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:265 +#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:102 +#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:107 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:52 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:56 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:111 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:215 +#: modules/FlaskModule/API/user/UserQueryAssetsArchive.py:116 +#: modules/FlaskModule/API/user/UserQueryAssetsArchive.py:125 +#: modules/FlaskModule/API/user/UserQueryAssetsArchive.py:136 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:306 +#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:125 +#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:262 +#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:115 +#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:170 +#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:107 +#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:195 +#: modules/FlaskModule/API/user/UserQueryDevices.py:296 +#: modules/FlaskModule/API/user/UserQueryDevices.py:300 +#: modules/FlaskModule/API/user/UserQueryDevices.py:422 +#: modules/FlaskModule/API/user/UserQueryDisconnect.py:53 +#: modules/FlaskModule/API/user/UserQueryDisconnect.py:61 +#: modules/FlaskModule/API/user/UserQueryDisconnect.py:67 +#: modules/FlaskModule/API/user/UserQueryDisconnect.py:73 +#: modules/FlaskModule/API/user/UserQueryDisconnect.py:79 +#: modules/FlaskModule/API/user/UserQueryDisconnect.py:85 +#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:110 +#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:164 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:368 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:371 +#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:207 +#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:279 +#: modules/FlaskModule/API/user/UserQueryProjects.py:155 +#: modules/FlaskModule/API/user/UserQueryProjects.py:160 +#: modules/FlaskModule/API/user/UserQueryProjects.py:270 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:121 #: modules/FlaskModule/API/user/UserQueryServiceAccess.py:126 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:134 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:213 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:217 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:221 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:225 -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:151 -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:155 -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:159 -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:165 -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:240 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:130 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:138 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:219 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:223 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:227 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:231 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:154 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:158 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:162 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:168 #: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:245 -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:249 -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:253 -#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:79 -#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:136 -#: modules/FlaskModule/API/user/UserQueryServiceSites.py:129 -#: modules/FlaskModule/API/user/UserQueryServiceSites.py:259 -#: modules/FlaskModule/API/user/UserQueryServices.py:121 -#: modules/FlaskModule/API/user/UserQueryServices.py:134 -#: modules/FlaskModule/API/user/UserQueryServices.py:231 -#: modules/FlaskModule/API/user/UserQueryServices.py:239 -#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:91 -#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:144 -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:204 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:203 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:267 -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:55 -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:107 -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:138 -#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:187 -#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:275 -#: modules/FlaskModule/API/user/UserQuerySites.py:124 -#: modules/FlaskModule/API/user/UserQuerySites.py:127 -#: modules/FlaskModule/API/user/UserQuerySites.py:176 -#: modules/FlaskModule/API/user/UserQueryStats.py:52 -#: modules/FlaskModule/API/user/UserQueryStats.py:57 -#: modules/FlaskModule/API/user/UserQueryStats.py:62 -#: modules/FlaskModule/API/user/UserQueryStats.py:68 -#: modules/FlaskModule/API/user/UserQueryStats.py:73 -#: modules/FlaskModule/API/user/UserQueryStats.py:81 -#: modules/FlaskModule/API/user/UserQueryStats.py:86 -#: modules/FlaskModule/API/user/UserQueryStats.py:91 -#: modules/FlaskModule/API/user/UserQueryTestType.py:62 -#: modules/FlaskModule/API/user/UserQueryTestType.py:169 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:205 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:193 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:256 -#: modules/FlaskModule/API/user/UserQueryTests.py:140 -#: modules/FlaskModule/API/user/UserQueryUserGroups.py:147 -#: modules/FlaskModule/API/user/UserQueryUserPreferences.py:47 -#: modules/FlaskModule/API/user/UserQueryUserPreferences.py:96 -#: modules/FlaskModule/API/user/UserQueryUsers.py:214 -#: modules/FlaskModule/API/user/UserQueryUsers.py:219 -#: modules/FlaskModule/API/user/UserQueryUsers.py:348 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:250 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:254 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:258 +#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:83 +#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:142 +#: modules/FlaskModule/API/user/UserQueryServiceSites.py:133 +#: modules/FlaskModule/API/user/UserQueryServiceSites.py:265 +#: modules/FlaskModule/API/user/UserQueryServices.py:124 +#: modules/FlaskModule/API/user/UserQueryServices.py:137 +#: modules/FlaskModule/API/user/UserQueryServices.py:236 +#: modules/FlaskModule/API/user/UserQueryServices.py:244 +#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:94 +#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:149 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:208 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:207 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:273 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:57 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:111 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:142 +#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:190 +#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:280 +#: modules/FlaskModule/API/user/UserQuerySites.py:128 +#: modules/FlaskModule/API/user/UserQuerySites.py:132 +#: modules/FlaskModule/API/user/UserQuerySites.py:183 +#: modules/FlaskModule/API/user/UserQueryStats.py:54 +#: modules/FlaskModule/API/user/UserQueryStats.py:59 +#: modules/FlaskModule/API/user/UserQueryStats.py:64 +#: modules/FlaskModule/API/user/UserQueryStats.py:70 +#: modules/FlaskModule/API/user/UserQueryStats.py:75 +#: modules/FlaskModule/API/user/UserQueryStats.py:83 +#: modules/FlaskModule/API/user/UserQueryStats.py:88 +#: modules/FlaskModule/API/user/UserQueryStats.py:93 +#: modules/FlaskModule/API/user/UserQueryTestType.py:64 +#: modules/FlaskModule/API/user/UserQueryTestType.py:173 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:209 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:197 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:262 +#: modules/FlaskModule/API/user/UserQueryTests.py:147 +#: modules/FlaskModule/API/user/UserQueryUserGroups.py:150 +#: modules/FlaskModule/API/user/UserQueryUserPreferences.py:48 +#: modules/FlaskModule/API/user/UserQueryUserPreferences.py:99 +#: modules/FlaskModule/API/user/UserQueryUsers.py:229 +#: modules/FlaskModule/API/user/UserQueryUsers.py:234 +#: modules/FlaskModule/API/user/UserQueryUsers.py:356 #: opentera/services/ServiceAccessManager.py:116 #: opentera/services/ServiceAccessManager.py:166 #: opentera/services/ServiceAccessManager.py:195 @@ -191,964 +217,1037 @@ msgstr "Configuration manquante" msgid "Forbidden" msgstr "Accès refusé" -#: modules/FlaskModule/API/device/DeviceQueryDevices.py:93 -#: modules/FlaskModule/API/device/DeviceQuerySessionEvents.py:74 -#: modules/FlaskModule/API/device/DeviceQuerySessionEvents.py:89 -#: modules/FlaskModule/API/device/DeviceQuerySessions.py:157 -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:233 -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:247 -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:284 -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:136 -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:153 -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:201 -#: modules/FlaskModule/API/service/ServiceQueryProjects.py:116 -#: modules/FlaskModule/API/service/ServiceQueryProjects.py:138 -#: modules/FlaskModule/API/service/ServiceQueryProjects.py:176 -#: modules/FlaskModule/API/service/ServiceQueryRoles.py:73 -#: modules/FlaskModule/API/service/ServiceQueryRoles.py:94 -#: modules/FlaskModule/API/service/ServiceQueryRoles.py:123 -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:138 -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:150 -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:172 -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:205 -#: modules/FlaskModule/API/service/ServiceQuerySessionEvents.py:104 -#: modules/FlaskModule/API/service/ServiceQuerySessionEvents.py:119 -#: modules/FlaskModule/API/service/ServiceQuerySessions.py:185 -#: modules/FlaskModule/API/service/ServiceQuerySessions.py:218 -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:233 -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:274 -#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:111 -#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:132 -#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:166 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:231 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:242 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:277 -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:151 -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:168 -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:225 -#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:180 -#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:221 -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:276 -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:331 -#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:232 -#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:268 -#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:130 -#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:145 -#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:187 -#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:121 -#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:136 -#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:189 -#: modules/FlaskModule/API/user/UserQueryDevices.py:328 -#: modules/FlaskModule/API/user/UserQueryDevices.py:343 -#: modules/FlaskModule/API/user/UserQueryDevices.py:460 -#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:120 -#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:135 -#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:178 -#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:244 -#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:285 -#: modules/FlaskModule/API/user/UserQueryProjects.py:196 -#: modules/FlaskModule/API/user/UserQueryProjects.py:211 -#: modules/FlaskModule/API/user/UserQueryProjects.py:282 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:156 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:168 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:190 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:236 -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:208 -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:264 -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:287 -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:322 -#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:98 -#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:119 -#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:147 -#: modules/FlaskModule/API/user/UserQueryServiceSites.py:244 -#: modules/FlaskModule/API/user/UserQueryServiceSites.py:286 -#: modules/FlaskModule/API/user/UserQueryServices.py:162 -#: modules/FlaskModule/API/user/UserQueryServices.py:182 -#: modules/FlaskModule/API/user/UserQueryServices.py:259 -#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:104 -#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:119 -#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:155 -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:251 -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:291 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:244 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:286 -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:197 -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:212 -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:342 -#: modules/FlaskModule/API/user/UserQuerySessions.py:179 -#: modules/FlaskModule/API/user/UserQuerySessions.py:194 -#: modules/FlaskModule/API/user/UserQuerySessions.py:272 -#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:246 -#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:286 -#: modules/FlaskModule/API/user/UserQuerySites.py:140 -#: modules/FlaskModule/API/user/UserQuerySites.py:155 -#: modules/FlaskModule/API/user/UserQuerySites.py:194 -#: modules/FlaskModule/API/user/UserQueryTestType.py:222 -#: modules/FlaskModule/API/user/UserQueryTestType.py:237 -#: modules/FlaskModule/API/user/UserQueryTestType.py:345 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:252 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:293 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:233 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:274 -#: modules/FlaskModule/API/user/UserQueryTests.py:151 -#: modules/FlaskModule/API/user/UserQueryUserGroups.py:190 -#: modules/FlaskModule/API/user/UserQueryUserGroups.py:205 -#: modules/FlaskModule/API/user/UserQueryUserGroups.py:233 -#: modules/FlaskModule/API/user/UserQueryUserGroups.py:262 -#: modules/FlaskModule/API/user/UserQueryUserGroups.py:310 -#: modules/FlaskModule/API/user/UserQueryUserPreferences.py:111 -#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:138 -#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:174 -#: modules/FlaskModule/API/user/UserQueryUsers.py:240 -#: modules/FlaskModule/API/user/UserQueryUsers.py:272 -#: modules/FlaskModule/API/user/UserQueryUsers.py:385 +#: modules/FlaskModule/API/device/DeviceQueryDevices.py:99 +#: modules/FlaskModule/API/device/DeviceQuerySessionEvents.py:79 +#: modules/FlaskModule/API/device/DeviceQuerySessionEvents.py:94 +#: modules/FlaskModule/API/device/DeviceQuerySessions.py:162 +#: modules/FlaskModule/API/participant/ParticipantQuerySessions.py:188 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:239 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:253 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:293 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:142 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:159 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:210 +#: modules/FlaskModule/API/service/ServiceQueryProjects.py:122 +#: modules/FlaskModule/API/service/ServiceQueryProjects.py:144 +#: modules/FlaskModule/API/service/ServiceQueryProjects.py:185 +#: modules/FlaskModule/API/service/ServiceQueryRoles.py:79 +#: modules/FlaskModule/API/service/ServiceQueryRoles.py:100 +#: modules/FlaskModule/API/service/ServiceQueryRoles.py:132 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:144 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:156 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:178 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:214 +#: modules/FlaskModule/API/service/ServiceQuerySessionEvents.py:109 +#: modules/FlaskModule/API/service/ServiceQuerySessionEvents.py:124 +#: modules/FlaskModule/API/service/ServiceQuerySessions.py:191 +#: modules/FlaskModule/API/service/ServiceQuerySessions.py:224 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:239 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:283 +#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:117 +#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:138 +#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:175 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:237 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:248 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:286 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:156 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:173 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:233 +#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:183 +#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:226 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:279 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:336 +#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:235 +#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:273 +#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:133 +#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:148 +#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:192 +#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:123 +#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:138 +#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:193 +#: modules/FlaskModule/API/user/UserQueryDevices.py:331 +#: modules/FlaskModule/API/user/UserQueryDevices.py:346 +#: modules/FlaskModule/API/user/UserQueryDevices.py:465 +#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:123 +#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:138 +#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:183 +#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:247 +#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:290 +#: modules/FlaskModule/API/user/UserQueryProjects.py:200 +#: modules/FlaskModule/API/user/UserQueryProjects.py:215 +#: modules/FlaskModule/API/user/UserQueryProjects.py:288 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:160 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:172 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:194 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:242 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:211 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:269 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:291 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:328 +#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:102 +#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:123 +#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:153 +#: modules/FlaskModule/API/user/UserQueryServiceSites.py:248 +#: modules/FlaskModule/API/user/UserQueryServiceSites.py:292 +#: modules/FlaskModule/API/user/UserQueryServices.py:165 +#: modules/FlaskModule/API/user/UserQueryServices.py:185 +#: modules/FlaskModule/API/user/UserQueryServices.py:264 +#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:107 +#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:122 +#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:160 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:255 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:297 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:248 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:292 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:201 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:216 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:349 +#: modules/FlaskModule/API/user/UserQuerySessions.py:183 +#: modules/FlaskModule/API/user/UserQuerySessions.py:198 +#: modules/FlaskModule/API/user/UserQuerySessions.py:278 +#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:249 +#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:291 +#: modules/FlaskModule/API/user/UserQuerySites.py:145 +#: modules/FlaskModule/API/user/UserQuerySites.py:160 +#: modules/FlaskModule/API/user/UserQuerySites.py:201 +#: modules/FlaskModule/API/user/UserQueryTestType.py:226 +#: modules/FlaskModule/API/user/UserQueryTestType.py:241 +#: modules/FlaskModule/API/user/UserQueryTestType.py:351 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:256 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:299 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:237 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:280 +#: modules/FlaskModule/API/user/UserQueryTests.py:158 +#: modules/FlaskModule/API/user/UserQueryUserGroups.py:193 +#: modules/FlaskModule/API/user/UserQueryUserGroups.py:208 +#: modules/FlaskModule/API/user/UserQueryUserGroups.py:236 +#: modules/FlaskModule/API/user/UserQueryUserGroups.py:265 +#: modules/FlaskModule/API/user/UserQueryUserGroups.py:315 +#: modules/FlaskModule/API/user/UserQueryUserPreferences.py:114 +#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:142 +#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:180 +#: modules/FlaskModule/API/user/UserQueryUsers.py:255 +#: modules/FlaskModule/API/user/UserQueryUsers.py:293 +#: modules/FlaskModule/API/user/UserQueryUsers.py:393 msgid "Database error" msgstr "Erreur de base de données" #: modules/FlaskModule/API/device/DeviceQueryParticipants.py:41 -#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:82 -#: modules/FlaskModule/API/service/ServiceQueryProjects.py:72 -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:67 -#: modules/FlaskModule/API/service/ServiceQueryServices.py:76 -#: modules/FlaskModule/API/service/ServiceQuerySessionEvents.py:62 -#: modules/FlaskModule/API/service/ServiceQuerySessions.py:122 -#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:109 -#: modules/FlaskModule/API/user/UserQueryDisconnect.py:86 -#: modules/FlaskModule/API/user/UserQueryProjects.py:116 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:84 -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:123 -#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:67 -#: modules/FlaskModule/API/user/UserQueryServiceSites.py:112 -#: modules/FlaskModule/API/user/UserQueryServices.py:104 -#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:62 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:101 -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:79 -#: modules/FlaskModule/API/user/UserQuerySessions.py:103 -#: modules/FlaskModule/API/user/UserQuerySites.py:97 -#: modules/FlaskModule/API/user/UserQueryTestType.py:112 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:99 -#: modules/FlaskModule/API/user/UserQueryUserPreferences.py:67 +#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:84 +#: modules/FlaskModule/API/service/ServiceQueryProjects.py:75 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:70 +#: modules/FlaskModule/API/service/ServiceQueryServices.py:79 +#: modules/FlaskModule/API/service/ServiceQuerySessionEvents.py:64 +#: modules/FlaskModule/API/service/ServiceQuerySessions.py:125 +#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:110 +#: modules/FlaskModule/API/user/UserQueryDisconnect.py:87 +#: modules/FlaskModule/API/user/UserQueryProjects.py:118 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:86 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:125 +#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:69 +#: modules/FlaskModule/API/user/UserQueryServiceSites.py:114 +#: modules/FlaskModule/API/user/UserQueryServices.py:105 +#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:63 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:103 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:81 +#: modules/FlaskModule/API/user/UserQuerySessions.py:105 +#: modules/FlaskModule/API/user/UserQuerySites.py:99 +#: modules/FlaskModule/API/user/UserQueryTestType.py:114 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:101 +#: modules/FlaskModule/API/user/UserQueryUserPreferences.py:68 msgid "Invalid request" msgstr "Requête invalide" -#: modules/FlaskModule/API/device/DeviceQuerySessionEvents.py:31 -#: modules/FlaskModule/API/device/DeviceQuerySessionEvents.py:97 -#: modules/FlaskModule/API/device/DeviceQuerySessions.py:72 -#: modules/FlaskModule/API/device/DeviceQuerySessions.py:193 +#: modules/FlaskModule/API/device/DeviceQuerySessionEvents.py:33 +#: modules/FlaskModule/API/device/DeviceQuerySessionEvents.py:105 +#: modules/FlaskModule/API/device/DeviceQuerySessions.py:74 +#: modules/FlaskModule/API/device/DeviceQuerySessions.py:201 msgid "Forbidden for security reasons" msgstr "Accès interdit pour raison de sécurité" -#: modules/FlaskModule/API/device/DeviceQuerySessionEvents.py:47 -#: modules/FlaskModule/API/device/DeviceQuerySessionEvents.py:53 -#: modules/FlaskModule/API/device/DeviceQueryStatus.py:48 -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:270 -#: modules/FlaskModule/API/service/ServiceQueryDevices.py:73 -#: modules/FlaskModule/API/service/ServiceQueryDevices.py:87 -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:76 -#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:104 -#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:119 -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:49 -#: modules/FlaskModule/API/service/ServiceQuerySessionEvents.py:46 -#: modules/FlaskModule/API/service/ServiceQuerySiteProjectAccessRoles.py:43 -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:55 -#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:152 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:263 -#: modules/FlaskModule/API/service/ServiceQueryUsers.py:38 -#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:70 -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:65 -#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:61 -#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:157 -#: modules/FlaskModule/API/user/UserQueryForms.py:85 -#: modules/FlaskModule/API/user/UserQueryParticipants.py:76 -#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:81 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:53 -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:63 -#: modules/FlaskModule/API/user/UserQueryServiceSites.py:60 -#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:46 -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:62 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:59 -#: modules/FlaskModule/API/user/UserQuerySessions.py:59 -#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:80 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:62 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:60 +#: modules/FlaskModule/API/device/DeviceQuerySessionEvents.py:52 +#: modules/FlaskModule/API/device/DeviceQuerySessionEvents.py:58 +#: modules/FlaskModule/API/device/DeviceQueryStatus.py:50 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:279 +#: modules/FlaskModule/API/service/ServiceQueryDevices.py:71 +#: modules/FlaskModule/API/service/ServiceQueryDevices.py:88 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:79 +#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:107 +#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:125 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:52 +#: modules/FlaskModule/API/service/ServiceQuerySessionEvents.py:48 +#: modules/FlaskModule/API/service/ServiceQuerySiteProjectAccessRoles.py:46 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:58 +#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:161 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:272 +#: modules/FlaskModule/API/service/ServiceQueryUsers.py:41 +#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:71 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:66 +#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:62 +#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:161 +#: modules/FlaskModule/API/user/UserQueryForms.py:98 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:78 +#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:82 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:55 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:65 +#: modules/FlaskModule/API/user/UserQueryServiceSites.py:62 +#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:47 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:64 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:61 +#: modules/FlaskModule/API/user/UserQuerySessions.py:61 +#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:81 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:64 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:62 msgid "Missing arguments" msgstr "Arguments manquants" -#: modules/FlaskModule/API/device/DeviceQuerySessionEvents.py:61 -#: modules/FlaskModule/API/device/DeviceQuerySessions.py:137 -#: modules/LoginModule/LoginModule.py:584 -#: modules/LoginModule/LoginModule.py:684 -#: modules/LoginModule/LoginModule.py:750 -#: modules/LoginModule/LoginModule.py:777 -#: modules/LoginModule/LoginModule.py:796 +#: modules/FlaskModule/API/device/DeviceQuerySessionEvents.py:66 +#: modules/FlaskModule/API/device/DeviceQuerySessions.py:142 +#: modules/FlaskModule/API/participant/ParticipantQuerySessions.py:169 +#: modules/LoginModule/LoginModule.py:598 modules/LoginModule/LoginModule.py:698 +#: modules/LoginModule/LoginModule.py:764 modules/LoginModule/LoginModule.py:791 +#: modules/LoginModule/LoginModule.py:810 modules/LoginModule/LoginModule.py:829 +#: modules/LoginModule/LoginModule.py:831 msgid "Unauthorized" msgstr "Non autorisé" -#: modules/FlaskModule/API/device/DeviceQuerySessions.py:87 -#: modules/FlaskModule/API/service/ServiceQuerySessions.py:136 -#: modules/FlaskModule/API/user/UserQuerySessions.py:120 +#: modules/FlaskModule/API/device/DeviceQuerySessions.py:92 +#: modules/FlaskModule/API/participant/ParticipantQuerySessions.py:120 +#: modules/FlaskModule/API/service/ServiceQuerySessions.py:142 +#: modules/FlaskModule/API/user/UserQuerySessions.py:124 msgid "Missing session" msgstr "Champ session manquant" -#: modules/FlaskModule/API/device/DeviceQuerySessions.py:95 +#: modules/FlaskModule/API/device/DeviceQuerySessions.py:100 +#: modules/FlaskModule/API/participant/ParticipantQuerySessions.py:128 msgid "Missing id_session value" msgstr "Champ id_session manquant" -#: modules/FlaskModule/API/device/DeviceQuerySessions.py:99 +#: modules/FlaskModule/API/device/DeviceQuerySessions.py:104 +#: modules/FlaskModule/API/participant/ParticipantQuerySessions.py:132 msgid "Missing id_session_type value" msgstr "Champ id_session_type manquant" -#: modules/FlaskModule/API/device/DeviceQuerySessions.py:104 +#: modules/FlaskModule/API/device/DeviceQuerySessions.py:109 +#: modules/FlaskModule/API/participant/ParticipantQuerySessions.py:137 msgid "Missing session participants and/or users and/or devices" msgstr "Utilisateurs et/ou participants et/ou appareils manquants pour la séance" -#: modules/FlaskModule/API/device/DeviceQuerySessions.py:114 +#: modules/FlaskModule/API/device/DeviceQuerySessions.py:119 +#: modules/FlaskModule/API/participant/ParticipantQuerySessions.py:146 msgid "No access to session type" msgstr "Aucun accès au type de séance" -#: modules/FlaskModule/API/device/DeviceQuerySessions.py:119 +#: modules/FlaskModule/API/device/DeviceQuerySessions.py:124 +#: modules/FlaskModule/API/participant/ParticipantQuerySessions.py:151 msgid "Missing argument 'session name'" msgstr "Paramètre 'session name' manquant" -#: modules/FlaskModule/API/device/DeviceQuerySessions.py:121 +#: modules/FlaskModule/API/device/DeviceQuerySessions.py:126 +#: modules/FlaskModule/API/participant/ParticipantQuerySessions.py:153 msgid "Missing argument 'session_start_datetime'" msgstr "Paramètre 'session_start_datetime' manquant" -#: modules/FlaskModule/API/device/DeviceQuerySessions.py:170 -#: modules/FlaskModule/API/service/ServiceQueryAccess.py:130 +#: modules/FlaskModule/API/device/DeviceQuerySessions.py:175 +#: modules/FlaskModule/API/participant/ParticipantQuerySessions.py:201 +#: modules/FlaskModule/API/service/ServiceQueryAccess.py:132 msgid "Invalid participant uuid" msgstr "UUID participant invalide" -#: modules/FlaskModule/API/device/DeviceQueryStatus.py:67 +#: modules/FlaskModule/API/device/DeviceQueryStatus.py:69 msgid "Status update forbidden on offline device." msgstr "Mise à jour de l'état interdite sur un appareil hors-ligne." -#: modules/FlaskModule/API/device/DeviceRegister.py:73 -#: modules/FlaskModule/API/device/DeviceRegister.py:99 +#: modules/FlaskModule/API/device/DeviceRegister.py:76 +#: modules/FlaskModule/API/device/DeviceRegister.py:105 msgid "Invalid registration key" msgstr "Clé d'enregistrement invalide" -#: modules/FlaskModule/API/device/DeviceRegister.py:103 +#: modules/FlaskModule/API/device/DeviceRegister.py:109 msgid "Invalid content type" msgstr "Content-Type invalide" -#: modules/FlaskModule/API/device/DeviceRegister.py:137 +#: modules/FlaskModule/API/device/DeviceRegister.py:143 msgid "Invalid CSR signature" msgstr "La signature du certificat CSR est invalides" -#: modules/FlaskModule/API/participant/ParticipantLogin.py:92 +#: modules/FlaskModule/API/participant/ParticipantLogin.py:96 msgid "Participant already logged in." msgstr "Le participant est déjà connecté." -#: modules/FlaskModule/API/participant/ParticipantLogin.py:126 +#: modules/FlaskModule/API/participant/ParticipantLogin.py:138 msgid "Missing current_participant" msgstr "Champ current_participant manquant" -#: modules/FlaskModule/API/participant/ParticipantLogout.py:41 +#: modules/FlaskModule/API/participant/ParticipantLogout.py:44 msgid "Participant logged out." msgstr "Participant hors ligne." -#: modules/FlaskModule/API/participant/ParticipantLogout.py:43 +#: modules/FlaskModule/API/participant/ParticipantLogout.py:46 msgid "Participant not logged in" msgstr "Le participant n'est pas connecté" -#: modules/FlaskModule/API/participant/ParticipantQueryParticipants.py:55 -#: modules/FlaskModule/API/participant/ParticipantQuerySessions.py:73 +#: modules/FlaskModule/API/participant/ParticipantQueryParticipants.py:58 msgid "Not implemented" msgstr "Non implémenté" -#: modules/FlaskModule/API/service/ServiceQueryAccess.py:57 -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:61 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:57 -#: modules/FlaskModule/API/user/UserQueryAssets.py:60 -#: modules/FlaskModule/API/user/UserQueryTests.py:54 +#: modules/FlaskModule/API/service/ServiceQueryAccess.py:59 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:64 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:60 +#: modules/FlaskModule/API/user/UserQueryAssets.py:61 +#: modules/FlaskModule/API/user/UserQueryTests.py:56 msgid "No arguments specified" msgstr "Aucun argument spécifié" -#: modules/FlaskModule/API/service/ServiceQueryAccess.py:67 +#: modules/FlaskModule/API/service/ServiceQueryAccess.py:69 msgid "Missing at least one from argument for uuids" msgstr "Au moins un champ \"UUID\" manquant" -#: modules/FlaskModule/API/service/ServiceQueryAccess.py:77 +#: modules/FlaskModule/API/service/ServiceQueryAccess.py:79 msgid "Invalid user uuid" msgstr "UUID d'utilisateur invalide" -#: modules/FlaskModule/API/service/ServiceQueryAccess.py:126 +#: modules/FlaskModule/API/service/ServiceQueryAccess.py:128 msgid "Participant cannot be admin" msgstr "Un participant ne peut être administrateur" -#: modules/FlaskModule/API/service/ServiceQueryAccess.py:163 +#: modules/FlaskModule/API/service/ServiceQueryAccess.py:165 msgid "Device cannot be admin" msgstr "Un appareil ne peut être administrateur" -#: modules/FlaskModule/API/service/ServiceQueryAccess.py:167 +#: modules/FlaskModule/API/service/ServiceQueryAccess.py:169 msgid "Invalid device uuid" msgstr "UUID de l'appareil invalide" -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:64 -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:90 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:60 -#: modules/FlaskModule/API/user/UserQueryAssets.py:63 -#: modules/FlaskModule/API/user/UserQueryAssets.py:91 -#: modules/FlaskModule/API/user/UserQueryTests.py:57 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:67 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:93 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:63 +#: modules/FlaskModule/API/user/UserQueryAssets.py:64 +#: modules/FlaskModule/API/user/UserQueryAssets.py:92 +#: modules/FlaskModule/API/user/UserQueryTests.py:59 msgid "Device access denied" msgstr "Accès à l'appareil interdit" -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:68 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:64 -#: modules/FlaskModule/API/user/UserQueryAssets.py:67 -#: modules/FlaskModule/API/user/UserQueryTests.py:61 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:71 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:67 +#: modules/FlaskModule/API/user/UserQueryAssets.py:68 +#: modules/FlaskModule/API/user/UserQueryTests.py:63 msgid "Session access denied" msgstr "Accès à la session refusé" -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:72 -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:86 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:68 -#: modules/FlaskModule/API/user/UserQueryAssets.py:71 -#: modules/FlaskModule/API/user/UserQueryAssets.py:87 -#: modules/FlaskModule/API/user/UserQueryTests.py:65 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:75 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:89 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:71 +#: modules/FlaskModule/API/user/UserQueryAssets.py:72 +#: modules/FlaskModule/API/user/UserQueryAssets.py:88 +#: modules/FlaskModule/API/user/UserQueryTests.py:67 msgid "Participant access denied" msgstr "Accès au participant refusé" -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:76 -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:82 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:72 -#: modules/FlaskModule/API/user/UserQueryAssets.py:75 -#: modules/FlaskModule/API/user/UserQueryAssets.py:83 -#: modules/FlaskModule/API/user/UserQueryTests.py:69 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:79 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:85 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:75 +#: modules/FlaskModule/API/user/UserQueryAssets.py:76 +#: modules/FlaskModule/API/user/UserQueryAssets.py:84 +#: modules/FlaskModule/API/user/UserQueryTests.py:71 msgid "User access denied" msgstr "Accès à l'utilisateur refusé" -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:97 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:80 -#: modules/FlaskModule/API/user/UserQueryAssets.py:100 -#: modules/FlaskModule/API/user/UserQueryTests.py:76 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:100 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:83 +#: modules/FlaskModule/API/user/UserQueryAssets.py:101 +#: modules/FlaskModule/API/user/UserQueryTests.py:78 msgid "Missing argument" msgstr "Paramètre manquant" -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:163 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:169 msgid "Missing asset field" msgstr "Champ asset manquant" -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:169 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:175 msgid "Missing id_asset field" msgstr "Champ id_asset manquant" -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:173 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:141 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:179 #: modules/FlaskModule/API/service/ServiceQueryTests.py:147 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:153 msgid "Unknown session" msgstr "Séance inconnue" -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:176 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:182 msgid "Invalid asset type" msgstr "Mauvais type de données (asset)" -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:179 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:185 msgid "Invalid asset name" msgstr "Nom de donnée (asset) invalide" -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:189 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:195 msgid "Service can't create assets for that session" msgstr "Le service ne peut pas créer de données pour cette séance" -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:196 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:196 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:202 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:202 msgid "Invalid participant" msgstr "Nom de participant incorrect" -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:204 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:204 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:210 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:210 msgid "Invalid user" msgstr "Utilisateur invalide" -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:212 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:212 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:218 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:218 msgid "Invalid device" msgstr "Appareil invalide" -#: modules/FlaskModule/API/service/ServiceQueryAssets.py:273 +#: modules/FlaskModule/API/service/ServiceQueryAssets.py:282 msgid "Service can't delete assets for that session" msgstr "Le service ne peut pas effacer les données de cette séance" -#: modules/FlaskModule/API/service/ServiceQueryDevices.py:93 +#: modules/FlaskModule/API/service/ServiceQueryDevices.py:94 msgid "Unknown device type" msgstr "Type d’appareil inconnu" -#: modules/FlaskModule/API/service/ServiceQueryDevices.py:97 +#: modules/FlaskModule/API/service/ServiceQueryDevices.py:98 msgid "Unknown device subtype" msgstr "Sous-type d'appareil inconnu" -#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:84 -#: modules/FlaskModule/API/user/UserQueryDisconnect.py:88 +#: modules/FlaskModule/API/service/ServiceQueryDisconnect.py:86 +#: modules/FlaskModule/API/user/UserQueryDisconnect.py:89 msgid "Success" msgstr "Succès" -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:57 -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:191 -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:252 -#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:203 -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:296 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:209 -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:63 -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:308 -#: modules/FlaskModule/API/user/UserQueryServiceSites.py:266 -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:270 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:271 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:60 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:200 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:261 +#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:208 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:301 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:215 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:64 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:314 +#: modules/FlaskModule/API/user/UserQueryServiceSites.py:272 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:276 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:277 msgid "Not found" msgstr "Non trouvé" -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:95 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:101 msgid "Missing participant_group" msgstr "Groupe participant manquant" -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:102 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:108 msgid "Missing id_participant_group" msgstr "ID groupe participant manquant" -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:107 -#: modules/FlaskModule/API/service/ServiceQueryProjects.py:93 -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:180 -#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:198 -#: modules/FlaskModule/API/user/UserQueryProjects.py:138 -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:179 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:113 +#: modules/FlaskModule/API/service/ServiceQueryProjects.py:99 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:183 +#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:201 +#: modules/FlaskModule/API/user/UserQueryProjects.py:142 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:183 msgid "Missing id_project" msgstr "Champ manquant : id_project" -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:114 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:120 msgid "Missing group name" msgstr "Nom du groupe manquant" -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:184 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:193 msgid "The id_participant_group given was not found" msgstr "L'ID du groupe participant n'a pu être trouvé" -#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:190 +#: modules/FlaskModule/API/service/ServiceQueryParticipantGroups.py:199 msgid "Deletion impossible: Participant group still has participant(s)" msgstr "Suppression impossible: le groupe participant a encore des participants" -#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:125 +#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:131 msgid "Unknown project" msgstr "Projet inconnu" -#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:128 +#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:134 msgid "Invalid participant name" msgstr "Nom de participant incorrect" -#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:131 +#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:137 msgid "Invalid participant email" msgstr "Courriel de participant incorrect" -#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:155 -#: modules/FlaskModule/API/user/UserQueryParticipants.py:311 +#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:161 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:315 msgid "Can't insert participant: participant's project is disabled or invalid." msgstr "" -"Impossible d'ajouter le participant: le projet du participant est " -"désactivé ou invalide." +"Impossible d'ajouter le participant: le projet du participant est désactivé ou " +"invalide." -#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:165 -#: modules/FlaskModule/API/user/UserQueryParticipants.py:284 +#: modules/FlaskModule/API/service/ServiceQueryParticipants.py:171 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:288 msgid "Can't update participant: participant's project is disabled." msgstr "" "Impossible de mettre à jour le participant: le projet du participant est " "désactivé." -#: modules/FlaskModule/API/service/ServiceQueryProjects.py:48 +#: modules/FlaskModule/API/service/ServiceQueryProjects.py:51 msgid "Missing parameter" msgstr "Paramètre manquant" -#: modules/FlaskModule/API/service/ServiceQueryProjects.py:87 -#: modules/FlaskModule/API/user/UserQueryProjects.py:132 +#: modules/FlaskModule/API/service/ServiceQueryProjects.py:93 +#: modules/FlaskModule/API/user/UserQueryProjects.py:136 msgid "Missing project" msgstr "Projet manquant" -#: modules/FlaskModule/API/service/ServiceQueryProjects.py:96 -#: modules/FlaskModule/API/user/UserQueryProjects.py:140 +#: modules/FlaskModule/API/service/ServiceQueryProjects.py:102 +#: modules/FlaskModule/API/user/UserQueryProjects.py:144 msgid "Missing id_site arguments" msgstr "Champ id_site manquant" -#: modules/FlaskModule/API/service/ServiceQueryProjects.py:169 -#: modules/FlaskModule/API/user/UserQueryProjects.py:275 +#: modules/FlaskModule/API/service/ServiceQueryProjects.py:178 +#: modules/FlaskModule/API/user/UserQueryProjects.py:281 msgid "" -"Can't delete project: please delete all participants with sessions before" -" deleting." +"Can't delete project: please delete all participants with sessions before " +"deleting." msgstr "" -"Impossible de supprimer le projet: veuillez supprimer tous les " -"participants ayant des séances au préalable." +"Impossible de supprimer le projet: veuillez supprimer tous les participants " +"ayant des séances au préalable." -#: modules/FlaskModule/API/service/ServiceQueryRoles.py:52 +#: modules/FlaskModule/API/service/ServiceQueryRoles.py:58 msgid "Missing service_role field" msgstr "Champ iservice_role manquant" -#: modules/FlaskModule/API/service/ServiceQueryRoles.py:58 -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:154 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:172 -#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:85 +#: modules/FlaskModule/API/service/ServiceQueryRoles.py:64 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:160 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:176 +#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:89 msgid "Missing id_service_role" msgstr "Champ id_service_role manquant" -#: modules/FlaskModule/API/service/ServiceQueryRoles.py:81 -#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:119 -#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:106 +#: modules/FlaskModule/API/service/ServiceQueryRoles.py:87 +#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:125 +#: modules/FlaskModule/API/user/UserQueryServiceRoles.py:110 msgid "Missing fields" msgstr "Champs manquants" -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:87 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:104 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:93 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:108 msgid "Missing service_access" msgstr "Accès Service manquant (service_access)" -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:91 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:108 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:97 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:112 msgid "Missing id_service_access" msgstr "Champ manquant: id_service_access" -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:95 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:112 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:101 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:116 msgid "Can't combine id_user_group, id_participant_group and id_device in request" msgstr "" -"Ne peut pas combiner id_user_group, id_participant et id_device dans la " -"requête" +"Ne peut pas combiner id_user_group, id_participant et id_device dans la requête" -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:114 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:132 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:120 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:136 msgid "Bad id_service_role" msgstr "Mauvais id_service_role" -#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:158 -#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:176 -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:192 +#: modules/FlaskModule/API/service/ServiceQueryServiceAccess.py:164 +#: modules/FlaskModule/API/user/UserQueryServiceAccess.py:180 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:195 msgid "Missing at least one id field" msgstr "Au moins un champ id manquant" -#: modules/FlaskModule/API/service/ServiceQueryServices.py:38 +#: modules/FlaskModule/API/service/ServiceQueryServices.py:41 msgid "Missing service key, id or uuid" msgstr "Clé, ID ou UUID de service manquant" -#: modules/FlaskModule/API/service/ServiceQuerySessionEvents.py:76 -#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:77 +#: modules/FlaskModule/API/service/ServiceQuerySessionEvents.py:81 +#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:80 msgid "Missing session_event field" msgstr "Champ session_event manquant" -#: modules/FlaskModule/API/service/ServiceQuerySessionEvents.py:82 -#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:83 +#: modules/FlaskModule/API/service/ServiceQuerySessionEvents.py:87 +#: modules/FlaskModule/API/user/UserQuerySessionEvents.py:86 msgid "Missing id_session or id_session_event fields" msgstr "Champs manquants: id_session ou id_session_event" -#: modules/FlaskModule/API/service/ServiceQuerySessions.py:95 +#: modules/FlaskModule/API/service/ServiceQuerySessions.py:98 msgid "Missing arguments: at least one id is required" msgstr "Au moins un champ id manquant" -#: modules/FlaskModule/API/service/ServiceQuerySessions.py:142 -#: modules/FlaskModule/API/user/UserQuerySessions.py:126 +#: modules/FlaskModule/API/service/ServiceQuerySessions.py:148 +#: modules/FlaskModule/API/user/UserQuerySessions.py:130 msgid "Missing id_session" msgstr "Champ id_session manquant" -#: modules/FlaskModule/API/service/ServiceQuerySessions.py:157 +#: modules/FlaskModule/API/service/ServiceQuerySessions.py:163 msgid "Service doesn't have access to at least one participant of that session." msgstr "Le service n'a pas accès à au moins un participant de la séance." -#: modules/FlaskModule/API/service/ServiceQuerySessions.py:164 +#: modules/FlaskModule/API/service/ServiceQuerySessions.py:170 msgid "Service doesn't have access to at least one user of that session." msgstr "Le service n'a pas accès à au moins un utilisateur de la séance." -#: modules/FlaskModule/API/service/ServiceQuerySessions.py:171 +#: modules/FlaskModule/API/service/ServiceQuerySessions.py:177 msgid "Service doesn't have access to at least one device of that session." msgstr "Le service n'a pas accès à au moins un appareil de la séance." -#: modules/FlaskModule/API/service/ServiceQuerySessions.py:189 -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:129 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:121 -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:101 +#: modules/FlaskModule/API/service/ServiceQuerySessions.py:195 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:133 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:125 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:105 msgid "Missing id_session_type" msgstr "Champ id_session_type manquant" -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:115 -#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:93 -#: modules/FlaskModule/API/user/UserQueryTestType.py:134 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:127 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:119 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:121 +#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:99 +#: modules/FlaskModule/API/user/UserQueryTestType.py:138 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:131 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:123 msgid "Missing id_test_type" msgstr "Champ id_test_type manquant" -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:117 -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:143 -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:144 -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:131 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:129 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:123 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:146 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:148 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:135 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:133 msgid "Missing projects" msgstr "Projets manquants" -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:129 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:141 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:135 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:145 msgid "Access denied to at least one project" msgstr "Accès refusé pour au moins un projet" -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:145 -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:176 -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:266 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:157 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:188 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:285 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:151 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:182 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:275 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:161 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:192 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:291 msgid "" -"Can't delete test type from project: please delete all tests of that type" -" in the project before deleting." +"Can't delete test type from project: please delete all tests of that type in the " +"project before deleting." msgstr "" -"Impossible de supprimer le type de test: veuillez supprimer tous les " -"tests de ce type au préalable." +"Impossible de supprimer le type de test: veuillez supprimer tous les tests de ce " +"type au préalable." -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:152 -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:161 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:164 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:158 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:165 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:168 msgid "Missing project ID" msgstr "ID de projet manquant" -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:154 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:166 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:152 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:160 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:170 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:156 msgid "Missing test types" msgstr "Types de test manquants" -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:185 -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:196 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:197 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:191 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:200 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:201 msgid "Unknown format" msgstr "Format inconnu" -#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:190 -#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:195 -#: modules/FlaskModule/API/user/UserQueryServiceSites.py:207 -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:201 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:200 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:202 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:190 +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:196 +#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:198 +#: modules/FlaskModule/API/user/UserQueryServiceSites.py:211 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:205 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:204 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:206 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:194 msgid "Badly formatted request" msgstr "Requête mal formée" -#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:87 -#: modules/FlaskModule/API/user/UserQueryTestType.py:128 +#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:93 +#: modules/FlaskModule/API/user/UserQueryTestType.py:132 msgid "Missing test_type" msgstr "Champ test_type manquant" -#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:155 +#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:164 msgid "Test type not related to this service. Can't delete." msgstr "Le type de test n’est pas associé au service. Impossible de supprimer." -#: modules/FlaskModule/API/service/ServiceQueryTests.py:131 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:137 msgid "Missing test field" msgstr "Champ \"test\" manquant" -#: modules/FlaskModule/API/service/ServiceQueryTests.py:137 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:143 msgid "Missing id_test field" msgstr "Champ id_test manquant" -#: modules/FlaskModule/API/service/ServiceQueryTests.py:154 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:160 msgid "Missing id_test_type field" msgstr "Champ id_test_type manquant" -#: modules/FlaskModule/API/service/ServiceQueryTests.py:159 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:165 msgid "Invalid test type" msgstr "Type de test invalide" -#: modules/FlaskModule/API/service/ServiceQueryTests.py:189 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:195 msgid "Service can't create tests for that session" msgstr "Le service ne peut pas créer de tests pour cette séance" -#: modules/FlaskModule/API/service/ServiceQueryTests.py:266 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:275 msgid "Service can't delete tests for that session" msgstr "Le service ne peut pas effacer les tests de cette séance" -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:81 -#: modules/FlaskModule/API/user/UserQueryUserGroups.py:132 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:86 +#: modules/FlaskModule/API/user/UserQueryUserGroups.py:135 msgid "Missing user_group" msgstr "Champ user_group manquant" -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:88 -#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:196 -#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:179 -#: modules/FlaskModule/API/user/UserQueryUserGroups.py:139 -#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:102 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:93 +#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:199 +#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:182 +#: modules/FlaskModule/API/user/UserQueryUserGroups.py:142 +#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:106 msgid "Missing id_user_group" msgstr "Champ id_user_group manquant" -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:100 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:105 msgid "Missing service role name or id_service_role" msgstr "Nom du rôle ou id_service_role manquant" -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:113 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:118 msgid "Can't set access to service other than self" msgstr "Impossible d'ajuster les accès à un service autre que soi-même" -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:119 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:124 msgid "No access for at a least one project in the list" msgstr "Aucun accès pour au moins un projet dans la liste" -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:125 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:130 msgid "No access for at a least one site in the list" msgstr "Aucun accès pour au moins un site dans la liste" -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:137 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:142 msgid "Bad role name for service" msgstr "Nom de rôle invalide pour le service" -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:155 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:160 msgid "A new usergroup must have at least one service access" msgstr "Un nouveau groupe utilisateur doit avoir au moins un accès à un service" -#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:217 -#: modules/FlaskModule/API/user/UserQueryUserGroups.py:302 +#: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:225 +#: modules/FlaskModule/API/user/UserQueryUserGroups.py:307 msgid "" -"Can't delete user group: please delete all users part of that user group " -"before deleting." +"Can't delete user group: please delete all users part of that user group before " +"deleting." msgstr "" -"Impossible de supprimer le groupe d'utilisateurs: veuillez retirer tous " -"les utilisateurs de ce groupe au préalable." +"Impossible de supprimer le groupe d'utilisateurs: veuillez retirer tous les " +"utilisateurs de ce groupe au préalable." -#: modules/FlaskModule/API/service/ServiceSessionManager.py:116 -#: modules/FlaskModule/API/user/UserSessionManager.py:107 +#: modules/FlaskModule/API/service/ServiceSessionManager.py:118 +#: modules/FlaskModule/API/user/UserSessionManager.py:108 msgid "Missing action" msgstr "Action manquante" -#: modules/FlaskModule/API/service/ServiceSessionManager.py:131 -#: modules/FlaskModule/API/user/UserSessionManager.py:121 -#: modules/FlaskModule/API/user/UserSessionManager.py:134 +#: modules/FlaskModule/API/service/ServiceSessionManager.py:133 +#: modules/FlaskModule/API/user/UserSessionManager.py:122 +#: modules/FlaskModule/API/user/UserSessionManager.py:135 msgid "Invalid session" msgstr "Séance invalide" -#: modules/FlaskModule/API/service/ServiceSessionManager.py:139 +#: modules/FlaskModule/API/service/ServiceSessionManager.py:141 msgid "Service doesn't have access to that session" msgstr "Le service n'a pas accès à cette séance" -#: modules/FlaskModule/API/service/ServiceSessionManager.py:160 -#: modules/FlaskModule/API/user/UserSessionManager.py:155 +#: modules/FlaskModule/API/service/ServiceSessionManager.py:162 +#: modules/FlaskModule/API/user/UserSessionManager.py:156 msgid "Missing required id_session_type for new sessions" msgstr "Champ id_session_type manquant pour les nouvelles séances" -#: modules/FlaskModule/API/service/ServiceSessionManager.py:166 +#: modules/FlaskModule/API/service/ServiceSessionManager.py:168 msgid "Invalid session type" msgstr "Type de séance invalide" -#: modules/FlaskModule/API/service/ServiceSessionManager.py:201 -#: modules/FlaskModule/API/user/UserSessionManager.py:192 +#: modules/FlaskModule/API/service/ServiceSessionManager.py:203 +#: modules/FlaskModule/API/user/UserSessionManager.py:193 msgid "Service not found" msgstr "Service non trouvé" -#: modules/FlaskModule/API/service/ServiceSessionManager.py:209 -#: modules/FlaskModule/API/user/UserSessionManager.py:197 +#: modules/FlaskModule/API/service/ServiceSessionManager.py:211 +#: modules/FlaskModule/API/user/UserSessionManager.py:198 msgid "Not implemented yet" msgstr "Non encore implémenté" -#: modules/FlaskModule/API/service/ServiceSessionManager.py:224 -#: modules/FlaskModule/API/user/UserSessionManager.py:213 +#: modules/FlaskModule/API/service/ServiceSessionManager.py:226 +#: modules/FlaskModule/API/user/UserSessionManager.py:214 msgid "No answer from service." msgstr "Aucune réponse du service." -#: modules/FlaskModule/API/user/UserLogin.py:82 +#: modules/FlaskModule/API/user/UserLogin.py:41 +msgid "Password change required for this user." +msgstr "Changement de mot de passe requis pour cet utilisateur." + +#: modules/FlaskModule/API/user/UserLogin.py:52 +msgid "2FA required for this user." +msgstr "2FA requise pour cet utilisateur." + +#: modules/FlaskModule/API/user/UserLogin.py:56 +msgid "2FA enabled but OTP not set for this user.Please setup 2FA." +msgstr "" +"Authentification multi-facteurs requise, mais non configurée pour cet " +"utilisateur. Veuillez configurer celle-ci." + +#: modules/FlaskModule/API/user/UserLogin.py:79 +#: modules/FlaskModule/API/user/UserLogin2FA.py:101 +#: modules/FlaskModule/API/user/UserLoginBase.py:154 +#: modules/FlaskModule/API/user/UserLoginSetup2FA.py:85 +#: modules/FlaskModule/API/user/UserLoginSetup2FA.py:155 +msgid "Client major version too old, not accepting login" +msgstr "La version du client est trop vieille (major), accès refusé" + +#: modules/FlaskModule/API/user/UserLogin2FA.py:49 +#: modules/FlaskModule/API/user/UserLoginSetup2FA.py:49 +#: modules/FlaskModule/API/user/UserLoginSetup2FA.py:119 +msgid "User does not have 2FA enabled" +msgstr "L'utilisation n'a pas la double authentification d'activée" + +#: modules/FlaskModule/API/user/UserLogin2FA.py:54 +msgid "User does not have 2FA OTP enabled or secret set" +msgstr "" +"L'utilisateur n'a pas la double authentification par OTP d'activée ou de " +"configurée" + +#: modules/FlaskModule/API/user/UserLogin2FA.py:74 +#: modules/FlaskModule/API/user/UserLoginSetup2FA.py:133 +msgid "Invalid OTP code" +msgstr "Code OTP invalide" + +#: modules/FlaskModule/API/user/UserLoginBase.py:90 +msgid "User already logged in :" +msgstr "L'utilisateur est déjà connecté :" + +#: modules/FlaskModule/API/user/UserLoginBase.py:92 msgid "User already logged in." msgstr "L'utilisateur est déjà connecté." -#: modules/FlaskModule/API/user/UserLogin.py:116 -#: modules/FlaskModule/API/user/UserLogin.py:134 +#: modules/FlaskModule/API/user/UserLoginBase.py:101 +msgid "Too many 2FA attempts. Please wait and try again." +msgstr "Trop d'essais d'authentification. Veuillez attendre et essayer à nouveau." + +#: modules/FlaskModule/API/user/UserLoginBase.py:134 +msgid "Client major version mismatch" +msgstr "La version majeure du client ne correspond pas" + +#: modules/FlaskModule/API/user/UserLoginBase.py:151 msgid "Client version mismatch" msgstr "La version du client ne correspond pas" -#: modules/FlaskModule/API/user/UserLogin.py:136 -msgid "Client major version too old, not accepting login" -msgstr "La version du client est trop vieille (major), accès refusé" +#: modules/FlaskModule/API/user/UserLoginBase.py:171 +msgid "Unknown client name :" +msgstr "Nom du client inconnu :" + +#: modules/FlaskModule/API/user/UserLoginChangePassword.py:34 +#: modules/FlaskModule/Views/LoginChangePasswordView.py:48 +msgid "New password and confirm password do not match" +msgstr "Le nouveau et l'ancien mot de passe ne correspondent pas" + +#: modules/FlaskModule/API/user/UserLoginChangePassword.py:37 +msgid "User not required to change password" +msgstr "L'utilisateur n'a pas à changer son mot de passe" + +#: modules/FlaskModule/API/user/UserLoginChangePassword.py:47 +#: modules/FlaskModule/API/user/UserQueryUsers.py:260 +msgid "New password same as old password" +msgstr "Le nouveau mot de passe est identique à l'ancien" -#: modules/FlaskModule/API/user/UserLogin.py:143 -msgid "Invalid client version handler" -msgstr "Mauvaise version du client" +#: modules/FlaskModule/API/user/UserLoginSetup2FA.py:53 +#: modules/FlaskModule/API/user/UserLoginSetup2FA.py:123 +msgid "User already has 2FA OTP secret set" +msgstr "L'utilisateur a déjà configuré la double authentification par OTP" -#: modules/FlaskModule/API/user/UserLogout.py:34 +#: modules/FlaskModule/API/user/UserLoginSetup2FA.py:146 +msgid "2FA enabled for this user." +msgstr "Double authentification activée pour cet utilisateur." + +#: modules/FlaskModule/API/user/UserLogout.py:36 msgid "User logged out." msgstr "Utilisateur déconnecté." -#: modules/FlaskModule/API/user/UserLogout.py:36 +#: modules/FlaskModule/API/user/UserLogout.py:38 msgid "User not logged in" msgstr "L'utilisateur n'est pas connecté" -#: modules/FlaskModule/API/user/UserQueryAssets.py:79 +#: modules/FlaskModule/API/user/UserQueryAssets.py:80 msgid "Service access denied" msgstr "Accès au service refusé" -#: modules/FlaskModule/API/user/UserQueryAssets.py:174 +#: modules/FlaskModule/API/user/UserQueryAssets.py:177 msgid "" -"Asset information update and creation must be done directly into a " -"service (such as Filetransfer service)" +"Asset information update and creation must be done directly into a service (such " +"as Filetransfer service)" msgstr "" -"La création et la mise à jour d'information sur les ressources doivent " -"être fait directement dans un service (comme le service de transfert de " -"fichiers - FileTransfer)" +"La création et la mise à jour d'information sur les ressources doivent être fait " +"directement dans un service (comme le service de transfert de fichiers - " +"FileTransfer)" -#: modules/FlaskModule/API/user/UserQueryAssets.py:182 +#: modules/FlaskModule/API/user/UserQueryAssets.py:187 msgid "" "Asset information deletion must be done directly into a service (such as " "Filetransfer service)" msgstr "" -"La suppression d'information sur les ressources doivent être fait " -"directement dans un service (comme le service de transfert de fichiers - " -"FileTransfer)" +"La suppression d'information sur les ressources doivent être fait directement " +"dans un service (comme le service de transfert de fichiers - FileTransfer)" -#: modules/FlaskModule/API/user/UserQueryAssetsArchive.py:89 +#: modules/FlaskModule/API/user/UserQueryAssetsArchive.py:88 msgid "Only one of the ID parameter is supported at once" -msgstr "" +msgstr "Un seul des paramètres d'ID est supporté à la fois" -#: modules/FlaskModule/API/user/UserQueryAssetsArchive.py:152 -#, fuzzy +#: modules/FlaskModule/API/user/UserQueryAssetsArchive.py:151 msgid "Missing required parameter" -msgstr "Paramètre manquant" +msgstr "Paramètre requis manquant" -#: modules/FlaskModule/API/user/UserQueryAssetsArchive.py:189 +#: modules/FlaskModule/API/user/UserQueryAssetsArchive.py:188 msgid "Unable to create archive information from FileTransferService" msgstr "" +"Impossible de créer les informations de l'archive dans le service de transfert " +"de fichiers" -#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:128 -#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:207 +#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:131 +#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:212 msgid "User is not admin of the participant's project" msgstr "L’utilisateur n’est pas administrateur du projet du participant" -#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:131 -#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:210 +#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:134 +#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:215 msgid "Access denied to device" msgstr "Aucun accès à l’appareil" -#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:149 +#: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:152 msgid "Device not assigned to project or participant" msgstr "Appareil non assigné à un projet ou un participant" -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:141 -#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:127 -#: modules/FlaskModule/API/user/UserQueryDevices.py:259 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:144 +#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:130 +#: modules/FlaskModule/API/user/UserQueryDevices.py:262 msgid "Missing id_device" msgstr "Champ manquant : id_device" -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:157 -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:165 -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:188 -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:232 -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:188 -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:243 -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:135 -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:168 -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:273 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:127 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:165 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:133 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:171 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:275 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:125 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:157 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:160 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:168 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:191 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:235 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:192 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:247 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:139 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:172 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:279 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:131 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:169 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:137 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:175 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:281 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:129 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:161 msgid "Access denied" msgstr "Accès refusé" -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:172 -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:208 -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:321 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:175 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:211 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:326 msgid "" -"Can't delete device from project. Please remove all participants " -"associated with the device or all sessions in the project referring to " -"the device before deleting." +"Can't delete device from project. Please remove all participants associated with " +"the device or all sessions in the project referring to the device before " +"deleting." msgstr "" "Impossible de retirer l'appareil du projet: veuillez retirer tous les " -"participants associés à cet appareil et/ou toutes les séances de ce " -"projet impliquant cet appareil." +"participants associés à cet appareil et/ou toutes les séances de ce projet " +"impliquant cet appareil." -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:182 -#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:157 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:185 +#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:160 msgid "Missing devices" msgstr "Appareils manquants" -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:238 -msgid "" -"At least one device is not part of the allowed device for that project " -"site" +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:241 +msgid "At least one device is not part of the allowed device for that project site" msgstr "Au moins un appareil n'est pas admissible pour ce projet pour ce site" -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:314 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:319 msgid "" -"Can't delete device from project: please remove all participants with " -"device before deleting." +"Can't delete device from project: please remove all participants with device " +"before deleting." msgstr "" -"Impossible de retirer l'appareil du projet: veuillez désassocier tous les" -" participants liés à cet appareil dans ce projet au préalable." +"Impossible de retirer l'appareil du projet: veuillez désassocier tous les " +"participants liés à cet appareil dans ce projet au préalable." -#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:317 +#: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:322 msgid "" -"Can't delete device from project: please remove all sessions in this " -"project referring to that device before deleting." +"Can't delete device from project: please remove all sessions in this project " +"referring to that device before deleting." msgstr "" -"Impossible de retirer l'appareil du projet: veuillez retirer toutes les " -"séances impliquant cet appareil dans ce projet au préalable." +"Impossible de retirer l'appareil du projet: veuillez retirer toutes les séances " +"impliquant cet appareil dans ce projet au préalable." -#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:129 -#: modules/FlaskModule/API/user/UserQueryServiceSites.py:136 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:123 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:121 +#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:132 +#: modules/FlaskModule/API/user/UserQueryServiceSites.py:140 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:127 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:125 msgid "Missing sites" msgstr "Sites manquants" -#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:148 -#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:175 +#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:151 +#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:178 msgid "" -"Can't delete device from site. Please remove all participants associated " -"with the device or all sessions in the site referring to the device " -"before deleting." +"Can't delete device from site. Please remove all participants associated with " +"the device or all sessions in the site referring to the device before deleting." msgstr "" -"Impossible de retirer l'appareil du site: veuillez retirer tous les " -"participants associés à cet appareil dans ce site et/ou toutes les " -"séances de ce site impliquant cet appareil." - -#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:155 -#: modules/FlaskModule/API/user/UserQueryServiceSites.py:164 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:158 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:150 +"Impossible de retirer l'appareil du site: veuillez retirer tous les participants " +"associés à cet appareil dans ce site et/ou toutes les séances de ce site " +"impliquant cet appareil." + +#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:158 +#: modules/FlaskModule/API/user/UserQueryServiceSites.py:168 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:162 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:154 msgid "Missing site ID" msgstr "Champ id_site manquant" -#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:252 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:263 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:252 +#: modules/FlaskModule/API/user/UserQueryDeviceSites.py:257 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:269 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:258 msgid "Bad parameter" msgstr "Mauvais Paramètre(s)" -#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:50 +#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:51 msgid "Too Many IDs" msgstr "Trop d'IDs" -#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:58 +#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:59 msgid "No access to device subtype" msgstr "Aucun accès au sous-type d'appareil" -#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:64 +#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:65 msgid "No access to device type" msgstr "Aucun accès au type d'appareil" -#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:102 +#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:105 msgid "Missing device_subtype" msgstr "Champ device_subtype manquant" -#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:109 +#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:112 msgid "Missing id_device_subtype" msgstr "Champ id_device_subtype manquant" -#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:121 +#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:124 msgid "Invalid device subtype" msgstr "Sous-type d’appareil invalide" -#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:169 +#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:174 msgid "Device subtype not found" msgstr "Sous-type d'appareil non trouvé" -#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:179 +#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:184 msgid "" -"Can't delete device subtype: please delete all devices of that subtype " -"before deleting." +"Can't delete device subtype: please delete all devices of that subtype before " +"deleting." msgstr "" -"Impossible de supprimer le sous-type d'appareil: veuillez supprimer ou " -"modifier tous les appareils utilisant ce sous-type au préalable." +"Impossible de supprimer le sous-type d'appareil: veuillez supprimer ou modifier " +"tous les appareils utilisant ce sous-type au préalable." -#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:189 +#: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:194 msgid "Device subtype successfully deleted" msgstr "Sous-type d'appareil supprimé avec succès" @@ -1158,744 +1257,756 @@ msgid "Unexisting ID/Forbidden access" msgstr "ID inexistant/Accès interdit" #: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:79 -#: modules/FlaskModule/API/user/UserQueryParticipants.py:399 -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:182 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:405 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:185 msgid "Database Error" msgstr "Erreur de bases de données" -#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:95 +#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:97 msgid "Missing device type" msgstr "Champ device_type manquant" -#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:102 +#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:104 msgid "Missing id_device_type" msgstr "Champ id_device_type manquant" -#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:113 +#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:115 msgid "Invalid device type" msgstr "Type d’appareil invalide" -#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:160 +#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:164 msgid "Tried to delete with 2 parameters" msgstr "Tentative de suppression avec 2 paramètres conflictuels" -#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:168 -#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:171 +#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:172 +#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:175 msgid "Device type not found" msgstr "Type d'appareil non trouvé" -#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:181 +#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:185 msgid "" -"Can't delete device type: please delete all associated devices before " -"deleting." +"Can't delete device type: please delete all associated devices before deleting." msgstr "" "Impossible de supprimer le type d'appareil: veuillez supprimer tous les " "appareils associés au préalable." -#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:193 +#: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:197 msgid "Device type successfully deleted" msgstr "Type d'appareil supprimé avec succès" -#: modules/FlaskModule/API/user/UserQueryDevices.py:130 +#: modules/FlaskModule/API/user/UserQueryDevices.py:131 msgid "Too many IDs" msgstr "Trop de paramètres ID" -#: modules/FlaskModule/API/user/UserQueryDevices.py:253 +#: modules/FlaskModule/API/user/UserQueryDevices.py:256 msgid "Missing device" msgstr "Appareil manquant" -#: modules/FlaskModule/API/user/UserQueryDevices.py:275 -#: modules/FlaskModule/API/user/UserQueryUserGroups.py:177 +#: modules/FlaskModule/API/user/UserQueryDevices.py:278 +#: modules/FlaskModule/API/user/UserQueryUserGroups.py:180 msgid "No site admin access for at a least one project in the list" msgstr "Aucun accès administrateur pour au moins un projet dans la liste" -#: modules/FlaskModule/API/user/UserQueryDevices.py:287 -#: modules/FlaskModule/API/user/UserQueryUserGroups.py:158 +#: modules/FlaskModule/API/user/UserQueryDevices.py:290 +#: modules/FlaskModule/API/user/UserQueryUserGroups.py:161 msgid "No site admin access for at a least one site in the list" msgstr "Aucun administrateur de site pour au moins un site dans la liste" -#: modules/FlaskModule/API/user/UserQueryDevices.py:412 -#: modules/FlaskModule/API/user/UserQueryUsers.py:343 +#: modules/FlaskModule/API/user/UserQueryDevices.py:417 +#: modules/FlaskModule/API/user/UserQueryUsers.py:351 msgid "Invalid id" msgstr "ID invalide" -#: modules/FlaskModule/API/user/UserQueryDevices.py:436 +#: modules/FlaskModule/API/user/UserQueryDevices.py:441 msgid "" -"Can't delete device: please delete all participants association before " -"deleting." +"Can't delete device: please delete all participants association before deleting." msgstr "" -"Impossible de supprimer l'appareil: veuillez supprimer ou retirer tous " -"les participants associés au préalable." +"Impossible de supprimer l'appareil: veuillez supprimer ou retirer tous les " +"participants associés au préalable." -#: modules/FlaskModule/API/user/UserQueryDevices.py:439 +#: modules/FlaskModule/API/user/UserQueryDevices.py:444 msgid "" -"Can't delete device: please remove all sessions referring to that device " -"before deleting." +"Can't delete device: please remove all sessions referring to that device before " +"deleting." msgstr "" "Impossible de supprimer l'appareil: veuillez retirer toutes les séances " "impliquant cet appareil." -#: modules/FlaskModule/API/user/UserQueryDevices.py:442 +#: modules/FlaskModule/API/user/UserQueryDevices.py:447 msgid "" -"Can't delete device: please remove all sessions created by that device " -"before deleting." +"Can't delete device: please remove all sessions created by that device before " +"deleting." msgstr "" -"Impossible de supprimer l'appareil: veuillez retirer toutes les séances " -"créées par cet appareil au préalable." +"Impossible de supprimer l'appareil: veuillez retirer toutes les séances créées " +"par cet appareil au préalable." -#: modules/FlaskModule/API/user/UserQueryDevices.py:445 +#: modules/FlaskModule/API/user/UserQueryDevices.py:450 msgid "" -"Can't delete device: please delete all assets created by that device " -"before deleting." +"Can't delete device: please delete all assets created by that device before " +"deleting." msgstr "" -"Impossible de supprimer l'appareil: veuillez supprimer toutes les " -"ressources crées par cet appareil au préalable." +"Impossible de supprimer l'appareil: veuillez supprimer toutes les ressources " +"crées par cet appareil au préalable." -#: modules/FlaskModule/API/user/UserQueryDevices.py:448 +#: modules/FlaskModule/API/user/UserQueryDevices.py:453 msgid "" -"Can't delete device: please delete all tests created by that device " -"before deleting." +"Can't delete device: please delete all tests created by that device before " +"deleting." msgstr "" -"Impossible de supprimer l'appareil: veuillez supprimer tous les tests " -"créés par cet appareil au préalable." +"Impossible de supprimer l'appareil: veuillez supprimer tous les tests créés par " +"cet appareil au préalable." -#: modules/FlaskModule/API/user/UserQueryDevices.py:451 +#: modules/FlaskModule/API/user/UserQueryDevices.py:456 msgid "" -"Can't delete device: please remove all related sessions, assets and tests" -" before deleting." +"Can't delete device: please remove all related sessions, assets and tests before " +"deleting." msgstr "" "Impossible de supprimer l'appareil: veuillez retirer toutes les séances, " "ressources et tests associés au préalable." -#: modules/FlaskModule/API/user/UserQueryDevices.py:469 +#: modules/FlaskModule/API/user/UserQueryDevices.py:474 msgid "Device successfully deleted" msgstr "Appareil supprimé avec succès" -#: modules/FlaskModule/API/user/UserQueryDisconnect.py:49 -#: modules/FlaskModule/API/user/UserQueryDisconnect.py:57 +#: modules/FlaskModule/API/user/UserQueryDisconnect.py:50 +#: modules/FlaskModule/API/user/UserQueryDisconnect.py:58 msgid "Use Logout instead to disconnect current user" msgstr "Utilisez l'API de Logout pour déconnecter l'utilisateur actuel" -#: modules/FlaskModule/API/user/UserQueryForms.py:82 +#: modules/FlaskModule/API/user/UserQueryForms.py:95 msgid "Missing type" msgstr "Champ type manquant" -#: modules/FlaskModule/API/user/UserQueryForms.py:137 +#: modules/FlaskModule/API/user/UserQueryForms.py:150 msgid "Missing session type id" msgstr "Champ id_session_type manquant" -#: modules/FlaskModule/API/user/UserQueryForms.py:148 +#: modules/FlaskModule/API/user/UserQueryForms.py:161 msgid "No reply from service while querying session type config" msgstr "" -"Aucune réponse du service lors de la requête de la configuration du type " -"de séance" +"Aucune réponse du service lors de la requête de la configuration du type de " +"séance" -#: modules/FlaskModule/API/user/UserQueryForms.py:182 +#: modules/FlaskModule/API/user/UserQueryForms.py:195 msgid "Invalid service specified" msgstr "Service non valide" -#: modules/FlaskModule/API/user/UserQueryForms.py:195 +#: modules/FlaskModule/API/user/UserQueryForms.py:208 msgid "Unknown form type: " msgstr "Formulaire inconnu: " #: modules/FlaskModule/API/user/UserQueryOnlineDevices.py:60 -#: modules/FlaskModule/API/user/UserQueryOnlineParticipants.py:69 -#: modules/FlaskModule/API/user/UserQueryOnlineUsers.py:57 +#: modules/FlaskModule/API/user/UserQueryOnlineParticipants.py:70 +#: modules/FlaskModule/API/user/UserQueryOnlineUsers.py:59 msgid "Internal server error when making RPC call." msgstr "Erreur interne du serveur pour RPC." -#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:93 +#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:96 msgid "Missing group" msgstr "Groupe manquant" -#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:102 +#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:105 msgid "Missing id_participant_group or id_project" msgstr "Champs id_participant_group ou id_project manquants" -#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:170 +#: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:175 msgid "" -"Can't delete participant group: please delete all sessions from all " -"participants before deleting." +"Can't delete participant group: please delete all sessions from all participants " +"before deleting." msgstr "" -"Impossible de supprimer le groupe: veuillez supprimer toutes les séances " -"de tous les participants du groupe au préalable." +"Impossible de supprimer le groupe: veuillez supprimer toutes les séances de tous " +"les participants du groupe au préalable." -#: modules/FlaskModule/API/user/UserQueryParticipants.py:219 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:223 msgid "Missing participant" msgstr "Participant manquant" -#: modules/FlaskModule/API/user/UserQueryParticipants.py:225 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:229 msgid "Missing id_participant" msgstr "ID Participant manquant" -#: modules/FlaskModule/API/user/UserQueryParticipants.py:229 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:233 msgid "Missing id_project or id_participant_group" msgstr "Champ manquants: id_participant, id_project ou id_participant_group" -#: modules/FlaskModule/API/user/UserQueryParticipants.py:244 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:248 msgid "No admin access to project" msgstr "Aucun accès administrateur à ce projet" -#: modules/FlaskModule/API/user/UserQueryParticipants.py:252 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:256 msgid "No admin access to group" msgstr "Aucun accès administrateur à ce groupe" -#: modules/FlaskModule/API/user/UserQueryParticipants.py:261 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:265 msgid "Participant group not found." msgstr "Le groupe participant n'existe pas." -#: modules/FlaskModule/API/user/UserQueryParticipants.py:265 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:269 msgid "Mismatch between id_project and group's project" msgstr "Aucune correspondance entre id_project et le projet" -#: modules/FlaskModule/API/user/UserQueryParticipants.py:382 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:388 msgid "Can't delete participant: please remove all related sessions beforehand." msgstr "" -"Impossible de supprimer le participant: veuillez supprimer toutes les " -"séances de ce participant au préalable." +"Impossible de supprimer le participant: veuillez supprimer toutes les séances de " +"ce participant au préalable." -#: modules/FlaskModule/API/user/UserQueryParticipants.py:384 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:390 msgid "" -"Can't delete participant: please remove all sessions created by this " -"participant beforehand." +"Can't delete participant: please remove all sessions created by this participant " +"beforehand." msgstr "" -"Impossible de supprimer le participant: veuillez supprimer toutes les " -"séances créées par ce participant au préalable." +"Impossible de supprimer le participant: veuillez supprimer toutes les séances " +"créées par ce participant au préalable." -#: modules/FlaskModule/API/user/UserQueryParticipants.py:387 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:393 msgid "Can't delete participant: please remove all related assets beforehand." msgstr "" -"Impossible de supprimer le participant: veuillez supprimer toutes les " -"ressources de ce participant au préalable." +"Impossible de supprimer le participant: veuillez supprimer toutes les ressources " +"de ce participant au préalable." -#: modules/FlaskModule/API/user/UserQueryParticipants.py:389 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:395 msgid "Can't delete participant: please remove all related tests beforehand." msgstr "" -"Impossible de supprimer le participant: veuillez supprimer tous les tests" -" associés à ce participant au préalable." +"Impossible de supprimer le participant: veuillez supprimer tous les tests " +"associés à ce participant au préalable." -#: modules/FlaskModule/API/user/UserQueryParticipants.py:391 +#: modules/FlaskModule/API/user/UserQueryParticipants.py:397 msgid "" -"Can't delete participant: please remove all related sessions, assets and " -"tests before deleting." +"Can't delete participant: please remove all related sessions, assets and tests " +"before deleting." msgstr "" -"Impossible de supprimer le participant: veuillez retirer toutes les " -"séances, ressources et tests associés à ce participant au préalable." +"Impossible de supprimer le participant: veuillez retirer toutes les séances, " +"ressources et tests associés à ce participant au préalable." -#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:200 -#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:183 +#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:203 +#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:186 msgid "Missing role name or id" msgstr "Nom du rôle ou ID manquant" -#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:231 +#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:234 msgid "Invalid role name or id for that project" msgstr "Nom du rôle ou ID invalide pour ce projet" -#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:270 +#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:275 msgid "No project access to delete." msgstr "Aucun accès au projet pour supprimer." -#: modules/FlaskModule/API/user/UserQueryProjects.py:169 +#: modules/FlaskModule/API/user/UserQueryProjects.py:173 msgid "No access to a session type for at least one of it" msgstr "Pas d'accès à ce type de session pour au moins un projet" -#: modules/FlaskModule/API/user/UserQueryProjects.py:181 +#: modules/FlaskModule/API/user/UserQueryProjects.py:185 msgid "At least one session type is not associated to the project site" msgstr "Au moins un type de séance n’est pas associé au site du projet" -#: modules/FlaskModule/API/user/UserQueryServiceAccessToken.py:35 -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:189 -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:142 -#: modules/FlaskModule/API/user/UserQueryServiceSites.py:134 -#: modules/FlaskModule/API/user/UserQueryServices.py:128 +#: modules/FlaskModule/API/user/UserQueryServiceAccessToken.py:37 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:192 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:146 +#: modules/FlaskModule/API/user/UserQueryServiceSites.py:138 +#: modules/FlaskModule/API/user/UserQueryServices.py:131 msgid "Missing id_service" msgstr "Champ id_service manquant" -#: modules/FlaskModule/API/user/UserQueryServiceAccessToken.py:38 +#: modules/FlaskModule/API/user/UserQueryServiceAccessToken.py:40 msgid "No access to specified service" msgstr "Aucun accès au service spécifié" -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:72 -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:146 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:73 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:149 msgid "Can't combine id_user, id_participant and id_device in request" msgstr "Ne peut pas combiner id_user, id_participant et id_device dans la requête" -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:121 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:124 msgid "Missing service_config" msgstr "Champ service_config manquant" -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:175 -#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:198 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:178 +#: modules/FlaskModule/API/user/UserQueryServiceConfigs.py:201 msgid "Invalid config format provided" msgstr "Le format de la configuration est invalide" -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:170 -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:211 -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:339 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:174 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:215 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:345 msgid "" -"Can't delete service-project: please remove all related sessions, assets " -"and tests before deleting." +"Can't delete service-project: please remove all related sessions, assets and " +"tests before deleting." msgstr "" -"Impossible de retirer ce service de ce projet: veuillez supprimer toutes " -"les séances, ressources et tests en lien avec ce service au préalable." +"Impossible de retirer ce service de ce projet: veuillez supprimer toutes les " +"séances, ressources et tests en lien avec ce service au préalable." -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:181 -#: modules/FlaskModule/API/user/UserQueryServiceSites.py:166 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:185 +#: modules/FlaskModule/API/user/UserQueryServiceSites.py:170 msgid "Missing services" msgstr "Services manquants" -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:249 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:253 msgid "" -"At least one service is not part of the allowed service for that project " -"site" +"At least one service is not part of the allowed service for that project site" msgstr "Au moins un service n'est pas permis pour ce projet pour ce site" -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:311 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:317 msgid "Operation not completed" msgstr "Opération non complétée" -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:332 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:338 msgid "" -"Can't delete service-project: please remove all sessions involving a " -"session type using this project beforehand." +"Can't delete service-project: please remove all sessions involving a session " +"type using this project beforehand." msgstr "" -"Impossible de retirer ce service de ce projet: veuillez supprimer toutes " -"les séances impliquant un type de séance en lien avec ce service au " -"préalable." +"Impossible de retirer ce service de ce projet: veuillez supprimer toutes les " +"séances impliquant un type de séance en lien avec ce service au préalable." -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:335 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:341 msgid "Can't delete service-project: please remove all related assets beforehand." msgstr "" -"Impossible de retirer ce service de ce projet: veuillez supprimer toutes " -"les ressources associées au préalable." +"Impossible de retirer ce service de ce projet: veuillez supprimer toutes les " +"ressources associées au préalable." -#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:337 +#: modules/FlaskModule/API/user/UserQueryServiceProjects.py:343 msgid "Can't delete service-project: please remove all related tests beforehand." msgstr "" -"Impossible de retirer ce service de ce projet: veuillez supprimer tous " -"les tests associés à ce service au préalable." +"Impossible de retirer ce service de ce projet: veuillez supprimer tous les tests " +"associés à ce service au préalable." -#: modules/FlaskModule/API/user/UserQueryServiceSites.py:155 -#: modules/FlaskModule/API/user/UserQueryServiceSites.py:185 -#: modules/FlaskModule/API/user/UserQueryServiceSites.py:278 +#: modules/FlaskModule/API/user/UserQueryServiceSites.py:159 +#: modules/FlaskModule/API/user/UserQueryServiceSites.py:189 +#: modules/FlaskModule/API/user/UserQueryServiceSites.py:284 msgid "" -"Can't delete service from site: please delete all sessions, assets and " -"tests related to that service beforehand." +"Can't delete service from site: please delete all sessions, assets and tests " +"related to that service beforehand." msgstr "" -"Impossible de retirer le service de ce site: veuillez supprimer toutes " -"les séances, ressources et tests reliés à ce service dans ce site au " -"préalable." +"Impossible de retirer le service de ce site: veuillez supprimer toutes les " +"séances, ressources et tests reliés à ce service dans ce site au préalable." -#: modules/FlaskModule/API/user/UserQueryServices.py:150 +#: modules/FlaskModule/API/user/UserQueryServices.py:153 msgid "OpenTera service can't be updated using this API" msgstr "Les services ne peuvent pas être mis-à-jour par cet API" -#: modules/FlaskModule/API/user/UserQueryServices.py:164 -#: modules/FlaskModule/API/user/UserQueryServices.py:184 +#: modules/FlaskModule/API/user/UserQueryServices.py:167 +#: modules/FlaskModule/API/user/UserQueryServices.py:187 msgid "Invalid config json schema" msgstr "Schéma JSON invalide pour la configuration" -#: modules/FlaskModule/API/user/UserQueryServices.py:168 +#: modules/FlaskModule/API/user/UserQueryServices.py:171 msgid "Missing service_key" msgstr "Clé du service manquante" -#: modules/FlaskModule/API/user/UserQueryServices.py:236 +#: modules/FlaskModule/API/user/UserQueryServices.py:241 msgid "Invalid service" msgstr "Service invalide" -#: modules/FlaskModule/API/user/UserQueryServices.py:251 +#: modules/FlaskModule/API/user/UserQueryServices.py:256 msgid "" -"Can't delete service: please delete all sessions, assets and tests " -"related to that service beforehand." +"Can't delete service: please delete all sessions, assets and tests related to " +"that service beforehand." msgstr "" -"Impossible de supprimer le service: veuillez supprimer toutes les " -"séances, ressources et tests reliés à ce service au préalable." +"Impossible de supprimer le service: veuillez supprimer toutes les séances, " +"ressources et tests reliés à ce service au préalable." -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:154 -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:186 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:158 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:190 msgid "" -"Can't delete session type from project: please delete all sessions using " -"that type in that project before deleting." +"Can't delete session type from project: please delete all sessions using that " +"type in that project before deleting." msgstr "" -"Impossible de retirer le type de séance du projet: veuillez supprimer " -"toutes les séances de ce type dans ce projet avant de supprimer." +"Impossible de retirer le type de séance du projet: veuillez supprimer toutes les " +"séances de ce type dans ce projet avant de supprimer." -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:163 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:160 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:167 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:164 msgid "Missing session types" msgstr "Champ session_type manquant" -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:212 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:216 msgid "At least one session type is not associated to the site of its project" msgstr "Au moins un type de session n'est pas associé au site de ce projet" -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:283 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:289 msgid "" -"Can't delete session type from project: please delete all sessions of " -"that type in the project before deleting." +"Can't delete session type from project: please delete all sessions of that type " +"in the project before deleting." msgstr "" -"Impossible de retirer le type de séance du projet: veuillez supprimer " -"toutes les séances de ce type dans ce projet avant de supprimer." +"Impossible de retirer le type de séance du projet: veuillez supprimer toutes les " +"séances de ce type dans ce projet avant de supprimer." -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:150 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:185 -#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:278 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:154 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:189 +#: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:284 msgid "" -"Can't delete session type from site: please delete all sessions of that " -"type in the site before deleting." +"Can't delete session type from site: please delete all sessions of that type in " +"the site before deleting." msgstr "" -"Impossible de retirer le type de séance du site: veuillez supprimer " -"toutes les séances de ce type dans ce site au préalable." +"Impossible de retirer le type de séance du site: veuillez supprimer toutes les " +"séances de ce type dans ce site au préalable." -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:95 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:99 msgid "Missing session_type" msgstr "Champ session_type manquant" -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:111 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:115 msgid "Missing site(s) to associate that session type to" msgstr "Site(s) manquant(s) pour l'association avec un type de session" -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:136 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:140 msgid "Missing id_service for session type of type service" msgstr "Champ id_service pour la session manquant" -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:151 -#: modules/FlaskModule/API/user/UserQueryTestType.py:183 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:155 +#: modules/FlaskModule/API/user/UserQueryTestType.py:187 msgid "No site admin access for at least one site in the list" msgstr "Aucun accès administrateur de site pour au moins un site dans la liste" -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:161 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:165 msgid "At least one site isn't associated with the service of that session type" msgstr "Au moins un site n'est pas associé avec ce type de session pour ce service" -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:178 -#: modules/FlaskModule/API/user/UserQueryTestType.py:206 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:182 +#: modules/FlaskModule/API/user/UserQueryTestType.py:210 msgid "No project admin access for at a least one project in the list" msgstr "Pas administrateur pour au moins un projet da la liste" -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:264 -#: modules/FlaskModule/API/user/UserQueryTestType.py:286 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:268 +#: modules/FlaskModule/API/user/UserQueryTestType.py:290 msgid "Session type not associated to project site" msgstr "Type de séance non associé au site du projet" -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:292 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:296 msgid "Session type has a a service not associated to its site" msgstr "" -"Tentative d'association avec un type de séance qui a un service non " -"associé à son site" +"Tentative d'association avec un type de séance qui a un service non associé à " +"son site" -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:316 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:323 msgid "Cannot delete because you are not admin in all projects." msgstr "" -"Impossible de supprimer: vous n'êtes pas administrateur dans tous les " -"projets." +"Impossible de supprimer: vous n'êtes pas administrateur dans tous les projets." -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:321 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:328 msgid "Unable to delete - not admin in at least one project" msgstr "Impossible de supprimer - pas administrateur dans au moins un projet" -#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:334 +#: modules/FlaskModule/API/user/UserQuerySessionTypes.py:341 msgid "" -"Can't delete session type: please delete all sessions with that type " -"before deleting." +"Can't delete session type: please delete all sessions with that type before " +"deleting." msgstr "" -"Impossible de supprimer le type de séance: veuillez supprimer toutes les " -"séances de ce type au préalable." +"Impossible de supprimer le type de séance: veuillez supprimer toutes les séances " +"de ce type au préalable." -#: modules/FlaskModule/API/user/UserQuerySessions.py:135 +#: modules/FlaskModule/API/user/UserQuerySessions.py:139 msgid "Missing session participants and users" msgstr "Usagers ou participants manquants pour la session" -#: modules/FlaskModule/API/user/UserQuerySessions.py:141 +#: modules/FlaskModule/API/user/UserQuerySessions.py:145 msgid "No access to session." msgstr "Aucun accès à la séance." -#: modules/FlaskModule/API/user/UserQuerySessions.py:156 -#: modules/FlaskModule/API/user/UserQuerySessions.py:247 +#: modules/FlaskModule/API/user/UserQuerySessions.py:160 +#: modules/FlaskModule/API/user/UserQuerySessions.py:253 msgid "User doesn't have access to at least one participant of that session." msgstr "L'utilisateur n'a pas accès à au moins un participant de la séance." -#: modules/FlaskModule/API/user/UserQuerySessions.py:161 -#: modules/FlaskModule/API/user/UserQuerySessions.py:252 +#: modules/FlaskModule/API/user/UserQuerySessions.py:165 +#: modules/FlaskModule/API/user/UserQuerySessions.py:258 msgid "User doesn't have access to at least one user of that session." msgstr "L'utilisateur n'a pas accès à au moins un utilisateur de la séance." -#: modules/FlaskModule/API/user/UserQuerySessions.py:166 -#: modules/FlaskModule/API/user/UserQuerySessions.py:257 +#: modules/FlaskModule/API/user/UserQuerySessions.py:170 +#: modules/FlaskModule/API/user/UserQuerySessions.py:263 msgid "User doesn't have access to at least one device of that session." msgstr "L'utilisateur n'a pas accès à au moins un appareil de la séance." -#: modules/FlaskModule/API/user/UserQuerySessions.py:261 +#: modules/FlaskModule/API/user/UserQuerySessions.py:267 msgid "Session is in progress: can't delete that session." msgstr "La séance est en cours: impossible de supprimer." -#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:181 +#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:184 msgid "Missing id_site" msgstr "Champ id_site manquant" -#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:230 +#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:233 msgid "Invalid role name or id for that site" msgstr "Nom de rôle ou id invalide(s) pour ce site" -#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:271 +#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:276 msgid "No site access to delete" msgstr "Pas d'accès à effacer" -#: modules/FlaskModule/API/user/UserQuerySites.py:112 +#: modules/FlaskModule/API/user/UserQuerySites.py:116 msgid "Missing site" msgstr "Site manquant" -#: modules/FlaskModule/API/user/UserQuerySites.py:119 +#: modules/FlaskModule/API/user/UserQuerySites.py:123 msgid "Missing id_site field" msgstr "Champ id_site manquant" -#: modules/FlaskModule/API/user/UserQuerySites.py:187 +#: modules/FlaskModule/API/user/UserQuerySites.py:194 msgid "" -"Can't delete site: please delete all participants with sessions before " -"deleting." +"Can't delete site: please delete all participants with sessions before deleting." msgstr "" -"Impossible de supprimer le site: veuillez supprimer tous les participants" -" au préalable." +"Impossible de supprimer le site: veuillez supprimer tous les participants au " +"préalable." -#: modules/FlaskModule/API/user/UserQueryStats.py:94 +#: modules/FlaskModule/API/user/UserQueryStats.py:96 msgid "Missing id argument" msgstr "Champ id manquant" -#: modules/FlaskModule/API/user/UserQueryTestType.py:151 +#: modules/FlaskModule/API/user/UserQueryTestType.py:155 msgid "Missing project(s) to associate that test type to" msgstr "Projet(s) manquant(s) pour l'association avec ce type de test" -#: modules/FlaskModule/API/user/UserQueryTestType.py:192 +#: modules/FlaskModule/API/user/UserQueryTestType.py:196 msgid "At least one site isn't associated with the service of that test type" msgstr "Au moins un site n'est pas associé avec le service de ce type de test" -#: modules/FlaskModule/API/user/UserQueryTestType.py:306 +#: modules/FlaskModule/API/user/UserQueryTestType.py:310 msgid "Test type has a a service not associated to its site" msgstr "" -"Tentative d'association avec un type de test qui a un service non associé" -" à son site" +"Tentative d'association avec un type de test qui a un service non associé à son " +"site" -#: modules/FlaskModule/API/user/UserQueryTestType.py:328 +#: modules/FlaskModule/API/user/UserQueryTestType.py:334 msgid "Unable to delete - not admin in the related test type service" msgstr "" -"Impossible de supprimer - pas administrateur pour le type de test du " -"service" +"Impossible de supprimer - pas administrateur pour le type de test du service" -#: modules/FlaskModule/API/user/UserQueryTestType.py:338 +#: modules/FlaskModule/API/user/UserQueryTestType.py:344 msgid "" -"Can't delete test type: please delete all tests of that type before " -"deleting." +"Can't delete test type: please delete all tests of that type before deleting." msgstr "" -"Impossible de supprimer le type de test: veuillez supprimer tous les " -"tests de ce type au préalable." +"Impossible de supprimer le type de test: veuillez supprimer tous les tests de ce " +"type au préalable." -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:213 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:217 msgid "At least one test type is not associated to the site of its project" msgstr "Au moins un type de test n'est pas associé au site de ce projet" -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:143 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:176 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:266 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:147 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:180 +#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:272 msgid "" -"Can't delete test type from site: please delete all tests of that type in" -" the site before deleting." +"Can't delete test type from site: please delete all tests of that type in the " +"site before deleting." msgstr "" -"Impossible de retirer ce type de test du site: veuillez supprimer tous " -"les tests de ce type dans ce site au préalable." +"Impossible de retirer ce type de test du site: veuillez supprimer tous les tests " +"de ce type dans ce site au préalable." -#: modules/FlaskModule/API/user/UserQueryTests.py:123 +#: modules/FlaskModule/API/user/UserQueryTests.py:127 msgid "" -"Test information update and creation must be done directly into a service" -" (such as Test service)" +"Test information update and creation must be done directly into a service (such " +"as Test service)" msgstr "" -"La création et la mise à jour d'information sur les tests doivent être " -"fait directement dans un service" +"La création et la mise à jour d'information sur les tests doivent être fait " +"directement dans un service" -#: modules/FlaskModule/API/user/UserQueryUndelete.py:30 +#: modules/FlaskModule/API/user/UserQueryUndelete.py:32 msgid "No access to this API" msgstr "Aucun accès à cet API" -#: modules/FlaskModule/API/user/UserQueryUndelete.py:46 +#: modules/FlaskModule/API/user/UserQueryUndelete.py:48 msgid "Item to undelete not found" msgstr "L'élément à restaurer est introuvable" -#: modules/FlaskModule/API/user/UserQueryUndelete.py:49 +#: modules/FlaskModule/API/user/UserQueryUndelete.py:51 msgid "Item can't be undeleted" msgstr "L'élément ne peut pas être restauré" -#: modules/FlaskModule/API/user/UserQueryUndelete.py:52 +#: modules/FlaskModule/API/user/UserQueryUndelete.py:54 msgid "Item isn't deleted" msgstr "L'élément n'est pas supprimé" -#: modules/FlaskModule/API/user/UserQueryUserGroups.py:142 +#: modules/FlaskModule/API/user/UserQueryUserGroups.py:145 msgid "Missing user group name" msgstr "Nom du groupe utilisateur manquant" -#: modules/FlaskModule/API/user/UserQueryUserPreferences.py:89 +#: modules/FlaskModule/API/user/UserQueryUserPreferences.py:92 msgid "Missing app tag" msgstr "Champ App Tag manquant" -#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:57 +#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:59 msgid "At least one id must be specified" msgstr "Au moins un ID doit être spécifié" -#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:89 +#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:93 msgid "Missing user user group" msgstr "Champ user_user_group manquant" -#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:100 -#: modules/FlaskModule/API/user/UserQueryUsers.py:183 +#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:104 +#: modules/FlaskModule/API/user/UserQueryUsers.py:198 msgid "Missing id_user" msgstr "Champ id_user manquant" -#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:106 +#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:110 msgid "No access to specified user" msgstr "Aucun accès à l'utilisateur" -#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:108 +#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:112 msgid "No access to specified user group" msgstr "Aucun accès au groupe utilisateurs" -#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:113 +#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:117 msgid "Super admins can't be associated to an user group" msgstr "" -"Les super administrateurs ne peuvent pas être associés à un groupe " -"d'utilisateurs" +"Les super administrateurs ne peuvent pas être associés à un groupe d'utilisateurs" -#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:158 +#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:164 msgid "Can't delete specified relationship" msgstr "Impossible de supprimer cette relation" -#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:161 +#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:167 msgid "No access to relationship's user" msgstr "Aucun accès à l'utilisateur" -#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:163 +#: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:169 msgid "No access to relationship's user group" msgstr "Aucun accès au groupe" -#: modules/FlaskModule/API/user/UserQueryUsers.py:176 +#: modules/FlaskModule/API/user/UserQueryUsers.py:57 +msgid "Password not long enough" +msgstr "Mot de passe pas assez long" + +#: modules/FlaskModule/API/user/UserQueryUsers.py:191 msgid "Missing user" msgstr "Champ user manquant" -#: modules/FlaskModule/API/user/UserQueryUsers.py:198 +#: modules/FlaskModule/API/user/UserQueryUsers.py:213 msgid "No access for at a least one user group in the list" msgstr "Aucun accès pour au moins un groupe d'utilisateurs dans la liste" -#: modules/FlaskModule/API/user/UserQueryUsers.py:229 +#: modules/FlaskModule/API/user/UserQueryUsers.py:244 msgid "Username can't be modified" msgstr "Le code utilisateur ne peut pas être modifié" -#: modules/FlaskModule/API/user/UserQueryUsers.py:250 +#: modules/FlaskModule/API/user/UserQueryUsers.py:257 +#: modules/FlaskModule/API/user/UserQueryUsers.py:295 +msgid "Password not strong enough" +msgstr "Mot de passe insécure" + +#: modules/FlaskModule/API/user/UserQueryUsers.py:271 msgid "Missing required fields: " msgstr "Champs manquants: " -#: modules/FlaskModule/API/user/UserQueryUsers.py:253 +#: modules/FlaskModule/API/user/UserQueryUsers.py:274 msgid "Invalid password" msgstr "Mot de passe incorrect" -#: modules/FlaskModule/API/user/UserQueryUsers.py:257 +#: modules/FlaskModule/API/user/UserQueryUsers.py:278 msgid "Username unavailable." msgstr "Nom d'utilisateur non disponible." -#: modules/FlaskModule/API/user/UserQueryUsers.py:336 +#: modules/FlaskModule/API/user/UserQueryUsers.py:344 msgid "Sorry, you can't delete yourself!" msgstr "Désolé, vous ne pouvez pas vous supprimer!" -#: modules/FlaskModule/API/user/UserQueryUsers.py:366 +#: modules/FlaskModule/API/user/UserQueryUsers.py:374 msgid "" -"Can't delete user: please remove all sessions that this user is part of " -"before deleting." +"Can't delete user: please remove all sessions that this user is part of before " +"deleting." msgstr "" -"Impossible de supprimer l'utilisateur: veuillez supprimer toutes les " -"séances dont cet utilisateur fait partie au préalable." +"Impossible de supprimer l'utilisateur: veuillez supprimer toutes les séances " +"dont cet utilisateur fait partie au préalable." -#: modules/FlaskModule/API/user/UserQueryUsers.py:369 +#: modules/FlaskModule/API/user/UserQueryUsers.py:377 msgid "" -"Can't delete user: please remove all sessions created by this user before" -" deleting." +"Can't delete user: please remove all sessions created by this user before " +"deleting." msgstr "" -"Impossible de supprimer l'utilisateur: veuillez supprimer toutes les " -"séances créées par cet utilisateur au préalable." +"Impossible de supprimer l'utilisateur: veuillez supprimer toutes les séances " +"créées par cet utilisateur au préalable." -#: modules/FlaskModule/API/user/UserQueryUsers.py:372 +#: modules/FlaskModule/API/user/UserQueryUsers.py:380 msgid "" -"Can't delete user: please remove all tests created by this user before " -"deleting." +"Can't delete user: please remove all tests created by this user before deleting." msgstr "" -"Impossible de supprimer l'utilisateur: veuillez supprimer tous les tests " -"créés par cet utilisateur au préalable." +"Impossible de supprimer l'utilisateur: veuillez supprimer tous les tests créés " +"par cet utilisateur au préalable." -#: modules/FlaskModule/API/user/UserQueryUsers.py:375 +#: modules/FlaskModule/API/user/UserQueryUsers.py:383 msgid "" -"Can't delete user: please remove all assets created by this user before " -"deleting." +"Can't delete user: please remove all assets created by this user before deleting." msgstr "" -"Impossible de supprimer l'utilisateur: veuillez supprimer toutes les " -"ressources créées par cet utilisateur au préalable." +"Impossible de supprimer l'utilisateur: veuillez supprimer toutes les ressources " +"créées par cet utilisateur au préalable." -#: modules/FlaskModule/API/user/UserQueryUsers.py:378 +#: modules/FlaskModule/API/user/UserQueryUsers.py:386 msgid "" -"Can't delete user: please delete all assets created by this user before " -"deleting." +"Can't delete user: please delete all assets created by this user before deleting." msgstr "" -"Impossible de supprimer l'utilisateur: veuillez supprimer toutes les " -"ressources créées par cet utilisateur au préalable." +"Impossible de supprimer l'utilisateur: veuillez supprimer toutes les ressources " +"créées par cet utilisateur au préalable." -#: modules/FlaskModule/API/user/UserQueryVersions.py:40 +#: modules/FlaskModule/API/user/UserQueryVersions.py:42 msgid "No version information found" msgstr "Aucune information de version disponible" -#: modules/FlaskModule/API/user/UserQueryVersions.py:71 +#: modules/FlaskModule/API/user/UserQueryVersions.py:75 msgid "Wrong ClientVersions" msgstr "Mauvais version du client (ClientVersions)" -#: modules/FlaskModule/API/user/UserQueryVersions.py:77 +#: modules/FlaskModule/API/user/UserQueryVersions.py:81 msgid "Not authorized" msgstr "Non autorisé" -#: modules/FlaskModule/API/user/UserSessionManager.py:129 +#: modules/FlaskModule/API/user/UserSessionManager.py:130 msgid "User doesn't have access to that session" msgstr "L'utilisateur n'a pas accès à cette session" -#: modules/FlaskModule/API/user/UserSessionManager.py:160 +#: modules/FlaskModule/API/user/UserSessionManager.py:161 msgid "User doesn't have access to that service." msgstr "L'utilisateur n'a pas accès à ce service." -#: modules/FlaskModule/API/user/UserSessionManager.py:165 +#: modules/FlaskModule/API/user/UserSessionManager.py:166 msgid "Missing parameters" msgstr "Paramètres manquants" -#: modules/FlaskModule/API/user/UserSessionManager.py:168 +#: modules/FlaskModule/API/user/UserSessionManager.py:169 msgid "Missing reply code in parameters" msgstr "Manque le reply code dans les paramètres" -#: modules/FlaskModule/API/user/UserSessionManager.py:181 -#: modules/FlaskModule/API/user/UserSessionManager.py:184 +#: modules/FlaskModule/API/user/UserSessionManager.py:182 +#: modules/FlaskModule/API/user/UserSessionManager.py:185 msgid "Invalid reply code" msgstr "Le champ reply code est invalide" -#: modules/LoginModule/LoginModule.py:619 -#: modules/LoginModule/LoginModule.py:652 +#: modules/FlaskModule/Views/LoginChangePasswordView.py:38 +msgid "Missing information" +msgstr "Information manquante" + +#: modules/FlaskModule/Views/LoginChangePasswordView.py:53 +msgid "New password must be different from current" +msgstr "Le nouveau mot de passe doit être différent de l'actuel" + +#: modules/LoginModule/LoginModule.py:219 +msgid "Unauthorized - User must login first to change password" +msgstr "" +"Non-autorisé - L'utilisateur doit se connecter pour changer son mot de passe" + +#: modules/LoginModule/LoginModule.py:222 +msgid "Unauthorized - 2FA is enabled, must login first and use token" +msgstr "" +"Non-autorisé - Authentification multi-facteurs activée, doit se connecter et " +"utiliser un jeton" + +#: modules/LoginModule/LoginModule.py:633 modules/LoginModule/LoginModule.py:666 msgid "Disabled device" msgstr "Appareil désactivé" -#: modules/LoginModule/LoginModule.py:629 +#: modules/LoginModule/LoginModule.py:643 msgid "Invalid token" msgstr "Jeton invalide" -#: modules/LoginModule/LoginModule.py:729 +#: modules/LoginModule/LoginModule.py:743 msgid "Invalid Token" msgstr "Jeton invalide" -#: opentera/db/models/TeraSessionType.py:151 -#: opentera/forms/TeraSessionForm.py:105 +#: opentera/db/models/TeraSessionType.py:151 opentera/forms/TeraSessionForm.py:105 msgid "Unknown" msgstr "Inconnue" #: opentera/db/models/TeraSessionType.py:153 -#: opentera/forms/TeraSessionTypeForm.py:35 -#: opentera/forms/TeraTestTypeForm.py:24 +#: opentera/forms/TeraSessionTypeForm.py:35 opentera/forms/TeraTestTypeForm.py:24 msgid "Service" msgstr "Service" @@ -1915,8 +2026,7 @@ msgstr "Protocole" msgid "Parameters" msgstr "Paramètres" -#: opentera/forms/TeraDeviceForm.py:28 -#: opentera/forms/TeraServiceConfigForm.py:28 +#: opentera/forms/TeraDeviceForm.py:28 opentera/forms/TeraServiceConfigForm.py:28 msgid "Device ID" msgstr "ID Appareil" @@ -1928,8 +2038,7 @@ msgstr "UUID Appareil" msgid "Device Name" msgstr "Nom Appareil" -#: opentera/forms/TeraDeviceForm.py:31 -#: opentera/forms/TeraDeviceSubTypeForm.py:32 +#: opentera/forms/TeraDeviceForm.py:31 opentera/forms/TeraDeviceSubTypeForm.py:32 #: opentera/forms/TeraDeviceTypeForm.py:21 msgid "Device Type ID" msgstr "Type d'appareil" @@ -1955,20 +2064,17 @@ msgid "Device Onlineable?" msgstr "Se met en ligne?" #: opentera/forms/TeraDeviceForm.py:44 opentera/forms/TeraParticipantForm.py:50 -#: opentera/forms/TeraUserForm.py:30 +#: opentera/forms/TeraUserForm.py:46 msgid "Last Connection" msgstr "Dernière connexion" -#: opentera/forms/TeraDeviceForm.py:47 -#: opentera/forms/TeraDeviceSubTypeForm.py:24 -#: opentera/forms/TeraDeviceTypeForm.py:17 -#: opentera/forms/TeraParticipantForm.py:24 +#: opentera/forms/TeraDeviceForm.py:47 opentera/forms/TeraDeviceSubTypeForm.py:24 +#: opentera/forms/TeraDeviceTypeForm.py:17 opentera/forms/TeraParticipantForm.py:24 #: opentera/forms/TeraParticipantGroupForm.py:18 -#: opentera/forms/TeraProjectForm.py:18 -#: opentera/forms/TeraServiceConfigForm.py:18 +#: opentera/forms/TeraProjectForm.py:18 opentera/forms/TeraServiceConfigForm.py:18 #: opentera/forms/TeraServiceForm.py:18 opentera/forms/TeraSessionForm.py:120 -#: opentera/forms/TeraSessionTypeForm.py:25 opentera/forms/TeraSiteForm.py:12 -#: opentera/forms/TeraUserForm.py:13 opentera/forms/TeraUserGroupForm.py:18 +#: opentera/forms/TeraSessionTypeForm.py:25 opentera/forms/TeraSiteForm.py:13 +#: opentera/forms/TeraUserForm.py:14 opentera/forms/TeraUserGroupForm.py:18 #: opentera/forms/TeraVersionsForm.py:18 msgid "Information" msgstr "Information" @@ -1986,7 +2092,7 @@ msgid "Device Configuration" msgstr "Configuration de l'appareil" #: opentera/forms/TeraDeviceForm.py:55 opentera/forms/TeraDeviceForm.py:57 -#: opentera/forms/TeraUserForm.py:28 +#: opentera/forms/TeraUserForm.py:48 msgid "Notes" msgstr "Notes" @@ -2092,7 +2198,7 @@ msgstr "Site" msgid "Role" msgstr "Rôle" -#: opentera/forms/TeraProjectForm.py:28 opentera/forms/TeraSiteForm.py:17 +#: opentera/forms/TeraProjectForm.py:28 opentera/forms/TeraSiteForm.py:18 msgid "Site Name" msgstr "Nom Site" @@ -2101,8 +2207,7 @@ msgstr "Nom Site" msgid "Description" msgstr "Description" -#: opentera/forms/TeraServiceConfigForm.py:25 -#: opentera/forms/TeraServiceForm.py:25 +#: opentera/forms/TeraServiceConfigForm.py:25 opentera/forms/TeraServiceForm.py:25 msgid "Service ID" msgstr "ID Service" @@ -2110,7 +2215,7 @@ msgstr "ID Service" msgid "Service Config ID" msgstr "ID Configuration Service" -#: opentera/forms/TeraServiceConfigForm.py:27 opentera/forms/TeraUserForm.py:17 +#: opentera/forms/TeraServiceConfigForm.py:27 opentera/forms/TeraUserForm.py:18 msgid "User ID" msgstr "ID Usager" @@ -2154,11 +2259,13 @@ msgstr "Adresse Réseau" msgid "Port" msgstr "Port" -#: opentera/forms/TeraServiceConfigForm.py:71 opentera/forms/TeraUserForm.py:20 +#: opentera/forms/TeraServiceConfigForm.py:71 opentera/forms/TeraUserForm.py:21 +#: templates/login.html:125 msgid "Username" msgstr "Code utilisateur" -#: opentera/forms/TeraServiceConfigForm.py:74 opentera/forms/TeraUserForm.py:26 +#: opentera/forms/TeraServiceConfigForm.py:74 opentera/forms/TeraUserForm.py:44 +#: templates/login.html:131 msgid "Password" msgstr "Mot de passe" @@ -2330,11 +2437,15 @@ msgstr "Couleur du Type de Séance" msgid "Session Type Configuration" msgstr "Configuration du Type de Séance" -#: opentera/forms/TeraSiteForm.py:16 +#: opentera/forms/TeraSiteForm.py:17 msgid "Site ID" msgstr "ID Site" -#: opentera/forms/TeraSiteForm.py:18 +#: opentera/forms/TeraSiteForm.py:20 +msgid "Users Require 2FA" +msgstr "Authentification Multi-Facteurs (MFA) requise" + +#: opentera/forms/TeraSiteForm.py:22 msgid "Site Role" msgstr "Rôle du Site" @@ -2366,35 +2477,51 @@ msgstr "A une interface web" msgid "Expose Web editor" msgstr "A un éditeur web" -#: opentera/forms/TeraUserForm.py:18 +#: opentera/forms/TeraUserForm.py:19 msgid "User UUID" msgstr "UUID Utilisateur" -#: opentera/forms/TeraUserForm.py:19 +#: opentera/forms/TeraUserForm.py:20 msgid "User Full Name" msgstr "Nom complet Utilisateur" -#: opentera/forms/TeraUserForm.py:21 +#: opentera/forms/TeraUserForm.py:22 msgid "User Enabled" msgstr "Utilisateur activé" -#: opentera/forms/TeraUserForm.py:22 +#: opentera/forms/TeraUserForm.py:26 +msgid "Force password change" +msgstr "Imposer changement de mot de passe" + +#: opentera/forms/TeraUserForm.py:28 +msgid "2FA Enabled" +msgstr "Double authentification (2FA) activée" + +#: opentera/forms/TeraUserForm.py:31 +msgid "2FA OTP Enabled" +msgstr "2FA OTP activé" + +#: opentera/forms/TeraUserForm.py:34 +msgid "2FA Email Enabled" +msgstr "2FA par courriel activée" + +#: opentera/forms/TeraUserForm.py:40 msgid "First Name" msgstr "Prénom" -#: opentera/forms/TeraUserForm.py:23 +#: opentera/forms/TeraUserForm.py:41 msgid "Last Name" msgstr "Nom" -#: opentera/forms/TeraUserForm.py:24 +#: opentera/forms/TeraUserForm.py:42 msgid "Email" msgstr "Courriel" -#: opentera/forms/TeraUserForm.py:27 +#: opentera/forms/TeraUserForm.py:45 msgid "User Is Super Administrator" msgstr "Utilisateur est Super Administrateur" -#: opentera/forms/TeraUserForm.py:29 +#: opentera/forms/TeraUserForm.py:49 msgid "Profile" msgstr "Profil" @@ -2423,7 +2550,6 @@ msgid "OpenTeraServer patch version number" msgstr "Version OpenTeraServeur (Patch)" #: opentera/forms/TeraVersionsForm.py:43 templates/about.html:26 -#: templates/disabled_doc.html:26 msgid "Versions" msgstr "Versions" @@ -2478,8 +2604,7 @@ msgstr "Impossible de créer l'événement de séance \"Arrêt de séance\"" #: opentera/services/BaseWebRTCService.py:517 msgid "Error stopping session - check server logs" msgstr "" -"Erreur lors de l'arrêt de la séance - veuillez vérifier les journaux " -"système" +"Erreur lors de l'arrêt de la séance - veuillez vérifier les journaux système" #: opentera/services/BaseWebRTCService.py:519 msgid "No matching session to stop" @@ -2522,20 +2647,20 @@ msgstr "Erreur de mise à jour de la séance" #: opentera/services/BaseWebRTCService.py:622 msgid "Error creating user left session event" msgstr "" -"Erreur lors de la création de l'événement de séance 'Utilisateur a quitté" -" la séance\"" +"Erreur lors de la création de l'événement de séance 'Utilisateur a quitté la " +"séance\"" #: opentera/services/BaseWebRTCService.py:637 msgid "Error creating participant left session event" msgstr "" -"Erreur lors de la création de l'événement de séance 'Participant a quitté" -" la séance\"" +"Erreur lors de la création de l'événement de séance 'Participant a quitté la " +"séance\"" #: opentera/services/BaseWebRTCService.py:653 msgid "Error creating device left session event" msgstr "" -"Erreur lors de la création de l'événement de séance 'Appareil a quitté la" -" séance\"" +"Erreur lors de la création de l'événement de séance 'Appareil a quitté la " +"séance\"" #: opentera/services/BaseWebRTCService.py:710 #: opentera/services/BaseWebRTCService.py:725 @@ -2551,22 +2676,121 @@ msgstr "Téléchargements" msgid "Latest version: " msgstr "Dernière version: " -#: templates/about.html:63 templates/disabled_doc.html:35 +#: templates/about.html:64 msgid "License" msgstr "Licence" -#: templates/about.html:77 templates/disabled_doc.html:53 +#: templates/about.html:78 msgid "Authors" msgstr "Auteurs" -#: templates/about.html:87 templates/disabled_doc.html:63 +#: templates/about.html:88 msgid "Contributors" msgstr "Collaborateurs" -#: templates/disabled_doc.html:48 +#: templates/disabled_doc.html:27 msgid "Documentation is disabled!" msgstr "La documentation d’API est désactivée!" +#: templates/login.html:5 +msgid "OpenTera Login Page" +msgstr "OpenTera - Page de connexion" + +#: templates/login.html:89 +msgid "Invalid username or password" +msgstr "Code utilisateur ou mot de passe incorrect" + +#: templates/login.html:139 +msgid "Login" +msgstr "Connecter" + +#: templates/login_change_password.html:5 +msgid "OpenTera - Change Password" +msgstr "OpenTera - Changement de mot de passe" + +#: templates/login_change_password.html:96 +msgid "Password successfully changed!" +msgstr "Mot de passe changé avec succès!" + +#: templates/login_change_password.html:97 +msgid "Redirecting to login screen..." +msgstr "Redirection vers la page de connexion..." + +#: templates/login_change_password.html:104 +msgid "Password change required" +msgstr "Changement de mot de passe requis" + +#: templates/login_change_password.html:110 +msgid "New Password" +msgstr "Nouveau" + +#: templates/login_change_password.html:115 +msgid "Confirm Password" +msgstr "Confirmation" + +#: templates/login_change_password.html:122 +msgid "Change Password" +msgstr "Changer mot de passe" + +#: templates/login_setup_2fa.html:107 +msgid "You need to setup multi-factor authentication before continuing." +msgstr "" +"Vous devez configurer l'authentification multi-facteurs avant de poursuivre." + +#: templates/login_setup_2fa.html:121 +msgid "OTP configuration" +msgstr "Configuration OTP" + +#: templates/login_setup_2fa.html:127 +msgid "Scan the QR code with your authenticator app" +msgstr "Numérisez le code QR avec votre application d'authentification" + +#: templates/login_setup_2fa.html:128 +msgid "Enter the generated code:" +msgstr "Entrez le code généré:" + +#: templates/login_setup_2fa.html:134 templates/login_setup_2fa.html:151 +#: templates/login_validate_2fa.html:162 +msgid "Validate" +msgstr "Valider" + +#: templates/login_setup_2fa.html:144 +msgid "Use email for authentication" +msgstr "Utiliser courriel pour authentification" + +#: templates/login_setup_2fa.html:149 +msgid "No email address configured. Please contact your administrator." +msgstr "" +"Aucune adresse courriel configurée. Veuillez contacter votre administrateur." + +#: templates/login_validate_2fa.html:32 +msgid "Code validation timeout. Redirecting to login." +msgstr "Délai de validation expiré. Veuillez vous connecter à nouveau." + +#: templates/login_validate_2fa.html:117 +msgid "Too many attempts - returning to login" +msgstr "Trop d'essais - veuillez attendre et vous reconnecter à nouveau" + +#: templates/login_validate_2fa.html:120 +msgid "Invalid code" +msgstr "Code invalide" + +#: templates/login_validate_2fa.html:120 +msgid "attempts left" +msgstr "essais restants" + +#: templates/login_validate_2fa.html:144 +msgid "Multi Factor Authentication" +msgstr "Authentification Multi-Facteurs" + +#: templates/login_validate_2fa.html:151 +msgid "Authentication code" +msgstr "Code d'authentification" + +#: templates/login_validate_2fa.html:171 +msgid "Successfully authenticated" +msgstr "Authentification complétée" + #~ msgid "Missing id_project or id_site arguments" #~ msgstr "Champs : id_projet oou id_site manquants" @@ -2585,3 +2809,8 @@ msgstr "La documentation d’API est désactivée!" #~ msgid "Invalid client name :" #~ msgstr "Nom du client invalide :" +#~ msgid "Invalid client version handler" +#~ msgstr "Mauvaise version du client" + +#~ msgid "Invalid old password" +#~ msgstr "Ancien mot de passe incorrect"