From fa3f0644e7d49a01c8679902b42fbba28b24decd Mon Sep 17 00:00:00 2001
From: Dale Cannon <118175145+dalecannon@users.noreply.github.com>
Date: Wed, 20 Mar 2024 15:57:13 +0000
Subject: [PATCH 1/4] TP2000-1241 Implement new homepage design (#1173)
* Update header and footer
* Add new homepage
* Add search bar to homepage
* Add tests
* Filter in ERRORED workbaskets
* Tweak homepage card padding
* Add Resources view
* Style workbasket action links as buttons
* Remove unused permission boolean
* Change worbasket action buttons styling
* Save workbasket icon in svg file
* Close img tag
* Add docstring to get_search_result
* Add missing alt attribute on workbasket icon img
* Amend doctstring
* Keep search icon inline if form error message is displayed
* Add placeholder cards and text for Test & Train session
* Add notification icon placeholder
* Tweak UI for Test & Train session
* Revert test & train placeholder code
* Fix homepage view unit test
---
common/forms.py | 113 +++-----
common/jinja2/common/homepage.jinja | 165 +++++++++++
common/jinja2/common/maintenance.jinja | 21 +-
common/jinja2/common/resources.jinja | 93 +++++++
common/jinja2/common/search_page.jinja | 2 +-
common/jinja2/common/workbasket_action.jinja | 18 --
common/jinja2/layouts/layout.jinja | 166 ++++++-----
common/static/common/images/search-icon.svg | 4 +
.../static/common/images/workbasket-icon.svg | 4 +
common/static/common/scss/_badges.scss | 13 +
common/static/common/scss/_components.scss | 73 ++++-
common/static/common/scss/_environment.scss | 24 --
common/static/common/scss/application.scss | 1 -
common/tests/test_views.py | 257 +++++++++++-------
common/urls.py | 1 +
common/views.py | 210 ++++++++++++--
pii-secret-exclude.txt | 1 +
workbaskets/tests/test_views.py | 5 +-
18 files changed, 849 insertions(+), 322 deletions(-)
create mode 100644 common/jinja2/common/homepage.jinja
create mode 100644 common/jinja2/common/resources.jinja
delete mode 100644 common/jinja2/common/workbasket_action.jinja
create mode 100644 common/static/common/images/search-icon.svg
create mode 100644 common/static/common/images/workbasket-icon.svg
delete mode 100644 common/static/common/scss/_environment.scss
diff --git a/common/forms.py b/common/forms.py
index b2240f482..4cf740eed 100644
--- a/common/forms.py
+++ b/common/forms.py
@@ -5,22 +5,20 @@
from crispy_forms_gds.fields import DateInputField
from crispy_forms_gds.helper import FormHelper
-from crispy_forms_gds.layout import HTML
from crispy_forms_gds.layout import Div
from crispy_forms_gds.layout import Field
-from crispy_forms_gds.layout import Fieldset
from crispy_forms_gds.layout import Layout
from crispy_forms_gds.layout import Size
from crispy_forms_gds.layout import Submit
from django import forms
from django.contrib.postgres.forms.ranges import DateRangeField
from django.core.exceptions import ValidationError
-from django.db.models import TextChoices
from django.forms.renderers import get_default_renderer
from django.forms.utils import ErrorList
from common.util import TaricDateRange
from common.util import get_model_indefinite_article
+from common.validators import AlphanumericValidator
from common.widgets import FormSetFieldWidget
from common.widgets import MultipleFileInput
from common.widgets import RadioNestedWidget
@@ -235,85 +233,6 @@ def get_bound_field(self, form, field_name):
return super().get_bound_field(form, field_name)
-class WorkbasketActions(TextChoices):
- CREATE = "CREATE", "Create a new workbasket"
- EDIT = "EDIT", "Edit workbaskets"
-
-
-class DITTariffManagerActions(TextChoices):
- PACKAGE_WORKBASKETS = "PACKAGE_WORKBASKETS", "Package workbaskets"
-
-
-class HMRCCDSManagerActions(TextChoices):
- PROCESS_ENVELOPES = "PROCESS_ENVELOPES", "Process envelopes"
-
-
-class CommonUserActions(TextChoices):
- SEARCH = "SEARCH", "Search the tariff"
- # Change this to be dependent on permissions later
-
-
-class ImportUserActions(TextChoices):
- IMPORT = "IMPORT", "Import EU TARIC files"
-
-
-class WorkbasketManagerActions(TextChoices):
- WORKBASKET_LIST_ALL = "WORKBASKET_LIST_ALL", "Search for workbaskets"
-
-
-class HomeForm(forms.Form):
- def __init__(self, *args, **kwargs):
- self.user = kwargs.pop("user")
- super().__init__(*args, **kwargs)
-
- choices = []
-
- if self.user.has_perm("workbaskets.add_workbasket"):
- choices += WorkbasketActions.choices
-
- if self.user.has_perm("publishing.manage_packaging_queue"):
- choices += DITTariffManagerActions.choices
-
- if self.user.has_perm("publishing.consume_from_packaging_queue"):
- choices += HMRCCDSManagerActions.choices
-
- choices += CommonUserActions.choices
-
- if self.user.has_perm("common.add_trackedmodel") or self.user.has_perm(
- "common.change_trackedmodel",
- ):
- choices += ImportUserActions.choices
-
- if self.user.has_perm("workbaskets.view_workbasket"):
- choices += WorkbasketManagerActions.choices
-
- self.fields["workbasket_action"] = forms.ChoiceField(
- label="",
- choices=choices,
- widget=forms.RadioSelect,
- required=True,
- )
-
- self.helper = FormHelper(self)
- self.helper.layout = Layout(
- Fieldset(
- HTML.h3("What would you like to do?"),
- HTML.details(
- "What is a workbasket?",
- "A workbasket is used to collect all the changes you make to the UK's import and export tariff data. "
- "Workbaskets group these changes so they can be checked by the Customs Declaration Service (CDS) before going live.",
- ),
- "workbasket_action",
- ),
- Submit(
- "submit",
- "Continue",
- data_module="govuk-button",
- data_prevent_double_click="true",
- ),
- )
-
-
class DescriptionHelpBox(Div):
template = "components/description_help.jinja"
@@ -814,3 +733,33 @@ def clean(self, data, initial=None):
else:
files = cleaned_data(data, initial)
return files
+
+
+class HomeSearchForm(forms.Form):
+ search_term = forms.CharField(
+ required=False,
+ widget=forms.TextInput(
+ attrs={"placeholder": "Search by tariff element name or ID"},
+ ),
+ validators=[AlphanumericValidator],
+ max_length=18,
+ )
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.helper = FormHelper(self)
+ self.helper.label_size = Size.SMALL
+ self.helper.legend_size = Size.SMALL
+ self.helper.layout = Layout(
+ Div(
+ Field.text("search_term"),
+ Submit(
+ "submit",
+ "Search",
+ data_module="govuk-button",
+ data_prevent_double_click="true",
+ ),
+ css_id="homepage-search-form",
+ ),
+ )
diff --git a/common/jinja2/common/homepage.jinja b/common/jinja2/common/homepage.jinja
new file mode 100644
index 000000000..dd1c2da6f
--- /dev/null
+++ b/common/jinja2/common/homepage.jinja
@@ -0,0 +1,165 @@
+{% extends "layouts/form.jinja" %}
+
+{% set page_title = "Home" %}
+
+{% block content %}
+
Search the Tariff Application Platform
+ This service lets you manage the UK's import and export tariff data
+
+ {% block form %}
+
+
+ {% call django_form() %}
+ {{ crispy(form) }}
+ {% endcall %}
+
+
+ {% endblock %}
+
+
+ {% if can_view_workbasket %}
+
+ What would you like to do?
+
+ Create a new workbasket, edit an existing workbasket, package or search for workbaskets.
+
+
+
+ {% if can_add_workbasket %}
+
+
+
+ {% endif %}
+
+ {% if can_manage_packaging %}
+
+ {% endif %}
+
+
+
+
+
+
+ Currently working on
+ {% if not assigned_workbaskets %}
+ You are not currently assigned to a workbasket.
+ {% else %}
+
+ {% for workbasket in assigned_workbaskets %}
+ -
+
+ Workbasket ID {{ workbasket["id"] }}
+
+ {% if workbasket["rule_violations_count"] %}
+ - {{ workbasket["assignment_type"] }}, rule violation{{ workbasket["rule_violations_count"]|pluralize }}
+ {{ workbasket["rule_violations_count"] }}
+ {% else %}
+ - {{ workbasket["assignment_type"] }}
+ {% endif %}
+
+
+
+ {% endfor %}
+
+ {% endif %}
+
+ {% endif %}
+
+ {% if can_import_taric %}
+
+ EU TARIC files
+
+ View EU import list and import new TARIC files
+
+
+ {% endif %}
+
+ {% if can_consume_packaging %}
+
+ Envelopes
+
+ Process envelopes
+
+
+ {% endif %}
+
+
+ Resources
+
+ We have a range of resources you can use with the Tariff Application Platform.
+
+
+ -
+ Application information
+
+ {% if request.user.is_superuser %}
+ -
+ Importer V1
+
+ -
+ Importer V2
+
+ {% endif %}
+ {% if can_view_reports %}
+ -
+ TAP reports
+
+ {% endif %}
+ -
+ Tariff data manual
+
+
+
+
+
+
+
+
+
How can we support you?
+
+
+ Get help
+
+ Find documentation, guidance, training and updates in the
+
+ TAP help centre.
+
+
+
+
+
+ Get in touch
+ Contact us if you have a question or suggestion.
+
+
+{% endblock %}
diff --git a/common/jinja2/common/maintenance.jinja b/common/jinja2/common/maintenance.jinja
index 3aafad122..a0b8a5e46 100644
--- a/common/jinja2/common/maintenance.jinja
+++ b/common/jinja2/common/maintenance.jinja
@@ -3,13 +3,24 @@
{% set page_title = "Sorry, the service is unavailable" %}
{% set workbasket_html %}{% endset %}
+{% set logout_login_link %}{% endset %}
+{% set can_view_workbasket %}{% endset %}
+{% set can_import_taric %}{% endset %}
+{% set can_manage_packaging %}{% endset %}
+{% set can_consume_packaging %}{% endset %}
{% block header %}
-{{ govukHeader({
- "homepageUrl": "https://gov.uk/",
- "serviceName": service_name,
- "serviceUrl": "/",
-}) }}
+
{% endblock %}
{% block breadcrumb %}{% endblock %}
diff --git a/common/jinja2/common/resources.jinja b/common/jinja2/common/resources.jinja
new file mode 100644
index 000000000..972cb25f0
--- /dev/null
+++ b/common/jinja2/common/resources.jinja
@@ -0,0 +1,93 @@
+{% extends "layouts/layout.jinja" %}
+
+{% set page_title = "Resources" %}
+
+{% macro resource(heading, description, url, open_new_tab="") %}
+
+
+
+{% endmacro %}
+
+{% block breadcrumb %}
+
+
+ -
+ Home
+
+ -
+ {{ page_title }}
+
+
+
+{% endblock %}
+
+{% block content %}
+
+
+
{{ page_title }}
+
Tariff Application Platform offers access to a range of resources.
+
+
+
+
+
+
+ {{
+ resource(
+ heading="Application information",
+ description="",
+ url=url("app-info"),
+ )
+ }}
+
+ {{
+ resource(
+ heading="Importer V1",
+ description="",
+ url=url("import_batch-ui-list"),
+ )
+ }}
+
+ {{ resource(
+ heading="Importer V2",
+ description="",
+ url=url("taric_parser_import_ui_list"),
+ )}}
+
+ {{
+ resource(
+ heading="TAP reports",
+ description="",
+ url=url("reports:index"),
+ )
+ }}
+
+ {{
+ resource(
+ heading="Tariff data manual",
+ description="",
+ url="https://uktrade.github.io/tariff-data-manual/#home",
+ open_new_tab="True",
+ )
+ }}
+
+
+
+{% endblock %}
diff --git a/common/jinja2/common/search_page.jinja b/common/jinja2/common/search_page.jinja
index 3111d607d..534f4e0cb 100644
--- a/common/jinja2/common/search_page.jinja
+++ b/common/jinja2/common/search_page.jinja
@@ -1,7 +1,7 @@
{% extends "layouts/layout.jinja" %}
{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %}
-{% set page_title = "Search the Tariff Management Tool" %}
+{% set page_title = "Search the Tariff Application Platform" %}
{% set page_subtitle = "What would you like to do?" %}
{% block breadcrumb %}
diff --git a/common/jinja2/common/workbasket_action.jinja b/common/jinja2/common/workbasket_action.jinja
deleted file mode 100644
index 78a15d3bb..000000000
--- a/common/jinja2/common/workbasket_action.jinja
+++ /dev/null
@@ -1,18 +0,0 @@
-{% extends "layouts/form.jinja" %}
-
-{% set page_title = "Tariff Management Tool" %}
-
-{% block content %}
- Tariff management tool
- This service lets you manage UK trade tariffs
-
- {% block form %}
-
-
- {% call django_form() %}
- {{ crispy(form) }}
- {% endcall %}
-
-
- {% endblock %}
-{% endblock %}
diff --git a/common/jinja2/layouts/layout.jinja b/common/jinja2/layouts/layout.jinja
index bcbf54251..8f0d41ec1 100644
--- a/common/jinja2/layouts/layout.jinja
+++ b/common/jinja2/layouts/layout.jinja
@@ -4,116 +4,144 @@
{% extends "layouts/_generic.jinja" %}
-{% set service_name = "Manage Trade Tariffs" %}
+{% set service_name = "Tariff Application Platform" %}
+{% set phase_banner_tag = "Alpha" %}
{% block pageTitle %}
{{ page_title }} | {{ service_name }}
{% endblock %}
-{% set workbasket_svg %}
-
-{% endset %}
-
-{% block header %}
-
{% set workbasket_html %}
{% if request.user.current_workbasket %}
+
+ class="govuk-header__link workbasket-link govuk-header__navigation-item"
+ >Workbasket ID {{ request.user.current_workbasket.id }}
{% endif %}
{% endset %}
{% set logout_login_link %}
{% if request.user.is_authenticated %}
-
{% else %}
-
+
{% endif %}
{% endset %}
-{{ govukHeader({
- "homepageUrl": "https://gov.uk/",
- "serviceName": service_name,
- "serviceUrl": "/",
- "navigation": [
- { "html": workbasket_html } if workbasket_html else "",
- {
- "html": logout_login_link,
- } if not settings.SSO_ENABLED else "",
- ],
- "useTudorCrown": true,
-}) }}
+{% set can_view_workbasket = request.user.has_perm("workbaskets.view_workbasket") %}
+{% set can_import_taric = request.user.has_perm("common.add_trackedmodel") %}
+{% set can_manage_packaging = request.user.has_perm("publishing.manage_packaging_queue") %}
+{% set can_consume_packaging = request.user.has_perm("publishing.consume_from_packaging_queue") %}
+
+{% block header %}
+
{% endblock %}
{% block beforeContent %}
- {{ govukPhaseBanner({
- "tag": {
- "text": "alpha"
- },
- "text": "This is a new service – your feedback will help us to improve it."
- }) }}
{% block breadcrumb %}
- {% if request.path != "/" %}
- {{ breadcrumbs(request, [{"text": page_title}]) }}
- {% endif %}
+ {% if request.path != "/" %}
+ {{ breadcrumbs(request, [{"text": page_title}]) }}
+ {% endif %}
{% endblock %}
-
- {% if env != "production"%}
-
- {% endif %}
{% endblock %}
{% block footer %}
{{ govukFooter({
"meta": {
"items": [
- {
- "href": "https://workspace.trade.gov.uk/working-at-dbt/policies-and-guidance/policies/tariff-application-privacy-policy/",
- "text": "Privacy policy"
- },
{
"href": url("accessibility-statement"),
"text": "Accessibility statement"
},
{
- "href": "https://uktrade.github.io/tariff-data-manual/#home",
- "text": "Tariff data manual"
- },
- {
- "href": url("app-info"),
- "text": "Application information"
- },
- {
- "href": url("import_batch-ui-list"),
- "text": "Importer V1"
- },
- {
- "href": url("taric_parser_import_ui_list"),
- "text": "Importer V2"
+ "href": "https://workspace.trade.gov.uk/working-at-dbt/policies-and-guidance/policies/tariff-application-privacy-policy/",
+ "text": "Privacy policy"
},
{
- "href": url("reports:index"),
- "text": "TAP reports"
+ "href": "https://data-services-help.trade.gov.uk/tariff-application-platform/",
+ "text": "Help centre"
},
]
}
diff --git a/common/static/common/images/search-icon.svg b/common/static/common/images/search-icon.svg
new file mode 100644
index 000000000..7b202b2e0
--- /dev/null
+++ b/common/static/common/images/search-icon.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/common/static/common/images/workbasket-icon.svg b/common/static/common/images/workbasket-icon.svg
new file mode 100644
index 000000000..67e30ae4b
--- /dev/null
+++ b/common/static/common/images/workbasket-icon.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/common/static/common/scss/_badges.scss b/common/static/common/scss/_badges.scss
index 6ce776ddd..fdcece3f2 100644
--- a/common/static/common/scss/_badges.scss
+++ b/common/static/common/scss/_badges.scss
@@ -56,3 +56,16 @@
color: govuk-shade(govuk-colour("green"), 20);
background: govuk-tint(govuk-colour("green"), 80);
}
+
+.rule-violation-badge {
+ display: inline-block;
+ font-weight: bold;
+ color: govuk-colour("white");
+ background-color: $govuk-error-colour;
+ min-height: 16px;
+ min-width: 16px;
+ line-height: 1;
+ text-align: center;
+ border-radius: 50%;
+ padding: 4px;
+}
\ No newline at end of file
diff --git a/common/static/common/scss/_components.scss b/common/static/common/scss/_components.scss
index a0c445891..acbd03f0d 100644
--- a/common/static/common/scss/_components.scss
+++ b/common/static/common/scss/_components.scss
@@ -43,7 +43,7 @@
}
}
- button#sign-out {
+ .logout-login {
color: govuk-colour("white");
font-weight: bold;
text-decoration: none;
@@ -90,3 +90,74 @@
}
}
+
+.homepage-card {
+ box-sizing: border-box;
+ border: 1px solid $govuk-border-colour;
+ padding: govuk-spacing(3) govuk-spacing(3) 0;
+ margin: govuk-spacing(3);
+ width: calc(100% - govuk-spacing(6));
+ float: left;
+}
+
+@media screen and (min-width: 1063px) {
+ .homepage-card {
+ width: calc(50% - govuk-spacing(6));
+ }
+}
+
+#homepage-tap-resources {
+ columns: 2;
+}
+
+.homepage-workbasket-details {
+ display: inline-block;
+}
+
+#current-workbasket {
+ img {
+ vertical-align: text-bottom;
+ }
+ a, form {
+ display: inline-block;
+ }
+}
+
+@media screen and (min-width: 770px) {
+ #current-workbasket {
+ float: right;
+ }
+}
+
+#homepage-search-form {
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: flex-end;
+
+ .govuk-input {
+ height: 50px;
+ font-size: 21px;
+ }
+
+ .govuk-form-group {
+ width: 100%;
+ }
+
+ .govuk-label {
+ @extend .govuk-visually-hidden
+ }
+
+ .govuk-button {
+ background: no-repeat url("/common/static/common/images/search-icon.svg");
+ background-color: govuk-colour("black");
+ background-position: center;
+ color: transparent;
+ margin-left: 0;
+ width: 50px;
+ height: 48px;
+ }
+}
+
+.homepage-workbasket-action {
+ width: 100%;
+}
diff --git a/common/static/common/scss/_environment.scss b/common/static/common/scss/_environment.scss
deleted file mode 100644
index f24043de9..000000000
--- a/common/static/common/scss/_environment.scss
+++ /dev/null
@@ -1,24 +0,0 @@
-.environment-banner {
- position: fixed;
- top: 0;
- right: 0;
- width: 0;
- height: 0;
- border-left: 150px solid transparent;
- border-top: 150px solid green;
- color: white;
- text-align: center;
- font-size: 18px;
-
- .text-wrapper {
- position: absolute;
- font-weight: 900;
- text-transform: uppercase;
- transform: translate(-90%, -325%) rotate(45deg);
- color: white;
- background-color: transparent;
- padding: 4px;
- white-space: nowrap;
- }
-}
-
diff --git a/common/static/common/scss/application.scss b/common/static/common/scss/application.scss
index 3ac96053c..4bda561ef 100644
--- a/common/static/common/scss/application.scss
+++ b/common/static/common/scss/application.scss
@@ -34,4 +34,3 @@ $govuk-image-url-function: frontend-image-url;
@import "quota-definitions";
@import "commodities";
@import "utils";
-@import "environment";
diff --git a/common/tests/test_views.py b/common/tests/test_views.py
index efb6ed520..0e07bc0ed 100644
--- a/common/tests/test_views.py
+++ b/common/tests/test_views.py
@@ -1,5 +1,3 @@
-import re
-
import pytest
from bs4 import BeautifulSoup
from django.conf import settings
@@ -7,43 +5,20 @@
from django.test import modify_settings
from django.test import override_settings
from django.urls import reverse
+from django.urls import reverse_lazy
+from checks.tests.factories import TrackedModelCheckFactory
from common.tests import factories
from common.util import xml_fromstring
from common.views import HealthCheckResponse
from common.views import handler403
from common.views import handler500
+from tasks.models import UserAssignment
+from workbaskets.validators import WorkflowStatus
pytestmark = pytest.mark.django_db
-@pytest.mark.parametrize(
- ("action", "permission"),
- [
- ("Create a new workbasket", "add_workbasket"),
- ("Edit workbaskets", "add_workbasket"),
- ("Package workbaskets", "manage_packaging_queue"),
- ("Process envelopes", "consume_from_packaging_queue"),
- ("Search the tariff", ""),
- ("Import EU TARIC files", "add_trackedmodel"),
- ("Search for workbaskets", "view_workbasket"),
- ],
-)
-def test_home_form_actions_match_permissions(action, permission, client):
- """Tests that the workbasket action form on the home page displays the
- appropriate radio options for the user's permissions."""
- user = factories.UserFactory.create()
- if permission:
- user.user_permissions.add(Permission.objects.get(codename=permission))
- client.force_login(user)
-
- response = client.get(reverse("home"))
- assert response.status_code == 200
-
- page = BeautifulSoup(response.content.decode(response.charset), "html.parser")
- assert page.find("label", class_="govuk-radios__label", string=re.compile(action))
-
-
def test_index_displays_logout_buttons_correctly_SSO_off_logged_in(valid_user_client):
settings.SSO_ENABLED = False
response = valid_user_client.get(reverse("home"))
@@ -73,65 +48,6 @@ def test_index_displays_login_buttons_correctly_SSO_on(valid_user_client):
assert not page.find_all("a", {"href": "/login"})
-@pytest.mark.parametrize(
- ("data", "response_url"),
- (
- (
- {
- "workbasket_action": "CREATE",
- },
- "workbaskets:workbasket-ui-create",
- ),
- (
- {
- "workbasket_action": "EDIT",
- },
- "workbaskets:workbasket-ui-list",
- ),
- (
- {
- "workbasket_action": "PACKAGE_WORKBASKETS",
- },
- "publishing:packaged-workbasket-queue-ui-list",
- ),
- (
- {
- "workbasket_action": "PROCESS_ENVELOPES",
- },
- "publishing:envelope-queue-ui-list",
- ),
- (
- {
- "workbasket_action": "SEARCH",
- },
- "search-page",
- ),
- (
- {
- "workbasket_action": "IMPORT",
- },
- "commodity_importer-ui-list",
- ),
- (
- {
- "workbasket_action": "WORKBASKET_LIST_ALL",
- },
- "workbaskets:workbasket-ui-list-all",
- ),
- ),
-)
-def test_workbasket_action_form_response_redirects_user(
- valid_user,
- client,
- data,
- response_url,
-):
- client.force_login(valid_user)
- response = client.post(reverse("home"), data)
- assert response.status_code == 302
- assert response.url == reverse(response_url)
-
-
@pytest.mark.parametrize(
"response, status_code, status",
[
@@ -191,12 +107,10 @@ def test_index_displays_footer_links(valid_user_client):
page = BeautifulSoup(str(response.content), "html.parser")
a_tags = page.select("footer a")
- assert len(a_tags) == 7
- assert "Privacy policy" in a_tags[0].text
- assert (
- a_tags[0].attrs["href"]
- == "https://workspace.trade.gov.uk/working-at-dbt/policies-and-guidance/policies/tariff-application-privacy-policy/"
- )
+ assert len(a_tags) == 3
+ assert "Accessibility statement" in a_tags[0].text
+ assert "Privacy policy" in a_tags[1].text
+ assert "Help centre" in a_tags[2].text
def test_search_page_displays_links(valid_user_client):
@@ -261,3 +175,158 @@ def test_maintenance_mode_page_content(valid_user_client):
response = valid_user_client.get(reverse("maintenance"))
assert response.status_code == 200
assert "Sorry, the service is unavailable" in str(response.content)
+
+
+@pytest.mark.parametrize(
+ ("card", "permission"),
+ [
+ ("What would you like to do?", "view_workbasket"),
+ ("Currently working on", "view_workbasket"),
+ ("EU TARIC files", "add_trackedmodel"),
+ ("Envelopes", "consume_from_packaging_queue"),
+ ("Resources", ""),
+ ("Get help", ""),
+ ("Get in touch", ""),
+ ],
+)
+def test_homepage_cards_match_user_permissions(card, permission, client):
+ user = factories.UserFactory.create()
+ client.force_login(user)
+
+ if permission:
+ response = client.get(reverse("home"))
+ assert not card in str(response.content)
+ user.user_permissions.add(Permission.objects.get(codename=permission))
+
+ response = client.get(reverse("home"))
+ assert card in str(response.content)
+
+
+def test_homepage_cards_contain_expected_links(superuser_client):
+ expected_urls = {
+ "Create a new workbasket": "workbaskets:workbasket-ui-create",
+ "Edit a workbasket": "workbaskets:workbasket-ui-list",
+ "Package a workbasket": "publishing:packaged-workbasket-queue-ui-list",
+ "Search for workbaskets": "workbaskets:workbasket-ui-list-all",
+ "View EU import list": "commodity_importer-ui-list",
+ "Process envelopes": "publishing:envelope-queue-ui-list",
+ "Application information": "app-info",
+ "Importer V1": "import_batch-ui-list",
+ "Importer V2": "taric_parser_import_ui_list",
+ "TAP reports": "reports:index",
+ }
+ response = superuser_client.get(reverse("home"))
+ page = BeautifulSoup(response.content.decode(response.charset), "html.parser")
+ links = [a["href"] for a in page.find_all("a")]
+
+ for url in expected_urls.values():
+ assert reverse(url) in links
+
+
+@pytest.mark.parametrize(
+ ("status"),
+ [
+ (WorkflowStatus.EDITING),
+ (WorkflowStatus.ERRORED),
+ ],
+)
+def test_homepage_card_currently_working_on(status, valid_user, valid_user_client):
+ workbasket = factories.WorkBasketFactory.create(status=status)
+ review_assignment = factories.UserAssignmentFactory.create(
+ user=valid_user,
+ assignment_type=UserAssignment.AssignmentType.WORKBASKET_REVIEWER,
+ task__workbasket=workbasket,
+ )
+ rule_violation = TrackedModelCheckFactory.create(
+ transaction_check__transaction=workbasket.new_transaction(),
+ successful=False,
+ )
+
+ response = valid_user_client.get(reverse("home"))
+ page = BeautifulSoup(response.content.decode(response.charset), "html.parser")
+ card = page.find("h3", string="Currently working on")
+ assigned_workbasket = card.find_next("a")
+ details = card.find_next("span")
+
+ assert f"Workbasket ID {workbasket.pk}" in assigned_workbasket.text
+ assert "Reviewing" and "rule violation" in details.text
+
+
+@pytest.mark.parametrize(
+ ("factory", "id", "kwargs"),
+ [
+ (factories.AdditionalCodeFactory, None, {}),
+ (factories.CertificateFactory, "code", {}),
+ (factories.FootnoteFactory, None, {}),
+ (factories.GeographicalAreaFactory, "area_id", {}),
+ (factories.GoodsNomenclatureFactory, "item_id", {}),
+ (factories.MeasureFactory, "sid", {"sid": 123456}),
+ (factories.QuotaOrderNumberFactory, "order_number", {}),
+ (factories.RegulationFactory, "regulation_id", {}),
+ ],
+)
+def test_homepage_search_by_element_id_returns_result(
+ factory,
+ id,
+ kwargs,
+ valid_user_client,
+):
+ model = factory(**kwargs)
+ form_data = {
+ "search_term": getattr(model, id) if id else model.__str__(),
+ }
+ response = valid_user_client.post(reverse("home"), form_data)
+ assert response.status_code == 302
+ assert response.url == model.get_url()
+
+
+@pytest.mark.parametrize(
+ ("name", "expected_url"),
+ [
+ ("additional codes", "additional_code-ui-list"),
+ ("certificates", "certificate-ui-list"),
+ ("footnotes", "footnote-ui-list"),
+ ("geographical areas", "geo_area-ui-list"),
+ ("commodities", "commodity-ui-list"),
+ ("measures", "measure-ui-search"),
+ ("quotas", "quota-ui-list"),
+ ("regulations", "regulation-ui-list"),
+ ],
+)
+def test_homepage_search_by_element_name_returns_result(
+ name,
+ expected_url,
+ valid_user_client,
+):
+ form_data = {
+ "search_term": name,
+ }
+ response = valid_user_client.post(reverse("home"), form_data)
+ assert response.status_code == 302
+ assert response.url == reverse(expected_url)
+
+
+def test_homepage_search_no_result(valid_user_client):
+ factories.FootnoteFactory.create()
+ response = valid_user_client.post(reverse("home"), {"search_term": "empty"})
+ assert response.status_code == 302
+ assert response.url == reverse("search-page")
+
+
+@pytest.mark.parametrize(
+ ("heading", "expected_url"),
+ [
+ ("Application information", reverse_lazy("app-info")),
+ ("Importer V1", reverse_lazy("import_batch-ui-list")),
+ ("Importer V2", reverse_lazy("taric_parser_import_ui_list")),
+ ("TAP reports", reverse_lazy("reports:index")),
+ ("Tariff data manual", "https://uktrade.github.io/tariff-data-manual/#home"),
+ ],
+)
+def test_resources_view_displays_resources(heading, expected_url, valid_user_client):
+ response = valid_user_client.get(reverse("resources"))
+ assert response.status_code == 200
+
+ page = BeautifulSoup(response.content.decode(response.charset), "html.parser")
+ assert page.find("h3", string=heading)
+ assert page.find("a", href=expected_url)
diff --git a/common/urls.py b/common/urls.py
index 5e7361195..5d60becf6 100644
--- a/common/urls.py
+++ b/common/urls.py
@@ -17,6 +17,7 @@
urlpatterns = [
path("", views.HomeView.as_view(), name="home"),
path("search/", views.SearchPageView.as_view(), name="search-page"),
+ path("resources/", views.ResourcesView.as_view(), name="resources"),
path("healthcheck", views.healthcheck, name="healthcheck"),
path("app-info", views.AppInfoView.as_view(), name="app-info"),
path(
diff --git a/common/views.py b/common/views.py
index f8e7bd12c..12515bc93 100644
--- a/common/views.py
+++ b/common/views.py
@@ -19,6 +19,7 @@
from django.db import connection
from django.db import transaction
from django.db.models import Model
+from django.db.models import Q
from django.db.models import QuerySet
from django.http import Http404
from django.http import HttpResponse
@@ -26,56 +27,219 @@
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils.timezone import make_aware
-from django.views import generic
+from django.views.generic import DetailView
from django.views.generic import FormView
from django.views.generic import TemplateView
-from django.views.generic.base import View
from django.views.generic.edit import FormMixin
from django_filters.views import FilterView
from redis.exceptions import TimeoutError as RedisTimeoutError
-from common import forms
+from additional_codes.models import AdditionalCode
+from certificates.models import Certificate
+from commodities.models import GoodsNomenclature
from common.business_rules import BusinessRule
from common.business_rules import BusinessRuleViolation
from common.celery import app
+from common.forms import HomeSearchForm
from common.models import TrackedModel
from common.models import Transaction
from common.pagination import build_pagination_list
from common.validators import UpdateType
+from footnotes.models import Footnote
+from geo_areas.models import GeographicalArea
+from measures.models import Measure
from publishing.models import PackagedWorkBasket
+from quotas.models import QuotaOrderNumber
+from regulations.models import Regulation
+from tasks.models import UserAssignment
from workbaskets.models import WorkBasket
+from workbaskets.models import WorkflowStatus
from workbaskets.views.mixins import WithCurrentWorkBasket
-class HomeView(FormView, View):
- template_name = "common/workbasket_action.jinja"
- form_class = forms.HomeForm
+class HomeView(LoginRequiredMixin, FormView):
+ template_name = "common/homepage.jinja"
+ form_class = HomeSearchForm
- REDIRECT_MAPPING = {
- "EDIT": "workbaskets:workbasket-ui-list",
- "CREATE": "workbaskets:workbasket-ui-create",
- "PACKAGE_WORKBASKETS": "publishing:packaged-workbasket-queue-ui-list",
- "PROCESS_ENVELOPES": "publishing:envelope-queue-ui-list",
- "SEARCH": "search-page",
- "IMPORT": "commodity_importer-ui-list",
- "WORKBASKET_LIST_ALL": "workbaskets:workbasket-ui-list-all",
- }
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+
+ assignments = (
+ UserAssignment.objects.filter(user=self.request.user)
+ .assigned()
+ .select_related("task__workbasket")
+ .filter(
+ Q(task__workbasket__status=WorkflowStatus.EDITING)
+ | Q(task__workbasket__status=WorkflowStatus.ERRORED),
+ )
+ )
+ assigned_workbaskets = []
+ for assignment in assignments:
+ workbasket = assignment.task.workbasket
+ assignment_type = (
+ "Assigned"
+ if assignment.assignment_type
+ == UserAssignment.AssignmentType.WORKBASKET_WORKER
+ else "Reviewing"
+ )
+ rule_violations_count = workbasket.tracked_model_check_errors.count()
+ assigned_workbaskets.append(
+ {
+ "id": workbasket.id,
+ "rule_violations_count": rule_violations_count,
+ "assignment_type": assignment_type,
+ },
+ )
+
+ context.update(
+ {
+ "assigned_workbaskets": assigned_workbaskets,
+ "can_add_workbasket": self.request.user.has_perm(
+ "workbaskets.add_workbasket",
+ ),
+ "can_view_reports": self.request.user.has_perm(
+ "reports.view_report_index",
+ ),
+ },
+ )
+ return context
+
+ def get_search_result(self, search_term: str) -> Optional[str]:
+ """
+ Returns the outcome of a search for a given `search_term`.
+
+ The search term is expected to be either a tariff element name or a tariff
+ element ID, with a length between 2 and 18 characters.
+
+ For a tariff element name, we attempt to find a matching key in `list_view_map` dict,
+ returning the corresponding 'Find and edit' view URL of the matching element.
+
+ For a tariff element ID, we perform case-insensitive DB lookups for tariff elements whose ID
+ takes the form of the search term, returning the detail view URL of the matching element.
+
+ If no match can be found for a given search term, then `None` is returned.
+ """
+ # Check if the search term matches an element name
+ list_view_map = {
+ "additional codes": "additional_code-ui-list",
+ "certificates": "certificate-ui-list",
+ "footnotes": "footnote-ui-list",
+ "geographical areas": "geo_area-ui-list",
+ "commodities": "commodity-ui-list",
+ "measures": "measure-ui-search",
+ "quotas": "quota-ui-list",
+ "regulations": "regulation-ui-list",
+ }
+ match = list_view_map.get(search_term, None)
+ if match:
+ return match
+
+ # Otherwise attempt to match the search term to an element ID
+ search_len = len(search_term)
+ if search_len == 2:
+ match = (
+ GeographicalArea.objects.filter(area_id__iexact=search_term)
+ .current()
+ .last()
+ )
+ return match.get_url() if match else None
+
+ elif search_len == 4:
+ match = (
+ AdditionalCode.objects.filter(
+ type__sid__iexact=search_term[0],
+ code__iexact=search_term[1:],
+ )
+ .current()
+ .last()
+ )
+ if match:
+ return match.get_url()
- def get_form_kwargs(self):
- kwargs = super().get_form_kwargs()
- kwargs["user"] = self.request.user
- return kwargs
+ match = (
+ Certificate.objects.filter(
+ certificate_type__sid__iexact=search_term[0],
+ sid__iexact=search_term[1:],
+ )
+ .current()
+ .last()
+ )
+ if match:
+ return match.get_url()
+
+ match = (
+ GeographicalArea.objects.filter(area_id__iexact=search_term)
+ .current()
+ .last()
+ )
+ return match.get_url() if match else None
+
+ elif search_len == 5:
+ match = (
+ Footnote.objects.filter(
+ footnote_type__footnote_type_id__iexact=search_term[:2],
+ footnote_id=search_term[2:],
+ )
+ .current()
+ .last()
+ )
+ return match.get_url() if match else None
+
+ elif search_term.isnumeric():
+ if search_len == 6 and search_term[0] == "0":
+ match = (
+ QuotaOrderNumber.objects.filter(order_number=search_term)
+ .current()
+ .last()
+ )
+ return match.get_url() if match else None
+ if search_len == 10:
+ match = (
+ GoodsNomenclature.objects.filter(item_id=search_term)
+ .current()
+ .last()
+ )
+ if match:
+ return match.get_url()
+ else:
+ match = Measure.objects.filter(sid=search_term).current().last()
+ return match.get_url() if match else None
+
+ elif search_len == 8:
+ match = (
+ Regulation.objects.filter(regulation_id__iexact=search_term)
+ .current()
+ .last()
+ )
+ return match.get_url() if match else None
+
+ # No match has been found for the search term
+ else:
+ return None
def form_valid(self, form):
- return redirect(
- reverse(self.REDIRECT_MAPPING[form.cleaned_data["workbasket_action"]]),
- )
+ search_term = form.cleaned_data["search_term"]
+ if not search_term:
+ return self.get_success_url(reverse("search-page"))
+
+ result = self.get_search_result(search_term)
+ if result:
+ return self.get_success_url(result)
+ else:
+ return self.get_success_url(reverse("search-page"))
+
+ def get_success_url(self, url):
+ return redirect(url)
class SearchPageView(TemplateView):
template_name = "common/search_page.jinja"
+class ResourcesView(TemplateView):
+ template_name = "common/resources.jinja"
+
+
class HealthCheckResponse(HttpResponse):
"""
Formatted HTTP response for healthcheck.
@@ -316,7 +480,7 @@ def get_object(self, queryset: Optional[QuerySet] = None) -> Model:
class TrackedModelDetailView(
WithCurrentWorkBasket,
TrackedModelDetailMixin,
- generic.DetailView,
+ DetailView,
):
"""Base view class for displaying a single TrackedModel."""
diff --git a/pii-secret-exclude.txt b/pii-secret-exclude.txt
index 4fdcdec14..0c2d0b618 100644
--- a/pii-secret-exclude.txt
+++ b/pii-secret-exclude.txt
@@ -27,3 +27,4 @@ Dockerfile
common/jinja2/common/accessibility.jinja
common/static/common/js/components/QuotaOriginFormset/tests/__snapshots__/index.test.js.snap
common/migrations/0001_initial.py
+common/jinja2/common/homepage.jinja
diff --git a/workbaskets/tests/test_views.py b/workbaskets/tests/test_views.py
index 89b086edf..7e84ccefb 100644
--- a/workbaskets/tests/test_views.py
+++ b/workbaskets/tests/test_views.py
@@ -1811,10 +1811,7 @@ def test_application_access_after_workbasket_delete(
# user workbasket deletion.
assert response.status_code == 200
- assert (
- f"Workbasket {workbasket_pk}"
- in page.select("header nav a.workbasket-link")[0].text
- )
+ assert f"Workbasket ID {workbasket_pk}" in page.select("a.workbasket-link")[0].text
user_empty_workbasket.delete()
From 020065aeb046320122cea03222adba1a2c508c0e Mon Sep 17 00:00:00 2001
From: Dale Cannon <118175145+dalecannon@users.noreply.github.com>
Date: Thu, 21 Mar 2024 11:32:04 +0000
Subject: [PATCH 2/4] TP2000-1255 Correct homepage card permission (#1182)
* Correct homepage card permission
---
common/jinja2/common/homepage.jinja | 4 ++++
common/tests/test_views.py | 2 +-
common/views.py | 3 +++
3 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/common/jinja2/common/homepage.jinja b/common/jinja2/common/homepage.jinja
index dd1c2da6f..b9292f30e 100644
--- a/common/jinja2/common/homepage.jinja
+++ b/common/jinja2/common/homepage.jinja
@@ -32,7 +32,9 @@
class="govuk-button homepage-workbasket-action"
>Create a
workbasket
+ {% endif %}
+ {% if can_edit_workbasket %}
+ {% endif %}
+ {% if can_edit_workbasket %}
Currently working on
{% if not assigned_workbaskets %}
diff --git a/common/tests/test_views.py b/common/tests/test_views.py
index 0e07bc0ed..5eabe484a 100644
--- a/common/tests/test_views.py
+++ b/common/tests/test_views.py
@@ -181,7 +181,7 @@ def test_maintenance_mode_page_content(valid_user_client):
("card", "permission"),
[
("What would you like to do?", "view_workbasket"),
- ("Currently working on", "view_workbasket"),
+ ("Currently working on", "change_workbasket"),
("EU TARIC files", "add_trackedmodel"),
("Envelopes", "consume_from_packaging_queue"),
("Resources", ""),
diff --git a/common/views.py b/common/views.py
index 12515bc93..e1d10bf12 100644
--- a/common/views.py
+++ b/common/views.py
@@ -97,6 +97,9 @@ def get_context_data(self, **kwargs):
"can_add_workbasket": self.request.user.has_perm(
"workbaskets.add_workbasket",
),
+ "can_edit_workbasket": self.request.user.has_perm(
+ "workbaskets.change_workbasket",
+ ),
"can_view_reports": self.request.user.has_perm(
"reports.view_report_index",
),
From 91275a463e5cc27821473c2fcf8bfc513ba30b9c Mon Sep 17 00:00:00 2001
From: Panos Paralakis
Date: Thu, 21 Mar 2024 11:47:00 +0000
Subject: [PATCH 3/4] Add codeowners file (#1183)
---
CODEOWNERS | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 CODEOWNERS
diff --git a/CODEOWNERS b/CODEOWNERS
new file mode 100644
index 000000000..6ba4af0d2
--- /dev/null
+++ b/CODEOWNERS
@@ -0,0 +1,5 @@
+# These owners will be the default owners for everything in
+# the repo. Unless a later match takes precedence,
+# @tamato will be requested for
+# review when someone opens a pull request.
+* @uktrade/tamato
From 7ca6977b8268d579782df38b5e6fd928963c0095 Mon Sep 17 00:00:00 2001
From: Paul Pepper <85895113+paulpepper-trade@users.noreply.github.com>
Date: Fri, 22 Mar 2024 10:31:49 +0000
Subject: [PATCH 4/4] Alter field from DecimalField to FloatField. (#1186)
Co-authored-by: "Dale Cannon"
---
..._alter_trackedmodelcheck_processing_time.py | 18 ++++++++++++++++++
checks/models.py | 2 +-
2 files changed, 19 insertions(+), 1 deletion(-)
create mode 100644 checks/migrations/0008_alter_trackedmodelcheck_processing_time.py
diff --git a/checks/migrations/0008_alter_trackedmodelcheck_processing_time.py b/checks/migrations/0008_alter_trackedmodelcheck_processing_time.py
new file mode 100644
index 000000000..6dcb9acf5
--- /dev/null
+++ b/checks/migrations/0008_alter_trackedmodelcheck_processing_time.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.10 on 2024-03-21 18:05
+
+from django.db import migrations
+from django.db import models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("checks", "0007_transactioncheck_timestamps"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="trackedmodelcheck",
+ name="processing_time",
+ field=models.FloatField(null=True),
+ ),
+ ]
diff --git a/checks/models.py b/checks/models.py
index 0bec9a901..2904084b4 100644
--- a/checks/models.py
+++ b/checks/models.py
@@ -143,7 +143,7 @@ class TrackedModelCheck(TimestampedMixin):
message = fields.TextField(null=True)
"""The text content returned by the check, if any."""
- processing_time = fields.DecimalField(null=True, decimal_places=4, max_digits=6)
+ processing_time = fields.FloatField(null=True)
"""Added processing_time used to record the time taken to process a rule in
seconds."""