Skip to content

Commit

Permalink
Notifications model and goods report notification (#1036)
Browse files Browse the repository at this point in the history
* Introduce Notification base-class and specialising sub-classes.

* Change emphasis of sub-class implementations - get notify template ID and notified users.

* Associate Notifications with their source notified object.

* Use proxy model of inheritance in base classes.

* WIP mostly working fix notification choices

* address type error and serialisation issues

* remove comment add todo

* Refactor notification inits and fixed email list

* tests, mocks broke & goods report not working

* Working tests!

* removing unused fixtures

* test fixes and admin filter

* test fix

* Confirm should be true by default

* PR comments

---------

Co-authored-by: Paul Pepper <[email protected]>
  • Loading branch information
a-gleeson and paulpepper-trade authored Sep 20, 2023
1 parent 6833b1b commit bec000a
Show file tree
Hide file tree
Showing 31 changed files with 1,685 additions and 363 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,5 @@ _dumped_cache.pkl

# Vim
.swp

tamato_*.sql
31 changes: 31 additions & 0 deletions common/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -1426,3 +1426,34 @@ class Meta:
commodity = factory.SubFactory(GoodsNomenclatureFactory)
duty_sentence = factory.Faker("text", max_nb_chars=24)
valid_between = date_ranges("no_end")


class SucceededImportBatchFactory(ImportBatchFactory):
status = ImportBatchStatus.SUCCEEDED
goods_import = True
taric_file = "goods.xml"


class GoodsSuccessfulImportNotificationFactory(factory.django.DjangoModelFactory):
"""This is a factory for a goods report notification, requires an import id
passed by notified_object_id."""

class Meta:
model = "notifications.GoodsSuccessfulImportNotification"


class EnvelopeReadyForProcessingNotificationFactory(factory.django.DjangoModelFactory):
"""This is a factory for an envelope ready for processing notificaiton."""

class Meta:
model = "notifications.EnvelopeReadyForProcessingNotification"


class CrownDependenciesEnvelopeSuccessNotificationFactory(
factory.django.DjangoModelFactory,
):
"""This is a factory for a crown dependencies envelope success
notification."""

class Meta:
model = "notifications.CrownDependenciesEnvelopeSuccessNotification"
139 changes: 139 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from typing import Sequence
from typing import Tuple
from typing import Type
from unittest.mock import MagicMock
from unittest.mock import patch

import boto3
Expand Down Expand Up @@ -66,6 +67,7 @@
from measures.models import MeasurementUnitQualifier
from measures.models import MonetaryUnit
from measures.parsers import DutySentenceParser
from publishing.models import PackagedWorkBasket
from workbaskets.models import WorkBasket
from workbaskets.models import get_partition_scheme
from workbaskets.validators import WorkflowStatus
Expand Down Expand Up @@ -1820,3 +1822,140 @@ def duty_sentence_x_2_data(request, get_component_data):
(expected, [get_component_data(*args) for args in component_data]),
)
return history


@pytest.fixture()
def mocked_send_emails_apply_async():
with patch(
"notifications.tasks.send_emails_task.apply_async",
return_value=MagicMock(id=factory.Faker("uuid4")),
) as mocked_delay:
yield mocked_delay


@pytest.fixture()
def mocked_send_emails():
with patch(
"notifications.tasks.send_emails_task",
return_value=MagicMock(id=factory.Faker("uuid4")),
) as mocked_delay:
yield mocked_delay


@pytest.fixture(scope="function")
def packaged_workbasket_factory(queued_workbasket_factory):
"""
Factory fixture to create a packaged workbasket.
params:
workbasket defaults to queued_workbasket_factory() which creates a
Workbasket in the state QUEUED with an approved transaction and tracked models
"""

def factory_method(workbasket=None, **kwargs):
if not workbasket:
workbasket = queued_workbasket_factory()
with patch(
"publishing.tasks.create_xml_envelope_file.apply_async",
return_value=MagicMock(id=factory.Faker("uuid4")),
):
packaged_workbasket = factories.QueuedPackagedWorkBasketFactory(
workbasket=workbasket, **kwargs
)
return packaged_workbasket

