class CLIN(Base, mixins.TimestampsMixin): __tablename__ = "clins" id = types.Id() task_order_id = Column(ForeignKey("task_orders.id"), nullable=False) task_order = relationship("TaskOrder") number = Column(String, nullable=False) start_date = Column(Date, nullable=False) end_date = Column(Date, nullable=False) total_amount = Column(Numeric(scale=2), nullable=False) obligated_amount = Column(Numeric(scale=2), nullable=False) jedi_clin_type = Column(SQLAEnum(JEDICLINType, native_enum=False), nullable=False) # # NOTE: For now obligated CLINS are CLIN 1 + CLIN 3 # def is_obligated(self): return self.jedi_clin_type in [ JEDICLINType.JEDI_CLIN_1, JEDICLINType.JEDI_CLIN_3, ] @property def type(self): return "Base" if self.number[0] == "0" else "Option" @property def is_completed(self): return all([ self.number, self.start_date, self.end_date, self.total_amount, self.obligated_amount, self.jedi_clin_type, ]) def to_dictionary(self): return { c.name: getattr(self, c.name) for c in self.__table__.columns if c.name not in ["id"] } @property def is_active(self): return (self.start_date <= date.today() <= self.end_date) and self.task_order.signed_at
class Attachment(Base, mixins.TimestampsMixin): __tablename__ = "attachments" id = types.Id() filename = Column(String, nullable=False) object_name = Column(String, unique=True, nullable=False) resource = Column(String) resource_id = Column(UUID(as_uuid=True), index=True) @classmethod def get_or_create(cls, object_name, params): try: return db.session.query(Attachment).filter_by(object_name=object_name).one() except NoResultFound: new_attachment = cls(**params) db.session.add(new_attachment) db.session.commit() return new_attachment @classmethod def get(cls, id_): try: return db.session.query(Attachment).filter_by(id=id_).one() except NoResultFound: raise NotFoundError("attachment") @classmethod def get_for_resource(cls, resource, resource_id): try: return ( db.session.query(Attachment) .filter_by(resource=resource, resource_id=resource_id) .one() ) except NoResultFound: raise NotFoundError("attachment") @classmethod def delete_for_resource(cls, resource, resource_id): try: return ( db.session.query(Attachment) .filter_by(resource=resource, resource_id=resource_id) .update({"resource_id": None}) ) except NoResultFound: raise NotFoundError("attachment") def __repr__(self): return "<Attachment(name='{}', id='{}')>".format(self.filename, self.id)
class PermissionSet(Base, mixins.TimestampsMixin): __tablename__ = "permission_sets" id = types.Id() name = Column(String, index=True, unique=True, nullable=False) display_name = Column(String, nullable=False) description = Column(String, nullable=False) permissions = Column(ARRAY(String), index=True, server_default="{}", nullable=False) def __repr__(self): return "<PermissionSet(name='{}', description='{}', permissions='{}', id='{}')>".format( self.name, self.description, self.permissions, self.id)
class AuditEvent(Base, TimestampsMixin): __tablename__ = "audit_events" id = types.Id() user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), index=True) user = relationship("User", backref="audit_events") portfolio_id = Column(UUID(as_uuid=True), ForeignKey("portfolios.id"), index=True) portfolio = relationship("Portfolio", backref="audit_events") application_id = Column(UUID(as_uuid=True), ForeignKey("applications.id"), index=True) application = relationship("Application", backref="audit_events") changed_state = Column(JSONB()) event_details = Column(JSONB()) resource_type = Column(String(), nullable=False) resource_id = Column(UUID(as_uuid=True), index=True, nullable=False) display_name = Column(String()) action = Column(String(), nullable=False) @property def log(self): return { "portfolio_id": str(self.portfolio_id), "application_id": str(self.application_id), "changed_state": self.changed_state, "event_details": self.event_details, "resource_type": self.resource_type, "resource_id": str(self.resource_id), "display_name": self.display_name, "action": self.action, } def save(self, connection): attrs = inspect(self).dict connection.execute(self.__table__.insert(), **attrs) def __repr__(self): # pragma: no cover return "<AuditEvent(name='{}', action='{}', id='{}')>".format( self.display_name, self.action, self.id)
class User(Base, mixins.TimestampsMixin, mixins.AuditableMixin, mixins.PermissionsMixin): __tablename__ = "users" id = types.Id() username = Column(String) permission_sets = relationship("PermissionSet", secondary=users_permission_sets) portfolio_roles = relationship("PortfolioRole", backref="user") application_roles = relationship( "ApplicationRole", backref="user", primaryjoin= "and_(ApplicationRole.user_id == User.id, ApplicationRole.deleted == False)", ) portfolio_invitations = relationship( "PortfolioInvitation", foreign_keys=PortfolioInvitation.user_id) sent_portfolio_invitations = relationship( "PortfolioInvitation", foreign_keys=PortfolioInvitation.inviter_id) application_invitations = relationship( "ApplicationInvitation", foreign_keys=ApplicationInvitation.user_id) sent_application_invitations = relationship( "ApplicationInvitation", foreign_keys=ApplicationInvitation.inviter_id) email = Column(String) dod_id = Column(String, unique=True, nullable=False) first_name = Column(String, nullable=False) last_name = Column(String, nullable=False) phone_number = Column(String) phone_ext = Column(String) service_branch = Column(String) citizenship = Column(String) designation = Column(String) date_latest_training = Column(Date) last_login = Column(TIMESTAMP(timezone=True), nullable=True) last_session_id = Column(UUID(as_uuid=True), nullable=True) provisional = Column(Boolean) cloud_id = Column(String) REQUIRED_FIELDS = [ "email", "dod_id", "first_name", "last_name", "phone_number", "service_branch", "citizenship", "designation", "date_latest_training", ] @property def profile_complete(self): return all([ getattr(self, field_name) is not None for field_name in self.REQUIRED_FIELDS ]) @property def full_name(self): return "{} {}".format(self.first_name, self.last_name) @property def displayname(self): return self.full_name @property def portfolio_id(self): return None @property def application_id(self): return None def __repr__(self): return "<User(name='{}', dod_id='{}', email='{}', id='{}')>".format( self.full_name, self.dod_id, self.email, self.id) def to_dictionary(self): return { c.name: getattr(self, c.name) for c in self.__table__.columns if c.name not in ["id"] } @staticmethod def audit_update(mapper, connection, target): changes = AuditableMixin.get_changes(target) if changes and not "last_login" in changes: target.create_audit_event(connection, target, ACTION_UPDATE)
class EnvironmentRole( Base, mixins.TimestampsMixin, mixins.AuditableMixin, mixins.DeletableMixin ): __tablename__ = "environment_roles" id = types.Id() environment_id = Column( UUID(as_uuid=True), ForeignKey("environments.id"), nullable=False ) environment = relationship("Environment", backref="roles") role = Column(String()) application_role_id = Column( UUID(as_uuid=True), ForeignKey("application_roles.id"), nullable=False ) application_role = relationship("ApplicationRole") job_failures = relationship("EnvironmentRoleJobFailure") csp_user_id = Column(String()) claimed_until = Column(TIMESTAMP(timezone=True)) class Status(Enum): PENDING = "pending" COMPLETED = "completed" PENDING_DELETE = "pending_delete" status = Column(SQLAEnum(Status, native_enum=False), default=Status.PENDING) def __repr__(self): return "<EnvironmentRole(role='{}', user='******', environment='{}', id='{}')>".format( self.role, self.application_role.user_name, self.environment.name, self.id ) @property def history(self): return self.get_changes() @property def portfolio_id(self): return self.environment.application.portfolio_id @property def application_id(self): return self.environment.application_id @property def displayname(self): return self.role @property def event_details(self): return { "updated_user_name": self.application_role.user_name, "updated_application_role_id": str(self.application_role_id), "role": self.role, "environment": self.environment.displayname, "environment_id": str(self.environment_id), "application": self.environment.application.name, "application_id": str(self.environment.application_id), "portfolio": self.environment.application.portfolio.name, "portfolio_id": str(self.environment.application.portfolio.id), }
class NotificationRecipient(Base, mixins.TimestampsMixin): __tablename__ = "notification_recipients" id = types.Id() email = Column(String, nullable=False)
class TaskOrder(Base, mixins.TimestampsMixin): __tablename__ = "task_orders" id = types.Id() portfolio_id = Column(ForeignKey("portfolios.id"), nullable=False) portfolio = relationship("Portfolio") pdf_attachment_id = Column(ForeignKey("attachments.id")) _pdf = relationship("Attachment", foreign_keys=[pdf_attachment_id]) number = Column(String, unique=True,) # Task Order Number signer_dod_id = Column(String) signed_at = Column(DateTime) clins = relationship( "CLIN", back_populates="task_order", cascade="all, delete-orphan" ) @property def sorted_clins(self): return sorted(self.clins, key=lambda clin: (clin.number[1:], clin.number[0])) @hybrid_property def pdf(self): return self._pdf @pdf.setter def pdf(self, new_pdf): self._pdf = self._set_attachment(new_pdf, "_pdf") def _set_attachment(self, new_attachment, attribute): if isinstance(new_attachment, Attachment): return new_attachment elif isinstance(new_attachment, dict): if new_attachment["filename"] and new_attachment["object_name"]: attachment = Attachment.get_or_create( new_attachment["object_name"], new_attachment ) return attachment else: return None elif not new_attachment and hasattr(self, attribute): return None else: raise TypeError("Could not set attachment with invalid type") @property def is_draft(self): return self.status == Status.DRAFT @property def is_active(self): return self.status == Status.ACTIVE @property def is_expired(self): return self.status == Status.EXPIRED @property def clins_are_completed(self): return all([len(self.clins), (clin.is_completed for clin in self.clins)]) @property def is_completed(self): return all([self.pdf, self.number, self.clins_are_completed]) @property def is_signed(self): return self.signed_at is not None @property def status(self): todays_date = today(tz="UTC").date() if not self.is_completed and not self.is_signed: return Status.DRAFT elif self.is_completed and not self.is_signed: return Status.UNSIGNED elif todays_date < self.start_date: return Status.UPCOMING elif todays_date > self.end_date: return Status.EXPIRED elif self.start_date <= todays_date <= self.end_date: return Status.ACTIVE @property def start_date(self): return min((c.start_date for c in self.clins), default=None) @property def end_date(self): return max((c.end_date for c in self.clins), default=None) @property def days_to_expiration(self): if self.end_date: return (self.end_date - today(tz="UTC").date()).days @property def total_obligated_funds(self): return sum( (clin.obligated_amount for clin in self.clins if clin.obligated_amount) ) @property def total_contract_amount(self): return sum((clin.total_amount for clin in self.clins if clin.total_amount)) @property def invoiced_funds(self): # TODO: implement this using reporting data from the CSP if self.is_active: return self.total_obligated_funds * Decimal(0.75) else: return 0 @property def display_status(self): return self.status.value @property def portfolio_name(self): return self.portfolio.name def to_dictionary(self): return { "portfolio_name": self.portfolio_name, "pdf": self.pdf, "clins": [clin.to_dictionary() for clin in self.clins], **{ c.name: getattr(self, c.name) for c in self.__table__.columns if c.name not in ["id"] }, } def __repr__(self): return "<TaskOrder(number='{}', id='{}')>".format(self.number, self.id)
class Portfolio(Base, mixins.TimestampsMixin, mixins.AuditableMixin, mixins.DeletableMixin): __tablename__ = "portfolios" id = types.Id() name = Column(String) defense_component = Column(String) # Department of Defense Component app_migration = Column(String) # App Migration complexity = Column(ARRAY(String)) # Application Complexity complexity_other = Column(String) description = Column(String) dev_team = Column(ARRAY(String)) # Development Team dev_team_other = Column(String) native_apps = Column(String) # Native Apps team_experience = Column(String) # Team Experience applications = relationship( "Application", back_populates="portfolio", primaryjoin= "and_(Application.portfolio_id == Portfolio.id, Application.deleted == False)", ) roles = relationship("PortfolioRole") task_orders = relationship("TaskOrder") @property def owner_role(self): def _is_portfolio_owner(portfolio_role): return PermissionSets.PORTFOLIO_POC in [ perms_set.name for perms_set in portfolio_role.permission_sets ] return first_or_none(_is_portfolio_owner, self.roles) @property def owner(self): owner_role = self.owner_role return owner_role.user if owner_role else None @property def users(self): return set(role.user for role in self.roles) @property def user_count(self): return len(self.members) @property def num_task_orders(self): return len(self.task_orders) @property def active_clins(self): return [ clin for task_order in self.task_orders for clin in task_order.clins if clin.is_active ] @property def active_task_orders(self): return [ task_order for task_order in self.task_orders if task_order.is_active ] @property def funding_duration(self): """ Return the earliest period of performance start date and latest period of performance end date for all active task orders in a portfolio. @return: (datetime.date or None, datetime.date or None) """ start_dates = (task_order.start_date for task_order in self.task_orders if task_order.is_active) end_dates = (task_order.end_date for task_order in self.task_orders if task_order.is_active) earliest_pop_start_date = min(start_dates, default=None) latest_pop_end_date = max(end_dates, default=None) return (earliest_pop_start_date, latest_pop_end_date) @property def days_to_funding_expiration(self): """ Returns the number of days between today and the lastest period performance end date of all active Task Orders """ return max( (task_order.days_to_expiration for task_order in self.task_orders if task_order.is_active), default=0, ) @property def members(self): return (db.session.query(PortfolioRole).filter( PortfolioRole.portfolio_id == self.id).filter( PortfolioRole.status != PortfolioRoleStatus.DISABLED).all()) @property def displayname(self): return self.name @property def all_environments(self): return list( chain.from_iterable(p.environments for p in self.applications)) @property def portfolio_id(self): return self.id @property def application_id(self): return None def __repr__(self): return "<Portfolio(name='{}', user_count='{}', id='{}')>".format( self.name, self.user_count, self.id)
class TaskOrder(Base, mixins.TimestampsMixin): __tablename__ = "task_orders" id = types.Id() portfolio_id = Column(ForeignKey("portfolios.id")) portfolio = relationship("Portfolio") user_id = Column(ForeignKey("users.id")) creator = relationship("User", foreign_keys="TaskOrder.user_id") pdf_attachment_id = Column(ForeignKey("attachments.id")) _pdf = relationship("Attachment", foreign_keys=[pdf_attachment_id]) number = Column(String) # Task Order Number signer_dod_id = Column(String) signed_at = Column(DateTime) clins = relationship("CLIN", back_populates="task_order", cascade="all, delete-orphan") @property def sorted_clins(self): return sorted(self.clins, key=lambda clin: (clin.number[1:], clin.number[0])) @hybrid_property def pdf(self): return self._pdf @pdf.setter def pdf(self, new_pdf): self._pdf = self._set_attachment(new_pdf, "_pdf") def _set_attachment(self, new_attachment, attribute): if isinstance(new_attachment, Attachment): return new_attachment elif isinstance(new_attachment, dict): if new_attachment["filename"] and new_attachment["object_name"]: attachment = Attachment.get_or_create( new_attachment["object_name"], new_attachment) return attachment else: return None elif not new_attachment and hasattr(self, attribute): return None else: raise TypeError("Could not set attachment with invalid type") @property def is_draft(self): return self.status == Status.DRAFT @property def is_active(self): return self.status == Status.ACTIVE @property def is_upcoming(self): return self.status == Status.UPCOMING @property def is_expired(self): return self.status == Status.EXPIRED @property def is_unsigned(self): return self.status == Status.UNSIGNED @property def has_begun(self): return self.start_date is not None and Clock.today() >= self.start_date @property def has_ended(self): return self.start_date is not None and Clock.today() >= self.end_date @property def clins_are_completed(self): return all( [len(self.clins), (clin.is_completed for clin in self.clins)]) @property def is_completed(self): return all([self.pdf, self.number, self.clins_are_completed]) @property def is_signed(self): return self.signed_at is not None @property def status(self): today = Clock.today() if not self.is_completed and not self.is_signed: return Status.DRAFT elif self.is_completed and not self.is_signed: return Status.UNSIGNED elif today < self.start_date: return Status.UPCOMING elif today >= self.end_date: return Status.EXPIRED elif self.start_date <= today < self.end_date: return Status.ACTIVE @property def start_date(self): return min((c.start_date for c in self.clins), default=self.time_created.date()) @property def end_date(self): default_end_date = self.start_date + timedelta(days=1) return max((c.end_date for c in self.clins), default=default_end_date) @property def days_to_expiration(self): if self.end_date: return (self.end_date - Clock.today()).days @property def total_obligated_funds(self): total = 0 for clin in self.clins: if clin.obligated_amount is not None: total += clin.obligated_amount return total @property def total_contract_amount(self): total = 0 for clin in self.clins: if clin.total_amount is not None: total += clin.total_amount return total @property # TODO delete when we delete task_order_review flow def budget(self): return 100000 @property def balance(self): # TODO: fix task order -- reimplement using CLINs # Faked for display purposes return 50 @property def display_status(self): return self.status.value @property def portfolio_name(self): return self.portfolio.name def to_dictionary(self): return { "portfolio_name": self.portfolio_name, "pdf": self.pdf, "clins": [clin.to_dictionary() for clin in self.clins], **{ c.name: getattr(self, c.name) for c in self.__table__.columns if c.name not in ["id"] }, } def __repr__(self): return "<TaskOrder(number='{}', id='{}')>".format(self.number, self.id)
class Portfolio( Base, mixins.TimestampsMixin, mixins.AuditableMixin, mixins.DeletableMixin ): __tablename__ = "portfolios" id = types.Id() name = Column(String) defense_component = Column(String) # Department of Defense Component app_migration = Column(String) # App Migration complexity = Column(ARRAY(String)) # Application Complexity complexity_other = Column(String) description = Column(String) dev_team = Column(ARRAY(String)) # Development Team dev_team_other = Column(String) native_apps = Column(String) # Native Apps team_experience = Column(String) # Team Experience applications = relationship( "Application", back_populates="portfolio", primaryjoin=and_(Application.portfolio_id == id, Application.deleted == False), ) roles = relationship("PortfolioRole") task_orders = relationship("TaskOrder") @property def owner_role(self): def _is_portfolio_owner(portfolio_role): return PermissionSets.PORTFOLIO_POC in [ perms_set.name for perms_set in portfolio_role.permission_sets ] return first_or_none(_is_portfolio_owner, self.roles) @property def owner(self): owner_role = self.owner_role return owner_role.user if owner_role else None @property def users(self): return set(role.user for role in self.roles) @property def user_count(self): return len(self.members) @property def num_task_orders(self): return len(self.task_orders) @property def members(self): return ( db.session.query(PortfolioRole) .filter(PortfolioRole.portfolio_id == self.id) .filter(PortfolioRole.status != PortfolioRoleStatus.DISABLED) .all() ) @property def displayname(self): return self.name @property def all_environments(self): return list(chain.from_iterable(p.environments for p in self.applications)) @property def portfolio_id(self): return self.id @property def application_id(self): return None def __repr__(self): return "<Portfolio(name='{}', user_count='{}', id='{}')>".format( self.name, self.user_count, self.id )
class InvitesMixin(object): id = types.Id() @declared_attr def user_id(cls): return Column(UUID(as_uuid=True), ForeignKey("users.id"), index=True) @declared_attr def user(cls): return relationship("User", foreign_keys=[cls.user_id]) @declared_attr def inviter_id(cls): return Column(UUID(as_uuid=True), ForeignKey("users.id"), index=True, nullable=False) @declared_attr def inviter(cls): return relationship("User", foreign_keys=[cls.inviter_id]) status = Column( SQLAEnum(Status, native_enum=False, default=Status.PENDING, nullable=False)) expiration_time = Column(TIMESTAMP(timezone=True), nullable=False) token = Column(String, index=True, default=lambda: secrets.token_urlsafe(), nullable=False) email = Column(String, nullable=False) dod_id = Column(String, nullable=False) first_name = Column(String, nullable=False) last_name = Column(String, nullable=False) phone_number = Column(String) phone_ext = Column(String) def __repr__(self): role_id = self.role.id if self.role else None return "<{}(user='******', role='{}', id='{}', email='{}')>".format( self.__class__.__name__, self.user_id, role_id, self.id, self.email) @property def is_accepted(self): return self.status == Status.ACCEPTED @property def is_revoked(self): return self.status == Status.REVOKED @property def is_pending(self): return self.status == Status.PENDING @property def is_rejected(self): return self.status in [ Status.REJECTED_WRONG_USER, Status.REJECTED_EXPIRED ] @property def is_rejected_expired(self): return self.status == Status.REJECTED_EXPIRED @property def is_rejected_wrong_user(self): return self.status == Status.REJECTED_WRONG_USER @property def is_expired(self): return (datetime.datetime.now(self.expiration_time.tzinfo) > self.expiration_time and not self.status == Status.ACCEPTED) @property def is_inactive(self): return self.is_expired or self.status in [ Status.REJECTED_WRONG_USER, Status.REJECTED_EXPIRED, Status.REVOKED, ] @property def user_name(self): return "{} {}".format(self.first_name, self.last_name) @property def is_revokable(self): return self.is_pending and not self.is_expired @property def can_resend(self): return self.is_pending or self.is_expired @property def user_dod_id(self): return self.user.dod_id if self.user is not None else None @property def event_details(self): """Overrides the same property in AuditableMixin. Provides the event details for an invite that are required for the audit log """ return {"email": self.email, "dod_id": self.user_dod_id} @property def history(self): """Overrides the same property in AuditableMixin Determines whether or not invite status has been updated """ changes = self.get_changes() change_set = {} if "status" in changes: change_set["status"] = [s.name for s in changes["status"]] return change_set
class ApplicationRole( Base, mixins.TimestampsMixin, mixins.AuditableMixin, mixins.PermissionsMixin, mixins.DeletableMixin, ): __tablename__ = "application_roles" id = types.Id() application_id = Column( UUID(as_uuid=True), ForeignKey("applications.id"), index=True, nullable=False ) application = relationship("Application", back_populates="roles") user_id = Column( UUID(as_uuid=True), ForeignKey("users.id"), index=True, nullable=True ) status = Column( SQLAEnum(Status, native_enum=False), default=Status.PENDING, nullable=False ) permission_sets = relationship( "PermissionSet", secondary=application_roles_permission_sets ) environment_roles = relationship( "EnvironmentRole", primaryjoin="and_(EnvironmentRole.application_role_id == ApplicationRole.id, EnvironmentRole.deleted == False)", ) @property def latest_invitation(self): if self.invitations: return self.invitations[-1] @property def user_name(self): if self.user: return self.user.full_name elif self.latest_invitation: return self.latest_invitation.user_name def __repr__(self): return "<ApplicationRole(application='{}', user_id='{}', id='{}', permissions={})>".format( self.application.name, self.user_id, self.id, self.permissions ) @property def history(self): previous_state = self.get_changes() change_set = {} if "status" in previous_state: from_status = previous_state["status"][0].value to_status = self.status.value change_set["status"] = [from_status, to_status] return change_set def has_permission_set(self, perm_set_name): return first_or_none( lambda prms: prms.name == perm_set_name, self.permission_sets ) @property def portfolio_id(self): return self.application.portfolio_id @property def event_details(self): return { "updated_user_name": self.user_name, "updated_user_id": str(self.user_id), "application": self.application.name, "portfolio": self.application.portfolio.name, } @property def is_pending(self): return self.status == Status.PENDING @property def is_active(self): return self.status == Status.ACTIVE @property def display_status(self): if ( self.is_pending and self.latest_invitation and self.latest_invitation.is_expired ): return "invite_expired" elif ( self.is_pending and self.latest_invitation and self.latest_invitation.is_pending ): return "invite_pending" elif self.is_active and any( env_role.is_pending for env_role in self.environment_roles ): return "changes_pending" return None
class PortfolioRole(Base, mixins.TimestampsMixin, mixins.AuditableMixin, mixins.PermissionsMixin): __tablename__ = "portfolio_roles" id = types.Id() portfolio_id = Column(UUID(as_uuid=True), ForeignKey("portfolios.id"), index=True, nullable=False) portfolio = relationship("Portfolio", back_populates="roles") user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), index=True, nullable=True) status = Column(SQLAEnum(Status, native_enum=False), default=Status.PENDING, nullable=False) permission_sets = relationship("PermissionSet", secondary=portfolio_roles_permission_sets) def __repr__(self): return "<PortfolioRole(portfolio='{}', user_id='{}', id='{}', permissions={})>".format( self.portfolio.name, self.user_id, self.id, self.permissions) @property def history(self): previous_state = self.get_changes() change_set = {} if "status" in previous_state: from_status = previous_state["status"][0].value to_status = self.status.value change_set["status"] = [from_status, to_status] return change_set @property def event_details(self): return { "updated_user_name": self.user_name, "updated_user_id": str(self.user_id), } @property def latest_invitation(self): if self.invitations: return self.invitations[-1] @property def display_status(self): if self.status == Status.ACTIVE: return "active" elif self.status == Status.DISABLED: return "disabled" elif self.latest_invitation: if self.latest_invitation.is_revoked: return "invite_revoked" elif self.latest_invitation.is_rejected_wrong_user: return "invite_error" elif (self.latest_invitation.is_rejected_expired or self.latest_invitation.is_expired): return "invite_expired" else: return "invite_pending" else: return "unknown" def has_permission_set(self, perm_set_name): return first_or_none(lambda prms: prms.name == perm_set_name, self.permission_sets) @property def has_dod_id_error(self): return self.latest_invitation and self.latest_invitation.is_rejected_wrong_user @property def user_name(self): if self.user: return self.user.full_name else: return self.latest_invitation.user_name @property def full_name(self): return self.user_name @property def is_active(self): return self.status == Status.ACTIVE @property def can_resend_invitation(self): return not self.is_active and (self.latest_invitation and self.latest_invitation.is_inactive) @property def application_id(self): return None
class Environment(Base, mixins.TimestampsMixin, mixins.AuditableMixin, mixins.DeletableMixin): __tablename__ = "environments" id = types.Id() name = Column(String, nullable=False) application_id = Column(ForeignKey("applications.id"), nullable=False) application = relationship("Application") # User user.id as the foreign key here beacuse the Environment creator may # not have an application role. We may need to revisit this if we receive any # requirements around tracking an environment's custodian. creator_id = Column(ForeignKey("users.id"), nullable=False) creator = relationship("User") cloud_id = Column(String) root_user_info = Column(JSONB(none_as_null=True)) claimed_until = Column(TIMESTAMP(timezone=True)) job_failures = relationship("EnvironmentJobFailure") roles = relationship( "EnvironmentRole", back_populates="environment", primaryjoin= "and_(EnvironmentRole.environment_id == Environment.id, EnvironmentRole.deleted == False)", ) class ProvisioningStatus(Enum): PENDING = "pending" COMPLETED = "completed" @property def users(self): return {r.application_role.user for r in self.roles} @property def num_users(self): return len(self.users) @property def displayname(self): return self.name @property def portfolio(self): return self.application.portfolio @property def portfolio_id(self): return self.application.portfolio_id @property def provisioning_status(self) -> ProvisioningStatus: if self.cloud_id is None or self.root_user_info is None: return self.ProvisioningStatus.PENDING else: return self.ProvisioningStatus.COMPLETED @property def is_pending(self): return self.provisioning_status == self.ProvisioningStatus.PENDING def __repr__(self): return "<Environment(name='{}', num_users='{}', application='{}', portfolio='{}', id='{}')>".format( self.name, self.num_users, self.application.name, self.application.portfolio.name, self.id, ) @property def history(self): return self.get_changes() @property def csp_credentials(self): return (self.root_user_info.get("credentials") if self.root_user_info is not None else None)