Merge branch 'staging' into revoke-portfolio-invite-success-message
This commit is contained in:
commit
e66605ff4a
@ -24,6 +24,7 @@ from .models import (
|
|||||||
BillingProfileTenantAccessCSPResult,
|
BillingProfileTenantAccessCSPResult,
|
||||||
BillingProfileVerificationCSPPayload,
|
BillingProfileVerificationCSPPayload,
|
||||||
BillingProfileVerificationCSPResult,
|
BillingProfileVerificationCSPResult,
|
||||||
|
CostManagementQueryCSPResult,
|
||||||
KeyVaultCredentials,
|
KeyVaultCredentials,
|
||||||
ManagementGroupCSPResponse,
|
ManagementGroupCSPResponse,
|
||||||
ProductPurchaseCSPPayload,
|
ProductPurchaseCSPPayload,
|
||||||
@ -32,6 +33,7 @@ from .models import (
|
|||||||
ProductPurchaseVerificationCSPResult,
|
ProductPurchaseVerificationCSPResult,
|
||||||
PrincipalAdminRoleCSPPayload,
|
PrincipalAdminRoleCSPPayload,
|
||||||
PrincipalAdminRoleCSPResult,
|
PrincipalAdminRoleCSPResult,
|
||||||
|
ReportingCSPPayload,
|
||||||
TaskOrderBillingCreationCSPPayload,
|
TaskOrderBillingCreationCSPPayload,
|
||||||
TaskOrderBillingCreationCSPResult,
|
TaskOrderBillingCreationCSPResult,
|
||||||
TaskOrderBillingVerificationCSPPayload,
|
TaskOrderBillingVerificationCSPPayload,
|
||||||
@ -1070,3 +1072,41 @@ class AzureCloudProvider(CloudProviderInterface):
|
|||||||
hashed = sha256_hex(tenant_id)
|
hashed = sha256_hex(tenant_id)
|
||||||
raw_creds = self.get_secret(hashed)
|
raw_creds = self.get_secret(hashed)
|
||||||
return KeyVaultCredentials(**json.loads(raw_creds))
|
return KeyVaultCredentials(**json.loads(raw_creds))
|
||||||
|
|
||||||
|
def get_reporting_data(self, payload: ReportingCSPPayload):
|
||||||
|
"""
|
||||||
|
Queries the Cost Management API for an invoice section's raw reporting data
|
||||||
|
|
||||||
|
We query at the invoiceSection scope. The full scope path is passed in
|
||||||
|
with the payload at the `invoice_section_id` key.
|
||||||
|
"""
|
||||||
|
creds = self._source_tenant_creds(payload.tenant_id)
|
||||||
|
token = self._get_sp_token(
|
||||||
|
payload.tenant_id, creds.tenant_sp_client_id, creds.tenant_sp_key
|
||||||
|
)
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
raise AuthenticationException("Could not retrieve tenant access token")
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
|
||||||
|
request_body = {
|
||||||
|
"type": "Usage",
|
||||||
|
"timeframe": "Custom",
|
||||||
|
"timePeriod": {"from": payload.from_date, "to": payload.to_date,},
|
||||||
|
"dataset": {
|
||||||
|
"granularity": "Daily",
|
||||||
|
"aggregation": {"totalCost": {"name": "PreTaxCost", "function": "Sum"}},
|
||||||
|
"grouping": [{"type": "Dimension", "name": "InvoiceId"}],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cost_mgmt_url = (
|
||||||
|
f"/providers/Microsoft.CostManagement/query?api-version=2019-11-01"
|
||||||
|
)
|
||||||
|
result = self.sdk.requests.post(
|
||||||
|
f"{self.sdk.cloud.endpoints.resource_manager}{payload.invoice_section_id}{cost_mgmt_url}",
|
||||||
|
json=request_body,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
if result.ok:
|
||||||
|
return CostManagementQueryCSPResult(**result.json())
|
||||||
|
@ -25,12 +25,15 @@ from .models import (
|
|||||||
BillingProfileTenantAccessCSPResult,
|
BillingProfileTenantAccessCSPResult,
|
||||||
BillingProfileVerificationCSPPayload,
|
BillingProfileVerificationCSPPayload,
|
||||||
BillingProfileVerificationCSPResult,
|
BillingProfileVerificationCSPResult,
|
||||||
|
CostManagementQueryCSPResult,
|
||||||
|
CostManagementQueryProperties,
|
||||||
ProductPurchaseCSPPayload,
|
ProductPurchaseCSPPayload,
|
||||||
ProductPurchaseCSPResult,
|
ProductPurchaseCSPResult,
|
||||||
ProductPurchaseVerificationCSPPayload,
|
ProductPurchaseVerificationCSPPayload,
|
||||||
ProductPurchaseVerificationCSPResult,
|
ProductPurchaseVerificationCSPResult,
|
||||||
PrincipalAdminRoleCSPPayload,
|
PrincipalAdminRoleCSPPayload,
|
||||||
PrincipalAdminRoleCSPResult,
|
PrincipalAdminRoleCSPResult,
|
||||||
|
ReportingCSPPayload,
|
||||||
SubscriptionCreationCSPPayload,
|
SubscriptionCreationCSPPayload,
|
||||||
SubscriptionCreationCSPResult,
|
SubscriptionCreationCSPResult,
|
||||||
SubscriptionVerificationCSPPayload,
|
SubscriptionVerificationCSPPayload,
|
||||||
@ -487,3 +490,25 @@ class MockCloudProvider(CloudProviderInterface):
|
|||||||
|
|
||||||
def update_tenant_creds(self, tenant_id, secret):
|
def update_tenant_creds(self, tenant_id, secret):
|
||||||
return secret
|
return secret
|
||||||
|
|
||||||
|
def get_reporting_data(self, payload: ReportingCSPPayload):
|
||||||
|
self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION)
|
||||||
|
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
|
||||||
|
self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION)
|
||||||
|
object_id = str(uuid4())
|
||||||
|
|
||||||
|
properties = CostManagementQueryProperties(
|
||||||
|
**dict(
|
||||||
|
columns=[
|
||||||
|
{"name": "PreTaxCost", "type": "Number"},
|
||||||
|
{"name": "UsageDate", "type": "Number"},
|
||||||
|
{"name": "InvoiceId", "type": "String"},
|
||||||
|
{"name": "Currency", "type": "String"},
|
||||||
|
],
|
||||||
|
rows=[],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return CostManagementQueryCSPResult(
|
||||||
|
**dict(name=object_id, properties=properties,)
|
||||||
|
)
|
||||||
|
@ -499,3 +499,34 @@ class UserCSPPayload(BaseCSPPayload):
|
|||||||
|
|
||||||
class UserCSPResult(AliasModel):
|
class UserCSPResult(AliasModel):
|
||||||
id: str
|
id: str
|
||||||
|
|
||||||
|
|
||||||
|
class QueryColumn(AliasModel):
|
||||||
|
name: str
|
||||||
|
type: str
|
||||||
|
|
||||||
|
|
||||||
|
class CostManagementQueryProperties(AliasModel):
|
||||||
|
columns: List[QueryColumn]
|
||||||
|
rows: List[Optional[list]]
|
||||||
|
|
||||||
|
|
||||||
|
class CostManagementQueryCSPResult(AliasModel):
|
||||||
|
name: str
|
||||||
|
properties: CostManagementQueryProperties
|
||||||
|
|
||||||
|
|
||||||
|
class ReportingCSPPayload(BaseCSPPayload):
|
||||||
|
invoice_section_id: str
|
||||||
|
from_date: str
|
||||||
|
to_date: str
|
||||||
|
|
||||||
|
@root_validator(pre=True)
|
||||||
|
def extract_invoice_section(cls, values):
|
||||||
|
try:
|
||||||
|
values["invoice_section_id"] = values["billing_profile_properties"][
|
||||||
|
"invoice_sections"
|
||||||
|
][0]["invoice_section_id"]
|
||||||
|
return values
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
raise ValueError("Invoice section ID not present in payload")
|
||||||
|
@ -15,6 +15,8 @@ from atst.models import (
|
|||||||
Permissions,
|
Permissions,
|
||||||
PortfolioRole,
|
PortfolioRole,
|
||||||
PortfolioRoleStatus,
|
PortfolioRoleStatus,
|
||||||
|
TaskOrder,
|
||||||
|
CLIN,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .query import PortfoliosQuery, PortfolioStateMachinesQuery
|
from .query import PortfoliosQuery, PortfolioStateMachinesQuery
|
||||||
@ -144,7 +146,7 @@ class Portfolios(object):
|
|||||||
return db.session.query(Portfolio.id)
|
return db.session.query(Portfolio.id)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_portfolios_pending_provisioning(cls) -> List[UUID]:
|
def get_portfolios_pending_provisioning(cls, now) -> List[UUID]:
|
||||||
"""
|
"""
|
||||||
Any portfolio with a corresponding State Machine that is either:
|
Any portfolio with a corresponding State Machine that is either:
|
||||||
not started yet,
|
not started yet,
|
||||||
@ -153,22 +155,18 @@ class Portfolios(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
results = (
|
results = (
|
||||||
cls.base_provision_query()
|
db.session.query(Portfolio.id)
|
||||||
.join(PortfolioStateMachine)
|
.join(PortfolioStateMachine)
|
||||||
|
.join(TaskOrder)
|
||||||
|
.join(CLIN)
|
||||||
|
.filter(Portfolio.deleted == False)
|
||||||
|
.filter(CLIN.start_date <= now)
|
||||||
|
.filter(CLIN.end_date > now)
|
||||||
.filter(
|
.filter(
|
||||||
or_(
|
or_(
|
||||||
PortfolioStateMachine.state == FSMStates.UNSTARTED,
|
PortfolioStateMachine.state == FSMStates.UNSTARTED,
|
||||||
PortfolioStateMachine.state == FSMStates.FAILED,
|
PortfolioStateMachine.state.like("%CREATED"),
|
||||||
PortfolioStateMachine.state == FSMStates.TENANT_FAILED,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return [id_ for id_, in results]
|
return [id_ for id_, in results]
|
||||||
|
|
||||||
# db.session.query(PortfolioStateMachine).\
|
|
||||||
# filter(
|
|
||||||
# or_(
|
|
||||||
# PortfolioStateMachine.state==FSMStates.UNSTARTED,
|
|
||||||
# PortfolioStateMachine.state==FSMStates.UNSTARTED,
|
|
||||||
# )
|
|
||||||
# ).all()
|
|
||||||
|
@ -203,7 +203,7 @@ def dispatch_provision_portfolio(self):
|
|||||||
"""
|
"""
|
||||||
Iterate over portfolios with a corresponding State Machine that have not completed.
|
Iterate over portfolios with a corresponding State Machine that have not completed.
|
||||||
"""
|
"""
|
||||||
for portfolio_id in Portfolios.get_portfolios_pending_provisioning():
|
for portfolio_id in Portfolios.get_portfolios_pending_provisioning(pendulum.now()):
|
||||||
provision_portfolio.delay(portfolio_id=portfolio_id)
|
provision_portfolio.delay(portfolio_id=portfolio_id)
|
||||||
|
|
||||||
|
|
||||||
|
@ -175,11 +175,14 @@ class PortfolioStateMachine(
|
|||||||
app.logger.info(exc.json())
|
app.logger.info(exc.json())
|
||||||
print(exc.json())
|
print(exc.json())
|
||||||
app.logger.info(payload_data)
|
app.logger.info(payload_data)
|
||||||
|
# TODO: Ensure that failing the stage does not preclude a Celery retry
|
||||||
self.fail_stage(stage)
|
self.fail_stage(stage)
|
||||||
|
# TODO: catch and handle general CSP exception here
|
||||||
except (ConnectionException, UnknownServerException) as exc:
|
except (ConnectionException, UnknownServerException) as exc:
|
||||||
app.logger.error(
|
app.logger.error(
|
||||||
f"CSP api call. Caught exception for {self.__repr__()}.", exc_info=1,
|
f"CSP api call. Caught exception for {self.__repr__()}.", exc_info=1,
|
||||||
)
|
)
|
||||||
|
# TODO: Ensure that failing the stage does not preclude a Celery retry
|
||||||
self.fail_stage(stage)
|
self.fail_stage(stage)
|
||||||
|
|
||||||
self.finish_stage(stage)
|
self.finish_stage(stage)
|
||||||
|
@ -1,30 +1,21 @@
|
|||||||
|
import ExpandSidenavMixin from '../mixins/expand_sidenav'
|
||||||
import ToggleMixin from '../mixins/toggle'
|
import ToggleMixin from '../mixins/toggle'
|
||||||
|
|
||||||
const cookieName = 'expandSidenav'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'sidenav-toggler',
|
name: 'sidenav-toggler',
|
||||||
|
|
||||||
mixins: [ToggleMixin],
|
mixins: [ExpandSidenavMixin, ToggleMixin],
|
||||||
|
|
||||||
props: {
|
mounted: function() {
|
||||||
defaultVisible: {
|
this.$parent.$emit('sidenavToggle', this.isVisible)
|
||||||
type: Boolean,
|
|
||||||
default: function() {
|
|
||||||
if (document.cookie.match(cookieName)) {
|
|
||||||
return !!document.cookie.match(cookieName + ' *= *true')
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
toggle: function(e) {
|
toggle: function(e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.isVisible = !this.isVisible
|
this.isVisible = !this.isVisible
|
||||||
document.cookie = cookieName + '=' + this.isVisible + '; path=/'
|
document.cookie = this.cookieName + '=' + this.isVisible + '; path=/'
|
||||||
|
this.$parent.$emit('sidenavToggle', this.isVisible)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
12
js/index.js
12
js/index.js
@ -32,12 +32,14 @@ import ToForm from './components/forms/to_form'
|
|||||||
import ClinFields from './components/clin_fields'
|
import ClinFields from './components/clin_fields'
|
||||||
import PopDateRange from './components/pop_date_range'
|
import PopDateRange from './components/pop_date_range'
|
||||||
import ToggleMenu from './components/toggle_menu'
|
import ToggleMenu from './components/toggle_menu'
|
||||||
|
import ExpandSidenav from './mixins/expand_sidenav'
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
Vue.use(VTooltip)
|
Vue.use(VTooltip)
|
||||||
|
|
||||||
Vue.mixin(Modal)
|
Vue.mixin(Modal)
|
||||||
|
Vue.mixin(ExpandSidenav)
|
||||||
|
|
||||||
const app = new Vue({
|
const app = new Vue({
|
||||||
el: '#app-root',
|
el: '#app-root',
|
||||||
@ -67,6 +69,12 @@ const app = new Vue({
|
|||||||
ToggleMenu,
|
ToggleMenu,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
sidenavExpanded: this.defaultVisible,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
mounted: function() {
|
mounted: function() {
|
||||||
this.$on('modalOpen', data => {
|
this.$on('modalOpen', data => {
|
||||||
if (data['isOpen']) {
|
if (data['isOpen']) {
|
||||||
@ -105,6 +113,10 @@ const app = new Vue({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.$on('sidenavToggle', data => {
|
||||||
|
this.sidenavExpanded = data
|
||||||
|
})
|
||||||
},
|
},
|
||||||
delimiters: ['!{', '}'],
|
delimiters: ['!{', '}'],
|
||||||
|
|
||||||
|
15
js/mixins/expand_sidenav.js
Normal file
15
js/mixins/expand_sidenav.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
cookieName: 'expandSidenav',
|
||||||
|
defaultVisible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: function() {
|
||||||
|
if (document.cookie.match(this.cookieName)) {
|
||||||
|
return !!document.cookie.match(this.cookieName + ' *= *true')
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
@ -47,3 +47,4 @@
|
|||||||
@import "sections/application_edit";
|
@import "sections/application_edit";
|
||||||
@import "sections/reports";
|
@import "sections/reports";
|
||||||
@import "sections/task_order";
|
@import "sections/task_order";
|
||||||
|
@import "sections/ccpo";
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
.usa-alert {
|
.usa-alert {
|
||||||
padding-bottom: 2.4rem;
|
padding-bottom: 2.4rem;
|
||||||
|
max-width: $max-panel-width;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin alert {
|
@mixin alert {
|
||||||
@ -97,38 +98,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert {
|
|
||||||
@include alert;
|
|
||||||
@include alert-level("info");
|
|
||||||
|
|
||||||
&.alert--success {
|
|
||||||
@include alert-level("success");
|
|
||||||
|
|
||||||
.alert__actions {
|
|
||||||
.icon-link {
|
|
||||||
@include icon-link-color($color-green, $color-white);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.alert--warning {
|
|
||||||
@include alert-level("warning");
|
|
||||||
|
|
||||||
.alert__actions {
|
|
||||||
.icon-link {
|
|
||||||
@include icon-link-color($color-gold-dark, $color-white);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.alert--error {
|
|
||||||
@include alert-level("error");
|
|
||||||
|
|
||||||
.alert__actions {
|
|
||||||
.icon-link {
|
|
||||||
@include icon-link-color($color-red, $color-white);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
.error-page {
|
.error-page {
|
||||||
max-width: 475px;
|
max-width: $max-page-width;
|
||||||
margin: auto;
|
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
|
box-shadow: none;
|
||||||
|
background-color: unset;
|
||||||
|
border: none;
|
||||||
|
max-width: 475px;
|
||||||
|
margin: auto;
|
||||||
|
|
||||||
&__heading {
|
&__heading {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: $gap 0;
|
padding: $gap 0;
|
||||||
@ -15,17 +20,6 @@
|
|||||||
margin-bottom: $gap;
|
margin-bottom: $gap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__body {
|
|
||||||
padding: $gap * 2;
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
hr {
|
|
||||||
width: 80%;
|
|
||||||
margin: auto;
|
|
||||||
margin-bottom: $gap * 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
max-width: 1190px;
|
max-width: $max-page-width;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: $color-white;
|
color: $color-white;
|
||||||
|
@ -19,6 +19,7 @@ $sidenav-collapsed-width: 10rem;
|
|||||||
$max-panel-width: 90rem;
|
$max-panel-width: 90rem;
|
||||||
$home-pg-icon-width: 6rem;
|
$home-pg-icon-width: 6rem;
|
||||||
$large-spacing: 4rem;
|
$large-spacing: 4rem;
|
||||||
|
$max-page-width: $max-panel-width + $sidenav-expanded-width + $large-spacing;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* USWDS Variables
|
* USWDS Variables
|
||||||
|
@ -32,22 +32,35 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.action-group-footer {
|
.action-group-footer {
|
||||||
@extend .action-group;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
padding-top: $gap;
|
padding-top: $gap;
|
||||||
padding-bottom: $gap;
|
padding-bottom: $gap;
|
||||||
|
padding-right: $gap * 4;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: $footer-height;
|
bottom: $footer-height;
|
||||||
|
left: 0;
|
||||||
background: white;
|
background: white;
|
||||||
right: 0;
|
|
||||||
padding-right: $gap * 4;
|
|
||||||
border-top: 1px solid $color-gray-lighter;
|
border-top: 1px solid $color-gray-lighter;
|
||||||
width: 100%;
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&.action-group-footer--expand-offset {
|
||||||
|
padding-left: $sidenav-expanded-width;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.action-group-footer--collapse-offset {
|
||||||
|
padding-left: $sidenav-collapsed-width;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-group-footer--container {
|
||||||
|
@extend .action-group;
|
||||||
|
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-left: $large-spacing;
|
||||||
|
max-width: $max-panel-width;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
3
styles/sections/_ccpo.scss
Normal file
3
styles/sections/_ccpo.scss
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.ccpo-panel-container {
|
||||||
|
max-width: $max-panel-width;
|
||||||
|
}
|
@ -39,14 +39,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<span class="action-group-footer">
|
<div
|
||||||
{% block next_button %}
|
class="action-group-footer"
|
||||||
{{ SaveButton(text=('portfolios.applications.new.step_1_button_text' | translate)) }}
|
v-bind:class="{'action-group-footer--expand-offset': this.$root.sidenavExpanded, 'action-group-footer--collapse-offset': !this.$root.sidenavExpanded}">
|
||||||
{% endblock %}
|
<div class="action-group-footer--container">
|
||||||
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
|
{% block next_button %}
|
||||||
Cancel
|
{{ SaveButton(text=('portfolios.applications.new.step_1_button_text' | translate)) }}
|
||||||
</a>
|
{% endblock %}
|
||||||
</span>
|
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</base-form>
|
</base-form>
|
||||||
|
|
||||||
|
@ -58,20 +58,24 @@
|
|||||||
{{ Icon("plus") }}
|
{{ Icon("plus") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="action-group-footer"
|
||||||
|
v-bind:class="{'action-group-footer--expand-offset': this.$root.sidenavExpanded, 'action-group-footer--collapse-offset': !this.$root.sidenavExpanded}">
|
||||||
|
<div class="action-group-footer--container">
|
||||||
|
{% block next_button %}
|
||||||
|
{{ SaveButton(text=('portfolios.applications.new.step_2_button_text' | translate)) }}
|
||||||
|
{% endblock %}
|
||||||
|
<a class="usa-button usa-button-secondary" href="{{ url_for('applications.view_new_application_step_1', application_id=application.id) }}">
|
||||||
|
Previous
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<span class="action-group-footer">
|
|
||||||
{% block next_button %}
|
|
||||||
{{ SaveButton(text=('portfolios.applications.new.step_2_button_text' | translate)) }}
|
|
||||||
{% endblock %}
|
|
||||||
<a class="usa-button usa-button-secondary" href="{{ url_for('applications.view_new_application_step_1', application_id=application.id) }}">
|
|
||||||
Previous
|
|
||||||
</a>
|
|
||||||
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
|
|
||||||
Cancel
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</form>
|
</form>
|
||||||
</application-environments>
|
</application-environments>
|
||||||
|
|
||||||
|
@ -25,16 +25,20 @@
|
|||||||
action_update="applications.update_new_application_step_3") }}
|
action_update="applications.update_new_application_step_3") }}
|
||||||
|
|
||||||
|
|
||||||
<span class="action-group-footer">
|
<div
|
||||||
<a class="usa-button" href="{{ url_for('applications.settings', application_id=application_id) }}">
|
class="action-group-footer"
|
||||||
{{ "portfolios.applications.new.step_3_button_text" | translate }}
|
v-bind:class="{'action-group-footer--expand-offset': this.$root.sidenavExpanded, 'action-group-footer--collapse-offset': !this.$root.sidenavExpanded}">
|
||||||
</a>
|
<div class="action-group-footer--container">
|
||||||
<a class="usa-button usa-button-secondary" href="{{ url_for('applications.view_new_application_step_2', application_id=application.id) }}">
|
<a class="usa-button" href="{{ url_for('applications.settings', application_id=application_id) }}">
|
||||||
{{ "common.previous" | translate }}
|
{{ "portfolios.applications.new.step_3_button_text" | translate }}
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
|
<a class="usa-button usa-button-secondary" href="{{ url_for('applications.view_new_application_step_2', application_id=application.id) }}">
|
||||||
{{ "common.cancel" | translate }}
|
{{ "common.previous" | translate }}
|
||||||
</a>
|
</a>
|
||||||
</span>
|
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
|
||||||
|
{{ "common.cancel" | translate }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -4,21 +4,23 @@
|
|||||||
{% from "components/text_input.html" import TextInput %}
|
{% from "components/text_input.html" import TextInput %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<base-form inline-template>
|
<div class="ccpo-panel-container">
|
||||||
<form id="add-ccpo-user-form" action="{{ url_for('ccpo.submit_new_user') }}" method="POST">
|
<base-form inline-template>
|
||||||
{{ form.csrf_token }}
|
<form id="add-ccpo-user-form" action="{{ url_for('ccpo.submit_new_user') }}" method="POST">
|
||||||
<h1>{{ "ccpo.form.add_user_title" | translate }}</h1>
|
{{ form.csrf_token }}
|
||||||
<div class='form-row'>
|
<h1>{{ "ccpo.form.add_user_title" | translate }}</h1>
|
||||||
<div class='form-col form-col--two-thirds'>
|
<div class='form-row'>
|
||||||
{{ TextInput(form.dod_id, validation='dodId', optional=False) }}
|
<div class='form-col form-col--two-thirds'>
|
||||||
</div>
|
{{ TextInput(form.dod_id, validation='dodId', optional=False) }}
|
||||||
<div class="form-col form-col--third">
|
</div>
|
||||||
<div class='action-group'>
|
<div class="form-col form-col--third">
|
||||||
{{ SaveButton(text="common.next"|translate, element="input", additional_classes="action-group__action", form="add-ccpo-user-form") }}
|
<div class='action-group'>
|
||||||
<a class='action-group__action icon-link icon-link--default' href="{{ url_for('ccpo.users') }}">{{ "common.cancel" | translate }}</a>
|
{{ SaveButton(text="common.next"|translate, element="input", additional_classes="action-group__action", form="add-ccpo-user-form") }}
|
||||||
|
<a class='action-group__action icon-link icon-link--default' href="{{ url_for('ccpo.users') }}">{{ "common.cancel" | translate }}</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</form>
|
</base-form>
|
||||||
</base-form>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -3,31 +3,33 @@
|
|||||||
{% from "components/text_input.html" import TextInput %}
|
{% from "components/text_input.html" import TextInput %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if new_user %}
|
<div class="ccpo-panel-container">
|
||||||
<h3>{{ 'ccpo.form.confirm_user_title' | translate }}</h3>
|
{% if new_user %}
|
||||||
<form id="add-ccpo-user-form" action="{{ url_for('ccpo.confirm_new_user') }}" method="POST">
|
<h3>{{ 'ccpo.form.confirm_user_title' | translate }}</h3>
|
||||||
{{ form.csrf_token }}
|
<form id="add-ccpo-user-form" action="{{ url_for('ccpo.confirm_new_user') }}" method="POST">
|
||||||
<input type="hidden" name="dod_id" value="{{ form.dod_id.data }}">
|
{{ form.csrf_token }}
|
||||||
<div>
|
<input type="hidden" name="dod_id" value="{{ form.dod_id.data }}">
|
||||||
<p>
|
<div>
|
||||||
{{ "ccpo.form.confirm_user_text" | translate }}
|
<p>
|
||||||
</p>
|
{{ "ccpo.form.confirm_user_text" | translate }}
|
||||||
<p>
|
</p>
|
||||||
{{ new_user.full_name }}
|
<p>
|
||||||
</p>
|
{{ new_user.full_name }}
|
||||||
<p>
|
</p>
|
||||||
{{ new_user.email }}
|
<p>
|
||||||
</p>
|
{{ new_user.email }}
|
||||||
</div>
|
</p>
|
||||||
<div class='action-group'>
|
</div>
|
||||||
<input
|
<div class='action-group'>
|
||||||
type='submit'
|
<input
|
||||||
class='action-group__action usa-button'
|
type='submit'
|
||||||
value='{{ "ccpo.form.confirm_button" | translate }}'>
|
class='action-group__action usa-button'
|
||||||
<a class='action-group__action icon-link icon-link--default' href="{{ url_for('ccpo.users') }}">
|
value='{{ "ccpo.form.confirm_button" | translate }}'>
|
||||||
{{ "common.cancel" | translate }}
|
<a class='action-group__action icon-link icon-link--default' href="{{ url_for('ccpo.users') }}">
|
||||||
</a>
|
{{ "common.cancel" | translate }}
|
||||||
</div>
|
</a>
|
||||||
</form>
|
</div>
|
||||||
{% endif %}
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -6,78 +6,80 @@
|
|||||||
{% from "components/modal.html" import Modal %}
|
{% from "components/modal.html" import Modal %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class='col'>
|
<div class="ccpo-panel-container">
|
||||||
<div class="h2">
|
<div class='col'>
|
||||||
{{ "ccpo.users_title" | translate }}
|
<div class="h2">
|
||||||
</div>
|
{{ "ccpo.users_title" | translate }}
|
||||||
|
</div>
|
||||||
|
|
||||||
{% include "fragments/flash.html" %}
|
{% include "fragments/flash.html" %}
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{{ "common.name" | translate }}</th>
|
|
||||||
<th>{{ "common.email" | translate }}</th>
|
|
||||||
<th>{{ "common.dod_id" | translate }}</th>
|
|
||||||
{% if user_can(permissions.DELETE_CCPO_USER) %}
|
|
||||||
<th></th>
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for user, form in users_info %}
|
|
||||||
{% set modal_id = "disable_ccpo_user_{}".format(user.dod_id) %}
|
|
||||||
{% set disable_button_class = 'button-danger-outline' %}
|
|
||||||
{% if user == g.current_user %}
|
|
||||||
{% set disable_button_class = "usa-button-disabled" %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ user.full_name }}</td>
|
<th>{{ "common.name" | translate }}</th>
|
||||||
<td>{{ user.email }}</td>
|
<th>{{ "common.email" | translate }}</th>
|
||||||
<td>{{ user.dod_id }}</td>
|
<th>{{ "common.dod_id" | translate }}</th>
|
||||||
{% if user_can(permissions.DELETE_CCPO_USER) %}
|
{% if user_can(permissions.DELETE_CCPO_USER) %}
|
||||||
<td>
|
<th></th>
|
||||||
<a v-on:click="openModal('{{ modal_id }}')" class='usa-button {{ disable_button_class }}'>
|
|
||||||
{{ "common.disable" | translate }}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
{% for user, form in users_info %}
|
||||||
|
{% set modal_id = "disable_ccpo_user_{}".format(user.dod_id) %}
|
||||||
|
{% set disable_button_class = 'button-danger-outline' %}
|
||||||
|
{% if user == g.current_user %}
|
||||||
|
{% set disable_button_class = "usa-button-disabled" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>{{ user.full_name }}</td>
|
||||||
|
<td>{{ user.email }}</td>
|
||||||
|
<td>{{ user.dod_id }}</td>
|
||||||
|
{% if user_can(permissions.DELETE_CCPO_USER) %}
|
||||||
|
<td>
|
||||||
|
<a v-on:click="openModal('{{ modal_id }}')" class='usa-button {{ disable_button_class }}'>
|
||||||
|
{{ "common.disable" | translate }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if user_can(permissions.CREATE_CCPO_USER) %}
|
||||||
|
<a class="icon-link" href="{{ url_for('ccpo.add_new_user')}}">
|
||||||
|
{{ "ccpo.add_user" | translate }} {{ Icon("plus") }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if user_can(permissions.DELETE_CCPO_USER) %}
|
||||||
|
{% for user, form in users_info %}
|
||||||
|
{% set modal_id = "disable_ccpo_user_{}".format(user.dod_id) %}
|
||||||
|
{% call Modal(name=modal_id) %}
|
||||||
|
<h1>Disable CCPO User</h1>
|
||||||
|
<hr>
|
||||||
|
{{
|
||||||
|
Alert(
|
||||||
|
title=("components.modal.destructive_title" | translate),
|
||||||
|
message=("ccpo.disable_user.alert_message" | translate({"user_name": user.full_name})),
|
||||||
|
level="warning"
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
{{
|
||||||
|
DeleteConfirmation(
|
||||||
|
modal_id=modal_id,
|
||||||
|
delete_text='Remove Access',
|
||||||
|
delete_action=(url_for('ccpo.remove_access', user_id=user.id)),
|
||||||
|
form=form,
|
||||||
|
confirmation_text='remove'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
{% endcall %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if user_can(permissions.CREATE_CCPO_USER) %}
|
|
||||||
<a class="icon-link" href="{{ url_for('ccpo.add_new_user')}}">
|
|
||||||
{{ "ccpo.add_user" | translate }} {{ Icon("plus") }}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if user_can(permissions.DELETE_CCPO_USER) %}
|
|
||||||
{% for user, form in users_info %}
|
|
||||||
{% set modal_id = "disable_ccpo_user_{}".format(user.dod_id) %}
|
|
||||||
{% call Modal(name=modal_id) %}
|
|
||||||
<h1>Disable CCPO User</h1>
|
|
||||||
<hr>
|
|
||||||
{{
|
|
||||||
Alert(
|
|
||||||
title=("components.modal.destructive_title" | translate),
|
|
||||||
message=("ccpo.disable_user.alert_message" | translate({"user_name": user.full_name})),
|
|
||||||
level="warning"
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
{{
|
|
||||||
DeleteConfirmation(
|
|
||||||
modal_id=modal_id,
|
|
||||||
delete_text='Remove Access',
|
|
||||||
delete_action=(url_for('ccpo.remove_access', user_id=user.id)),
|
|
||||||
form=form,
|
|
||||||
confirmation_text='remove'
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
{% endcall %}
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<main class="usa-section usa-content error-page">
|
<main class="usa-section usa-content error-page">
|
||||||
|
<div class="panel">
|
||||||
<div class="panel__heading">
|
<div class="panel__heading">
|
||||||
{{ Icon('cloud', classes="icon--red icon--large")}}
|
{{ Icon('cloud', classes="icon--red icon--large")}}
|
||||||
<hr>
|
<hr>
|
||||||
@ -17,6 +18,7 @@
|
|||||||
{%- endif %}
|
{%- endif %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -10,29 +10,30 @@
|
|||||||
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='img/favicon.ico') }} " />
|
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='img/favicon.ico') }} " />
|
||||||
</head>
|
</head>
|
||||||
<body class="{% if g.modalOpen %} modalOpen{% endif %}">
|
<body class="{% if g.modalOpen %} modalOpen{% endif %}">
|
||||||
|
<div id='app-root'>
|
||||||
|
{% block template_vars %}{% endblock %}
|
||||||
|
|
||||||
{% block template_vars %}{% endblock %}
|
{% include 'components/usa_header.html' %}
|
||||||
|
|
||||||
{% include 'components/usa_header.html' %}
|
{% include 'navigation/topbar.html' %}
|
||||||
|
|
||||||
{% include 'navigation/topbar.html' %}
|
<div class='global-layout'>
|
||||||
|
|
||||||
<div class='global-layout'>
|
<div class='global-panel-container'>
|
||||||
|
{% block sidenav %}{% endblock %}
|
||||||
|
|
||||||
<div class='global-panel-container'>
|
{% block content %}
|
||||||
{% block sidenav %}{% endblock %}
|
these are not the droids you are looking for
|
||||||
|
{% endblock %}
|
||||||
{% block content %}
|
</div>
|
||||||
these are not the droids you are looking for
|
|
||||||
{% endblock %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% include 'footer.html' %}
|
||||||
|
|
||||||
|
{% block modal %}{% endblock %}
|
||||||
|
{% assets "js_all" %}
|
||||||
|
<script src="{{ ASSET_URL }}"></script>
|
||||||
|
{% endassets %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% include 'footer.html' %}
|
|
||||||
|
|
||||||
{% block modal %}{% endblock %}
|
|
||||||
{% assets "js_all" %}
|
|
||||||
<script src="{{ ASSET_URL }}"></script>
|
|
||||||
{% endassets %}
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
<p>{{ "portfolios.header" | translate }}</p>
|
<p>{{ "portfolios.header" | translate }}</p>
|
||||||
<h1>{{ 'portfolios.new.title' | translate }}</h1>
|
<h1>{{ 'portfolios.new.title' | translate }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{{ StickyCTA(text="portfolios.new.cta_step_1"|translate, context=("portfolios.new.sticky_header_context"|translate({"step": "1"}) )) }}
|
{{ StickyCTA(text="portfolios.new.cta_step_1"|translate, context=("portfolios.new.sticky_header_context"|translate({"step": "1"}) )) }}
|
||||||
<base-form inline-template>
|
<base-form inline-template>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -38,13 +39,18 @@
|
|||||||
{{ "forms.portfolio.defense_component.help_text" | translate | safe }}
|
{{ "forms.portfolio.defense_component.help_text" | translate | safe }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class='action-group-footer'>
|
<div
|
||||||
{% block next_button %}
|
class='action-group-footer'
|
||||||
{{ SaveButton(text=('portfolios.new.save' | translate), form="portfolio-create", element="input") }}
|
v-bind:class="{'action-group-footer--expand-offset': this.$root.sidenavExpanded, 'action-group-footer--collapse-offset': !this.$root.sidenavExpanded}">
|
||||||
{% endblock %}
|
<div class="action-group-footer--container">
|
||||||
<a class="usa-button usa-button-secondary" href="{{ url_for('atst.home') }}">
|
{% block next_button %}
|
||||||
Cancel
|
{{ SaveButton(text=('portfolios.new.save' | translate), form="portfolio-create", element="input") }}
|
||||||
</a>
|
{% endblock %}
|
||||||
|
<a class="usa-button usa-button-secondary" href="{{ url_for('atst.home') }}">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</base-form>
|
</base-form>
|
||||||
|
@ -31,7 +31,10 @@
|
|||||||
<div class="task-order">
|
<div class="task-order">
|
||||||
{% block to_builder_form_field %}{% endblock %}
|
{% block to_builder_form_field %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
<span class="action-group-footer">
|
<div
|
||||||
|
class="action-group-footer"
|
||||||
|
v-bind:class="{'action-group-footer--expand-offset': this.$root.sidenavExpanded, 'action-group-footer--collapse-offset': !this.$root.sidenavExpanded}">
|
||||||
|
<div class="action-group-footer--container">
|
||||||
{% block next_button %}
|
{% block next_button %}
|
||||||
<input
|
<input
|
||||||
type="submit"
|
type="submit"
|
||||||
@ -58,14 +61,13 @@
|
|||||||
</a>
|
</a>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<a
|
<a
|
||||||
v-on:click="openModal('cancel')"
|
v-on:click="openModal('cancel')"
|
||||||
class="action-group__action icon-link">
|
class="action-group__action icon-link">
|
||||||
{{ "common.cancel" | translate }}
|
{{ "common.cancel" | translate }}
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</to-form>
|
</to-form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -5,6 +5,8 @@ from uuid import uuid4
|
|||||||
import pytest
|
import pytest
|
||||||
from tests.factories import ApplicationFactory, EnvironmentFactory
|
from tests.factories import ApplicationFactory, EnvironmentFactory
|
||||||
from tests.mock_azure import AUTH_CREDENTIALS, mock_azure
|
from tests.mock_azure import AUTH_CREDENTIALS, mock_azure
|
||||||
|
import pendulum
|
||||||
|
import pydantic
|
||||||
|
|
||||||
from atst.domain.csp.cloud import AzureCloudProvider
|
from atst.domain.csp.cloud import AzureCloudProvider
|
||||||
from atst.domain.csp.cloud.models import (
|
from atst.domain.csp.cloud.models import (
|
||||||
@ -20,10 +22,12 @@ from atst.domain.csp.cloud.models import (
|
|||||||
BillingProfileTenantAccessCSPResult,
|
BillingProfileTenantAccessCSPResult,
|
||||||
BillingProfileVerificationCSPPayload,
|
BillingProfileVerificationCSPPayload,
|
||||||
BillingProfileVerificationCSPResult,
|
BillingProfileVerificationCSPResult,
|
||||||
|
CostManagementQueryCSPResult,
|
||||||
ProductPurchaseCSPPayload,
|
ProductPurchaseCSPPayload,
|
||||||
ProductPurchaseCSPResult,
|
ProductPurchaseCSPResult,
|
||||||
ProductPurchaseVerificationCSPPayload,
|
ProductPurchaseVerificationCSPPayload,
|
||||||
ProductPurchaseVerificationCSPResult,
|
ProductPurchaseVerificationCSPResult,
|
||||||
|
ReportingCSPPayload,
|
||||||
SubscriptionCreationCSPPayload,
|
SubscriptionCreationCSPPayload,
|
||||||
SubscriptionCreationCSPResult,
|
SubscriptionCreationCSPResult,
|
||||||
SubscriptionVerificationCSPPayload,
|
SubscriptionVerificationCSPPayload,
|
||||||
@ -718,3 +722,77 @@ def test_create_subscription_verification(mock_azure: AzureCloudProvider):
|
|||||||
payload
|
payload
|
||||||
)
|
)
|
||||||
assert result.subscription_id == "60fbbb72-0516-4253-ab18-c92432ba3230"
|
assert result.subscription_id == "60fbbb72-0516-4253-ab18-c92432ba3230"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_reporting_data(mock_azure: AzureCloudProvider):
|
||||||
|
mock_result = Mock()
|
||||||
|
mock_result.json.return_value = {
|
||||||
|
"eTag": None,
|
||||||
|
"id": "providers/Microsoft.Billing/billingAccounts/52865e4c-52e8-5a6c-da6b-c58f0814f06f:7ea5de9d-b8ce-4901-b1c5-d864320c7b03_2019-05-31/billingProfiles/XQDJ-6LB4-BG7-TGB/invoiceSections/P73M-XC7J-PJA-TGB/providers/Microsoft.CostManagement/query/e82d0cda-2ffb-4476-a98a-425c83c216f9",
|
||||||
|
"location": None,
|
||||||
|
"name": "e82d0cda-2ffb-4476-a98a-425c83c216f9",
|
||||||
|
"properties": {
|
||||||
|
"columns": [
|
||||||
|
{"name": "PreTaxCost", "type": "Number"},
|
||||||
|
{"name": "UsageDate", "type": "Number"},
|
||||||
|
{"name": "InvoiceId", "type": "String"},
|
||||||
|
{"name": "Currency", "type": "String"},
|
||||||
|
],
|
||||||
|
"nextLink": None,
|
||||||
|
"rows": [],
|
||||||
|
},
|
||||||
|
"sku": None,
|
||||||
|
"type": "Microsoft.CostManagement/query",
|
||||||
|
}
|
||||||
|
mock_result.ok = True
|
||||||
|
mock_azure.sdk.requests.post.return_value = mock_result
|
||||||
|
mock_azure = mock_get_secret(mock_azure)
|
||||||
|
|
||||||
|
# Subset of a profile's CSP data that we care about for reporting
|
||||||
|
csp_data = {
|
||||||
|
"tenant_id": "6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4",
|
||||||
|
"billing_profile_properties": {
|
||||||
|
"invoice_sections": [
|
||||||
|
{
|
||||||
|
"invoice_section_id": "providers/Microsoft.Billing/billingAccounts/52865e4c-52e8-5a6c-da6b-c58f0814f06f:7ea5de9d-b8ce-4901-b1c5-d864320c7b03_2019-05-31/billingProfiles/XQDJ-6LB4-BG7-TGB/invoiceSections/P73M-XC7J-PJA-TGB",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
data: CostManagementQueryCSPResult = mock_azure.get_reporting_data(
|
||||||
|
ReportingCSPPayload(
|
||||||
|
from_date=pendulum.now().subtract(years=1).add(days=1).format("YYYY-MM-DD"),
|
||||||
|
to_date=pendulum.now().format("YYYY-MM-DD"),
|
||||||
|
**csp_data,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(data, CostManagementQueryCSPResult)
|
||||||
|
assert data.name == "e82d0cda-2ffb-4476-a98a-425c83c216f9"
|
||||||
|
assert len(data.properties.columns) == 4
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_reporting_data_malformed_payload(mock_azure: AzureCloudProvider):
|
||||||
|
mock_result = Mock()
|
||||||
|
mock_result.ok = True
|
||||||
|
mock_azure.sdk.requests.post.return_value = mock_result
|
||||||
|
mock_azure = mock_get_secret(mock_azure)
|
||||||
|
|
||||||
|
# Malformed csp_data payloads that should throw pydantic validation errors
|
||||||
|
index_error = {
|
||||||
|
"tenant_id": "6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4",
|
||||||
|
"billing_profile_properties": {"invoice_sections": [],},
|
||||||
|
}
|
||||||
|
key_error = {
|
||||||
|
"tenant_id": "6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4",
|
||||||
|
"billing_profile_properties": {"invoice_sections": [{}],},
|
||||||
|
}
|
||||||
|
|
||||||
|
for malformed_payload in [key_error, index_error]:
|
||||||
|
with pytest.raises(pydantic.ValidationError):
|
||||||
|
assert mock_azure.get_reporting_data(
|
||||||
|
ReportingCSPPayload(
|
||||||
|
from_date="foo", to_date="bar", **malformed_payload,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import pendulum
|
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from atst.domain.environments import Environments
|
from atst.domain.environments import Environments
|
||||||
@ -14,6 +13,7 @@ from tests.factories import (
|
|||||||
EnvironmentRoleFactory,
|
EnvironmentRoleFactory,
|
||||||
ApplicationRoleFactory,
|
ApplicationRoleFactory,
|
||||||
)
|
)
|
||||||
|
from tests.utils import EnvQueryTest
|
||||||
|
|
||||||
|
|
||||||
def test_create_environments():
|
def test_create_environments():
|
||||||
@ -119,40 +119,6 @@ def test_update_does_not_duplicate_names_within_application():
|
|||||||
Environments.update(dupe_env, name)
|
Environments.update(dupe_env, name)
|
||||||
|
|
||||||
|
|
||||||
class EnvQueryTest:
|
|
||||||
@property
|
|
||||||
def NOW(self):
|
|
||||||
return pendulum.now()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def YESTERDAY(self):
|
|
||||||
return self.NOW.subtract(days=1)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def TOMORROW(self):
|
|
||||||
return self.NOW.add(days=1)
|
|
||||||
|
|
||||||
def create_portfolio_with_clins(self, start_and_end_dates, env_data=None):
|
|
||||||
env_data = env_data or {}
|
|
||||||
return PortfolioFactory.create(
|
|
||||||
applications=[
|
|
||||||
{
|
|
||||||
"name": "Mos Eisley",
|
|
||||||
"description": "Where Han shot first",
|
|
||||||
"environments": [{"name": "thebar", **env_data}],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
task_orders=[
|
|
||||||
{
|
|
||||||
"create_clins": [
|
|
||||||
{"start_date": start_date, "end_date": end_date}
|
|
||||||
for (start_date, end_date) in start_and_end_dates
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestGetEnvironmentsPendingCreate(EnvQueryTest):
|
class TestGetEnvironmentsPendingCreate(EnvQueryTest):
|
||||||
def test_with_expired_clins(self, session):
|
def test_with_expired_clins(self, session):
|
||||||
self.create_portfolio_with_clins([(self.YESTERDAY, self.YESTERDAY)])
|
self.create_portfolio_with_clins([(self.YESTERDAY, self.YESTERDAY)])
|
||||||
|
@ -26,6 +26,7 @@ from tests.factories import (
|
|||||||
PortfolioStateMachineFactory,
|
PortfolioStateMachineFactory,
|
||||||
get_all_portfolio_permission_sets,
|
get_all_portfolio_permission_sets,
|
||||||
)
|
)
|
||||||
|
from tests.utils import EnvQueryTest
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
@ -263,10 +264,44 @@ def test_create_state_machine(portfolio):
|
|||||||
assert fsm
|
assert fsm
|
||||||
|
|
||||||
|
|
||||||
def test_get_portfolios_pending_provisioning(session):
|
class TestGetPortfoliosPendingCreate(EnvQueryTest):
|
||||||
for x in range(5):
|
def test_finds_unstarted(self):
|
||||||
portfolio = PortfolioFactory.create()
|
for x in range(5):
|
||||||
sm = PortfolioStateMachineFactory.create(portfolio=portfolio)
|
if x == 2:
|
||||||
if x == 2:
|
state = "COMPLETED"
|
||||||
sm.state = FSMStates.COMPLETED
|
else:
|
||||||
assert len(Portfolios.get_portfolios_pending_provisioning()) == 4
|
state = "UNSTARTED"
|
||||||
|
self.create_portfolio_with_clins(
|
||||||
|
[(self.YESTERDAY, self.TOMORROW)], state_machine_status=state
|
||||||
|
)
|
||||||
|
assert len(Portfolios.get_portfolios_pending_provisioning(self.NOW)) == 4
|
||||||
|
|
||||||
|
def test_finds_created(self):
|
||||||
|
self.create_portfolio_with_clins(
|
||||||
|
[(self.YESTERDAY, self.TOMORROW)], state_machine_status="TENANT_CREATED"
|
||||||
|
)
|
||||||
|
assert len(Portfolios.get_portfolios_pending_provisioning(self.NOW)) == 1
|
||||||
|
|
||||||
|
def test_does_not_find_failed(self):
|
||||||
|
self.create_portfolio_with_clins(
|
||||||
|
[(self.YESTERDAY, self.TOMORROW)], state_machine_status="TENANT_FAILED"
|
||||||
|
)
|
||||||
|
assert len(Portfolios.get_portfolios_pending_provisioning(self.NOW)) == 0
|
||||||
|
|
||||||
|
def test_with_expired_clins(self):
|
||||||
|
self.create_portfolio_with_clins([(self.YESTERDAY, self.YESTERDAY)])
|
||||||
|
assert len(Portfolios.get_portfolios_pending_provisioning(self.NOW)) == 0
|
||||||
|
|
||||||
|
def test_with_active_clins(self):
|
||||||
|
portfolio = self.create_portfolio_with_clins([(self.YESTERDAY, self.TOMORROW)])
|
||||||
|
Portfolios.get_portfolios_pending_provisioning(self.NOW) == [portfolio.id]
|
||||||
|
|
||||||
|
def test_with_future_clins(self):
|
||||||
|
self.create_portfolio_with_clins([(self.TOMORROW, self.TOMORROW)])
|
||||||
|
assert len(Portfolios.get_portfolios_pending_provisioning(self.NOW)) == 0
|
||||||
|
|
||||||
|
def test_with_already_provisioned_env(self):
|
||||||
|
self.create_portfolio_with_clins(
|
||||||
|
[(self.YESTERDAY, self.TOMORROW)], env_data={"cloud_id": uuid4().hex}
|
||||||
|
)
|
||||||
|
assert len(Portfolios.get_portfolios_pending_provisioning(self.NOW)) == 0
|
||||||
|
@ -286,9 +286,19 @@ def test_create_environment_no_dupes(session, celery_app, celery_worker):
|
|||||||
assert environment.claimed_until == None
|
assert environment.claimed_until == None
|
||||||
|
|
||||||
|
|
||||||
def test_dispatch_provision_portfolio(
|
def test_dispatch_provision_portfolio(csp, monkeypatch):
|
||||||
csp, session, portfolio, celery_app, celery_worker, monkeypatch
|
portfolio = PortfolioFactory.create(
|
||||||
):
|
task_orders=[
|
||||||
|
{
|
||||||
|
"create_clins": [
|
||||||
|
{
|
||||||
|
"start_date": pendulum.now().subtract(days=1),
|
||||||
|
"end_date": pendulum.now().add(days=1),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
sm = PortfolioStateMachineFactory.create(portfolio=portfolio)
|
sm = PortfolioStateMachineFactory.create(portfolio=portfolio)
|
||||||
mock = Mock()
|
mock = Mock()
|
||||||
monkeypatch.setattr("atst.jobs.provision_portfolio", mock)
|
monkeypatch.setattr("atst.jobs.provision_portfolio", mock)
|
||||||
|
@ -5,9 +5,12 @@ from unittest.mock import Mock
|
|||||||
from OpenSSL import crypto
|
from OpenSSL import crypto
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
from flask import template_rendered
|
from flask import template_rendered
|
||||||
|
import pendulum
|
||||||
|
|
||||||
from atst.utils.notification_sender import NotificationSender
|
from atst.utils.notification_sender import NotificationSender
|
||||||
|
|
||||||
|
import tests.factories as factories
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def captured_templates(app):
|
def captured_templates(app):
|
||||||
@ -62,3 +65,40 @@ def make_crl_list(x509_obj, x509_path):
|
|||||||
issuer = x509_obj.issuer.public_bytes(default_backend())
|
issuer = x509_obj.issuer.public_bytes(default_backend())
|
||||||
filename = os.path.basename(x509_path)
|
filename = os.path.basename(x509_path)
|
||||||
return [(filename, issuer.hex())]
|
return [(filename, issuer.hex())]
|
||||||
|
|
||||||
|
|
||||||
|
class EnvQueryTest:
|
||||||
|
@property
|
||||||
|
def NOW(self):
|
||||||
|
return pendulum.now()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def YESTERDAY(self):
|
||||||
|
return self.NOW.subtract(days=1)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def TOMORROW(self):
|
||||||
|
return self.NOW.add(days=1)
|
||||||
|
|
||||||
|
def create_portfolio_with_clins(
|
||||||
|
self, start_and_end_dates, env_data=None, state_machine_status=None
|
||||||
|
):
|
||||||
|
env_data = env_data or {}
|
||||||
|
return factories.PortfolioFactory.create(
|
||||||
|
state=state_machine_status,
|
||||||
|
applications=[
|
||||||
|
{
|
||||||
|
"name": "Mos Eisley",
|
||||||
|
"description": "Where Han shot first",
|
||||||
|
"environments": [{"name": "thebar", **env_data}],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
task_orders=[
|
||||||
|
{
|
||||||
|
"create_clins": [
|
||||||
|
{"start_date": start_date, "end_date": end_date}
|
||||||
|
for (start_date, end_date) in start_and_end_dates
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user