return factory_method


@pytest.fixture(scope="function")
def published_envelope_factory(packaged_workbasket_factory, envelope_storage):
"""
Factory fixture to create an envelope and update the packaged_workbasket
envelope field.
params:
packaged_workbasket defaults to packaged_workbasket_factory() which creates a
Packaged workbasket with a Workbasket in the state QUEUED
with an approved transaction and tracked models
"""

def factory_method(packaged_workbasket=None, **kwargs):
if not packaged_workbasket:
packaged_workbasket = packaged_workbasket_factory()

with patch(
"publishing.storages.EnvelopeStorage.save",
wraps=MagicMock(side_effect=envelope_storage.save),
) as mock_save:
envelope = factories.PublishedEnvelopeFactory(
packaged_work_basket=packaged_workbasket,
**kwargs,
)
mock_save.assert_called_once()

packaged_workbasket.envelope = envelope
packaged_workbasket.save()
return envelope

return factory_method


@pytest.fixture(scope="function")
def successful_envelope_factory(
published_envelope_factory,
mocked_send_emails_apply_async,
):
"""
Factory fixture to create a successfully processed envelope and update the
packaged_workbasket envelope field.
params:
packaged_workbasket defaults to packaged_workbasket_factory() which creates a
Packaged workbasket with a Workbasket in the state QUEUED
with an approved transaction and tracked models
"""

def factory_method(**kwargs):
envelope = published_envelope_factory(**kwargs)

packaged_workbasket = PackagedWorkBasket.objects.get(
envelope=envelope,
)

packaged_workbasket.begin_processing()
assert packaged_workbasket.position == 0
assert (
packaged_workbasket.pk
== PackagedWorkBasket.objects.currently_processing().pk
)
factories.LoadingReportFactory.create(packaged_workbasket=packaged_workbasket)
packaged_workbasket.processing_succeeded()
packaged_workbasket.save()
assert packaged_workbasket.position == 0
return envelope

return factory_method


@pytest.fixture(scope="function")
def crown_dependencies_envelope_factory(successful_envelope_factory):
"""
Factory fixture to create a crown dependencies envelope.
params:
packaged_workbasket defaults to packaged_workbasket_factory() which creates a
Packaged workbasket with a Workbasket in the state QUEUED
with an approved transaction and tracked models
"""

def factory_method(**kwargs):
envelope = successful_envelope_factory(**kwargs)

packaged_workbasket = PackagedWorkBasket.objects.get(
envelope=envelope,
)
return factories.CrownDependenciesEnvelopeFactory(
packaged_work_basket=packaged_workbasket,
)

return factory_method
40 changes: 40 additions & 0 deletions importer/jinja2/eu-importer/notify-success.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{% extends "layouts/layout.jinja" %}
{% from "components/breadcrumbs/macro.njk" import govukBreadcrumbs %}
{% from "components/button/macro.njk" import govukButton %}
{% from "components/panel/macro.njk" import govukPanel %}


{% set page_title = "Notify Goods Report" %}


{% block breadcrumb %}
{{ govukBreadcrumbs({
"items": [
{"text": "Home", "href": url("home")},
{"text": "Edit an existing workbasket", "href": url("workbaskets:workbasket-ui-list")},
{"text": "Workbasket " ~ request.session.workbasket.id ~ " - Review goods",
"href": url("workbaskets:workbasket-ui-review-goods")},
{"text": page_title}
]
}) }}
{% endblock %}


{% block content %}
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
{{ govukPanel({
"titleText": "Notification email sent",
"text": "An email notification has been successfully sent to Channel Islands.",
"classes": "govuk-!-margin-bottom-7"
}) }}

