soft deletes available for applications and environments

- parent relation will not include applications or environments marked
  as deleted
- domain classes will exclude deleted objects from selections
- changed some test factories to use domain_word for resource names,
  because they were using person names and it bugged me
This commit is contained in:
dandds 2019-04-09 06:35:50 -04:00
parent c2a76c2504
commit 1c0c5dd9c5
11 changed files with 111 additions and 12 deletions

View File

@ -0,0 +1,31 @@
"""add soft delete to applications and environments
Revision ID: fd0cf917f682
Revises: 32438a35cfb5
Create Date: 2019-04-09 06:16:15.445951
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.sql import expression
# revision identifiers, used by Alembic.
revision = 'fd0cf917f682'
down_revision = '32438a35cfb5'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('applications', sa.Column('deleted', sa.Boolean(), nullable=False, server_default=expression.false()))
op.add_column('environments', sa.Column('deleted', sa.Boolean(), nullable=False, server_default=expression.false()))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('environments', 'deleted')
op.drop_column('applications', 'deleted')
# ### end Alembic commands ###

View File

@ -1,3 +1,5 @@
from sqlalchemy.orm.exc import NoResultFound
from atst.database import db from atst.database import db
from atst.domain.environments import Environments from atst.domain.environments import Environments
from atst.domain.exceptions import NotFoundError from atst.domain.exceptions import NotFoundError
@ -23,7 +25,9 @@ class Applications(object):
def get(cls, application_id): def get(cls, application_id):
try: try:
application = ( application = (
db.session.query(Application).filter_by(id=application_id).one() db.session.query(Application)
.filter_by(id=application_id, deleted=False)
.one()
) )
except NoResultFound: except NoResultFound:
raise NotFoundError("application") raise NotFoundError("application")

View File

@ -51,7 +51,11 @@ class Environments(object):
@classmethod @classmethod
def get(cls, environment_id): def get(cls, environment_id):
try: try:
env = db.session.query(Environment).filter_by(id=environment_id).one() env = (
db.session.query(Environment)
.filter_by(id=environment_id, deleted=False)
.one()
)
except NoResultFound: except NoResultFound:
raise NotFoundError("environment") raise NotFoundError("environment")

View File

@ -1,4 +1,4 @@
from sqlalchemy import Column, ForeignKey, String from sqlalchemy import Column, ForeignKey, String, Boolean
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from atst.models import Base from atst.models import Base
@ -15,9 +15,15 @@ class Application(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
portfolio_id = Column(ForeignKey("portfolios.id"), nullable=False) portfolio_id = Column(ForeignKey("portfolios.id"), nullable=False)
portfolio = relationship("Portfolio") portfolio = relationship("Portfolio")
environments = relationship("Environment", back_populates="application") environments = relationship(
"Environment",
back_populates="application",
primaryjoin="and_(Environment.application_id==Application.id, Environment.deleted==False)",
)
roles = relationship("ApplicationRole") roles = relationship("ApplicationRole")
deleted = Column(Boolean, default=False)
@property @property
def users(self): def users(self):
return set(role.user for role in self.roles) return set(role.user for role in self.roles)

View File

@ -1,4 +1,4 @@
from sqlalchemy import Column, ForeignKey, String from sqlalchemy import Column, ForeignKey, String, Boolean
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from atst.models import Base from atst.models import Base
@ -17,6 +17,8 @@ class Environment(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
cloud_id = Column(String) cloud_id = Column(String)
deleted = Column(Boolean, default=False)
@property @property
def users(self): def users(self):
return [r.user for r in self.roles] return [r.user for r in self.roles]

View File

@ -16,7 +16,11 @@ class Portfolio(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
name = Column(String) name = Column(String)
defense_component = Column(String) # Department of Defense Component defense_component = Column(String) # Department of Defense Component
applications = relationship("Application", back_populates="portfolio") applications = relationship(
"Application",
back_populates="portfolio",
primaryjoin="and_(Application.portfolio_id==Portfolio.id, Application.deleted==False)",
)
roles = relationship("PortfolioRole") roles = relationship("PortfolioRole")
task_orders = relationship("TaskOrder") task_orders = relationship("TaskOrder")

View File

@ -1,6 +1,9 @@
import pytest
from atst.domain.applications import Applications from atst.domain.applications import Applications
from tests.factories import UserFactory, PortfolioFactory from atst.domain.exceptions import NotFoundError
from atst.domain.portfolios import Portfolios
from tests.factories import ApplicationFactory, UserFactory, PortfolioFactory
def test_create_application_with_multiple_environments(): def test_create_application_with_multiple_environments():
@ -53,3 +56,9 @@ def test_can_only_update_name_and_description():
assert application.description == "a new application" assert application.description == "a new application"
assert len(application.environments) == 1 assert len(application.environments) == 1
assert application.environments[0].name == env_name assert application.environments[0].name == env_name
def test_get_excludes_deleted():
app = ApplicationFactory.create(deleted=True)
with pytest.raises(NotFoundError):
Applications.get(app.id)

View File

@ -1,8 +1,16 @@
import pytest
from atst.domain.environments import Environments from atst.domain.environments import Environments
from atst.domain.environment_roles import EnvironmentRoles from atst.domain.environment_roles import EnvironmentRoles
from atst.domain.portfolio_roles import PortfolioRoles from atst.domain.portfolio_roles import PortfolioRoles
from atst.domain.exceptions import NotFoundError
from tests.factories import ApplicationFactory, UserFactory, PortfolioFactory from tests.factories import (
ApplicationFactory,
UserFactory,
PortfolioFactory,
EnvironmentFactory,
)
def test_create_environments(): def test_create_environments():
@ -186,3 +194,11 @@ def test_get_scoped_environments(db):
application2_envs = Environments.for_user(developer, portfolio.applications[1]) application2_envs = Environments.for_user(developer, portfolio.applications[1])
assert [env.name for env in application2_envs] == ["application2 staging"] assert [env.name for env in application2_envs] == ["application2 staging"]
def test_get_excludes_deleted():
env = EnvironmentFactory.create(
deleted=True, application=ApplicationFactory.create()
)
with pytest.raises(NotFoundError):
Environments.get(env.id)

View File

@ -105,7 +105,7 @@ class PortfolioFactory(Base):
class Meta: class Meta:
model = Portfolio model = Portfolio
name = factory.Faker("name") name = factory.Faker("domain_word")
defense_component = factory.LazyFunction(random_service_branch) defense_component = factory.LazyFunction(random_service_branch)
@classmethod @classmethod
@ -157,7 +157,7 @@ class ApplicationFactory(Base):
model = Application model = Application
portfolio = factory.SubFactory(PortfolioFactory) portfolio = factory.SubFactory(PortfolioFactory)
name = factory.Faker("name") name = factory.Faker("domain_word")
description = "A test application" description = "A test application"
@classmethod @classmethod
@ -192,6 +192,8 @@ class EnvironmentFactory(Base):
class Meta: class Meta:
model = Environment model = Environment
name = factory.Faker("domain_word")
@classmethod @classmethod
def _create(cls, model_class, *args, **kwargs): def _create(cls, model_class, *args, **kwargs):
with_members = kwargs.pop("members", []) with_members = kwargs.pop("members", [])

View File

@ -1,4 +1,8 @@
from tests.factories import ApplicationFactory, ApplicationRoleFactory from tests.factories import (
ApplicationFactory,
ApplicationRoleFactory,
EnvironmentFactory,
)
def test_application_num_users(): def test_application_num_users():
@ -9,3 +13,11 @@ def test_application_num_users():
ApplicationRoleFactory.create(application=application) ApplicationRoleFactory.create(application=application)
assert application.num_users == 1 assert application.num_users == 1
def test_application_environments_excludes_deleted():
app = ApplicationFactory.create()
env = EnvironmentFactory.create(application=app)
EnvironmentFactory.create(application=app, deleted=True)
assert len(app.environments) == 1
assert app.environments[0].id == env.id

View File

@ -0,0 +1,9 @@
from tests.factories import ApplicationFactory, PortfolioFactory
def test_portfolio_applications_excludes_deleted():
portfolio = PortfolioFactory.create()
app = ApplicationFactory.create(portfolio=portfolio)
ApplicationFactory.create(portfolio=portfolio, deleted=True)
assert len(portfolio.applications) == 1
assert portfolio.applications[0].id == app.id