From 77f8b750bf5e02d546ef168122f39dffc492f17c Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Wed, 31 May 2023 11:42:43 -0400 Subject: [PATCH 01/80] Updated version to 1.2.4 --- docs/conf.py | 2 +- teraserver/CMakeLists.txt | 2 +- teraserver/python/setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 48f8b267..92e18e08 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,7 +9,7 @@ project = 'OpenTera' copyright = '2023, Simon Brière, Dominic Létourneau' author = 'Simon Brière, Dominic Létourneau' -release = '1.2.3' +release = '1.2.4' version = release html_logo = 'images/LogoOpenTera200px.png' diff --git a/teraserver/CMakeLists.txt b/teraserver/CMakeLists.txt index f055b48d..72ff9ace 100755 --- a/teraserver/CMakeLists.txt +++ b/teraserver/CMakeLists.txt @@ -10,7 +10,7 @@ endif(NOT CMAKE_BUILD_TYPE) # Software version SET(OPENTERA_VERSION_MAJOR "1") SET(OPENTERA_VERSION_MINOR "2") -SET(OPENTERA_VERSION_PATCH "3") +SET(OPENTERA_VERSION_PATCH "4") SET(OPENTERA_SERVER_VERSION OpenTera_v${OPENTERA_VERSION_MAJOR}.${OPENTERA_VERSION_MINOR}.${OPENTERA_VERSION_PATCH}) diff --git a/teraserver/python/setup.py b/teraserver/python/setup.py index e41e3b78..2962575a 100644 --- a/teraserver/python/setup.py +++ b/teraserver/python/setup.py @@ -9,7 +9,7 @@ setuptools.setup( name="opentera", - version="1.2.3", + version="1.2.4", author="Dominic Létourneau, Simon Brière", author_email="dominic.letourneau@usherbrooke.ca, simon.briere@usherbrooke.ca", description="OpenTera base package", From 90b0df8e1ce2cc4d7f0dcce8f29bc8d8b8df2728 Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Mon, 12 Jun 2023 08:23:00 -0400 Subject: [PATCH 02/80] Fixed issues in UserQueryTestType for inaccessible test types. --- .../DatabaseModule/DBManagerTeraUserAccess.py | 9 +++++---- .../FlaskModule/API/user/UserQueryTestType.py | 20 ++++++++++++------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/teraserver/python/modules/DatabaseModule/DBManagerTeraUserAccess.py b/teraserver/python/modules/DatabaseModule/DBManagerTeraUserAccess.py index 1dea5d42..0a3de257 100644 --- a/teraserver/python/modules/DatabaseModule/DBManagerTeraUserAccess.py +++ b/teraserver/python/modules/DatabaseModule/DBManagerTeraUserAccess.py @@ -563,11 +563,12 @@ def query_session_type_by_id(self, session_type_id: int): return session_type def query_test_type(self, test_type_id: int): - site_ids = self.get_accessible_sites_ids() - proj_ids = self.get_accessible_projects_ids() + # site_ids = self.get_accessible_sites_ids() + # proj_ids = self.get_accessible_projects_ids() service_ids = self.get_accessible_services_ids() - test_type = TeraTestType.query.filter_by(id_test_type=test_type_id).filter(TeraSite.id_site.in_(site_ids))\ - .filter(TeraTestType.id_service.in_(service_ids)).filter(TeraProject.id_project.in_(proj_ids)).first() + test_type = TeraTestType.query.filter_by(id_test_type=test_type_id).first() + # .filter(TeraSite.id_site.in_(site_ids))\ + # .filter(TeraTestType.id_service.in_(service_ids)).filter(TeraProject.id_project.in_(proj_ids)).first() return test_type def query_projects_for_site(self, site_id: int): diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryTestType.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryTestType.py index d1fffbff..aeadc691 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryTestType.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryTestType.py @@ -135,12 +135,16 @@ def post(self): # Check if current user can modify the posted type if json_test_type['id_test_type'] > 0: - projects_ids = [proj.id_project for proj in - TeraTestTypeProject.get_projects_for_test_type(json_test_type['id_test_type'])] - admin_projects_ids = user_access.get_accessible_projects_ids(admin_only=True) - - if len(set(projects_ids).difference(admin_projects_ids)) == len(projects_ids): - return gettext('Not project admin in at least one project'), 403 + # Test types can be modified if the user has admin access to service (e.g. has admin access to a site + # related to that service or is super admin) -> This will be checked below. + pass + # projects_ids = [proj.id_project for proj in + # TeraTestTypeProject.get_projects_for_test_type(json_test_type['id_test_type'])] + # admin_projects_ids = user_access.get_accessible_projects_ids(admin_only=True) + # + # accessible_projects = [proj_id for proj_id in projects_ids if proj_id in admin_projects_ids] + # if len(accessible_projects) and not current_user.user_superadmin: + # return gettext('Not project admin in at least one project'), 403 else: # Test types can be created without a project if super admin, but require a project otherwise if 'test_type_projects' not in json_test_type and not current_user.user_superadmin: @@ -160,7 +164,8 @@ def post(self): test_type: TeraTestType = TeraTestType.get_test_type_by_id(json_test_type['id_test_type']) if test_type: current_service_id = test_type.id_service - if current_service_id not in user_access.get_accessible_services_ids(): + + if current_service_id not in user_access.get_accessible_services_ids(admin_only=True): return gettext('Forbidden'), 403 tt_sites_ids = [] @@ -172,6 +177,7 @@ def post(self): test_type_sites = [test_type_sites] tt_sites_ids = [site['id_site'] for site in test_type_sites] admin_sites_ids = user_access.get_accessible_sites_ids(admin_only=True) + if set(tt_sites_ids).difference(admin_sites_ids): # We have some sites where we are not admin return gettext('No site admin access for at least one site in the list'), 403 From b1f375c7257d9fecf667b4fff54132df439a024e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominic=20Le=CC=81tourneau?= Date: Mon, 12 Jun 2023 10:46:47 -0400 Subject: [PATCH 03/80] Refs #219, Fixed certificates error (not checking path) when not using ssl --- teraserver/python/CreateCertificates.py | 6 ++- teraserver/python/config/TeraServerConfig.ini | 6 +-- .../{ => config}/certificates/README.TXT | 0 teraserver/python/config/nginx.conf | 8 ++-- .../modules/TwistedModule/TwistedModule.py | 39 +++++++++---------- 5 files changed, 30 insertions(+), 29 deletions(-) rename teraserver/python/{ => config}/certificates/README.TXT (100%) diff --git a/teraserver/python/CreateCertificates.py b/teraserver/python/CreateCertificates.py index d20ee6b7..b4fed6d3 100755 --- a/teraserver/python/CreateCertificates.py +++ b/teraserver/python/CreateCertificates.py @@ -46,12 +46,16 @@ def generate_certificates(config: ConfigManager): site_info = crypto.generate_local_certificate() # Save files crypto.write_private_key_and_certificate(site_info, keyfile=site_key_path, certfile=site_certificate_path) + else: + print('Site certificate and key already exists, skipping...') if not os.path.exists(ca_certificate_path) or not os.path.exists(ca_key_path): - print('Generating Site certificate and key') + print('Generating CA certificate and key') ca_info = crypto.generate_ca_certificate(common_name='Local CA') # Save files crypto.write_private_key_and_certificate(ca_info, keyfile=ca_key_path, certfile=ca_certificate_path) + else: + print('CA certificate and key already exists, skipping...') if __name__ == '__main__': diff --git a/teraserver/python/config/TeraServerConfig.ini b/teraserver/python/config/TeraServerConfig.ini index a8459dc6..1c8431ea 100755 --- a/teraserver/python/config/TeraServerConfig.ini +++ b/teraserver/python/config/TeraServerConfig.ini @@ -1,16 +1,16 @@ { "Server": { - "name": "Serveur de developement", + "name": "Development Server", "hostname": "127.0.0.1", "port": 4040, "use_ssl": false, - "ssl_path": "certificates", + "ssl_path": "config/certificates", "site_certificate": "site_cert.pem", "site_private_key": "site_key.pem", "ca_certificate": "ca_cert.pem", "ca_private_key": "ca_key.pem", "upload_path": "uploads", - "debug_mode": true, + "debug_mode": false, "enable_docs": true }, "Database": { diff --git a/teraserver/python/certificates/README.TXT b/teraserver/python/config/certificates/README.TXT similarity index 100% rename from teraserver/python/certificates/README.TXT rename to teraserver/python/config/certificates/README.TXT diff --git a/teraserver/python/config/nginx.conf b/teraserver/python/config/nginx.conf index 350c9206..c107932e 100644 --- a/teraserver/python/config/nginx.conf +++ b/teraserver/python/config/nginx.conf @@ -39,9 +39,9 @@ http { # listen 40075; server_name 127.0.0.1; - ssl_certificate ../certificates/site_cert.pem; - ssl_certificate_key ../certificates/site_key.pem; - ssl_client_certificate ../certificates/ca_cert.pem; + ssl_certificate certificates/site_cert.pem; + ssl_certificate_key certificates/site_key.pem; + ssl_client_certificate certificates/ca_cert.pem; # Redirect http to https error_page 497 =301 https://$host:$server_port$request_uri; @@ -55,6 +55,4 @@ http { include opentera.conf; include external_services.conf; } - - } diff --git a/teraserver/python/modules/TwistedModule/TwistedModule.py b/teraserver/python/modules/TwistedModule/TwistedModule.py index f284dd5c..9366e768 100755 --- a/teraserver/python/modules/TwistedModule/TwistedModule.py +++ b/teraserver/python/modules/TwistedModule/TwistedModule.py @@ -44,7 +44,8 @@ def allHeadersReceived(self): if req.requestHeaders.hasHeader('X-Participant-UUID'): req.requestHeaders.removeHeader('X-Participant-UUID') # TODO raise error ? - # + + # TODO remove this ? # if cert is not None: # # Certificate found, add information in header # subject = cert.get_subject() @@ -139,7 +140,7 @@ def __init__(self, config: ConfigManager): # Establish root resource root_resource = WSGIRootResource(wsgi_resource, {b'assets': static_resource, b'wss': wss_root}) - # Create a Twisted Web Site + # Create a Twisted Website site = MySite(root_resource) # List of available CA clients certificates @@ -147,26 +148,27 @@ def __init__(self, config: ConfigManager): # caCerts=[cert.original] caCerts = [] - # Use verify = True to verify certificates - self.ssl_factory = ssl.CertificateOptions(verify=False, caCerts=caCerts, - requireCertificate=False, - enableSessions=False) + # SSL is normally handled by nginx + if self.config.server_config['use_ssl']: + # Use verify = True to verify certificates + self.ssl_factory = ssl.CertificateOptions(verify=False, caCerts=caCerts, + requireCertificate=False, + enableSessions=False) - ctx = self.ssl_factory.getContext() - ctx.use_privatekey_file(self.config.server_config['ssl_path'] + '/' - + self.config.server_config['site_private_key']) + ctx = self.ssl_factory.getContext() + ctx.use_privatekey_file(self.config.server_config['ssl_path'] + '/' + + self.config.server_config['site_private_key']) - ctx.use_certificate_file(self.config.server_config['ssl_path'] + '/' - + self.config.server_config['site_certificate']) + ctx.use_certificate_file(self.config.server_config['ssl_path'] + '/' + + self.config.server_config['site_certificate']) - # Certificate verification callback - ctx.set_verify(SSL.VERIFY_NONE, self.verifyCallback) + # Certificate verification callback + ctx.set_verify(SSL.VERIFY_NONE, self.verifyCallback) - # With self-signed certs we have to explicitely tell the server to trust certificates - ctx.load_verify_locations(self.config.server_config['ssl_path'] + '/' - + self.config.server_config['ca_certificate']) + # With self-signed certs we have to explicitly tell the server to trust certificates + ctx.load_verify_locations(self.config.server_config['ssl_path'] + '/' + + self.config.server_config['ca_certificate']) - if self.config.server_config['use_ssl']: reactor.listenSSL(self.config.server_config['port'], site, self.ssl_factory) else: reactor.listenTCP(self.config.server_config['port'], site) @@ -200,6 +202,3 @@ def notify_module_messages(self, pattern, channel, message): def run(self): log.startLogging(sys.stdout) reactor.run() - - - From 6a9750ccd12d371a1e127452ad21ae5022ae7871 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominic=20Le=CC=81tourneau?= Date: Mon, 12 Jun 2023 10:47:38 -0400 Subject: [PATCH 04/80] Refs #219, moved certificates in the config directory. --- teraserver/python/certificates/devices/README.TXT | 1 - 1 file changed, 1 deletion(-) delete mode 100644 teraserver/python/certificates/devices/README.TXT diff --git a/teraserver/python/certificates/devices/README.TXT b/teraserver/python/certificates/devices/README.TXT deleted file mode 100644 index be7a66fb..00000000 --- a/teraserver/python/certificates/devices/README.TXT +++ /dev/null @@ -1 +0,0 @@ -Device test certificates here... From 2489d6aca73f44afa2a9070ec72d0e6b83c515d1 Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Mon, 12 Jun 2023 11:20:35 -0400 Subject: [PATCH 05/80] Fixed DBManagerTeraUserAccess query_test_type --- .../python/modules/DatabaseModule/DBManagerTeraUserAccess.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/teraserver/python/modules/DatabaseModule/DBManagerTeraUserAccess.py b/teraserver/python/modules/DatabaseModule/DBManagerTeraUserAccess.py index 0a3de257..dec78bce 100644 --- a/teraserver/python/modules/DatabaseModule/DBManagerTeraUserAccess.py +++ b/teraserver/python/modules/DatabaseModule/DBManagerTeraUserAccess.py @@ -566,7 +566,8 @@ def query_test_type(self, test_type_id: int): # site_ids = self.get_accessible_sites_ids() # proj_ids = self.get_accessible_projects_ids() service_ids = self.get_accessible_services_ids() - test_type = TeraTestType.query.filter_by(id_test_type=test_type_id).first() + test_type = TeraTestType.query.filter_by(id_test_type=test_type_id)\ + .filter(TeraTestType.id_service.in_(service_ids)).first() # .filter(TeraSite.id_site.in_(site_ids))\ # .filter(TeraTestType.id_service.in_(service_ids)).filter(TeraProject.id_project.in_(proj_ids)).first() return test_type From 05412290dfc33807ab1b9f6a5461a650ad17df99 Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Thu, 22 Jun 2023 11:30:28 -0400 Subject: [PATCH 06/80] Changed delete access check in UserQueryTestType --- .../DatabaseModule/DBManagerTeraUserAccess.py | 4 ++-- .../FlaskModule/API/user/UserQueryTestType.py | 12 +++--------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/teraserver/python/modules/DatabaseModule/DBManagerTeraUserAccess.py b/teraserver/python/modules/DatabaseModule/DBManagerTeraUserAccess.py index dec78bce..ad487578 100644 --- a/teraserver/python/modules/DatabaseModule/DBManagerTeraUserAccess.py +++ b/teraserver/python/modules/DatabaseModule/DBManagerTeraUserAccess.py @@ -372,9 +372,9 @@ def get_accessible_services(self, admin_only=False): from opentera.db.models.TeraServiceSite import TeraServiceSite if self.user.user_superadmin: - if self.user.user_superadmin: - return TeraService.query.all() + return TeraService.query.all() + # TODO: Check if SQL query is OK # Accessible services are those from projects and sites where the user is admin accessible_projects_ids = self.get_accessible_projects_ids() admin_sites_ids = self.get_accessible_sites_ids(admin_only=True) diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQueryTestType.py b/teraserver/python/modules/FlaskModule/API/user/UserQueryTestType.py index aeadc691..fe4e96f9 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQueryTestType.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQueryTestType.py @@ -323,15 +323,9 @@ def delete(self): # Check if current user can delete test_type = TeraTestType.get_test_type_by_id(id_todel) - # Check if we are admin of all projects of that test type - if len(test_type.test_type_projects) > 0: - for proj in test_type.test_type_projects: - if user_access.get_project_role(proj.id_project) != "admin": - return gettext('Cannot delete because you are not admin in all projects.'), 403 - else: - # No project right now for that test type - must at least project admin somewhere to delete - if len(user_access.get_accessible_projects(admin_only=True)) == 0: - return gettext('Unable to delete - not admin in at least one project'), 403 + # Check if we have admin access to the related test type service + if test_type.id_service not in user_access.get_accessible_services_ids(admin_only=True): + return gettext('Unable to delete - not admin in the related test type service'), 403 # If we are here, we are allowed to delete. Do so. try: From 7b1040896386759aa1bcdf039daa557fe881c210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominic=20Le=CC=81tourneau?= Date: Thu, 6 Jul 2023 15:09:06 -0400 Subject: [PATCH 07/80] Refs #220, Allow participants and devices with static token to post file assets --- .../python/services/FileTransferService/API/QueryAssetFile.py | 2 +- .../services/FileTransferService/API/QueryAssetFileInfos.py | 2 +- .../services/FileTransferService/test_QueryAssetFileAndInfos.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/teraserver/python/services/FileTransferService/API/QueryAssetFile.py b/teraserver/python/services/FileTransferService/API/QueryAssetFile.py index 2f76f565..0fd5e565 100644 --- a/teraserver/python/services/FileTransferService/API/QueryAssetFile.py +++ b/teraserver/python/services/FileTransferService/API/QueryAssetFile.py @@ -57,7 +57,7 @@ def get(self): responses={200: 'Success - Return informations about file assets', 400: 'Required parameter is missing', 403: 'Access denied to the requested asset'}) - @ServiceAccessManager.service_or_others_token_required(allow_dynamic_tokens=True, allow_static_tokens=False) + @ServiceAccessManager.service_or_others_token_required(allow_dynamic_tokens=True, allow_static_tokens=True) def post(self): if not request.content_type.__contains__('multipart/form-data'): return gettext('Wrong content type'), 400 diff --git a/teraserver/python/services/FileTransferService/API/QueryAssetFileInfos.py b/teraserver/python/services/FileTransferService/API/QueryAssetFileInfos.py index 4baa1870..62d0fb53 100644 --- a/teraserver/python/services/FileTransferService/API/QueryAssetFileInfos.py +++ b/teraserver/python/services/FileTransferService/API/QueryAssetFileInfos.py @@ -63,7 +63,7 @@ def get(self): responses={200: 'Success - Return informations about file assets', 400: 'Required parameter is missing', 403: 'Access denied to the requested asset'}) - @ServiceAccessManager.service_or_others_token_required(allow_dynamic_tokens=True, allow_static_tokens=False) + @ServiceAccessManager.service_or_others_token_required(allow_dynamic_tokens=True, allow_static_tokens=True) def post(self): if 'assets' not in request.json and 'file_asset' not in request.json: return gettext('Badly formatted request'), 400 diff --git a/teraserver/python/tests/services/FileTransferService/test_QueryAssetFileAndInfos.py b/teraserver/python/tests/services/FileTransferService/test_QueryAssetFileAndInfos.py index d5c0e464..2b03a9eb 100644 --- a/teraserver/python/tests/services/FileTransferService/test_QueryAssetFileAndInfos.py +++ b/teraserver/python/tests/services/FileTransferService/test_QueryAssetFileAndInfos.py @@ -564,7 +564,7 @@ def test_full_as_static_participant(self): files=files, endpoint=self.asset_endpoint) - self.assertEqual(403, response.status_code, 'Forbidden access to API') + self.assertEqual(200, response.status_code) def test_full_as_service(self): with self.app_context(): From 9b5119cb219a0ef68f7d01559570fdf8ae4be2e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominic=20Le=CC=81tourneau?= Date: Thu, 6 Jul 2023 15:20:40 -0400 Subject: [PATCH 08/80] Refs #221 Test for object is not None before handling update / delete --- teraserver/python/opentera/db/Base.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/teraserver/python/opentera/db/Base.py b/teraserver/python/opentera/db/Base.py index 3b45e9c0..76416480 100755 --- a/teraserver/python/opentera/db/Base.py +++ b/teraserver/python/opentera/db/Base.py @@ -175,8 +175,12 @@ def update(cls, update_id: int, values: dict): # with Session(cls.db().engine) as session: update_obj = cls.db().session.query(cls).filter(getattr(cls, cls.get_primary_key_name()) == update_id).first() - update_obj.from_json(update_values) - cls.db().session.commit() + + if update_obj: + update_obj.from_json(update_values) + cls.db().session.commit() + else: + raise SQLAlchemyError(cls.__name__ + ' with id ' + str(update_id) + ' cannot update.') @classmethod def commit(cls): @@ -201,16 +205,20 @@ def delete_check_integrity(self) -> IntegrityError | None: @classmethod def delete(cls, id_todel, autocommit: bool = True): delete_obj = cls.db().session.query(cls).filter(getattr(cls, cls.get_primary_key_name()) == id_todel).first() - cannot_be_deleted_exception = delete_obj.delete_check_integrity() - if cannot_be_deleted_exception: - raise cannot_be_deleted_exception + if delete_obj: + cannot_be_deleted_exception = delete_obj.delete_check_integrity() + if cannot_be_deleted_exception: + raise cannot_be_deleted_exception + if getattr(delete_obj, 'soft_delete', None): delete_obj.soft_delete() else: cls.db().session.delete(delete_obj) if autocommit: cls.commit() + else: + raise SQLAlchemyError(cls.__name__ + ' with id ' + str(id_todel) + ' cannot delete.') @classmethod def undelete(cls, id_to_undelete): @@ -222,6 +230,8 @@ def undelete(cls, id_to_undelete): cls.commit() else: print(cls.__name__ + ' with id ' + str(id_to_undelete) + ' cannot undelete.') + raise SQLAlchemyError(cls.__name__ + ' with id ' + str(id_to_undelete) + ' cannot undelete.') + # @classmethod # def handle_include_deleted_flag(cls, include_deleted=False): From 0ee563880cfcbc5efc91d4687650a92b3ce8d7e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominic=20Le=CC=81tourneau?= Date: Thu, 6 Jul 2023 15:30:56 -0400 Subject: [PATCH 09/80] Refs #221 Test for object is not None before handling update / delete --- teraserver/python/opentera/db/Base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teraserver/python/opentera/db/Base.py b/teraserver/python/opentera/db/Base.py index 76416480..b3249e28 100755 --- a/teraserver/python/opentera/db/Base.py +++ b/teraserver/python/opentera/db/Base.py @@ -231,7 +231,7 @@ def undelete(cls, id_to_undelete): else: print(cls.__name__ + ' with id ' + str(id_to_undelete) + ' cannot undelete.') raise SQLAlchemyError(cls.__name__ + ' with id ' + str(id_to_undelete) + ' cannot undelete.') - + # @classmethod # def handle_include_deleted_flag(cls, include_deleted=False): From 74b04a931f2082e69fabb5bd285a7f763cb587b1 Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Tue, 11 Jul 2023 07:59:00 -0400 Subject: [PATCH 10/80] Fixed project admins unable to delete sessions created by site admins. --- docs/Deployment.md | 10 +++++----- teraserver/easyrtc/static/js/tera_webrtc.js | 3 +++ .../modules/DatabaseModule/DBManagerTeraUserAccess.py | 4 ++-- .../modules/FlaskModule/API/user/UserQuerySessions.py | 2 +- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/Deployment.md b/docs/Deployment.md index f390b071..de4f4beb 100644 --- a/docs/Deployment.md +++ b/docs/Deployment.md @@ -109,10 +109,10 @@ There is a few config files to edit. You should edit each of them and put the co 1. Create nginx configuration file: `sudo nano /etc/nginx/sites-available/opentera` 2. Copy the `server` section (only) from the `teraserver/python/config/nginx.conf` file. 3. Edit the `ssl_certificate`, `ssl_certificate_key`, `ssl_client_certificate` to point to your correct SSL setup. -4. Edit the `include opentera.conf` line with the full path to the `opentera.conf` file, for example: `/home/baseuser/opentera/teraserver/python/config/opentera.conf;` +4. Edit the `include opentera.conf` and the `include external_services.conf` lines with the full path to the `*.conf` files, for example: `/home/baseuser/opentera/teraserver/python/config/opentera.conf;` 5. Enable the site by creating a symbolic link into the sites-enabled folder: `sudo ln -s /etc/nginx/sites-available/opentera /etc/nginx/sites-enabled/` - -6. Restart the nginx server: `sudo systemctl restart nginx` +6. Remove the default nginx config (if needed) that listens to port 80 (`sudo rm /etc/nginx/sites-enabled/default`) +7. Restart the nginx server: `sudo systemctl restart nginx` #### Service configuration TO ensure that OpenTera will run automatically and after a reboot, a systemd service can be created. @@ -127,7 +127,7 @@ After=network-online.target User=**PUT THE EXECUTING USER HERE** Group=**PUT THE EXECUTING GROUP HERE** Environment=PYTHONPATH=**(path to opentera)**/opentera/teraserver/python -ExecStart=**(path to opentera)**/opentera/teraserver/python/env/python-3.8/bin/python3 **(path to opentera)**/opentera/teraserver/python/TeraServer.py +ExecStart=**(path to opentera)**/opentera/teraserver/python/env/python-3.10/bin/python3 **(path to opentera)**/opentera/teraserver/python/TeraServer.py WorkingDirectory=**(path to opentera)**/opentera/teraserver/python StandardOutput=syslog+console StandardError=syslog+console @@ -190,5 +190,5 @@ key=(path to private key file) 1. Install certbot agent: `sudo apt-get install certbot` 2. Install nginx plugin: `sudo apt-get install python3-certbot-nginx` -3. Run certbot: `sudo certbot --nginx -d (your_host_name)` +3. Run certbot: `sudo certbot run -a standalone -i nginx -d (your_host_name)` diff --git a/teraserver/easyrtc/static/js/tera_webrtc.js b/teraserver/easyrtc/static/js/tera_webrtc.js index 74794dec..55d73d06 100644 --- a/teraserver/easyrtc/static/js/tera_webrtc.js +++ b/teraserver/easyrtc/static/js/tera_webrtc.js @@ -1155,9 +1155,12 @@ async function shareScreen(local, start){ // Start screen sharing let screenStream = undefined; try { + // TODO: Check if video can put to false or set "enabled=false" screenStream = await navigator.mediaDevices.getDisplayMedia({video: true, audio: currentConfig.screenAudio}); easyrtc.register3rdPartyLocalMediaStream(screenStream, streamName); + // TODO: Use easyrtc.setSdpFilters to improve audio quality + // easyrtc.setSdpFilters() console.log("Starting screen sharing - with audio: " + currentConfig.screenAudio); diff --git a/teraserver/python/modules/DatabaseModule/DBManagerTeraUserAccess.py b/teraserver/python/modules/DatabaseModule/DBManagerTeraUserAccess.py index ad487578..915c959a 100644 --- a/teraserver/python/modules/DatabaseModule/DBManagerTeraUserAccess.py +++ b/teraserver/python/modules/DatabaseModule/DBManagerTeraUserAccess.py @@ -26,8 +26,8 @@ class DBManagerTeraUserAccess: def __init__(self, user: TeraUser): self.user = user - def get_accessible_users_ids(self, admin_only=False): - users = self.get_accessible_users(admin_only=admin_only) + def get_accessible_users_ids(self, admin_only=False, include_site_access=False): + users = self.get_accessible_users(admin_only=admin_only, include_site_access=include_site_access) users_ids = [] for user in users: if user.id_user not in users_ids: diff --git a/teraserver/python/modules/FlaskModule/API/user/UserQuerySessions.py b/teraserver/python/modules/FlaskModule/API/user/UserQuerySessions.py index 4865296e..f6c4d6aa 100644 --- a/teraserver/python/modules/FlaskModule/API/user/UserQuerySessions.py +++ b/teraserver/python/modules/FlaskModule/API/user/UserQuerySessions.py @@ -246,7 +246,7 @@ def delete(self): # At least one participant is not accessible to the user return gettext('User doesn\'t have access to at least one participant of that session.'), 403 - accessibles_user_ids = user_access.get_accessible_users_ids() + accessibles_user_ids = user_access.get_accessible_users_ids(include_site_access=True) if set(session_users_ids).difference(accessibles_user_ids): # At least one session user is not accessible to the user return gettext('User doesn\'t have access to at least one user of that session.'), 403 From e804d619415343a2bc34529e5d791b0bce7ddba8 Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Tue, 11 Jul 2023 08:21:20 -0400 Subject: [PATCH 11/80] Refs #222. Updated non-flask requirements. --- teraserver/python/env/requirements.txt | 22 +- .../en/LC_MESSAGES/filetransferservice.po | 2 +- .../fr/LC_MESSAGES/filetransferservice.po | 2 +- .../en/LC_MESSAGES/loggingservice.po | 2 +- .../fr/LC_MESSAGES/loggingservice.po | 2 +- .../en/LC_MESSAGES/videorehabservice.po | 2 +- .../fr/LC_MESSAGES/videorehabservice.po | 45 +- .../translations/en/LC_MESSAGES/messages.po | 248 ++++--- .../translations/fr/LC_MESSAGES/messages.po | 632 ++++++++++-------- 9 files changed, 530 insertions(+), 427 deletions(-) diff --git a/teraserver/python/env/requirements.txt b/teraserver/python/env/requirements.txt index 920de1a9..ba7df68f 100644 --- a/teraserver/python/env/requirements.txt +++ b/teraserver/python/env/requirements.txt @@ -1,14 +1,14 @@ pypiwin32==223; sys_platform == 'win32' Twisted==22.10.0 treq==22.2.0 -cryptography==40.0.2 -autobahn==23.1.2 +cryptography==41.0.2 +autobahn==23.6.2 SQLAlchemy==1.4.48 sqlalchemy-schemadisplay==1.3 pydot==1.4.2 psycopg2-binary==2.9.6 Flask==2.3.2 -Flask-SQLAlchemy==3.0.3 +Flask-SQLAlchemy==3.0.5 Flask-Login==0.6.2 Flask-Login-Multi==0.1.2 Flask-HTTPAuth==4.8.0 @@ -23,19 +23,19 @@ flask-swagger-ui==4.11.1 Flask-Limiter==3.3.1 Flask-Mail==0.9.1 Flask-Principal==0.4.0 -redis==4.5.5 -txredisapi==1.4.9 +redis==4.6.0 +txredisapi==1.4.10 passlib==1.7.4 bcrypt==4.0.1 WTForms==3.0.1 -pyOpenSSL==23.1.1 -service-identity==21.1.0 +pyOpenSSL==23.2.0 +service-identity==23.1.0 PyJWT==2.7.0 pylzma==0.5.0 bz2file==0.98 python-slugify==8.0.1 -websocket-client==1.5.2 -pytest==7.3.1 -jsonschema==4.17.3 +websocket-client==1.6.1 +pytest==7.4.0 +jsonschema==4.18.0 Jinja2==3.1.2 -ua-parser==0.16.1 +ua-parser==0.18.0 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 af6864e6..e1130ae3 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: 2023-05-23 14:24-0400\n" +"POT-Creation-Date: 2023-07-11 08:07-0400\n" "PO-Revision-Date: 2021-01-19 16:16-0500\n" "Last-Translator: FULL NAME \n" "Language: en\n" 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 898dbd8a..99227e6b 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: 2023-05-23 14:24-0400\n" +"POT-Creation-Date: 2023-07-11 08:07-0400\n" "PO-Revision-Date: 2023-02-28 08:22-0500\n" "Last-Translator: \n" "Language: fr\n" 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 162794cf..6a6b0e30 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: 2023-05-23 14:24-0400\n" +"POT-Creation-Date: 2023-07-11 08:07-0400\n" "PO-Revision-Date: 2023-01-26 13:29-0500\n" "Last-Translator: FULL NAME \n" "Language: en\n" 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 c1a38066..78db085f 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: 2023-05-23 14:24-0400\n" +"POT-Creation-Date: 2023-07-11 08:07-0400\n" "PO-Revision-Date: 2023-02-28 08:10-0500\n" "Last-Translator: \n" "Language: fr\n" 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 62b556b7..2e0bd723 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: 2023-05-23 14:24-0400\n" +"POT-Creation-Date: 2023-07-11 08:07-0400\n" "PO-Revision-Date: 2021-01-19 16:16-0500\n" "Last-Translator: FULL NAME \n" "Language: en\n" 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 832b60ca..4338be75 100644 --- a/teraserver/python/services/VideoRehabService/translations/fr/LC_MESSAGES/videorehabservice.po +++ b/teraserver/python/services/VideoRehabService/translations/fr/LC_MESSAGES/videorehabservice.po @@ -7,17 +7,16 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2023-05-23 14:24-0400\n" +"POT-Creation-Date: 2023-07-11 08:07-0400\n" "PO-Revision-Date: 2023-05-23 14:29-0400\n" "Last-Translator: \n" -"Language-Team: fr \n" "Language: fr\n" +"Language-Team: fr \n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" "Generated-By: Babel 2.12.1\n" -"X-Generator: Poedit 3.3.1\n" #: VideoRehabService.py:44 msgid "General configuration" @@ -74,11 +73,12 @@ msgstr "Navigateur non-supporté détecté" #: templates/participant_dashboard.html:28 templates/user_dashboard.html:28 msgid "" -"Your browser is not supported. Session might or might not work, but it is " -"recommended to user another browser." +"Your browser is not supported. Session might or might not work, but it is" +" recommended to user another browser." msgstr "" "Votre navigateur Internet n'est pas supporté. La séance peut quand même " -"fonctionner correctement, mais il est recommandé d'utiliser un autre navigateur." +"fonctionner correctement, mais il est recommandé d'utiliser un autre " +"navigateur." #: templates/participant_dashboard.html:29 templates/user_dashboard.html:29 msgid "Your browser" @@ -94,17 +94,17 @@ msgstr "Impossible de se connecter" #: templates/participant_dashboard.html:32 templates/user_dashboard.html:32 msgid "" -"Your access might have been disabled or you might be already logged in on " -"another device" +"Your access might have been disabled or you might be already logged in on" +" another device" msgstr "" -"Votre accès peut avoir été désactivé ou vous êtes peut-être déjà connecté sur " -"un autre appareil" +"Votre accès peut avoir été désactivé ou vous êtes peut-être déjà connecté" +" sur un autre appareil" #: templates/participant_dashboard.html:33 templates/user_dashboard.html:33 msgid "Your session is now over. You may now logout or close this page." msgstr "" -"La séance est maintenant terminée. Vous pouvez vous déconnecter ou fermer cette " -"page." +"La séance est maintenant terminée. Vous pouvez vous déconnecter ou fermer" +" cette page." #: templates/participant_dashboard.html:59 templates/user_dashboard.html:59 msgid "Error" @@ -159,11 +159,13 @@ msgstr "Impossible d'accéder à la caméra et/ou au microphone" msgid "Error message" msgstr "Message d'erreur" -#: templates/participant_localview.html:116 templates/user_session_lobby.html:126 +#: templates/participant_localview.html:116 +#: templates/user_session_lobby.html:126 msgid "Session starting..." msgstr "Démarrage de la séance en cours..." -#: templates/participant_localview.html:119 templates/user_session_lobby.html:129 +#: templates/participant_localview.html:119 +#: templates/user_session_lobby.html:129 msgid "The session is about to start... Enjoy your session!" msgstr "La séance est sur le point de démarrer. Bonne séance!" @@ -235,15 +237,22 @@ msgstr "Appareils" #~ msgid "Error creating user left session event" #~ msgstr "" -#~ "Erreur lors de la création de l'événement \"Utilisateur a quitté la séance\"" +#~ "Erreur lors de la création de " +#~ "l'événement \"Utilisateur a quitté la " +#~ "séance\"" #~ msgid "Error creating participant left session event" #~ msgstr "" -#~ "Erreur lors de la création de l'événement \"Participant a quitté la séance\"" +#~ "Erreur lors de la création de " +#~ "l'événement \"Participant a quitté la " +#~ "séance\"" #~ msgid "Error creating device left session event" #~ msgstr "" -#~ "Erreur lors de la création de l'événement \"Appareil a quitté la séance\"" +#~ "Erreur lors de la création de " +#~ "l'événement \"Appareil a quitté la " +#~ "séance\"" #~ msgid "Cannot create refused session event" #~ msgstr "Erreur lors de la création de l'événement \"Invitation refusée\"" + diff --git a/teraserver/python/translations/en/LC_MESSAGES/messages.po b/teraserver/python/translations/en/LC_MESSAGES/messages.po index 14f31d6f..e23624a2 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: 2023-05-23 14:24-0400\n" +"POT-Creation-Date: 2023-07-11 08:07-0400\n" "PO-Revision-Date: 2021-01-25 13:01-0500\n" "Last-Translator: \n" "Language: en\n" @@ -68,6 +68,12 @@ msgstr "" #: 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:96 #: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:197 #: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:301 @@ -91,7 +97,7 @@ msgstr "" #: modules/FlaskModule/API/user/UserQueryParticipants.py:363 #: modules/FlaskModule/API/user/UserQueryParticipants.py:366 #: modules/FlaskModule/API/user/UserQueryProjectAccess.py:204 -#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:275 +#: 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 @@ -128,7 +134,7 @@ msgstr "" #: 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:270 +#: 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 @@ -141,7 +147,7 @@ msgstr "" #: modules/FlaskModule/API/user/UserQueryStats.py:85 #: modules/FlaskModule/API/user/UserQueryStats.py:90 #: modules/FlaskModule/API/user/UserQueryTestType.py:62 -#: modules/FlaskModule/API/user/UserQueryTestType.py:164 +#: 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 @@ -185,9 +191,14 @@ msgstr "" #: 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/ServiceQueryTests.py:234 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:245 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:280 +#: 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:141 #: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:158 #: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:215 @@ -209,8 +220,8 @@ msgstr "" #: 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:245 -#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:286 +#: 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 @@ -243,13 +254,13 @@ msgstr "" #: 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:241 -#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:281 +#: 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:216 -#: modules/FlaskModule/API/user/UserQueryTestType.py:231 +#: 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 @@ -259,8 +270,8 @@ msgstr "" #: 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:260 -#: modules/FlaskModule/API/user/UserQueryUserGroups.py:308 +#: 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 @@ -318,7 +329,9 @@ msgstr "" #: 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/ServiceQueryTests.py:266 +#: 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 @@ -402,7 +415,7 @@ msgstr "" #: modules/FlaskModule/API/service/ServiceQueryAccess.py:57 #: modules/FlaskModule/API/service/ServiceQueryAssets.py:61 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:60 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:57 #: modules/FlaskModule/API/user/UserQueryAssets.py:60 #: modules/FlaskModule/API/user/UserQueryTests.py:54 msgid "No arguments specified" @@ -430,7 +443,7 @@ msgstr "" #: modules/FlaskModule/API/service/ServiceQueryAssets.py:64 #: modules/FlaskModule/API/service/ServiceQueryAssets.py:90 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:63 +#: 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 @@ -438,7 +451,7 @@ msgid "Device access denied" msgstr "" #: modules/FlaskModule/API/service/ServiceQueryAssets.py:68 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:67 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:64 #: modules/FlaskModule/API/user/UserQueryAssets.py:67 #: modules/FlaskModule/API/user/UserQueryTests.py:61 msgid "Session access denied" @@ -446,7 +459,7 @@ msgstr "" #: modules/FlaskModule/API/service/ServiceQueryAssets.py:72 #: modules/FlaskModule/API/service/ServiceQueryAssets.py:86 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:71 +#: 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 @@ -455,7 +468,7 @@ msgstr "" #: modules/FlaskModule/API/service/ServiceQueryAssets.py:76 #: modules/FlaskModule/API/service/ServiceQueryAssets.py:82 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:75 +#: 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 @@ -463,7 +476,7 @@ msgid "User access denied" msgstr "" #: modules/FlaskModule/API/service/ServiceQueryAssets.py:97 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:83 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:80 #: modules/FlaskModule/API/user/UserQueryAssets.py:100 #: modules/FlaskModule/API/user/UserQueryTests.py:76 msgid "Missing argument" @@ -478,8 +491,8 @@ msgid "Missing id_asset field" msgstr "" #: modules/FlaskModule/API/service/ServiceQueryAssets.py:173 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:144 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:150 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:141 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:147 msgid "Unknown session" msgstr "" @@ -496,17 +509,17 @@ msgid "Service can't create assets for that session" msgstr "" #: modules/FlaskModule/API/service/ServiceQueryAssets.py:196 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:199 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:196 msgid "Invalid participant" msgstr "" #: modules/FlaskModule/API/service/ServiceQueryAssets.py:204 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:207 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:204 msgid "Invalid user" msgstr "" #: modules/FlaskModule/API/service/ServiceQueryAssets.py:212 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:215 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:212 msgid "Invalid device" msgstr "" @@ -590,6 +603,7 @@ 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 msgid "Missing fields" msgstr "" @@ -621,6 +635,7 @@ msgid "Missing at least one id field" msgstr "" #: 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 @@ -679,27 +694,96 @@ msgstr "" msgid "Missing id_session_type" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryTests.py:134 +#: 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 +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 +msgid "Missing projects" +msgstr "" + +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:129 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:141 +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 +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 +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 +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 +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 +msgid "Badly formatted request" +msgstr "" + +#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:87 +#: modules/FlaskModule/API/user/UserQueryTestType.py:128 +msgid "Missing test_type" +msgstr "" + +#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:155 +msgid "Test type not related to this service. Can't delete." +msgstr "" + +#: modules/FlaskModule/API/service/ServiceQueryTests.py:131 msgid "Missing test field" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryTests.py:140 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:137 msgid "Missing id_test field" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryTests.py:157 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:154 msgid "Missing id_test_type field" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryTests.py:162 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:159 msgid "Invalid test type" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryTests.py:192 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:189 msgid "Service can't create tests for that session" msgstr "" -#: modules/FlaskModule/API/service/ServiceQueryTests.py:269 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:266 msgid "Service can't delete tests for that session" msgstr "" @@ -741,7 +825,7 @@ msgid "A new usergroup must have at least one service access" msgstr "" #: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:207 -#: modules/FlaskModule/API/user/UserQueryUserGroups.py:300 +#: modules/FlaskModule/API/user/UserQueryUserGroups.py:302 msgid "" "Can't delete user group: please delete all users part of that user group " "before deleting." @@ -851,13 +935,6 @@ msgstr "" msgid "Missing id_device" msgstr "" -#: 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 -msgid "Missing projects" -msgstr "" - #: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:157 #: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:165 #: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:188 @@ -931,15 +1008,6 @@ msgstr "" msgid "Missing site ID" msgstr "" -#: 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 -msgid "Badly formatted request" -msgstr "" - #: modules/FlaskModule/API/user/UserQueryDeviceSites.py:252 #: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:263 #: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:252 @@ -1191,11 +1259,11 @@ msgstr "" msgid "Missing role name or id" msgstr "" -#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:229 +#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:231 msgid "Invalid role name or id for that project" msgstr "" -#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:271 +#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:270 msgid "No project access to delete." msgstr "" @@ -1308,21 +1376,11 @@ msgid "" "that type in that project before deleting." msgstr "" -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:161 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:164 -msgid "Missing project ID" -msgstr "" - #: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:163 #: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:160 msgid "Missing session types" msgstr "" -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:196 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:197 -msgid "Unknown format" -msgstr "" - #: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:212 msgid "At least one session type is not associated to the site of its project" msgstr "" @@ -1354,7 +1412,7 @@ msgid "Missing id_service for session type of type service" msgstr "" #: modules/FlaskModule/API/user/UserQuerySessionTypes.py:151 -#: modules/FlaskModule/API/user/UserQueryTestType.py:177 +#: modules/FlaskModule/API/user/UserQueryTestType.py:183 msgid "No site admin access for at least one site in the list" msgstr "" @@ -1363,12 +1421,12 @@ 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:200 +#: modules/FlaskModule/API/user/UserQueryTestType.py:206 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:280 +#: modules/FlaskModule/API/user/UserQueryTestType.py:286 msgid "Session type not associated to project site" msgstr "" @@ -1377,12 +1435,10 @@ msgid "Session type has a a service not associated to its site" msgstr "" #: modules/FlaskModule/API/user/UserQuerySessionTypes.py:316 -#: modules/FlaskModule/API/user/UserQueryTestType.py:324 msgid "Cannot delete because you are not admin in all projects." msgstr "" #: modules/FlaskModule/API/user/UserQuerySessionTypes.py:321 -#: modules/FlaskModule/API/user/UserQueryTestType.py:328 msgid "Unable to delete - not admin in at least one project" msgstr "" @@ -1423,11 +1479,11 @@ msgstr "" msgid "Missing id_site" msgstr "" -#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:225 +#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:230 msgid "Invalid role name or id for that site" msgstr "" -#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:266 +#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:271 msgid "No site access to delete" msgstr "" @@ -1449,55 +1505,28 @@ msgstr "" msgid "Missing id argument" msgstr "" -#: modules/FlaskModule/API/user/UserQueryTestType.py:128 -msgid "Missing test_type" -msgstr "" - -#: modules/FlaskModule/API/user/UserQueryTestType.py:134 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:127 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:119 -msgid "Missing id_test_type" -msgstr "" - -#: modules/FlaskModule/API/user/UserQueryTestType.py:143 -msgid "Not project admin in at least one project" -msgstr "" - -#: modules/FlaskModule/API/user/UserQueryTestType.py:147 +#: modules/FlaskModule/API/user/UserQueryTestType.py:151 msgid "Missing project(s) to associate that test type to" msgstr "" -#: modules/FlaskModule/API/user/UserQueryTestType.py:186 +#: modules/FlaskModule/API/user/UserQueryTestType.py:192 msgid "At least one site isn't associated with the service of that test type" msgstr "" -#: modules/FlaskModule/API/user/UserQueryTestType.py:300 +#: modules/FlaskModule/API/user/UserQueryTestType.py:306 msgid "Test type has a a service not associated to its site" msgstr "" +#: modules/FlaskModule/API/user/UserQueryTestType.py:328 +msgid "Unable to delete - not admin in the related test type service" +msgstr "" + #: modules/FlaskModule/API/user/UserQueryTestType.py:338 msgid "" "Can't delete test type: please delete all tests of that type before " "deleting." msgstr "" -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:141 -msgid "Access denied to at least one project" -msgstr "" - -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:157 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:188 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:285 -msgid "" -"Can't delete test type from project: please delete all tests of that type" -" in the project before deleting." -msgstr "" - -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:166 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:152 -msgid "Missing test types" -msgstr "" - #: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:213 msgid "At least one test type is not associated to the site of its project" msgstr "" @@ -1677,26 +1706,26 @@ msgstr "" msgid "Invalid Token" msgstr "" -#: opentera/db/models/TeraSessionType.py:149 +#: opentera/db/models/TeraSessionType.py:151 #: opentera/forms/TeraSessionForm.py:105 msgid "Unknown" msgstr "" -#: opentera/db/models/TeraSessionType.py:151 +#: opentera/db/models/TeraSessionType.py:153 #: opentera/forms/TeraSessionTypeForm.py:35 #: opentera/forms/TeraTestTypeForm.py:24 msgid "Service" msgstr "" -#: opentera/db/models/TeraSessionType.py:153 +#: opentera/db/models/TeraSessionType.py:155 msgid "File Transfer" msgstr "" -#: opentera/db/models/TeraSessionType.py:155 +#: opentera/db/models/TeraSessionType.py:157 msgid "Data Collect" msgstr "" -#: opentera/db/models/TeraSessionType.py:157 +#: opentera/db/models/TeraSessionType.py:159 msgid "Protocol" msgstr "" @@ -2369,3 +2398,6 @@ msgstr "" #~ msgid "Allow session recording" #~ msgstr "" +#~ msgid "Not project admin in at least one project" +#~ msgstr "" + diff --git a/teraserver/python/translations/fr/LC_MESSAGES/messages.po b/teraserver/python/translations/fr/LC_MESSAGES/messages.po index 73a213a2..6fbedb12 100644 --- a/teraserver/python/translations/fr/LC_MESSAGES/messages.po +++ b/teraserver/python/translations/fr/LC_MESSAGES/messages.po @@ -7,17 +7,16 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2023-05-23 14:24-0400\n" +"POT-Creation-Date: 2023-07-11 08:07-0400\n" "PO-Revision-Date: 2023-05-23 14:28-0400\n" "Last-Translator: \n" -"Language-Team: fr \n" "Language: fr\n" +"Language-Team: fr \n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" "Generated-By: Babel 2.12.1\n" -"X-Generator: Poedit 3.3.1\n" #: modules/FlaskModule/API/device/DeviceLogin.py:82 msgid "Unable to get online devices." @@ -69,6 +68,12 @@ msgstr "Configuration manquante" #: 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:96 #: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:197 #: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:301 @@ -92,7 +97,7 @@ msgstr "Configuration manquante" #: modules/FlaskModule/API/user/UserQueryParticipants.py:363 #: modules/FlaskModule/API/user/UserQueryParticipants.py:366 #: modules/FlaskModule/API/user/UserQueryProjectAccess.py:204 -#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:275 +#: 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 @@ -129,7 +134,7 @@ msgstr "Configuration manquante" #: 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:270 +#: 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 @@ -142,7 +147,7 @@ msgstr "Configuration manquante" #: modules/FlaskModule/API/user/UserQueryStats.py:85 #: modules/FlaskModule/API/user/UserQueryStats.py:90 #: modules/FlaskModule/API/user/UserQueryTestType.py:62 -#: modules/FlaskModule/API/user/UserQueryTestType.py:164 +#: 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 @@ -186,9 +191,14 @@ msgstr "Accès refusé" #: 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/ServiceQueryTests.py:234 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:245 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:280 +#: 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:141 #: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:158 #: modules/FlaskModule/API/service/ServiceQueryUserGroups.py:215 @@ -210,8 +220,8 @@ msgstr "Accès refusé" #: 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:245 -#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:286 +#: 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 @@ -244,13 +254,13 @@ msgstr "Accès refusé" #: 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:241 -#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:281 +#: 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:216 -#: modules/FlaskModule/API/user/UserQueryTestType.py:231 +#: 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 @@ -260,8 +270,8 @@ msgstr "Accès refusé" #: 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:260 -#: modules/FlaskModule/API/user/UserQueryUserGroups.py:308 +#: 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 @@ -319,7 +329,9 @@ msgstr "Accès interdit pour raison de sécurité" #: 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/ServiceQueryTests.py:266 +#: 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 @@ -344,8 +356,10 @@ msgstr "Arguments manquants" #: modules/FlaskModule/API/device/DeviceQuerySessionEvents.py:61 #: modules/FlaskModule/API/device/DeviceQuerySessions.py:114 #: modules/FlaskModule/API/device/DeviceQuerySessions.py:137 -#: modules/LoginModule/LoginModule.py:587 modules/LoginModule/LoginModule.py:687 -#: modules/LoginModule/LoginModule.py:753 modules/LoginModule/LoginModule.py:780 +#: modules/LoginModule/LoginModule.py:587 +#: modules/LoginModule/LoginModule.py:687 +#: modules/LoginModule/LoginModule.py:753 +#: modules/LoginModule/LoginModule.py:780 #: modules/LoginModule/LoginModule.py:799 msgid "Unauthorized" msgstr "Non autorisé" @@ -401,7 +415,7 @@ msgstr "Non implémenté" #: modules/FlaskModule/API/service/ServiceQueryAccess.py:57 #: modules/FlaskModule/API/service/ServiceQueryAssets.py:61 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:60 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:57 #: modules/FlaskModule/API/user/UserQueryAssets.py:60 #: modules/FlaskModule/API/user/UserQueryTests.py:54 msgid "No arguments specified" @@ -429,7 +443,7 @@ 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:63 +#: 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 @@ -437,7 +451,7 @@ msgid "Device access denied" msgstr "Accès à l'appareil interdit" #: modules/FlaskModule/API/service/ServiceQueryAssets.py:68 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:67 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:64 #: modules/FlaskModule/API/user/UserQueryAssets.py:67 #: modules/FlaskModule/API/user/UserQueryTests.py:61 msgid "Session access denied" @@ -445,7 +459,7 @@ 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:71 +#: 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 @@ -454,7 +468,7 @@ 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:75 +#: 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 @@ -462,7 +476,7 @@ msgid "User access denied" msgstr "Accès à l'utilisateur refusé" #: modules/FlaskModule/API/service/ServiceQueryAssets.py:97 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:83 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:80 #: modules/FlaskModule/API/user/UserQueryAssets.py:100 #: modules/FlaskModule/API/user/UserQueryTests.py:76 msgid "Missing argument" @@ -477,8 +491,8 @@ msgid "Missing id_asset field" msgstr "Champ id_asset manquant" #: modules/FlaskModule/API/service/ServiceQueryAssets.py:173 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:144 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:150 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:141 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:147 msgid "Unknown session" msgstr "Séance inconnue" @@ -495,17 +509,17 @@ 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:199 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:196 msgid "Invalid participant" msgstr "Nom de participant incorrect" #: modules/FlaskModule/API/service/ServiceQueryAssets.py:204 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:207 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:204 msgid "Invalid user" msgstr "Utilisateur invalide" #: modules/FlaskModule/API/service/ServiceQueryAssets.py:212 -#: modules/FlaskModule/API/service/ServiceQueryTests.py:215 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:212 msgid "Invalid device" msgstr "Appareil invalide" @@ -542,8 +556,8 @@ msgstr "Courriel de participant incorrect" #: modules/FlaskModule/API/user/UserQueryParticipants.py:312 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:130 #: modules/FlaskModule/API/user/UserQueryParticipants.py:285 @@ -577,11 +591,11 @@ msgstr "Champ id_site manquant" #: modules/FlaskModule/API/service/ServiceQueryProjects.py:169 #: modules/FlaskModule/API/user/UserQueryProjects.py:275 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 msgid "Missing service_role field" @@ -595,6 +609,7 @@ 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 msgid "Missing fields" msgstr "Champs manquants" @@ -613,7 +628,8 @@ msgstr "Champ manquant: id_service_access" #: modules/FlaskModule/API/user/UserQueryServiceAccess.py:112 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 @@ -627,6 +643,7 @@ msgid "Missing at least one id field" msgstr "Au moins un champ id manquant" #: 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 @@ -685,27 +702,98 @@ msgstr "Le service n'a pas accès à au moins un appareil de la séance." msgid "Missing id_session_type" msgstr "Champ id_session_type manquant" -#: modules/FlaskModule/API/service/ServiceQueryTests.py:134 +#: 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 +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 +msgid "Missing projects" +msgstr "Projets manquants" + +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:129 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:141 +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 +msgid "" +"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." + +#: modules/FlaskModule/API/service/ServiceQueryTestTypeProjects.py:152 +#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:161 +#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:164 +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 +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 +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 +msgid "Badly formatted request" +msgstr "Requête mal formée" + +#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:87 +#: modules/FlaskModule/API/user/UserQueryTestType.py:128 +msgid "Missing test_type" +msgstr "Champ test_type manquant" + +#: modules/FlaskModule/API/service/ServiceQueryTestTypes.py:155 +msgid "Test type not related to this service. Can't delete." +msgstr "" + +#: modules/FlaskModule/API/service/ServiceQueryTests.py:131 msgid "Missing test field" msgstr "Champ \"test\" manquant" -#: modules/FlaskModule/API/service/ServiceQueryTests.py:140 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:137 msgid "Missing id_test field" msgstr "Champ id_test manquant" -#: modules/FlaskModule/API/service/ServiceQueryTests.py:157 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:154 msgid "Missing id_test_type field" msgstr "Champ id_test_type manquant" -#: modules/FlaskModule/API/service/ServiceQueryTests.py:162 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:159 msgid "Invalid test type" msgstr "Type de test invalide" -#: modules/FlaskModule/API/service/ServiceQueryTests.py:192 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:189 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:269 +#: modules/FlaskModule/API/service/ServiceQueryTests.py:266 msgid "Service can't delete tests for that session" msgstr "Le service ne peut pas effacer les tests de cette séance" @@ -747,13 +835,13 @@ 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:207 -#: modules/FlaskModule/API/user/UserQueryUserGroups.py:300 +#: modules/FlaskModule/API/user/UserQueryUserGroups.py:302 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 @@ -829,20 +917,21 @@ msgstr "Accès au service refusé" #: modules/FlaskModule/API/user/UserQueryAssets.py:174 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 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/UserQueryDeviceParticipants.py:128 #: modules/FlaskModule/API/user/UserQueryDeviceParticipants.py:207 @@ -864,13 +953,6 @@ msgstr "Appareil non assigné à un projet ou un participant" msgid "Missing id_device" msgstr "Champ manquant : id_device" -#: 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 -msgid "Missing projects" -msgstr "Projets manquants" - #: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:157 #: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:165 #: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:188 @@ -894,13 +976,13 @@ msgstr "Accès refusé" #: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:208 #: modules/FlaskModule/API/user/UserQueryDeviceProjects.py:321 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 @@ -908,24 +990,26 @@ 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" +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 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 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 @@ -937,12 +1021,13 @@ msgstr "Sites manquants" #: modules/FlaskModule/API/user/UserQueryDeviceSites.py:148 #: modules/FlaskModule/API/user/UserQueryDeviceSites.py:175 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." +"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 @@ -951,15 +1036,6 @@ msgstr "" msgid "Missing site ID" msgstr "Champ id_site manquant" -#: 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 -msgid "Badly formatted request" -msgstr "Requête mal formée" - #: modules/FlaskModule/API/user/UserQueryDeviceSites.py:252 #: modules/FlaskModule/API/user/UserQuerySessionTypeSites.py:263 #: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:252 @@ -996,11 +1072,11 @@ msgstr "Sous-type d'appareil non trouvé" #: modules/FlaskModule/API/user/UserQueryDeviceSubTypes.py:180 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:190 msgid "Device subtype successfully deleted" @@ -1040,7 +1116,8 @@ msgstr "Type d'appareil non trouvé" #: modules/FlaskModule/API/user/UserQueryDeviceTypes.py:181 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." @@ -1074,47 +1151,48 @@ msgstr "ID invalide" #: modules/FlaskModule/API/user/UserQueryDevices.py:436 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 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 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 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 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 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." @@ -1139,8 +1217,8 @@ msgstr "Champ id_session_type manquant" #: modules/FlaskModule/API/user/UserQueryForms.py:148 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 msgid "Invalid service specified" @@ -1166,11 +1244,11 @@ msgstr "Champs id_participant_group ou id_project manquants" #: modules/FlaskModule/API/user/UserQueryParticipantGroup.py:170 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:220 msgid "Missing participant" @@ -1203,47 +1281,47 @@ msgstr "Aucune correspondance entre id_project et le projet" #: modules/FlaskModule/API/user/UserQueryParticipants.py:383 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:385 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:388 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:390 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:392 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 msgid "Missing role name or id" msgstr "Nom du rôle ou ID manquant" -#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:229 +#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:231 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:271 +#: modules/FlaskModule/API/user/UserQueryProjectAccess.py:270 msgid "No project access to delete." msgstr "Aucun accès au projet pour supprimer." @@ -1285,11 +1363,11 @@ msgstr "Le format de la configuration est invalide" #: modules/FlaskModule/API/user/UserQueryServiceProjects.py:211 #: modules/FlaskModule/API/user/UserQueryServiceProjects.py:339 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 @@ -1298,7 +1376,8 @@ msgstr "Services manquants" #: modules/FlaskModule/API/user/UserQueryServiceProjects.py:249 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 @@ -1307,33 +1386,35 @@ msgstr "Opération non complétée" #: modules/FlaskModule/API/user/UserQueryServiceProjects.py:332 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 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 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 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 msgid "OpenTera service can't be updated using this API" @@ -1354,57 +1435,47 @@ msgstr "Service invalide" #: modules/FlaskModule/API/user/UserQueryServices.py:251 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 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." - -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:161 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:164 -msgid "Missing project ID" -msgstr "ID de projet manquant" +"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 msgid "Missing session types" msgstr "Champ session_type manquant" -#: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:196 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:197 -msgid "Unknown format" -msgstr "Format inconnu" - #: modules/FlaskModule/API/user/UserQuerySessionTypeProjects.py:212 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 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 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 msgid "Missing session_type" @@ -1419,7 +1490,7 @@ 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:177 +#: modules/FlaskModule/API/user/UserQueryTestType.py:183 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" @@ -1428,39 +1499,38 @@ 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:200 +#: modules/FlaskModule/API/user/UserQueryTestType.py:206 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:280 +#: modules/FlaskModule/API/user/UserQueryTestType.py:286 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 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/UserQueryTestType.py:324 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/UserQueryTestType.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 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 msgid "Missing session participants and users" @@ -1493,11 +1563,11 @@ msgstr "La séance est en cours: impossible de supprimer." msgid "Missing id_site" msgstr "Champ id_site manquant" -#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:225 +#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:230 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:266 +#: modules/FlaskModule/API/user/UserQuerySiteAccess.py:271 msgid "No site access to delete" msgstr "Pas d'accès à effacer" @@ -1511,68 +1581,42 @@ msgstr "Champ id_site manquant" #: modules/FlaskModule/API/user/UserQuerySites.py:187 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:93 msgid "Missing id argument" msgstr "Champ id manquant" -#: modules/FlaskModule/API/user/UserQueryTestType.py:128 -msgid "Missing test_type" -msgstr "Champ test_type manquant" - -#: modules/FlaskModule/API/user/UserQueryTestType.py:134 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:127 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:119 -msgid "Missing id_test_type" -msgstr "Champ id_test_type manquant" - -#: modules/FlaskModule/API/user/UserQueryTestType.py:143 -msgid "Not project admin in at least one project" -msgstr "Pas administrateur de projet pour au moins un projet" - -#: modules/FlaskModule/API/user/UserQueryTestType.py:147 +#: modules/FlaskModule/API/user/UserQueryTestType.py:151 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:186 +#: modules/FlaskModule/API/user/UserQueryTestType.py:192 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:300 +#: modules/FlaskModule/API/user/UserQueryTestType.py:306 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" - -#: modules/FlaskModule/API/user/UserQueryTestType.py:338 -msgid "" -"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." +"Tentative d'association avec un type de test qui a un service non associé" +" à son site" -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:141 -msgid "Access denied to at least one project" -msgstr "Accès refusé pour au moins un projet" +#: modules/FlaskModule/API/user/UserQueryTestType.py:328 +#, fuzzy +msgid "Unable to delete - not admin in the related test type service" +msgstr "Impossible de supprimer - pas administrateur dans au moins un projet" -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:157 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:188 -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:285 +#: modules/FlaskModule/API/user/UserQueryTestType.py:338 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: 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." - -#: modules/FlaskModule/API/user/UserQueryTestTypeProjects.py:166 -#: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:152 -msgid "Missing test types" -msgstr "Types de test manquants" +"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 msgid "At least one test type is not associated to the site of its project" @@ -1582,19 +1626,19 @@ msgstr "Au moins un type de test n'est pas associé au site de ce projet" #: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:176 #: modules/FlaskModule/API/user/UserQueryTestTypeSites.py:266 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 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 msgid "No access to this API" @@ -1644,7 +1688,8 @@ msgstr "Aucun accès au groupe utilisateurs" #: modules/FlaskModule/API/user/UserQueryUserUserGroups.py:113 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 msgid "Can't delete specified relationship" @@ -1688,40 +1733,43 @@ msgstr "Désolé, vous ne pouvez pas vous supprimer!" #: modules/FlaskModule/API/user/UserQueryUsers.py:366 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 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 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 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 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:69 msgid "Wrong ClientVersions" @@ -1752,7 +1800,8 @@ msgstr "Manque le reply code dans les paramètres" msgid "Invalid reply code" msgstr "Le champ reply code est invalide" -#: modules/LoginModule/LoginModule.py:622 modules/LoginModule/LoginModule.py:655 +#: modules/LoginModule/LoginModule.py:622 +#: modules/LoginModule/LoginModule.py:655 msgid "Disabled device" msgstr "Appareil désactivé" @@ -1764,24 +1813,26 @@ msgstr "Jeton invalide" msgid "Invalid Token" msgstr "Jeton invalide" -#: opentera/db/models/TeraSessionType.py:149 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:151 -#: opentera/forms/TeraSessionTypeForm.py:35 opentera/forms/TeraTestTypeForm.py:24 +#: opentera/db/models/TeraSessionType.py:153 +#: opentera/forms/TeraSessionTypeForm.py:35 +#: opentera/forms/TeraTestTypeForm.py:24 msgid "Service" msgstr "Service" -#: opentera/db/models/TeraSessionType.py:153 +#: opentera/db/models/TeraSessionType.py:155 msgid "File Transfer" msgstr "Transfert de fichiers" -#: opentera/db/models/TeraSessionType.py:155 +#: opentera/db/models/TeraSessionType.py:157 msgid "Data Collect" msgstr "Collecte de données" -#: opentera/db/models/TeraSessionType.py:157 +#: opentera/db/models/TeraSessionType.py:159 msgid "Protocol" msgstr "Protocole" @@ -1789,7 +1840,8 @@ 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" @@ -1801,7 +1853,8 @@ 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" @@ -1831,10 +1884,13 @@ msgstr "Se met en ligne?" 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 @@ -1970,7 +2026,8 @@ 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" @@ -2346,7 +2403,8 @@ 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" @@ -2389,20 +2447,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 @@ -2445,3 +2503,7 @@ msgstr "La documentation d’API est désactivée!" #~ msgid "Allow session recording" #~ msgstr "Autoriser l'enregistrement des séances" + +#~ msgid "Not project admin in at least one project" +#~ msgstr "Pas administrateur de projet pour au moins un projet" + From e86b27a34f32bcafdb963ed632b6d551e27e31fc Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Tue, 11 Jul 2023 08:56:03 -0400 Subject: [PATCH 12/80] Refs #222. Upgraded SQLAlchemy to 2.0.18 --- teraserver/python/env/requirements.txt | 2 +- teraserver/python/modules/DatabaseModule/DBManager.py | 4 ++-- teraserver/python/modules/TwistedModule/TwistedModule.py | 4 ++-- teraserver/python/opentera/db/Base.py | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/teraserver/python/env/requirements.txt b/teraserver/python/env/requirements.txt index ba7df68f..37b7a096 100644 --- a/teraserver/python/env/requirements.txt +++ b/teraserver/python/env/requirements.txt @@ -3,7 +3,7 @@ Twisted==22.10.0 treq==22.2.0 cryptography==41.0.2 autobahn==23.6.2 -SQLAlchemy==1.4.48 +SQLAlchemy==2.0.18 sqlalchemy-schemadisplay==1.3 pydot==1.4.2 psycopg2-binary==2.9.6 diff --git a/teraserver/python/modules/DatabaseModule/DBManager.py b/teraserver/python/modules/DatabaseModule/DBManager.py index 69faa031..d630f33c 100755 --- a/teraserver/python/modules/DatabaseModule/DBManager.py +++ b/teraserver/python/modules/DatabaseModule/DBManager.py @@ -1,5 +1,5 @@ from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import event +from sqlalchemy import event, inspect from sqlalchemy.engine import Engine from sqlalchemy.engine.reflection import Inspector from sqlite3 import Connection as SQLite3Connection @@ -297,7 +297,7 @@ def open(self, echo=False): # Init tables with self.app.app_context(): - inspector = Inspector.from_engine(self.db.engine) + inspector = inspect(self.db.engine) tables = inspector.get_table_names() # tables = db.engine.table_names() if not tables: diff --git a/teraserver/python/modules/TwistedModule/TwistedModule.py b/teraserver/python/modules/TwistedModule/TwistedModule.py index 9366e768..eeb6da89 100755 --- a/teraserver/python/modules/TwistedModule/TwistedModule.py +++ b/teraserver/python/modules/TwistedModule/TwistedModule.py @@ -17,7 +17,7 @@ from twisted.web.http import HTTPChannel from twisted.web.server import Site from twisted.web.static import File -from twisted.web import resource +from twisted.web import resource, pages from twisted.web.wsgi import WSGIResource from twisted.python import log from OpenSSL import SSL @@ -131,7 +131,7 @@ def __init__(self, config: ConfigManager): # root_resource = WSGIRootResource(wsgi_resource, {b'wss': wss_resource}) # Avoid using the wss resource at root level - wss_root = resource.ForbiddenResource() + wss_root = pages.forbidden() wss_root.putChild(b'user', wss_user_resource) wss_root.putChild(b'participant', wss_participant_resource) diff --git a/teraserver/python/opentera/db/Base.py b/teraserver/python/opentera/db/Base.py index b3249e28..821cf9eb 100755 --- a/teraserver/python/opentera/db/Base.py +++ b/teraserver/python/opentera/db/Base.py @@ -4,7 +4,7 @@ import time import typing as t import sqlalchemy.sql.sqltypes -from flask_sqlalchemy import SQLAlchemy, BaseQuery, Model +from flask_sqlalchemy import SQLAlchemy, query, model from sqlalchemy.ext.declarative import declarative_base, declared_attr from sqlalchemy import Column, ForeignKey, Integer, String, BigInteger, text from sqlalchemy.inspection import inspect as sqlinspector @@ -14,7 +14,7 @@ class _QueryProperty: - def __get__(self, obj: Model | None, cls: t.Type[Model]) -> BaseQuery: + def __get__(self, obj: model.Model | None, cls: t.Type[model.Model]) -> query.Query: return cls.db().session.query(cls) @@ -322,4 +322,4 @@ def validate_required_fields(cls, json_data: dict, ignore_fields: list = None): # Declarative base, inherit from Base for all models -BaseModel = declarative_base(cls=BaseMixin) +BaseModel = sqlalchemy.orm.declarative_base(cls=BaseMixin) From fdfad72e24e926ddc8787d80e44d972ea6af57ad Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Tue, 11 Jul 2023 09:42:45 -0400 Subject: [PATCH 13/80] Fixes #223. Replaced "_request_ctx_stack" with the global variable 'g' --- .../opentera/services/ServiceAccessManager.py | 40 +++++++++---------- .../LoggingService/LoggingService.json | 2 +- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/teraserver/python/opentera/services/ServiceAccessManager.py b/teraserver/python/opentera/services/ServiceAccessManager.py index c312608b..b3906384 100644 --- a/teraserver/python/opentera/services/ServiceAccessManager.py +++ b/teraserver/python/opentera/services/ServiceAccessManager.py @@ -1,6 +1,6 @@ from werkzeug.local import LocalProxy from functools import wraps -from flask import _request_ctx_stack, request +from flask import g, request from flask_restx import reqparse from typing import List from enum import Enum @@ -17,11 +17,11 @@ import opentera.messages.python as messages # Current client identity, stacked -current_user_client = LocalProxy(lambda: getattr(_request_ctx_stack.top, 'current_user_client', None)) -current_device_client = LocalProxy(lambda: getattr(_request_ctx_stack.top, 'current_device_client', None)) -current_participant_client = LocalProxy(lambda: getattr(_request_ctx_stack.top, 'current_participant_client', None)) -current_service_client = LocalProxy(lambda: getattr(_request_ctx_stack.top, 'current_service_client', None)) -current_login_type = LocalProxy(lambda: getattr(_request_ctx_stack.top, 'current_login_type', LoginType.UNKNOWN_LOGIN)) +current_user_client = LocalProxy(lambda: g.setdefault('current_user_client', None)) +current_device_client = LocalProxy(lambda: g.setdefault('current_device_client', None)) +current_participant_client = LocalProxy(lambda: g.setdefault('current_participant_client', None)) +current_service_client = LocalProxy(lambda: g.setdefault('current_service_client', None)) +current_login_type = LocalProxy(lambda: g.setdefault('current_login_type', LoginType.UNKNOWN_LOGIN)) class LoginType(Enum): @@ -313,9 +313,8 @@ def validate_user_token(token: str) -> bool: return False # User token is valid and not disabled - _request_ctx_stack.top.current_user_client = \ - TeraUserClient(token_dict, token, ServiceAccessManager.service.config_man) - _request_ctx_stack.top.current_login_type = LoginType.USER_LOGIN + g.current_user_client = TeraUserClient(token_dict, token, ServiceAccessManager.service.config_man) + g.current_login_type = LoginType.USER_LOGIN return True @staticmethod @@ -328,9 +327,8 @@ def validate_device_token(token: str, allow_dynamic_tokens: bool, allow_static_t pass else: # Device token - _request_ctx_stack.top.current_device_client = \ - TeraDeviceClient(token_dict, token, ServiceAccessManager.service.config_man) - _request_ctx_stack.top.current_login_type = LoginType.DEVICE_LOGIN + g.current_device_client = TeraDeviceClient(token_dict, token, ServiceAccessManager.service.config_man) + g.current_login_type = LoginType.DEVICE_LOGIN return True if allow_static_tokens: # Check for static device token @@ -341,9 +339,8 @@ def validate_device_token(token: str, allow_dynamic_tokens: bool, allow_static_t pass else: # Device token - _request_ctx_stack.top.current_device_client = \ - TeraDeviceClient(token_dict, token, ServiceAccessManager.service.config_man) - _request_ctx_stack.top.current_login_type = LoginType.DEVICE_LOGIN + g.current_device_client = TeraDeviceClient(token_dict, token, ServiceAccessManager.service.config_man) + g.current_login_type = LoginType.DEVICE_LOGIN return True return False @@ -362,9 +359,9 @@ def validate_participant_token(token: str, allow_dynamic_tokens: bool, allow_sta return False # Participant token is not disabled, everything is ok - _request_ctx_stack.top.current_participant_client = \ + g.current_participant_client = \ TeraParticipantClient(token_dict, token, ServiceAccessManager.service.config_man) - _request_ctx_stack.top.current_login_type = LoginType.PARTICIPANT_LOGIN + g.current_login_type = LoginType.PARTICIPANT_LOGIN return True if allow_static_tokens: # Check for static participant token @@ -376,9 +373,9 @@ def validate_participant_token(token: str, allow_dynamic_tokens: bool, allow_sta pass else: # Participant token - _request_ctx_stack.top.current_participant_client = \ + g.current_participant_client = \ TeraParticipantClient(token_dict, token, ServiceAccessManager.service.config_man) - _request_ctx_stack.top.current_login_type = LoginType.PARTICIPANT_LOGIN + g.current_login_type = LoginType.PARTICIPANT_LOGIN return True return False @@ -392,9 +389,8 @@ def validate_service_token(token: str) -> bool: pass else: # Service token - _request_ctx_stack.top.current_service_client = \ - TeraServiceClient(token_dict, token, ServiceAccessManager.service.config_man) - _request_ctx_stack.top.current_login_type = LoginType.SERVICE_LOGIN + g.current_service_client = TeraServiceClient(token_dict, token, ServiceAccessManager.service.config_man) + g.current_login_type = LoginType.SERVICE_LOGIN return True return False diff --git a/teraserver/python/services/LoggingService/LoggingService.json b/teraserver/python/services/LoggingService/LoggingService.json index 583724ec..a4b9c586 100644 --- a/teraserver/python/services/LoggingService/LoggingService.json +++ b/teraserver/python/services/LoggingService/LoggingService.json @@ -3,7 +3,7 @@ "name": "LoggingService", "hostname": "127.0.0.1", "port": 4041, - "debug_mode": false + "debug_mode": true }, "Backend": { "hostname": "127.0.0.1", From 82f588b0facda6552914cc85cfed46ed5b83879f Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Tue, 11 Jul 2023 13:15:47 -0400 Subject: [PATCH 14/80] Refs #222. Added "future" parameters to use new SQLAlchemy internals. --- .../modules/DatabaseModule/DBManager.py | 3 +- .../python/modules/LoginModule/LoginModule.py | 47 +++++++++---------- teraserver/python/opentera/db/Base.py | 5 +- 3 files changed, 25 insertions(+), 30 deletions(-) diff --git a/teraserver/python/modules/DatabaseModule/DBManager.py b/teraserver/python/modules/DatabaseModule/DBManager.py index d630f33c..02cb8e5d 100755 --- a/teraserver/python/modules/DatabaseModule/DBManager.py +++ b/teraserver/python/modules/DatabaseModule/DBManager.py @@ -74,7 +74,8 @@ def __init__(self, config: ConfigManager, app=flask_app): BaseModule.__init__(self, ModuleNames.DATABASE_MODULE_NAME.value, config) - self.db = SQLAlchemy() + # Future parameters = use only SQLALchemy 2.x features + self.db = SQLAlchemy(engine_options={'future': True}, session_options={'future': True}) self.db_uri = None self.app = app diff --git a/teraserver/python/modules/LoginModule/LoginModule.py b/teraserver/python/modules/LoginModule/LoginModule.py index f49ddee9..a974d19b 100755 --- a/teraserver/python/modules/LoginModule/LoginModule.py +++ b/teraserver/python/modules/LoginModule/LoginModule.py @@ -16,7 +16,7 @@ import datetime import redis -from flask import request, _request_ctx_stack +from flask import request, g from flask_babel import gettext from werkzeug.local import LocalProxy from flask_restx import reqparse @@ -30,16 +30,16 @@ from opentera.utils.UserAgentParser import UserAgentParser # Current participant identity, stacked -current_participant = LocalProxy(lambda: getattr(_request_ctx_stack.top, 'current_participant', None)) +current_participant = LocalProxy(lambda: g.setdefault('current_participant', None)) # Current device identity, stacked -current_device = LocalProxy(lambda: getattr(_request_ctx_stack.top, 'current_device', None)) +current_device = LocalProxy(lambda: g.setdefault('current_device', None)) # Current user identity, stacked -current_user = LocalProxy(lambda: getattr(_request_ctx_stack.top, 'current_user', None)) +current_user = LocalProxy(lambda: g.setdefault('current_user', None)) # Current service identity, stacked -current_service = LocalProxy(lambda: getattr(_request_ctx_stack.top, 'current_service', None)) +current_service = LocalProxy(lambda: g.setdefault('current_service', None)) # Authentication schemes for users user_http_auth = HTTPBasicAuth(realm='user') @@ -102,7 +102,7 @@ def setup_module_pubsub(self): # We wait until we are connected to redis # Every 30 minutes? - loopDeferred = self.cleanup_disabled_tokens_loop_task.start(60.0 * 30) + self.cleanup_disabled_tokens_loop_task.start(60.0 * 30) def notify_module_messages(self, pattern, channel, message): """ @@ -171,8 +171,7 @@ def user_verify_password(self, username, password): self.logger.send_login_event(sender='LoginModule.user_verify_password', level=messages.LogEvent.LOGLEVEL_ERROR, login_type=messages.LoginEvent.LOGIN_TYPE_PASSWORD, - login_status= - messages.LoginEvent.LOGIN_STATUS_FAILED_WITH_WRONG_USERNAME, + login_status=messages.LoginEvent.LOGIN_STATUS_FAILED_WITH_WRONG_USERNAME, client_name=login_infos['client_name'], client_version=login_infos['client_version'], client_ip=login_infos['client_ip'], @@ -195,8 +194,7 @@ def user_verify_password(self, username, password): self.logger.send_login_event(sender='LoginModule.user_verify_password', level=messages.LogEvent.LOGLEVEL_ERROR, login_type=messages.LoginEvent.LOGIN_TYPE_PASSWORD, - login_status= - messages.LoginEvent.LOGIN_STATUS_FAILED_WITH_MAX_ATTEMPTS_REACHED, + login_status=messages.LoginEvent.LOGIN_STATUS_FAILED_WITH_MAX_ATTEMPTS_REACHED, client_name=login_infos['client_name'], client_version=login_infos['client_version'], client_ip=login_infos['client_ip'], @@ -209,7 +207,7 @@ 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(): - _request_ctx_stack.top.current_user = logged_user + g.current_user = logged_user # print('user_verify_password, found user: ', current_user) # current_user.update_last_online() @@ -319,7 +317,7 @@ def user_verify_token(self, token_value): server_endpoint=login_infos['server_endpoint']) return False - _request_ctx_stack.top.current_user = TeraUser.get_user_by_uuid(token_dict['user_uuid']) + g.current_user = TeraUser.get_user_by_uuid(token_dict['user_uuid']) # TODO: Validate if user is also online? if current_user and current_user.is_active(): # current_user.update_last_online() @@ -375,8 +373,7 @@ def participant_verify_password(self, username, password): self.logger.send_login_event(sender='LoginModule.participant_verify_password', level=messages.LogEvent.LOGLEVEL_ERROR, login_type=messages.LoginEvent.LOGIN_TYPE_PASSWORD, - login_status= - messages.LoginEvent.LOGIN_STATUS_FAILED_WITH_MAX_ATTEMPTS_REACHED, + login_status=messages.LoginEvent.LOGIN_STATUS_FAILED_WITH_MAX_ATTEMPTS_REACHED, client_name=login_infos['client_name'], client_version=login_infos['client_version'], client_ip=login_infos['client_ip'], @@ -389,7 +386,7 @@ def participant_verify_password(self, username, password): logged_participant = TeraParticipant.verify_password(username=username, password=password, participant=tentative_participant) if logged_participant and logged_participant.is_active(): - _request_ctx_stack.top.current_participant = TeraParticipant.get_participant_by_username(username) + g.current_participant = TeraParticipant.get_participant_by_username(username) # print('participant_verify_password, found participant: ', current_participant) # current_participant.update_last_online() @@ -397,7 +394,7 @@ def participant_verify_password(self, username, password): login_user(current_participant, remember=True) # Flag that participant has full API access - current_participant.fullAccess = True + g.current_participant.fullAccess = True # Clear attempts counter self.redisDelete(attempts_key) @@ -415,8 +412,7 @@ def participant_verify_password(self, username, password): self.logger.send_login_event(sender='LoginModule.participant_verify_password', level=messages.LogEvent.LOGLEVEL_ERROR, login_type=messages.LoginEvent.LOGIN_TYPE_PASSWORD, - login_status= - messages.LoginEvent.LOGIN_STATUS_FAILED_WITH_WRONG_PASSWORD if + login_status=messages.LoginEvent.LOGIN_STATUS_FAILED_WITH_WRONG_PASSWORD if not logged_participant else messages.LoginEvent.LOGIN_STATUS_FAILED_WITH_DISABLED_ACCOUNT, client_name=login_infos['client_name'], @@ -444,10 +440,11 @@ def participant_verify_token(self, token_value): # print('LoginModule - participant_verify_token for ', token_value, self) # TeraParticipant verifies if the participant is active and login is enabled - _request_ctx_stack.top.current_participant = TeraParticipant.get_participant_by_token(token_value) + g.current_participant = TeraParticipant.get_participant_by_token(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) return True @@ -530,12 +527,12 @@ def participant_verify_token(self, token_value): server_endpoint=login_infos['server_endpoint']) return False - _request_ctx_stack.top.current_participant = \ + g.current_participant = \ TeraParticipant.get_participant_by_uuid(token_dict['participant_uuid']) if current_participant and current_participant.is_active(): # Flag that participant has full API access - current_participant.fullAccess = True + g.current_participant.fullAccess = True # current_participant.update_last_online() login_user(current_participant, remember=True) return True @@ -601,7 +598,7 @@ def decorated(*args, **kwargs): # We are interested in the content of two fields : X-Device-Uuid, X-Participant-Uuid if request.headers.__contains__('X-Device-Uuid'): # Load device from DB - _request_ctx_stack.top.current_device = TeraDevice.get_device_by_uuid( + g.current_device = TeraDevice.get_device_by_uuid( request.headers['X-Device-Uuid']) # Device must be found and enabled @@ -634,7 +631,7 @@ def decorated(*args, **kwargs): # Verify scheme and token if scheme == 'OpenTera': # Load device from DB - _request_ctx_stack.top.current_device = TeraDevice.get_device_by_token(token) + g.current_device = TeraDevice.get_device_by_token(token) # Device must be found and enabled if current_device: @@ -662,7 +659,7 @@ def decorated(*args, **kwargs): # Verify token in params if 'token' in token_args: # Load device from DB - _request_ctx_stack.top.current_device = TeraDevice.get_device_by_token(token_args['token']) + g.current_device = TeraDevice.get_device_by_token(token_args['token']) # Device must be found and enabled if current_device and current_device.device_enabled: @@ -783,7 +780,7 @@ def decorated(*args, **kwargs): # Check if service is allowed to connect service = TeraService.get_service_by_uuid(service_uuid) if service and service.service_enabled: - _request_ctx_stack.top.current_service = service + g.current_service = service return f(*args, **kwargs) # Any other case, do not call function since no valid auth found. diff --git a/teraserver/python/opentera/db/Base.py b/teraserver/python/opentera/db/Base.py index 821cf9eb..36045ece 100755 --- a/teraserver/python/opentera/db/Base.py +++ b/teraserver/python/opentera/db/Base.py @@ -1,16 +1,13 @@ import inspect import datetime -# import uuid import time import typing as t import sqlalchemy.sql.sqltypes from flask_sqlalchemy import SQLAlchemy, query, model -from sqlalchemy.ext.declarative import declarative_base, declared_attr -from sqlalchemy import Column, ForeignKey, Integer, String, BigInteger, text +from sqlalchemy import Column, BigInteger from sqlalchemy.inspection import inspect as sqlinspector from sqlalchemy.exc import IntegrityError, SQLAlchemyError from sqlalchemy.orm import Session -from functools import wraps class _QueryProperty: From e62514857587ee366b198d09b09ab32b496b18ca Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Tue, 11 Jul 2023 14:01:32 -0400 Subject: [PATCH 15/80] Updated to Python 3.11 --- teraserver/python/CMakeLists.txt | 2 +- teraserver/python/env/CMakeLists.txt | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/teraserver/python/CMakeLists.txt b/teraserver/python/CMakeLists.txt index ca48ac38..1a7a938c 100755 --- a/teraserver/python/CMakeLists.txt +++ b/teraserver/python/CMakeLists.txt @@ -1,5 +1,5 @@ #TODO MAKE THIS GENERIC -set (PYTHON_VERSION 3.10) +set (PYTHON_VERSION 3.11) # This will create environment from Anaconda add_subdirectory(env) diff --git a/teraserver/python/env/CMakeLists.txt b/teraserver/python/env/CMakeLists.txt index fb4cfaf6..d2840308 100755 --- a/teraserver/python/env/CMakeLists.txt +++ b/teraserver/python/env/CMakeLists.txt @@ -1,8 +1,6 @@ # Will try to install env if not present. # Conda needs to be installed first - - set (ENV_PATH ${CMAKE_CURRENT_SOURCE_DIR}/python-${PYTHON_VERSION}) file(MAKE_DIRECTORY ${ENV_PATH}) @@ -21,7 +19,6 @@ else(WIN32) endif(WIN32) - # Use the requirements file instead of listing all packages in CMake. add_custom_target( python-requirements @@ -35,10 +32,8 @@ add_custom_command( WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) - # TODO - INSTALL WITH CONDA ? - # Will always be considered out of date... # Always proceed... add_custom_target( From 895b27718e5adc21b199a43cb862b7cfaf91764c Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Tue, 11 Jul 2023 14:09:02 -0400 Subject: [PATCH 16/80] Updated workflows to use Python 3.11.4 --- .github/workflows/gen-doc-and-deploy-to-github-pages.yml | 2 +- .github/workflows/python-package-pypi.yml | 2 +- .github/workflows/run_tests_on_pull_request.yml | 2 +- .gitignore | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gen-doc-and-deploy-to-github-pages.yml b/.github/workflows/gen-doc-and-deploy-to-github-pages.yml index aadfb08a..f433ba78 100644 --- a/.github/workflows/gen-doc-and-deploy-to-github-pages.yml +++ b/.github/workflows/gen-doc-and-deploy-to-github-pages.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.10.7] + python-version: [3.11.4] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/python-package-pypi.yml b/.github/workflows/python-package-pypi.yml index da393e07..12a87bb5 100644 --- a/.github/workflows/python-package-pypi.yml +++ b/.github/workflows/python-package-pypi.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.10.7] + python-version: [3.11.4] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/run_tests_on_pull_request.yml b/.github/workflows/run_tests_on_pull_request.yml index a2028206..d243f241 100644 --- a/.github/workflows/run_tests_on_pull_request.yml +++ b/.github/workflows/run_tests_on_pull_request.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.10.7] + python-version: [3.11.4] steps: - uses: actions/checkout@v2 diff --git a/.gitignore b/.gitignore index 8e70b07d..978230bf 100644 --- a/.gitignore +++ b/.gitignore @@ -92,3 +92,4 @@ joss-paper/paper.jats joss-paper/paper.pdf /.vscode _python-3.10 +python-3.11 From ddbb4cd8eb2c44d966caedf95094ddae09ed5f94 Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Tue, 11 Jul 2023 14:13:25 -0400 Subject: [PATCH 17/80] Fixed workflows. --- .github/workflows/gen-doc-and-deploy-to-github-pages.yml | 2 +- .github/workflows/python-package-pypi.yml | 6 +++--- .github/workflows/run_tests_on_pull_request.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/gen-doc-and-deploy-to-github-pages.yml b/.github/workflows/gen-doc-and-deploy-to-github-pages.yml index f433ba78..2b7c692a 100644 --- a/.github/workflows/gen-doc-and-deploy-to-github-pages.yml +++ b/.github/workflows/gen-doc-and-deploy-to-github-pages.yml @@ -56,7 +56,7 @@ jobs: run: | cmake . make python-all - echo "OPENTERA_PYTHON=$(echo $PWD/python/env/python-3.10/bin/python)" >> $GITHUB_ENV + echo "OPENTERA_PYTHON=$(echo $PWD/python/env/python-3.11/bin/python)" >> $GITHUB_ENV echo "PYTHONPATH=$(echo $PWD/python)" >> $GITHUB_ENV - name: Generate Self Signed TLS Certificates diff --git a/.github/workflows/python-package-pypi.yml b/.github/workflows/python-package-pypi.yml index 12a87bb5..63be6bc5 100644 --- a/.github/workflows/python-package-pypi.yml +++ b/.github/workflows/python-package-pypi.yml @@ -42,8 +42,8 @@ jobs: - name: Build dist package with setuptools working-directory: teraserver/python run: | - ./env/python-3.10/bin/python3 -m pip install --upgrade setuptools wheel twine - ./env/python-3.10/bin/python3 setup.py sdist bdist_wheel + ./env/python-3.11/bin/python3 -m pip install --upgrade setuptools wheel twine + ./env/python-3.11/bin/python3 setup.py sdist bdist_wheel du -h dist - name: Run Tests @@ -56,7 +56,7 @@ jobs: - name: Upload package to PyPi working-directory: teraserver/python run: | - ./env/python-3.10/bin/python3 -m twine upload dist/* + ./env/python-3.11/bin/python3 -m twine upload dist/* env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/run_tests_on_pull_request.yml b/.github/workflows/run_tests_on_pull_request.yml index d243f241..e20c6815 100644 --- a/.github/workflows/run_tests_on_pull_request.yml +++ b/.github/workflows/run_tests_on_pull_request.yml @@ -56,7 +56,7 @@ jobs: run: | cmake . make python-all - echo "OPENTERA_PYTHON=$(echo $PWD/python/env/python-3.10/bin/python)" >> $GITHUB_ENV + echo "OPENTERA_PYTHON=$(echo $PWD/python/env/python-3.11/bin/python)" >> $GITHUB_ENV echo "PYTHONPATH=$(echo $PWD/python)" >> $GITHUB_ENV - name: Generate Self Signed TLS Certificates From 703faee3afd5721ab971e7748009413177531f82 Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Wed, 12 Jul 2023 14:48:46 -0400 Subject: [PATCH 18/80] Refs #203. Implemented base for hard delete - working for assets and devices so far. --- teraserver/python/opentera/db/Base.py | 48 ++++--- .../python/opentera/db/SoftDeleteMixin.py | 99 +++++++++++---- .../python/opentera/db/models/TeraDevice.py | 17 ++- .../opentera/db/models/TeraDeviceProject.py | 6 +- .../opentera/db/models/TeraDeviceSite.py | 7 +- .../opentera/db/models/TeraDeviceSubType.py | 4 +- .../opentera/db/models/TeraDeviceType.py | 4 +- .../opentera/db/models/TeraParticipant.py | 12 +- .../db/models/TeraParticipantGroup.py | 6 +- .../python/opentera/db/models/TeraProject.py | 4 +- .../python/opentera/db/models/TeraService.py | 4 +- .../opentera/db/models/TeraServiceProject.py | 12 +- .../opentera/db/models/TeraServiceSite.py | 6 +- .../python/opentera/db/models/TeraSession.py | 6 + .../opentera/db/models/TeraSessionType.py | 4 +- .../db/models/TeraSessionTypeProject.py | 5 +- .../opentera/db/models/TeraSessionTypeSite.py | 6 +- .../python/opentera/db/models/TeraSite.py | 4 +- .../python/opentera/db/models/TeraTest.py | 5 + .../python/opentera/db/models/TeraTestType.py | 4 +- .../opentera/db/models/TeraTestTypeProject.py | 7 +- .../opentera/db/models/TeraTestTypeSite.py | 7 +- .../python/opentera/db/models/TeraUser.py | 10 +- .../opentera/db/models/TeraUserGroup.py | 2 +- .../opentera/db/models/BaseModelsTest.py | 1 + .../opentera/db/models/test_TeraAsset.py | 36 ++++++ .../opentera/db/models/test_TeraDevice.py | 119 ++++++++++++++++++ .../opentera/db/models/test_TeraUserGroup.py | 2 +- .../db/models/test_TeraUserUserGroup.py | 4 +- 29 files changed, 345 insertions(+), 106 deletions(-) diff --git a/teraserver/python/opentera/db/Base.py b/teraserver/python/opentera/db/Base.py index 36045ece..ce1e94bf 100755 --- a/teraserver/python/opentera/db/Base.py +++ b/teraserver/python/opentera/db/Base.py @@ -196,22 +196,35 @@ def insert(cls, db_object): cls.commit() return db_object - def delete_check_integrity(self) -> IntegrityError | None: + def delete_check_integrity(self, with_deleted: bool = False) -> IntegrityError | None: return None # Can delete by default @classmethod - def delete(cls, id_todel, autocommit: bool = True): - delete_obj = cls.db().session.query(cls).filter(getattr(cls, cls.get_primary_key_name()) == id_todel).first() + def delete(cls, id_todel, autocommit: bool = True, hard_delete: bool = False): + delete_obj = cls.db().session.query(cls).filter(getattr(cls, cls.get_primary_key_name()) == id_todel)\ + .execution_options(include_deleted=hard_delete).first() if delete_obj: cannot_be_deleted_exception = delete_obj.delete_check_integrity() if cannot_be_deleted_exception: raise cannot_be_deleted_exception - if getattr(delete_obj, 'soft_delete', None): + has_soft_delete = getattr(delete_obj, 'soft_delete', None) is not None + has_hard_delete = getattr(delete_obj, 'hard_delete', None) is not None + if has_soft_delete and not hard_delete: delete_obj.soft_delete() else: - cls.db().session.delete(delete_obj) + # if has_soft_delete: + # # Check that object was soft deleted before doing a hard delete + # if not delete_obj.deleted_at: + # # Object must be soft deleted first before being hard deleted! + # raise SQLAlchemyError(cls.__name__ + ' with id ' + str(id_todel) + + # ' cannot be hard deleted: not soft deleted beforehand!') + if has_hard_delete and hard_delete: + delete_obj.hard_delete() + return + else: + cls.db().session.delete(delete_obj) if autocommit: cls.commit() else: @@ -231,21 +244,16 @@ def undelete(cls, id_to_undelete): # @classmethod - # def handle_include_deleted_flag(cls, include_deleted=False): - # if 'include_deleted' not in cls.db().session.info: - # cls.db().session.info['include_deleted'] = list() - # - # if include_deleted: - # cls.db().session.info['include_deleted'].push(cls.__name__) - # else: - # cls.db().session.info['include_deleted'].pop(-1) - - @classmethod - def hard_delete(cls, id_todel): - delete_obj = cls.db().session.query(cls).filter(getattr(cls, cls.get_primary_key_name()) == id_todel).first() - if delete_obj: - cls.db().session.delete(delete_obj) - cls.commit() + # def hard_delete(cls, id_todel): + # delete_obj = cls.db().session.query(cls).execution_options(include_deleted=True)\ + # .filter(getattr(cls, cls.get_primary_key_name()) == id_todel).first() + # if delete_obj: + # if not delete_obj.deleted_at: + # # Object must be soft deleted first before being hard deleted! + # raise SQLAlchemyError(cls.__name__ + ' with id ' + str(id_todel) + + # ' cannot be hard deleted: not soft deleted beforehand!') + # cls.db().session.delete(delete_obj) + # cls.commit() @classmethod def query_with_filters(cls, filters=None, with_deleted: bool = False): diff --git a/teraserver/python/opentera/db/SoftDeleteMixin.py b/teraserver/python/opentera/db/SoftDeleteMixin.py index 86cd3ce1..f88d54ca 100644 --- a/teraserver/python/opentera/db/SoftDeleteMixin.py +++ b/teraserver/python/opentera/db/SoftDeleteMixin.py @@ -25,31 +25,35 @@ def activate_soft_delete_hook(deleted_field_name: str, disable_soft_delete_option_name: str): """Activate an event hook to rewrite the queries.""" # Enable Soft Delete on all Relationship Loads which implement SoftDeleteMixin - # @listens_for(Session, "do_orm_execute") - # def soft_delete_execute(state: ORMExecuteState): - # if not state.is_select: - # return - # if 'include_deleted' in state.session.info and len(state.session.info['include_deleted']) > 0: - # print('test_include_deleted') - # return - # - # adapted = SoftDeleteQueryRewriter(deleted_field_name, disable_soft_delete_option_name).rewrite_statement( - # state.statement - # ) - # state.statement = adapted - @listens_for(Engine, "before_execute", retval=True) - def soft_delete_execute(conn: Connection, clauseelement, multiparams, params, execution_options): - if not isinstance(clauseelement, Select): - return clauseelement, multiparams, params + @listens_for(Session, "do_orm_execute") + def soft_delete_execute(state: ORMExecuteState): + if not state.is_select: + return + + if disable_soft_delete_option_name in state.execution_options \ + and state.execution_options[disable_soft_delete_option_name]: + return - if disable_soft_delete_option_name in execution_options and execution_options[disable_soft_delete_option_name]: - # print('test_include_deleted') - return clauseelement, multiparams, params + if 'include_deleted' in state.session.info and len(state.session.info['include_deleted']) > 0: + return adapted = SoftDeleteQueryRewriter(deleted_field_name, disable_soft_delete_option_name).rewrite_statement( - clauseelement + state.statement ) - return adapted, multiparams, params + state.statement = adapted + # @listens_for(Engine, "before_execute", retval=True) + # def soft_delete_execute(conn: Connection, clauseelement, multiparams, params, execution_options): + # if not isinstance(clauseelement, Select): + # return clauseelement, multiparams, params + # + # if disable_soft_delete_option_name in execution_options and execution_options[disable_soft_delete_option_name]: + # # print('test_include_deleted') + # return clauseelement, multiparams, params + # + # adapted = SoftDeleteQueryRewriter(deleted_field_name, disable_soft_delete_option_name).rewrite_statement( + # clauseelement + # ) + # return adapted, multiparams, params def generate_soft_delete_mixin_class( @@ -62,7 +66,9 @@ def generate_soft_delete_mixin_class( delete_method_default_value: Callable[[], Any] = lambda: datetime.utcnow(), generate_undelete_method: bool = True, undelete_method_name: str = "undelete", - handle_cascade_delete: bool = True + handle_cascade_delete: bool = True, + generate_hard_delete_method: bool = True, + hard_delete_method_name: str = "hard_delete" ) -> Type: """Generate the actual soft-delete Mixin class.""" class_attributes = {deleted_field_name: Column(deleted_field_name, deleted_field_type)} @@ -76,7 +82,6 @@ def get_class_from_tablename(_self, tablename: str) -> DeclarativeMeta | None: class_attributes['get_class_from_tablename'] = get_class_from_tablename if generate_delete_method: - def delete_method(_self, v: Optional[Any] = None): setattr(_self, deleted_field_name, v or delete_method_default_value()) if handle_cascade_delete: @@ -104,8 +109,43 @@ def delete_method(_self, v: Optional[Any] = None): class_attributes[delete_method_name] = delete_method - if generate_undelete_method: + if generate_hard_delete_method: + def hard_delete_method(_self): + _self.handle_include_deleted_flag(True) + # Callback actions before doing hard delete, if required + if getattr(_self, 'hard_delete_before', None): + _self.hard_delete_before() + if handle_cascade_delete: + primary_key_name = inspect(_self.__class__).primary_key[0].name + for relation in inspect(_self.__class__).relationships.items(): + # Relationship has a cascade delete or a secondary table + if relation[1].cascade.delete: + for item in getattr(_self, relation[0]): + # print("Cascade deleting " + str(item)) + hard_item_deleter = getattr(item, hard_delete_method_name) + hard_item_deleter() + if relation[1].secondary is not None and relation[1].passive_deletes: + if deleted_field_name in relation[1].entity.columns.keys(): + model_class = _self.get_class_from_tablename(relation[1].secondary.name) + if model_class: + related_items = model_class.query.filter(text(primary_key_name + "=" + + str(getattr(_self, primary_key_name))) + ).execution_options(include_deleted=True).all() + for item in related_items: + # print("Cascade deleting " + str(model_class) + ": " + primary_key_name + " = " + + # str(getattr(_self, primary_key_name))) + item_hard_deleter = getattr(item, hard_delete_method_name) + item_hard_deleter() + + if _self not in _self.db().session.deleted: + _self.db().session.delete(_self) + _self.commit() + _self.handle_include_deleted_flag(False) + + class_attributes[hard_delete_method_name] = hard_delete_method + + if generate_undelete_method: def undelete_method(_self): if handle_cascade_delete: primary_key_name = inspect(_self.__class__).primary_key[0].name @@ -137,6 +177,17 @@ def undelete_method(_self): activate_soft_delete_hook(deleted_field_name, disable_soft_delete_filtering_option_name) + def handle_include_deleted_flag(_self, include_deleted=False): + if 'include_deleted' not in _self.db().session.info: + _self.db().session.info['include_deleted'] = list() + + if include_deleted: + _self.db().session.info['include_deleted'].append(_self.get_model_name()) + else: + _self.db().session.info['include_deleted'].pop(-1) + + class_attributes['handle_include_deleted_flag'] = handle_include_deleted_flag + generated_class = type(class_name, tuple(), class_attributes) return generated_class diff --git a/teraserver/python/opentera/db/models/TeraDevice.py b/teraserver/python/opentera/db/models/TeraDevice.py index 718041eb..fb02c1e5 100644 --- a/teraserver/python/opentera/db/models/TeraDevice.py +++ b/teraserver/python/opentera/db/models/TeraDevice.py @@ -246,21 +246,26 @@ def update(cls, update_id: int, values: dict): super().update(update_id=update_id, values=values) - def delete_check_integrity(self) -> IntegrityError | None: + def delete_check_integrity(self, with_deleted: bool = False) -> IntegrityError | None: # Safety check - can't delete participants with sessions - if TeraDeviceParticipant.get_count(filters={'id_device': self.id_device}) > 0: + if TeraDeviceParticipant.get_count(filters={'id_device': self.id_device}, with_deleted=with_deleted) > 0: return IntegrityError('Device still associated to participant(s)', self.id_device, 't_devices_participants') - if TeraSessionDevices.get_count(filters={'id_device': self.id_device}) > 0: + if TeraSessionDevices.get_count(filters={'id_device': self.id_device}, with_deleted=with_deleted) > 0: return IntegrityError('Device still has sessions', self.id_device, 't_sessions_devices') - if TeraSession.get_count(filters={'id_creator_device': self.id_device}) > 0: + if TeraSession.get_count(filters={'id_creator_device': self.id_device}, with_deleted=with_deleted) > 0: return IntegrityError('Device still has created sessions', self.id_device, 't_sessions') - if TeraAsset.get_count(filters={'id_device': self.id_device}) > 0: + if TeraAsset.get_count(filters={'id_device': self.id_device}, with_deleted=with_deleted) > 0: return IntegrityError('Device still has created assets', self.id_device, 't_assets') - if TeraTest.get_count(filters={'id_device': self.id_device}) > 0: + if TeraTest.get_count(filters={'id_device': self.id_device}, with_deleted=with_deleted) > 0: return IntegrityError('Device still has created tests', self.id_device, 't_tests') return None + + def hard_delete_before(self): + # Delete sessions that we are part of since they will not be deleted otherwise + for ses in self.device_sessions: + ses.hard_delete() diff --git a/teraserver/python/opentera/db/models/TeraDeviceProject.py b/teraserver/python/opentera/db/models/TeraDeviceProject.py index 84f8f209..2d1dd7b1 100644 --- a/teraserver/python/opentera/db/models/TeraDeviceProject.py +++ b/teraserver/python/opentera/db/models/TeraDeviceProject.py @@ -81,19 +81,19 @@ def delete_with_ids(device_id: int, project_id: int, autocommit: bool = True): if delete_obj: TeraDeviceProject.delete(delete_obj.id_device_project, autocommit=autocommit) - def delete_check_integrity(self) -> IntegrityError | None: + def delete_check_integrity(self, with_deleted: bool = False) -> IntegrityError | None: from opentera.db.models.TeraDeviceParticipant import TeraDeviceParticipant from opentera.db.models.TeraParticipant import TeraParticipant from opentera.db.models.TeraSession import TeraSession - if TeraDeviceParticipant.query.join(TeraParticipant).\ + if TeraDeviceParticipant.query.execution_options(include_deleted=with_deleted).join(TeraParticipant).\ filter(TeraParticipant.id_project == self.id_project).\ filter(TeraDeviceParticipant.id_device == self.id_device).count(): return IntegrityError('Project still has participant associated to the device', self.id_device_project, 't_participants') # Find sessions with matching device and project - device_sessions = TeraSession.get_sessions_for_device(self.id_device) + device_sessions = TeraSession.get_sessions_for_device(self.id_device, with_deleted=with_deleted) device_project_sessions = [ses.id_session for ses in device_sessions if ses.get_associated_project_id() == self.id_project] diff --git a/teraserver/python/opentera/db/models/TeraDeviceSite.py b/teraserver/python/opentera/db/models/TeraDeviceSite.py index 1714822f..da6ed1ad 100644 --- a/teraserver/python/opentera/db/models/TeraDeviceSite.py +++ b/teraserver/python/opentera/db/models/TeraDeviceSite.py @@ -116,17 +116,18 @@ def delete(cls, id_todel, autocommit: bool = True): for device_project in specific_device_projects: TeraDeviceProject.delete(device_project.id_device_project, autocommit=autocommit) - def delete_check_integrity(self) -> IntegrityError | None: + def delete_check_integrity(self, with_deleted: bool = False) -> IntegrityError | None: from opentera.db.models.TeraDeviceProject import TeraDeviceProject from opentera.db.models.TeraProject import TeraProject # Will check if device is part of a project in the site specific_device_projects = TeraDeviceProject.query.join(TeraProject).\ - filter(TeraDeviceProject.id_device == self.id_device).filter(TeraProject.id_site == self.id_site).all() + filter(TeraDeviceProject.id_device == self.id_device).filter(TeraProject.id_site == self.id_site).\ + execution_options(include_deleted=with_deleted).all() # Check integrity of device_projects for device_project in specific_device_projects: - integrity_check = device_project.delete_check_integrity() + integrity_check = device_project.delete_check_integrity(with_deleted) if integrity_check is not None: return integrity_check diff --git a/teraserver/python/opentera/db/models/TeraDeviceSubType.py b/teraserver/python/opentera/db/models/TeraDeviceSubType.py index 62b0f7a7..28768dd5 100644 --- a/teraserver/python/opentera/db/models/TeraDeviceSubType.py +++ b/teraserver/python/opentera/db/models/TeraDeviceSubType.py @@ -60,8 +60,8 @@ def get_device_subtype_by_id(dev_subtype: int): def get_device_subtypes_for_type(dev_type: int): return TeraDeviceSubType.query.filter_by(id_device_type=dev_type).all() - def delete_check_integrity(self) -> IntegrityError | None: - if (TeraDevice.get_count(filters={'id_device_subtype': self.id_device_subtype})) > 0: + def delete_check_integrity(self, with_deleted: bool = False) -> IntegrityError | None: + if (TeraDevice.get_count(filters={'id_device_subtype': self.id_device_subtype}, with_deleted=with_deleted)) > 0: return IntegrityError('Device subtype still have devices with that subtype', self.id_device_subtype, 't_devices') return None diff --git a/teraserver/python/opentera/db/models/TeraDeviceType.py b/teraserver/python/opentera/db/models/TeraDeviceType.py index 1f1205ae..91cebb40 100644 --- a/teraserver/python/opentera/db/models/TeraDeviceType.py +++ b/teraserver/python/opentera/db/models/TeraDeviceType.py @@ -69,10 +69,10 @@ def get_device_type_by_name(dev_name: str): def get_device_type_by_key(dev_key: str): return TeraDeviceType.query.filter_by(device_type_key=dev_key).first() - def delete_check_integrity(self) -> IntegrityError | None: + def delete_check_integrity(self, with_deleted: bool = False) -> IntegrityError | None: # More efficient that relationships from opentera.db.models.TeraDevice import TeraDevice # Here to prevent circular import - if TeraDevice.get_count(filters={'id_device_type': self.id_device_type}) > 0: + if TeraDevice.get_count(filters={'id_device_type': self.id_device_type}, with_deleted=with_deleted) > 0: return IntegrityError('Device Type still has associated devices', self.id_device_type, 't_devices') return None diff --git a/teraserver/python/opentera/db/models/TeraParticipant.py b/teraserver/python/opentera/db/models/TeraParticipant.py index 0f361daf..9118369c 100644 --- a/teraserver/python/opentera/db/models/TeraParticipant.py +++ b/teraserver/python/opentera/db/models/TeraParticipant.py @@ -390,18 +390,20 @@ def insert(cls, participant): raise IntegrityError('Participant project disabled - no insert allowed', -1, 't_projects') TeraParticipant.db().session.commit() - def delete_check_integrity(self) -> IntegrityError | None: + def delete_check_integrity(self, with_deleted: bool = False) -> IntegrityError | None: # Safety check - can't delete participants with sessions - if TeraSessionParticipants.get_session_count_for_participant(self.id_participant) > 0: + if TeraSessionParticipants.get_session_count_for_participant(self.id_participant, + with_deleted=with_deleted) > 0: return IntegrityError('Participant still has sessions', self.id_participant, 't_sessions_participants') - if TeraSession.get_count(filters={'id_creator_participant': self.id_participant}) > 0: + if TeraSession.get_count(filters={'id_creator_participant': self.id_participant}, + with_deleted=with_deleted) > 0: return IntegrityError('Participant still has created sessions', self.id_participant, 't_sessions') - if TeraAsset.get_count(filters={'id_participant': self.id_participant}) > 0: + if TeraAsset.get_count(filters={'id_participant': self.id_participant}, with_deleted=with_deleted) > 0: return IntegrityError('Participant still has created assets', self.id_participant, 't_assets') - if TeraTest.get_count(filters={'id_participant': self.id_participant}) > 0: + if TeraTest.get_count(filters={'id_participant': self.id_participant}, with_deleted=with_deleted) > 0: return IntegrityError('Participant still has created tests', self.id_participant, 't_tests') return None diff --git a/teraserver/python/opentera/db/models/TeraParticipantGroup.py b/teraserver/python/opentera/db/models/TeraParticipantGroup.py index 1431d82b..dcfa2b59 100644 --- a/teraserver/python/opentera/db/models/TeraParticipantGroup.py +++ b/teraserver/python/opentera/db/models/TeraParticipantGroup.py @@ -71,15 +71,15 @@ def create_defaults(test=False): def update(cls, update_id: int, values: dict): # If group project changed, also changed project from all participants in that group if 'id_project' in values: - updated_group:TeraParticipantGroup = TeraParticipantGroup.get_participant_group_by_id(update_id) + updated_group: TeraParticipantGroup = TeraParticipantGroup.get_participant_group_by_id(update_id) if updated_group: for participant in updated_group.participant_group_participants: participant.id_project = values['id_project'] super().update(update_id=update_id, values=values) - def delete_check_integrity(self) -> IntegrityError | None: + def delete_check_integrity(self, with_deleted: bool = False) -> IntegrityError | None: for participant in self.participant_group_participants: - cannot_be_deleted_exception = participant.delete_check_integrity() + cannot_be_deleted_exception = participant.delete_check_integrity(with_deleted=with_deleted) if cannot_be_deleted_exception: return IntegrityError('Participant group still has participant(s)', self.id_participant_group, 't_participants') diff --git a/teraserver/python/opentera/db/models/TeraProject.py b/teraserver/python/opentera/db/models/TeraProject.py index 2538e472..4e967585 100644 --- a/teraserver/python/opentera/db/models/TeraProject.py +++ b/teraserver/python/opentera/db/models/TeraProject.py @@ -141,9 +141,9 @@ def get_project_by_id(project_id, with_deleted: bool = False): # .filter_by(**filter_args).all() # return None - def delete_check_integrity(self) -> IntegrityError | None: + def delete_check_integrity(self, with_deleted: bool = False) -> IntegrityError | None: for participant in self.project_participants: - cannot_be_deleted_exception = participant.delete_check_integrity() + cannot_be_deleted_exception = participant.delete_check_integrity(with_deleted=with_deleted) if cannot_be_deleted_exception: return IntegrityError('Still have participants with session', self.id_project, 't_participants') return None diff --git a/teraserver/python/opentera/db/models/TeraService.py b/teraserver/python/opentera/db/models/TeraService.py index 829af72a..72097178 100644 --- a/teraserver/python/opentera/db/models/TeraService.py +++ b/teraserver/python/opentera/db/models/TeraService.py @@ -239,9 +239,9 @@ def insert(cls, service): # new_role.service_role_name = 'user' # TeraServiceRole.insert(new_role) - def delete_check_integrity(self) -> IntegrityError | None: + def delete_check_integrity(self, with_deleted: bool = False) -> IntegrityError | None: for service_site in self.service_sites: - if service_site.delete_check_integrity(): + if service_site.delete_check_integrity(with_deleted=with_deleted): return IntegrityError('Have sessions, assets or tests using that service', self.id_service, 't_sessions') return None diff --git a/teraserver/python/opentera/db/models/TeraServiceProject.py b/teraserver/python/opentera/db/models/TeraServiceProject.py index 6f17c621..32021bcc 100644 --- a/teraserver/python/opentera/db/models/TeraServiceProject.py +++ b/teraserver/python/opentera/db/models/TeraServiceProject.py @@ -113,24 +113,26 @@ def delete_with_ids(service_id: int, project_id: int, autocommit: bool = True): if delete_obj: TeraServiceProject.delete(delete_obj.id_service_project, autocommit=autocommit) - def delete_check_integrity(self) -> IntegrityError | None: + def delete_check_integrity(self, with_deleted: bool = False) -> IntegrityError | None: # This check will be quite long to process with lot of sessions and data... session_types_ids = \ - [st.id_session_type for st in TeraSessionType.get_session_types_for_service(self.id_service)] + [st.id_session_type for st in TeraSessionType.get_session_types_for_service(self.id_service, + with_deleted=with_deleted)] - sessions = TeraSession.get_sessions_for_project(self.id_project) + sessions = TeraSession.get_sessions_for_project(self.id_project, with_deleted=with_deleted) for session in sessions: if session.id_session_type in session_types_ids: return IntegrityError('Service has sessions of related session type in this project', self.id_service, 't_sessions') if TeraTest.query.join(TeraTestType).filter(TeraTest.id_session == session.id_session).\ filter(or_(TeraTest.id_service == self.id_service, TeraTestType.id_service == self.id_service))\ - .count() > 0: + .execution_options(include_deleted=with_deleted).count() > 0: return IntegrityError('Service has tests of related test type in this project', self.id_service, 't_tests') if TeraAsset.query.filter_by(id_session=session.id_session).\ filter(or_(TeraAsset.asset_service_uuid == self.service_project_service.service_uuid, - TeraAsset.id_service == self.id_service)).count() > 0: + TeraAsset.id_service == self.id_service)).execution_options(include_deleted=with_deleted)\ + .count() > 0: return IntegrityError('Service has related assets in this project', self.id_service, 't_assets') return None diff --git a/teraserver/python/opentera/db/models/TeraServiceSite.py b/teraserver/python/opentera/db/models/TeraServiceSite.py index 93188698..382681e1 100644 --- a/teraserver/python/opentera/db/models/TeraServiceSite.py +++ b/teraserver/python/opentera/db/models/TeraServiceSite.py @@ -124,12 +124,12 @@ def delete(cls, id_todel, autocommit: bool = True): # Ok, delete it super().delete(id_todel, autocommit=autocommit) - def delete_check_integrity(self) -> IntegrityError | None: + def delete_check_integrity(self, with_deleted: bool = False) -> IntegrityError | None: from opentera.db.models.TeraServiceProject import TeraServiceProject - projects = TeraServiceProject.get_projects_for_service(self.id_service) + projects = TeraServiceProject.get_projects_for_service(self.id_service, with_deleted=with_deleted) for service_project in projects: if service_project.service_project_project.id_site == self.id_site: - if service_project.delete_check_integrity(): + if service_project.delete_check_integrity(with_deleted=with_deleted): return IntegrityError('Still have sessions with that service', self.id_service, 't_sessions') return None diff --git a/teraserver/python/opentera/db/models/TeraSession.py b/teraserver/python/opentera/db/models/TeraSession.py index 7ee4fa35..eace6aa2 100644 --- a/teraserver/python/opentera/db/models/TeraSession.py +++ b/teraserver/python/opentera/db/models/TeraSession.py @@ -438,6 +438,12 @@ def terminate_past_inprogress_sessions(): def insert(cls, session): session.session_uuid = str(uuid.uuid4()) + if not session.session_start_datetime: + session.session_start_datetime = datetime.now() + + if not session.session_status: + session.session_status = TeraSessionStatus.STATUS_INPROGRESS.value + if type(session.session_parameters) is dict: # Dumps dictionary into json session.session_parameters = json.dumps(session.session_parameters) diff --git a/teraserver/python/opentera/db/models/TeraSessionType.py b/teraserver/python/opentera/db/models/TeraSessionType.py index 49400c67..a6dd13a7 100644 --- a/teraserver/python/opentera/db/models/TeraSessionType.py +++ b/teraserver/python/opentera/db/models/TeraSessionType.py @@ -160,8 +160,8 @@ def get_category_name(category: SessionCategoryEnum): return name - def delete_check_integrity(self) -> IntegrityError | None: - if TeraSession.get_count(filters={'id_session_type': self.id_session_type}) > 0: + def delete_check_integrity(self, with_deleted: bool = False) -> IntegrityError | None: + if TeraSession.get_count(filters={'id_session_type': self.id_session_type}, with_deleted=with_deleted) > 0: return IntegrityError('Still have sessions with that type', self.id_session_type, 't_sessions') return None diff --git a/teraserver/python/opentera/db/models/TeraSessionTypeProject.py b/teraserver/python/opentera/db/models/TeraSessionTypeProject.py index 4daecfab..f5d89229 100644 --- a/teraserver/python/opentera/db/models/TeraSessionTypeProject.py +++ b/teraserver/python/opentera/db/models/TeraSessionTypeProject.py @@ -140,9 +140,10 @@ def check_integrity(obj_to_check): new_service_project.id_project = obj_to_check.session_type_project_project.id_project TeraServiceProject.insert(new_service_project) - def delete_check_integrity(self) -> IntegrityError | None: + def delete_check_integrity(self, with_deleted: bool = False) -> IntegrityError | None: sessions = TeraSession.get_sessions_for_project(project_id=self.id_project, - session_type_id=self.id_session_type) + session_type_id=self.id_session_type, + with_deleted=with_deleted) if len(sessions) > 0: return IntegrityError('Session type has sessions in this project', self.id_session_type, 't_sessions') return None diff --git a/teraserver/python/opentera/db/models/TeraSessionTypeSite.py b/teraserver/python/opentera/db/models/TeraSessionTypeSite.py index 748fbd35..bf15c07c 100644 --- a/teraserver/python/opentera/db/models/TeraSessionTypeSite.py +++ b/teraserver/python/opentera/db/models/TeraSessionTypeSite.py @@ -179,12 +179,12 @@ def insert(cls, sts): TeraSessionTypeSite.check_integrity(inserted_obj) return inserted_obj - def delete_check_integrity(self) -> IntegrityError | None: + def delete_check_integrity(self, with_deleted: bool = False) -> IntegrityError | None: for project in self.session_type_site_site.site_projects: ses_type_project = TeraSessionTypeProject.get_session_type_project_for_session_type_project( - project.id_project, self.id_session_type) + project.id_project, self.id_session_type, with_deleted=with_deleted) if ses_type_project: - cannot_be_deleted_exception = ses_type_project.delete_check_integrity() + cannot_be_deleted_exception = ses_type_project.delete_check_integrity(with_deleted=with_deleted) if cannot_be_deleted_exception: return IntegrityError('Still have sessions of that type in the site', self.id_session_type, 't_sessions') diff --git a/teraserver/python/opentera/db/models/TeraSite.py b/teraserver/python/opentera/db/models/TeraSite.py index dc471a2d..959593c9 100644 --- a/teraserver/python/opentera/db/models/TeraSite.py +++ b/teraserver/python/opentera/db/models/TeraSite.py @@ -60,9 +60,9 @@ def get_site_by_sitename(sitename, with_deleted: bool = False): def get_site_by_id(site_id: int, with_deleted: bool = False): return TeraSite.query.execution_options(include_deleted=with_deleted).filter_by(id_site=site_id).first() - def delete_check_integrity(self) -> IntegrityError | None: + def delete_check_integrity(self, with_deleted: bool = False) -> IntegrityError | None: for project in self.site_projects: - cannot_be_deleted_exception = project.delete_check_integrity() + cannot_be_deleted_exception = project.delete_check_integrity(with_deleted=with_deleted) if cannot_be_deleted_exception: return IntegrityError('Still have projects with participants with sessions', self.id_site, 't_projects') diff --git a/teraserver/python/opentera/db/models/TeraTest.py b/teraserver/python/opentera/db/models/TeraTest.py index 7704027a..b507a7aa 100644 --- a/teraserver/python/opentera/db/models/TeraTest.py +++ b/teraserver/python/opentera/db/models/TeraTest.py @@ -3,8 +3,10 @@ from opentera.db.Base import BaseModel from opentera.db.SoftDeleteMixin import SoftDeleteMixin from enum import Enum + import uuid import json +from datetime import datetime class TeraTestStatus(Enum): @@ -197,6 +199,9 @@ def insert(cls, test): if isinstance(test.test_summary, dict): test.test_summary = json.dumps(test.test_summary) + if not test.test_datetime: + test.test_datetime = datetime.now() + super().insert(test) @classmethod diff --git a/teraserver/python/opentera/db/models/TeraTestType.py b/teraserver/python/opentera/db/models/TeraTestType.py index e4dba174..ba44cbc5 100644 --- a/teraserver/python/opentera/db/models/TeraTestType.py +++ b/teraserver/python/opentera/db/models/TeraTestType.py @@ -139,8 +139,8 @@ def get_service_urls(self, server_url: str, server_port: int) -> dict: return urls - def delete_check_integrity(self) -> IntegrityError | None: - if TeraTest.get_count(filters={'id_test_type': self.id_test_type}) > 0: + def delete_check_integrity(self, with_deleted: bool = False) -> IntegrityError | None: + if TeraTest.get_count(filters={'id_test_type': self.id_test_type}, with_deleted=with_deleted) > 0: return IntegrityError('Test Type still has associated tests', self.id_test_type, 't_tests') return None diff --git a/teraserver/python/opentera/db/models/TeraTestTypeProject.py b/teraserver/python/opentera/db/models/TeraTestTypeProject.py index d42f189d..43f1cde3 100644 --- a/teraserver/python/opentera/db/models/TeraTestTypeProject.py +++ b/teraserver/python/opentera/db/models/TeraTestTypeProject.py @@ -125,10 +125,11 @@ def insert(cls, ttp): TeraTestTypeProject.check_integrity(inserted_obj) return inserted_obj - def delete_check_integrity(self) -> IntegrityError | None: - session_ids = [session.id_session for session in TeraSession.get_sessions_for_project(self.id_project)] + def delete_check_integrity(self, with_deleted: bool = False) -> IntegrityError | None: + session_ids = [session.id_session for session in TeraSession.get_sessions_for_project(self.id_project, + with_deleted=with_deleted)] test_count = TeraTest.query.filter(TeraTest.id_session.in_(session_ids))\ - .filter(TeraTest.id_test_type == self.id_test_type).count() + .filter(TeraTest.id_test_type == self.id_test_type).execution_options(include_deleted=with_deleted).count() if test_count > 0: return IntegrityError('Test type has tests in this project', self.id_test_type, 't_tests') diff --git a/teraserver/python/opentera/db/models/TeraTestTypeSite.py b/teraserver/python/opentera/db/models/TeraTestTypeSite.py index 818999ba..b8a4cb71 100644 --- a/teraserver/python/opentera/db/models/TeraTestTypeSite.py +++ b/teraserver/python/opentera/db/models/TeraTestTypeSite.py @@ -133,12 +133,13 @@ def insert(cls, tts): TeraTestTypeSite.check_integrity(inserted_obj) return inserted_obj - def delete_check_integrity(self) -> IntegrityError | None: + def delete_check_integrity(self, with_deleted: bool = False) -> IntegrityError | None: for project in self.test_type_site_site.site_projects: test_type_project = TeraTestTypeProject.get_test_type_project_for_test_type_project(project.id_project, - self.id_test_type) + self.id_test_type, + with_deleted) if test_type_project: - cannot_be_deleted_exception = test_type_project.delete_check_integrity() + cannot_be_deleted_exception = test_type_project.delete_check_integrity(with_deleted=with_deleted) if cannot_be_deleted_exception: return IntegrityError('Still have test of that type in the site', self.id_test_type, 't_tests') return None diff --git a/teraserver/python/opentera/db/models/TeraUser.py b/teraserver/python/opentera/db/models/TeraUser.py index 578ea19a..a657fdc3 100755 --- a/teraserver/python/opentera/db/models/TeraUser.py +++ b/teraserver/python/opentera/db/models/TeraUser.py @@ -331,17 +331,17 @@ def insert(cls, user): super().insert(user) - def delete_check_integrity(self) -> IntegrityError | None: - if TeraSessionUsers.get_session_count_for_user(self.id_user) > 0: + def delete_check_integrity(self, with_deleted: bool = False) -> IntegrityError | None: + if TeraSessionUsers.get_session_count_for_user(self.id_user, with_deleted=with_deleted) > 0: return IntegrityError('User still has sessions', self.id_user, 't_sessions_users') - if TeraSession.get_count(filters={'id_creator_user': self.id_user}) > 0: + if TeraSession.get_count(filters={'id_creator_user': self.id_user}, with_deleted=with_deleted) > 0: return IntegrityError('User still has created sessions', self.id_user, 't_sessions') - if TeraAsset.get_count(filters={'id_user': self.id_user}) > 0: + if TeraAsset.get_count(filters={'id_user': self.id_user}, with_deleted=with_deleted) > 0: return IntegrityError('User still has created assets', self.id_user, 't_assets') - if TeraTest.get_count(filters={'id_user': self.id_user}) > 0: + if TeraTest.get_count(filters={'id_user': self.id_user}, with_deleted=with_deleted) > 0: return IntegrityError('User still has created tests', self.id_user, 't_tests') return None diff --git a/teraserver/python/opentera/db/models/TeraUserGroup.py b/teraserver/python/opentera/db/models/TeraUserGroup.py index d1e1d04b..30ef6797 100644 --- a/teraserver/python/opentera/db/models/TeraUserGroup.py +++ b/teraserver/python/opentera/db/models/TeraUserGroup.py @@ -185,7 +185,7 @@ def create_defaults(test=False): TeraUserGroup.db().session.commit() - def delete_check_integrity(self) -> IntegrityError | None: + def delete_check_integrity(self, with_deleted: bool = False) -> IntegrityError | None: if len(self.user_group_users) > 0: return IntegrityError('User group still has associated users', self.id_user_group, 't_users') return None diff --git a/teraserver/python/tests/opentera/db/models/BaseModelsTest.py b/teraserver/python/tests/opentera/db/models/BaseModelsTest.py index 563740e8..a9e41ad0 100644 --- a/teraserver/python/tests/opentera/db/models/BaseModelsTest.py +++ b/teraserver/python/tests/opentera/db/models/BaseModelsTest.py @@ -12,6 +12,7 @@ def setUpClass(cls): cls._flask_app = Flask('BaseModelsTest') cls._flask_app.debug = False cls._flask_app.testing = True + cls._flask_app.config.update({'PROPAGATE_EXCEPTIONS': True}) cls._db_man = DBManager(cls._config, app=cls._flask_app) # Setup DB in RAM cls._db_man.open_local({}, echo=False, ram=True) diff --git a/teraserver/python/tests/opentera/db/models/test_TeraAsset.py b/teraserver/python/tests/opentera/db/models/test_TeraAsset.py index bd0e63f6..351083dc 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraAsset.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraAsset.py @@ -7,6 +7,7 @@ from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraParticipant import TeraParticipant from tests.opentera.db.models.BaseModelsTest import BaseModelsTest +from sqlalchemy.exc import SQLAlchemyError class TeraAssetTest(BaseModelsTest): @@ -274,3 +275,38 @@ def test_delete(self): self.db.session.expire_all() session = TeraSession.get_session_by_id(2) self.assertIsNotNone(session) + + def test_hard_delete(self): + with self._flask_app.app_context(): + # Create new + asset = TeraAsset() + asset.asset_name = 'Test asset' + asset.id_session = TeraSession.get_session_by_id(2).id_session + asset.asset_service_uuid = TeraService.get_service_by_id(1).service_uuid + asset.asset_type = 'application/test' + TeraAsset.insert(asset) + self.assertIsNotNone(asset.id_asset) + id_asset = asset.id_asset + + # Try to hard delete while not soft deleted + # with self.assertRaises(SQLAlchemyError): + # TeraAsset.delete(id_asset, hard_delete=True) + # + # # Soft delete + # TeraAsset.delete(id_asset) + # + # # Assert soft deleted + # asset = TeraAsset.query.filter_by(id_asset=id_asset).execution_options(include_deleted=True).first() + # self.assertIsNotNone(asset) + # self.assertIsNotNone(asset.deleted_at) + + # Hard delete + TeraAsset.delete(id_asset, hard_delete=True) + + # Make sure it is deleted + # Warning, it was deleted, object is not valid anymore + self.assertIsNone(TeraAsset.get_asset_by_id(id_asset, with_deleted=True)) + # Check session is still present + self.db.session.expire_all() + session = TeraSession.get_session_by_id(2) + self.assertIsNotNone(session) diff --git a/teraserver/python/tests/opentera/db/models/test_TeraDevice.py b/teraserver/python/tests/opentera/db/models/test_TeraDevice.py index 1ae81590..58e23fa6 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraDevice.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraDevice.py @@ -1,5 +1,13 @@ +from opentera.db.models.TeraAsset import TeraAsset +from opentera.db.models.TeraTest import TeraTest +from opentera.db.models.TeraService import TeraService from opentera.db.models.TeraDevice import TeraDevice from opentera.db.models.TeraDeviceType import TeraDeviceType +from opentera.db.models.TeraDeviceProject import TeraDeviceProject +from opentera.db.models.TeraDeviceSite import TeraDeviceSite +from opentera.db.models.TeraDeviceParticipant import TeraDeviceParticipant +from opentera.db.models.TeraServiceConfig import TeraServiceConfig +from opentera.db.models.TeraSession import TeraSession from tests.opentera.db.models.BaseModelsTest import BaseModelsTest @@ -107,4 +115,115 @@ def test_soft_delete(self): self.assertIsNotNone(device) self.assertIsNotNone(device.deleted_at) + def test_hard_delete(self): + with self._flask_app.app_context(): + # Create a new device + device = TeraDevice() + device.device_name = 'Test device' + device.id_device_type = TeraDeviceType.get_device_type_by_key('capteur').id_device_type + TeraDevice.insert(device) + self.assertIsNotNone(device.id_device) + id_device = device.id_device + + # Assign device to site + device_site = TeraDeviceSite() + device_site.id_site = 1 + device_site.id_device = id_device + TeraDeviceSite.insert(device_site) + id_device_site = device_site.id_device_site + + # Assign device to project + device_project = TeraDeviceProject() + device_project.id_device = id_device + device_project.id_project = 1 + TeraDeviceProject.insert(device_project) + id_device_project = device_project.id_device_project + + # Assign device to participants + device_participant = TeraDeviceParticipant() + device_participant.id_device = id_device + device_participant.id_participant = 1 + TeraDeviceParticipant.insert(device_participant) + id_device_participant = device_participant.id_device_participant + + # Assign device to sessions + device_session = TeraSession() + device_session.id_creator_device = id_device + device_session.id_session_type = 1 + device_session.session_name = 'Creator device session' + TeraSession.insert(device_session) + id_session = device_session.id_session + + device_session = TeraSession() + device_session.id_creator_service = 1 + device_session.id_session_type = 1 + device_session.session_name = "Device invitee session" + device_session.session_devices = [device] + TeraSession.insert(device_session) + id_session_invitee = device_session.id_session + + # Attach asset + asset = TeraAsset() + asset.asset_name = "Device asset test" + asset.id_device = id_device + asset.id_session = id_session + asset.asset_service_uuid = TeraService.get_openteraserver_service().service_uuid + asset.asset_type = 'Test' + TeraAsset.insert(asset) + id_asset = asset.id_asset + + # ... and test + test = TeraTest() + test.id_device = id_device + test.id_session = id_session + test.id_test_type = 1 + test.test_name = "Device test test!" + TeraTest.insert(test) + id_test = test.id_test + + # Create service config for device + device_service_config = TeraServiceConfig() + device_service_config.id_device = id_device + device_service_config.id_service = 2 + TeraServiceConfig.insert(device_service_config) + id_service_config = device_service_config.id_service_config + + # Soft delete device to prevent relationship integrity errors as we want to test hard-delete cascade here + TeraDeviceParticipant.delete(id_device_participant) + TeraSession.delete(id_session) + TeraSession.delete(id_session_invitee) + TeraDevice.delete(id_device) + + # Check that device and relationships are still there + self.assertIsNone(TeraDevice.get_device_by_id(id_device)) + self.assertIsNotNone(TeraDevice.get_device_by_id(id_device, True)) + self.assertIsNone(TeraDeviceProject.get_device_project_by_id(id_device_project)) + self.assertIsNotNone(TeraDeviceProject.get_device_project_by_id(id_device_project, True)) + self.assertIsNone(TeraDeviceParticipant.get_device_participant_by_id(id_device_participant)) + self.assertIsNotNone(TeraDeviceParticipant.get_device_participant_by_id(id_device_participant, True)) + self.assertIsNone(TeraSession.get_session_by_id(id_session)) + self.assertIsNotNone(TeraSession.get_session_by_id(id_session, True)) + self.assertIsNone(TeraSession.get_session_by_id(id_session_invitee)) + self.assertIsNotNone(TeraSession.get_session_by_id(id_session_invitee, True)) + self.assertIsNone(TeraDeviceSite.get_device_site_by_id(id_device_site)) + self.assertIsNotNone(TeraDeviceSite.get_device_site_by_id(id_device_site, True)) + self.assertIsNone(TeraAsset.get_asset_by_id(id_asset)) + self.assertIsNotNone(TeraAsset.get_asset_by_id(id_asset, True)) + self.assertIsNone(TeraTest.get_test_by_id(id_test)) + self.assertIsNotNone(TeraTest.get_test_by_id(id_test, True)) + self.assertIsNone(TeraServiceConfig.get_service_config_by_id(id_service_config)) + self.assertIsNotNone(TeraServiceConfig.get_service_config_by_id(id_service_config, True)) + + # Hard delete device + TeraDevice.delete(device.id_device, hard_delete=True) + # Make sure device and associations are deleted + self.assertIsNone(TeraDevice.get_device_by_id(id_device, True)) + self.assertIsNone(TeraDeviceProject.get_device_project_by_id(id_device_project, True)) + self.assertIsNone(TeraDeviceParticipant.get_device_participant_by_id(id_device_participant, True)) + self.assertIsNone(TeraSession.get_session_by_id(id_session, True)) + self.assertIsNone(TeraSession.get_session_by_id(id_session_invitee, True)) + self.assertIsNone(TeraAsset.get_asset_by_id(id_asset, True)) + self.assertIsNone(TeraTest.get_test_by_id(id_test, True)) + self.assertIsNone(TeraServiceConfig.get_service_config_by_id(id_service_config, True)) + self.assertIsNone(TeraDeviceSite.get_device_site_by_id(id_device_site, True)) diff --git a/teraserver/python/tests/opentera/db/models/test_TeraUserGroup.py b/teraserver/python/tests/opentera/db/models/test_TeraUserGroup.py index 5b91cd3f..9ee571f5 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraUserGroup.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraUserGroup.py @@ -197,7 +197,7 @@ def test_insert_with_empty_users_and_roles(self): self.assertTrue(new_group.id_user_group > 0) self.assertIsNotNone(TeraUserGroup.get_user_group_by_id(id_user_group)) # Cleanup - TeraUserGroup.hard_delete(new_group.id_user_group) + TeraUserGroup.delete(new_group.id_user_group, hard_delete=True) self.assertIsNone(TeraUserGroup.get_user_group_by_id(id_user_group)) def test_update_with_modified_id_user_group(self): diff --git a/teraserver/python/tests/opentera/db/models/test_TeraUserUserGroup.py b/teraserver/python/tests/opentera/db/models/test_TeraUserUserGroup.py index f70e8b4a..9c3108bf 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraUserUserGroup.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraUserUserGroup.py @@ -58,7 +58,7 @@ def test_insert_with_new_uug(self): self.assertIsNotNone(uug_result) self.assertEqual(uug, uug_result) # Cleanup - TeraUserUserGroup.hard_delete(uug_result.id_user_user_group) + TeraUserUserGroup.delete(uug_result.id_user_user_group, hard_delete=True) self.assertEqual(initial_count, TeraUserUserGroup.query.count()) self.assertIsNone(TeraUserUserGroup.get_user_user_group_by_id( uug_result.id_user_user_group, with_deleted=True)) @@ -128,7 +128,7 @@ def test_hard_delete(self): uug_result = TeraUserUserGroup.insert(uug) self.assertIsNotNone(uug_result) id_user_user_group = uug.id_user_user_group - TeraUserUserGroup.hard_delete(id_user_user_group) + TeraUserUserGroup.delete(id_user_user_group, hard_delete=True) self.assertIsNone(TeraUserUserGroup.get_user_user_group_by_id(id_user_user_group, with_deleted=True)) def test_soft_delete(self): From 912fc54e00ac5f8a444e0b0e40901f1a764b3058 Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Thu, 13 Jul 2023 07:55:50 -0400 Subject: [PATCH 19/80] Refs #203. Implemented hard delete for participant --- .../opentera/db/models/TeraParticipant.py | 5 + .../opentera/db/models/BaseModelsTest.py | 1 + .../db/models/test_TeraParticipant.py | 99 ++++++++++++++++++- .../db/models/test_TeraParticipantGroup.py | 26 +++++ 4 files changed, 126 insertions(+), 5 deletions(-) diff --git a/teraserver/python/opentera/db/models/TeraParticipant.py b/teraserver/python/opentera/db/models/TeraParticipant.py index 9118369c..0dd145d4 100644 --- a/teraserver/python/opentera/db/models/TeraParticipant.py +++ b/teraserver/python/opentera/db/models/TeraParticipant.py @@ -407,3 +407,8 @@ def delete_check_integrity(self, with_deleted: bool = False) -> IntegrityError | return IntegrityError('Participant still has created tests', self.id_participant, 't_tests') return None + + def hard_delete_before(self): + # Delete sessions that we are part of since they will not be deleted otherwise + for ses in self.participant_sessions: + ses.hard_delete() diff --git a/teraserver/python/tests/opentera/db/models/BaseModelsTest.py b/teraserver/python/tests/opentera/db/models/BaseModelsTest.py index a9e41ad0..81c384c2 100644 --- a/teraserver/python/tests/opentera/db/models/BaseModelsTest.py +++ b/teraserver/python/tests/opentera/db/models/BaseModelsTest.py @@ -15,6 +15,7 @@ def setUpClass(cls): cls._flask_app.config.update({'PROPAGATE_EXCEPTIONS': True}) cls._db_man = DBManager(cls._config, app=cls._flask_app) # Setup DB in RAM + # cls._db_man.open_local({'filename': 'D:\\temp\\opentera.db'}, echo=False, ram=False) cls._db_man.open_local({}, echo=False, ram=True) # Creating default users / tests. Time-consuming, only once per test file. diff --git a/teraserver/python/tests/opentera/db/models/test_TeraParticipant.py b/teraserver/python/tests/opentera/db/models/test_TeraParticipant.py index f3d8d362..468287a5 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraParticipant.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraParticipant.py @@ -1,5 +1,10 @@ from opentera.db.models.TeraParticipant import TeraParticipant from opentera.db.models.TeraParticipantGroup import TeraParticipantGroup +from opentera.db.models.TeraSession import TeraSession +from opentera.db.models.TeraAsset import TeraAsset +from opentera.db.models.TeraTest import TeraTest +from opentera.db.models.TeraService import TeraService + import uuid from tests.opentera.db.models.BaseModelsTest import BaseModelsTest @@ -34,12 +39,96 @@ def test_token(self): loadedParticipant = TeraParticipant.get_participant_by_token(token) self.assertEqual(loadedParticipant.participant_uuid, participant.participant_uuid) - def test_json(self): + def test_soft_delete(self): with self._flask_app.app_context(): - return - participant = TeraParticipant.get_participant_by_name('Participant #1') + # Create a new participant + participant = TeraParticipant() + participant.participant_name = "Test participant" + participant.id_project = 1 + TeraParticipant.insert(participant) + self.assertIsNotNone(participant.id_participant) + id_participant = participant.id_participant + + # Delete participant + TeraParticipant.delete(id_participant) + # Make sure participant is deleted + self.assertIsNone(TeraParticipant.get_participant_by_id(id_participant)) + + # Query, with soft delete flag + participant = TeraParticipant.query.filter_by(id_participant=id_participant)\ + .execution_options(include_deleted=True).first() + self.assertIsNotNone(participant) + self.assertIsNotNone(participant.deleted_at) + + def test_hard_delete(self): + with self._flask_app.app_context(): + # Create a new participant + participant = TeraParticipant() + participant.participant_name = "Test participant" + participant.id_project = 1 + TeraParticipant.insert(participant) + self.assertIsNotNone(participant.id_participant) + id_participant = participant.id_participant + + # Assign participant to sessions + part_session = TeraSession() + part_session.id_creator_participant = id_participant + part_session.id_session_type = 1 + part_session.session_name = 'Creator participant session' + TeraSession.insert(part_session) + id_session = part_session.id_session + + part_session = TeraSession() + part_session.id_creator_service = 1 + part_session.id_session_type = 1 + part_session.session_name = "Participant invitee session" + part_session.session_participants = [participant] + TeraSession.insert(part_session) + id_session_invitee = part_session.id_session + + # Attach asset + asset = TeraAsset() + asset.asset_name = "Participant asset test" + asset.id_participant = id_participant + asset.id_session = id_session + asset.asset_service_uuid = TeraService.get_openteraserver_service().service_uuid + asset.asset_type = 'Test' + TeraAsset.insert(asset) + id_asset = asset.id_asset + + # ... and test + test = TeraTest() + test.id_participant = id_participant + test.id_session = id_session + test.id_test_type = 1 + test.test_name = "Device test test!" + TeraTest.insert(test) + id_test = test.id_test + + # Soft delete device to prevent relationship integrity errors as we want to test hard-delete cascade here + TeraSession.delete(id_session) + TeraSession.delete(id_session_invitee) + TeraParticipant.delete(id_participant) + + # Check that device and relationships are still there + self.assertIsNone(TeraParticipant.get_participant_by_id(id_participant)) + self.assertIsNotNone(TeraParticipant.get_participant_by_id(id_participant, True)) + self.assertIsNone(TeraSession.get_session_by_id(id_session)) + self.assertIsNotNone(TeraSession.get_session_by_id(id_session, True)) + self.assertIsNone(TeraSession.get_session_by_id(id_session_invitee)) + self.assertIsNotNone(TeraSession.get_session_by_id(id_session_invitee, True)) + self.assertIsNone(TeraAsset.get_asset_by_id(id_asset)) + self.assertIsNotNone(TeraAsset.get_asset_by_id(id_asset, True)) + self.assertIsNone(TeraTest.get_test_by_id(id_test)) + self.assertIsNotNone(TeraTest.get_test_by_id(id_test, True)) - json = participant.to_json() + # Hard delete participant + TeraParticipant.delete(participant.id_participant, hard_delete=True) - print(json) + # Make sure device and associations are deleted + self.assertIsNone(TeraParticipant.get_participant_by_id(id_participant, True)) + self.assertIsNone(TeraSession.get_session_by_id(id_session, True)) + self.assertIsNone(TeraSession.get_session_by_id(id_session_invitee, True)) + self.assertIsNone(TeraAsset.get_asset_by_id(id_asset, True)) + self.assertIsNone(TeraTest.get_test_by_id(id_test, True)) diff --git a/teraserver/python/tests/opentera/db/models/test_TeraParticipantGroup.py b/teraserver/python/tests/opentera/db/models/test_TeraParticipantGroup.py index 7051964e..4fed7a12 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraParticipantGroup.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraParticipantGroup.py @@ -1,7 +1,33 @@ from tests.opentera.db.models.BaseModelsTest import BaseModelsTest +from opentera.db.models.TeraParticipantGroup import TeraParticipantGroup + class TeraParticipantGroupTest(BaseModelsTest): def test_defaults(self): pass + + def test_soft_delete(self): + with self._flask_app.app_context(): + # Create a new participant group + group = TeraParticipantGroup() + group.participant_group_name = "Test participant group" + group.id_project = 1 + TeraParticipantGroup.insert(group) + self.assertIsNotNone(group.id_participant_group) + id_participant_group = group.id_participant_group + + # Delete participant group + TeraParticipantGroup.delete(id_participant_group) + # Make sure participant group is deleted + self.assertIsNone(TeraParticipantGroup.get_participant_group_by_id(id_participant_group)) + + # Query, with soft delete flag + group = TeraParticipantGroup.query.filter_by(id_participant_group=id_participant_group) \ + .execution_options(include_deleted=True).first() + self.assertIsNotNone(group) + self.assertIsNotNone(group.deleted_at) + + def test_hard_delete(self): + pass From 8a3a480a4529074be33e8dee9e679bc976dcb30b Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Thu, 13 Jul 2023 15:36:10 -0400 Subject: [PATCH 20/80] Refs #203. Added tests for hard delete in all main models. --- teraserver/python/opentera/db/Base.py | 52 +++---- .../python/opentera/db/SoftDeleteMixin.py | 13 +- .../opentera/db/models/TeraSessionType.py | 5 + .../python/opentera/db/models/TeraTestType.py | 3 +- .../python/opentera/db/models/TeraUser.py | 2 +- .../opentera/db/models/TeraUserGroup.py | 1 - .../opentera/db/models/test_TeraAsset.py | 2 +- .../db/models/test_TeraParticipant.py | 2 +- .../db/models/test_TeraParticipantGroup.py | 36 ++++- .../opentera/db/models/test_TeraProject.py | 112 +++++++++++++++ .../opentera/db/models/test_TeraService.py | 131 +++++++++++++----- .../opentera/db/models/test_TeraSession.py | 110 +++++++++++++++ .../db/models/test_TeraSessionType.py | 61 ++++++++ .../tests/opentera/db/models/test_TeraSite.py | 85 +++++++++++- .../tests/opentera/db/models/test_TeraTest.py | 41 ++++++ .../opentera/db/models/test_TeraTestType.py | 58 ++++++++ .../tests/opentera/db/models/test_TeraUser.py | 106 ++++++++++++++ .../opentera/db/models/test_TeraUserGroup.py | 62 ++++++++- 18 files changed, 794 insertions(+), 88 deletions(-) diff --git a/teraserver/python/opentera/db/Base.py b/teraserver/python/opentera/db/Base.py index ce1e94bf..7d177f41 100755 --- a/teraserver/python/opentera/db/Base.py +++ b/teraserver/python/opentera/db/Base.py @@ -15,19 +15,6 @@ def __get__(self, obj: model.Model | None, cls: t.Type[model.Model]) -> query.Qu return cls.db().session.query(cls) -class HandleIncludeDeletedFlag: - def __init__(self, cls): - self.cls = cls - - def __call__(self, f, *args, **kwargs): - if 'include_deleted' not in self.cls.db().session.info: - self.cls.db().session.info['include_deleted'] = list() - self.cls.db().session.info['include_deleted'].push(self.cls.__name__) - retval = f(args, kwargs) - self.cls.db().session.info['include_deleted'].pop(-1) - return retval - - class BaseMixin(object): version_id = Column(BigInteger, nullable=False, default=int(time.time()*1000)) @@ -201,16 +188,21 @@ def delete_check_integrity(self, with_deleted: bool = False) -> IntegrityError | @classmethod def delete(cls, id_todel, autocommit: bool = True, hard_delete: bool = False): - delete_obj = cls.db().session.query(cls).filter(getattr(cls, cls.get_primary_key_name()) == id_todel)\ - .execution_options(include_deleted=hard_delete).first() + if hard_delete: + cls.handle_include_deleted_flag(True) + autocommit = False + delete_obj = cls.db().session.query(cls).filter(getattr(cls, cls.get_primary_key_name()) == id_todel).first() if delete_obj: - cannot_be_deleted_exception = delete_obj.delete_check_integrity() - if cannot_be_deleted_exception: - raise cannot_be_deleted_exception - has_soft_delete = getattr(delete_obj, 'soft_delete', None) is not None has_hard_delete = getattr(delete_obj, 'hard_delete', None) is not None + + if (has_soft_delete and not delete_obj.deleted_at) or not has_soft_delete: + # Don't check integrity for already soft-deleted objects + cannot_be_deleted_exception = delete_obj.delete_check_integrity() + if cannot_be_deleted_exception: + raise cannot_be_deleted_exception + if has_soft_delete and not hard_delete: delete_obj.soft_delete() else: @@ -222,13 +214,14 @@ def delete(cls, id_todel, autocommit: bool = True, hard_delete: bool = False): # ' cannot be hard deleted: not soft deleted beforehand!') if has_hard_delete and hard_delete: delete_obj.hard_delete() - return else: cls.db().session.delete(delete_obj) if autocommit: cls.commit() else: raise SQLAlchemyError(cls.__name__ + ' with id ' + str(id_todel) + ' cannot delete.') + if hard_delete: + cls.handle_include_deleted_flag(False) @classmethod def undelete(cls, id_to_undelete): @@ -242,18 +235,15 @@ def undelete(cls, id_to_undelete): print(cls.__name__ + ' with id ' + str(id_to_undelete) + ' cannot undelete.') raise SQLAlchemyError(cls.__name__ + ' with id ' + str(id_to_undelete) + ' cannot undelete.') + @classmethod + def handle_include_deleted_flag(cls, include_deleted=False): + if 'include_deleted' not in cls.db().session.info: + cls.db().session.info['include_deleted'] = list() - # @classmethod - # def hard_delete(cls, id_todel): - # delete_obj = cls.db().session.query(cls).execution_options(include_deleted=True)\ - # .filter(getattr(cls, cls.get_primary_key_name()) == id_todel).first() - # if delete_obj: - # if not delete_obj.deleted_at: - # # Object must be soft deleted first before being hard deleted! - # raise SQLAlchemyError(cls.__name__ + ' with id ' + str(id_todel) + - # ' cannot be hard deleted: not soft deleted beforehand!') - # cls.db().session.delete(delete_obj) - # cls.commit() + if include_deleted: + cls.db().session.info['include_deleted'].append(cls.get_model_name()) + else: + cls.db().session.info['include_deleted'].pop(-1) @classmethod def query_with_filters(cls, filters=None, with_deleted: bool = False): diff --git a/teraserver/python/opentera/db/SoftDeleteMixin.py b/teraserver/python/opentera/db/SoftDeleteMixin.py index f88d54ca..f4149c3d 100644 --- a/teraserver/python/opentera/db/SoftDeleteMixin.py +++ b/teraserver/python/opentera/db/SoftDeleteMixin.py @@ -121,7 +121,7 @@ def hard_delete_method(_self): # Relationship has a cascade delete or a secondary table if relation[1].cascade.delete: for item in getattr(_self, relation[0]): - # print("Cascade deleting " + str(item)) + print("Cascade deleting " + str(item)) hard_item_deleter = getattr(item, hard_delete_method_name) hard_item_deleter() @@ -177,17 +177,6 @@ def undelete_method(_self): activate_soft_delete_hook(deleted_field_name, disable_soft_delete_filtering_option_name) - def handle_include_deleted_flag(_self, include_deleted=False): - if 'include_deleted' not in _self.db().session.info: - _self.db().session.info['include_deleted'] = list() - - if include_deleted: - _self.db().session.info['include_deleted'].append(_self.get_model_name()) - else: - _self.db().session.info['include_deleted'].pop(-1) - - class_attributes['handle_include_deleted_flag'] = handle_include_deleted_flag - generated_class = type(class_name, tuple(), class_attributes) return generated_class diff --git a/teraserver/python/opentera/db/models/TeraSessionType.py b/teraserver/python/opentera/db/models/TeraSessionType.py index a6dd13a7..b48e8769 100644 --- a/teraserver/python/opentera/db/models/TeraSessionType.py +++ b/teraserver/python/opentera/db/models/TeraSessionType.py @@ -180,3 +180,8 @@ def insert(cls, st): st.id_service = None super().insert(st) + + def hard_delete_before(self): + # Delete sessions related to that session type + for ses in self.session_type_sessions: + ses.hard_delete() diff --git a/teraserver/python/opentera/db/models/TeraTestType.py b/teraserver/python/opentera/db/models/TeraTestType.py index ba44cbc5..2fafc148 100644 --- a/teraserver/python/opentera/db/models/TeraTestType.py +++ b/teraserver/python/opentera/db/models/TeraTestType.py @@ -28,12 +28,13 @@ class TeraTestType(BaseModel, SoftDeleteMixin): test_type_service = relationship("TeraService") test_type_projects = relationship("TeraProject", secondary="t_tests_types_projects") test_type_sites = relationship("TeraSite", secondary="t_tests_types_sites") + test_type_tests = relationship("TeraTest", cascade='delete') def to_json(self, ignore_fields=None, minimal=False): if ignore_fields is None: ignore_fields = [] ignore_fields.extend(['test_type_service', 'test_type_sites', 'test_type_projects', - 'test_type_test_type_projects', 'test_type_test_type_sites']) + 'test_type_test_type_projects', 'test_type_test_type_sites', 'test_type_tests']) if minimal: ignore_fields.extend(['test_type_description']) diff --git a/teraserver/python/opentera/db/models/TeraUser.py b/teraserver/python/opentera/db/models/TeraUser.py index a657fdc3..8c50555a 100755 --- a/teraserver/python/opentera/db/models/TeraUser.py +++ b/teraserver/python/opentera/db/models/TeraUser.py @@ -52,7 +52,7 @@ class TeraUser(BaseModel, SoftDeleteMixin): user_user_groups = relationship("TeraUserGroup", secondary="t_users_users_groups", back_populates="user_group_users", passive_deletes=True) user_sessions = relationship("TeraSession", secondary="t_sessions_users", back_populates="session_users", - passive_deletes=True) + passive_deletes=True, cascade='delete') user_created_sessions = relationship("TeraSession", cascade='delete', back_populates='session_creator_user', passive_deletes=True) diff --git a/teraserver/python/opentera/db/models/TeraUserGroup.py b/teraserver/python/opentera/db/models/TeraUserGroup.py index 30ef6797..9ff635ae 100644 --- a/teraserver/python/opentera/db/models/TeraUserGroup.py +++ b/teraserver/python/opentera/db/models/TeraUserGroup.py @@ -182,7 +182,6 @@ def create_defaults(test=False): rolename='user') access.id_service_role = user_role.id_service_role TeraUserGroup.db().session.add(access) - TeraUserGroup.db().session.commit() def delete_check_integrity(self, with_deleted: bool = False) -> IntegrityError | None: diff --git a/teraserver/python/tests/opentera/db/models/test_TeraAsset.py b/teraserver/python/tests/opentera/db/models/test_TeraAsset.py index 351083dc..b949075c 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraAsset.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraAsset.py @@ -255,7 +255,7 @@ def test_update(self): self.assertEqual('New asset name', asset.asset_name) self.assertEqual('Unknown', asset.asset_type) - def test_delete(self): + def test_soft_delete(self): with self._flask_app.app_context(): # Create new asset = TeraAsset() diff --git a/teraserver/python/tests/opentera/db/models/test_TeraParticipant.py b/teraserver/python/tests/opentera/db/models/test_TeraParticipant.py index 468287a5..1103a616 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraParticipant.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraParticipant.py @@ -110,7 +110,7 @@ def test_hard_delete(self): TeraSession.delete(id_session_invitee) TeraParticipant.delete(id_participant) - # Check that device and relationships are still there + # Check that relationships are still there self.assertIsNone(TeraParticipant.get_participant_by_id(id_participant)) self.assertIsNotNone(TeraParticipant.get_participant_by_id(id_participant, True)) self.assertIsNone(TeraSession.get_session_by_id(id_session)) diff --git a/teraserver/python/tests/opentera/db/models/test_TeraParticipantGroup.py b/teraserver/python/tests/opentera/db/models/test_TeraParticipantGroup.py index 4fed7a12..418ec75c 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraParticipantGroup.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraParticipantGroup.py @@ -1,6 +1,7 @@ from tests.opentera.db.models.BaseModelsTest import BaseModelsTest from opentera.db.models.TeraParticipantGroup import TeraParticipantGroup +from opentera.db.models.TeraParticipant import TeraParticipant class TeraParticipantGroupTest(BaseModelsTest): @@ -30,4 +31,37 @@ def test_soft_delete(self): self.assertIsNotNone(group.deleted_at) def test_hard_delete(self): - pass + with self._flask_app.app_context(): + # Create a new participant group + group = TeraParticipantGroup() + group.participant_group_name = "Test participant group" + group.id_project = 1 + TeraParticipantGroup.insert(group) + self.assertIsNotNone(group.id_participant_group) + id_participant_group = group.id_participant_group + + # Create a new participant in that group + participant = TeraParticipant() + participant.participant_name = "Test participant" + participant.id_project = 1 + participant.id_participant_group = id_participant_group + TeraParticipant.insert(participant) + self.assertIsNotNone(participant.id_participant) + id_participant = participant.id_participant + + # Soft delete device to prevent relationship integrity errors as we want to test hard-delete cascade here + TeraParticipant.delete(id_participant) + TeraParticipantGroup.delete(id_participant_group) + + # Check that relationships are still there + self.assertIsNone(TeraParticipant.get_participant_by_id(id_participant)) + self.assertIsNotNone(TeraParticipant.get_participant_by_id(id_participant, True)) + self.assertIsNone(TeraParticipantGroup.get_participant_group_by_id(id_participant_group)) + self.assertIsNotNone(TeraParticipantGroup.get_participant_group_by_id(id_participant_group, True)) + + # Hard delete + TeraParticipantGroup.delete(id_participant_group, hard_delete=True) + + # Make sure eveything is deleted + self.assertIsNone(TeraParticipant.get_participant_by_id(id_participant, True)) + self.assertIsNone(TeraParticipantGroup.get_participant_group_by_id(id_participant_group, True)) diff --git a/teraserver/python/tests/opentera/db/models/test_TeraProject.py b/teraserver/python/tests/opentera/db/models/test_TeraProject.py index c3ffbd16..6c7e9d1e 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraProject.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraProject.py @@ -3,6 +3,7 @@ from opentera.db.models.TeraProject import TeraProject from opentera.db.models.TeraParticipant import TeraParticipant from opentera.db.models.TeraDevice import TeraDevice +from opentera.db.models.TeraParticipantGroup import TeraParticipantGroup class TeraProjectTest(BaseModelsTest): @@ -173,3 +174,114 @@ def test_update_set_inactive(self): # Check that associated devices are not anymore for device in devices: self.assertEqual([], device.device_participants) + + def test_soft_delete(self): + with self._flask_app.app_context(): + # Create new + project = TeraProject() + project.project_name = "Test project" + project.id_site = 1 + TeraProject.insert(project) + self.assertIsNotNone(project.id_project) + id_project = project.id_project + + # Soft delete + TeraProject.delete(id_project) + + # Make sure it is deleted + self.assertIsNone(TeraProject.get_project_by_id(id_project)) + + # Query, with soft delete flag + project = TeraProject.query.filter_by(id_project=id_project) \ + .execution_options(include_deleted=True).first() + self.assertIsNotNone(project) + self.assertIsNotNone(project.deleted_at) + + def test_hard_delete(self): + with self._flask_app.app_context(): + # Create new + project = TeraProject() + project.project_name = "Test project" + project.id_site = 1 + TeraProject.insert(project) + self.assertIsNotNone(project.id_project) + id_project = project.id_project + + # Create a new participant in that project + participant = TeraParticipant() + participant.participant_name = "Test participant" + participant.id_project = id_project + TeraParticipant.insert(participant) + self.assertIsNotNone(participant.id_participant) + id_participant = participant.id_participant + + # Soft delete to prevent relationship integrity errors as we want to test hard-delete cascade here + TeraParticipant.delete(id_participant) + TeraProject.delete(id_project) + + # Check that relationships are still there + self.assertIsNone(TeraParticipant.get_participant_by_id(id_participant)) + self.assertIsNotNone(TeraParticipant.get_participant_by_id(id_participant, True)) + self.assertIsNone(TeraProject.get_project_by_id(id_project)) + self.assertIsNotNone(TeraProject.get_project_by_id(id_project, True)) + + # Hard delete + TeraProject.delete(id_project, hard_delete=True) + + # Make sure eveything is deleted + self.assertIsNone(TeraParticipant.get_participant_by_id(id_participant, True)) + self.assertIsNone(TeraProject.get_project_by_id(id_project, True)) + + def test_project_relationships_deletion_and_access(self): + with self._flask_app.app_context(): + # Create new + project = TeraProject() + project.project_name = "Test project" + project.id_site = 1 + TeraProject.insert(project) + self.assertIsNotNone(project.id_project) + id_project = project.id_project + + # Create participant groups + group = TeraParticipantGroup() + group.participant_group_name = "Test participant group 1" + group.id_project = id_project + TeraParticipantGroup.insert(group) + self.assertIsNotNone(group.id_participant_group) + id_participant_group1 = group.id_participant_group + + participant = TeraParticipant() + participant.participant_name = "Test participant" + participant.id_project = 1 + participant.id_participant_group = id_participant_group1 + TeraParticipant.insert(participant) + self.assertIsNotNone(participant.id_participant) + id_participant = participant.id_participant + + group = TeraParticipantGroup() + group.participant_group_name = "Test participant group 2" + group.id_project = id_project + TeraParticipantGroup.insert(group) + self.assertIsNotNone(group.id_participant_group) + id_participant_group2 = group.id_participant_group + + self.assertEqual(2, len(project.project_participants_groups)) + + # Soft delete one group + TeraParticipantGroup.delete(id_participant_group2) + + self.db.session.expire_all() + project = TeraProject.get_project_by_id(id_project) + self.assertEqual(1, len(project.project_participants_groups)) + + self.db.session.expire_all() + project = TeraProject.get_project_by_id(id_project, with_deleted=True) + self.assertEqual(1, len(project.project_participants_groups)) # Don't get deleted groups even with flag + + self.assertEqual(1, len(project.project_participants_groups[0].participant_group_participants)) + TeraParticipant.delete(id_participant) + self.db.session.expire_all() + project = TeraProject.get_project_by_id(id_project) + self.assertEqual(0, len(project.project_participants_groups[0].participant_group_participants)) + + diff --git a/teraserver/python/tests/opentera/db/models/test_TeraService.py b/teraserver/python/tests/opentera/db/models/test_TeraService.py index 88ff4692..ff421005 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraService.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraService.py @@ -1,6 +1,7 @@ from tests.opentera.db.models.BaseModelsTest import BaseModelsTest from sqlalchemy import exc from opentera.db.models.TeraService import TeraService +from opentera.db.models.TeraServiceSite import TeraServiceSite class TeraServiceTest(BaseModelsTest): @@ -330,23 +331,23 @@ def test_service_uuid_string(self): # """ return - with self._flask_app.app_context(): - new_service = TeraService() - new_service.service_uuid = 'Definitely longer than a 36 characters string' - new_service.service_name = 'Name' - new_service.service_key = 'key' - new_service.service_hostname = 'Hostname' - new_service.service_port = 2 - new_service.service_endpoint = "Endpoint" - new_service.service_clientendpoint = 'Clientendpoint' - new_service.service_enabled = True - new_service.service_system = True - new_service.service_editable_config = True - self.db.session.add(new_service) - self.db.session.commit() - self.assertRaises(exc.IntegrityError, self.db.session.commit) - - def test_service_port_integer(self): + # with self._flask_app.app_context(): + # new_service = TeraService() + # new_service.service_uuid = 'Definitely longer than a 36 characters string' + # new_service.service_name = 'Name' + # new_service.service_key = 'key' + # new_service.service_hostname = 'Hostname' + # new_service.service_port = 2 + # new_service.service_endpoint = "Endpoint" + # new_service.service_clientendpoint = 'Clientendpoint' + # new_service.service_enabled = True + # new_service.service_system = True + # new_service.service_editable_config = True + # self.db.session.add(new_service) + # self.db.session.commit() + # self.assertRaises(exc.IntegrityError, self.db.session.commit) + + def test_service_port_integer_value(self): """ # # SQLite uses what it calls a dynamic typing system, which ultimately means that you can store text @@ -354,20 +355,84 @@ def test_service_port_integer(self): # attempts to do this will fail - not with SQLite. """ return + # with self._flask_app.app_context(): + # new_service = TeraService() + # + # new_service.service_uuid = 'uuid' + # new_service.service_name = 'Name' + # new_service.service_key = 'key' + # new_service.service_hostname = 'Hostname' + # + # new_service.service_port = 'not an integer' + # + # new_service.service_endpoint = "Endpoint" + # new_service.service_clientendpoint = 'Clientendpoint' + # new_service.service_enabled = True + # new_service.service_system = True + # new_service.service_editable_config = True + # self.db.session.add(new_service) + # self.db.session.commit() + + def test_soft_delete(self): with self._flask_app.app_context(): - new_service = TeraService() - - new_service.service_uuid = 'uuid' - new_service.service_name = 'Name' - new_service.service_key = 'key' - new_service.service_hostname = 'Hostname' - - new_service.service_port = 'not an integer' - - new_service.service_endpoint = "Endpoint" - new_service.service_clientendpoint = 'Clientendpoint' - new_service.service_enabled = True - new_service.service_system = True - new_service.service_editable_config = True - self.db.session.add(new_service) - self.db.session.commit() + # Create new + service = TeraService() + service.service_name = 'Test Service' + service.service_key = 'TestService' + service.service_hostname = 'localhost' + service.service_port = 12345 + service.service_endpoint = 'test' + service.service_clientendpoint = '/' + TeraService.insert(service) + self.assertIsNotNone(service.id_service) + id_service = service.id_service + + # Soft delete + TeraService.delete(id_service) + + # Make sure it is deleted + self.assertIsNone(TeraService.get_service_by_id(id_service)) + + # Query, with soft delete flag + service = TeraService.query.filter_by(id_service=id_service).execution_options(include_deleted=True).first() + self.assertIsNotNone(service) + self.assertIsNotNone(service.deleted_at) + + def test_hard_delete(self): + with self._flask_app.app_context(): + # Create new + service = TeraService() + service.service_name = 'Test Service' + service.service_key = 'TestService' + service.service_hostname = 'localhost' + service.service_port = 12345 + service.service_endpoint = 'test' + service.service_clientendpoint = '/' + TeraService.insert(service) + self.assertIsNotNone(service.id_service) + id_service = service.id_service + + # Create a new site association for that service + site_service = TeraServiceSite() + site_service.id_service = id_service + site_service.id_site = 1 + TeraServiceSite.insert(site_service) + self.assertIsNotNone(site_service.id_service_site) + id_site_service = site_service.id_service_site + + # Soft delete to prevent relationship integrity errors as we want to test hard-delete cascade here + TeraServiceSite.delete(id_site_service) + TeraService.delete(id_service) + + # Check that relationships are still there + self.assertIsNone(TeraService.get_service_by_id(id_service)) + self.assertIsNotNone(TeraService.get_service_by_id(id_service, True)) + self.assertIsNone(TeraServiceSite.get_service_site_by_id(id_site_service)) + self.assertIsNotNone(TeraServiceSite.get_service_site_by_id(id_site_service, True)) + + # Hard delete + TeraService.delete(id_service, hard_delete=True) + + # Make sure eveything is deleted + self.assertIsNone(TeraService.get_service_by_id(id_service, True)) + self.assertIsNone(TeraServiceSite.get_service_site_by_id(id_site_service, True)) diff --git a/teraserver/python/tests/opentera/db/models/test_TeraSession.py b/teraserver/python/tests/opentera/db/models/test_TeraSession.py index d4c29cba..ab5bd96a 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraSession.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraSession.py @@ -1,6 +1,13 @@ from opentera.db.models.TeraParticipant import TeraParticipant from opentera.db.models.TeraDevice import TeraDevice from opentera.db.models.TeraSession import TeraSession, TeraSessionStatus +from opentera.db.models.TeraUser import TeraUser +from opentera.db.models.TeraSessionDevices import TeraSessionDevices +from opentera.db.models.TeraSessionUsers import TeraSessionUsers +from opentera.db.models.TeraSessionParticipants import TeraSessionParticipants +from opentera.db.models.TeraAsset import TeraAsset +from opentera.db.models.TeraTest import TeraTest +from opentera.db.models.TeraService import TeraService from tests.opentera.db.models.BaseModelsTest import BaseModelsTest @@ -49,3 +56,106 @@ def test_session_from_json(self): # 'session_name': 'TEST', # 'session_status': 0, # 'session_start_datetime': str(datetime.now())}} + + def test_soft_delete(self): + with self._flask_app.app_context(): + # Create new + ses = TeraSession() + ses.id_creator_service = 1 + ses.id_session_type = 1 + ses.session_name = "Test session" + ses.session_participants = [TeraParticipant.get_participant_by_id(1)] + ses.session_devices = [TeraDevice.get_device_by_id(1)] + ses.session_users = [TeraUser.get_user_by_id(2)] + TeraSession.insert(ses) + id_session = ses.id_session + + # Attach asset + asset = TeraAsset() + asset.asset_name = "Asset test" + asset.id_device = 1 + asset.id_session = id_session + asset.asset_service_uuid = TeraService.get_openteraserver_service().service_uuid + asset.asset_type = 'Test' + TeraAsset.insert(asset) + id_asset = asset.id_asset + + # ... and test + test = TeraTest() + test.id_device = 1 + test.id_session = id_session + test.id_test_type = 1 + test.test_name = "Test test!" + TeraTest.insert(test) + id_test = test.id_test + + # Soft delete + TeraSession.delete(id_session) + + # Make sure it is deleted + self.assertIsNone(TeraSession.get_session_by_id(id_session)) + + # Query, with soft delete flag + ses = TeraSession.query.filter_by(id_session=id_session).execution_options(include_deleted=True).first() + self.assertIsNotNone(ses) + self.assertIsNotNone(ses.deleted_at) + self.assertIsNone(TeraSessionParticipants.query.filter_by(id_session=id_session).first()) + self.assertIsNone(TeraSessionUsers.query.filter_by(id_session=id_session).first()) + self.assertIsNone(TeraSessionDevices.query.filter_by(id_session=id_session).first()) + self.assertIsNone(TeraAsset.get_asset_by_id(id_asset)) + self.assertIsNone(TeraTest.get_test_by_id(id_test)) + self.assertIsNotNone(TeraSessionParticipants.query.filter_by(id_session=id_session) + .execution_options(include_deleted=True).first()) + self.assertIsNotNone(TeraSessionUsers.query.filter_by(id_session=id_session) + .execution_options(include_deleted=True).first()) + self.assertIsNotNone(TeraSessionDevices.query.filter_by(id_session=id_session) + .execution_options(include_deleted=True).first()) + self.assertIsNotNone(TeraAsset.get_asset_by_id(id_asset, True)) + self.assertIsNotNone(TeraTest.get_test_by_id(id_test, True)) + + def test_hard_delete(self): + with self._flask_app.app_context(): + # Create new + ses = TeraSession() + ses.id_creator_service = 1 + ses.id_session_type = 1 + ses.session_name = "Test session" + ses.session_participants = [TeraParticipant.get_participant_by_id(1)] + ses.session_devices = [TeraDevice.get_device_by_id(1)] + ses.session_users = [TeraUser.get_user_by_id(2)] + TeraSession.insert(ses) + id_session = ses.id_session + + # Attach asset + asset = TeraAsset() + asset.asset_name = "Asset test" + asset.id_device = 1 + asset.id_session = id_session + asset.asset_service_uuid = TeraService.get_openteraserver_service().service_uuid + asset.asset_type = 'Test' + TeraAsset.insert(asset) + id_asset = asset.id_asset + + # ... and test + test = TeraTest() + test.id_device = 1 + test.id_session = id_session + test.id_test_type = 1 + test.test_name = "Test test!" + TeraTest.insert(test) + id_test = test.id_test + + # Hard delete + TeraSession.delete(id_session, hard_delete=True) + + # Make sure eveything is deleted + self.assertIsNone(TeraSession.get_session_by_id(id_session, True)) + self.assertIsNone(TeraSessionParticipants.query.filter_by(id_session=id_session) + .execution_options(include_deleted=True).first()) + self.assertIsNone(TeraSessionUsers.query.filter_by(id_session=id_session) + .execution_options(include_deleted=True).first()) + self.assertIsNone(TeraSessionDevices.query.filter_by(id_session=id_session) + .execution_options(include_deleted=True).first()) + self.assertIsNone(TeraAsset.get_asset_by_id(id_asset, with_deleted=True)) + self.assertIsNone(TeraTest.get_test_by_id(id_test, with_deleted=True)) + diff --git a/teraserver/python/tests/opentera/db/models/test_TeraSessionType.py b/teraserver/python/tests/opentera/db/models/test_TeraSessionType.py index a2cadb43..8f8f2e7b 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraSessionType.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraSessionType.py @@ -1,4 +1,6 @@ from tests.opentera.db.models.BaseModelsTest import BaseModelsTest +from opentera.db.models.TeraSessionType import TeraSessionType +from opentera.db.models.TeraSession import TeraSession class TeraSessionTypeTest(BaseModelsTest): @@ -6,3 +8,62 @@ class TeraSessionTypeTest(BaseModelsTest): def test_defaults(self): with self._flask_app.app_context(): pass + + def test_soft_delete(self): + with self._flask_app.app_context(): + # Create new + ses_type = TeraSessionType() + ses_type.session_type_online = False + ses_type.session_type_color = "" + ses_type.session_type_category = TeraSessionType.SessionCategoryEnum.DATACOLLECT.value + ses_type.session_type_name = 'Session Type Test' + TeraSessionType.insert(ses_type) + id_session_type = ses_type.id_session_type + + # Soft delete + TeraSessionType.delete(id_session_type) + + # Make sure it is deleted + self.assertIsNone(TeraSessionType.get_session_type_by_id(id_session_type)) + + # Query, with soft delete flag + ses_type = TeraSessionType.query.filter_by(id_session_type=id_session_type).\ + execution_options(include_deleted=True).first() + self.assertIsNotNone(ses_type) + self.assertIsNotNone(ses_type.deleted_at) + + def test_hard_delete(self): + with self._flask_app.app_context(): + # Create new + ses_type = TeraSessionType() + ses_type.session_type_online = False + ses_type.session_type_color = "" + ses_type.session_type_category = TeraSessionType.SessionCategoryEnum.DATACOLLECT.value + ses_type.session_type_name = 'Session Type Test' + TeraSessionType.insert(ses_type) + id_session_type = ses_type.id_session_type + + # Create a new session of that session type + ses = TeraSession() + ses.id_creator_service = 1 + ses.id_session_type = id_session_type + ses.session_name = "Test session" + TeraSession.insert(ses) + id_session = ses.id_session + + # Soft delete to prevent relationship integrity errors as we want to test hard-delete cascade here + TeraSession.delete(id_session) + TeraSessionType.delete(id_session_type) + + # Check that relationships are still there + self.assertIsNone(TeraSessionType.get_session_type_by_id(id_session_type)) + self.assertIsNotNone(TeraSessionType.get_session_type_by_id(id_session_type, True)) + self.assertIsNone(TeraSession.get_session_by_id(id_session)) + self.assertIsNotNone(TeraSession.get_session_by_id(id_session, True)) + + # Hard delete + TeraSessionType.delete(id_session_type, hard_delete=True) + + # Make sure eveything is deleted + self.assertIsNone(TeraSessionType.get_session_type_by_id(id_session_type, True)) + self.assertIsNone(TeraSession.get_session_by_id(id_session, True)) diff --git a/teraserver/python/tests/opentera/db/models/test_TeraSite.py b/teraserver/python/tests/opentera/db/models/test_TeraSite.py index 480d8f8f..86442fbe 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraSite.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraSite.py @@ -1,6 +1,9 @@ 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 +from opentera.db.models.TeraParticipant import TeraParticipant +from opentera.db.models.TeraSession import TeraSession class TeraSiteTest(BaseModelsTest): @@ -93,7 +96,83 @@ def test_insert_and_delete(self): self.assertGreaterEqual(new_site.id_site, 1) id_to_del = TeraSite.get_site_by_id(new_site.id_site).id_site TeraSite.delete(id_todel=id_to_del) - Same_site = TeraSite() - Same_site.site_name = 'test_insert_and_delete' - self.db.session.add(Same_site) + same_site = TeraSite() + same_site.site_name = 'test_insert_and_delete' + self.db.session.add(same_site) self.db.session.commit() + + def test_soft_delete(self): + with self._flask_app.app_context(): + # Create new + site = TeraSite() + site.site_name = "Test Site" + TeraSite.insert(site) + id_site = site.id_site + + # Soft delete + TeraSite.delete(id_site) + + # Make sure it is deleted + self.assertIsNone(TeraSite.get_site_by_id(id_site)) + + # Query, with soft delete flag + site = TeraSite.query.filter_by(id_site=id_site).execution_options(include_deleted=True).first() + self.assertIsNotNone(site) + self.assertIsNotNone(site.deleted_at) + + def test_hard_delete(self): + with self._flask_app.app_context(): + # Create new + site = TeraSite() + site.site_name = "Test Site" + TeraSite.insert(site) + id_site = site.id_site + + project = TeraProject() + project.project_name = "Test project" + project.id_site = id_site + TeraProject.insert(project) + self.assertIsNotNone(project.id_project) + id_project = project.id_project + + participant = TeraParticipant() + participant.participant_name = "Test participant" + participant.id_project = id_project + TeraParticipant.insert(participant) + self.assertIsNotNone(participant.id_participant) + id_participant = participant.id_participant + + ses = TeraSession() + ses.id_creator_participant = id_participant + ses.id_session_type = 1 + ses.session_name = "Test session" + ses.session_participants = [participant] + TeraSession.insert(ses) + id_session = ses.id_session + + # Soft delete to prevent relationship integrity errors as we want to test hard-delete cascade here + TeraSession.delete(id_session) + TeraProject.delete(id_project) + TeraSite.delete(id_site) + + # Check that relationships are still there + self.assertIsNone(TeraSite.get_site_by_id(id_site)) + self.assertIsNotNone(TeraSite.get_site_by_id(id_site, True)) + self.assertIsNone(TeraProject.get_project_by_id(id_project)) + self.assertIsNotNone(TeraProject.get_project_by_id(id_project, True)) + self.assertIsNone(TeraParticipant.get_participant_by_id(id_participant)) + self.assertIsNotNone(TeraParticipant.get_participant_by_id(id_participant, True)) + self.assertIsNone(TeraSession.get_session_by_id(id_session)) + self.assertIsNotNone(TeraSession.get_session_by_id(id_session, True)) + + # Hard delete + self.db.session.expire_all() + TeraSite.delete(id_site, hard_delete=True) + + # Make sure eveything is deleted + self.assertIsNone(TeraSite.get_site_by_id(id_site, True)) + self.assertIsNone(TeraProject.get_project_by_id(id_project, True)) + self.assertIsNone(TeraParticipant.get_participant_by_id(id_participant, True)) + self.assertIsNone(TeraSession.get_session_by_id(id_session, True)) + + diff --git a/teraserver/python/tests/opentera/db/models/test_TeraTest.py b/teraserver/python/tests/opentera/db/models/test_TeraTest.py index 66065d92..746f776a 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraTest.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraTest.py @@ -1,4 +1,5 @@ from tests.opentera.db.models.BaseModelsTest import BaseModelsTest +from opentera.db.models.TeraTest import TeraTest class TeraTestTest(BaseModelsTest): @@ -6,3 +7,43 @@ class TeraTestTest(BaseModelsTest): def test_defaults(self): with self._flask_app.app_context(): pass + + def test_soft_delete(self): + with self._flask_app.app_context(): + # Create new + test = TeraTest() + test.id_participant = 1 + test.id_session = 1 + test.id_test_type = 1 + test.test_name = "Test test!" + TeraTest.insert(test) + id_test = test.id_test + + # Soft delete + TeraTest.delete(id_test) + + # Make sure it is deleted + self.assertIsNone(TeraTest.get_test_by_id(id_test)) + + # Query, with soft delete flag + test = TeraTest.query.filter_by(id_test=id_test).execution_options(include_deleted=True).first() + self.assertIsNotNone(test) + self.assertIsNotNone(test.deleted_at) + + def test_hard_delete(self): + with self._flask_app.app_context(): + # Create new + test = TeraTest() + test.id_participant = 1 + test.id_session = 1 + test.id_test_type = 1 + test.test_name = "Test test!" + TeraTest.insert(test) + self.assertIsNotNone(test.id_test) + id_test = test.id_test + + # Hard delete + TeraTest.delete(id_test, hard_delete=True) + + # Make sure eveything is deleted + self.assertIsNone(TeraTest.get_test_by_id(id_test, True)) diff --git a/teraserver/python/tests/opentera/db/models/test_TeraTestType.py b/teraserver/python/tests/opentera/db/models/test_TeraTestType.py index 9679baa1..714de598 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraTestType.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraTestType.py @@ -1,4 +1,6 @@ from tests.opentera.db.models.BaseModelsTest import BaseModelsTest +from opentera.db.models.TeraTestType import TeraTestType +from opentera.db.models.TeraTest import TeraTest class TeraTestTypeTest(BaseModelsTest): @@ -6,3 +8,59 @@ class TeraTestTypeTest(BaseModelsTest): def test_defaults(self): with self._flask_app.app_context(): pass + + def test_soft_delete(self): + with self._flask_app.app_context(): + # Create new + test_type = TeraTestType() + test_type.test_type_name = "Test test type..." + test_type.id_service = 1 + TeraTestType.insert(test_type) + id_test_type = test_type.id_test_type + + # Soft delete + TeraTestType.delete(id_test_type) + + # Make sure it is deleted + self.assertIsNone(TeraTestType.get_test_type_by_id(id_test_type)) + + # Query, with soft delete flag + test_type = TeraTestType.query.filter_by(id_test_type=id_test_type)\ + .execution_options(include_deleted=True).first() + self.assertIsNotNone(test_type) + self.assertIsNotNone(test_type.deleted_at) + + def test_hard_delete(self): + with self._flask_app.app_context(): + # Create new + test_type = TeraTestType() + test_type.test_type_name = "Test test type..." + test_type.id_service = 1 + TeraTestType.insert(test_type) + id_test_type = test_type.id_test_type + + test = TeraTest() + test.id_participant = 1 + test.id_session = 1 + test.id_test_type = id_test_type + test.test_name = "Test test!" + TeraTest.insert(test) + self.assertIsNotNone(test.id_test) + id_test = test.id_test + + # Soft delete to prevent relationship integrity errors as we want to test hard-delete cascade here + TeraTest.delete(id_test) + TeraTestType.delete(id_test_type) + + # Check that relationships are still there + self.assertIsNone(TeraTest.get_test_by_id(id_test)) + self.assertIsNotNone(TeraTest.get_test_by_id(id_test, True)) + self.assertIsNone(TeraTestType.get_test_type_by_id(id_test_type)) + self.assertIsNotNone(TeraTestType.get_test_type_by_id(id_test_type, True)) + + # Hard delete + TeraTestType.delete(id_test_type, hard_delete=True) + + # Make sure eveything is deleted + self.assertIsNone(TeraTest.get_test_by_id(id_test, True)) + self.assertIsNone(TeraTestType.get_test_type_by_id(id_test_type, True)) diff --git a/teraserver/python/tests/opentera/db/models/test_TeraUser.py b/teraserver/python/tests/opentera/db/models/test_TeraUser.py index 3fb0e47e..d70b8216 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraUser.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraUser.py @@ -2,6 +2,10 @@ from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraSite import TeraSite from opentera.db.models.TeraProject import TeraProject +from opentera.db.models.TeraSession import TeraSession +from opentera.db.models.TeraAsset import TeraAsset +from opentera.db.models.TeraTest import TeraTest +from opentera.db.models.TeraService import TeraService from tests.opentera.db.models.BaseModelsTest import BaseModelsTest @@ -64,3 +68,105 @@ def test_multisite_user(self): # Verify that multi can access 2 projects projects = DBManager.userAccess(multi).get_accessible_projects() self.assertEqual(len(projects), 2) + + def test_soft_delete(self): + with self._flask_app.app_context(): + # Create new + user = TeraUser() + user.user_enabled = True + user.user_firstname = "Test" + user.user_lastname = "User" + user.user_profile = "" + user.user_password = TeraUser.encrypt_password("test") + user.user_superadmin = False + user.user_username = "test" + TeraUser.insert(user) + self.assertIsNotNone(user.id_user) + id_user = user.id_user + + # Soft delete + TeraUser.delete(id_user) + # Make sure participant is deleted + self.assertIsNone(TeraUser.get_user_by_id(id_user)) + + # Query, with soft delete flag + user = TeraUser.query.filter_by(id_user=id_user).execution_options(include_deleted=True).first() + self.assertIsNotNone(user) + self.assertIsNotNone(user.deleted_at) + + def test_hard_delete(self): + with self._flask_app.app_context(): + # Create new user + user = TeraUser() + user.user_enabled = True + user.user_firstname = "Test" + user.user_lastname = "User" + user.user_profile = "" + user.user_password = TeraUser.encrypt_password("test") + user.user_superadmin = False + user.user_username = "test" + TeraUser.insert(user) + self.assertIsNotNone(user.id_user) + id_user = user.id_user + + # Assign user to sessions + user_session = TeraSession() + user_session.id_creator_user = id_user + user_session.id_session_type = 1 + user_session.session_name = 'Creator user session' + TeraSession.insert(user_session) + id_session = user_session.id_session + + user_session = TeraSession() + user_session.id_creator_service = 1 + user_session.id_session_type = 1 + user_session.session_name = "User invitee session" + user_session.session_users = [user] + TeraSession.insert(user_session) + id_session_invitee = user_session.id_session + + # Attach asset + asset = TeraAsset() + asset.asset_name = "User asset test" + asset.id_user = id_user + asset.id_session = id_session + asset.asset_service_uuid = TeraService.get_openteraserver_service().service_uuid + asset.asset_type = 'Test' + TeraAsset.insert(asset) + id_asset = asset.id_asset + + # ... and test + test = TeraTest() + test.id_user = id_user + test.id_session = id_session + test.id_test_type = 1 + test.test_name = "User test test!" + TeraTest.insert(test) + id_test = test.id_test + + # Soft delete device to prevent relationship integrity errors as we want to test hard-delete cascade here + TeraSession.delete(id_session) + TeraSession.delete(id_session_invitee) + TeraUser.delete(id_user) + + # Check that relationships are still there + self.assertIsNone(TeraUser.get_user_by_id(id_user)) + self.assertIsNotNone(TeraUser.get_user_by_id(id_user, True)) + self.assertIsNone(TeraSession.get_session_by_id(id_session)) + self.assertIsNotNone(TeraSession.get_session_by_id(id_session, True)) + self.assertIsNone(TeraSession.get_session_by_id(id_session_invitee)) + self.assertIsNotNone(TeraSession.get_session_by_id(id_session_invitee, True)) + self.assertIsNone(TeraAsset.get_asset_by_id(id_asset)) + self.assertIsNotNone(TeraAsset.get_asset_by_id(id_asset, True)) + self.assertIsNone(TeraTest.get_test_by_id(id_test)) + self.assertIsNotNone(TeraTest.get_test_by_id(id_test, True)) + + # Hard delete participant + TeraUser.delete(user.id_user, hard_delete=True) + + # Make sure device and associations are deleted + self.assertIsNone(TeraUser.get_user_by_id(id_user, True)) + self.assertIsNone(TeraSession.get_session_by_id(id_session, True)) + self.assertIsNone(TeraSession.get_session_by_id(id_session_invitee, True)) + self.assertIsNone(TeraAsset.get_asset_by_id(id_asset, True)) + self.assertIsNone(TeraTest.get_test_by_id(id_test, True)) diff --git a/teraserver/python/tests/opentera/db/models/test_TeraUserGroup.py b/teraserver/python/tests/opentera/db/models/test_TeraUserGroup.py index 9ee571f5..d49687b4 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraUserGroup.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraUserGroup.py @@ -227,11 +227,67 @@ def test_update_with_invalid_fields(self): self.assertRaises(SQLAlchemyError, TeraUserGroup.update, group.id_user_group, invalid_fields) TeraUserGroup.db().session.rollback() + def test_soft_delete(self): + with self._flask_app.app_context(): + # Create new + ug = TeraUserGroup() + ug.user_group_name = "Test User Group" + TeraUserGroup.insert(ug) + self.assertIsNotNone(ug.id_user_group) + id_user_group = ug.id_user_group + + # Soft delete + TeraUserGroup.delete(id_user_group) + # Make sure participant is deleted + self.assertIsNone(TeraUserGroup.get_user_group_by_id(id_user_group)) + + # Query, with soft delete flag + ug = TeraUserGroup.query.filter_by(id_user_group=id_user_group).\ + execution_options(include_deleted=True).first() + self.assertIsNotNone(ug) + self.assertIsNotNone(ug.deleted_at) + def test_hard_delete(self): with self._flask_app.app_context(): - pass + # Create new + ug = TeraUserGroup() + ug.user_group_name = "Test User Group" + TeraUserGroup.insert(ug) + self.assertIsNotNone(ug.id_user_group) + id_user_group = ug.id_user_group + + user = TeraUser() + user.user_enabled = True + user.user_firstname = "Test" + user.user_lastname = "User" + user.user_profile = "" + user.user_password = TeraUser.encrypt_password("test") + user.user_superadmin = False + user.user_username = "test" + user.user_user_groups = [ug] + TeraUser.insert(user) + self.assertIsNotNone(user.id_user) + id_user = user.id_user + + # Soft delete to prevent relationship integrity errors as we want to test hard-delete cascade here + id_user_user_group = TeraUserUserGroup.query_user_user_group_for_user_user_group(id_user, id_user_group)\ + .id_user_user_group + TeraUserUserGroup.delete(id_user_user_group) + TeraUserGroup.delete(id_user_group) + + # Check that relationships are still there + self.assertIsNotNone(TeraUser.get_user_by_id(id_user)) + self.assertIsNone(TeraUserGroup.get_user_group_by_id(id_user_group)) + self.assertIsNotNone(TeraUserGroup.get_user_group_by_id(id_user_group, True)) + self.assertIsNone(TeraUserUserGroup.get_user_user_group_by_id(id_user_user_group)) + self.assertIsNotNone(TeraUserUserGroup.get_user_user_group_by_id(id_user_user_group, True)) + + # Hard delete + TeraUserGroup.delete(id_user_group, hard_delete=True) - # def test_soft_delete(self): - # pass + # Make sure eveything is deleted + self.assertIsNotNone(TeraUser.get_user_by_id(id_user, True)) + self.assertIsNone(TeraUserGroup.get_user_group_by_id(id_user_group, True)) + self.assertIsNone(TeraUserUserGroup.get_user_user_group_by_id(id_user_user_group, True)) From 65f634fef7e3d4ad87be3e0697baa231fa6e5278 Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Mon, 17 Jul 2023 13:21:38 -0400 Subject: [PATCH 21/80] Fixed issue querying session types as super admin but with session types not associated to projects. --- .../modules/DatabaseModule/DBManagerTeraUserAccess.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/teraserver/python/modules/DatabaseModule/DBManagerTeraUserAccess.py b/teraserver/python/modules/DatabaseModule/DBManagerTeraUserAccess.py index 915c959a..dd795fd2 100644 --- a/teraserver/python/modules/DatabaseModule/DBManagerTeraUserAccess.py +++ b/teraserver/python/modules/DatabaseModule/DBManagerTeraUserAccess.py @@ -558,9 +558,13 @@ def query_sites_for_device(self, device_id: int): def query_session_type_by_id(self, session_type_id: int): proj_ids = self.get_accessible_projects_ids() - session_type = TeraSessionType.query.filter_by(id_session_type=session_type_id).filter(TeraProject.id_project. - in_(proj_ids)).first() - return session_type + session_type = TeraSessionType.query.filter_by(id_session_type=session_type_id)\ + + if not self.user.user_superadmin: + # Super admin = get all session types even if not associated to a project + session_type = session_type.filter(TeraProject.id_project.in_(proj_ids)) + + return session_type.first() def query_test_type(self, test_type_id: int): # site_ids = self.get_accessible_sites_ids() From 1c01cba0201ca20659e086b8e7d05f522bd73af3 Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Tue, 25 Jul 2023 11:24:44 -0400 Subject: [PATCH 22/80] Refs #202. Work started on undelete feature (TeraAsset for now) --- .../python/opentera/db/SoftDeleteMixin.py | 53 +++++++-- .../opentera/db/models/BaseModelsTest.py | 5 +- .../opentera/db/models/test_TeraAsset.py | 107 ++++++++++++++---- .../opentera/db/models/test_TeraDevice.py | 40 +++---- .../db/models/test_TeraParticipant.py | 66 +++++------ .../opentera/db/models/test_TeraSession.py | 58 +++++----- .../tests/opentera/db/models/test_TeraTest.py | 19 ++++ .../tests/opentera/db/models/test_TeraUser.py | 33 +++--- 8 files changed, 242 insertions(+), 139 deletions(-) diff --git a/teraserver/python/opentera/db/SoftDeleteMixin.py b/teraserver/python/opentera/db/SoftDeleteMixin.py index f4149c3d..a2008557 100644 --- a/teraserver/python/opentera/db/SoftDeleteMixin.py +++ b/teraserver/python/opentera/db/SoftDeleteMixin.py @@ -121,7 +121,7 @@ def hard_delete_method(_self): # Relationship has a cascade delete or a secondary table if relation[1].cascade.delete: for item in getattr(_self, relation[0]): - print("Cascade deleting " + str(item)) + # print("Cascade deleting " + str(item)) hard_item_deleter = getattr(item, hard_delete_method_name) hard_item_deleter() @@ -147,19 +147,18 @@ def hard_delete_method(_self): if generate_undelete_method: def undelete_method(_self): + if not getattr(_self, deleted_field_name): + print("Object " + str(_self.__class__) + " not deleted - returning.") + return + _self.handle_include_deleted_flag(True) + setattr(_self, deleted_field_name, None) + print("Undeleting " + str(_self.__class__)) if handle_cascade_delete: primary_key_name = inspect(_self.__class__).primary_key[0].name for relation in inspect(_self.__class__).relationships.items(): - if relation[1].cascade.delete: # Relationship has a cascade delete - # Item has a delete_at field (thus supports soft-delete) - if deleted_field_name in relation[1].entity.columns.keys(): - # Cascade undelete - must manually query to get deleted rows - related_items = relation[1].entity.class_.query.execution_options(include_deleted=True).\ - filter(text(primary_key_name + '=' + str(getattr(_self, primary_key_name)))).all() - for item in related_items: - item_undeleter = getattr(item, undelete_method_name) - item_undeleter() + print(str(_self.__class__) + " - relation " + str(relation)) if relation[1].secondary is not None: + print("-> Undeleting secondary table relationship " + relation[1].secondary.name) # Item has a delete_at field (thus supports soft-delete) if deleted_field_name in relation[1].entity.columns.keys(): model_class = _self.get_class_from_tablename(relation[1].secondary.name) @@ -171,7 +170,39 @@ def undelete_method(_self): item_undeleter = getattr(item, undelete_method_name) item_undeleter() - setattr(_self, deleted_field_name, None) + # Undelete "left-side" item of the relationship + remote_primary_key = relation[1].target.primary_key.columns[0].name + remote_model = _self.get_class_from_tablename(relation[1].target.name) + remote_item = remote_model.query.filter(text(remote_primary_key + '=' + + str(getattr(item, remote_primary_key))) + ).execution_options(include_deleted=True)\ + .first() + if remote_item: + print("--> Undeleting left side of secondary table " + relation[1].target.name) + item_undeleter = getattr(remote_item, undelete_method_name) + item_undeleter() + + continue + # Check for parents or related items + if relation[1].back_populates: + print("--> Undeleting back_populates relationship " + str(relation[1])) + # if relation[1].cascade.delete: # Relationship has a cascade delete + # Item has a delete_at field (thus supports soft-delete) + if deleted_field_name in relation[1].entity.columns.keys(): + # Cascade undelete - must manually query to get deleted rows + remote_primary_key = list(relation[1].remote_side)[0].name + local_primary_key = list(relation[1].local_columns)[0].name + self_key_value = getattr(_self, local_primary_key) + if not self_key_value: + continue + related_items = relation[1].entity.class_.query.execution_options(include_deleted=True).\ + filter(text(remote_primary_key + '=' + str(self_key_value))).all() + for item in related_items: + item_undeleter = getattr(item, undelete_method_name) + item_undeleter() + continue + print("Skipped undelete") + _self.handle_include_deleted_flag(False) class_attributes[undelete_method_name] = undelete_method diff --git a/teraserver/python/tests/opentera/db/models/BaseModelsTest.py b/teraserver/python/tests/opentera/db/models/BaseModelsTest.py index 81c384c2..cba132c8 100644 --- a/teraserver/python/tests/opentera/db/models/BaseModelsTest.py +++ b/teraserver/python/tests/opentera/db/models/BaseModelsTest.py @@ -15,7 +15,10 @@ def setUpClass(cls): cls._flask_app.config.update({'PROPAGATE_EXCEPTIONS': True}) cls._db_man = DBManager(cls._config, app=cls._flask_app) # Setup DB in RAM - # cls._db_man.open_local({'filename': 'D:\\temp\\opentera.db'}, echo=False, ram=False) + # filename = 'D:\\temp\\opentera.db' + # import os + # os.remove(filename) + # cls._db_man.open_local({'filename': filename}, echo=False, ram=False) cls._db_man.open_local({}, echo=False, ram=True) # Creating default users / tests. Time-consuming, only once per test file. diff --git a/teraserver/python/tests/opentera/db/models/test_TeraAsset.py b/teraserver/python/tests/opentera/db/models/test_TeraAsset.py index b949075c..96fd3f1f 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraAsset.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraAsset.py @@ -258,14 +258,11 @@ def test_update(self): def test_soft_delete(self): with self._flask_app.app_context(): # Create new - asset = TeraAsset() - asset.asset_name = 'Test asset' - asset.id_session = TeraSession.get_session_by_id(2).id_session - asset.asset_service_uuid = TeraService.get_service_by_id(1).service_uuid - asset.asset_type = 'application/test' - TeraAsset.insert(asset) + asset = TeraAssetTest.new_test_asset(id_session=2, + service_uuid=TeraService.get_service_by_id(1).service_uuid) self.assertIsNotNone(asset.id_asset) id_asset = asset.id_asset + # Delete TeraAsset.delete(id_asset) # Make sure it is deleted @@ -279,27 +276,11 @@ def test_soft_delete(self): def test_hard_delete(self): with self._flask_app.app_context(): # Create new - asset = TeraAsset() - asset.asset_name = 'Test asset' - asset.id_session = TeraSession.get_session_by_id(2).id_session - asset.asset_service_uuid = TeraService.get_service_by_id(1).service_uuid - asset.asset_type = 'application/test' - TeraAsset.insert(asset) + asset = TeraAssetTest.new_test_asset(id_session=2, + service_uuid=TeraService.get_service_by_id(1).service_uuid) self.assertIsNotNone(asset.id_asset) id_asset = asset.id_asset - # Try to hard delete while not soft deleted - # with self.assertRaises(SQLAlchemyError): - # TeraAsset.delete(id_asset, hard_delete=True) - # - # # Soft delete - # TeraAsset.delete(id_asset) - # - # # Assert soft deleted - # asset = TeraAsset.query.filter_by(id_asset=id_asset).execution_options(include_deleted=True).first() - # self.assertIsNotNone(asset) - # self.assertIsNotNone(asset.deleted_at) - # Hard delete TeraAsset.delete(id_asset, hard_delete=True) @@ -310,3 +291,81 @@ def test_hard_delete(self): self.db.session.expire_all() session = TeraSession.get_session_by_id(2) self.assertIsNotNone(session) + + def test_undelete(self): + with self._flask_app.app_context(): + # Create new participant + from 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 + device = TeraDeviceTest.new_test_device() + id_device = device.id_device + + # Create new user + from test_TeraUser import TeraUserTest + user = TeraUserTest.new_test_user() + id_user = user.id_user + + # Create new session + from test_TeraSession import TeraSessionTest + ses = TeraSessionTest.new_test_session(participants=[participant], users=[user], devices=[device]) + id_session = ses.id_session + + # Create new asset + asset = TeraAssetTest.new_test_asset(id_session=ses.id_session, + service_uuid=TeraService.get_service_by_id(1).service_uuid) + self.assertIsNotNone(asset.id_asset) + id_asset = asset.id_asset + + # Delete + # Asset will be deleted with the session + TeraSession.delete(id_session) + TeraParticipant.delete(id_participant) + TeraDevice.delete(id_device) + TeraUser.delete(id_user) + # TeraAsset.delete(id_asset) + # Make sure it is deleted + # Warning, it was deleted, object is not valid anymore + self.assertIsNone(TeraAsset.get_asset_by_id(id_asset)) + + # Undelete + TeraAsset.undelete(id_asset) + + # Make sure it is back! + self.db.session.expire_all() + asset = TeraAsset.get_asset_by_id(id_asset) + self.assertIsNotNone(asset) + self.assertIsNone(asset.deleted_at) + + ses = TeraSession.get_session_by_id(id_session) + self.assertIsNotNone(ses) + user = TeraUser.get_user_by_id(id_user) + self.assertIsNotNone(user) + device = TeraDevice.get_device_by_id(id_device) + self.assertIsNotNone(device) + participant = TeraParticipant.get_participant_by_id(id_participant) + self.assertIsNotNone(participant) + + @staticmethod + def new_test_asset(id_session: int, service_uuid: str, id_device: int | None = None, + id_participant: int | None = None, id_user: int | None = None, + id_service: int | None = None) -> TeraAsset: + asset = TeraAsset() + asset.asset_name = 'Test asset' + asset.id_session = id_session + if id_device: + asset.id_device = id_device + if id_participant: + asset.id_participant = id_participant + if id_user: + asset.id_user = id_user, + if id_service: + asset.id_service = id_service, + asset.asset_service_uuid = service_uuid + 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 58e23fa6..621a124a 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraDevice.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraDevice.py @@ -51,10 +51,7 @@ def test_json_full_and_minimal(self): def test_insert_with_minimal_config(self): with self._flask_app.app_context(): # Create a new device - device = TeraDevice() - device.device_name = 'Test device' - device.id_device_type = TeraDeviceType.get_device_type_by_key('capteur').id_device_type - TeraDevice.insert(device) + device = TeraDeviceTest.new_test_device() self.assertIsNotNone(device.id_device) self.assertIsNotNone(device.device_token) self.assertIsNotNone(device.device_uuid) @@ -83,10 +80,7 @@ def test_update(self): def test_delete(self): with self._flask_app.app_context(): # Create a new device - device = TeraDevice() - device.device_name = 'Test device' - device.id_device_type = TeraDeviceType.get_device_type_by_key('capteur').id_device_type - TeraDevice.insert(device) + device = TeraDeviceTest.new_test_device() self.assertIsNotNone(device.id_device) id_device = device.id_device # Delete device @@ -98,10 +92,7 @@ def test_delete(self): def test_soft_delete(self): with self._flask_app.app_context(): # Create a new device - device = TeraDevice() - device.device_name = 'Test device' - device.id_device_type = TeraDeviceType.get_device_type_by_key('capteur').id_device_type - TeraDevice.insert(device) + device = TeraDeviceTest.new_test_device() self.assertIsNotNone(device.id_device) id_device = device.id_device # Delete device @@ -118,10 +109,7 @@ def test_soft_delete(self): def test_hard_delete(self): with self._flask_app.app_context(): # Create a new device - device = TeraDevice() - device.device_name = 'Test device' - device.id_device_type = TeraDeviceType.get_device_type_by_key('capteur').id_device_type - TeraDevice.insert(device) + device = TeraDeviceTest.new_test_device() self.assertIsNotNone(device.id_device) id_device = device.id_device @@ -163,13 +151,10 @@ def test_hard_delete(self): id_session_invitee = device_session.id_session # Attach asset - asset = TeraAsset() - asset.asset_name = "Device asset test" - asset.id_device = id_device - asset.id_session = id_session - asset.asset_service_uuid = TeraService.get_openteraserver_service().service_uuid - asset.asset_type = 'Test' - TeraAsset.insert(asset) + from 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 @@ -227,3 +212,12 @@ def test_hard_delete(self): self.assertIsNone(TeraTest.get_test_by_id(id_test, True)) self.assertIsNone(TeraServiceConfig.get_service_config_by_id(id_service_config, True)) self.assertIsNone(TeraDeviceSite.get_device_site_by_id(id_device_site, True)) + + @staticmethod + def new_test_device() -> TeraDevice: + device = TeraDevice() + device.device_name = 'Test device' + device.id_device_type = TeraDeviceType.get_device_type_by_key('capteur').id_device_type + TeraDevice.insert(device) + + return device diff --git a/teraserver/python/tests/opentera/db/models/test_TeraParticipant.py b/teraserver/python/tests/opentera/db/models/test_TeraParticipant.py index 1103a616..636a4208 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraParticipant.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraParticipant.py @@ -14,38 +14,35 @@ class TeraParticipantTest(BaseModelsTest): def test_token(self): with self._flask_app.app_context(): return - participantGroup = TeraParticipantGroup() - participantGroup.participant_group_name = 'participants' - participantGroup.id_project = 1 - - participant = TeraParticipant() - participant.participant_name = 'Test Participant' - participant.participant_uuid = str(uuid.uuid4()) - participant.participant_participant_group = participantGroup - - token = participant.create_token() - - self.assertNotEqual(token, "") - self.db.session.add(participantGroup) - self.db.session.add(participant) - - self.db.session.commit() - - # Load participant from invalid token - loadedParticipant = TeraParticipant.get_participant_by_token('rien') - self.assertEqual(loadedParticipant, None) - - # Load participant from valid token - loadedParticipant = TeraParticipant.get_participant_by_token(token) - self.assertEqual(loadedParticipant.participant_uuid, participant.participant_uuid) + # participantGroup = TeraParticipantGroup() + # participantGroup.participant_group_name = 'participants' + # participantGroup.id_project = 1 + # + # participant = TeraParticipant() + # participant.participant_name = 'Test Participant' + # participant.participant_uuid = str(uuid.uuid4()) + # participant.participant_participant_group = participantGroup + # + # token = participant.create_token() + # + # self.assertNotEqual(token, "") + # self.db.session.add(participantGroup) + # self.db.session.add(participant) + # + # self.db.session.commit() + # + # # Load participant from invalid token + # loadedParticipant = TeraParticipant.get_participant_by_token('rien') + # self.assertEqual(loadedParticipant, None) + # + # # Load participant from valid token + # loadedParticipant = TeraParticipant.get_participant_by_token(token) + # self.assertEqual(loadedParticipant.participant_uuid, participant.participant_uuid) def test_soft_delete(self): with self._flask_app.app_context(): # Create a new participant - participant = TeraParticipant() - participant.participant_name = "Test participant" - participant.id_project = 1 - TeraParticipant.insert(participant) + participant = TeraParticipantTest.new_test_participant(id_project=1) self.assertIsNotNone(participant.id_participant) id_participant = participant.id_participant @@ -63,10 +60,7 @@ def test_soft_delete(self): def test_hard_delete(self): with self._flask_app.app_context(): # Create a new participant - participant = TeraParticipant() - participant.participant_name = "Test participant" - participant.id_project = 1 - TeraParticipant.insert(participant) + participant = TeraParticipantTest.new_test_participant(id_project=1) self.assertIsNotNone(participant.id_participant) id_participant = participant.id_participant @@ -132,3 +126,11 @@ def test_hard_delete(self): self.assertIsNone(TeraAsset.get_asset_by_id(id_asset, True)) self.assertIsNone(TeraTest.get_test_by_id(id_test, True)) + @staticmethod + def new_test_participant(id_project: int) -> TeraParticipant: + participant = TeraParticipant() + participant.participant_name = "Test participant" + participant.id_project = id_project + TeraParticipant.insert(participant) + + return participant diff --git a/teraserver/python/tests/opentera/db/models/test_TeraSession.py b/teraserver/python/tests/opentera/db/models/test_TeraSession.py index ab5bd96a..dfaddc41 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraSession.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraSession.py @@ -60,24 +60,16 @@ def test_session_from_json(self): def test_soft_delete(self): with self._flask_app.app_context(): # Create new - ses = TeraSession() - ses.id_creator_service = 1 - ses.id_session_type = 1 - ses.session_name = "Test session" - ses.session_participants = [TeraParticipant.get_participant_by_id(1)] - ses.session_devices = [TeraDevice.get_device_by_id(1)] - ses.session_users = [TeraUser.get_user_by_id(2)] - TeraSession.insert(ses) + ses = TeraSessionTest.new_test_session(participants=[TeraParticipant.get_participant_by_id(1)], + devices=[TeraDevice.get_device_by_id(1)], + users=[TeraUser.get_user_by_id(2)]) id_session = ses.id_session # Attach asset - asset = TeraAsset() - asset.asset_name = "Asset test" - asset.id_device = 1 - asset.id_session = id_session - asset.asset_service_uuid = TeraService.get_openteraserver_service().service_uuid - asset.asset_type = 'Test' - TeraAsset.insert(asset) + from 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 @@ -116,24 +108,16 @@ def test_soft_delete(self): def test_hard_delete(self): with self._flask_app.app_context(): # Create new - ses = TeraSession() - ses.id_creator_service = 1 - ses.id_session_type = 1 - ses.session_name = "Test session" - ses.session_participants = [TeraParticipant.get_participant_by_id(1)] - ses.session_devices = [TeraDevice.get_device_by_id(1)] - ses.session_users = [TeraUser.get_user_by_id(2)] - TeraSession.insert(ses) + ses = ses = TeraSessionTest.new_test_session(participants=[TeraParticipant.get_participant_by_id(1)], + devices=[TeraDevice.get_device_by_id(1)], + users=[TeraUser.get_user_by_id(2)]) id_session = ses.id_session # Attach asset - asset = TeraAsset() - asset.asset_name = "Asset test" - asset.id_device = 1 - asset.id_session = id_session - asset.asset_service_uuid = TeraService.get_openteraserver_service().service_uuid - asset.asset_type = 'Test' - TeraAsset.insert(asset) + from 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 @@ -159,3 +143,17 @@ def test_hard_delete(self): self.assertIsNone(TeraAsset.get_asset_by_id(id_asset, with_deleted=True)) self.assertIsNone(TeraTest.get_test_by_id(id_test, with_deleted=True)) + @staticmethod + def new_test_session(id_creator_service: int = 1, id_session_type: int = 1, participants: list | None = None, + devices: list | None = None, users: list | None = None) -> TeraSession: + if participants is None: + participants = [] + ses = TeraSession() + ses.id_creator_service = id_creator_service + ses.id_session_type = id_session_type + ses.session_name = "Test session" + ses.session_participants = participants + ses.session_devices = devices + ses.session_users = users + TeraSession.insert(ses) + return ses diff --git a/teraserver/python/tests/opentera/db/models/test_TeraTest.py b/teraserver/python/tests/opentera/db/models/test_TeraTest.py index 746f776a..db530d3f 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraTest.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraTest.py @@ -47,3 +47,22 @@ def test_hard_delete(self): # Make sure eveything is deleted self.assertIsNone(TeraTest.get_test_by_id(id_test, True)) + + def test_undelete(self): + with self._flask_app.app_context(): + # Create new + test = TeraTest() + test.id_participant = 1 + test.id_session = 1 + test.id_test_type = 1 + test.test_name = "Test test!" + TeraTest.insert(test) + self.assertIsNotNone(test.id_test) + id_test = test.id_test + + # Soft delete + TeraTest.delete(id_test) + + # Make sure it is deleted + self.assertIsNone(TeraTest.get_test_by_id(id_test)) + diff --git a/teraserver/python/tests/opentera/db/models/test_TeraUser.py b/teraserver/python/tests/opentera/db/models/test_TeraUser.py index d70b8216..6ff98ee4 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraUser.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraUser.py @@ -72,15 +72,7 @@ def test_multisite_user(self): def test_soft_delete(self): with self._flask_app.app_context(): # Create new - user = TeraUser() - user.user_enabled = True - user.user_firstname = "Test" - user.user_lastname = "User" - user.user_profile = "" - user.user_password = TeraUser.encrypt_password("test") - user.user_superadmin = False - user.user_username = "test" - TeraUser.insert(user) + user = TeraUserTest.new_test_user() self.assertIsNotNone(user.id_user) id_user = user.id_user @@ -97,15 +89,7 @@ def test_soft_delete(self): def test_hard_delete(self): with self._flask_app.app_context(): # Create new user - user = TeraUser() - user.user_enabled = True - user.user_firstname = "Test" - user.user_lastname = "User" - user.user_profile = "" - user.user_password = TeraUser.encrypt_password("test") - user.user_superadmin = False - user.user_username = "test" - TeraUser.insert(user) + user = TeraUserTest.new_test_user() self.assertIsNotNone(user.id_user) id_user = user.id_user @@ -170,3 +154,16 @@ def test_hard_delete(self): self.assertIsNone(TeraSession.get_session_by_id(id_session_invitee, True)) self.assertIsNone(TeraAsset.get_asset_by_id(id_asset, True)) self.assertIsNone(TeraTest.get_test_by_id(id_test, True)) + + @staticmethod + def new_test_user() -> TeraUser: + user = TeraUser() + user.user_enabled = True + user.user_firstname = "Test" + user.user_lastname = "User" + user.user_profile = "" + user.user_password = TeraUser.encrypt_password("test") + user.user_superadmin = False + user.user_username = "test" + TeraUser.insert(user) + return user From cbe916e8c7625659b06641ead978e9a9b033fee5 Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Wed, 26 Jul 2023 12:54:20 -0400 Subject: [PATCH 23/80] Refs #202. Updated undelete behavior --- .../python/opentera/db/SoftDeleteMixin.py | 142 +++++++++++++----- .../python/opentera/db/models/TeraSession.py | 7 +- .../opentera/db/models/BaseModelsTest.py | 10 +- .../opentera/db/models/test_TeraAsset.py | 65 ++++++-- .../opentera/db/models/test_TeraDevice.py | 127 ++++++++++++---- .../db/models/test_TeraDeviceParticipant.py | 8 + .../db/models/test_TeraDeviceProject.py | 8 + .../opentera/db/models/test_TeraDeviceSite.py | 7 + .../db/models/test_TeraServiceConfig.py | 16 ++ .../opentera/db/models/test_TeraSession.py | 22 ++- .../tests/opentera/db/models/test_TeraSite.py | 35 ++--- .../tests/opentera/db/models/test_TeraTest.py | 129 +++++++++++++--- 12 files changed, 434 insertions(+), 142 deletions(-) diff --git a/teraserver/python/opentera/db/SoftDeleteMixin.py b/teraserver/python/opentera/db/SoftDeleteMixin.py index a2008557..c219c3af 100644 --- a/teraserver/python/opentera/db/SoftDeleteMixin.py +++ b/teraserver/python/opentera/db/SoftDeleteMixin.py @@ -13,8 +13,7 @@ from sqlalchemy.ext.declarative import DeclarativeMeta from sqlalchemy.event import listens_for from sqlalchemy.orm import ORMExecuteState, Session -from sqlalchemy.engine import Engine, Connection -from sqlalchemy.sql import Select +from sqlalchemy.exc import IntegrityError from functools import cache @@ -146,47 +145,44 @@ def hard_delete_method(_self): class_attributes[hard_delete_method_name] = hard_delete_method if generate_undelete_method: + def get_undelete_cascade_relations(_self) -> list: + return [] # By default, no relationships are automatically undeleted when undeleting + + class_attributes['get_undelete_cascade_relations'] = get_undelete_cascade_relations + def undelete_method(_self): if not getattr(_self, deleted_field_name): print("Object " + str(_self.__class__) + " not deleted - returning.") return - _self.handle_include_deleted_flag(True) - setattr(_self, deleted_field_name, None) + + current_obj = inspect(_self.__class__) + primary_key_name = current_obj.primary_key[0].name + + # Check data integrity before undeleting + for col in current_obj.columns: + if not col.foreign_keys: + continue + # If column foreign key is not nullable or there is a value for this object, we must check if + # related object is still there + col_value = getattr(_self, col.name, None) + if not col.nullable or col_value: + remote_table_name = list(col.foreign_keys)[0].column.table.name + model_class = _self.get_class_from_tablename(remote_table_name) + remote_key_name = list(col.foreign_keys)[0].column.key + related_item = model_class.query.filter(text(remote_key_name + '="' + str(col_value) + '"')).first() + if not related_item: + # A required object isn't there (soft-deleted) - throw exception to undelete it first! + raise IntegrityError('Cannot undelete: unsatisfied foreign key - ' + col.name, col_value, + remote_table_name) + # Undelete! print("Undeleting " + str(_self.__class__)) + setattr(_self, deleted_field_name, None) + + # Check relationships that are cascade deleted to restore them if handle_cascade_delete: - primary_key_name = inspect(_self.__class__).primary_key[0].name for relation in inspect(_self.__class__).relationships.items(): - print(str(_self.__class__) + " - relation " + str(relation)) - if relation[1].secondary is not None: - print("-> Undeleting secondary table relationship " + relation[1].secondary.name) - # Item has a delete_at field (thus supports soft-delete) - if deleted_field_name in relation[1].entity.columns.keys(): - model_class = _self.get_class_from_tablename(relation[1].secondary.name) - if model_class: - related_items = model_class.query.filter(text(primary_key_name + '=' + - str(getattr(_self, primary_key_name))) - ).execution_options(include_deleted=True).all() - for item in related_items: - item_undeleter = getattr(item, undelete_method_name) - item_undeleter() - - # Undelete "left-side" item of the relationship - remote_primary_key = relation[1].target.primary_key.columns[0].name - remote_model = _self.get_class_from_tablename(relation[1].target.name) - remote_item = remote_model.query.filter(text(remote_primary_key + '=' + - str(getattr(item, remote_primary_key))) - ).execution_options(include_deleted=True)\ - .first() - if remote_item: - print("--> Undeleting left side of secondary table " + relation[1].target.name) - item_undeleter = getattr(remote_item, undelete_method_name) - item_undeleter() - - continue - # Check for parents or related items - if relation[1].back_populates: - print("--> Undeleting back_populates relationship " + str(relation[1])) - # if relation[1].cascade.delete: # Relationship has a cascade delete + # Relationship has a cascade undelete ? + if relation[1].key in _self.get_undelete_cascade_relations(): # Item has a delete_at field (thus supports soft-delete) if deleted_field_name in relation[1].entity.columns.keys(): # Cascade undelete - must manually query to get deleted rows @@ -198,11 +194,81 @@ def undelete_method(_self): related_items = relation[1].entity.class_.query.execution_options(include_deleted=True).\ filter(text(remote_primary_key + '=' + str(self_key_value))).all() for item in related_items: + print("--> Undeleting relationship " + relation[1].key) item_undeleter = getattr(item, undelete_method_name) item_undeleter() continue - print("Skipped undelete") - _self.handle_include_deleted_flag(False) + + # Check secondary relationships and restore them if both ends are now undeleted + if relation[1].secondary is not None: + if deleted_field_name in relation[1].entity.columns.keys(): + model_class = _self.get_class_from_tablename(relation[1].secondary.name) + related_items = model_class.query.execution_options(include_deleted=True).\ + filter(text(primary_key_name + '=' + str(getattr(_self, primary_key_name)))).all() + for item in related_items: + # Check if other side of the relationship is present and, if so, restores it + remote_primary_key = relation[1].target.primary_key.columns[0].name + remote_model = _self.get_class_from_tablename(relation[1].target.name) + remote_item = remote_model.query.filter( + text(remote_primary_key + '=' + str(getattr(item, remote_primary_key)))).first() + if remote_item: + print("--> Undeleting relationship with " + relation[1].target.name) + item_undeleter = getattr(item, undelete_method_name) + item_undeleter() + + # _self.handle_include_deleted_flag(True) + # setattr(_self, deleted_field_name, None) + # print("Undeleting " + str(_self.__class__)) + # if handle_cascade_delete: + # primary_key_name = inspect(_self.__class__).primary_key[0].name + # for relation in inspect(_self.__class__).relationships.items(): + # print(str(_self.__class__) + " - relation " + str(relation)) + # if relation[1].secondary is not None: + # print("-> Undeleting secondary table relationship " + relation[1].secondary.name) + # # Item has a delete_at field (thus supports soft-delete) + # if deleted_field_name in relation[1].entity.columns.keys(): + # model_class = _self.get_class_from_tablename(relation[1].secondary.name) + # if model_class: + # related_items = model_class.query.filter(text(primary_key_name + '=' + + # str(getattr(_self, primary_key_name))) + # ).execution_options(include_deleted=True).all() + # for item in related_items: + # item_undeleter = getattr(item, undelete_method_name) + # item_undeleter() + # + # # Undelete "left-side" item of the relationship + # remote_primary_key = relation[1].target.primary_key.columns[0].name + # remote_model = _self.get_class_from_tablename(relation[1].target.name) + # remote_item = remote_model.query.filter(text(remote_primary_key + '=' + + # str(getattr(item, remote_primary_key))) + # ).execution_options(include_deleted=True)\ + # .first() + # if remote_item: + # print("--> Undeleting left side of secondary table " + relation[1].target.name) + # item_undeleter = getattr(remote_item, undelete_method_name) + # item_undeleter() + # + # continue + # # Check for parents or related items + # if relation[1].back_populates: + # print("--> Undeleting back_populates relationship " + str(relation[1])) + # # if relation[1].cascade.delete: # Relationship has a cascade delete + # # Item has a delete_at field (thus supports soft-delete) + # if deleted_field_name in relation[1].entity.columns.keys(): + # # Cascade undelete - must manually query to get deleted rows + # remote_primary_key = list(relation[1].remote_side)[0].name + # local_primary_key = list(relation[1].local_columns)[0].name + # self_key_value = getattr(_self, local_primary_key) + # if not self_key_value: + # continue + # related_items = relation[1].entity.class_.query.execution_options(include_deleted=True).\ + # filter(text(remote_primary_key + '=' + str(self_key_value))).all() + # for item in related_items: + # item_undeleter = getattr(item, undelete_method_name) + # item_undeleter() + # continue + # print("Skipped undelete") + # _self.handle_include_deleted_flag(False) class_attributes[undelete_method_name] = undelete_method diff --git a/teraserver/python/opentera/db/models/TeraSession.py b/teraserver/python/opentera/db/models/TeraSession.py index eace6aa2..a3b28fdd 100644 --- a/teraserver/python/opentera/db/models/TeraSession.py +++ b/teraserver/python/opentera/db/models/TeraSession.py @@ -40,8 +40,8 @@ class TeraSession(BaseModel, SoftDeleteMixin): back_populates="participant_sessions", lazy="selectin") session_users = relationship("TeraUser", secondary="t_sessions_users", back_populates="user_sessions", lazy="selectin") - session_devices = relationship("TeraDevice", secondary="t_sessions_devices", - back_populates="device_sessions", lazy="selectin") + session_devices = relationship("TeraDevice", secondary="t_sessions_devices", back_populates="device_sessions", + lazy="selectin") session_creator_user = relationship('TeraUser') session_creator_device = relationship('TeraDevice') @@ -457,3 +457,6 @@ def update(cls, update_id: int, values: dict): # Dumps dictionnary into json values['session_parameters'] = json.dumps(values['session_parameters']) super().update(update_id=update_id, values=values) + + def get_undelete_cascade_relations(self): + return ['session_events', 'session_assets', 'session_tests'] diff --git a/teraserver/python/tests/opentera/db/models/BaseModelsTest.py b/teraserver/python/tests/opentera/db/models/BaseModelsTest.py index cba132c8..57ff6b0a 100644 --- a/teraserver/python/tests/opentera/db/models/BaseModelsTest.py +++ b/teraserver/python/tests/opentera/db/models/BaseModelsTest.py @@ -15,11 +15,11 @@ def setUpClass(cls): cls._flask_app.config.update({'PROPAGATE_EXCEPTIONS': True}) cls._db_man = DBManager(cls._config, app=cls._flask_app) # Setup DB in RAM - # filename = 'D:\\temp\\opentera.db' - # import os - # os.remove(filename) - # cls._db_man.open_local({'filename': filename}, echo=False, ram=False) - cls._db_man.open_local({}, echo=False, ram=True) + filename = 'D:\\temp\\opentera.db' + import os + os.remove(filename) + cls._db_man.open_local({'filename': filename}, echo=False, ram=False) + # cls._db_man.open_local({}, echo=False, ram=True) # Creating default users / tests. Time-consuming, only once per test file. with cls._flask_app.app_context(): diff --git a/teraserver/python/tests/opentera/db/models/test_TeraAsset.py b/teraserver/python/tests/opentera/db/models/test_TeraAsset.py index 96fd3f1f..f8c1ffc2 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraAsset.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraAsset.py @@ -7,7 +7,7 @@ from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraParticipant import TeraParticipant from tests.opentera.db.models.BaseModelsTest import BaseModelsTest -from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.exc import IntegrityError class TeraAssetTest(BaseModelsTest): @@ -316,17 +316,13 @@ def test_undelete(self): # Create new asset asset = TeraAssetTest.new_test_asset(id_session=ses.id_session, - service_uuid=TeraService.get_service_by_id(1).service_uuid) + service_uuid=TeraService.get_service_by_id(1).service_uuid, + id_user=id_user, id_participant=id_participant, id_device=id_device) self.assertIsNotNone(asset.id_asset) id_asset = asset.id_asset # Delete - # Asset will be deleted with the session - TeraSession.delete(id_session) - TeraParticipant.delete(id_participant) - TeraDevice.delete(id_device) - TeraUser.delete(id_user) - # TeraAsset.delete(id_asset) + TeraAsset.delete(id_asset) # Make sure it is deleted # Warning, it was deleted, object is not valid anymore self.assertIsNone(TeraAsset.get_asset_by_id(id_asset)) @@ -340,14 +336,55 @@ def test_undelete(self): self.assertIsNotNone(asset) self.assertIsNone(asset.deleted_at) - ses = TeraSession.get_session_by_id(id_session) - self.assertIsNotNone(ses) + # Now, delete again but with its dependencies... + # Asset will be deleted with the session + TeraSession.delete(id_session) + TeraParticipant.delete(id_participant) + TeraDevice.delete(id_device) + TeraUser.delete(id_user) + + # Exception should be thrown when trying to undelete + with self.assertRaises(IntegrityError) as cm: + TeraAsset.undelete(id_asset) + + # Restore participant + TeraParticipant.undelete(id_participant) + participant = TeraParticipant.get_participant_by_id(id_participant) + self.assertIsNotNone(participant) + + # Restore asset - still has dependencies issues... + with self.assertRaises(IntegrityError) as cm: + TeraAsset.undelete(id_asset) + + # Restore user + TeraUser.undelete(id_user) user = TeraUser.get_user_by_id(id_user) self.assertIsNotNone(user) + + # Restore asset - still has dependencies issues... + with self.assertRaises(IntegrityError) as cm: + TeraAsset.undelete(id_asset) + + # Restore device + TeraDevice.undelete(id_device) device = TeraDevice.get_device_by_id(id_device) self.assertIsNotNone(device) - participant = TeraParticipant.get_participant_by_id(id_participant) - self.assertIsNotNone(participant) + + # Restore asset - still has dependencies issues... + with self.assertRaises(IntegrityError) as cm: + TeraAsset.undelete(id_asset) + + # Restore session + TeraSession.undelete(id_session) + + ses = TeraSession.get_session_by_id(id_session) + self.assertIsNotNone(ses) + + # Asset was restored with the session... + self.db.session.expire_all() + asset = TeraAsset.get_asset_by_id(id_asset) + self.assertIsNotNone(asset) + self.assertIsNone(asset.deleted_at) @staticmethod def new_test_asset(id_session: int, service_uuid: str, id_device: int | None = None, @@ -361,9 +398,9 @@ def new_test_asset(id_session: int, service_uuid: str, id_device: int | None = N if id_participant: asset.id_participant = id_participant if id_user: - asset.id_user = id_user, + asset.id_user = id_user if id_service: - asset.id_service = id_service, + asset.id_service = id_service asset.asset_service_uuid = service_uuid asset.asset_type = 'application/test' TeraAsset.insert(asset) diff --git a/teraserver/python/tests/opentera/db/models/test_TeraDevice.py b/teraserver/python/tests/opentera/db/models/test_TeraDevice.py index 621a124a..87ba939c 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraDevice.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraDevice.py @@ -8,6 +8,7 @@ from opentera.db.models.TeraDeviceParticipant import TeraDeviceParticipant from opentera.db.models.TeraServiceConfig import TeraServiceConfig from opentera.db.models.TeraSession import TeraSession + from tests.opentera.db.models.BaseModelsTest import BaseModelsTest @@ -114,40 +115,27 @@ def test_hard_delete(self): id_device = device.id_device # Assign device to site - device_site = TeraDeviceSite() - device_site.id_site = 1 - device_site.id_device = id_device - TeraDeviceSite.insert(device_site) + from 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 - device_project = TeraDeviceProject() - device_project.id_device = id_device - device_project.id_project = 1 - TeraDeviceProject.insert(device_project) + from 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 - device_participant = TeraDeviceParticipant() - device_participant.id_device = id_device - device_participant.id_participant = 1 - TeraDeviceParticipant.insert(device_participant) + from 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 - device_session = TeraSession() - device_session.id_creator_device = id_device - device_session.id_session_type = 1 - device_session.session_name = 'Creator device session' - TeraSession.insert(device_session) + from test_TeraSession import TeraSessionTest + device_session = TeraSessionTest.new_test_session(id_creator_device=id_device) id_session = device_session.id_session - device_session = TeraSession() - device_session.id_creator_service = 1 - device_session.id_session_type = 1 - device_session.session_name = "Device invitee session" - device_session.session_devices = [device] - TeraSession.insert(device_session) + device_session = TeraSessionTest.new_test_session(id_creator_service=1, devices=[device]) id_session_invitee = device_session.id_session # Attach asset @@ -158,19 +146,13 @@ def test_hard_delete(self): id_asset = asset.id_asset # ... and test - test = TeraTest() - test.id_device = id_device - test.id_session = id_session - test.id_test_type = 1 - test.test_name = "Device test test!" - TeraTest.insert(test) + from 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 - device_service_config = TeraServiceConfig() - device_service_config.id_device = id_device - device_service_config.id_service = 2 - TeraServiceConfig.insert(device_service_config) + from 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 # Soft delete device to prevent relationship integrity errors as we want to test hard-delete cascade here @@ -213,6 +195,85 @@ def test_hard_delete(self): self.assertIsNone(TeraServiceConfig.get_service_config_by_id(id_service_config, True)) self.assertIsNone(TeraDeviceSite.get_device_site_by_id(id_device_site, True)) + def test_undelete(self): + with self._flask_app.app_context(): + # Create a new device + device = TeraDeviceTest.new_test_device() + self.assertIsNotNone(device.id_device) + id_device = device.id_device + + # Assign device to site + from 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 + 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 + 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 + device_session = TeraSessionTest.new_test_session(id_creator_device=id_device) + id_session = device_session.id_session + + device_session = TeraSessionTest.new_test_session(id_creator_service=1, devices=[device]) + id_session_invitee = device_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_device=id_device) + id_asset = asset.id_asset + + # ... and test + from 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 + device_service_config = TeraServiceConfigTest.new_test_service_config(id_device=id_device, id_service=2) + id_service_config = device_service_config.id_service_config + + # Delete other items too to prevent integrity errors (as this is not what we want to test here) + TeraDeviceParticipant.delete(id_device_participant) + TeraSession.delete(id_session) + TeraSession.delete(id_session_invitee) + TeraDevice.delete(id_device) + + # Other checks are done in other tests - just make sure device is deleted for now + self.assertIsNone(TeraDevice.get_device_by_id(id_device)) + + # Undelete + TeraDevice.undelete(id_device) + + # Check that everything was undeleted + device = TeraDevice.get_device_by_id(id_device) + self.assertIsNotNone(device) + self.assertIsNone(device.deleted_at) + device_site = TeraDeviceSite.get_device_site_by_id(id_device_site) + self.assertIsNotNone(device_site) + device_project = TeraDeviceProject.get_device_project_by_id(id_device_project) + self.assertIsNotNone(device_project) + device_participant = TeraDeviceParticipant.get_device_participant_by_id(id_device_participant) + self.assertIsNotNone(device_participant) + device_session = TeraSession.get_session_by_id(id_session_invitee) + self.assertIsNone(device_session) + device_session = TeraSession.get_session_by_id(id_session) + self.assertIsNone(device_session) # Not undeleted + asset = TeraAsset.get_asset_by_id(id_asset) + self.assertIsNone(asset) # Asset stays deleted + test = TeraTest.get_test_by_id(id_test) + self.assertIsNone(test) # Test stays deleted + @staticmethod def new_test_device() -> TeraDevice: device = TeraDevice() diff --git a/teraserver/python/tests/opentera/db/models/test_TeraDeviceParticipant.py b/teraserver/python/tests/opentera/db/models/test_TeraDeviceParticipant.py index 25cf0386..08cfdb92 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraDeviceParticipant.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraDeviceParticipant.py @@ -79,3 +79,11 @@ def _check_json(self, json_data: dict, with_minimal: bool = True): self.assertTrue('id_participant' in json_data) self.assertFalse('device_participant_device' in json_data) self.assertFalse('device_participant_participant' in json_data) + + @staticmethod + def new_test_device_participant(id_device: int, id_participant: int) -> TeraDeviceParticipant: + device_part = TeraDeviceParticipant() + device_part.id_participant = id_participant + device_part.id_device = id_device + TeraDeviceParticipant.insert(device_part) + return device_part diff --git a/teraserver/python/tests/opentera/db/models/test_TeraDeviceProject.py b/teraserver/python/tests/opentera/db/models/test_TeraDeviceProject.py index b73e73e7..1f0b5e54 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraDeviceProject.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraDeviceProject.py @@ -119,3 +119,11 @@ def _check_json(self, json_data: dict, with_minimal: bool = True): self.assertTrue('id_project' in json_data) self.assertFalse('device_project_device' in json_data) self.assertFalse('device_project_project' in json_data) + + @staticmethod + def new_test_device_project(id_device: int, id_project: int) -> TeraDeviceProject: + device_project = TeraDeviceProject() + device_project.id_project = id_project + device_project.id_device = id_device + TeraDeviceProject.insert(device_project) + return device_project diff --git a/teraserver/python/tests/opentera/db/models/test_TeraDeviceSite.py b/teraserver/python/tests/opentera/db/models/test_TeraDeviceSite.py index 39a92de3..a5c03e78 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraDeviceSite.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraDeviceSite.py @@ -264,3 +264,10 @@ def _check_json(self, json_data: dict, with_minimal: bool = True): self.assertFalse('device_site_site' in json_data) self.assertFalse('device_site_device' in json_data) + @staticmethod + def new_test_device_site(id_device: int, id_site: int) -> TeraDeviceSite: + device_site = TeraDeviceSite() + device_site.id_site = id_site + device_site.id_device = id_device + TeraDeviceSite.insert(device_site) + return device_site diff --git a/teraserver/python/tests/opentera/db/models/test_TeraServiceConfig.py b/teraserver/python/tests/opentera/db/models/test_TeraServiceConfig.py index f34192e8..53c26014 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraServiceConfig.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraServiceConfig.py @@ -1,4 +1,5 @@ from tests.opentera.db.models.BaseModelsTest import BaseModelsTest +from opentera.db.models.TeraServiceConfig import TeraServiceConfig class TeraServiceConfigTest(BaseModelsTest): @@ -6,3 +7,18 @@ class TeraServiceConfigTest(BaseModelsTest): def test_defaults(self): with self._flask_app.app_context(): pass + + @staticmethod + def new_test_service_config(id_service: int, id_device: int | None = None, id_participant: int | None = None, + id_user: int | None = None, config: str = "{}"): + device_service_config = TeraServiceConfig() + if id_device: + device_service_config.id_device = id_device + if id_participant: + device_service_config.id_participant = id_participant + if id_user: + device_service_config.id_user = id_user + device_service_config.id_service = id_service + device_service_config.service_config_config = config + TeraServiceConfig.insert(device_service_config) + return device_service_config diff --git a/teraserver/python/tests/opentera/db/models/test_TeraSession.py b/teraserver/python/tests/opentera/db/models/test_TeraSession.py index dfaddc41..f1687641 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraSession.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraSession.py @@ -144,16 +144,28 @@ def test_hard_delete(self): self.assertIsNone(TeraTest.get_test_by_id(id_test, with_deleted=True)) @staticmethod - def new_test_session(id_creator_service: int = 1, id_session_type: int = 1, participants: list | None = None, + def new_test_session(id_session_type: int = 1, id_creator_service: int | None = None, + id_creator_device: int | None = None, id_creator_participant: int | None = None, + id_creator_user: int | None = None, participants: list | None = None, devices: list | None = None, users: list | None = None) -> TeraSession: if participants is None: participants = [] ses = TeraSession() - ses.id_creator_service = id_creator_service + if id_creator_service: + ses.id_creator_service = id_creator_service + if id_creator_device: + ses.id_creator_device = id_creator_device + if id_creator_participant: + ses.id_creator_participant = id_creator_participant + if id_creator_user: + ses.id_creator_user = id_creator_user ses.id_session_type = id_session_type ses.session_name = "Test session" - ses.session_participants = participants - ses.session_devices = devices - ses.session_users = users + if participants: + ses.session_participants = participants + if devices: + ses.session_devices = devices + if users: + ses.session_users = users TeraSession.insert(ses) return ses diff --git a/teraserver/python/tests/opentera/db/models/test_TeraSite.py b/teraserver/python/tests/opentera/db/models/test_TeraSite.py index 86442fbe..ebb3b9ee 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraSite.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraSite.py @@ -27,10 +27,7 @@ def test_unique_args(self): def test_to_json(self): with self._flask_app.app_context(): - new_site = TeraSite() - new_site.site_name = 'Site Name' - self.db.session.add(new_site) - self.db.session.commit() + new_site = TeraSiteTest.new_test_site() new_site_json = new_site.to_json() new_site_json_minimal = new_site.to_json(minimal=True) self.assertEqual(new_site_json['site_name'], 'Site Name') @@ -52,21 +49,14 @@ def test_to_json_create_event(self): def test_to_json_update_event(self): with self._flask_app.app_context(): - new_site = TeraSite() - new_site.site_name = 'test_to_json_update_event' - self.db.session.add(new_site) - self.db.session.commit() - self.db.session.rollback() + new_site = TeraSiteTest.new_test_site() new_site_json = new_site.to_json_update_event() self.assertEqual(new_site_json['site_name'], new_site.site_name) self.assertGreaterEqual(new_site_json['id_site'], 1) def test_to_json_delete_event(self): with self._flask_app.app_context(): - new_site = TeraSite() - new_site.site_name = 'test_to_json_delete_event' - self.db.session.add(new_site) - self.db.session.commit() + new_site = TeraSiteTest.new_test_site() new_site_json_delete = new_site.to_json_delete_event() self.assertGreaterEqual(new_site_json_delete['id_site'], 1) @@ -90,9 +80,7 @@ def test_get_site_by_id(self): def test_insert_and_delete(self): with self._flask_app.app_context(): - new_site = TeraSite() - new_site.site_name = 'test_insert_and_delete' - TeraSite.insert(site=new_site) + new_site = TeraSiteTest.new_test_site() self.assertGreaterEqual(new_site.id_site, 1) id_to_del = TeraSite.get_site_by_id(new_site.id_site).id_site TeraSite.delete(id_todel=id_to_del) @@ -104,9 +92,7 @@ def test_insert_and_delete(self): def test_soft_delete(self): with self._flask_app.app_context(): # Create new - site = TeraSite() - site.site_name = "Test Site" - TeraSite.insert(site) + site = TeraSiteTest.new_test_site() id_site = site.id_site # Soft delete @@ -123,9 +109,7 @@ def test_soft_delete(self): def test_hard_delete(self): with self._flask_app.app_context(): # Create new - site = TeraSite() - site.site_name = "Test Site" - TeraSite.insert(site) + site = TeraSiteTest.new_test_site() id_site = site.id_site project = TeraProject() @@ -175,4 +159,11 @@ def test_hard_delete(self): self.assertIsNone(TeraParticipant.get_participant_by_id(id_participant, True)) self.assertIsNone(TeraSession.get_session_by_id(id_session, True)) + @staticmethod + def new_test_site() -> TeraSite: + site = TeraSite() + site.site_name = "Test Site" + TeraSite.insert(site) + return site + diff --git a/teraserver/python/tests/opentera/db/models/test_TeraTest.py b/teraserver/python/tests/opentera/db/models/test_TeraTest.py index db530d3f..ae9143f0 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraTest.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraTest.py @@ -1,5 +1,10 @@ from tests.opentera.db.models.BaseModelsTest import BaseModelsTest from opentera.db.models.TeraTest import TeraTest +from opentera.db.models.TeraSession import TeraSession +from opentera.db.models.TeraParticipant import TeraParticipant +from opentera.db.models.TeraUser import TeraUser +from opentera.db.models.TeraDevice import TeraDevice +from sqlalchemy.exc import IntegrityError class TeraTestTest(BaseModelsTest): @@ -11,12 +16,7 @@ def test_defaults(self): def test_soft_delete(self): with self._flask_app.app_context(): # Create new - test = TeraTest() - test.id_participant = 1 - test.id_session = 1 - test.id_test_type = 1 - test.test_name = "Test test!" - TeraTest.insert(test) + test = TeraTestTest.new_test_test(id_session=1, id_participant=1) id_test = test.id_test # Soft delete @@ -33,12 +33,7 @@ def test_soft_delete(self): def test_hard_delete(self): with self._flask_app.app_context(): # Create new - test = TeraTest() - test.id_participant = 1 - test.id_session = 1 - test.id_test_type = 1 - test.test_name = "Test test!" - TeraTest.insert(test) + test = TeraTestTest.new_test_test(id_session=1, id_device=1) self.assertIsNotNone(test.id_test) id_test = test.id_test @@ -50,19 +45,107 @@ def test_hard_delete(self): def test_undelete(self): with self._flask_app.app_context(): - # Create new - test = TeraTest() - test.id_participant = 1 - test.id_session = 1 - test.id_test_type = 1 - test.test_name = "Test test!" - TeraTest.insert(test) + # Create new participant + from 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 + device = TeraDeviceTest.new_test_device() + id_device = device.id_device + + # Create new user + from test_TeraUser import TeraUserTest + user = TeraUserTest.new_test_user() + id_user = user.id_user + + # Create new session + from test_TeraSession import TeraSessionTest + ses = TeraSessionTest.new_test_session(participants=[participant], users=[user], devices=[device]) + id_session = ses.id_session + + # Create new test + test = TeraTestTest.new_test_test(id_session=ses.id_session, id_user=id_user, id_participant=id_participant, + id_device=id_device) self.assertIsNotNone(test.id_test) id_test = test.id_test - # Soft delete - TeraTest.delete(id_test) + # Undelete + TeraTest.undelete(id_test) - # Make sure it is deleted - self.assertIsNone(TeraTest.get_test_by_id(id_test)) + # Make sure it is back! + self.db.session.expire_all() + test = TeraTest.get_test_by_id(id_test) + self.assertIsNotNone(test) + self.assertIsNone(test.deleted_at) + + # Now, delete again but with its dependencies... + # Test will be deleted with the session + TeraSession.delete(id_session) + TeraParticipant.delete(id_participant) + TeraDevice.delete(id_device) + TeraUser.delete(id_user) + + # Exception should be thrown when trying to undelete + with self.assertRaises(IntegrityError) as cm: + TeraTest.undelete(id_test) + + # Restore participant + TeraParticipant.undelete(id_participant) + participant = TeraParticipant.get_participant_by_id(id_participant) + self.assertIsNotNone(participant) + + # Restore test - still has dependencies issues... + with self.assertRaises(IntegrityError) as cm: + TeraTest.undelete(id_test) + + # Restore user + TeraUser.undelete(id_user) + user = TeraUser.get_user_by_id(id_user) + self.assertIsNotNone(user) + + # Restore test - still has dependencies issues... + with self.assertRaises(IntegrityError) as cm: + TeraTest.undelete(id_test) + + # Restore device + TeraDevice.undelete(id_device) + device = TeraDevice.get_device_by_id(id_device) + self.assertIsNotNone(device) + + # Restore test - still has dependencies issues... + with self.assertRaises(IntegrityError) as cm: + TeraTest.undelete(id_test) + + # Restore session + TeraSession.undelete(id_session) + + ses = TeraSession.get_session_by_id(id_session) + self.assertIsNotNone(ses) + + # Test was restored with the session... + self.db.session.expire_all() + test = TeraTest.get_test_by_id(id_test) + self.assertIsNotNone(test) + self.assertIsNone(test.deleted_at) + + @staticmethod + def new_test_test(id_session: int, id_test_type: int = 1, id_device: int | None = None, + id_participant: int | None = None, id_service: int | None = None, + id_user: int | None = None) -> TeraTest: + test = TeraTest() + test.id_session = id_session + test.id_test_type = id_test_type + test.test_name = "Test test!" + if id_device: + test.id_device = id_device + if id_participant: + test.id_participant = id_participant + if id_service: + test.id_service = id_service + if id_user: + test.id_user = id_user + TeraTest.insert(test) + return test From ac63e456b918a62eb614dea07ef5d78022cf09ed Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Wed, 26 Jul 2023 14:45:19 -0400 Subject: [PATCH 24/80] Refs #202 Work in progress... --- .../python/opentera/db/models/TeraDevice.py | 3 + .../opentera/db/models/TeraParticipant.py | 3 + .../opentera/db/models/test_TeraDevice.py | 2 + .../db/models/test_TeraParticipant.py | 135 ++++++++++++--- .../db/models/test_TeraParticipantGroup.py | 68 +++++++- .../opentera/db/models/test_TeraProject.py | 159 +++++++++--------- 6 files changed, 252 insertions(+), 118 deletions(-) diff --git a/teraserver/python/opentera/db/models/TeraDevice.py b/teraserver/python/opentera/db/models/TeraDevice.py index fb02c1e5..d48b9f45 100644 --- a/teraserver/python/opentera/db/models/TeraDevice.py +++ b/teraserver/python/opentera/db/models/TeraDevice.py @@ -269,3 +269,6 @@ def hard_delete_before(self): # Delete sessions that we are part of since they will not be deleted otherwise for ses in self.device_sessions: ses.hard_delete() + + def get_undelete_cascade_relations(self): + return ['device_service_config'] diff --git a/teraserver/python/opentera/db/models/TeraParticipant.py b/teraserver/python/opentera/db/models/TeraParticipant.py index 0dd145d4..6cc64f73 100644 --- a/teraserver/python/opentera/db/models/TeraParticipant.py +++ b/teraserver/python/opentera/db/models/TeraParticipant.py @@ -412,3 +412,6 @@ def hard_delete_before(self): # Delete sessions that we are part of since they will not be deleted otherwise for ses in self.participant_sessions: ses.hard_delete() + + def get_undelete_cascade_relations(self): + return ['participant_service_config'] diff --git a/teraserver/python/tests/opentera/db/models/test_TeraDevice.py b/teraserver/python/tests/opentera/db/models/test_TeraDevice.py index 87ba939c..901ea1fb 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraDevice.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraDevice.py @@ -273,6 +273,8 @@ def test_undelete(self): self.assertIsNone(asset) # Asset stays deleted test = TeraTest.get_test_by_id(id_test) self.assertIsNone(test) # Test stays deleted + config = TeraServiceConfig.get_service_config_by_id(id_service_config) + self.assertIsNotNone(config) @staticmethod def new_test_device() -> TeraDevice: diff --git a/teraserver/python/tests/opentera/db/models/test_TeraParticipant.py b/teraserver/python/tests/opentera/db/models/test_TeraParticipant.py index 636a4208..20f03b58 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraParticipant.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraParticipant.py @@ -4,6 +4,9 @@ from opentera.db.models.TeraAsset import TeraAsset from opentera.db.models.TeraTest import TeraTest from opentera.db.models.TeraService import TeraService +from opentera.db.models.TeraProject import TeraProject +from opentera.db.models.TeraServiceConfig import TeraServiceConfig +from sqlalchemy.exc import IntegrityError import uuid from tests.opentera.db.models.BaseModelsTest import BaseModelsTest @@ -65,38 +68,23 @@ def test_hard_delete(self): id_participant = participant.id_participant # Assign participant to sessions - part_session = TeraSession() - part_session.id_creator_participant = id_participant - part_session.id_session_type = 1 - part_session.session_name = 'Creator participant session' - TeraSession.insert(part_session) + from test_TeraSession import TeraSessionTest + part_session = TeraSessionTest.new_test_session(id_creator_participant=id_participant) id_session = part_session.id_session - part_session = TeraSession() - part_session.id_creator_service = 1 - part_session.id_session_type = 1 - part_session.session_name = "Participant invitee session" - part_session.session_participants = [participant] - TeraSession.insert(part_session) + part_session = TeraSessionTest.new_test_session(id_creator_service=1, participants=[participant]) id_session_invitee = part_session.id_session # Attach asset - asset = TeraAsset() - asset.asset_name = "Participant asset test" - asset.id_participant = id_participant - asset.id_session = id_session - asset.asset_service_uuid = TeraService.get_openteraserver_service().service_uuid - asset.asset_type = 'Test' - TeraAsset.insert(asset) + from 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 - test = TeraTest() - test.id_participant = id_participant - test.id_session = id_session - test.id_test_type = 1 - test.test_name = "Device test test!" - TeraTest.insert(test) + from test_TeraTest import TeraTestTest + test = TeraTestTest.new_test_test(id_session=id_session, id_participant=id_participant) id_test = test.id_test # Soft delete device to prevent relationship integrity errors as we want to test hard-delete cascade here @@ -126,11 +114,108 @@ def test_hard_delete(self): self.assertIsNone(TeraAsset.get_asset_by_id(id_asset, True)) self.assertIsNone(TeraTest.get_test_by_id(id_test, True)) + def test_undelete(self): + with self._flask_app.app_context(): + # Create a new project + from test_TeraProject import TeraProjectTest + project = TeraProjectTest.new_test_project() + id_project = project.id_project + + # Create a new participant + participant = TeraParticipantTest.new_test_participant(id_project=id_project) + self.assertIsNotNone(participant.id_participant) + id_participant = participant.id_participant + + # Assign participant to sessions + from test_TeraSession import TeraSessionTest + part_session = TeraSessionTest.new_test_session(id_creator_participant=id_participant) + id_session = part_session.id_session + + part_session = TeraSessionTest.new_test_session(id_creator_service=1, participants=[participant]) + id_session_invitee = part_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_participant=id_participant) + id_asset = asset.id_asset + + # ... and test + from 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 + device_service_config = TeraServiceConfigTest.new_test_service_config(id_participant=id_participant, + id_service=2) + id_service_config = device_service_config.id_service_config + + # Soft delete device to prevent relationship integrity errors as it's not what we want to test here + TeraSession.delete(id_session) + TeraSession.delete(id_session_invitee) + TeraParticipant.delete(id_participant) + + # Undelete participant + TeraParticipant.undelete(id_participant) + + # ... then delete it again... + TeraParticipant.delete(id_participant) + + # ... and its project... + TeraProject.delete(id_project) + + # Then undelete participant with an error + with self.assertRaises(IntegrityError) as cm: + TeraParticipant.undelete(id_participant) + + # Then undelete project... + TeraProject.undelete(id_project) + + # And then undelete again! + TeraParticipant.undelete(id_participant) + + # Create and associate participant group + from test_TeraParticipantGroup import TeraParticipantGroupTest + group = TeraParticipantGroupTest.new_test_group(id_project) + id_group = group.id_participant_group + + TeraParticipant.update(id_participant, {'id_participant_group': id_group}) + + # Delete again... + TeraParticipant.delete(id_participant) + + # ... and delete group + TeraParticipantGroup.delete(id_group) + + # Then undelete participant with an error + with self.assertRaises(IntegrityError) as cm: + TeraParticipant.undelete(id_participant) + + # Restore group + TeraParticipantGroup.undelete(id_group) + + # ... and participant + TeraParticipant.undelete(id_participant) + + # Make sure associations are still deleted + self.assertIsNotNone(TeraParticipant.get_participant_by_id(id_participant)) + self.assertIsNotNone(TeraParticipantGroup.get_participant_group_by_id(id_group)) + self.assertIsNone(TeraSession.get_session_by_id(id_session)) + self.assertIsNone(TeraSession.get_session_by_id(id_session_invitee)) + self.assertIsNone(TeraAsset.get_asset_by_id(id_asset)) + self.assertIsNone(TeraTest.get_test_by_id(id_test)) + self.assertIsNotNone(TeraServiceConfig.get_service_config_by_id(id_service_config)) + @staticmethod - def new_test_participant(id_project: int) -> TeraParticipant: + def new_test_participant(id_project: int, id_participant_group: int | None = None, enabled: bool = False) -> TeraParticipant: participant = TeraParticipant() participant.participant_name = "Test participant" participant.id_project = id_project + if id_participant_group: + participant.id_participant_group = id_participant_group + participant.participant_enabled = enabled TeraParticipant.insert(participant) return participant diff --git a/teraserver/python/tests/opentera/db/models/test_TeraParticipantGroup.py b/teraserver/python/tests/opentera/db/models/test_TeraParticipantGroup.py index 418ec75c..b99384f8 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraParticipantGroup.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraParticipantGroup.py @@ -2,6 +2,8 @@ from opentera.db.models.TeraParticipantGroup import TeraParticipantGroup from opentera.db.models.TeraParticipant import TeraParticipant +from opentera.db.models.TeraProject import TeraProject +from sqlalchemy.exc import IntegrityError class TeraParticipantGroupTest(BaseModelsTest): @@ -33,19 +35,14 @@ def test_soft_delete(self): def test_hard_delete(self): with self._flask_app.app_context(): # Create a new participant group - group = TeraParticipantGroup() - group.participant_group_name = "Test participant group" - group.id_project = 1 - TeraParticipantGroup.insert(group) + group = TeraParticipantGroupTest.new_test_group() self.assertIsNotNone(group.id_participant_group) id_participant_group = group.id_participant_group # Create a new participant in that group - participant = TeraParticipant() - participant.participant_name = "Test participant" - participant.id_project = 1 - participant.id_participant_group = id_participant_group - TeraParticipant.insert(participant) + from test_TeraParticipant import TeraParticipantTest + participant = TeraParticipantTest.new_test_participant(id_project=1, + id_participant_group=id_participant_group) self.assertIsNotNone(participant.id_participant) id_participant = participant.id_participant @@ -65,3 +62,56 @@ def test_hard_delete(self): # Make sure eveything is deleted self.assertIsNone(TeraParticipant.get_participant_by_id(id_participant, True)) self.assertIsNone(TeraParticipantGroup.get_participant_group_by_id(id_participant_group, True)) + + def test_undelete(self): + with self._flask_app.app_context(): + # Create a new project + from test_TeraProject import TeraProjectTest + project = TeraProjectTest.new_test_project(id_site=1) + id_project = project.id_project + + # Create a new participant group + group = TeraParticipantGroupTest.new_test_group(id_project=id_project) + self.assertIsNotNone(group.id_participant_group) + id_participant_group = group.id_participant_group + + # Create a new participant in that group + from test_TeraParticipant import TeraParticipantTest + participant = TeraParticipantTest.new_test_participant(id_project=id_project, + id_participant_group=id_participant_group) + self.assertIsNotNone(participant.id_participant) + id_participant = participant.id_participant + + # Soft delete device to prevent relationship integrity errors as we want to test hard-delete cascade here + TeraParticipant.delete(id_participant) + TeraParticipantGroup.delete(id_participant_group) + + # Undelete + TeraParticipantGroup.undelete(id_participant_group) + self.assertIsNotNone(TeraParticipantGroup.get_participant_group_by_id(id_participant_group)) + + # ... delete again... + TeraParticipantGroup.delete(id_participant_group) + + # ... but delete project before now + TeraProject.delete(id_project) + + # Undelete will not work now... + with self.assertRaises(IntegrityError) as cm: + TeraParticipantGroup.undelete(id_participant_group) + + # But will if we restore the project beforehand! + TeraProject.undelete(id_project) + TeraParticipantGroup.undelete(id_participant_group) + + # Make sure eveything is Ok + self.assertIsNone(TeraParticipant.get_participant_by_id(id_participant)) + self.assertIsNotNone(TeraParticipantGroup.get_participant_group_by_id(id_participant_group)) + + @staticmethod + def new_test_group(id_project: int = 1) -> TeraParticipantGroup: + group = TeraParticipantGroup() + group.participant_group_name = "Test Group" + group.id_project = id_project + TeraParticipantGroup.insert(group) + return group diff --git a/teraserver/python/tests/opentera/db/models/test_TeraProject.py b/teraserver/python/tests/opentera/db/models/test_TeraProject.py index 6c7e9d1e..8725fc7a 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraProject.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraProject.py @@ -1,9 +1,11 @@ from tests.opentera.db.models.BaseModelsTest import BaseModelsTest from sqlalchemy import exc from opentera.db.models.TeraProject import TeraProject +from opentera.db.models.TeraSite import TeraSite from opentera.db.models.TeraParticipant import TeraParticipant from opentera.db.models.TeraDevice import TeraDevice from opentera.db.models.TeraParticipantGroup import TeraParticipantGroup +from sqlalchemy.exc import IntegrityError class TeraProjectTest(BaseModelsTest): @@ -27,11 +29,7 @@ def test_unique_args(self): def test_to_json(self): with self._flask_app.app_context(): - new_project = TeraProject() - new_project.id_site = 1 - new_project.project_name = 'test_to_json' - self.db.session.add(new_project) - self.db.session.commit() + new_project = TeraProjectTest.new_test_project() new_project_json = new_project.to_json() new_project_json_minimal = new_project.to_json(minimal=True) self._check_json(new_project, project_test=new_project_json) @@ -46,71 +44,43 @@ def _check_json(self, project: TeraProject, project_test: dict, minimal=False): def test_to_json_create_event(self): with self._flask_app.app_context(): - new_project = TeraProject() - new_project.id_site = 1 - new_project.project_name = 'test_to_json_create_event' - self.db.session.add(new_project) - self.db.session.commit() + new_project = TeraProjectTest.new_test_project() new_project_json = new_project.to_json_create_event() self._check_json(new_project, project_test=new_project_json, minimal=True) def test_to_json_update_event(self): with self._flask_app.app_context(): - new_project = TeraProject() - new_project.id_site = 1 - new_project.project_name = 'test_to_json_update_event' - self.db.session.add(new_project) - self.db.session.commit() + new_project = TeraProjectTest.new_test_project() new_project_json = new_project.to_json_update_event() self._check_json(new_project, project_test=new_project_json, minimal=True) def test_to_json_delete_event(self): with self._flask_app.app_context(): - new_project = TeraProject() - new_project.id_site = 1 - new_project.project_name = 'test_to_json_delete_event' - self.db.session.add(new_project) - self.db.session.commit() + new_project = TeraProjectTest.new_test_project() new_project_json = new_project.to_json_delete_event() self.assertGreaterEqual(new_project_json['id_project'], 1) def test_get_users_ids_in_project(self): with self._flask_app.app_context(): - new_project = TeraProject() - new_project.id_site = 1 - new_project.project_name = 'test_get_users_ids_in_project' - self.db.session.add(new_project) - self.db.session.commit() + new_project = TeraProjectTest.new_test_project() users_ids = new_project.get_users_ids_in_project() self.assertIsNotNone(users_ids) def test_get_users_in_project(self): with self._flask_app.app_context(): - new_project = TeraProject() - new_project.id_site = 1 - new_project.project_name = 'test_get_users_in_project' - self.db.session.add(new_project) - self.db.session.commit() + new_project = TeraProjectTest.new_test_project() users = new_project.get_users_in_project() self.assertIsNotNone(users) def test_get_project_by_projectname(self): with self._flask_app.app_context(): - new_project = TeraProject() - new_project.id_site = 1 - new_project.project_name = 'test_get_project_by_projectname' - self.db.session.add(new_project) - self.db.session.commit() + new_project = TeraProjectTest.new_test_project(name='test_project_by_name') same_project = new_project.get_project_by_projectname(projectname=new_project.project_name) self.assertEqual(same_project, new_project) def test_get_project_by_id(self): with self._flask_app.app_context(): - new_project = TeraProject() - new_project.id_site = 1 - new_project.project_name = 'test_get_project_by_id' - self.db.session.add(new_project) - self.db.session.commit() + new_project = TeraProjectTest.new_test_project() same_project = new_project.get_project_by_id(project_id=new_project.id_project) self.assertEqual(same_project, new_project) @@ -132,20 +102,13 @@ def test_insert_and_delete(self): def test_update_set_inactive(self): with self._flask_app.app_context(): - new_project = TeraProject() - new_project.id_site = 1 - new_project.project_name = 'test_update_set_inactive' - self.db.session.add(new_project) - self.db.session.commit() + new_project = TeraProjectTest.new_test_project() # Create participants participants = [] + from test_TeraParticipant import TeraParticipantTest for i in range(3): - part = TeraParticipant() - part.id_project = new_project.id_project - part.participant_name = 'Participant #' + str(i+1) - part.participant_enabled = True - TeraParticipant.insert(part) + part = TeraParticipantTest.new_test_participant(id_project=new_project.id_project, enabled=True) participants.append(part) for part in participants: @@ -153,11 +116,9 @@ def test_update_set_inactive(self): # Associate devices devices = [] + from test_TeraDevice import TeraDeviceTest for i in range(2): - device = TeraDevice() - device.device_name = 'Device #' + str(i+1) - device.id_device_type = 1 - TeraDevice.insert(device) + device = TeraDeviceTest.new_test_device() devices.append(device) part = participants[0] for device in devices: @@ -178,10 +139,7 @@ def test_update_set_inactive(self): def test_soft_delete(self): with self._flask_app.app_context(): # Create new - project = TeraProject() - project.project_name = "Test project" - project.id_site = 1 - TeraProject.insert(project) + project = TeraProjectTest.new_test_project() self.assertIsNotNone(project.id_project) id_project = project.id_project @@ -200,18 +158,13 @@ def test_soft_delete(self): def test_hard_delete(self): with self._flask_app.app_context(): # Create new - project = TeraProject() - project.project_name = "Test project" - project.id_site = 1 - TeraProject.insert(project) + project = TeraProjectTest.new_test_project() self.assertIsNotNone(project.id_project) id_project = project.id_project # Create a new participant in that project - participant = TeraParticipant() - participant.participant_name = "Test participant" - participant.id_project = id_project - TeraParticipant.insert(participant) + from test_TeraParticipant import TeraParticipantTest + participant = TeraParticipantTest.new_test_participant(id_project=id_project) self.assertIsNotNone(participant.id_participant) id_participant = participant.id_participant @@ -232,36 +185,68 @@ def test_hard_delete(self): self.assertIsNone(TeraParticipant.get_participant_by_id(id_participant, True)) self.assertIsNone(TeraProject.get_project_by_id(id_project, True)) + def test_undelete(self): + with self._flask_app.app_context(): + # Create site + from test_TeraSite import TeraSiteTest + site = TeraSiteTest.new_test_site() + id_site = site.id_site + + # Create new + project = TeraProjectTest.new_test_project(id_site=id_site) + self.assertIsNotNone(project.id_project) + id_project = project.id_project + + # Create a new participant in that 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 + + # Soft delete to prevent relationship integrity errors as we want to test hard-delete cascade here + TeraParticipant.delete(id_participant) + TeraProject.delete(id_project) + self.assertIsNone(TeraProject.get_project_by_id(id_project)) + + # Undelete + TeraProject.undelete(id_project) + self.assertIsNotNone(TeraProject.get_project_by_id(id_project)) + + # Delete project again... + TeraProject.delete(id_project) + + # ... then site. + TeraSite.delete(id_site) + self.assertIsNone(TeraSite.get_site_by_id(id_site)) + + # Undelete project with an error (since site is now deleted) + with self.assertRaises(IntegrityError) as cm: + TeraProject.undelete(id_project) + + # Undelete everything - should work + TeraSite.undelete(id_site) + TeraProject.undelete(id_project) + def test_project_relationships_deletion_and_access(self): with self._flask_app.app_context(): # Create new - project = TeraProject() - project.project_name = "Test project" - project.id_site = 1 - TeraProject.insert(project) + project = TeraProjectTest.new_test_project() self.assertIsNotNone(project.id_project) id_project = project.id_project # Create participant groups - group = TeraParticipantGroup() - group.participant_group_name = "Test participant group 1" - group.id_project = id_project - TeraParticipantGroup.insert(group) + from 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 - participant = TeraParticipant() - participant.participant_name = "Test participant" - participant.id_project = 1 - participant.id_participant_group = id_participant_group1 - TeraParticipant.insert(participant) + from test_TeraParticipant import TeraParticipantTest + participant = TeraParticipantTest.new_test_participant(id_project=1, + id_participant_group=id_participant_group1) self.assertIsNotNone(participant.id_participant) id_participant = participant.id_participant - group = TeraParticipantGroup() - group.participant_group_name = "Test participant group 2" - group.id_project = id_project - TeraParticipantGroup.insert(group) + group = TeraParticipantGroupTest.new_test_group(id_project=id_project) self.assertIsNotNone(group.id_participant_group) id_participant_group2 = group.id_participant_group @@ -284,4 +269,10 @@ def test_project_relationships_deletion_and_access(self): project = TeraProject.get_project_by_id(id_project) self.assertEqual(0, len(project.project_participants_groups[0].participant_group_participants)) - + @staticmethod + def new_test_project(id_site: int = 1, name: str = "Test Project") -> TeraProject: + project = TeraProject() + project.project_name = name + project.id_site = id_site + TeraProject.insert(project) + return project From fb55592da2647e10a8f38ffaade5ac70900f4db6 Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Thu, 27 Jul 2023 15:23:09 -0400 Subject: [PATCH 25/80] Refs #202. Almost completed all models tests for undelete feature. --- .../python/opentera/db/SoftDeleteMixin.py | 6 +- .../python/opentera/db/models/TeraProject.py | 3 + .../python/opentera/db/models/TeraService.py | 3 + .../opentera/db/models/TeraSessionEvent.py | 1 + .../python/opentera/db/models/TeraSite.py | 3 + .../opentera/db/models/BaseModelsTest.py | 10 +-- .../opentera/db/models/test_TeraAsset.py | 6 +- .../opentera/db/models/test_TeraProject.py | 10 ++- .../opentera/db/models/test_TeraService.py | 80 ++++++++++++----- .../db/models/test_TeraServiceProject.py | 9 ++ .../db/models/test_TeraServiceRole.py | 10 +++ .../db/models/test_TeraServiceSite.py | 9 ++ .../opentera/db/models/test_TeraSession.py | 64 +++++++++++-- .../db/models/test_TeraSessionEvent.py | 11 +++ .../db/models/test_TeraSessionType.py | 89 ++++++++++++++---- .../db/models/test_TeraSessionTypeProject.py | 9 ++ .../db/models/test_TeraSessionTypeSite.py | 17 ++++ .../tests/opentera/db/models/test_TeraSite.py | 90 +++++++++++++++---- .../opentera/db/models/test_TeraTestType.py | 67 +++++++++++--- .../db/models/test_TeraTestTypeProject.py | 17 ++++ .../db/models/test_TeraTestTypeSite.py | 17 ++++ .../tests/opentera/db/models/test_TeraUser.py | 87 ++++++++++++------ .../opentera/db/models/test_TeraUserGroup.py | 28 +++--- .../db/models/test_TeraUserUserGroup.py | 8 ++ 24 files changed, 523 insertions(+), 131 deletions(-) create mode 100644 teraserver/python/tests/opentera/db/models/test_TeraSessionTypeSite.py create mode 100644 teraserver/python/tests/opentera/db/models/test_TeraTestTypeProject.py create mode 100644 teraserver/python/tests/opentera/db/models/test_TeraTestTypeSite.py diff --git a/teraserver/python/opentera/db/SoftDeleteMixin.py b/teraserver/python/opentera/db/SoftDeleteMixin.py index c219c3af..6e76f9fb 100644 --- a/teraserver/python/opentera/db/SoftDeleteMixin.py +++ b/teraserver/python/opentera/db/SoftDeleteMixin.py @@ -175,7 +175,7 @@ def undelete_method(_self): raise IntegrityError('Cannot undelete: unsatisfied foreign key - ' + col.name, col_value, remote_table_name) # Undelete! - print("Undeleting " + str(_self.__class__)) + # print("Undeleting " + str(_self.__class__)) setattr(_self, deleted_field_name, None) # Check relationships that are cascade deleted to restore them @@ -194,7 +194,7 @@ def undelete_method(_self): related_items = relation[1].entity.class_.query.execution_options(include_deleted=True).\ filter(text(remote_primary_key + '=' + str(self_key_value))).all() for item in related_items: - print("--> Undeleting relationship " + relation[1].key) + # print("--> Undeleting relationship " + relation[1].key) item_undeleter = getattr(item, undelete_method_name) item_undeleter() continue @@ -212,7 +212,7 @@ def undelete_method(_self): remote_item = remote_model.query.filter( text(remote_primary_key + '=' + str(getattr(item, remote_primary_key)))).first() if remote_item: - print("--> Undeleting relationship with " + relation[1].target.name) + # print("--> Undeleting relationship with " + relation[1].target.name) item_undeleter = getattr(item, undelete_method_name) item_undeleter() diff --git a/teraserver/python/opentera/db/models/TeraProject.py b/teraserver/python/opentera/db/models/TeraProject.py index 4e967585..34a35fda 100644 --- a/teraserver/python/opentera/db/models/TeraProject.py +++ b/teraserver/python/opentera/db/models/TeraProject.py @@ -186,3 +186,6 @@ def insert(cls, project): TeraProject.db().session.add(access_role) TeraProject.db().session.commit() + + def get_undelete_cascade_relations(self): + return ['project_services_roles'] diff --git a/teraserver/python/opentera/db/models/TeraService.py b/teraserver/python/opentera/db/models/TeraService.py index 72097178..bf15a966 100644 --- a/teraserver/python/opentera/db/models/TeraService.py +++ b/teraserver/python/opentera/db/models/TeraService.py @@ -253,3 +253,6 @@ def update(cls, update_id: int, values: dict): del values['service_uuid'] super().update(update_id, values) + + def get_undelete_cascade_relations(self) -> list: + return ['service_sites', 'service_projects', 'service_roles'] diff --git a/teraserver/python/opentera/db/models/TeraSessionEvent.py b/teraserver/python/opentera/db/models/TeraSessionEvent.py index 9444990d..aef0b2e9 100644 --- a/teraserver/python/opentera/db/models/TeraSessionEvent.py +++ b/teraserver/python/opentera/db/models/TeraSessionEvent.py @@ -42,6 +42,7 @@ class SessionEventTypes(Enum): __tablename__ = 't_sessions_events' id_session_event = Column(Integer, Sequence('id_session_events_sequence'), primary_key=True, autoincrement=True) id_session = Column(Integer, ForeignKey('t_sessions.id_session', ondelete='cascade'), nullable=False) + # TODO: Typo that should be fixed someday... id_session_event_type = Column(Integer, nullable=False) session_event_datetime = Column(TIMESTAMP(timezone=True), nullable=False) session_event_text = Column(String, nullable=True) diff --git a/teraserver/python/opentera/db/models/TeraSite.py b/teraserver/python/opentera/db/models/TeraSite.py index 959593c9..a6804a24 100644 --- a/teraserver/python/opentera/db/models/TeraSite.py +++ b/teraserver/python/opentera/db/models/TeraSite.py @@ -87,3 +87,6 @@ def insert(cls, site): access_role.id_site = site.id_site access_role.service_role_name = 'user' TeraServiceRole.insert(access_role) + + def get_undelete_cascade_relations(self) -> list: + return ['site_services_roles'] diff --git a/teraserver/python/tests/opentera/db/models/BaseModelsTest.py b/teraserver/python/tests/opentera/db/models/BaseModelsTest.py index 57ff6b0a..cba132c8 100644 --- a/teraserver/python/tests/opentera/db/models/BaseModelsTest.py +++ b/teraserver/python/tests/opentera/db/models/BaseModelsTest.py @@ -15,11 +15,11 @@ def setUpClass(cls): cls._flask_app.config.update({'PROPAGATE_EXCEPTIONS': True}) cls._db_man = DBManager(cls._config, app=cls._flask_app) # Setup DB in RAM - filename = 'D:\\temp\\opentera.db' - import os - os.remove(filename) - cls._db_man.open_local({'filename': filename}, echo=False, ram=False) - # cls._db_man.open_local({}, echo=False, ram=True) + # filename = 'D:\\temp\\opentera.db' + # import os + # os.remove(filename) + # cls._db_man.open_local({'filename': filename}, echo=False, ram=False) + cls._db_man.open_local({}, echo=False, ram=True) # Creating default users / tests. Time-consuming, only once per test file. with cls._flask_app.app_context(): diff --git a/teraserver/python/tests/opentera/db/models/test_TeraAsset.py b/teraserver/python/tests/opentera/db/models/test_TeraAsset.py index f8c1ffc2..fbbbbb69 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraAsset.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraAsset.py @@ -353,7 +353,7 @@ def test_undelete(self): self.assertIsNotNone(participant) # Restore asset - still has dependencies issues... - with self.assertRaises(IntegrityError) as cm: + with self.assertRaises(IntegrityError): TeraAsset.undelete(id_asset) # Restore user @@ -362,7 +362,7 @@ def test_undelete(self): self.assertIsNotNone(user) # Restore asset - still has dependencies issues... - with self.assertRaises(IntegrityError) as cm: + with self.assertRaises(IntegrityError): TeraAsset.undelete(id_asset) # Restore device @@ -371,7 +371,7 @@ def test_undelete(self): self.assertIsNotNone(device) # Restore asset - still has dependencies issues... - with self.assertRaises(IntegrityError) as cm: + with self.assertRaises(IntegrityError): TeraAsset.undelete(id_asset) # Restore session diff --git a/teraserver/python/tests/opentera/db/models/test_TeraProject.py b/teraserver/python/tests/opentera/db/models/test_TeraProject.py index 8725fc7a..2ad0e968 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraProject.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraProject.py @@ -3,7 +3,7 @@ from opentera.db.models.TeraProject import TeraProject from opentera.db.models.TeraSite import TeraSite from opentera.db.models.TeraParticipant import TeraParticipant -from opentera.db.models.TeraDevice import TeraDevice +from opentera.db.models.TeraServiceRole import TeraServiceRole from opentera.db.models.TeraParticipantGroup import TeraParticipantGroup from sqlalchemy.exc import IntegrityError @@ -213,7 +213,10 @@ def test_undelete(self): self.assertIsNotNone(TeraProject.get_project_by_id(id_project)) # Delete project again... + project_service_roles_ids = [role.id_service_role for role in project.project_services_roles] TeraProject.delete(id_project) + for id_role in project_service_roles_ids: + self.assertIsNone(TeraServiceRole.get_service_role_by_id(id_role)) # ... then site. TeraSite.delete(id_site) @@ -227,6 +230,11 @@ def test_undelete(self): TeraSite.undelete(id_site) TeraProject.undelete(id_project) + # Check that project service roles are also undeleted + self.db.session.expire_all() + project = TeraProject.get_project_by_id(id_project) + self.assertEqual(len(project.project_services_roles), 2) + def test_project_relationships_deletion_and_access(self): with self._flask_app.app_context(): # Create new diff --git a/teraserver/python/tests/opentera/db/models/test_TeraService.py b/teraserver/python/tests/opentera/db/models/test_TeraService.py index ff421005..7e91c906 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraService.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraService.py @@ -2,6 +2,8 @@ from sqlalchemy import exc from opentera.db.models.TeraService import TeraService from opentera.db.models.TeraServiceSite import TeraServiceSite +from opentera.db.models.TeraServiceProject import TeraServiceProject +from opentera.db.models.TeraServiceRole import TeraServiceRole class TeraServiceTest(BaseModelsTest): @@ -63,7 +65,6 @@ def test_nullable_bool1(self): new_service.service_clientendpoint = 'Clientendpoint' new_service.service_system = True new_service.service_editable_config = True - new_service.service_enabled = None self.db.session.add(new_service) self.assertRaises(exc.IntegrityError, self.db.session.commit) @@ -376,14 +377,7 @@ def test_service_port_integer_value(self): def test_soft_delete(self): with self._flask_app.app_context(): # Create new - service = TeraService() - service.service_name = 'Test Service' - service.service_key = 'TestService' - service.service_hostname = 'localhost' - service.service_port = 12345 - service.service_endpoint = 'test' - service.service_clientendpoint = '/' - TeraService.insert(service) + service = TeraServiceTest.new_test_service('TestService') self.assertIsNotNone(service.id_service) id_service = service.id_service @@ -401,22 +395,13 @@ def test_soft_delete(self): def test_hard_delete(self): with self._flask_app.app_context(): # Create new - service = TeraService() - service.service_name = 'Test Service' - service.service_key = 'TestService' - service.service_hostname = 'localhost' - service.service_port = 12345 - service.service_endpoint = 'test' - service.service_clientendpoint = '/' - TeraService.insert(service) + service = TeraServiceTest.new_test_service('TestService') self.assertIsNotNone(service.id_service) id_service = service.id_service # Create a new site association for that service - site_service = TeraServiceSite() - site_service.id_service = id_service - site_service.id_site = 1 - TeraServiceSite.insert(site_service) + from 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 @@ -436,3 +421,56 @@ def test_hard_delete(self): # Make sure eveything is deleted self.assertIsNone(TeraService.get_service_by_id(id_service, True)) self.assertIsNone(TeraServiceSite.get_service_site_by_id(id_site_service, True)) + + def test_undelete(self): + with self._flask_app.app_context(): + # Create new + service = TeraServiceTest.new_test_service('TestService') + self.assertIsNotNone(service.id_service) + id_service = service.id_service + + # Create service roles + from test_TeraServiceRole import TeraServiceRoleTest + role = TeraServiceRoleTest.new_test_service_role(id_service=id_service, role_name='admin') + id_role_admin = role.id_service_role + + role = TeraServiceRoleTest.new_test_service_role(id_service=id_service, role_name='user') + id_role_user = role.id_service_role + + # Create service sites association + from 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 + service_project = TeraServiceProjectTest.new_test_service_project(id_service=id_service, id_project=1) + id_service_project = service_project.id_service_project + + # Soft delete to prevent relationship integrity errors as we want to test hard-delete cascade here + TeraServiceSite.delete(id_service_site) + TeraServiceProject.delete(id_service_project) + TeraService.delete(id_service) + self.assertIsNone(TeraService.get_service_by_id(id_service)) + self.assertIsNone(TeraServiceRole.get_service_role_by_id(id_role_user)) + self.assertIsNone(TeraServiceRole.get_service_role_by_id(id_role_admin)) + + # Undelete service + TeraService.undelete(id_service) + self.assertIsNotNone(TeraService.get_service_by_id(id_service)) + self.assertIsNotNone(TeraServiceProject.get_service_project_by_id(id_service_project)) + self.assertIsNotNone(TeraServiceSite.get_service_site_by_id(id_service_site)) + self.assertIsNotNone(TeraServiceRole.get_service_role_by_id(id_role_user)) + self.assertIsNotNone(TeraServiceRole.get_service_role_by_id(id_role_admin)) + + @staticmethod + def new_test_service(service_key: str): + service = TeraService() + service.service_name = 'Test Service' + service.service_key = service_key + service.service_hostname = 'localhost' + service.service_port = 12345 + service.service_endpoint = 'test' + service.service_clientendpoint = '/' + TeraService.insert(service) + return service diff --git a/teraserver/python/tests/opentera/db/models/test_TeraServiceProject.py b/teraserver/python/tests/opentera/db/models/test_TeraServiceProject.py index 9b24f839..ac82d0f9 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraServiceProject.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraServiceProject.py @@ -1,4 +1,5 @@ from tests.opentera.db.models.BaseModelsTest import BaseModelsTest +from opentera.db.models.TeraServiceProject import TeraServiceProject class TeraServiceProjectTest(BaseModelsTest): @@ -6,3 +7,11 @@ class TeraServiceProjectTest(BaseModelsTest): def test_defaults(self): with self._flask_app.app_context(): pass + + @staticmethod + def new_test_service_project(id_service: int, id_project:int) -> TeraServiceProject: + service_project = TeraServiceProject() + service_project.id_service = id_service + service_project.id_project = id_project + TeraServiceProject.insert(service_project) + return service_project diff --git a/teraserver/python/tests/opentera/db/models/test_TeraServiceRole.py b/teraserver/python/tests/opentera/db/models/test_TeraServiceRole.py index e7ceba4c..3d87ec68 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraServiceRole.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraServiceRole.py @@ -139,3 +139,13 @@ def test_get_service_role_by_id(self): self.db.session.commit() same_service_role = TeraServiceRole.get_service_role_by_id(role_id=new_service_role.id_service_role) self.assertEqual(same_service_role, new_service_role) + + @staticmethod + def new_test_service_role(id_service: int, role_name: str, id_site: int | None = None) -> TeraServiceRole: + service_role = TeraServiceRole() + service_role.id_service = id_service + service_role.service_role_name = role_name + if id_site: + service_role.id_site = id_site + TeraServiceRole.insert(service_role) + return service_role diff --git a/teraserver/python/tests/opentera/db/models/test_TeraServiceSite.py b/teraserver/python/tests/opentera/db/models/test_TeraServiceSite.py index c3ff81a4..8b8fafe1 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraServiceSite.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraServiceSite.py @@ -1,4 +1,5 @@ from tests.opentera.db.models.BaseModelsTest import BaseModelsTest +from opentera.db.models.TeraServiceSite import TeraServiceSite class TeraServiceSiteTest(BaseModelsTest): @@ -6,3 +7,11 @@ class TeraServiceSiteTest(BaseModelsTest): def test_defaults(self): with self._flask_app.app_context(): pass + + @staticmethod + def new_test_service_site(id_site: int, id_service: int) -> TeraServiceSite: + site_service = TeraServiceSite() + site_service.id_service = id_service + site_service.id_site = id_site + TeraServiceSite.insert(site_service) + return site_service diff --git a/teraserver/python/tests/opentera/db/models/test_TeraSession.py b/teraserver/python/tests/opentera/db/models/test_TeraSession.py index f1687641..a0678963 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraSession.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraSession.py @@ -8,6 +8,7 @@ from opentera.db.models.TeraAsset import TeraAsset from opentera.db.models.TeraTest import TeraTest from opentera.db.models.TeraService import TeraService +from opentera.db.models.TeraSessionEvent import TeraSessionEvent from tests.opentera.db.models.BaseModelsTest import BaseModelsTest @@ -108,9 +109,9 @@ def test_soft_delete(self): def test_hard_delete(self): with self._flask_app.app_context(): # Create new - ses = ses = TeraSessionTest.new_test_session(participants=[TeraParticipant.get_participant_by_id(1)], - devices=[TeraDevice.get_device_by_id(1)], - users=[TeraUser.get_user_by_id(2)]) + ses = TeraSessionTest.new_test_session(participants=[TeraParticipant.get_participant_by_id(1)], + devices=[TeraDevice.get_device_by_id(1)], + users=[TeraUser.get_user_by_id(2)]) id_session = ses.id_session # Attach asset @@ -121,12 +122,8 @@ def test_hard_delete(self): id_asset = asset.id_asset # ... and test - test = TeraTest() - test.id_device = 1 - test.id_session = id_session - test.id_test_type = 1 - test.test_name = "Test test!" - TeraTest.insert(test) + from test_TeraTest import TeraTestTest + test = TeraTestTest.new_test_test(id_session=id_session, id_participant=1) id_test = test.id_test # Hard delete @@ -143,6 +140,55 @@ def test_hard_delete(self): self.assertIsNone(TeraAsset.get_asset_by_id(id_asset, with_deleted=True)) self.assertIsNone(TeraTest.get_test_by_id(id_test, with_deleted=True)) + def test_undelete(self): + with self._flask_app.app_context(): + # Create new + ses = TeraSessionTest.new_test_session(participants=[TeraParticipant.get_participant_by_id(1)], + devices=[TeraDevice.get_device_by_id(1)], + users=[TeraUser.get_user_by_id(2)]) + id_session = ses.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_device=1) + id_asset = asset.id_asset + + # ... and test + from 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 + event = TeraSessionEventTest.new_test_session_event(id_session=id_session, id_event_type=1) + id_event = event.id_session_event + + # Delete + TeraSession.delete(id_session) + self.assertIsNone(TeraSession.get_session_by_id(id_session)) + self.assertIsNone(TeraTest.get_test_by_id(id_test)) + self.assertIsNone(TeraAsset.get_asset_by_id(id_asset)) + self.assertIsNone(TeraSessionEvent.get_session_event_by_id(id_event)) + self.assertIsNone(TeraSessionParticipants.query.filter_by(id_session=id_session).first()) + self.assertIsNone(TeraSessionUsers.query.filter_by(id_session=id_session).first()) + self.assertIsNone(TeraSessionDevices.query.filter_by(id_session=id_session).first()) + + # Undelete + TeraSession.undelete(id_session) + self.assertIsNotNone(TeraSession.get_session_by_id(id_session)) + self.assertIsNotNone(TeraTest.get_test_by_id(id_test)) + self.assertIsNotNone(TeraAsset.get_asset_by_id(id_asset)) + self.assertIsNotNone(TeraSessionEvent.get_session_event_by_id(id_event)) + self.assertIsNotNone(TeraSessionParticipants.query.filter_by(id_session=id_session) + .execution_options(include_deleted=True).first()) + self.assertIsNotNone(TeraSessionUsers.query.filter_by(id_session=id_session) + .execution_options(include_deleted=True).first()) + self.assertIsNotNone(TeraSessionDevices.query.filter_by(id_session=id_session) + .execution_options(include_deleted=True).first()) + + @staticmethod def new_test_session(id_session_type: int = 1, id_creator_service: int | None = None, id_creator_device: int | None = None, id_creator_participant: int | None = None, diff --git a/teraserver/python/tests/opentera/db/models/test_TeraSessionEvent.py b/teraserver/python/tests/opentera/db/models/test_TeraSessionEvent.py index e0f45ca0..e8b6ab51 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraSessionEvent.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraSessionEvent.py @@ -1,4 +1,6 @@ from tests.opentera.db.models.BaseModelsTest import BaseModelsTest +from opentera.db.models.TeraSessionEvent import TeraSessionEvent +import datetime class TeraSessionEventTest(BaseModelsTest): @@ -6,3 +8,12 @@ class TeraSessionEventTest(BaseModelsTest): def test_defaults(self): with self._flask_app.app_context(): pass + + @staticmethod + def new_test_session_event(id_session: int, id_event_type: int) -> TeraSessionEvent: + event = TeraSessionEvent() + event.id_session = id_session + event.id_session_event_type = id_event_type + event.session_event_datetime = datetime.datetime.now() + TeraSessionEvent.insert(event) + return event diff --git a/teraserver/python/tests/opentera/db/models/test_TeraSessionType.py b/teraserver/python/tests/opentera/db/models/test_TeraSessionType.py index 8f8f2e7b..0caf7854 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraSessionType.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraSessionType.py @@ -1,6 +1,10 @@ from tests.opentera.db.models.BaseModelsTest import BaseModelsTest from opentera.db.models.TeraSessionType import TeraSessionType from opentera.db.models.TeraSession import TeraSession +from opentera.db.models.TeraService import TeraService +from opentera.db.models.TeraSessionTypeProject import TeraSessionTypeProject +from opentera.db.models.TeraSessionTypeSite import TeraSessionTypeSite +from sqlalchemy.exc import IntegrityError class TeraSessionTypeTest(BaseModelsTest): @@ -12,12 +16,7 @@ def test_defaults(self): def test_soft_delete(self): with self._flask_app.app_context(): # Create new - ses_type = TeraSessionType() - ses_type.session_type_online = False - ses_type.session_type_color = "" - ses_type.session_type_category = TeraSessionType.SessionCategoryEnum.DATACOLLECT.value - ses_type.session_type_name = 'Session Type Test' - TeraSessionType.insert(ses_type) + ses_type = TeraSessionTypeTest.new_test_session_type() id_session_type = ses_type.id_session_type # Soft delete @@ -35,20 +34,12 @@ def test_soft_delete(self): def test_hard_delete(self): with self._flask_app.app_context(): # Create new - ses_type = TeraSessionType() - ses_type.session_type_online = False - ses_type.session_type_color = "" - ses_type.session_type_category = TeraSessionType.SessionCategoryEnum.DATACOLLECT.value - ses_type.session_type_name = 'Session Type Test' - TeraSessionType.insert(ses_type) + ses_type = TeraSessionTypeTest.new_test_session_type() id_session_type = ses_type.id_session_type # Create a new session of that session type - ses = TeraSession() - ses.id_creator_service = 1 - ses.id_session_type = id_session_type - ses.session_name = "Test session" - TeraSession.insert(ses) + from test_TeraSession import TeraSessionTest + ses = TeraSessionTest.new_test_session(id_session_type=id_session_type, id_creator_service=1) id_session = ses.id_session # Soft delete to prevent relationship integrity errors as we want to test hard-delete cascade here @@ -67,3 +58,67 @@ def test_hard_delete(self): # Make sure eveything is deleted self.assertIsNone(TeraSessionType.get_session_type_by_id(id_session_type, True)) self.assertIsNone(TeraSession.get_session_by_id(id_session, True)) + + def test_undelete(self): + with self._flask_app.app_context(): + # Create new service + from test_TeraService import TeraServiceTest + service = TeraServiceTest.new_test_service('SessionTypeService') + id_service = service.id_service + + # Create new + ses_type = TeraSessionTypeTest.new_test_session_type(id_service=id_service) + id_session_type = ses_type.id_session_type + + # Create a new session of that session type + from 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 + 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 + 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 + + # Soft delete to prevent relationship integrity errors as we want to test hard-delete cascade here + TeraSession.delete(id_session) + TeraSessionType.delete(id_session_type) + self.assertIsNone(TeraSessionType.get_session_type_by_id(id_session_type)) + self.assertIsNone(TeraSession.get_session_by_id(id_session)) + self.assertIsNotNone(TeraService.get_service_by_id(id_service)) + self.assertIsNone(TeraSessionTypeSite.get_session_type_site_by_id(id_session_type_site)) + self.assertIsNone(TeraSessionTypeProject.get_session_type_project_by_id(id_session_type_project)) + + # Delete service + TeraService.delete(id_service) + + # Try to undelete SessionType - should not work + with self.assertRaises(IntegrityError): + TeraSessionType.undelete(id_session_type) + + # Should now work... + TeraService.undelete(id_service) + TeraSessionType.undelete(id_session_type) + self.assertIsNotNone(TeraSessionType.get_session_type_by_id(id_session_type)) + self.assertIsNone(TeraSession.get_session_by_id(id_session)) + self.assertIsNotNone(TeraService.get_service_by_id(id_service)) + self.assertIsNotNone(TeraSessionTypeSite.get_session_type_site_by_id(id_session_type_site)) + self.assertIsNotNone(TeraSessionTypeProject.get_session_type_project_by_id(id_session_type_project)) + + @staticmethod + def new_test_session_type(id_service: int | None = None) -> TeraSessionType: + ses_type = TeraSessionType() + if id_service: + ses_type.id_service = id_service + ses_type.session_type_online = False + ses_type.session_type_color = "" + ses_type.session_type_category = TeraSessionType.SessionCategoryEnum.DATACOLLECT.value + ses_type.session_type_name = 'Session Type Test' + TeraSessionType.insert(ses_type) + return ses_type diff --git a/teraserver/python/tests/opentera/db/models/test_TeraSessionTypeProject.py b/teraserver/python/tests/opentera/db/models/test_TeraSessionTypeProject.py index 031b400c..c7da1f96 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraSessionTypeProject.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraSessionTypeProject.py @@ -1,4 +1,5 @@ from tests.opentera.db.models.BaseModelsTest import BaseModelsTest +from opentera.db.models.TeraSessionTypeProject import TeraSessionTypeProject class TeraSessionTypeProjectTest(BaseModelsTest): @@ -6,3 +7,11 @@ class TeraSessionTypeProjectTest(BaseModelsTest): def test_defaults(self): with self._flask_app.app_context(): pass + + @staticmethod + def new_test_session_type_project(id_project: int, id_session_type: int) -> TeraSessionTypeProject: + st_project = TeraSessionTypeProject() + st_project.id_project = id_project + st_project.id_session_type = id_session_type + TeraSessionTypeProject.insert(st_project) + return st_project diff --git a/teraserver/python/tests/opentera/db/models/test_TeraSessionTypeSite.py b/teraserver/python/tests/opentera/db/models/test_TeraSessionTypeSite.py new file mode 100644 index 00000000..873fa1c9 --- /dev/null +++ b/teraserver/python/tests/opentera/db/models/test_TeraSessionTypeSite.py @@ -0,0 +1,17 @@ +from tests.opentera.db.models.BaseModelsTest import BaseModelsTest +from opentera.db.models.TeraSessionTypeSite import TeraSessionTypeSite + + +class TeraSessionTypeSiteTest(BaseModelsTest): + + def test_defaults(self): + with self._flask_app.app_context(): + pass + + @staticmethod + def new_test_session_type_site(id_site: int, id_session_type: int) -> TeraSessionTypeSite: + st_site = TeraSessionTypeSite() + st_site.id_site = id_site + st_site.id_session_type = id_session_type + TeraSessionTypeSite.insert(st_site) + return st_site diff --git a/teraserver/python/tests/opentera/db/models/test_TeraSite.py b/teraserver/python/tests/opentera/db/models/test_TeraSite.py index ebb3b9ee..91fd2763 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraSite.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraSite.py @@ -4,6 +4,12 @@ from opentera.db.models.TeraProject import TeraProject from opentera.db.models.TeraParticipant import TeraParticipant from opentera.db.models.TeraSession import TeraSession +from opentera.db.models.TeraDeviceSite import TeraDeviceSite +from opentera.db.models.TeraServiceSite import TeraServiceSite +from opentera.db.models.TeraServiceRole import TeraServiceRole +from opentera.db.models.TeraSessionTypeSite import TeraSessionTypeSite +from opentera.db.models.TeraTestTypeSite import TeraTestTypeSite +from opentera.db.models.TeraDevice import TeraDevice class TeraSiteTest(BaseModelsTest): @@ -27,7 +33,7 @@ def test_unique_args(self): def test_to_json(self): with self._flask_app.app_context(): - new_site = TeraSiteTest.new_test_site() + 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_name'], 'Site Name') @@ -112,26 +118,19 @@ def test_hard_delete(self): site = TeraSiteTest.new_test_site() id_site = site.id_site - project = TeraProject() - project.project_name = "Test project" - project.id_site = id_site - TeraProject.insert(project) + from test_TeraProject import TeraProjectTest + project = TeraProjectTest.new_test_project(id_site=id_site) self.assertIsNotNone(project.id_project) id_project = project.id_project - participant = TeraParticipant() - participant.participant_name = "Test participant" - participant.id_project = id_project - TeraParticipant.insert(participant) + from test_TeraParticipant import TeraParticipantTest + participant = TeraParticipantTest.new_test_participant(id_project=id_project) self.assertIsNotNone(participant.id_participant) id_participant = participant.id_participant - ses = TeraSession() - ses.id_creator_participant = id_participant - ses.id_session_type = 1 - ses.session_name = "Test session" - ses.session_participants = [participant] - TeraSession.insert(ses) + 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 # Soft delete to prevent relationship integrity errors as we want to test hard-delete cascade here @@ -159,10 +158,67 @@ def test_hard_delete(self): self.assertIsNone(TeraParticipant.get_participant_by_id(id_participant, True)) self.assertIsNone(TeraSession.get_session_by_id(id_session, True)) + def test_undelete(self): + with self._flask_app.app_context(): + # Create new + site = TeraSiteTest.new_test_site() + 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 + + # And now, delete! + TeraSite.delete(id_site) + self.assertIsNone(TeraSite.get_site_by_id(id_site)) + self.assertIsNone(TeraDeviceSite.get_device_site_by_id(id_device_site)) + self.assertIsNone(TeraServiceSite.get_service_site_by_id(id_service_site)) + self.assertIsNone(TeraServiceRole.get_service_role_by_id(id_role)) + self.assertIsNone(TeraSessionTypeSite.get_session_type_site_by_id(id_session_type)) + self.assertIsNone(TeraTestTypeSite.get_test_type_site_by_id(id_test_type)) + + # Undelete + TeraDevice.delete(id_device) + TeraSite.undelete(id_site) + + # Check everything again! + self.assertIsNotNone(TeraSite.get_site_by_id(id_site)) + # Should not be restored since device was deleted + self.assertIsNone(TeraDeviceSite.get_device_site_by_id(id_device_site)) + self.assertIsNotNone(TeraServiceSite.get_service_site_by_id(id_service_site)) + self.assertIsNotNone(TeraServiceRole.get_service_role_by_id(id_role)) + self.assertIsNotNone(TeraSessionTypeSite.get_session_type_site_by_id(id_session_type)) + self.assertIsNotNone(TeraTestTypeSite.get_test_type_site_by_id(id_test_type)) + @staticmethod - def new_test_site() -> TeraSite: + def new_test_site(name: str = 'Test Site') -> TeraSite: site = TeraSite() - site.site_name = "Test Site" + site.site_name = name TeraSite.insert(site) return site diff --git a/teraserver/python/tests/opentera/db/models/test_TeraTestType.py b/teraserver/python/tests/opentera/db/models/test_TeraTestType.py index 714de598..32fedb4f 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraTestType.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraTestType.py @@ -1,6 +1,8 @@ from tests.opentera.db.models.BaseModelsTest import BaseModelsTest from opentera.db.models.TeraTestType import TeraTestType from opentera.db.models.TeraTest import TeraTest +from opentera.db.models.TeraTestTypeSite import TeraTestTypeSite +from opentera.db.models.TeraTestTypeProject import TeraTestTypeProject class TeraTestTypeTest(BaseModelsTest): @@ -12,10 +14,7 @@ def test_defaults(self): def test_soft_delete(self): with self._flask_app.app_context(): # Create new - test_type = TeraTestType() - test_type.test_type_name = "Test test type..." - test_type.id_service = 1 - TeraTestType.insert(test_type) + test_type = TeraTestTypeTest.new_test_test_type() id_test_type = test_type.id_test_type # Soft delete @@ -33,18 +32,11 @@ def test_soft_delete(self): def test_hard_delete(self): with self._flask_app.app_context(): # Create new - test_type = TeraTestType() - test_type.test_type_name = "Test test type..." - test_type.id_service = 1 - TeraTestType.insert(test_type) + test_type = TeraTestTypeTest.new_test_test_type() id_test_type = test_type.id_test_type - test = TeraTest() - test.id_participant = 1 - test.id_session = 1 - test.id_test_type = id_test_type - test.test_name = "Test test!" - TeraTest.insert(test) + from 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 @@ -64,3 +56,50 @@ def test_hard_delete(self): # Make sure eveything is deleted self.assertIsNone(TeraTest.get_test_by_id(id_test, True)) self.assertIsNone(TeraTestType.get_test_type_by_id(id_test_type, True)) + + def test_undelete(self): + with self._flask_app.app_context(): + # Create new + test_type = TeraTestTypeTest.new_test_test_type() + id_test_type = test_type.id_test_type + + from 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 + 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 + 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 + + # Delete + TeraTest.delete(id_test) + TeraTestType.delete(id_test_type) + + # Check that everything was deleted + self.assertIsNone(TeraTest.get_test_by_id(id_test)) + self.assertIsNone(TeraTestType.get_test_type_by_id(id_test_type)) + self.assertIsNone(TeraTestTypeSite.get_test_type_site_by_id(id_test_type_site)) + self.assertIsNone(TeraTestTypeProject.get_test_type_project_by_id(id_test_type_project)) + + # Undelete + TeraTestType.undelete(id_test_type) + + self.assertIsNone(TeraTest.get_test_by_id(id_test)) + self.assertIsNotNone(TeraTestType.get_test_type_by_id(id_test_type)) + self.assertIsNotNone(TeraTestTypeSite.get_test_type_site_by_id(id_test_type_site)) + self.assertIsNotNone(TeraTestTypeProject.get_test_type_project_by_id(id_test_type_project)) + + @staticmethod + def new_test_test_type(id_service: int = 1) -> TeraTestType: + test_type = TeraTestType() + test_type.test_type_name = "Test test type..." + test_type.id_service = id_service + TeraTestType.insert(test_type) + return test_type diff --git a/teraserver/python/tests/opentera/db/models/test_TeraTestTypeProject.py b/teraserver/python/tests/opentera/db/models/test_TeraTestTypeProject.py new file mode 100644 index 00000000..d67a90e2 --- /dev/null +++ b/teraserver/python/tests/opentera/db/models/test_TeraTestTypeProject.py @@ -0,0 +1,17 @@ +from tests.opentera.db.models.BaseModelsTest import BaseModelsTest +from opentera.db.models.TeraTestTypeProject import TeraTestTypeProject + + +class TeraTestTypeProjectTest(BaseModelsTest): + + def test_defaults(self): + with self._flask_app.app_context(): + pass + + @staticmethod + def new_test_test_type_project(id_project: int, id_test_type: int) -> TeraTestTypeProject: + tt_project = TeraTestTypeProject() + tt_project.id_project = id_project + tt_project.id_test_type = id_test_type + TeraTestTypeProject.insert(tt_project) + return tt_project diff --git a/teraserver/python/tests/opentera/db/models/test_TeraTestTypeSite.py b/teraserver/python/tests/opentera/db/models/test_TeraTestTypeSite.py new file mode 100644 index 00000000..d531c487 --- /dev/null +++ b/teraserver/python/tests/opentera/db/models/test_TeraTestTypeSite.py @@ -0,0 +1,17 @@ +from tests.opentera.db.models.BaseModelsTest import BaseModelsTest +from opentera.db.models.TeraTestTypeSite import TeraTestTypeSite + + +class TeraTestTypeSiteTest(BaseModelsTest): + + def test_defaults(self): + with self._flask_app.app_context(): + pass + + @staticmethod + def new_test_test_type_site(id_site: int, id_test_type: int) -> TeraTestTypeSite: + tt_site = TeraTestTypeSite() + tt_site.id_site = id_site + tt_site.id_test_type = id_test_type + TeraTestTypeSite.insert(tt_site) + return tt_site diff --git a/teraserver/python/tests/opentera/db/models/test_TeraUser.py b/teraserver/python/tests/opentera/db/models/test_TeraUser.py index 6ff98ee4..c06265fb 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraUser.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraUser.py @@ -6,6 +6,7 @@ from opentera.db.models.TeraAsset import TeraAsset from opentera.db.models.TeraTest import TeraTest from opentera.db.models.TeraService import TeraService +from opentera.db.models.TeraUserUserGroup import TeraUserUserGroup from tests.opentera.db.models.BaseModelsTest import BaseModelsTest @@ -94,38 +95,23 @@ def test_hard_delete(self): id_user = user.id_user # Assign user to sessions - user_session = TeraSession() - user_session.id_creator_user = id_user - user_session.id_session_type = 1 - user_session.session_name = 'Creator user session' - TeraSession.insert(user_session) + from test_TeraSession import TeraSessionTest + user_session = TeraSessionTest.new_test_session(id_creator_user=id_user) id_session = user_session.id_session - user_session = TeraSession() - user_session.id_creator_service = 1 - user_session.id_session_type = 1 - user_session.session_name = "User invitee session" - user_session.session_users = [user] - TeraSession.insert(user_session) + user_session = TeraSessionTest.new_test_session(id_creator_service=1, users=[user]) id_session_invitee = user_session.id_session # Attach asset - asset = TeraAsset() - asset.asset_name = "User asset test" - asset.id_user = id_user - asset.id_session = id_session - asset.asset_service_uuid = TeraService.get_openteraserver_service().service_uuid - asset.asset_type = 'Test' - TeraAsset.insert(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 - test = TeraTest() - test.id_user = id_user - test.id_session = id_session - test.id_test_type = 1 - test.test_name = "User test test!" - TeraTest.insert(test) + from test_TeraTest import TeraTestTest + test = TeraTestTest.new_test_test(id_session=id_session, id_user=id_user) id_test = test.id_test # Soft delete device to prevent relationship integrity errors as we want to test hard-delete cascade here @@ -155,8 +141,57 @@ def test_hard_delete(self): self.assertIsNone(TeraAsset.get_asset_by_id(id_asset, True)) self.assertIsNone(TeraTest.get_test_by_id(id_test, True)) + def test_undelete(self): + with self._flask_app.app_context(): + # Create new user + user = TeraUserTest.new_test_user() + self.assertIsNotNone(user.id_user) + 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 + + user_session = TeraSessionTest.new_test_session(id_creator_service=1, users=[user]) + 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 + + # Soft delete device to prevent relationship integrity errors as we want to test hard-delete cascade here + TeraSession.delete(id_session) + TeraSession.delete(id_session_invitee) + TeraUser.delete(id_user) + self.assertIsNone(TeraUserUserGroup.get_user_user_group_by_id(id_user_user_group)) + + # Undelete + TeraUser.undelete(id_user) + + # Validate + self.assertIsNotNone(TeraUser.get_user_by_id(id_user)) + self.assertIsNotNone(TeraUserUserGroup.get_user_user_group_by_id(id_user_user_group)) + self.assertIsNone(TeraSession.get_session_by_id(id_session)) + self.assertIsNone(TeraSession.get_session_by_id(id_session_invitee)) + self.assertIsNone(TeraAsset.get_asset_by_id(id_asset)) + self.assertIsNone(TeraTest.get_test_by_id(id_test)) + @staticmethod - def new_test_user() -> TeraUser: + def new_test_user(user_groups: list | None = None) -> TeraUser: user = TeraUser() user.user_enabled = True user.user_firstname = "Test" @@ -165,5 +200,7 @@ def new_test_user() -> TeraUser: user.user_password = TeraUser.encrypt_password("test") user.user_superadmin = False user.user_username = "test" + if user_groups: + user.user_user_groups = user_groups TeraUser.insert(user) return user diff --git a/teraserver/python/tests/opentera/db/models/test_TeraUserGroup.py b/teraserver/python/tests/opentera/db/models/test_TeraUserGroup.py index d49687b4..3fd0ac85 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraUserGroup.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraUserGroup.py @@ -230,9 +230,7 @@ def test_update_with_invalid_fields(self): def test_soft_delete(self): with self._flask_app.app_context(): # Create new - ug = TeraUserGroup() - ug.user_group_name = "Test User Group" - TeraUserGroup.insert(ug) + ug = TeraUserGroupTest.new_test_usergroup() self.assertIsNotNone(ug.id_user_group) id_user_group = ug.id_user_group @@ -250,22 +248,12 @@ def test_soft_delete(self): def test_hard_delete(self): with self._flask_app.app_context(): # Create new - ug = TeraUserGroup() - ug.user_group_name = "Test User Group" - TeraUserGroup.insert(ug) + ug = TeraUserGroupTest.new_test_usergroup() self.assertIsNotNone(ug.id_user_group) id_user_group = ug.id_user_group - user = TeraUser() - user.user_enabled = True - user.user_firstname = "Test" - user.user_lastname = "User" - user.user_profile = "" - user.user_password = TeraUser.encrypt_password("test") - user.user_superadmin = False - user.user_username = "test" - user.user_user_groups = [ug] - TeraUser.insert(user) + from test_TeraUser import TeraUserTest + user = TeraUserTest.new_test_user(user_groups=[ug]) self.assertIsNotNone(user.id_user) id_user = user.id_user @@ -290,4 +278,12 @@ def test_hard_delete(self): self.assertIsNone(TeraUserGroup.get_user_group_by_id(id_user_group, True)) self.assertIsNone(TeraUserUserGroup.get_user_user_group_by_id(id_user_user_group, True)) + def test_undelete(self): + pass + @staticmethod + def new_test_usergroup() -> TeraUserGroup: + ug = TeraUserGroup() + ug.user_group_name = "Test User Group" + TeraUserGroup.insert(ug) + return ug diff --git a/teraserver/python/tests/opentera/db/models/test_TeraUserUserGroup.py b/teraserver/python/tests/opentera/db/models/test_TeraUserUserGroup.py index 9c3108bf..c7259967 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraUserUserGroup.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraUserUserGroup.py @@ -143,3 +143,11 @@ def test_soft_delete(self): id_user_user_group = uug.id_user_user_group TeraUserUserGroup.delete(id_user_user_group) self.assertIsNotNone(TeraUserUserGroup.get_user_user_group_by_id(id_user_user_group, with_deleted=True)) + + @staticmethod + def new_test_user_usergroup(id_user: int, id_user_group: int) -> TeraUserUserGroup: + uug = TeraUserUserGroup() + uug.id_user = id_user + uug.id_user_group = id_user_group + TeraUserUserGroup.insert(uug) + return uug From 0250b68ac53ee3b901f675fb88da646e3ff9282e Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Fri, 28 Jul 2023 08:56:28 -0400 Subject: [PATCH 26/80] Refs #202. Completed undelete feature and tests for database models. --- .../python/opentera/db/models/TeraUser.py | 3 ++ .../opentera/db/models/test_TeraAsset.py | 4 +-- .../opentera/db/models/test_TeraService.py | 2 +- .../db/models/test_TeraServiceAccess.py | 16 +++++++++ .../tests/opentera/db/models/test_TeraTest.py | 10 +++--- .../tests/opentera/db/models/test_TeraUser.py | 17 ++++++--- .../opentera/db/models/test_TeraUserGroup.py | 36 +++++++++++++++++-- 7 files changed, 73 insertions(+), 15 deletions(-) diff --git a/teraserver/python/opentera/db/models/TeraUser.py b/teraserver/python/opentera/db/models/TeraUser.py index 8c50555a..c7aae924 100755 --- a/teraserver/python/opentera/db/models/TeraUser.py +++ b/teraserver/python/opentera/db/models/TeraUser.py @@ -427,3 +427,6 @@ def create_defaults(test=False): TeraUser.db().session.add(user) TeraUser.db().session.commit() + + def get_undelete_cascade_relations(self) -> list: + return ['user_service_config'] diff --git a/teraserver/python/tests/opentera/db/models/test_TeraAsset.py b/teraserver/python/tests/opentera/db/models/test_TeraAsset.py index fbbbbb69..fd15e8ff 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraAsset.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraAsset.py @@ -306,7 +306,7 @@ def test_undelete(self): # Create new user from test_TeraUser import TeraUserTest - user = TeraUserTest.new_test_user() + user = TeraUserTest.new_test_user(user_name="asset_user") id_user = user.id_user # Create new session @@ -344,7 +344,7 @@ def test_undelete(self): TeraUser.delete(id_user) # Exception should be thrown when trying to undelete - with self.assertRaises(IntegrityError) as cm: + with self.assertRaises(IntegrityError): TeraAsset.undelete(id_asset) # Restore participant diff --git a/teraserver/python/tests/opentera/db/models/test_TeraService.py b/teraserver/python/tests/opentera/db/models/test_TeraService.py index 7e91c906..f47d4e6d 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraService.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraService.py @@ -425,7 +425,7 @@ def test_hard_delete(self): def test_undelete(self): with self._flask_app.app_context(): # Create new - service = TeraServiceTest.new_test_service('TestService') + service = TeraServiceTest.new_test_service('TestServiceUndelete') self.assertIsNotNone(service.id_service) id_service = service.id_service diff --git a/teraserver/python/tests/opentera/db/models/test_TeraServiceAccess.py b/teraserver/python/tests/opentera/db/models/test_TeraServiceAccess.py index cfc27761..baefe0a9 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraServiceAccess.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraServiceAccess.py @@ -1,4 +1,5 @@ from tests.opentera.db.models.BaseModelsTest import BaseModelsTest +from opentera.db.models.TeraServiceAccess import TeraServiceAccess class TeraServiceAccessTest(BaseModelsTest): @@ -6,3 +7,18 @@ class TeraServiceAccessTest(BaseModelsTest): def test_defaults(self): with self._flask_app.app_context(): pass + + @staticmethod + def new_test_service_access(id_service_role: int, id_user_group: int | None = None, + id_participant_group: int | None = None, + id_device: int | None = None) -> TeraServiceAccess: + access = TeraServiceAccess() + access.id_service_role = id_service_role + if id_user_group: + access.id_user_group = id_user_group + if id_participant_group: + access.id_participant_group = id_participant_group + if id_device: + access.id_device = id_device + TeraServiceAccess.insert(access) + return access diff --git a/teraserver/python/tests/opentera/db/models/test_TeraTest.py b/teraserver/python/tests/opentera/db/models/test_TeraTest.py index ae9143f0..97c3ce65 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraTest.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraTest.py @@ -57,7 +57,7 @@ def test_undelete(self): # Create new user from test_TeraUser import TeraUserTest - user = TeraUserTest.new_test_user() + user = TeraUserTest.new_test_user(user_name='test_testuser') id_user = user.id_user # Create new session @@ -88,7 +88,7 @@ def test_undelete(self): TeraUser.delete(id_user) # Exception should be thrown when trying to undelete - with self.assertRaises(IntegrityError) as cm: + with self.assertRaises(IntegrityError): TeraTest.undelete(id_test) # Restore participant @@ -97,7 +97,7 @@ def test_undelete(self): self.assertIsNotNone(participant) # Restore test - still has dependencies issues... - with self.assertRaises(IntegrityError) as cm: + with self.assertRaises(IntegrityError): TeraTest.undelete(id_test) # Restore user @@ -106,7 +106,7 @@ def test_undelete(self): self.assertIsNotNone(user) # Restore test - still has dependencies issues... - with self.assertRaises(IntegrityError) as cm: + with self.assertRaises(IntegrityError): TeraTest.undelete(id_test) # Restore device @@ -115,7 +115,7 @@ def test_undelete(self): self.assertIsNotNone(device) # Restore test - still has dependencies issues... - with self.assertRaises(IntegrityError) as cm: + with self.assertRaises(IntegrityError): TeraTest.undelete(id_test) # Restore session diff --git a/teraserver/python/tests/opentera/db/models/test_TeraUser.py b/teraserver/python/tests/opentera/db/models/test_TeraUser.py index c06265fb..c7e30d39 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraUser.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraUser.py @@ -6,6 +6,7 @@ from opentera.db.models.TeraAsset import TeraAsset from opentera.db.models.TeraTest import TeraTest 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 @@ -73,7 +74,7 @@ def test_multisite_user(self): def test_soft_delete(self): with self._flask_app.app_context(): # Create new - user = TeraUserTest.new_test_user() + user = TeraUserTest.new_test_user(user_name="user_soft_delete") self.assertIsNotNone(user.id_user) id_user = user.id_user @@ -90,7 +91,7 @@ def test_soft_delete(self): def test_hard_delete(self): with self._flask_app.app_context(): # Create new user - user = TeraUserTest.new_test_user() + user = TeraUserTest.new_test_user(user_name="user_hard_delete") self.assertIsNotNone(user.id_user) id_user = user.id_user @@ -144,7 +145,7 @@ def test_hard_delete(self): def test_undelete(self): with self._flask_app.app_context(): # Create new user - user = TeraUserTest.new_test_user() + user = TeraUserTest.new_test_user(user_name="user_undelete") self.assertIsNotNone(user.id_user) id_user = user.id_user @@ -173,6 +174,11 @@ def test_undelete(self): 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 + # Soft delete device to prevent relationship integrity errors as we want to test hard-delete cascade here TeraSession.delete(id_session) TeraSession.delete(id_session_invitee) @@ -189,9 +195,10 @@ def test_undelete(self): self.assertIsNone(TeraSession.get_session_by_id(id_session_invitee)) self.assertIsNone(TeraAsset.get_asset_by_id(id_asset)) self.assertIsNone(TeraTest.get_test_by_id(id_test)) + self.assertIsNotNone(TeraServiceConfig.get_service_config_by_id(id_service_conf)) @staticmethod - def new_test_user(user_groups: list | None = None) -> TeraUser: + def new_test_user(user_name: str, user_groups: list | None = None) -> TeraUser: user = TeraUser() user.user_enabled = True user.user_firstname = "Test" @@ -199,7 +206,7 @@ def new_test_user(user_groups: list | None = None) -> TeraUser: user.user_profile = "" user.user_password = TeraUser.encrypt_password("test") user.user_superadmin = False - user.user_username = "test" + user.user_username = user_name if user_groups: user.user_user_groups = user_groups TeraUser.insert(user) diff --git a/teraserver/python/tests/opentera/db/models/test_TeraUserGroup.py b/teraserver/python/tests/opentera/db/models/test_TeraUserGroup.py index 3fd0ac85..761a2691 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraUserGroup.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraUserGroup.py @@ -3,6 +3,7 @@ from opentera.db.models.TeraUserUserGroup import TeraUserUserGroup from opentera.db.models.TeraProject import TeraProject from opentera.db.models.TeraServiceRole import TeraServiceRole +from opentera.db.models.TeraServiceAccess import TeraServiceAccess from opentera.db.models.TeraUser import TeraUser from opentera.db.models.TeraSite import TeraSite from sqlalchemy.exc import SQLAlchemyError @@ -253,7 +254,7 @@ def test_hard_delete(self): id_user_group = ug.id_user_group from test_TeraUser import TeraUserTest - user = TeraUserTest.new_test_user(user_groups=[ug]) + user = TeraUserTest.new_test_user(user_name="user_ug_harddelete", user_groups=[ug]) self.assertIsNotNone(user.id_user) id_user = user.id_user @@ -279,7 +280,38 @@ def test_hard_delete(self): self.assertIsNone(TeraUserUserGroup.get_user_user_group_by_id(id_user_user_group, True)) def test_undelete(self): - pass + with self._flask_app.app_context(): + # Create new + ug = TeraUserGroupTest.new_test_usergroup() + self.assertIsNotNone(ug.id_user_group) + id_user_group = ug.id_user_group + + from 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 + access = TeraServiceAccessTest.new_test_service_access(id_service_role=1, id_user_group=id_user_group) + id_access = access.id_service_access + + # Delete + id_user_user_group = TeraUserUserGroup.query_user_user_group_for_user_user_group(id_user, id_user_group) \ + .id_user_user_group + TeraUserUserGroup.delete(id_user_user_group) + TeraUserGroup.delete(id_user_group) + + self.assertIsNone(TeraUserGroup.get_user_group_by_id(id_user_group)) + self.assertIsNone(TeraServiceAccess.get_service_access_by_id(id_access)) + self.assertIsNone(TeraUserUserGroup.get_user_user_group_by_id(id_user_user_group)) + + # Undelete + TeraUserGroup.undelete(id_user_group) + + self.assertIsNotNone(TeraUserGroup.get_user_group_by_id(id_user_group)) + self.assertIsNotNone(TeraUserUserGroup.get_user_user_group_by_id(id_user_user_group)) + self.assertIsNotNone(TeraServiceAccess.get_service_access_by_id(id_access)) @staticmethod def new_test_usergroup() -> TeraUserGroup: From d60d787c95fbf46c8e73bf2b285deb142aff66a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominic=20Le=CC=81tourneau?= Date: Tue, 29 Aug 2023 09:34:26 -0400 Subject: [PATCH 27/80] Refs #225, review paper and suggestions. Will run GitHub action to generate paper as artifact on branch joss-paper-review. --- .github/workflows/draft-joss-paper-pdf.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/draft-joss-paper-pdf.yml b/.github/workflows/draft-joss-paper-pdf.yml index 67c0d002..29ca38a9 100644 --- a/.github/workflows/draft-joss-paper-pdf.yml +++ b/.github/workflows/draft-joss-paper-pdf.yml @@ -1,6 +1,10 @@ name: JOSS Paper Draft PDF -on: [push] +on: + push: + branches: [main, joss-paper-review] + workflow_dispatch: + branches: [main, joss-paper-review] jobs: paper: @@ -23,4 +27,3 @@ jobs: # PDF. Note, this should be the same directory as the input # paper.md path: joss-paper/paper.pdf - From 1fc7cc7b54228c24eaa4b8a5686544da5da150b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominic=20Le=CC=81tourneau?= Date: Tue, 29 Aug 2023 10:56:25 -0400 Subject: [PATCH 28/80] Refs #225, Incorporated @galessiorob suggestions. --- joss-paper/paper.md | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/joss-paper/paper.md b/joss-paper/paper.md index 1a3efd10..7729def1 100644 --- a/joss-paper/paper.md +++ b/joss-paper/paper.md @@ -67,34 +67,37 @@ bibliography: paper.bib # Summary -OpenTera is a microservice based framework primarily developed to support telehealth research projects and real-world deployment. This project is based on 20 years of experience linking at-home participants to remote users (such as clinicians, researchers, healthcare professionals) with audio-video-data connections and in-the-field sensors, such as biometrics, wearable and robotics devices. Applications of the OpenTera framework are not limited to research projects and can also be used in clinical environments. -Most telehealth-based research projects require a common data structure: data collection sites, projects, participants and sessions including various recorded data types from sensors or other sources. They also require many common features: user authentication based on various access roles, ability to add new features based on specific project needs, ease of use for the participant, and secure data hosting. These features are also shared between research projects: videoconferencing with specific health related features (e.g., angles measurement, timers), surveys data collection, data analysis and exportation. +OpenTera is a microservice-based framework primarily developed to support telehealth research projects and real-world deployment. This project is based on 20 years of experience linking at-home participants to remote users (such as clinicians, researchers, healthcare and professionals) with audio-video-data connections and in-the-field sensors, such as biometrics, wearable and robotics devices. Applications of the OpenTera framework are not limited to research projects and can also be used in clinical environments. -Many of the available solutions are either costly, feature limited, proprietary (e.g., can hardly be adapted for research purposes and raw data is harder to access) or hard to deploy in a telehealth context. OpenTera was built for extensibility to provide research projects full control over their data and hosting. +Most telehealth-based research projects require a common data structure: data collection sites, projects, participants and sessions, including various recorded data types from sensors or other sources. They also require standard features: user authentication based on various access roles, the ability to add new features based on specific project needs, ease of use for the participant, and secure data hosting. These features are also shared between research projects: videoconferencing with specific health-related features (e.g., angles measurement, timers), surveys data collection, data analysis and exportation. + +Many available solutions are costly, feature limited, proprietary (e.g., can hardly be adapted for research purposes and raw data is harder to access) or hard to deploy in a telehealth context. OpenTera was built for extensibility to provide research projects complete control over their data and hosting. # Statement of need From our research experience, common features between the different telehealth projects emerged: * **Data structure.** Store data in a structured way to ease data extraction and analysis. -* **Ecological data capture.** Collect data not only in laboratories or controlled environments, but also in homes or institutions. -* **Project adaptability.** Develop project-specific dashboards and user interfaces while reusing what was previously implemented as much as possible of to reduce development time. Rehabilitation projects may require implementing serious games or exergames, while teleoperation projects may require real-time navigation tools. Adapting already existing open-source software when possible is often the key. -* **Cost effectiveness.** Most of the recent commercial cloud telehealth applications available are subscription-based and do not offer the flexibility needed. Each vendor offers its own approach tailored for its products and services. We often have data collections from dozens of participants and users, and paying subscription fees would be prohibitive. +* **Ecological data capture.** Collect data not only in laboratories or controlled environments but also in homes or institutions. +* **Project adaptability.** Develop project-specific dashboards and user interfaces while reusing what was previously implemented as much as possible to reduce development time. Rehabilitation projects may require implementing serious games or exergames, while teleoperation projects may require real-time navigation tools. Adapting already existing open-source software when possible is often the key. +* **Cost-effectiveness.** Most of the recent commercial cloud telehealth applications available are subscription-based and do not offer the flexibility needed. Each vendor offers its own approach tailored to its products and services. We often have data collection from dozens of participants and users, and paying subscription fees would be prohibitive. * **Security.** Store and transfer data in a secure and controlled way. Access control to information depends on specific project requirements. Research projects involving participants must be approved by the ethics committee, and they often require servers hosted locally or in a specific region. -* **Uniformity.** Avoid the use of multiple applications and tools that would require the user to navigate between them (minimizing and restoring them as needed) and focusing attention on the current task. -* Ease of use. Implement an easy-to-use solution for users and participants at all steps of the process, I.e., authentication, data collection, data management. +* **Uniformity.** Avoid the use of multiple applications and tools that would require the user to navigate between them (minimizing and restoring them as needed) and focus attention on the current task. +* **Ease of use.** Implement an easy-to-use solution for users and participants at all steps of the process, I.e., authentication, data collection and data management. * **Synchronous and asynchronous sessions.** Support real-time sessions (synchronous) or on-demand pre-recorded or application-based sessions (asynchronous) with multiple users, devices and participants. * **No installation.** Connecting through a web browser with a personalized link is favored, avoiding complicated installation of apps and login / password / registration steps which is not an easy task for everyone, depending on their technological literacy. In the context of healthcare establishments, support of deployed apps often requires long-term planning and discussions with the Information Technology team, as opposed to web-based applications. * **Long term availability.** Research projects can be conducted over a long period of time, and software versions, data structures, APIs, and used features must be stable over that period. There is no guarantee with a commercial system that used features will be supported for the required duration. * **Server deployment and management.** Installation on low-cost hardware (e.g., Raspberry Pis), local servers and cloud infrastructure can be required, depending on the scale of the projects and its location. Deployments should be manageable by a small team. -Most of the open-source projects currently available concentrated their efforts on providing videoconferencing alternative to proprietary solutions (i.e., Skype, Google Meet, MS Teams, Zoom, etc.) with chat and file transfer capabilities. Alternative open-source projects include Big Blue Button, NextCloud Talk, Jami, OpenVidu, Jitsi Meet, and Kurento. Although excellent solutions for videoconferencing, they are not especially fit for research and do not meet all requirements for telehealth applications. They would also require customization at some level that can quickly become limitative or complicated. +# Existing Open-Source Solutions + +Open-source projects like Big Blue Button, NextCloud Talk, Jami, OpenVidu, Jitsi Meet, and Kurento offer excellent videoconferencing solutions, however, they don't fully meet telehealth requirements. While there are open-source rehabilitation-oriented applications available, such as the OpenRehab [@freitas_openrehab_2017] project which offers multiple rehabilitation tools for upper limbs, mobility, fitness, cognition, and balance, they often focus on specific domains and primarily contain pre-recorded videos or games prescribed by physiotherapists. Most of these applications lack teleconsultation features and remote access to research data. Open-source Electronic Health/Medical Records (EHR, EMR) [@neha_intelehealth_2017] can meet some research requirements, but storing personal and sensitive information on participants is not ideal. We prefer to use or connect to existing systems that comply with local regulations like the Health Insurance Portability and Accountability Act (HIPAA). + +# OpenTera Features -Open-source rehabilitation-oriented applications are also available. The OpenRehab [@freitas_openrehab_2017] project lists multiple rehabilitation tools for upper limb, mobility, fitness, cognition, balance. Such applications are often dedicated to a specific domain and mostly contain pre-recorded videos or games that are prescribed by physiotherapists. Most of them do not offer teleconsultation features and remote access to research data. -Finally, open-source Electronic Health/Medical Records (EHR, EMR) [@neha_intelehealth_2017] are available and can meet some research requirements, but we want to avoid storing personal and sensitive information on participants. We prefer to use or connect to existing systems that comply with local regulations like Health Insurance Portability and Accountability Act (HIPPAA). OpenTera is specifically designed to address the previously mentioned and required features for research. It is built using a microservice architecture based on recognized standards and best practices. This architecture provides scalability, flexibility, resilience, maintainability and technology diversity, all needed in a research context. -OpenTera contains the base server (TeraServer) offering a REST API [@fielding_rest_2002], useful to manage users, participants, devices, sites, projects, sessions, and supports multiple authentication methods via user/password, certificates or tokens. TeraServer also manages authorizations for users, participants and devices, providing a fine-grained access control on resources and assets. +OpenTera contains the base server (TeraServer) offering a REST API [@fielding_rest_2002], useful to manage users, participants, devices, sites, projects, sessions, and supports multiple authentication methods via user/password, certificates or tokens. TeraServer also manages authorizations for users, participants and devices, providing fine-grained access control on resources and assets. OpenTera also includes base services: Video Rehabilitation, Logging and File Transfer. They are used to conduct audio/video WebRTC sessions from the web along with appropriate logging and file transfer capabilities. Structured sessions enable organized information such as survey data, sensor data, metadata and analytics, and facilitate the retrieval of information and key statistics. Development of new microservices allows developers to add new features to the system such as serious or exergames, exercises coach / videos and participant calendar / portal. From 748be2df7a25639a4577ca73f643147980038e23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominic=20Le=CC=81tourneau?= Date: Tue, 29 Aug 2023 11:49:30 -0400 Subject: [PATCH 29/80] Refs #225, Incorporated @galessiorob suggestions. --- joss-paper/paper.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/joss-paper/paper.md b/joss-paper/paper.md index 7729def1..5a3ef48f 100644 --- a/joss-paper/paper.md +++ b/joss-paper/paper.md @@ -101,7 +101,7 @@ OpenTera contains the base server (TeraServer) offering a REST API [@fielding_re OpenTera also includes base services: Video Rehabilitation, Logging and File Transfer. They are used to conduct audio/video WebRTC sessions from the web along with appropriate logging and file transfer capabilities. Structured sessions enable organized information such as survey data, sensor data, metadata and analytics, and facilitate the retrieval of information and key statistics. Development of new microservices allows developers to add new features to the system such as serious or exergames, exercises coach / videos and participant calendar / portal. -# Related projects +# Related Projects \autoref{tab:opentera-related-projects} shows OpenTera-related open-source projects that are currently under active development, implementing new OpenTera services or underlying libraries. OpenTera has been deployed for robot teleoperation during COVID [@panchea_opentera_2022] and is currently used for multiple rehabilitation projects. From b943e7f2e592b09ffa4496ba9eefcf2d0b336bd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominic=20Le=CC=81tourneau?= Date: Tue, 29 Aug 2023 13:11:58 -0400 Subject: [PATCH 30/80] Refs #225, Incorporated @Rocsg clarification for earlier paper on OpenTera during COVID. --- joss-paper/paper.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/joss-paper/paper.md b/joss-paper/paper.md index 5a3ef48f..a6582d0f 100644 --- a/joss-paper/paper.md +++ b/joss-paper/paper.md @@ -103,7 +103,8 @@ OpenTera also includes base services: Video Rehabilitation, Logging and File Tra # Related Projects -\autoref{tab:opentera-related-projects} shows OpenTera-related open-source projects that are currently under active development, implementing new OpenTera services or underlying libraries. OpenTera has been deployed for robot teleoperation during COVID [@panchea_opentera_2022] and is currently used for multiple rehabilitation projects. +\autoref{tab:opentera-related-projects} shows OpenTera-related open-source projects that are currently under active development, implementing new OpenTera services or underlying libraries. Preliminary implementation have been deployed for robot teleoperation during COVID[@panchea_opentera_2022]. The current paper presents the implementation and design choices for a more generic OpenTera framework with a focus on open source implementation. Code quality, documentation, examples and usability have been greatly improved between these versions. + Table: OpenTera Related Projects \label{tab:opentera-related-projects} From 35ab166d5d7a2e80cbbc0a2f467e1a477174a254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominic=20Le=CC=81tourneau?= Date: Tue, 29 Aug 2023 13:12:42 -0400 Subject: [PATCH 31/80] Refs #225, Incorporated @Rocsg clarification for earlier paper on OpenTera during COVID. --- joss-paper/paper.md | 1 - 1 file changed, 1 deletion(-) diff --git a/joss-paper/paper.md b/joss-paper/paper.md index a6582d0f..5ee68131 100644 --- a/joss-paper/paper.md +++ b/joss-paper/paper.md @@ -105,7 +105,6 @@ OpenTera also includes base services: Video Rehabilitation, Logging and File Tra \autoref{tab:opentera-related-projects} shows OpenTera-related open-source projects that are currently under active development, implementing new OpenTera services or underlying libraries. Preliminary implementation have been deployed for robot teleoperation during COVID[@panchea_opentera_2022]. The current paper presents the implementation and design choices for a more generic OpenTera framework with a focus on open source implementation. Code quality, documentation, examples and usability have been greatly improved between these versions. - Table: OpenTera Related Projects \label{tab:opentera-related-projects} | GitHub Project Name | Description | From cc21c16e1f91e2e58dd98264b0dd8acf460d2b5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominic=20Le=CC=81tourneau?= Date: Wed, 13 Sep 2023 09:04:16 -0400 Subject: [PATCH 32/80] Updated requirements to latest version --- teraserver/python/env/requirements.txt | 28 +++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/teraserver/python/env/requirements.txt b/teraserver/python/env/requirements.txt index 37b7a096..d15e7398 100644 --- a/teraserver/python/env/requirements.txt +++ b/teraserver/python/env/requirements.txt @@ -1,41 +1,41 @@ pypiwin32==223; sys_platform == 'win32' -Twisted==22.10.0 +Twisted==23.8.0 treq==22.2.0 -cryptography==41.0.2 +cryptography==41.0.3 autobahn==23.6.2 -SQLAlchemy==2.0.18 +SQLAlchemy==2.0.20 sqlalchemy-schemadisplay==1.3 pydot==1.4.2 -psycopg2-binary==2.9.6 -Flask==2.3.2 -Flask-SQLAlchemy==3.0.5 +psycopg2-binary==2.9.7 +Flask==2.3.3 +Flask-SQLAlchemy==3.1.1 Flask-Login==0.6.2 Flask-Login-Multi==0.1.2 Flask-HTTPAuth==4.8.0 -Flask-SocketIO==5.3.4 +Flask-SocketIO==5.3.6 Flask-Session==0.5.0 flask-restx==1.1.0 Flask-Security==3.0.0 Flask-Babel==3.1.0 Flask-BabelEx==0.9.4 -Flask-Migrate==4.0.4 +Flask-Migrate==4.0.5 flask-swagger-ui==4.11.1 -Flask-Limiter==3.3.1 +Flask-Limiter==3.5.0 Flask-Mail==0.9.1 Flask-Principal==0.4.0 -redis==4.6.0 +redis==5.0.0 txredisapi==1.4.10 passlib==1.7.4 bcrypt==4.0.1 WTForms==3.0.1 pyOpenSSL==23.2.0 service-identity==23.1.0 -PyJWT==2.7.0 +PyJWT==2.8.0 pylzma==0.5.0 bz2file==0.98 python-slugify==8.0.1 -websocket-client==1.6.1 -pytest==7.4.0 -jsonschema==4.18.0 +websocket-client==1.6.3 +pytest==7.4.2 +jsonschema==4.19.0 Jinja2==3.1.2 ua-parser==0.18.0 From 403f6134226d32869e6aa04e33faa89bb8bdef2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominic=20Le=CC=81tourneau?= Date: Wed, 13 Sep 2023 11:21:56 -0400 Subject: [PATCH 33/80] Refs #227, #228 Implementing any or all user roles, simplified token structure, to be fully tested --- .../python/opentera/db/models/TeraUser.py | 8 +- .../opentera/services/ServiceAccessManager.py | 45 ++++++++- .../opentera/services/TeraUserClient.py | 6 +- .../tests/opentera/db/models/test_TeraUser.py | 95 +++++++++++++++++++ 4 files changed, 146 insertions(+), 8 deletions(-) diff --git a/teraserver/python/opentera/db/models/TeraUser.py b/teraserver/python/opentera/db/models/TeraUser.py index c7aae924..ab60555c 100755 --- a/teraserver/python/opentera/db/models/TeraUser.py +++ b/teraserver/python/opentera/db/models/TeraUser.py @@ -122,7 +122,7 @@ def get_token(self, token_key: str, expiration: int = 3600): return jwt.encode(payload, token_key, algorithm='HS256') def get_service_access_dict(self): - service_access = {'service_access': {}} + service_access = {} # Service access are defined in user groups, not needed for superadmin if not self.user_superadmin: @@ -135,10 +135,10 @@ def get_service_access_dict(self): if service_role.id_site is None and service_role.id_project is None: # Global access # Create entry if not exists - if service_key not in service_access['service_access']: - service_access['service_access'][service_key] = [] + if service_key not in service_access: + service_access[service_key] = [] # Add role to service - service_access['service_access'][service_key].append(role_name) + service_access[service_key].append(role_name) return service_access diff --git a/teraserver/python/opentera/services/ServiceAccessManager.py b/teraserver/python/opentera/services/ServiceAccessManager.py index b3906384..39e8cc2c 100644 --- a/teraserver/python/opentera/services/ServiceAccessManager.py +++ b/teraserver/python/opentera/services/ServiceAccessManager.py @@ -396,7 +396,7 @@ def validate_service_token(token: str) -> bool: return False @staticmethod - def service_user_roles_required(roles: List[str]): + def service_user_roles_all_required(roles: List[str]): def wrap(f): @wraps(f) def decorated(*args, **kwargs): @@ -427,3 +427,46 @@ def decorated(*args, **kwargs): return f(*args, **kwargs) return decorated return wrap + + +@staticmethod +def service_user_roles_any_required(roles: List[str]): + def wrap(f): + @wraps(f) + def decorated(*args, **kwargs): + + # Check if service is initialized + if ServiceAccessManager.service is None or 'service_key' \ + not in ServiceAccessManager.service.service_info: + return gettext('Forbidden'), 403 + + service_key = ServiceAccessManager.service.service_info['service_key'] + + # Check if user is logged in, watch out not None object but LocalProxy cannot use is None... + if not current_user_client: + return gettext('Forbidden'), 403 + + # Super admin pass through + if current_user_client.user_superadmin: + return f(*args, **kwargs) + + # Check if user has the required role (global roles are stored in token) + user_roles_from_token = current_user_client.get_roles_for_service(service_key) + + # Check if user has the required roles + if not any(role in user_roles_from_token for role in roles): + return gettext('Forbidden'), 403 + + # Everything ok, continue + return f(*args, **kwargs) + + return decorated + + return wrap + + +@staticmethod +def service_user_roles_required(roles: List[str]): + # For compatibility with old code + return ServiceAccessManager.service_user_roles_all_required(roles) + diff --git a/teraserver/python/opentera/services/TeraUserClient.py b/teraserver/python/opentera/services/TeraUserClient.py index e32b9d75..71c1c88b 100644 --- a/teraserver/python/opentera/services/TeraUserClient.py +++ b/teraserver/python/opentera/services/TeraUserClient.py @@ -77,9 +77,9 @@ def do_get_request_to_backend(self, path: str) -> Response: def get_roles_for_service(self, service_key: str) -> List[str]: # Roles are stored in the token, in the service_access dictionary roles: List[str] = [] - if 'service_access' in self.__service_access: - if service_key in self.__service_access['service_access']: - roles = self.__service_access['service_access'][service_key] + + if service_key in self.__service_access: + roles = self.__service_access[service_key] return roles def get_role_for_site(self, id_site: int) -> str: diff --git a/teraserver/python/tests/opentera/db/models/test_TeraUser.py b/teraserver/python/tests/opentera/db/models/test_TeraUser.py index c7e30d39..95a6383e 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraUser.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraUser.py @@ -9,6 +9,8 @@ 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 class TeraUserTest(BaseModelsTest): @@ -197,6 +199,99 @@ def test_undelete(self): self.assertIsNone(TeraTest.get_test_by_id(id_test)) self.assertIsNotNone(TeraServiceConfig.get_service_config_by_id(id_service_conf)) + def test_token_for_admin_should_have_empty_service_access(self): + with self._flask_app.app_context(): + user = TeraUser() + user.user_username = 'Test' + user.user_uuid = str(uuid.uuid4()) + user.user_email = 'test@test.com' + user.user_firstname = 'Test' + user.user_lastname = 'Test' + user.user_password = TeraUser.encrypt_password('test') + user.user_enabled = True + user.user_profile = '' + user.user_notes = '' + user.user_superadmin = True + + token_key = 'test' + + # Generate token + token = user.get_token(token_key) + self.assertIsNotNone(token) + + # Verify token + token_dict = jwt.decode(token, token_key, algorithms='HS256') + self.assertIsNotNone(token_dict) + + self.assertTrue('service_access' in token_dict) + 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) + + # Create a user group and add user and role to it + user_group = TeraUserGroup() + user_group.user_group_name = 'test' + TeraUserGroup.insert(user_group) + + # Add user to the group + user_user_group = TeraUserUserGroup() + user_user_group.id_user = user.id_user + user_user_group.id_user_group = user_group.id_user_group + TeraUserUserGroup.insert(user_user_group) + + # Create a role for service + service = TeraService.get_service_by_key('FileTransferService') + role = TeraServiceRole() + role.id_service = service.id_service + role.service_role_name = 'test' + TeraServiceRole.insert(role) + + # Create service access + service_access = TeraServiceAccess() + service_access.id_service_role = role.id_service_role + service_access.id_user_group = user_group.id_user_group + TeraServiceAccess.insert(service_access) + + # Add user to user group + token_key = 'test' + + # Generate token + token = user.get_token(token_key) + self.assertIsNotNone(token) + + # Verify token + token_dict = jwt.decode(token, token_key, algorithms='HS256') + self.assertIsNotNone(token_dict) + + # Verify service access + self.assertTrue('service_access' in token_dict) + self.assertEqual(token_dict['service_access'], {service.service_key: [role.service_role_name]}) + + # TODO delete everything ? + TeraUserUserGroup.delete(user_user_group.id_user_user_group) + TeraUserGroup.delete(user_group.id_user_group) + # TeraServiceAccess.delete(service_access.id_service_access) + # TeraServiceRole.delete(role.id_service_role) + + + + + + + + + + + @staticmethod def new_test_user(user_name: str, user_groups: list | None = None) -> TeraUser: user = TeraUser() From 9b0de760db6d3fffc6a08bea5933e34d801df481 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominic=20Le=CC=81tourneau?= Date: Wed, 13 Sep 2023 12:43:21 -0400 Subject: [PATCH 34/80] Refs #227, #228 Fixing wrong indent. --- .../opentera/services/ServiceAccessManager.py | 60 +++++++++---------- .../tests/opentera/services/FakeService.py | 0 .../services/test_ServiceAccessManager.py | 0 .../opentera/services/test_TeraUserClient.py | 0 4 files changed, 29 insertions(+), 31 deletions(-) create mode 100644 teraserver/python/tests/opentera/services/FakeService.py create mode 100644 teraserver/python/tests/opentera/services/test_ServiceAccessManager.py create mode 100644 teraserver/python/tests/opentera/services/test_TeraUserClient.py diff --git a/teraserver/python/opentera/services/ServiceAccessManager.py b/teraserver/python/opentera/services/ServiceAccessManager.py index 39e8cc2c..90fd3deb 100644 --- a/teraserver/python/opentera/services/ServiceAccessManager.py +++ b/teraserver/python/opentera/services/ServiceAccessManager.py @@ -428,45 +428,43 @@ def decorated(*args, **kwargs): return decorated return wrap + @staticmethod + def service_user_roles_any_required(roles: List[str]): + def wrap(f): + @wraps(f) + def decorated(*args, **kwargs): -@staticmethod -def service_user_roles_any_required(roles: List[str]): - def wrap(f): - @wraps(f) - def decorated(*args, **kwargs): - - # Check if service is initialized - if ServiceAccessManager.service is None or 'service_key' \ - not in ServiceAccessManager.service.service_info: - return gettext('Forbidden'), 403 - - service_key = ServiceAccessManager.service.service_info['service_key'] + # Check if service is initialized + if ServiceAccessManager.service is None or 'service_key' \ + not in ServiceAccessManager.service.service_info: + return gettext('Forbidden'), 403 - # Check if user is logged in, watch out not None object but LocalProxy cannot use is None... - if not current_user_client: - return gettext('Forbidden'), 403 + service_key = ServiceAccessManager.service.service_info['service_key'] - # Super admin pass through - if current_user_client.user_superadmin: - return f(*args, **kwargs) + # Check if user is logged in, watch out not None object but LocalProxy cannot use is None... + if not current_user_client: + return gettext('Forbidden'), 403 - # Check if user has the required role (global roles are stored in token) - user_roles_from_token = current_user_client.get_roles_for_service(service_key) + # Super admin pass through + if current_user_client.user_superadmin: + return f(*args, **kwargs) - # Check if user has the required roles - if not any(role in user_roles_from_token for role in roles): - return gettext('Forbidden'), 403 + # Check if user has the required role (global roles are stored in token) + user_roles_from_token = current_user_client.get_roles_for_service(service_key) - # Everything ok, continue - return f(*args, **kwargs) + # Check if user has the required roles + if not any(role in user_roles_from_token for role in roles): + return gettext('Forbidden'), 403 - return decorated + # Everything ok, continue + return f(*args, **kwargs) - return wrap + return decorated + return wrap -@staticmethod -def service_user_roles_required(roles: List[str]): - # For compatibility with old code - return ServiceAccessManager.service_user_roles_all_required(roles) + @staticmethod + def service_user_roles_required(roles: List[str]): + # For compatibility with old code + return ServiceAccessManager.service_user_roles_all_required(roles) diff --git a/teraserver/python/tests/opentera/services/FakeService.py b/teraserver/python/tests/opentera/services/FakeService.py new file mode 100644 index 00000000..e69de29b diff --git a/teraserver/python/tests/opentera/services/test_ServiceAccessManager.py b/teraserver/python/tests/opentera/services/test_ServiceAccessManager.py new file mode 100644 index 00000000..e69de29b diff --git a/teraserver/python/tests/opentera/services/test_TeraUserClient.py b/teraserver/python/tests/opentera/services/test_TeraUserClient.py new file mode 100644 index 00000000..e69de29b From d27db9d7fa9dc7f8e7ec8da199c989b820055baf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominic=20Le=CC=81tourneau?= Date: Wed, 13 Sep 2023 13:42:11 -0400 Subject: [PATCH 35/80] Refs #227, #228 Implementing any or all user roles, simplified token structure, to be fully tested --- .../python/tests/opentera/db/models/test_TeraUser.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/teraserver/python/tests/opentera/db/models/test_TeraUser.py b/teraserver/python/tests/opentera/db/models/test_TeraUser.py index 95a6383e..4e4e134e 100644 --- a/teraserver/python/tests/opentera/db/models/test_TeraUser.py +++ b/teraserver/python/tests/opentera/db/models/test_TeraUser.py @@ -282,16 +282,6 @@ def test_token_for_siteadmin_should_have_valid_service_access(self): # TeraServiceAccess.delete(service_access.id_service_access) # TeraServiceRole.delete(role.id_service_role) - - - - - - - - - - @staticmethod def new_test_user(user_name: str, user_groups: list | None = None) -> TeraUser: user = TeraUser() From 9962f8d3f208c3381b934c90cebc9f667a548d1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominic=20Le=CC=81tourneau?= Date: Wed, 13 Sep 2023 14:01:38 -0400 Subject: [PATCH 36/80] Updated with new python version --- teraserver/python/package.sh | 4 ++-- teraserver/python/start_conda.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/teraserver/python/package.sh b/teraserver/python/package.sh index 8b039197..0e1c14af 100755 --- a/teraserver/python/package.sh +++ b/teraserver/python/package.sh @@ -1,2 +1,2 @@ -./env/python-3.10/bin/python3 -m pip install --upgrade setuptools wheel twine -./env/python-3.10/bin/python3 setup.py sdist bdist_wheel +./env/python-3.11/bin/python3 -m pip install --upgrade setuptools wheel twine +./env/python-3.11/bin/python3 setup.py sdist bdist_wheel diff --git a/teraserver/python/start_conda.sh b/teraserver/python/start_conda.sh index 31a315f0..408ac6f2 100755 --- a/teraserver/python/start_conda.sh +++ b/teraserver/python/start_conda.sh @@ -2,4 +2,4 @@ SCRIPT=$(readlink -f $0) SCRIPTPATH=`dirname $SCRIPT` echo "$SCRIPTPATH" -$SCRIPTPATH/env/python-3.10/bin/python3 $SCRIPTPATH/TeraServer.py +$SCRIPTPATH/env/python-3.11/bin/python3 $SCRIPTPATH/TeraServer.py From cd7cd6af27c353683439983241192ad6dcdcb351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominic=20Le=CC=81tourneau?= Date: Wed, 13 Sep 2023 14:30:20 -0400 Subject: [PATCH 37/80] Updated Dockerfile --- docker/dev/teraserver/Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/dev/teraserver/Dockerfile b/docker/dev/teraserver/Dockerfile index b9997634..6915b5f9 100644 --- a/docker/dev/teraserver/Dockerfile +++ b/docker/dev/teraserver/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.10.10-bullseye +FROM python:3.11.5-bullseye # Change default shell to bash SHELL ["/bin/bash", "-c"] @@ -38,7 +38,7 @@ ADD ./teraserver/python/env/requirements.txt /requirements.txt RUN ["/bin/bash", "-c", "python3 -m pip install -r /requirements.txt"] # Install latest npm / nodejs -RUN curl -sL https://deb.nodesource.com/setup_16.x -o /nodesource_setup.sh +RUN curl -sL https://deb.nodesource.com/setup_18.x -o /nodesource_setup.sh RUN bash /nodesource_setup.sh RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y nodejs @@ -46,7 +46,7 @@ RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y nodejs ADD ./teraserver /teraserver # Remove local environment that was copied in the ADD command if it exists -RUN if [ -d "/teraserver/python/env/python-3.10" ]; then rm -rf /teraserver/python/env/python-3.10; fi +RUN if [ -d "/teraserver/python/env/python-3.11" ]; then rm -rf /teraserver/python/env/python-3.11; fi # Cleanup directory of cmake cache (we possibly are using it outside of docker) RUN if [ -f "/teraserver/CMakeCache.txt" ]; then rm -f /teraserver/CMakeCache.txt; fi From 4a4cf9c738defca1b1167823fc51be05f8dd4e3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominic=20Le=CC=81tourneau?= Date: Wed, 13 Sep 2023 14:31:59 -0400 Subject: [PATCH 38/80] Updated python version --- docker/dev/certificates/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/dev/certificates/Dockerfile b/docker/dev/certificates/Dockerfile index 2b12cab6..73f9ff08 100644 --- a/docker/dev/certificates/Dockerfile +++ b/docker/dev/certificates/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.10-bullseye +FROM python:3.11.5-bullseye # Install requirements (not changing often) ADD ./teraserver/python/env/requirements.txt /requirements.txt From f8a9e3886b43bbb368c943f060c37d70ec5e2f05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominic=20Le=CC=81tourneau?= Date: Wed, 13 Sep 2023 14:43:02 -0400 Subject: [PATCH 39/80] unused file --- teraserver/python/start_docker.sh | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100755 teraserver/python/start_docker.sh diff --git a/teraserver/python/start_docker.sh b/teraserver/python/start_docker.sh deleted file mode 100755 index 91f7083d..00000000 --- a/teraserver/python/start_docker.sh +++ /dev/null @@ -1,10 +0,0 @@ -echo "Starting postresql" -service postgresql start -echo "Starting redis-server" -service redis-server start -#redis-server & -echo "Sleeping 5 secs." -sleep 5 -echo "Starting TeraServer" -$PYTHON3_EXEC ./TeraServer.py - From ddafe9d107677ec1389b80d4a1077df40dfdbc02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominic=20Le=CC=81tourneau?= Date: Wed, 13 Sep 2023 16:19:22 -0400 Subject: [PATCH 40/80] Refs #228, #227, testing different decorators and roles --- .../tests/opentera/services/FakeService.py | 181 +++++++++++++++ .../services/test_ServiceAccessManager.py | 206 ++++++++++++++++++ .../python/tests/opentera/services/utils.py | 73 +++++++ 3 files changed, 460 insertions(+) create mode 100644 teraserver/python/tests/opentera/services/utils.py diff --git a/teraserver/python/tests/opentera/services/FakeService.py b/teraserver/python/tests/opentera/services/FakeService.py index e69de29b..ae66102b 100644 --- a/teraserver/python/tests/opentera/services/FakeService.py +++ b/teraserver/python/tests/opentera/services/FakeService.py @@ -0,0 +1,181 @@ +from services.FileTransferService.FlaskModule import CustomAPI, authorizations +from requests import Response +from modules.DatabaseModule.DBManager import DBManager +from services.FileTransferService.ConfigManager import ConfigManager +from opentera.modules.BaseModule import BaseModule +from opentera.services.ServiceOpenTera import ServiceOpenTera +from opentera.redis.RedisVars import RedisVars +from opentera.services.ServiceAccessManager import ServiceAccessManager +from flask import Flask +from flask import Response as FlaskResponse +from flask_babel import Babel +import redis +import uuid +from io import BytesIO +import opentera.messages.python as messages + + +class FakeFlaskModule(BaseModule): + def __init__(self, config: ConfigManager, flask_app): + BaseModule.__init__(self, 'FakeFlaskModule', config) + + # Will allow for user api to work + self.config.server_config = {'hostname': '127.0.0.1', 'port': 40075} + self.flask_app = flask_app + self.api = CustomAPI(self.flask_app, version='1.0.0', title='FakeService API', + description='FakeService API Documentation', doc='/doc', prefix='/', + authorizations=authorizations) + self.babel = Babel(self.flask_app) + + self.flask_app.debug = False + self.flask_app.testing = True + self.flask_app.secret_key = str(uuid.uuid4()) # Normally service UUID + self.flask_app.config.update({'SESSION_TYPE': 'redis'}) + redis_url = redis.from_url('redis://%(username)s:%(password)s@%(hostname)s:%(port)s/%(db)s' + % self.config.redis_config) + + self.flask_app.config.update({'SESSION_REDIS': redis_url}) + self.flask_app.config.update({'BABEL_DEFAULT_LOCALE': 'fr'}) + self.flask_app.config.update({'SESSION_COOKIE_SECURE': True}) + self.flask_app.config.update({'UPLOAD_FOLDER': '.'}) + + self.setup_api() + + def setup_api(self): + pass + + +class FakeService(ServiceOpenTera): + """ + The only thing we want here is a way to simulate communication with the base server. + We will simulate the service API with the database. + """ + service_token = str() + + def __init__(self, db=None): + self.flask_app = Flask('FakeService') + self.config_man = ConfigManager() + self.config_man.create_defaults() + self.db_manager = DBManager(self.config_man, self.flask_app) + # Cheating on db (reusing already opened from test) + if db is not None: + self.db_manager.db = db + else: + self.db_manager.open_local({}, echo=False, ram=True) + + with self.flask_app.app_context(): + # Update redis vars and basic token + self.db_manager.create_defaults(self.config_man, test=True) + + self.setup_service_access_manager() + + # Redis variables & db must be initialized before + ServiceOpenTera.__init__(self, self.config_man, {'service_key': 'FakeService'}) + + # Setup service + self.config['ServiceUUID'] = str(uuid.uuid4()) + self.config_man.service_config['ServiceUUID'] = self.config['ServiceUUID'] + + # Setup modules + self.flask_module = FakeFlaskModule(self.config_man, self.flask_app) + self.test_client = self.flask_app.test_client() + + with self.flask_app.app_context(): + self.service_token = self.generate_service_token() + + def generate_service_token(self) -> str: + pass + + def app_context(self): + return self.flask_app.app_context() + + def setup_service_access_manager(self): + from opentera.db.models.TeraServerSettings import TeraServerSettings + + self.redis = redis.Redis(host=self.config_man.redis_config['hostname'], + port=self.config_man.redis_config['port'], + db=self.config_man.redis_config['db'], + username=self.config_man.redis_config['username'], + password=self.config_man.redis_config['password'], + client_name=self.__class__.__name__) + + # Initialize service from redis, posing as FileTransferService + # User token key (dynamic) + ServiceAccessManager.api_user_token_key = 'test_api_user_token_key' + self.redis.set(RedisVars.RedisVar_UserTokenAPIKey, + ServiceAccessManager.api_user_token_key) + + # Participant token key from DB (static) + ServiceAccessManager.api_participant_static_token_key = TeraServerSettings.get_server_setting_value( + TeraServerSettings.ServerParticipantTokenKey) + self.redis.set(RedisVars.RedisVar_ParticipantStaticTokenAPIKey, + ServiceAccessManager.api_participant_static_token_key) + + # Participant token key (dynamic) + ServiceAccessManager.api_participant_token_key = 'test_api_participant_token_key' + self.redis.set(RedisVars.RedisVar_ParticipantTokenAPIKey, + ServiceAccessManager.api_participant_token_key) + + # Device Token Key from DB (static) + ServiceAccessManager.api_device_static_token_key = TeraServerSettings.get_server_setting_value( + TeraServerSettings.ServerDeviceTokenKey) + self.redis.set(RedisVars.RedisVar_DeviceStaticTokenAPIKey, ServiceAccessManager.api_device_static_token_key) + + # Device Token Key (dynamic = static) + ServiceAccessManager.api_device_token_key = TeraServerSettings.get_server_setting_value( + TeraServerSettings.ServerDeviceTokenKey) + self.redis.set(RedisVars.RedisVar_DeviceTokenAPIKey, ServiceAccessManager.api_device_token_key) + + # Service Token Key (dynamic) + ServiceAccessManager.api_service_token_key = 'test_api_service_token_key' + self.redis.set(RedisVars.RedisVar_ServiceTokenAPIKey, ServiceAccessManager.api_service_token_key) + ServiceAccessManager.config_man = self.config_man + + @staticmethod + def convert_to_standard_request_response(flask_response: FlaskResponse): + result = Response() + result.status_code = flask_response.status_code + result.headers = flask_response.headers + result.encoding = flask_response.content_encoding + result.raw = BytesIO(flask_response.data) + return result + + def post_to_opentera(self, api_url: str, json_data: dict, token=None) -> Response: + with self.flask_app.app_context(): + # Synchronous call to OpenTera fake backend + if not token: + token = self.service_token + request_headers = {'Authorization': 'OpenTera ' + token} + answer = self.test_client.post(api_url, headers=request_headers, json=json_data) + return FakeService.convert_to_standard_request_response(answer) + + def get_from_opentera(self, api_url: str, params: dict, token=None) -> Response: + with self.flask_app.app_context(): + # Synchronous call to OpenTera fake backend + if not token: + token = self.service_token + request_headers = {'Authorization': 'OpenTera ' + token} + answer = self.test_client.get(api_url, headers=request_headers, query_string=params) + return FakeService.convert_to_standard_request_response(answer) + + def delete_from_opentera(self, api_url: str, params: dict, token=None) -> Response: + with self.flask_app.app_context(): + # Synchronous call to OpenTera fake backend + if not token: + token = self.service_token + request_headers = {'Authorization': 'OpenTera ' + token} + answer = self.test_client.delete(api_url, headers=request_headers, query_string=params) + return FakeService.convert_to_standard_request_response(answer) + + def asset_event_received(self, event: messages.DatabaseEvent): + pass + + +if __name__ == '__main__': + service = FakeService() + with service.app_context(): + pass + + + + diff --git a/teraserver/python/tests/opentera/services/test_ServiceAccessManager.py b/teraserver/python/tests/opentera/services/test_ServiceAccessManager.py index e69de29b..3afbd847 100644 --- a/teraserver/python/tests/opentera/services/test_ServiceAccessManager.py +++ b/teraserver/python/tests/opentera/services/test_ServiceAccessManager.py @@ -0,0 +1,206 @@ +import unittest +from tests.opentera.services.FakeService import FakeService, FakeFlaskModule +from opentera.services.ServiceAccessManager import ServiceAccessManager +import tests.opentera.services.utils as utils +from flask_restx import Resource, inputs + + +class TestQueryWithServiceRoles(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) + self.test_case = kwargs.get('testCase', None) + + @ServiceAccessManager.token_required(allow_dynamic_tokens=True, allow_static_tokens=False) + @ServiceAccessManager.service_user_roles_required(['test-role']) + def get(self): + return 'OK', 200 + + +class TestQueryWithServiceRolesAny(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) + self.test_case = kwargs.get('testCase', None) + + @ServiceAccessManager.token_required(allow_dynamic_tokens=True, allow_static_tokens=False) + @ServiceAccessManager.service_user_roles_any_required(['test-role1', 'test-role2']) + def get(self): + return 'OK', 200 + + +class TestQueryWithServiceRolesAll(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) + + @ServiceAccessManager.token_required(allow_dynamic_tokens=True, allow_static_tokens=False) + @ServiceAccessManager.service_user_roles_all_required(['test-role1', 'test-role2']) + def get(self): + return 'OK', 200 + + +class ServiceAccessManagerTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.__service = FakeService() + + # Create api endpoint + # Default arguments + kwargs = {'flaskModule': cls.__service.flask_module, + 'test': True} + + # Resources + cls.__service.flask_module.api.add_resource(TestQueryWithServiceRoles, '/test', + resource_class_kwargs=kwargs) + cls.__service.flask_module.api.add_resource(TestQueryWithServiceRolesAny, '/test_any', + resource_class_kwargs=kwargs) + cls.__service.flask_module.api.add_resource(TestQueryWithServiceRolesAll, '/test_all', + resource_class_kwargs=kwargs) + + @classmethod + def tearDownClass(cls): + pass + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_endpoint_no_token(self): + with self.__service.app_context(): + # No token + response = self.__service.test_client.get('/test') + self.assertEqual(403, response.status_code) + + def test_endpoint_no_token_any(self): + with self.__service.app_context(): + # No token + response = self.__service.test_client.get('/test_any') + self.assertEqual(403, response.status_code) + + def test_endpoint_no_token_all(self): + with self.__service.app_context(): + # No token + response = self.__service.test_client.get('/test_all') + self.assertEqual(403, response.status_code) + + def test_endpoint_user_token_with_superadmin(self): + with self.__service.app_context(): + # User token with no role + user_token = utils._generate_fake_user_token(roles=[], superadmin=True) + response = self.__service.test_client.get('/test', + headers={'Authorization': 'OpenTera ' + user_token}) + self.assertEqual(200, response.status_code) + + def test_endpoint_user_token_with_superadmin_any(self): + with self.__service.app_context(): + # User token with no role + user_token = utils._generate_fake_user_token(roles=[], superadmin=True) + response = self.__service.test_client.get('/test_any', + headers={'Authorization': 'OpenTera ' + user_token}) + self.assertEqual(200, response.status_code) + + def test_endpoint_user_token_with_superadmin_all(self): + with self.__service.app_context(): + # User token with no role + user_token = utils._generate_fake_user_token(roles=[], superadmin=True) + response = self.__service.test_client.get('/test_all', + headers={'Authorization': 'OpenTera ' + user_token}) + self.assertEqual(200, response.status_code) + + def test_endpoint_user_token_with_no_role(self): + with self.__service.app_context(): + # User token with no role + user_token = utils._generate_fake_user_token(roles=[]) + response = self.__service.test_client.get('/test', + headers={'Authorization': 'OpenTera ' + user_token}) + self.assertEqual(403, response.status_code) + + def test_endpoint_user_token_with_no_role_any(self): + with self.__service.app_context(): + # User token with no role + user_token = utils._generate_fake_user_token(roles=[]) + response = self.__service.test_client.get('/test_any', + headers={'Authorization': 'OpenTera ' + user_token}) + self.assertEqual(403, response.status_code) + + def test_endpoint_user_token_with_no_role_all(self): + with self.__service.app_context(): + # User token with no role + user_token = utils._generate_fake_user_token(roles=[]) + response = self.__service.test_client.get('/test_all', + headers={'Authorization': 'OpenTera ' + user_token}) + self.assertEqual(403, response.status_code) + + def test_endpoint_user_token_with_required_role(self): + with self.__service.app_context(): + # User token with no role + user_token = utils._generate_fake_user_token(roles=['test-role']) + response = self.__service.test_client.get('/test', + headers={'Authorization': 'OpenTera ' + user_token}) + self.assertEqual(200, response.status_code) + + def test_endpoint_user_token_with_required_role_any(self): + with self.__service.app_context(): + # User token with no role + user_token = utils._generate_fake_user_token(roles=['test-role1']) + response = self.__service.test_client.get('/test_any', + headers={'Authorization': 'OpenTera ' + user_token}) + self.assertEqual(200, response.status_code) + + user_token = utils._generate_fake_user_token(roles=['test-role2']) + response = self.__service.test_client.get('/test_any', + headers={'Authorization': 'OpenTera ' + user_token}) + self.assertEqual(200, response.status_code) + + user_token = utils._generate_fake_user_token(roles=['test-role1', 'test-role2']) + response = self.__service.test_client.get('/test_any', + headers={'Authorization': 'OpenTera ' + user_token}) + self.assertEqual(200, response.status_code) + + def test_endpoint_user_token_with_required_role_all(self): + with self.__service.app_context(): + # User token with no role + user_token = utils._generate_fake_user_token(roles=['test-role1']) + response = self.__service.test_client.get('/test_all', + headers={'Authorization': 'OpenTera ' + user_token}) + self.assertEqual(403, response.status_code) + + user_token = utils._generate_fake_user_token(roles=['test-role2']) + response = self.__service.test_client.get('/test_all', + headers={'Authorization': 'OpenTera ' + user_token}) + self.assertEqual(403, response.status_code) + + user_token = utils._generate_fake_user_token(roles=['test-role1', 'test-role2']) + response = self.__service.test_client.get('/test_all', + headers={'Authorization': 'OpenTera ' + user_token}) + self.assertEqual(200, response.status_code) + + def test_endpoint_participant_token_should_fail(self): + with self.__service.app_context(): + # User token with no role + participant_token = utils._generate_fake_dynamic_participant_token() + response = self.__service.test_client.get('/test', + headers={'Authorization': 'OpenTera ' + participant_token}) + self.assertEqual(403, response.status_code) + + def test_endpoint_participant_token_should_fail_any(self): + with self.__service.app_context(): + # User token with no role + participant_token = utils._generate_fake_dynamic_participant_token() + response = self.__service.test_client.get('/test_any', + headers={'Authorization': 'OpenTera ' + participant_token}) + self.assertEqual(403, response.status_code) + + def test_endpoint_participant_token_should_fail_all(self): + with self.__service.app_context(): + # User token with no role + participant_token = utils._generate_fake_dynamic_participant_token() + response = self.__service.test_client.get('/test_any', + headers={'Authorization': 'OpenTera ' + participant_token}) + self.assertEqual(403, response.status_code) diff --git a/teraserver/python/tests/opentera/services/utils.py b/teraserver/python/tests/opentera/services/utils.py new file mode 100644 index 00000000..626a46c1 --- /dev/null +++ b/teraserver/python/tests/opentera/services/utils.py @@ -0,0 +1,73 @@ +import unittest +from flask_restx import Resource, inputs +import time +from opentera.services.ServiceAccessManager import ServiceAccessManager +import opentera.redis.RedisVars as RedisVars +import jwt +import uuid + + +def infinite_jti_sequence(): + num = 0 + while True: + yield num + num += 1 + + +# Initialize generator, call next(user_jti_generator) to get next sequence number +user_jti_generator = infinite_jti_sequence() +participant_jti_generator = infinite_jti_sequence() + +@staticmethod +def _generate_fake_user_token(name='FakeUser', user_uuid=str(uuid.uuid4()), roles=[], + superadmin=False, expiration=3600): + # Creating token with user info + now = time.time() + token_key = ServiceAccessManager.api_user_token_key + + payload = { + 'iat': int(now), + 'exp': int(now) + expiration, + 'iss': 'TeraServer', + 'jti': next(user_jti_generator), + 'user_uuid': user_uuid, + 'id_user': 1, + 'user_fullname': name, + 'user_superadmin': superadmin, + 'service_access': {'FakeService': roles} # role of the user + } + + return jwt.encode(payload, token_key, algorithm='HS256') + + +@staticmethod +def _generate_fake_static_participant_token(participant_uuid=str(uuid.uuid4())): + # Creating token with participant info + token_key = ServiceAccessManager.api_participant_static_token_key + payload = { + 'iss': 'TeraServer', + 'jti': next(participant_jti_generator), + 'participant_uuid': participant_uuid, + 'id_participant': 1 + } + return jwt.encode(payload, token_key, algorithm='HS256') + + +@staticmethod +def _generate_fake_dynamic_participant_token(name='FakeParticipant', participant_uuid=str(uuid.uuid4()), + expiration=3600): + # Creating token with participant info + now = time.time() + token_key = ServiceAccessManager.api_participant_token_key + payload = { + 'iat': int(now), + 'exp': int(now) + expiration, + 'iss': 'TeraServer', + 'jti': next(participant_jti_generator), + 'participant_uuid': participant_uuid, + 'id_participant': 2, + 'user_fullname': name + } + + return jwt.encode(payload, token_key, algorithm='HS256') + From 112046699d41a3a122f939593a4d2849c4b1f44e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominic=20Le=CC=81tourneau?= Date: Wed, 13 Sep 2023 16:21:13 -0400 Subject: [PATCH 41/80] Refs #228, #227, testing different decorators and roles --- .github/workflows/run_tests_on_pull_request.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run_tests_on_pull_request.yml b/.github/workflows/run_tests_on_pull_request.yml index e20c6815..9553b4e7 100644 --- a/.github/workflows/run_tests_on_pull_request.yml +++ b/.github/workflows/run_tests_on_pull_request.yml @@ -100,7 +100,12 @@ jobs: working-directory: teraserver/python/tests/opentera/db/models run: | $OPENTERA_PYTHON -m unittest discover . "test_*.py" - + + - name: Run Service Tests + working-directory: teraserver/python/tests/opentera/services + run: | + $OPENTERA_PYTHON -m unittest discover . "test_*.py" + - name: Run FileTransferService Tests working-directory: teraserver/python/tests/services/FileTransferService run: | From ff691b73fc862d4bdb3008328a0a974c06318483 Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Thu, 14 Sep 2023 09:34:58 -0400 Subject: [PATCH 42/80] Refs #230. Added first implementation music streaming in VideoRehab. --- teraserver/easyrtc/protected/index_users.html | 4 + teraserver/easyrtc/static/i18n/en.json | 3 +- teraserver/easyrtc/static/i18n/fr.json | 3 +- teraserver/easyrtc/static/images/music.png | Bin 0 -> 1155 bytes .../static/js/tera_layout_participants.js | 8 +- teraserver/easyrtc/static/js/tera_ui.js | 8 +- teraserver/easyrtc/static/js/tera_webrtc.js | 64 ++++-- teraserver/python/config/TeraServerConfig.ini | 2 +- .../python/opentera/db/models/TeraTest.py | 2 +- .../static/js/prettify/README | 214 ------------------ .../static/js/prettify/jquery.js | 4 - .../static/js/prettify/jquery.min.js | 4 - .../static/js/prettify/lang-css.js | 2 - .../static/js/prettify/lang-sql.js | 2 - .../static/js/prettify/lang-vhdl.js | 3 - .../static/js/prettify/lang-yaml.js | 2 - .../static/js/prettify/loadAndFilter.js | 124 ---------- .../static/js/prettify/prettify.css | 1 - .../static/js/prettify/prettify.js | 28 --- 19 files changed, 66 insertions(+), 412 deletions(-) create mode 100644 teraserver/easyrtc/static/images/music.png delete mode 100755 teraserver/python/services/VideoRehabService/static/js/prettify/README delete mode 100755 teraserver/python/services/VideoRehabService/static/js/prettify/jquery.js delete mode 100755 teraserver/python/services/VideoRehabService/static/js/prettify/jquery.min.js delete mode 100755 teraserver/python/services/VideoRehabService/static/js/prettify/lang-css.js delete mode 100755 teraserver/python/services/VideoRehabService/static/js/prettify/lang-sql.js delete mode 100755 teraserver/python/services/VideoRehabService/static/js/prettify/lang-vhdl.js delete mode 100755 teraserver/python/services/VideoRehabService/static/js/prettify/lang-yaml.js delete mode 100755 teraserver/python/services/VideoRehabService/static/js/prettify/loadAndFilter.js delete mode 100755 teraserver/python/services/VideoRehabService/static/js/prettify/prettify.css delete mode 100755 teraserver/python/services/VideoRehabService/static/js/prettify/prettify.js diff --git a/teraserver/easyrtc/protected/index_users.html b/teraserver/easyrtc/protected/index_users.html index 44188ce2..3f0cba00 100644 --- a/teraserver/easyrtc/protected/index_users.html +++ b/teraserver/easyrtc/protected/index_users.html @@ -383,6 +383,10 @@