<div class="govuk-button-group">
<a class="govuk-link" href="{{ url('workbaskets:workbasket-ui-review-goods') }}">
Back to Review goods list
</a>
</div>

</div>
</div>
{% endblock %}
43 changes: 43 additions & 0 deletions importer/management/commands/send_goods_report_notification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from django.core.management import BaseCommand
from django.core.management.base import CommandError

from importer.models import ImportBatch
from notifications.models import GoodsSuccessfulImportNotification


def send_notifcation(
import_id: int,
):
try:
import_batch = ImportBatch.objects.get(
pk=import_id,
)
except ImportBatch.DoesNotExist:
raise CommandError(
f"No ImportBatch instance found with pk={import_id}",
)

notification = GoodsSuccessfulImportNotification(
notified_object_pk=import_batch.id,
)
notification.save()
notification.synchronous_send_emails()


class Command(BaseCommand):
help = "Send a good report notifcation for a give Id"

def add_arguments(self, parser):
parser.add_argument(
"--import-batch-id",
help=(
"The primary key ID of ImportBatch instance for which a report "
"should be generated."
),
type=int,
)

def handle(self, *args, **options):
send_notifcation(
import_id=options["import_batch_id"],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from unittest.mock import MagicMock
from unittest.mock import patch

import pytest
from django.core.management import call_command
from django.core.management.base import CommandError

pytestmark = pytest.mark.django_db


@pytest.mark.parametrize(
"args, exception_type, error_msg",
[
(
[""],
CommandError,
"Error: unrecognized arguments:",
),
(
["--import-batch-id", "1234"],
CommandError,
"No ImportBatch instance found with pk=1234",
),
],
)
def test_send_goods_report_notification_required_arguments(
args,
exception_type,
error_msg,
):
"""Test that `send_goods_report_notification` command raises errors when
invalid arguments are provided."""
with pytest.raises(exception_type, match=error_msg):
call_command("send_goods_report_notification", *args)


def test_send_goods_report_notification(
completed_goods_import_batch,
):
"""Test that `send_goods_report_notification` command triggers an email
notification."""

with patch(
"notifications.models.send_emails_task",
return_value=MagicMock(),
) as mocked_email_task:
call_command(
"send_goods_report_notification",
"--import-batch-id",
str(completed_goods_import_batch.id),
)
mocked_email_task.assert_called_once()
27 changes: 27 additions & 0 deletions importer/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from os import path
from unittest.mock import MagicMock
from unittest.mock import patch

import pytest
Expand Down Expand Up @@ -282,3 +283,29 @@ def test_import_list_filters_return_correct_imports(
assert len(page.find_all(class_="status-badge")) == 1
assert page.find(class_="status-badge", text=expected_status_text)
assert page.find("tbody").find("td", text=import_batch.name)


def test_notify_channel_islands_redirects(
valid_user_client,
completed_goods_import_batch,
):
"""Tests that, when the notify button is clicked that it redirects to the
conformation page on successful notification trigger."""

with patch(
"notifications.models.send_emails_task",
return_value=MagicMock(),
) as mocked_email_task:
response = valid_user_client.get(
reverse(
"goods-report-notify",
kwargs={"pk": completed_goods_import_batch.pk},
),
)

mocked_email_task.assert_called_once()
assert response.status_code == 302
assert response.url == reverse(
"goods-report-notify-success",
kwargs={"pk": completed_goods_import_batch.pk},
)
10 changes: 10 additions & 0 deletions importer/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@
views.DownloadGoodsReportView.as_view(),
name="goods-report-ui-download",
),
path(
"notify-goods-report/<pk>/",
views.NotifyGoodsReportView.as_view(),
name="goods-report-notify",
),
path(
"notify-goods-report-confirm/<pk>/",
views.NotifyGoodsReportSuccessView.as_view(),
name="goods-report-notify-success",
),
]

urlpatterns = general_importer_urlpatterns + commodity_importer_urlpatterns
Loading

0 comments on commit bec000a

Please sign in to comment.