diff --git a/atst/app.py b/atst/app.py index 7d5cc0c6..afaeb6fd 100644 --- a/atst/app.py +++ b/atst/app.py @@ -22,6 +22,7 @@ from atst.domain.authnid.crl import CRLCache, NoOpCRLCache from atst.domain.auth import apply_authentication from atst.domain.authz import Authorization from atst.domain.csp import make_csp_provider +from atst.domain.portfolios import Portfolios from atst.models.permissions import Permissions from atst.eda_client import MockEDAClient from atst.utils import mailer @@ -90,6 +91,14 @@ def make_flask_callbacks(app): g.Authorization = Authorization g.Permissions = Permissions + @app.context_processor + def _portfolios(): + if not g.current_user: + return {} + + portfolios = Portfolios.for_user(g.current_user) + return {"portfolios": portfolios} + @app.after_request def _cleanup(response): g.current_user = None diff --git a/atst/routes/__init__.py b/atst/routes/__init__.py index ac05105f..c1acc655 100644 --- a/atst/routes/__init__.py +++ b/atst/routes/__init__.py @@ -13,6 +13,7 @@ from atst.domain.authnid import AuthenticationContext from atst.domain.audit_log import AuditLog from atst.domain.auth import logout as _logout from atst.domain.common import Paginator +from atst.domain.portfolios import Portfolios from atst.utils.flash import formatted_flash as flash @@ -52,16 +53,16 @@ def home(): if user.atat_role_name == "ccpo": return redirect(url_for("requests.requests_index")) - num_portfolios = len(user.portfolio_roles) + num_portfolios = len([role for role in user.portfolio_roles if role.is_active]) if num_portfolios == 0: return redirect(url_for("requests.requests_index")) elif num_portfolios == 1: portfolio_role = user.portfolio_roles[0] portfolio_id = portfolio_role.portfolio.id - is_request_owner = portfolio_role.role.name == "owner" + is_portfolio_owner = portfolio_role.role.name == "owner" - if is_request_owner: + if is_portfolio_owner: return redirect( url_for("portfolios.portfolio_reports", portfolio_id=portfolio_id) ) @@ -70,7 +71,13 @@ def home(): url_for("portfolios.portfolio_applications", portfolio_id=portfolio_id) ) else: - return redirect(url_for("portfolios.portfolios")) + portfolios = Portfolios.for_user(g.current_user) + first_portfolio = sorted(portfolios, key=lambda portfolio: portfolio.name)[0] + return redirect( + url_for( + "portfolios.portfolio_applications", portfolio_id=first_portfolio.id + ) + ) @bp.route("/styleguide") diff --git a/atst/routes/portfolios/__init__.py b/atst/routes/portfolios/__init__.py index 990c7c2e..ad935378 100644 --- a/atst/routes/portfolios/__init__.py +++ b/atst/routes/portfolios/__init__.py @@ -15,14 +15,12 @@ from atst.models.permissions import Permissions @portfolios_bp.context_processor def portfolio(): - portfolios = Portfolios.for_user(g.current_user) portfolio = None if "portfolio_id" in http_request.view_args: try: portfolio = Portfolios.get( g.current_user, http_request.view_args["portfolio_id"] ) - portfolios = [ws for ws in portfolios if not ws.id == portfolio.id] except UnauthorizedError: pass @@ -33,9 +31,4 @@ def portfolio(): ) return False - return { - "portfolio": portfolio, - "portfolios": portfolios, - "permissions": Permissions, - "user_can": user_can, - } + return {"portfolio": portfolio, "permissions": Permissions, "user_can": user_can} diff --git a/styles/components/_global_layout.scss b/styles/components/_global_layout.scss index ffc02311..a80b4ca5 100644 --- a/styles/components/_global_layout.scss +++ b/styles/components/_global_layout.scss @@ -16,10 +16,6 @@ flex-grow: 1; margin-bottom: $footer-height; - .global-navigation { - margin-top: -1px; - } - .global-panel-container { margin: $gap; max-width: $site-max-width; diff --git a/styles/components/_topbar.scss b/styles/components/_topbar.scss index 54424924..5740b3f9 100644 --- a/styles/components/_topbar.scss +++ b/styles/components/_topbar.scss @@ -1,5 +1,5 @@ .topbar { - background-color: $color-white; + background-color: $color-blue-darkest; border-bottom: 1px solid $color-black; .topbar__navigation { @@ -9,7 +9,7 @@ justify-content: space-between; .topbar__link { - color: $color-black; + color: $color-white; display: inline-flex; align-items: center; height: $topbar-height; @@ -18,10 +18,12 @@ .topbar__link-label { @include h5; + text-decoration: underline; } .topbar__link-icon { margin-left: $gap; + @include icon-color($color-white); } &.topbar__link--home { @@ -29,6 +31,7 @@ .topbar__link-label { padding-left: $gap; + text-decoration: none; } } @@ -45,10 +48,6 @@ &:hover { background-color: $color-primary-darker; color: $color-white; - - .topbar__link-icon { - @include icon-style-inverted; - } } } @@ -62,53 +61,6 @@ .topbar__portfolio-menu { margin-right: auto; position: relative; - - .topbar__portfolio-menu__toggle { - margin: 0; - border-radius: 0; - - &--open { - background-color: $color-blue-darkest; - position: relative; - - &::before { - content: ''; - display: block; - position: absolute; - bottom: 0; - left: $gap * 2; - right: $gap * 2; - height: $gap / 2; - background-color: $color-primary; - } - } - - .icon { - @include icon-size(10); - margin-left: $gap * 2; - } - } - - .topbar__portfolio-menu__panel { - position: absolute; - } - } - - &.topbar__context--portfolio { - background-color: $color-primary; - -ms-flex-pack: start; - - .topbar__link { - color: $color-white; - - .topbar__link-icon { - @include icon-style-inverted; - } - - &:hover { - background-color: $color-primary-darker; - } - } } } } diff --git a/styles/elements/_sidenav.scss b/styles/elements/_sidenav.scss index e335036c..dcfcfe10 100644 --- a/styles/elements/_sidenav.scss +++ b/styles/elements/_sidenav.scss @@ -1,22 +1,54 @@ .sidenav { + @include hide; + + @include media($large-screen) { + @include unhide; + width: 25rem; + margin: 0px; + } + + box-shadow: 0 6px 18px 0 rgba(48,58,65,0.15); + + .sidenav__title { + color: $color-gray-dark; + padding: $gap ($gap * 2); + text-transform: uppercase; + opacity: 0.54; + font-size: $small-font-size; + font-weight: bold; + } + ul { + &.sidenav__list--padded { + margin: 4 * $gap 0; + } + list-style: none; - margin: 0; padding: 0; - li { + li { margin: 0; display: block; } + + } + + .sidenav__divider--small { + display: block; + width: 4 * $gap; + border: 1px solid #D6D7D9; + margin-left: 2 * $gap; + margin-bottom: $gap; } .sidenav__link { display: block; - border-top: 1px solid $color-black; padding: $gap ($gap * 2); color: $color-black; text-decoration: none; white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; .sidenav__link-icon { margin-left: - ($gap * .5); @@ -27,16 +59,33 @@ pointer-events: none; } + &.sidenav__link--add { + color: $color-blue; + font-size: $small-font-size; + .icon { + @include icon-color($color-blue); + @include icon-size(14); + } + + } + &.sidenav__link--active { @include h4; color: $color-primary; - background-color: $color-white; + background-color: $color-aqua-lightest; box-shadow: inset ($gap / 2) 0 0 0 $color-primary; .sidenav__link-icon { @include icon-style-active; } + position: relative; + .sidenav__link-active_indicator .icon { + @include icon-color($color-primary); + position: absolute; + right: 0; + } + + ul { background-color: $color-primary; @@ -89,6 +138,7 @@ &:hover { color: $color-primary; + background-color: $color-aqua-lightest; .sidenav__link-icon { @include icon-style-active; @@ -97,3 +147,14 @@ } } } + +.sidenav--minimized { + @extend .sidenav; + + @include unhide; + margin: 0px; + + @include media($large-screen) { + @include hide; + } +} diff --git a/templates/base.html b/templates/base.html index 31d220df..89f56b4a 100644 --- a/templates/base.html +++ b/templates/base.html @@ -21,7 +21,9 @@ {% include 'navigation/topbar.html' %}
- {% include 'navigation/global_navigation.html' %} + {% block global_sidenav %} + {% include 'navigation/global_sidenav.html' %} + {% endblock %}
{% block sidenav %}{% endblock %} diff --git a/templates/base_public.html b/templates/base_public.html index d718a677..248c302a 100644 --- a/templates/base_public.html +++ b/templates/base_public.html @@ -17,31 +17,7 @@
{% include 'components/usa_header.html' %} -
- -
+ {% include 'navigation/topbar.html' %} {% block content %}{% endblock %} diff --git a/templates/components/sidenav_item.html b/templates/components/sidenav_item.html index 64f5b765..8e3c5135 100644 --- a/templates/components/sidenav_item.html +++ b/templates/components/sidenav_item.html @@ -7,7 +7,14 @@ {{ Icon(icon, classes="sidenav__link-icon") }} {% endif %} - {{label}} + + {{label}} + + {% if active %} + + {{ Icon("caret_right") }} + + {% endif %} {% if subnav and active %} diff --git a/templates/nav-side.html.to b/templates/nav-side.html.to deleted file mode 100644 index 66ad799f..00000000 --- a/templates/nav-side.html.to +++ /dev/null @@ -1,16 +0,0 @@ -
- - - -
diff --git a/templates/navigation/global_navigation.html b/templates/navigation/global_navigation.html deleted file mode 100644 index 90f9bac5..00000000 --- a/templates/navigation/global_navigation.html +++ /dev/null @@ -1,19 +0,0 @@ -{% from "components/sidenav_item.html" import SidenavItem %} - - diff --git a/templates/navigation/global_sidenav.html b/templates/navigation/global_sidenav.html new file mode 100644 index 00000000..00783943 --- /dev/null +++ b/templates/navigation/global_sidenav.html @@ -0,0 +1,24 @@ +{% from "components/icon.html" import Icon %} +{% from "components/sidenav_item.html" import SidenavItem %} + + + + + diff --git a/templates/navigation/topbar.html b/templates/navigation/topbar.html index 1f3d3164..0defde8a 100644 --- a/templates/navigation/topbar.html +++ b/templates/navigation/topbar.html @@ -2,71 +2,30 @@
diff --git a/tests/routes/requests/requests_form/test_new.py b/tests/routes/requests/requests_form/test_new.py index 81d195a8..bc3c1705 100644 --- a/tests/routes/requests/requests_form/test_new.py +++ b/tests/routes/requests/requests_form/test_new.py @@ -1,5 +1,6 @@ import datetime import re +import pytest from tests.factories import ( RequestFactory, UserFactory, @@ -78,6 +79,7 @@ def test_ccpo_can_view_request(client, user_session): assert response.status_code == 200 +@pytest.mark.skip(reason="create request flow no longer active") def test_nonexistent_request(client, user_session): user_session() response = client.get("/requests/new/1/foo", follow_redirects=True) diff --git a/tests/routes/test_home.py b/tests/routes/test_home.py index 01cd934c..b2dd8d95 100644 --- a/tests/routes/test_home.py +++ b/tests/routes/test_home.py @@ -2,30 +2,7 @@ import pytest from tests.factories import UserFactory, PortfolioFactory, RequestFactory from atst.domain.portfolios import Portfolios - - -def test_user_with_portfolios_has_portfolios_nav(client, user_session): - portfolio = PortfolioFactory.create() - user_session(portfolio.owner) - response = client.get("/home", follow_redirects=True) - assert b'href="/portfolios"' in response.data - - -@pytest.mark.skip(reason="this may no longer be accurate") -def test_user_without_portfolios_has_no_portfolios_nav(client, user_session): - user = UserFactory.create() - user_session(user) - response = client.get("/home", follow_redirects=True) - assert b'href="/portfolios"' not in response.data - - -@pytest.mark.skip(reason="this may no longer be accurate") -def test_request_owner_with_no_portfolios_redirected_to_requests(client, user_session): - request = RequestFactory.create() - user_session(request.creator) - response = client.get("/home", follow_redirects=False) - - assert "/requests" in response.location +from atst.models.portfolio_role import Status as PortfolioRoleStatus def test_request_owner_with_one_portfolio_redirected_to_reports(client, user_session): @@ -51,22 +28,14 @@ def test_request_owner_with_more_than_one_portfolio_redirected_to_portfolios( assert "/portfolios" in response.location -@pytest.mark.skip(reason="this may no longer be accurate") -def test_non_owner_user_with_no_portfolios_redirected_to_requests(client, user_session): - user = UserFactory.create() - - user_session(user) - response = client.get("/home", follow_redirects=False) - - assert "/requests" in response.location - - def test_non_owner_user_with_one_portfolio_redirected_to_portfolio_applications( client, user_session ): user = UserFactory.create() portfolio = PortfolioFactory.create() - Portfolios._create_portfolio_role(user, portfolio, "developer") + Portfolios._create_portfolio_role( + user, portfolio, "developer", status=PortfolioRoleStatus.ACTIVE + ) user_session(user) response = client.get("/home", follow_redirects=False) @@ -78,14 +47,20 @@ def test_non_owner_user_with_mulitple_portfolios_redirected_to_portfolios( client, user_session ): user = UserFactory.create() + portfolios = [] for _ in range(3): portfolio = PortfolioFactory.create() - Portfolios._create_portfolio_role(user, portfolio, "developer") + portfolios.append(portfolio) + role = Portfolios._create_portfolio_role( + user, portfolio, "developer", status=PortfolioRoleStatus.ACTIVE + ) user_session(user) response = client.get("/home", follow_redirects=False) + alphabetically_first_portfolio = sorted(portfolios, key=lambda p: p.name)[0] assert "/portfolios" in response.location + assert str(alphabetically_first_portfolio.id) in response.location @pytest.mark.skip(reason="this may no longer be accurate") diff --git a/translations.yaml b/translations.yaml index 46df4898..1f5202b6 100644 --- a/translations.yaml +++ b/translations.yaml @@ -290,7 +290,7 @@ login: title_tag: Sign in | JEDI Cloud navigation: topbar: - jedi_cloud_link_text: JEDI Cloud + jedi_cloud_link_text: JEDI logout_link_title: Log out of JEDI Cloud named_portfolio: 'Portfolio {portfolio}' no_other_active_portfolios: You have no other active JEDI portfolios.