Merge pull request #1232 from dod-ccpo/to-index-page-redesign

TO and App index pages redesign (Part 1)
This commit is contained in:
leigh-mil 2019-12-11 11:54:21 -05:00 committed by GitHub
commit 2af99da9cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 172 additions and 292 deletions

View File

@ -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()
)

View File

@ -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
)

View File

@ -40,8 +40,7 @@
}
&.col--grow {
flex: 1;
flex-grow: 1;
flex: 1 auto;
padding-right: $spacing-small;
}

View File

@ -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

View File

@ -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);
}
}

View File

@ -90,4 +90,8 @@
padding: 2px;
}
}
&--primary {
@include icon-color($color-primary);
}
}

View File

@ -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;

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}
&nbsp;&nbsp;|&nbsp;&nbsp;
{% 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 %}

View File

@ -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