Merge pull request #1232 from dod-ccpo/to-index-page-redesign
TO and App index pages redesign (Part 1)
This commit is contained in:
commit
2af99da9cf
@ -3,9 +3,10 @@ from flask import current_app as app
|
||||
|
||||
from atst.database import db
|
||||
from atst.models import (
|
||||
EnvironmentRole,
|
||||
ApplicationRole,
|
||||
Environment,
|
||||
EnvironmentRole,
|
||||
Application,
|
||||
ApplicationRole,
|
||||
ApplicationRoleStatus,
|
||||
)
|
||||
from atst.domain.exceptions import NotFoundError
|
||||
@ -126,3 +127,15 @@ class EnvironmentRoles(object):
|
||||
.one_or_none()
|
||||
)
|
||||
return existing_env_role
|
||||
|
||||
@classmethod
|
||||
def for_user(cls, user_id, portfolio_id):
|
||||
return (
|
||||
db.session.query(EnvironmentRole)
|
||||
.join(ApplicationRole)
|
||||
.join(Application)
|
||||
.filter(Application.portfolio_id == portfolio_id)
|
||||
.filter(ApplicationRole.application_id == Application.id)
|
||||
.filter(ApplicationRole.user_id == user_id)
|
||||
.all()
|
||||
)
|
||||
|
@ -1,7 +1,8 @@
|
||||
from flask import render_template
|
||||
from flask import render_template, g
|
||||
|
||||
from .blueprint import applications_bp
|
||||
from atst.domain.authz.decorator import user_can_access_decorator as user_can
|
||||
from atst.domain.environment_roles import EnvironmentRoles
|
||||
from atst.models.permissions import Permissions
|
||||
|
||||
|
||||
@ -23,4 +24,11 @@ def has_portfolio_applications(_user, portfolio=None, **_kwargs):
|
||||
message="view portfolio applications",
|
||||
)
|
||||
def portfolio_applications(portfolio_id):
|
||||
return render_template("applications/index.html")
|
||||
user_env_roles = EnvironmentRoles.for_user(g.current_user.id, portfolio_id)
|
||||
environment_access = {
|
||||
env_role.environment_id: env_role.role for env_role in user_env_roles
|
||||
}
|
||||
|
||||
return render_template(
|
||||
"applications/index.html", environment_access=environment_access
|
||||
)
|
||||
|
@ -40,8 +40,7 @@
|
||||
}
|
||||
|
||||
&.col--grow {
|
||||
flex: 1;
|
||||
flex-grow: 1;
|
||||
flex: 1 auto;
|
||||
padding-right: $spacing-small;
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@ $footer-height: 5rem;
|
||||
$usa-banner-height: 2.8rem;
|
||||
$sidenav-expanded-width: 25rem;
|
||||
$sidenav-collapsed-width: 10rem;
|
||||
$max-panel-width: 80rem;
|
||||
|
||||
/*
|
||||
* USWDS Variables
|
||||
|
@ -1,148 +1,50 @@
|
||||
.triangle-box {
|
||||
position: relative;
|
||||
|
||||
.triangle-up {
|
||||
$triangle-size: $gap * 1.5;
|
||||
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: $triangle-size solid transparent;
|
||||
border-right: $triangle-size solid transparent;
|
||||
border-bottom: $triangle-size solid $color-blue-light;
|
||||
bottom: -4px;
|
||||
right: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.accordion {
|
||||
@include block-list;
|
||||
|
||||
box-shadow: 0 4px 10px 0 rgba(193, 193, 193, 0.5);
|
||||
margin-bottom: 6 * $gap;
|
||||
|
||||
.icon-link {
|
||||
margin: (-$gap) 0;
|
||||
}
|
||||
|
||||
.icon-link,
|
||||
.label {
|
||||
&:first-child {
|
||||
margin-left: -$gap;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: -$gap;
|
||||
}
|
||||
}
|
||||
@include shadow-panel;
|
||||
margin: $gap * 3 0;
|
||||
max-width: $max-panel-width;
|
||||
|
||||
&__header {
|
||||
@include block-list-header;
|
||||
padding: $gap * 2 $gap * 3;
|
||||
background-color: $color-white;
|
||||
|
||||
border-top: 3px solid $color-blue;
|
||||
border-bottom: none;
|
||||
box-shadow: 0 2px 4px 0 rgba(216, 218, 222, 0.58);
|
||||
|
||||
&.row {
|
||||
background: $color-white;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
@include block-list__title;
|
||||
|
||||
color: $color-blue;
|
||||
|
||||
@include h3;
|
||||
|
||||
&.icon-link {
|
||||
&-text {
|
||||
margin: 0;
|
||||
display: block;
|
||||
padding: 0 $gap;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
@include block-list__description;
|
||||
|
||||
font-style: italic;
|
||||
font-size: $small-font-size;
|
||||
color: $color-gray;
|
||||
&__button {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
margin-top: $gap;
|
||||
margin-bottom: $gap * 0.5;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
&__content {
|
||||
padding: 0 ($gap * 3) $gap;
|
||||
|
||||
.icon-link {
|
||||
font-size: $small-font-size;
|
||||
&--list-item {
|
||||
border-bottom: 1px solid $color-gray-lightest;
|
||||
padding: $gap 0;
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: $gap;
|
||||
}
|
||||
|
||||
.col {
|
||||
padding-right: $gap * 2;
|
||||
|
||||
&:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: $gap * 2 0 $gap;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1rem;
|
||||
color: $color-gray;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
@include block-list__footer;
|
||||
|
||||
border-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__item {
|
||||
@include block-list-item;
|
||||
|
||||
opacity: 0.75;
|
||||
background-color: $color-blue-light;
|
||||
border-bottom: 1px solid rgba($color-gray-light, 0.5);
|
||||
|
||||
&--selectable {
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
|
||||
@include ie-only {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
> label {
|
||||
@include block-list-selectable-label;
|
||||
}
|
||||
}
|
||||
|
||||
> label {
|
||||
@include block-list-selectable-label;
|
||||
}
|
||||
|
||||
input:checked {
|
||||
+ label {
|
||||
color: $color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
@include ie-only {
|
||||
dl {
|
||||
width: 100%;
|
||||
padding-left: $gap * 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.counter {
|
||||
background-color: $color-cool-blue-light;
|
||||
color: $color-white;
|
||||
border-radius: 2px;
|
||||
padding: ($gap / 2) $gap;
|
||||
margin-left: $gap;
|
||||
}
|
||||
|
||||
.separator {
|
||||
border: 1px solid $color-gray-medium;
|
||||
opacity: 0.75;
|
||||
margin: 0 (0.5 * $gap);
|
||||
}
|
||||
}
|
||||
|
@ -90,4 +90,8 @@
|
||||
padding: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&--primary {
|
||||
@include icon-color($color-primary);
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +1,3 @@
|
||||
.task-order-list {
|
||||
margin-top: 6 * $gap;
|
||||
}
|
||||
|
||||
.task-order-card {
|
||||
&__buttons .usa-button {
|
||||
min-width: 10rem;
|
||||
}
|
||||
|
||||
&__buttons .usa-button-secondary {
|
||||
min-width: 14rem;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: $small-font-size;
|
||||
margin-right: 2 * $gap;
|
||||
min-width: 7rem;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
}
|
||||
|
||||
.task-order {
|
||||
margin-top: $gap * 4;
|
||||
width: 900px;
|
||||
|
@ -1,6 +1,7 @@
|
||||
{% from "components/icon.html" import Icon %}
|
||||
{% from "components/accordion.html" import Accordion %}
|
||||
{% from "components/empty_state.html" import EmptyState %}
|
||||
{% from "components/sticky_cta.html" import StickyCTA %}
|
||||
{% from "components/icon.html" import Icon %}
|
||||
|
||||
{% extends "portfolios/base.html" %}
|
||||
|
||||
@ -31,59 +32,51 @@
|
||||
) }}
|
||||
|
||||
{% else %}
|
||||
|
||||
<div class='application-list'>
|
||||
<div class="usa-accordion">
|
||||
{% for application in portfolio.applications|sort(attribute='name') %}
|
||||
{% set section_name = "application-{}".format(application.id) %}
|
||||
|
||||
<toggler inline-template>
|
||||
<div class='accordion application-list-item'>
|
||||
<header class='accordion__header row'>
|
||||
<div class='col col-grow'>
|
||||
<h3 class='icon-link accordion__title' v-on:click="toggleSection('{{ section_name }}')">{{ application.name }}</h3>
|
||||
<p class='accordion__description'>
|
||||
{{ application.description }}
|
||||
</p>
|
||||
<div class='accordion__actions'>
|
||||
<a class='icon-link' href='{{ url_for("applications.settings", application_id=application.id) }}'>
|
||||
<span>{{ "portfolios.applications.app_settings_text" | translate }}</span>
|
||||
</a>
|
||||
<div class='separator'></div>
|
||||
{% set has_environments = 0 < (application.environments|length) %}
|
||||
<a class='icon-link triangle-box' v-on:click="toggleSection('{{ section_name }}')" disabled="{{ not has_environments }}">
|
||||
<span>Environments ({{ application.environments|length }})</span>
|
||||
{% if has_environments %}
|
||||
<span v-if="selectedSection === '{{ section_name }}'">
|
||||
{{ Icon('caret_up') }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ Icon('caret_down') }}
|
||||
</span>
|
||||
<div class="triangle-up" v-if="selectedSection === '{{ section_name }}'"></div>
|
||||
{% set title = "Environments ({})".format(application.environments|length) %}
|
||||
<div class="accordion">
|
||||
<div class="accordion__header">
|
||||
<h3 class="accordion__header-text">
|
||||
<a href='{{ url_for("applications.settings", application_id=application.id) }}'>
|
||||
{{ application.name }} {{ Icon("caret_right", classes="icon--tiny icon--primary") }}
|
||||
</a>
|
||||
</h3>
|
||||
<p class="accordion__header-text">
|
||||
{{ application.description }}
|
||||
</p>
|
||||
</div>
|
||||
{% call Accordion(
|
||||
title=title,
|
||||
id=section_name,
|
||||
heading_tag="h4"
|
||||
) %}
|
||||
{% for environment in application.environments %}
|
||||
{% set env_access = environment_access[environment.id] %}
|
||||
<div class="accordion__content--list-item">
|
||||
<div class="row">
|
||||
<div class="col col--grow">
|
||||
{% if env_access %}
|
||||
<a href='{{ url_for("applications.access_environment", environment_id=environment.id)}}' target='_blank' rel='noopener noreferrer'>
|
||||
{{ environment.displayname }} {{ Icon('link', classes='icon--medium icon--primary') }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ environment.displayname }}
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
{% if env_access %}
|
||||
<div class="col">
|
||||
{{ env_access }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<ul v-show="selectedSection === '{{ section_name }}'">
|
||||
{% for environment in application.environments %}
|
||||
<li class='accordion__item application-list-item__environment'>
|
||||
<div class='application-list-item__environment__name'>
|
||||
<span>{{ environment.displayname }}</span>
|
||||
</div>
|
||||
{% if g.current_user in environment.users %}
|
||||
<a href='{{ url_for("applications.access_environment", environment_id=environment.id)}}' target='_blank' rel='noopener noreferrer' class='application-list-item__environment__csp_link icon-link'>
|
||||
<span>{{ "portfolios.applications.csp_console_text" | translate }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</toggler>
|
||||
{% endfor %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
{% macro Accordion(
|
||||
title,
|
||||
id,
|
||||
wrapper_tag="div",
|
||||
title,
|
||||
id,
|
||||
wrapper_tag="div",
|
||||
wrapper_classes="",
|
||||
heading_tag="h2",
|
||||
heading_classes="",
|
||||
@ -9,9 +9,9 @@
|
||||
content_classes="") %}
|
||||
<accordion v-cloak inline-template>
|
||||
<{{wrapper_tag}} class="{{ wrapper_classes }}">
|
||||
<{{heading_tag}} class="{{ heading_classes }}">
|
||||
<{{heading_tag}} class="accordion__button {{ heading_classes }}">
|
||||
<button
|
||||
v-on:click="toggle($event)"
|
||||
v-on:click="toggle($event)"
|
||||
class="usa-accordion-button"
|
||||
aria-controls="{{ id }}"
|
||||
v-bind:aria-expanded= "isVisible ? 'true' : 'false'"
|
||||
@ -20,11 +20,11 @@
|
||||
</button>
|
||||
</{{heading_tag}}>
|
||||
<{{content_tag}}
|
||||
id="{{ id }}"
|
||||
class="usa-accordion-content {{ content_classes }}"
|
||||
id="{{ id }}"
|
||||
class="usa-accordion-content accordion__content {{ content_classes }}"
|
||||
v-bind:aria-hidden="isVisible ? 'false' : 'true'">
|
||||
{{ caller() }}
|
||||
</{{content_tag}}>
|
||||
</{{wrapper_tag}}>
|
||||
</accordion>
|
||||
{% endmacro %}
|
||||
{% endmacro %}
|
||||
|
@ -1,3 +1,4 @@
|
||||
{% from "components/accordion.html" import Accordion %}
|
||||
{% from "components/empty_state.html" import EmptyState %}
|
||||
{% from "components/icon.html" import Icon %}
|
||||
{% from "components/sticky_cta.html" import StickyCTA %}
|
||||
@ -6,89 +7,45 @@
|
||||
|
||||
{% block portfolio_content %}
|
||||
|
||||
{% macro TaskOrderButton(task_order, route, text="Edit", secondary=False) %}
|
||||
<a href="{{ url_for(route, task_order_id=task_order.id) }}" class="usa-button {{ 'usa-button-secondary' if secondary else '' }}">
|
||||
{{ text }}
|
||||
</a>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro TaskOrderDateTime(dt, className="") %}
|
||||
<local-datetime timestamp="{{ dt }}" format="MMMM D, YYYY" class="{{ className }}"></local-datetime>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro TaskOrderDate(task_order) %}
|
||||
<span class="datetime">
|
||||
<!-- Draft: {Begins, Began} start_date -->
|
||||
<!-- Everything else: {Starts, Started} start_date | {Ends, Ended} end_date -->
|
||||
|
||||
{% if task_order.is_draft %}
|
||||
{% if task_order.has_begun %}
|
||||
Started on
|
||||
{% else %}
|
||||
Starts on
|
||||
{% endif %}
|
||||
{{ TaskOrderDateTime(task_order.time_created) }}
|
||||
{% else %}
|
||||
{% if task_order.has_begun %}
|
||||
Began
|
||||
{% else %}
|
||||
Begins
|
||||
{% endif %}
|
||||
{{ TaskOrderDateTime(task_order.start_date) }}
|
||||
{% endif %}
|
||||
|
||||
{% if not task_order.is_draft %}
|
||||
|
|
||||
|
||||
{% if task_order.has_ended %}
|
||||
Ended
|
||||
{% else %}
|
||||
Ends
|
||||
{% endif %}
|
||||
|
||||
{{ TaskOrderDateTime(task_order.end_date) }}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro TaskOrderActions(task_order) %}
|
||||
<div class="task-order-card__buttons">
|
||||
{% if task_order.is_draft and user_can(permissions.EDIT_TASK_ORDER_DETAILS) %}
|
||||
{{ TaskOrderButton(task_order, "task_orders.edit")}}
|
||||
{% elif task_order.is_expired %}
|
||||
{{ TaskOrderButton(task_order, "task_orders.review_task_order", text="View") }}
|
||||
{% elif task_order.is_unsigned %}
|
||||
{% if user_can(permissions.EDIT_TASK_ORDER_DETAILS) %}
|
||||
{{ TaskOrderButton(task_order, "task_orders.form_step_four_review", text="Sign", secondary=True) }}
|
||||
{% endif %}
|
||||
{{ TaskOrderButton(task_order, "task_orders.review_task_order", text="View") }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro TaskOrderList(task_orders, label='success') %}
|
||||
<div class="task-order-list">
|
||||
{% for task_order in task_orders %}
|
||||
<div class="card task-order-card">
|
||||
<div class="card__status">
|
||||
<span class='label label--{{ label_colors[task_order.status] }}'>{{ task_order.display_status }}</span>
|
||||
{{ TaskOrderDate(task_order) }}
|
||||
<span class="card__status-spacer"></span>
|
||||
<span class="card__button">
|
||||
{{ TaskOrderActions(task_order) }}
|
||||
</span>
|
||||
{% macro TaskOrderList(task_orders, status) %}
|
||||
{% set status = "All Task Orders" %}
|
||||
<div class="accordion usa-accordion">
|
||||
{% call Accordion(title=status, id=status, heading_tag="h4") %}
|
||||
{% for task_order in task_orders %}
|
||||
<div class="accordion__content--list-item">
|
||||
<h4><a href="{{ url_for('task_orders.review_task_order', task_order_id=task_order.id) }}">Task Order #{{ task_order.number }} {{ Icon("caret_right", classes="icon--tiny icon--primary" ) }}</a></h4>
|
||||
<div class="row">
|
||||
<div class="col col--grow">
|
||||
<h5>
|
||||
Current Period of Performance
|
||||
</h5>
|
||||
<p>
|
||||
{{ task_order.start_date | formattedDate(formatter="%b %d, %Y") }}
|
||||
-
|
||||
{{ task_order.end_date | formattedDate(formatter="%b %d, %Y") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col col--grow">
|
||||
<h5>Total Value</h5>
|
||||
<p>{{ task_order.total_contract_amount | dollars }}</p>
|
||||
</div>
|
||||
<div class="col col--grow">
|
||||
<h5>Total Obligated</h5>
|
||||
<p>{{ task_order.total_obligated_funds | dollars }}</p>
|
||||
</div>
|
||||
<div class="col col--grow">
|
||||
<h5>Total Expended</h5>
|
||||
<p>$0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card__header">
|
||||
<h3>Task Order #{{ task_order.number }}</h3>
|
||||
</div>
|
||||
<div class="card__body">
|
||||
<b>Total amount: </b>{{ task_order.total_contract_amount | dollars }}
|
||||
</div>
|
||||
<div class="card__body">
|
||||
<b>Obligated amount: </b>{{ task_order.total_obligated_funds | dollars }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
@ -136,3 +136,28 @@ def test_get_for_update(application_role, environment):
|
||||
assert role.application_role == application_role
|
||||
assert role.environment == environment
|
||||
assert role.deleted
|
||||
|
||||
|
||||
def test_for_user(application_role):
|
||||
portfolio = application_role.application.portfolio
|
||||
user = application_role.user
|
||||
# create roles for 2 environments associated with application_role fixture
|
||||
env_role_1 = EnvironmentRoleFactory.create(application_role=application_role)
|
||||
env_role_2 = EnvironmentRoleFactory.create(application_role=application_role)
|
||||
|
||||
# create role for environment in a different app in same portfolio
|
||||
application = ApplicationFactory.create(portfolio=portfolio)
|
||||
env_role_3 = EnvironmentRoleFactory.create(
|
||||
application_role=ApplicationRoleFactory.create(
|
||||
application=application, user=user
|
||||
)
|
||||
)
|
||||
|
||||
# create role for environment for random user in app2
|
||||
rando_app_role = ApplicationRoleFactory.create(application=application)
|
||||
rando_env_role = EnvironmentRoleFactory.create(application_role=rando_app_role)
|
||||
|
||||
env_roles = EnvironmentRoles.for_user(user.id, portfolio.id)
|
||||
assert len(env_roles) == 3
|
||||
assert env_roles == [env_role_1, env_role_2, env_role_3]
|
||||
assert not rando_env_role in env_roles
|
||||
|
Loading…
x
Reference in New Issue
Block a user