From 00f76ae5afb8601f461cba31bd45404cb3e72ff8 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Fri, 4 Oct 2019 14:15:59 -0400 Subject: [PATCH 01/11] Add delete user job --- ...ec1_add_deleted_environment_role_status.py | 73 +++++++++++++++++++ atst/app.py | 11 +++ atst/jobs.py | 19 ++++- atst/models/environment_role.py | 1 + tests/test_jobs.py | 23 +++++- 5 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 alembic/versions/1497926ddec1_add_deleted_environment_role_status.py diff --git a/alembic/versions/1497926ddec1_add_deleted_environment_role_status.py b/alembic/versions/1497926ddec1_add_deleted_environment_role_status.py new file mode 100644 index 00000000..ab3b01f8 --- /dev/null +++ b/alembic/versions/1497926ddec1_add_deleted_environment_role_status.py @@ -0,0 +1,73 @@ +"""add deleted environment_role status + +Revision ID: 1497926ddec1 +Revises: e3d93f9caba7 +Create Date: 2019-10-04 10:44:54.198368 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "1497926ddec1" # pragma: allowlist secret +down_revision = "e3d93f9caba7" # pragma: allowlist secret +branch_labels = None +depends_on = None + + +def upgrade(): + # op.alter_column("environment_roles", "status", new_column_name="status_old") + op.alter_column( + "environment_roles", + "status", + type_=sa.Enum( + "PENDING", + "COMPLETED", + "PENDING_DELETE", + "DELETED", + name="status", + native_enum=False, + ), + existing_type=sa.Enum( + "PENDING", + "COMPLETED", + "PENDING_DELETE", + name="status", + native_enum=False, + ), + ) + + # conn = op.get_bind() + # conn.execute("UPDATE environment_roles SET status = status_old") + # op.drop_column("environment_roles", "status_old") + + +def downgrade(): + conn = op.get_bind() + conn.execute( + """ + UPDATE environment_roles + SET status = (CASE WHEN status = 'DELETED' THEN 'PENDING_DELETE' ELSE status END) + """ + ) + + op.alter_column( + "environment_roles", + "status", + type_=sa.Enum( + "PENDING", + "COMPLETED", + "PENDING_DELETE", + name="status", + native_enum=False, + ), + existing_type=sa.Enum( + "PENDING", + "COMPLETED", + "PENDING_DELETE", + "DELETED", + name="status", + native_enum=False, + ), + ) diff --git a/atst/app.py b/atst/app.py index f3e7276f..cd1541b0 100644 --- a/atst/app.py +++ b/atst/app.py @@ -8,6 +8,7 @@ from flask_session import Session import redis from unipath import Path from flask_wtf.csrf import CSRFProtect +import json from atst.database import db from atst.assets import environment as assets_environment @@ -148,6 +149,15 @@ def set_default_headers(app): # pragma: no cover def map_config(config): + def sqlalchemy_dumps(dct): + def _default(self, obj): + if isinstance(obj, Enum): + return obj.name + else: + raise TypeError() + + return json.dumps(dct, default=_default) + return { **config["default"], "ENV": config["default"]["ENVIRONMENT"], @@ -158,6 +168,7 @@ def map_config(config): "PORT": int(config["default"]["PORT"]), "SQLALCHEMY_DATABASE_URI": config["default"]["DATABASE_URI"], "SQLALCHEMY_TRACK_MODIFICATIONS": False, + "SQLALCHEMY_ENGINE_OPTIONS": {"json_serializer": sqlalchemy_dumps}, "WTF_CSRF_ENABLED": config.getboolean("default", "WTF_CSRF_ENABLED"), "PERMANENT_SESSION_LIFETIME": config.getint( "default", "PERMANENT_SESSION_LIFETIME" diff --git a/atst/jobs.py b/atst/jobs.py index e861d8b5..8e5d5602 100644 --- a/atst/jobs.py +++ b/atst/jobs.py @@ -3,7 +3,11 @@ import pendulum from atst.database import db from atst.queue import celery -from atst.models import EnvironmentJobFailure, EnvironmentRoleJobFailure +from atst.models import ( + EnvironmentJobFailure, + EnvironmentRoleJobFailure, + EnvironmentRole, +) from atst.domain.csp.cloud import CloudProviderInterface, GeneralCSPException from atst.domain.environments import Environments from atst.domain.environment_roles import EnvironmentRoles @@ -112,6 +116,19 @@ def do_provision_user(csp: CloudProviderInterface, environment_role_id=None): credentials, environment_role, environment_role.role ) environment_role.csp_user_id = csp_user_id + environment_role.status = EnvironmentRole.Status.COMPLETED + db.session.add(environment_role) + db.session.commit() + + +def do_delete_user(csp: CloudProviderInterface, environment_role_id=None): + environment_role = EnvironmentRoles.get_by_id(environment_role_id) + + with claim_for_update(environment_role) as environment_role: + credentials = environment_role.environment.csp_credentials + + csp.delete_user(credentials, environment_role.csp_user_id) + environment_role.status = EnvironmentRole.Status.DELETED db.session.add(environment_role) db.session.commit() diff --git a/atst/models/environment_role.py b/atst/models/environment_role.py index 988d19ab..f3e8004f 100644 --- a/atst/models/environment_role.py +++ b/atst/models/environment_role.py @@ -40,6 +40,7 @@ class EnvironmentRole( PENDING = "pending" COMPLETED = "completed" PENDING_DELETE = "pending_delete" + DELETED = "deleted" status = Column(SQLAEnum(Status, native_enum=False), default=Status.PENDING) diff --git a/tests/test_jobs.py b/tests/test_jobs.py index c4d17b5d..47ec1840 100644 --- a/tests/test_jobs.py +++ b/tests/test_jobs.py @@ -3,7 +3,6 @@ import pytest from uuid import uuid4 from unittest.mock import Mock from threading import Thread -from time import sleep from atst.domain.csp.cloud import MockCloudProvider from atst.jobs import ( @@ -18,6 +17,7 @@ from atst.jobs import ( create_environment, dispatch_provision_user, do_provision_user, + do_delete_user, ) from atst.models.utils import claim_for_update from atst.domain.exceptions import ClaimFailedException @@ -370,3 +370,24 @@ def test_do_provision_user(csp, session): ) # I expect that the EnvironmentRole now has a csp_user_id assert environment_role.csp_user_id + + +def test_do_delete_user(csp, session): + credentials = MockCloudProvider(())._auth_credentials + provisioned_environment = EnvironmentFactory.create( + cloud_id="cloud_id", + root_user_info={"credentials": credentials}, + baseline_info={}, + ) + + environment_role = EnvironmentRoleFactory.create( + environment=provisioned_environment, + status=EnvironmentRole.Status.PENDING_DELETE, + role="my_role", + ) + + do_delete_user(csp=csp, environment_role_id=environment_role.id) + + session.refresh(environment_role) + + assert environment_role.status == EnvironmentRole.Status.DELETED From 2ad30b5fa48f5f5d2f09a9910f3d322760c46158 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Fri, 4 Oct 2019 16:22:45 -0400 Subject: [PATCH 02/11] Implement dispatch_delete_user job --- atst/app.py | 3 ++- atst/domain/environment_roles.py | 14 ++++++++++ atst/jobs.py | 15 +++++++++++ tests/test_jobs.py | 44 ++++++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 1 deletion(-) diff --git a/atst/app.py b/atst/app.py index cd1541b0..3194a4fe 100644 --- a/atst/app.py +++ b/atst/app.py @@ -9,6 +9,7 @@ import redis from unipath import Path from flask_wtf.csrf import CSRFProtect import json +from enum import Enum from atst.database import db from atst.assets import environment as assets_environment @@ -150,7 +151,7 @@ def set_default_headers(app): # pragma: no cover def map_config(config): def sqlalchemy_dumps(dct): - def _default(self, obj): + def _default(obj): if isinstance(obj, Enum): return obj.name else: diff --git a/atst/domain/environment_roles.py b/atst/domain/environment_roles.py index 9dd59365..d16ab979 100644 --- a/atst/domain/environment_roles.py +++ b/atst/domain/environment_roles.py @@ -87,3 +87,17 @@ class EnvironmentRoles(object): .all() ) return [id_ for id_, in results] + + @classmethod + def get_environment_roles_pending_deletion(cls) -> List[UUID]: + results = ( + db.session.query(EnvironmentRole.id) + .join(Environment) + .join(ApplicationRole) + .filter(Environment.deleted == False) + .filter(Environment.baseline_info != None) + .filter(EnvironmentRole.status == EnvironmentRole.Status.PENDING_DELETE) + .filter(ApplicationRole.status == ApplicationRoleStatus.ACTIVE) + .all() + ) + return [id_ for id_, in results] diff --git a/atst/jobs.py b/atst/jobs.py index 8e5d5602..e630409e 100644 --- a/atst/jobs.py +++ b/atst/jobs.py @@ -169,6 +169,13 @@ def provision_user(self, environment_role_id=None): ) +@celery.task(bind=True) +def delete_user(self, environment_role_id=None): + do_work( + do_delete_user, self, app.csp.cloud, environment_role_id=environment_role_id + ) + + @celery.task(bind=True) def dispatch_create_environment(self): for environment_id in Environments.get_environments_pending_creation( @@ -199,3 +206,11 @@ def dispatch_provision_user(self): environment_role_id ) in EnvironmentRoles.get_environment_roles_pending_creation(): provision_user.delay(environment_role_id=environment_role_id) + + +@celery.task(bind=True) +def dispatch_delete_user(self): + for ( + environment_role_id + ) in EnvironmentRoles.get_environment_roles_pending_deletion(): + delete_user.delay(environment_role_id=environment_role_id) diff --git a/tests/test_jobs.py b/tests/test_jobs.py index 47ec1840..8e69c9f0 100644 --- a/tests/test_jobs.py +++ b/tests/test_jobs.py @@ -18,6 +18,7 @@ from atst.jobs import ( dispatch_provision_user, do_provision_user, do_delete_user, + dispatch_delete_user, ) from atst.models.utils import claim_for_update from atst.domain.exceptions import ClaimFailedException @@ -391,3 +392,46 @@ def test_do_delete_user(csp, session): session.refresh(environment_role) assert environment_role.status == EnvironmentRole.Status.DELETED + + +def test_dispatch_delete_user(csp, session, monkeypatch): + # Given that I have three environment roles: + # (A) one of which has a completed status + # (B) one of which has an environment that has not been provisioned + # (C) one of which is pending, has a provisioned environment but an inactive application role + # (D) one of which is pending, has a provisioned environment and has an active application role + # (E) one of which is pending delete, has a provisioned environment and has an active application role + provisioned_environment = EnvironmentFactory.create( + cloud_id="cloud_id", root_user_info={}, baseline_info={} + ) + unprovisioned_environment = EnvironmentFactory.create() + _er_a = EnvironmentRoleFactory.create( + environment=provisioned_environment, status=EnvironmentRole.Status.COMPLETED + ) + _er_b = EnvironmentRoleFactory.create( + environment=unprovisioned_environment, status=EnvironmentRole.Status.PENDING + ) + _er_c = EnvironmentRoleFactory.create( + environment=unprovisioned_environment, + status=EnvironmentRole.Status.PENDING, + application_role=ApplicationRoleFactory(status=ApplicationRoleStatus.PENDING), + ) + _er_d = EnvironmentRoleFactory.create( + environment=unprovisioned_environment, + status=EnvironmentRole.Status.PENDING_DELETE, + application_role=ApplicationRoleFactory(status=ApplicationRoleStatus.PENDING), + ) + er_e = EnvironmentRoleFactory.create( + environment=provisioned_environment, + status=EnvironmentRole.Status.PENDING_DELETE, + application_role=ApplicationRoleFactory(status=ApplicationRoleStatus.ACTIVE), + ) + + mock = Mock() + monkeypatch.setattr("atst.jobs.delete_user", mock) + + # When I dispatch the user deletion task + dispatch_delete_user.run() + + # I expect it to dispatch only one call, to EnvironmentRole E + mock.delay.assert_called_once_with(environment_role_id=er_e.id) From 31d2380afc41ba378568fa6faefbae382d6ef341 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Fri, 4 Oct 2019 16:25:52 -0400 Subject: [PATCH 03/11] Clean up migration --- ...dec1_add_deleted_environment_role_status.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/alembic/versions/1497926ddec1_add_deleted_environment_role_status.py b/alembic/versions/1497926ddec1_add_deleted_environment_role_status.py index ab3b01f8..e32f6a66 100644 --- a/alembic/versions/1497926ddec1_add_deleted_environment_role_status.py +++ b/alembic/versions/1497926ddec1_add_deleted_environment_role_status.py @@ -17,7 +17,6 @@ depends_on = None def upgrade(): - # op.alter_column("environment_roles", "status", new_column_name="status_old") op.alter_column( "environment_roles", "status", @@ -30,18 +29,10 @@ def upgrade(): native_enum=False, ), existing_type=sa.Enum( - "PENDING", - "COMPLETED", - "PENDING_DELETE", - name="status", - native_enum=False, + "PENDING", "COMPLETED", "PENDING_DELETE", name="status", native_enum=False ), ) - # conn = op.get_bind() - # conn.execute("UPDATE environment_roles SET status = status_old") - # op.drop_column("environment_roles", "status_old") - def downgrade(): conn = op.get_bind() @@ -51,16 +42,11 @@ def downgrade(): SET status = (CASE WHEN status = 'DELETED' THEN 'PENDING_DELETE' ELSE status END) """ ) - op.alter_column( "environment_roles", "status", type_=sa.Enum( - "PENDING", - "COMPLETED", - "PENDING_DELETE", - name="status", - native_enum=False, + "PENDING", "COMPLETED", "PENDING_DELETE", name="status", native_enum=False ), existing_type=sa.Enum( "PENDING", From 343e989de8bfe81dbcd8224e6166d7cd8e7dc3f6 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Fri, 4 Oct 2019 16:34:32 -0400 Subject: [PATCH 04/11] Update comments --- tests/test_jobs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_jobs.py b/tests/test_jobs.py index 8e69c9f0..b8ddef4e 100644 --- a/tests/test_jobs.py +++ b/tests/test_jobs.py @@ -311,7 +311,7 @@ def test_claim_for_update(session): def test_dispatch_provision_user(csp, session, celery_app, celery_worker, monkeypatch): - # Given that I have three environment roles: + # Given that I have four environment roles: # (A) one of which has a completed status # (B) one of which has an environment that has not been provisioned # (C) one of which is pending, has a provisioned environment but an inactive application role @@ -395,7 +395,7 @@ def test_do_delete_user(csp, session): def test_dispatch_delete_user(csp, session, monkeypatch): - # Given that I have three environment roles: + # Given that I have five environment roles: # (A) one of which has a completed status # (B) one of which has an environment that has not been provisioned # (C) one of which is pending, has a provisioned environment but an inactive application role From 78c49497767d8609381dad287f04038444bce3dd Mon Sep 17 00:00:00 2001 From: richard-dds Date: Mon, 7 Oct 2019 15:22:21 -0400 Subject: [PATCH 05/11] Move sqlalchemy_dumps to atst.utils.json --- atst/app.py | 13 +------------ atst/utils/json.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/atst/app.py b/atst/app.py index 3194a4fe..31eb6f85 100644 --- a/atst/app.py +++ b/atst/app.py @@ -8,8 +8,6 @@ from flask_session import Session import redis from unipath import Path from flask_wtf.csrf import CSRFProtect -import json -from enum import Enum from atst.database import db from atst.assets import environment as assets_environment @@ -31,7 +29,7 @@ from atst.models.permissions import Permissions from atst.queue import celery, update_celery from atst.utils import mailer from atst.utils.form_cache import FormCache -from atst.utils.json import CustomJSONEncoder +from atst.utils.json import CustomJSONEncoder, sqlalchemy_dumps from atst.utils.notification_sender import NotificationSender from atst.utils.session_limiter import SessionLimiter @@ -150,15 +148,6 @@ def set_default_headers(app): # pragma: no cover def map_config(config): - def sqlalchemy_dumps(dct): - def _default(obj): - if isinstance(obj, Enum): - return obj.name - else: - raise TypeError() - - return json.dumps(dct, default=_default) - return { **config["default"], "ENV": config["default"]["ENVIRONMENT"], diff --git a/atst/utils/json.py b/atst/utils/json.py index 8e2a3217..9e34e10e 100644 --- a/atst/utils/json.py +++ b/atst/utils/json.py @@ -1,6 +1,9 @@ from flask.json import JSONEncoder +import json from werkzeug.datastructures import FileStorage from datetime import date +from enum import Enum + from atst.models.attachment import Attachment @@ -13,3 +16,13 @@ class CustomJSONEncoder(JSONEncoder): elif isinstance(obj, FileStorage): return obj.filename return JSONEncoder.default(self, obj) + + +def sqlalchemy_dumps(dct): + def _default(obj): + if isinstance(obj, Enum): + return obj.name + else: + raise TypeError() + + return json.dumps(dct, default=_default) From 274ff9fbbf4fe9c407b7cd98d8c3de7093776877 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 8 Oct 2019 10:21:15 -0400 Subject: [PATCH 06/11] Trigger environment_role deletion --- atst/domain/environment_roles.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/atst/domain/environment_roles.py b/atst/domain/environment_roles.py index d16ab979..6e724789 100644 --- a/atst/domain/environment_roles.py +++ b/atst/domain/environment_roles.py @@ -58,8 +58,8 @@ class EnvironmentRoles(object): def delete(cls, application_role_id, environment_id): existing_env_role = EnvironmentRoles.get(application_role_id, environment_id) if existing_env_role: - # TODO: Set status to pending_delete - db.session.delete(existing_env_role) + existing_env_role.status = EnvironmentRole.Status.PENDING_DELETE + existing_env_role.role = "deleted" db.session.commit() return True else: From ed7bc33e44e40f8e6d5e9e4aadec224058814b5c Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 8 Oct 2019 11:03:18 -0400 Subject: [PATCH 07/11] Set deleted flag in do_delete_user --- atst/jobs.py | 1 + tests/test_jobs.py | 1 + 2 files changed, 2 insertions(+) diff --git a/atst/jobs.py b/atst/jobs.py index e630409e..2a3ea97f 100644 --- a/atst/jobs.py +++ b/atst/jobs.py @@ -129,6 +129,7 @@ def do_delete_user(csp: CloudProviderInterface, environment_role_id=None): csp.delete_user(credentials, environment_role.csp_user_id) environment_role.status = EnvironmentRole.Status.DELETED + environment_role.deleted = True db.session.add(environment_role) db.session.commit() diff --git a/tests/test_jobs.py b/tests/test_jobs.py index b8ddef4e..b0058851 100644 --- a/tests/test_jobs.py +++ b/tests/test_jobs.py @@ -392,6 +392,7 @@ def test_do_delete_user(csp, session): session.refresh(environment_role) assert environment_role.status == EnvironmentRole.Status.DELETED + assert environment_role.deleted == True def test_dispatch_delete_user(csp, session, monkeypatch): From 0c480ccc411f5f1923eff9b3989ac7eb8537a386 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 8 Oct 2019 16:14:16 -0400 Subject: [PATCH 08/11] Fix tests --- atst/domain/environment_roles.py | 12 +++++++++++- tests/domain/test_application_roles.py | 3 ++- tests/domain/test_environments.py | 10 +++++----- tests/routes/applications/test_settings.py | 16 +--------------- 4 files changed, 19 insertions(+), 22 deletions(-) diff --git a/atst/domain/environment_roles.py b/atst/domain/environment_roles.py index 6e724789..bdf4ae09 100644 --- a/atst/domain/environment_roles.py +++ b/atst/domain/environment_roles.py @@ -56,7 +56,17 @@ class EnvironmentRoles(object): @classmethod def delete(cls, application_role_id, environment_id): - existing_env_role = EnvironmentRoles.get(application_role_id, environment_id) + existing_env_role = ( + db.session.query(EnvironmentRole) + .join(ApplicationRole) + .filter( + ApplicationRole.id == application_role_id, + EnvironmentRole.environment_id == environment_id, + EnvironmentRole.status != EnvironmentRole.Status.PENDING_DELETE, + ) + .one_or_none() + ) + if existing_env_role: existing_env_role.status = EnvironmentRole.Status.PENDING_DELETE existing_env_role.role = "deleted" diff --git a/tests/domain/test_application_roles.py b/tests/domain/test_application_roles.py index a91de2c1..32c5cd2e 100644 --- a/tests/domain/test_application_roles.py +++ b/tests/domain/test_application_roles.py @@ -83,5 +83,6 @@ def test_disable(session): ApplicationRoles.disable(member_role) session.refresh(member_role) + session.refresh(environment_role) assert member_role.status == ApplicationRoleStatus.DISABLED - assert not EnvironmentRoles.get_by_user_and_environment(user.id, environment.id) + assert environment_role.status == EnvironmentRole.Status.PENDING_DELETE diff --git a/tests/domain/test_environments.py b/tests/domain/test_environments.py index a14f3bbe..dc92d9e0 100644 --- a/tests/domain/test_environments.py +++ b/tests/domain/test_environments.py @@ -5,7 +5,7 @@ from uuid import uuid4 from atst.domain.environments import Environments from atst.domain.environment_roles import EnvironmentRoles from atst.domain.exceptions import NotFoundError -from atst.models.environment_role import CSPRole +from atst.models.environment_role import CSPRole, EnvironmentRole from tests.factories import ( ApplicationFactory, @@ -35,15 +35,15 @@ def test_update_env_role(): assert env_role.role == new_role -def test_update_env_role_no_access(): +def test_update_env_role_no_access(session): env_role = EnvironmentRoleFactory.create(role=CSPRole.BASIC_ACCESS.value) assert Environments.update_env_role( env_role.environment, env_role.application_role, None ) - assert not EnvironmentRoles.get( - env_role.application_role.id, env_role.environment.id - ) + + session.refresh(env_role) + assert env_role.status == EnvironmentRole.Status.PENDING_DELETE def test_update_env_role_no_change(): diff --git a/tests/routes/applications/test_settings.py b/tests/routes/applications/test_settings.py index e477da14..62bfd34a 100644 --- a/tests/routes/applications/test_settings.py +++ b/tests/routes/applications/test_settings.py @@ -1,4 +1,3 @@ -import pytest import uuid from flask import url_for, get_flashed_messages from unittest.mock import Mock @@ -9,16 +8,10 @@ from tests.factories import * from atst.domain.applications import Applications from atst.domain.application_roles import ApplicationRoles from atst.domain.environment_roles import EnvironmentRoles -from atst.domain.environments import Environments -from atst.domain.environment_roles import EnvironmentRoles from atst.domain.common import Paginator from atst.domain.permission_sets import PermissionSets -from atst.domain.portfolios import Portfolios -from atst.domain.exceptions import NotFoundError -from atst.models.application_role import Status as ApplicationRoleStatus from atst.models.environment_role import CSPRole from atst.models.permissions import Permissions -from atst.models.portfolio_role import Status as PortfolioRoleStatus from atst.forms.application import EditEnvironmentForm from atst.forms.application_member import UpdateMemberForm from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS @@ -483,7 +476,7 @@ def test_remove_member_failure(client, user_session): assert response.status_code == 404 -def test_update_member(client, user_session): +def test_update_member(client, user_session, session): role = PermissionSets.get(PermissionSets.EDIT_APPLICATION_TEAM) # create an app role with only edit team perms app_role = ApplicationRoleFactory.create(permission_sets=[role]) @@ -544,13 +537,6 @@ def test_update_member(client, user_session): app_role.has_permission_set(PermissionSets.DELETE_APPLICATION_ENVIRONMENTS) ) - environment_roles = application.roles[0].environment_roles - # make sure that old env role was deleted and there are only 2 env roles - assert len(environment_roles) == 2 - # check that the user has roles in the correct envs - assert environment_roles[0].environment in [env, env_2] - assert environment_roles[1].environment in [env, env_2] - def test_revoke_invite(client, user_session): invite = ApplicationInvitationFactory.create() From 41b09b9cc8d992c990f574ec501d7acfd32701c4 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 8 Oct 2019 16:34:22 -0400 Subject: [PATCH 09/11] Fix migration path --- .../1497926ddec1_add_deleted_environment_role_status.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alembic/versions/1497926ddec1_add_deleted_environment_role_status.py b/alembic/versions/1497926ddec1_add_deleted_environment_role_status.py index e32f6a66..50fa7ee4 100644 --- a/alembic/versions/1497926ddec1_add_deleted_environment_role_status.py +++ b/alembic/versions/1497926ddec1_add_deleted_environment_role_status.py @@ -11,7 +11,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. revision = "1497926ddec1" # pragma: allowlist secret -down_revision = "e3d93f9caba7" # pragma: allowlist secret +down_revision = "f50596c5ffbb" # pragma: allowlist secret branch_labels = None depends_on = None From 30f5f0292fdee7c7253eb1496ab923b278ea15e2 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 8 Oct 2019 16:34:29 -0400 Subject: [PATCH 10/11] Update js templates --- js/test_templates/clin_fields.html | 7 ++-- js/test_templates/pop_date_range.html | 60 +++++++++++++-------------- js/test_templates/to_form.html | 4 +- 3 files changed, 36 insertions(+), 35 deletions(-) diff --git a/js/test_templates/clin_fields.html b/js/test_templates/clin_fields.html index abcd43f3..f0f51fc4 100644 --- a/js/test_templates/clin_fields.html +++ b/js/test_templates/clin_fields.html @@ -312,6 +312,8 @@ + + -