From 178fa2afc6d859c607aefe4f730cb90cdabd3c86 Mon Sep 17 00:00:00 2001
From: Paul Pepper
Date: Tue, 29 Aug 2023 14:50:02 +0100
Subject: [PATCH 01/15] Introduce Notification base-class and specialising
sub-classes.
---
notifications/models.py | 120 +++++++++++++++++++++++++++++++++++++++-
1 file changed, 118 insertions(+), 2 deletions(-)
diff --git a/notifications/models.py b/notifications/models.py
index 502c1ee1e..ca5c7a52a 100644
--- a/notifications/models.py
+++ b/notifications/models.py
@@ -1,7 +1,15 @@
+import logging
+
+from django.conf import settings
from django.db import models
+from django.db.models.query_utils import Q
+from notifications_python_client.notifications import NotificationsAPIClient
+from polymorphic.models import PolymorphicModel
from common.models.mixins import TimestampedMixin
+logger = logging.getLogger(__name__)
+
class NotifiedUser(models.Model):
"""A NotifiedUser stores a user email address and whether that user is
@@ -17,10 +25,118 @@ def __str__(self):
class NotificationLog(TimestampedMixin):
"""
- A NotificationLog records which email template a group of users received.
+ A NotificationLog records which Notification a group of users received.
We create one each time a group of users receive an email.
"""
- template_id = models.CharField(max_length=100)
recipients = models.TextField()
+ notification = models.ForeignKey(
+ "notifications.Notification",
+ default=None,
+ null=True,
+ )
+
+
+class Notification(PolymorphicModel):
+ """
+ Base class to manage sending notifications.
+
+ Subclasses specialise this class's behaviour for specific categories of
+ notification.
+ """
+
+ notify_template_id: str
+ """GOV.UK Notify template ID, assigned in concrete subclasses."""
+
+ def send_emails(self):
+ """
+ Handle sending emails for this notification to all associated
+ NotifiedUser instances.
+
+ Implement in concrete subclasses.
+ """
+
+ raise NotImplementedError
+
+ def schedule_send_emails(self, countdown=1):
+ """Schedule a call to send a notification email, run as an asynchronous
+ background task."""
+
+ send_emails.apply_sync(args=[self.pk], countdown=countdown)
+
+ def send_email_notifications(self, notified_users):
+ """Send the individual notification emails to users via GOV.UK
+ Notify."""
+
+ if not notified_users:
+ logger.error(
+ f"No notified users for {self.__class__.__name__} "
+ f"with pk={self.pk}",
+ )
+ return
+
+ notifications_client = NotificationsAPIClient(settings.NOTIFICATIONS_API_KEY)
+ recipients = ""
+ for user in notified_users:
+ try:
+ notifications_client.send_email_notification(
+ email_address=user.email,
+ template_id=self.notify_template_id,
+ # TODO: get personalisation data.
+ personalisation={},
+ )
+
+ recipients += f"{user.email} \n"
+ except:
+ logger.error(
+ f"Failed to send email notification to {user.email}.",
+ )
+
+ NotificationLog.objects.create(
+ recipients=recipients,
+ notification=self,
+ )
+
+
+class EnvelopeReadyForProcessingNotification(Notification):
+ """Manage sending notifications when envelopes are ready for processing by
+ HMRC."""
+
+ notify_template_id = "todo:notify-template-id-goes-here"
+
+ def send_emails(self):
+ notified_users = NotifiedUser.objects.filter(
+ Q(enrol_packaging=True) | Q(enrole_api_publishing=True),
+ )
+ self.send_email_notifications(notified_users)
+
+
+class EnvelopeAcceptedNotification(Notification):
+ """TODO."""
+
+
+class EnvelopeRejectedNotification(Notification):
+ """TODO."""
+
+
+class GoodsSuccessfulImportNotification(Notification):
+ """TODO."""
+
+
+# ========================== start ==========================
+# Code between start and end is a rewrite of, and drop-in replacement for
+# notifications.tasks.send_emails().
+
+from celery import shared_task
+from django.db.transaction import atomic
+
+
+@shared_task
+@atomic
+def send_emails(notification_pk: int):
+ notification = Notification.objects.get(pk=notification_pk)
+ notification.send_emails()
+
+
+# ========================== end ==========================
From 18e5619e68e96fcc95e869539be99b2537f9efbf Mon Sep 17 00:00:00 2001
From: Paul Pepper
Date: Tue, 29 Aug 2023 17:04:37 +0100
Subject: [PATCH 02/15] Change emphasis of sub-class implementations - get
notify template ID and notified users.
---
notifications/models.py | 33 ++++++++++++++++++---------------
1 file changed, 18 insertions(+), 15 deletions(-)
diff --git a/notifications/models.py b/notifications/models.py
index ca5c7a52a..eafe5d549 100644
--- a/notifications/models.py
+++ b/notifications/models.py
@@ -46,17 +46,20 @@ class Notification(PolymorphicModel):
notification.
"""
- notify_template_id: str
- """GOV.UK Notify template ID, assigned in concrete subclasses."""
-
- def send_emails(self):
+ def notify_template_id(self) -> str:
"""
- Handle sending emails for this notification to all associated
- NotifiedUser instances.
+ GOV.UK Notify template ID specific to each Notification sub-class.
Implement in concrete subclasses.
"""
+ raise NotImplementedError
+
+ def notified_users(self) -> models.QuerySet[NotifiedUser]:
+ """
+ Returns the queryset of NotifiedUsers for a specific notifications.
+ Implement in concrete subclasses.
+ """
raise NotImplementedError
def schedule_send_emails(self, countdown=1):
@@ -65,10 +68,10 @@ def schedule_send_emails(self, countdown=1):
send_emails.apply_sync(args=[self.pk], countdown=countdown)
- def send_email_notifications(self, notified_users):
- """Send the individual notification emails to users via GOV.UK
- Notify."""
+ def send_emails(self):
+ """Send the notification emails to users via GOV.UK Notify."""
+ notified_users = self.notified_users()
if not notified_users:
logger.error(
f"No notified users for {self.__class__.__name__} "
@@ -82,7 +85,7 @@ def send_email_notifications(self, notified_users):
try:
notifications_client.send_email_notification(
email_address=user.email,
- template_id=self.notify_template_id,
+ template_id=self.notify_template_id(),
# TODO: get personalisation data.
personalisation={},
)
@@ -103,13 +106,13 @@ class EnvelopeReadyForProcessingNotification(Notification):
"""Manage sending notifications when envelopes are ready for processing by
HMRC."""
- notify_template_id = "todo:notify-template-id-goes-here"
+ def notify_template_id(self) -> str:
+ return settings.READY_FOR_CDS_TEMPLATE_ID
- def send_emails(self):
- notified_users = NotifiedUser.objects.filter(
+ def notified_users(self):
+ return NotifiedUser.objects.filter(
Q(enrol_packaging=True) | Q(enrole_api_publishing=True),
)
- self.send_email_notifications(notified_users)
class EnvelopeAcceptedNotification(Notification):
@@ -125,7 +128,7 @@ class GoodsSuccessfulImportNotification(Notification):
# ========================== start ==========================
-# Code between start and end is a rewrite of, and drop-in replacement for
+# Code between start and end is a rewrite of, and drop-in replacement for,
# notifications.tasks.send_emails().
from celery import shared_task
From d054d0917472e59f2243092e07114c414658ef9c Mon Sep 17 00:00:00 2001
From: Paul Pepper
Date: Tue, 29 Aug 2023 17:34:40 +0100
Subject: [PATCH 03/15] Associate Notifications with their source notified
object.
---
notifications/models.py | 27 +++++++++++++++++++++++++++
1 file changed, 27 insertions(+)
diff --git a/notifications/models.py b/notifications/models.py
index eafe5d549..5a3140b60 100644
--- a/notifications/models.py
+++ b/notifications/models.py
@@ -31,6 +31,8 @@ class NotificationLog(TimestampedMixin):
"""
recipients = models.TextField()
+ """Comma separated email addresses, as a single string, of the recipients of
+ the notification."""
notification = models.ForeignKey(
"notifications.Notification",
default=None,
@@ -46,6 +48,14 @@ class Notification(PolymorphicModel):
notification.
"""
+ notified_object_pk: int
+ """The primary key of the."""
+
+ notified_object_pk = models.IntegerField(
+ default=None,
+ null=True,
+ )
+
def notify_template_id(self) -> str:
"""
GOV.UK Notify template ID specific to each Notification sub-class.
@@ -62,6 +72,14 @@ def notified_users(self) -> models.QuerySet[NotifiedUser]:
"""
raise NotImplementedError
+ def notified_object(self) -> models.Model:
+ """
+ Returns the object instance that is being notified on.
+
+ Implement in concrete subclasses.
+ """
+ raise NotImplementedError
+
def schedule_send_emails(self, countdown=1):
"""Schedule a call to send a notification email, run as an asynchronous
background task."""
@@ -114,6 +132,15 @@ def notified_users(self):
Q(enrol_packaging=True) | Q(enrole_api_publishing=True),
)
+ def notified_object(self) -> models.Model:
+ from publishing.models import PackagedWorkBasket
+
+ return (
+ PackagedWorkBasket.objects.get(self.notified_object_pk)
+ if self.notified_object_pk
+ else None
+ )
+
class EnvelopeAcceptedNotification(Notification):
"""TODO."""
From 36b79db8ff57af6be352fdecc0d42a4fef1d2d5e Mon Sep 17 00:00:00 2001
From: Paul Pepper
Date: Wed, 30 Aug 2023 12:23:41 +0100
Subject: [PATCH 04/15] Use proxy model of inheritance in base classes.
---
notifications/models.py | 14 ++++++++++----
1 file changed, 10 insertions(+), 4 deletions(-)
diff --git a/notifications/models.py b/notifications/models.py
index 5a3140b60..0770f1dea 100644
--- a/notifications/models.py
+++ b/notifications/models.py
@@ -4,7 +4,6 @@
from django.db import models
from django.db.models.query_utils import Q
from notifications_python_client.notifications import NotificationsAPIClient
-from polymorphic.models import PolymorphicModel
from common.models.mixins import TimestampedMixin
@@ -40,21 +39,25 @@ class NotificationLog(TimestampedMixin):
)
-class Notification(PolymorphicModel):
+class Notification(models.Model):
"""
Base class to manage sending notifications.
Subclasses specialise this class's behaviour for specific categories of
notification.
+
+ Subclasses should specify the proxy model of inheritance:
+ https://docs.djangoproject.com/en/dev/topics/db/models/#proxy-models
"""
- notified_object_pk: int
- """The primary key of the."""
+ def __init__(self, notified_object_pk: int = None):
+ self.notified_object_pk = notified_object_pk
notified_object_pk = models.IntegerField(
default=None,
null=True,
)
+ """The primary key of the object being notified on."""
def notify_template_id(self) -> str:
"""
@@ -124,6 +127,9 @@ class EnvelopeReadyForProcessingNotification(Notification):
"""Manage sending notifications when envelopes are ready for processing by
HMRC."""
+ class Meta:
+ proxy = True
+
def notify_template_id(self) -> str:
return settings.READY_FOR_CDS_TEMPLATE_ID
From c68bf1a4abad50e123e7d7a7ba190afccc71df3f Mon Sep 17 00:00:00 2001
From: Anthoni Gleeson
Date: Fri, 8 Sep 2023 14:55:32 +0100
Subject: [PATCH 05/15] WIP mostly working fix notification choices
---
.gitignore | 2 +
.../jinja2/eu-importer/notify-success.jinja | 40 +++
.../send_goods_report_notification.py | 45 +++
importer/urls.py | 10 +
importer/views.py | 37 ++
notifications/admin.py | 51 +++
notifications/forms.py | 1 +
.../migrations/0003_auto_20230908_0850.py | 150 ++++++++
notifications/models.py | 334 +++++++++++++++---
notifications/notify.py | 86 +++++
notifications/tasks.py | 44 +--
notifications/utils.py | 0
.../models/crown_dependencies_envelope.py | 48 ++-
publishing/models/packaged_workbasket.py | 84 ++---
sample.env | 1 +
settings/common.py | 1 +
.../jinja2/workbaskets/review-goods.jinja | 9 +
.../workbaskets/summary-workbasket.jinja | 15 +-
workbaskets/views/ui.py | 25 ++
19 files changed, 814 insertions(+), 169 deletions(-)
create mode 100644 importer/jinja2/eu-importer/notify-success.jinja
create mode 100644 importer/management/commands/send_goods_report_notification.py
create mode 100644 notifications/migrations/0003_auto_20230908_0850.py
create mode 100644 notifications/notify.py
create mode 100644 notifications/utils.py
diff --git a/.gitignore b/.gitignore
index 43109bc38..28d2a8cf7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -165,3 +165,5 @@ _dumped_cache.pkl
# Vim
.swp
+
+tamato_*.sql
diff --git a/importer/jinja2/eu-importer/notify-success.jinja b/importer/jinja2/eu-importer/notify-success.jinja
new file mode 100644
index 000000000..17b495110
--- /dev/null
+++ b/importer/jinja2/eu-importer/notify-success.jinja
@@ -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 %}
+
+
+ {{ govukPanel({
+ "titleText": "Notification email sent",
+ "text": "An email notification has been successfully sent to Channel Islands.",
+ "classes": "govuk-!-margin-bottom-7"
+ }) }}
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/importer/management/commands/send_goods_report_notification.py b/importer/management/commands/send_goods_report_notification.py
new file mode 100644
index 000000000..f08a03ddc
--- /dev/null
+++ b/importer/management/commands/send_goods_report_notification.py
@@ -0,0 +1,45 @@
+from django.core.management import BaseCommand
+from django.core.management.base import CommandError
+
+from importer.models import ImportBatch
+from notifications.models import GoodsSuccessfulImportNotification
+from notifications.models import NotificationTypeChoices
+
+
+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(
+ notificaiton_type=NotificationTypeChoices.GOODS_REPORT,
+ 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"],
+ )
diff --git a/importer/urls.py b/importer/urls.py
index cbeae244e..6497ffca3 100644
--- a/importer/urls.py
+++ b/importer/urls.py
@@ -41,6 +41,16 @@
views.DownloadGoodsReportView.as_view(),
name="goods-report-ui-download",
),
+ path(
+ "notify-goods-report//",
+ views.NotifyGoodsReportView.as_view(),
+ name="goods-report-notify",
+ ),
+ path(
+ "notify-goods-report-confirm//",
+ views.NotifyGoodsReportSuccessView.as_view(),
+ name="goods-report-notify-success",
+ ),
]
urlpatterns = general_importer_urlpatterns + commodity_importer_urlpatterns
diff --git a/importer/views.py b/importer/views.py
index e40e948db..bdfad1855 100644
--- a/importer/views.py
+++ b/importer/views.py
@@ -19,6 +19,8 @@
from importer.filters import TaricImportFilter
from importer.goods_report import GoodsReporter
from importer.models import ImportBatchStatus
+from notifications.models import GoodsSuccessfulImportNotification
+from notifications.models import NotificationTypeChoices
from workbaskets.validators import WorkflowStatus
@@ -236,3 +238,38 @@ class DownloadGoodsReportView(
def get(self, request, *args, **kwargs) -> HttpResponse:
import_batch = self.get_object()
return self.download_response(import_batch)
+
+
+class NotifyGoodsReportView(
+ PermissionRequiredMixin,
+ DetailView,
+):
+ """View used to notify an import report of goods changes in Excel format."""
+
+ permission_required = "common.add_trackedmodel"
+ model = models.ImportBatch
+
+ def get(self, request, *args, **kwargs):
+ import_batch = self.get_object()
+
+ # create notification
+ notification = GoodsSuccessfulImportNotification(
+ notificaiton_type=(
+ NotificationTypeChoices.GOODS_REPORT.value,
+ NotificationTypeChoices.GOODS_REPORT.label,
+ ),
+ notified_object_pk=import_batch.id,
+ )
+ notification.save()
+ notification.synchronous_send_emails()
+
+ return redirect(
+ reverse("goods-report-notify-success", kwargs={"pk": import_batch.id}),
+ )
+
+
+class NotifyGoodsReportSuccessView(DetailView):
+ """Goods Report notification success trigger view."""
+
+ template_name = "eu-importer/notify-success.jinja"
+ model = models.ImportBatch
diff --git a/notifications/admin.py b/notifications/admin.py
index bc6da4940..4ffa133f1 100644
--- a/notifications/admin.py
+++ b/notifications/admin.py
@@ -1,6 +1,7 @@
from django.contrib import admin
from notifications.forms import NotifiedUserAdminForm
+from notifications.models import Notification
from notifications.models import NotificationLog
from notifications.models import NotifiedUser
@@ -13,12 +14,15 @@ class NotifiedUserAdmin(admin.ModelAdmin):
"email",
"enrol_packaging",
"enrol_api_publishing",
+ "enrol_goods_report",
)
actions = [
"set_enrol_packaging",
"unset_enrol_packaging",
"set_enrol_api_publishing",
"unset_enrol_api_publishing",
+ "set_enrol_goods_report",
+ "unset_enrol_goods_report",
]
def set_enrol_packaging(self, request, queryset):
@@ -33,6 +37,12 @@ def set_enrol_api_publishing(self, request, queryset):
def unset_enrol_api_publishing(self, request, queryset):
queryset.update(enrol_api_publishing=False)
+ def set_enrol_goods_report(self, request, queryset):
+ queryset.update(enrol_goods_report=True)
+
+ def unset_enrol_goods_report(self, request, queryset):
+ queryset.update(enrol_goods_report=False)
+
class NotificationLogAdmin(admin.ModelAdmin):
"""Class providing read-only list and detail views for notification logs."""
@@ -59,5 +69,46 @@ def changeform_view(self, request, object_id=None, form_url="", extra_context=No
)
+class NotificationAdmin(admin.ModelAdmin):
+ """Class providing read-only list and detail views for notification."""
+
+ ordering = ["pk"]
+ list_display = (
+ "pk",
+ "notificaiton_type",
+ "notified_object_pk",
+ # "display_users",
+ )
+
+ readonly_fields = []
+
+ def display_users(self, obj):
+ return ", ".join(
+ [user.email for user in obj.notified_users().all()],
+ )
+
+ display_users.short_description = "Recipients"
+
+ def get_readonly_fields(self, request, obj=None):
+ return list(self.readonly_fields) + [field.name for field in obj._meta.fields]
+
+ def has_add_permission(self, request):
+ return False
+
+ def has_delete_permission(self, request, obj=None):
+ return False
+
+ def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
+ extra_context = extra_context or {}
+ extra_context["show_save_and_continue"] = False
+ extra_context["show_save"] = False
+ return super(NotificationAdmin, self).changeform_view(
+ request,
+ object_id,
+ extra_context=extra_context,
+ )
+
+
admin.site.register(NotifiedUser, NotifiedUserAdmin)
admin.site.register(NotificationLog, NotificationLogAdmin)
+admin.site.register(Notification, NotificationAdmin)
diff --git a/notifications/forms.py b/notifications/forms.py
index 6812a9dd1..164a048f7 100644
--- a/notifications/forms.py
+++ b/notifications/forms.py
@@ -10,4 +10,5 @@ class Meta:
"email",
"enrol_packaging",
"enrol_api_publishing",
+ "enrol_goods_report",
]
diff --git a/notifications/migrations/0003_auto_20230908_0850.py b/notifications/migrations/0003_auto_20230908_0850.py
new file mode 100644
index 000000000..750fe6324
--- /dev/null
+++ b/notifications/migrations/0003_auto_20230908_0850.py
@@ -0,0 +1,150 @@
+# Generated by Django 3.2.20 on 2023-09-08 07:50
+
+import django.db.models.deletion
+from django.db import migrations
+from django.db import models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("notifications", "0002_notifieduser_enrol_api_publishing"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="Notification",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("notified_object_pk", models.IntegerField(default=None, null=True)),
+ (
+ "notificaiton_type",
+ models.CharField(
+ choices=[
+ (
+ "",
+ "Goods Report",
+ ),
+ (
+ "",
+ "Packaging Notify Ready",
+ ),
+ (
+ "",
+ "Packaging Accepted",
+ ),
+ (
+ "", # /PS-IGNORE
+ "Packaging Rejected",
+ ),
+ (
+ "",
+ "Publishing Success",
+ ),
+ (
+ "",
+ "Publishing Failed",
+ ),
+ ],
+ max_length=150,
+ ),
+ ),
+ ],
+ ),
+ migrations.RemoveField(
+ model_name="notificationlog",
+ name="template_id",
+ ),
+ migrations.AddField(
+ model_name="notificationlog",
+ name="response_ids",
+ field=models.TextField(default=None, null=True),
+ ),
+ migrations.AddField(
+ model_name="notificationlog",
+ name="success",
+ field=models.BooleanField(default=True),
+ ),
+ migrations.AddField(
+ model_name="notifieduser",
+ name="enrol_goods_report",
+ field=models.BooleanField(default=False),
+ ),
+ migrations.CreateModel(
+ name="CrownDependenciesEnvelopeFailedNotification",
+ fields=[],
+ options={
+ "proxy": True,
+ "indexes": [],
+ "constraints": [],
+ },
+ bases=("notifications.notification",),
+ ),
+ migrations.CreateModel(
+ name="CrownDependenciesEnvelopeSuccessNotification",
+ fields=[],
+ options={
+ "proxy": True,
+ "indexes": [],
+ "constraints": [],
+ },
+ bases=("notifications.notification",),
+ ),
+ migrations.CreateModel(
+ name="EnvelopeAcceptedNotification",
+ fields=[],
+ options={
+ "proxy": True,
+ "indexes": [],
+ "constraints": [],
+ },
+ bases=("notifications.notification",),
+ ),
+ migrations.CreateModel(
+ name="EnvelopeReadyForProcessingNotification",
+ fields=[],
+ options={
+ "proxy": True,
+ "indexes": [],
+ "constraints": [],
+ },
+ bases=("notifications.notification",),
+ ),
+ migrations.CreateModel(
+ name="EnvelopeRejectedNotification",
+ fields=[],
+ options={
+ "proxy": True,
+ "indexes": [],
+ "constraints": [],
+ },
+ bases=("notifications.notification",),
+ ),
+ migrations.CreateModel(
+ name="GoodsSuccessfulImportNotification",
+ fields=[],
+ options={
+ "proxy": True,
+ "indexes": [],
+ "constraints": [],
+ },
+ bases=("notifications.notification",),
+ ),
+ migrations.AddField(
+ model_name="notificationlog",
+ name="notification",
+ field=models.ForeignKey(
+ default=None,
+ null=True,
+ on_delete=django.db.models.deletion.PROTECT,
+ to="notifications.notification",
+ ),
+ ),
+ ]
diff --git a/notifications/models.py b/notifications/models.py
index 0770f1dea..a4bfb6b0c 100644
--- a/notifications/models.py
+++ b/notifications/models.py
@@ -3,9 +3,12 @@
from django.conf import settings
from django.db import models
from django.db.models.query_utils import Q
-from notifications_python_client.notifications import NotificationsAPIClient
+from django.urls import reverse
from common.models.mixins import TimestampedMixin
+from notifications.notify import prepare_link_to_file
+from notifications.notify import send_emails
+from notifications.tasks import send_emails_task
logger = logging.getLogger(__name__)
@@ -17,9 +20,10 @@ class NotifiedUser(models.Model):
email = models.EmailField()
enrol_packaging = models.BooleanField(default=True)
enrol_api_publishing = models.BooleanField(default=False)
+ enrol_goods_report = models.BooleanField(default=False)
- def __str__(self):
- return self.email
+ def __str__(self) -> str:
+ return str(self.email)
class NotificationLog(TimestampedMixin):
@@ -29,6 +33,10 @@ class NotificationLog(TimestampedMixin):
We create one each time a group of users receive an email.
"""
+ response_ids = models.TextField(
+ default=None,
+ null=True,
+ )
recipients = models.TextField()
"""Comma separated email addresses, as a single string, of the recipients of
the notification."""
@@ -36,6 +44,48 @@ class NotificationLog(TimestampedMixin):
"notifications.Notification",
default=None,
null=True,
+ on_delete=models.PROTECT,
+ )
+ success = models.BooleanField(default=True)
+
+
+class CustomNotificationChoice:
+ def __init__(self, value, label, template_id):
+ self.value = value
+ self.label = label
+ self.template_id = template_id
+
+
+class NotificationTypeChoices(models.TextChoices):
+ GOODS_REPORT = CustomNotificationChoice(
+ "goods_report",
+ "Goods Report",
+ settings.GOODS_REPORT_TEMPLATE_ID,
+ )
+ PACKAGING_NOTIFY_READY = CustomNotificationChoice(
+ "packaging_notify_ready",
+ "Packaging Notify Ready",
+ settings.READY_FOR_CDS_TEMPLATE_ID,
+ )
+ PACKAGING_ACCEPTED = CustomNotificationChoice(
+ "packaging_accepted",
+ "Packaging Accepted",
+ settings.CDS_ACCEPTED_TEMPLATE_ID,
+ )
+ PACKAGING_REJECTED = CustomNotificationChoice(
+ "packaging_rejected",
+ "Packaging Rejected",
+ settings.CDS_REJECTED_TEMPLATE_ID,
+ )
+ PUBLISHING_SUCCESS = CustomNotificationChoice(
+ "publishing_success",
+ "Publishing Successful",
+ settings.API_PUBLISH_SUCCESS_TEMPLATE_ID,
+ )
+ PUBLISHING_FAILED = CustomNotificationChoice(
+ "publishing_failed",
+ "Publishing Failed",
+ settings.API_PUBLISH_FAILED_TEMPLATE_ID,
)
@@ -50,8 +100,9 @@ class Notification(models.Model):
https://docs.djangoproject.com/en/dev/topics/db/models/#proxy-models
"""
- def __init__(self, notified_object_pk: int = None):
- self.notified_object_pk = notified_object_pk
+ # def __init__(self, notificaiton_type: NotificationTypeChoices, notified_object_pk: int = None, ):
+ # self.notified_object_pk = notified_object_pk
+ # self.notificaiton_type = notificaiton_type
notified_object_pk = models.IntegerField(
default=None,
@@ -59,6 +110,19 @@ def __init__(self, notified_object_pk: int = None):
)
"""The primary key of the object being notified on."""
+ notificaiton_type = models.CharField(
+ max_length=150,
+ choices=[(choice.value, choice.label) for choice in NotificationTypeChoices],
+ )
+
+ def get_personalisation(self) -> dict:
+ """
+ Returns the personalisation of the notified object.
+
+ Implement in concrete subclasses.
+ """
+ raise NotImplementedError
+
def notify_template_id(self) -> str:
"""
GOV.UK Notify template ID specific to each Notification sub-class.
@@ -83,11 +147,14 @@ def notified_object(self) -> models.Model:
"""
raise NotImplementedError
+ def synchronous_send_emails(self):
+ """Synchronously call to send a notification email."""
+ send_emails_task(self.pk, type(self))
+
def schedule_send_emails(self, countdown=1):
"""Schedule a call to send a notification email, run as an asynchronous
background task."""
-
- send_emails.apply_sync(args=[self.pk], countdown=countdown)
+ send_emails_task.apply_sync(args=[self.pk, type(self)], countdown=countdown)
def send_emails(self):
"""Send the notification emails to users via GOV.UK Notify."""
@@ -100,28 +167,28 @@ def send_emails(self):
)
return
- notifications_client = NotificationsAPIClient(settings.NOTIFICATIONS_API_KEY)
- recipients = ""
- for user in notified_users:
- try:
- notifications_client.send_email_notification(
- email_address=user.email,
- template_id=self.notify_template_id(),
- # TODO: get personalisation data.
- personalisation={},
- )
-
- recipients += f"{user.email} \n"
- except:
- logger.error(
- f"Failed to send email notification to {user.email}.",
- )
+ personalisation = self.get_personalisation()
+
+ result = send_emails(
+ self.notify_template_id(),
+ personalisation,
+ notified_users,
+ )
NotificationLog.objects.create(
- recipients=recipients,
+ response_ids=result["response_ids"],
+ recipients=result["recipients"],
notification=self,
)
+ # if any emails failed create a log for unsuccessful emails
+ if result["failed_recipients"]:
+ NotificationLog.objects.create(
+ recipients=result["failed_recipients"],
+ notification=self,
+ success=False,
+ )
+
class EnvelopeReadyForProcessingNotification(Notification):
"""Manage sending notifications when envelopes are ready for processing by
@@ -130,12 +197,33 @@ class EnvelopeReadyForProcessingNotification(Notification):
class Meta:
proxy = True
+ def get_personalisation(self) -> dict:
+ packaged_workbasket = self.notified_object()
+ eif = "Immediately"
+ if packaged_workbasket.eif:
+ eif = packaged_workbasket.eif.strftime("%d/%m/%Y")
+
+ personalisation = {
+ "envelope_id": packaged_workbasket.envelope.envelope_id,
+ "description": packaged_workbasket.description,
+ "download_url": (
+ settings.BASE_SERVICE_URL + reverse("publishing:envelope-queue-ui-list")
+ ),
+ "theme": packaged_workbasket.theme,
+ "eif": eif,
+ "embargo": packaged_workbasket.embargo
+ if packaged_workbasket.embargo
+ else "None",
+ "jira_url": packaged_workbasket.jira_url,
+ }
+ return personalisation
+
def notify_template_id(self) -> str:
- return settings.READY_FOR_CDS_TEMPLATE_ID
+ return NotificationTypeChoices.PACKAGING_NOTIFY_READY.template_id
def notified_users(self):
return NotifiedUser.objects.filter(
- Q(enrol_packaging=True) | Q(enrole_api_publishing=True),
+ Q(enrol_packaging=True),
)
def notified_object(self) -> models.Model:
@@ -149,30 +237,194 @@ def notified_object(self) -> models.Model:
class EnvelopeAcceptedNotification(Notification):
- """TODO."""
+ """Manage sending notifications when envelopes have been accepted by
+ HMRC."""
+
+ class Meta:
+ proxy = True
+
+ def get_personalisation(self) -> dict:
+ packaged_workbasket = self.notified_object()
+ loading_report_message = "Loading report: No loading report was provided."
+ loading_reports = packaged_workbasket.loadingreports.exclude(
+ file_name="",
+ ).values_list(
+ "file_name",
+ flat=True,
+ )
+ if loading_reports:
+ file_names = ", ".join(loading_reports)
+ loading_report_message = f"Loading report(s): {file_names}"
+
+ personalisation = {
+ "envelope_id": packaged_workbasket.envelope.envelope_id,
+ "transaction_count": packaged_workbasket.workbasket.transactions.count(),
+ "loading_report_message": loading_report_message,
+ "comments": packaged_workbasket.loadingreports.first().comments,
+ }
+ return personalisation
+
+ def notify_template_id(self) -> str:
+ return NotificationTypeChoices.PACKAGING_ACCEPTED.template_id
+
+ def notified_users(self):
+ return NotifiedUser.objects.filter(
+ Q(enrol_packaging=True),
+ )
+
+ def notified_object(self) -> models.Model:
+ from publishing.models import PackagedWorkBasket
+
+ return (
+ PackagedWorkBasket.objects.get(self.notified_object_pk)
+ if self.notified_object_pk
+ else None
+ )
class EnvelopeRejectedNotification(Notification):
- """TODO."""
+ """Manage sending notifications when envelopes have been rejected by
+ HMRC."""
+ class Meta:
+ proxy = True
-class GoodsSuccessfulImportNotification(Notification):
- """TODO."""
+ def get_personalisation(self) -> dict:
+ packaged_workbasket = self.notified_object()
+ loading_report_message = "Loading report: No loading report was provided."
+ loading_reports = packaged_workbasket.loadingreports.exclude(
+ file_name="",
+ ).values_list(
+ "file_name",
+ flat=True,
+ )
+ if loading_reports:
+ file_names = ", ".join(loading_reports)
+ loading_report_message = f"Loading report(s): {file_names}"
+
+ personalisation = {
+ "envelope_id": packaged_workbasket.envelope.envelope_id,
+ "transaction_count": packaged_workbasket.workbasket.transactions.count(),
+ "loading_report_message": loading_report_message,
+ "comments": packaged_workbasket.loadingreports.first().comments,
+ }
+ return personalisation
+ def notify_template_id(self) -> str:
+ return NotificationTypeChoices.PACKAGING_REJECTED.template_id
-# ========================== start ==========================
-# Code between start and end is a rewrite of, and drop-in replacement for,
-# notifications.tasks.send_emails().
+ def notified_users(self):
+ return NotifiedUser.objects.filter(
+ Q(enrol_packaging=True),
+ )
-from celery import shared_task
-from django.db.transaction import atomic
+ def notified_object(self) -> models.Model:
+ from publishing.models import PackagedWorkBasket
+ return (
+ PackagedWorkBasket.objects.get(self.notified_object_pk)
+ if self.notified_object_pk
+ else None
+ )
+
+
+class CrownDependenciesEnvelopeSuccessNotification(Notification):
+ """Manage sending notifications when envelopes have been successfully
+ published to the Crown Dependencies api."""
+
+ class Meta:
+ proxy = True
+
+ def get_personalisation(self) -> dict:
+ crown_dependicies_envelope = self.notified_object()
+ personalisation = {
+ "envelope_id": crown_dependicies_envelope.packagedworkbaskets.last().envelope.envelope_id,
+ }
+ return personalisation
+
+ def notify_template_id(self) -> str:
+ return NotificationTypeChoices.PUBLISHING_SUCCESS.template_id
+
+ def notified_users(self):
+ return NotifiedUser.objects.filter(
+ Q(enrole_api_publishing=True),
+ )
+
+ def notified_object(self) -> models.Model:
+ from publishing.models import CrownDependenciesEnvelope
+
+ return (
+ CrownDependenciesEnvelope.objects.get(self.notified_object_pk)
+ if self.notified_object_pk
+ else None
+ )
+
+
+class CrownDependenciesEnvelopeFailedNotification(Notification):
+ """Manage sending notifications when envelopes have been failed being
+ published to the Crown Dependencies api."""
+
+ class Meta:
+ proxy = True
+
+ def get_personalisation(self) -> dict:
+ self.notified_object()
+ personalisation = {
+ "envelope_id": self.packagedworkbaskets.last().envelope.envelope_id,
+ }
+ return personalisation
+
+ def notify_template_id(self) -> str:
+ return NotificationTypeChoices.PUBLISHING_FAILED.template_id
+
+ def notified_users(self):
+ return NotifiedUser.objects.filter(
+ Q(enrole_api_publishing=True),
+ )
+
+ def notified_object(self) -> models.Model:
+ from publishing.models import CrownDependenciesEnvelope
+
+ return (
+ CrownDependenciesEnvelope.objects.get(self.notified_object_pk)
+ if self.notified_object_pk
+ else None
+ )
+
+
+class GoodsSuccessfulImportNotification(Notification):
+ """Manage sending notifications when a goods report has been reviewed and
+ can be sent to Crown Dependencies."""
-@shared_task
-@atomic
-def send_emails(notification_pk: int):
- notification = Notification.objects.get(pk=notification_pk)
- notification.send_emails()
+ class Meta:
+ proxy = True
+
+ def get_personalisation(self) -> dict:
+ import_batch = self.notified_object()
+ personalisation = (
+ {
+ "tgb_id": import_batch.name,
+ "link_to_file": prepare_link_to_file(
+ import_batch.taric_file,
+ confirm_email_before_download=True,
+ ),
+ },
+ )
+ return personalisation
+ def notify_template_id(self) -> str:
+ return NotificationTypeChoices.GOODS_REPORT.template_id
-# ========================== end ==========================
+ def notified_users(self):
+ return NotifiedUser.objects.filter(
+ Q(enrol_goods_report=True),
+ )
+
+ def notified_object(self) -> models.Model:
+ from importer.models import ImportBatch
+
+ return (
+ ImportBatch.objects.get(self.notified_object_pk)
+ if self.notified_object_pk
+ else None
+ )
diff --git a/notifications/notify.py b/notifications/notify.py
new file mode 100644
index 000000000..643181268
--- /dev/null
+++ b/notifications/notify.py
@@ -0,0 +1,86 @@
+import logging
+from tempfile import NamedTemporaryFile
+from typing import List
+
+from django.conf import settings
+from notifications_python_client import prepare_upload
+from notifications_python_client.notifications import NotificationsAPIClient
+
+from importer.goods_report import GoodsReporter
+
+logger = logging.getLogger(__name__)
+
+
+def get_notifications_client():
+ return NotificationsAPIClient(settings.NOTIFICATIONS_API_KEY)
+
+
+def prepare_link_to_file(
+ file,
+ is_csv=False,
+ confirm_email_before_download=None,
+ retention_period=None,
+):
+ """
+ Prepare importer file to upload. Improvement possibility have file pre
+ genreated and in s3 possibly.
+
+ params:
+ file: file which to generate report from
+ is_csv: if the file being attached is a csv set to True, default False
+ confirm_email_before_download: security feature where user opening files must be on Gov Notify email list
+ retention_period: how long the file link is valid for, default 6 months
+ """
+
+ with NamedTemporaryFile(suffix=".xlsx") as tmp:
+ reporter = GoodsReporter(file)
+ goods_report = reporter.create_report()
+ goods_report.xlsx_file(tmp)
+ return prepare_upload(
+ tmp,
+ is_csv,
+ confirm_email_before_download,
+ retention_period,
+ )
+
+
+def send_emails(template_id, personalisation: dict, email_addresses: List[str]) -> dict:
+ """
+ Generic send emails function which triggers a notification to Gov notify.
+
+ params:
+ template_id: email template Id
+ personalisation: email personalisation
+ email_addresses: list of emails to send emails to
+
+ returns:
+ dict(
+ "response_ids": string of successful email response ids
+ "recipients": string of successful emails recipients
+ "failed_recipients": string of unsuccessful emails recipients
+ )
+ """
+ notification_client = get_notifications_client()
+ recipients = ""
+ failed_recipients = ""
+ response_ids = ""
+ for email in email_addresses:
+ try:
+ response = notification_client.send_email_notification(
+ email_address=email,
+ template_id=template_id,
+ personalisation=personalisation,
+ )
+ response_ids += f"{response['id']} \n"
+ recipients += f"{email} \n"
+ except Exception as e:
+ failed_recipients += f"{email} \n"
+ logger.error(
+ f"Failed to send email notification to {email}, with status code {e.status_code}.",
+ )
+
+ return {
+ "response_ids": response_ids,
+ "recipients": recipients,
+ "failed_recipients": failed_recipients,
+ }
diff --git a/notifications/tasks.py b/notifications/tasks.py
index 407f8e657..cc7d292f2 100644
--- a/notifications/tasks.py
+++ b/notifications/tasks.py
@@ -1,50 +1,18 @@
import logging
-from uuid import uuid4
from celery import shared_task
-from django.conf import settings
-from django.db.models.query_utils import Q
from django.db.transaction import atomic
-from notifications_python_client.notifications import NotificationsAPIClient
-
-from notifications.models import NotificationLog
-from notifications.models import NotifiedUser
logger = logging.getLogger(__name__)
-def get_notifications_client():
- return NotificationsAPIClient(settings.NOTIFICATIONS_API_KEY)
-
-
+# def get_notifications_client():
+# return NotificationsAPIClient(settings.NOTIFICATIONS_API_KEY)
@shared_task
@atomic
-def send_emails(template_id: uuid4, personalisation: dict, email_type: str = None):
+def send_emails_task(notification_pk: int, notification_type: "Notification"):
"""Task for emailing all users signed up to receive packaging updates and
creating a log to record which users received which email template."""
- user_filters = {
- "packaging": Q(enrol_packaging=True),
- "publishing": Q(enrol_api_publishing=True),
- }
- # Will get all users by default
- users = NotifiedUser.objects.filter(user_filters.get(email_type, Q()))
- if users.exists():
- notifications_client = get_notifications_client()
- recipients = ""
- for user in users:
- try:
- notifications_client.send_email_notification(
- email_address=user.email,
- template_id=template_id,
- personalisation=personalisation,
- )
- recipients += f"{user.email} \n"
- except:
- logger.error(
- f"Failed to send email notification to {user.email}.",
- )
-
- NotificationLog.objects.create(
- template_id=template_id,
- recipients=recipients,
- )
+ print(notification_type)
+ notification = notification_type.objects.get(pk=notification_pk)
+ notification.send_emails()
diff --git a/notifications/utils.py b/notifications/utils.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/publishing/models/crown_dependencies_envelope.py b/publishing/models/crown_dependencies_envelope.py
index b1e778659..4e49577ac 100644
--- a/publishing/models/crown_dependencies_envelope.py
+++ b/publishing/models/crown_dependencies_envelope.py
@@ -1,7 +1,6 @@
import logging
from datetime import datetime
-from django.conf import settings
from django.db.models import DateTimeField
from django.db.models import Manager
from django.db.models import QuerySet
@@ -10,7 +9,9 @@
from django_fsm import transition
from common.models.mixins import TimestampedMixin
-from notifications.tasks import send_emails
+from notifications.models import CrownDependenciesEnvelopeFailedNotification
+from notifications.models import CrownDependenciesEnvelopeSuccessNotification
+from notifications.models import NotificationTypeChoices
from publishing.models.decorators import save_after
from publishing.models.decorators import skip_notifications_if_disabled
from publishing.models.packaged_workbasket import PackagedWorkBasket
@@ -79,11 +80,13 @@ class CrownDependenciesEnvelope(TimestampedMixin):
"""
Represents a crown dependencies envelope.
- This model contains the Envelope upload status to the Channel islands API and it's publishing times.
+ This model contains the Envelope upload status to the Channel islands API
+ and it's publishing times.
An Envelope contains one or more Transactions, listing changes to be applied
to the tariff in the sequence defined by the transaction IDs. Contains
- xml_file which is a reference to the envelope stored on s3. This can be found in the Envelope model.
+ xml_file which is a reference to the envelope stored on s3. This can be
+ found in the Envelope model.
"""
class Meta:
@@ -149,43 +152,34 @@ def publishing_failed(self):
with a failed outcome."""
self.notify_publishing_failed()
- def notify_publishing_completed(self, template_id: str):
- """
- Notify users that envelope publishing has completed (success or failure)
- for this instance.
-
- `template_id` should be the ID of the Notify email template of either
- the successfully published or failed publishing email.
- """
-
- personalisation = {
- "envelope_id": self.packagedworkbaskets.last().envelope.envelope_id,
- }
-
- send_emails.delay(
- template_id=template_id,
- personalisation=personalisation,
- email_type="publishing",
- )
-
@skip_notifications_if_disabled
def notify_publishing_success(self):
"""Notify users that an envelope has successfully publishing to api."""
- self.notify_publishing_completed(settings.API_PUBLISH_SUCCESS_TEMPLATE_ID)
+ notification = CrownDependenciesEnvelopeSuccessNotification(
+ notificaiton_type=NotificationTypeChoices.PUBLISHING_SUCCESS,
+ notified_object_pk=self.pk,
+ )
+ notification.save()
+ notification.schedule_send_emails()
@skip_notifications_if_disabled
def notify_publishing_failed(self):
"""Notify users that an envelope has failed publishing to api."""
- self.notify_publishing_completed(settings.API_PUBLISH_FAILED_TEMPLATE_ID)
+ notification = CrownDependenciesEnvelopeFailedNotification(
+ notificaiton_type=NotificationTypeChoices.PUBLISHING_FAILED,
+ notified_object_pk=self.pk,
+ )
+ notification.save()
+ notification.schedule_send_emails()
@atomic
def refresh_from_db(self, using=None, fields=None):
"""Reload instance from database but avoid writing to
self.publishing_state directly in order to avoid the exception
- 'AttributeError: Direct publishing_state modification is not allowed.'
- """
+ 'AttributeError: Direct publishing_state modification is not
+ allowed.'."""
if fields is None:
refresh_state = True
fields = [f.name for f in self._meta.concrete_fields]
diff --git a/publishing/models/packaged_workbasket.py b/publishing/models/packaged_workbasket.py
index 1c7bc3cc5..72783ffb1 100644
--- a/publishing/models/packaged_workbasket.py
+++ b/publishing/models/packaged_workbasket.py
@@ -1,7 +1,6 @@
import logging
from datetime import datetime
-from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import PROTECT
from django.db.models import SET_NULL
@@ -20,13 +19,15 @@
from django.db.models import Value
from django.db.models.functions import Coalesce
from django.db.transaction import atomic
-from django.urls import reverse
from django_fsm import FSMField
from django_fsm import transition
from common.models.mixins import TimestampedMixin
+from notifications.models import EnvelopeAcceptedNotification
+from notifications.models import EnvelopeReadyForProcessingNotification
+from notifications.models import EnvelopeRejectedNotification
from notifications.models import NotificationLog
-from notifications.tasks import send_emails
+from notifications.models import NotificationTypeChoices
from publishing import models as publishing_models
from publishing.models.decorators import save_after
from publishing.models.decorators import skip_notifications_if_disabled
@@ -397,7 +398,7 @@ def create_envelope_for_top(cls):
def next_expected_to_api(self) -> bool:
"""
- checks if previous envelope in sequence has been published to the API.
+ Checks if previous envelope in sequence has been published to the API.
This check will check if the previous packaged workbasket has a
CrownDependenciesEnvelope OR has published_to_tariffs_api set in the
@@ -531,8 +532,8 @@ def abandon(self):
def refresh_from_db(self, using=None, fields=None):
"""Reload instance from database but avoid writing to
self.processing_state directly in order to avoid the exception
- 'AttributeError: Direct processing_state modification is not allowed.'
- """
+ 'AttributeError: Direct processing_state modification is not
+ allowed.'."""
if fields is None:
refresh_state = True
fields = [f.name for f in self._meta.concrete_fields]
@@ -563,71 +564,34 @@ def notify_ready_for_processing(self):
therefore normally called when the process for doing that has completed
(see `publishing.tasks.create_xml_envelope_file()`).
"""
- eif = "Immediately"
- if self.eif:
- eif = self.eif.strftime("%d/%m/%Y")
-
- personalisation = {
- "envelope_id": self.envelope.envelope_id,
- "description": self.description,
- "download_url": (
- settings.BASE_SERVICE_URL + reverse("publishing:envelope-queue-ui-list")
- ),
- "theme": self.theme,
- "eif": eif,
- "embargo": self.embargo if self.embargo else "None",
- "jira_url": self.jira_url,
- }
-
- send_emails.delay(
- template_id=settings.READY_FOR_CDS_TEMPLATE_ID,
- personalisation=personalisation,
- email_type="packaging",
- )
-
- def notify_processing_completed(self, template_id):
- """
- Notify users that envelope processing has completed (accepted or
- rejected) for this instance.
-
- `template_id` should be the ID of the Notify email template of either
- the successfully processed email or failed processing.
- """
- loading_report_message = "Loading report: No loading report was provided."
- loading_reports = self.loadingreports.exclude(file_name="").values_list(
- "file_name",
- flat=True,
- )
- if loading_reports:
- file_names = ", ".join(loading_reports)
- loading_report_message = f"Loading report(s): {file_names}"
-
- personalisation = {
- "envelope_id": self.envelope.envelope_id,
- "transaction_count": self.workbasket.transactions.count(),
- "loading_report_message": loading_report_message,
- "comments": self.loadingreports.first().comments,
- }
-
- send_emails.delay(
- template_id=template_id,
- personalisation=personalisation,
- email_type="packaging",
+ notification = EnvelopeReadyForProcessingNotification(
+ notificaiton_type=NotificationTypeChoices.PACKAGING_NOTIFY_READY,
+ notified_object_pk=self.pk,
)
+ notification.save()
+ notification.schedule_send_emails()
@skip_notifications_if_disabled
def notify_processing_succeeded(self):
"""Notify users that envelope processing has succeeded (i.e. the
associated envelope was correctly ingested into HMRC systems)."""
-
- self.notify_processing_completed(settings.CDS_ACCEPTED_TEMPLATE_ID)
+ notification = EnvelopeAcceptedNotification(
+ notificaiton_type=NotificationTypeChoices.PACKAGING_ACCEPTED,
+ notified_object_pk=self.pk,
+ )
+ notification.save()
+ notification.schedule_send_emails()
@skip_notifications_if_disabled
def notify_processing_failed(self):
"""Notify users that envelope processing has failed (i.e. HMRC systems
rejected this instance's associated envelope file)."""
-
- self.notify_processing_completed(settings.CDS_REJECTED_TEMPLATE_ID)
+ notification = EnvelopeRejectedNotification(
+ notificaiton_type=NotificationTypeChoices.PACKAGING_REJECTED,
+ notified_object_pk=self.pk,
+ )
+ notification.save()
+ notification.schedule_send_emails()
@property
def cds_notified_notification_log(self) -> NotificationLog:
diff --git a/sample.env b/sample.env
index d08a7e967..d363eecd8 100644
--- a/sample.env
+++ b/sample.env
@@ -58,6 +58,7 @@ CDS_ACCEPTED_TEMPLATE_ID=cds_accepted_template_id
CDS_REJECTED_TEMPLATE_ID=cds_rejected_template_id
API_PUBLISH_SUCCESS_TEMPLATE_ID=api_success_template_id
API_PUBLISH_FAILED_TEMPLATE_ID=api_failed_template_id
+GOODS_REPORT_TEMPLATE_ID=goods_report_template_id
# Base service URL.
BASE_SERVICE_URL=http://localhost:8000
diff --git a/settings/common.py b/settings/common.py
index 69febc37f..aa667ea4f 100644
--- a/settings/common.py
+++ b/settings/common.py
@@ -755,6 +755,7 @@
CDS_REJECTED_TEMPLATE_ID = os.environ.get("CDS_REJECTED_TEMPLATE_ID")
API_PUBLISH_SUCCESS_TEMPLATE_ID = os.environ.get("API_PUBLISH_SUCCESS_TEMPLATE_ID")
API_PUBLISH_FAILED_TEMPLATE_ID = os.environ.get("API_PUBLISH_FAILED_TEMPLATE_ID")
+GOODS_REPORT_TEMPLATE_ID = os.environ.get("GOODS_REPORT_TEMPLATE_ID")
# Base service URL - required when constructing an absolute TAP URL to a page
# from a Celery task where no HTTP request object is available.
diff --git a/workbaskets/jinja2/workbaskets/review-goods.jinja b/workbaskets/jinja2/workbaskets/review-goods.jinja
index 720cbb987..c39c982ea 100644
--- a/workbaskets/jinja2/workbaskets/review-goods.jinja
+++ b/workbaskets/jinja2/workbaskets/review-goods.jinja
@@ -37,6 +37,15 @@
"classes": "govuk-button--secondary",
}) }}
{% endif %}
+
+ {% if unsent_notification %}
+ {{ govukButton({
+ "text": "Notify Channel Islands",
+ "href": url("goods-report-notify", kwargs={"pk": import_batch_pk}),
+ "classes": "govuk-button--primary",
+ "preventDoubleClick": true,
+ }) }}
+ {% endif %}
{% if report_lines %}
diff --git a/workbaskets/jinja2/workbaskets/summary-workbasket.jinja b/workbaskets/jinja2/workbaskets/summary-workbasket.jinja
index c7d116e22..81f15cd6d 100644
--- a/workbaskets/jinja2/workbaskets/summary-workbasket.jinja
+++ b/workbaskets/jinja2/workbaskets/summary-workbasket.jinja
@@ -95,9 +95,18 @@
aria-labelledby="summary-title" role="region" data-module="govuk-notification-banner">
{{ rule_check_result_content() }}
-
- Send to packaging queue
-
+ {% if unsent_notification %}
+
+ For commodity code imports a channel islands notification must be sent from the review goods tab before packaging.
+
+
+ Send to packaging queue
+
+ {% else %}
+
+ Send to packaging queue
+
+ {% endif %}
{% endset %}
diff --git a/workbaskets/views/ui.py b/workbaskets/views/ui.py
index 9f67951ca..72bd43398 100644
--- a/workbaskets/views/ui.py
+++ b/workbaskets/views/ui.py
@@ -38,6 +38,8 @@
from importer.goods_report import GoodsReportLine
from measures.filters import MeasureFilter
from measures.models import Measure
+from notifications.models import Notification
+from notifications.models import NotificationTypeChoices
from workbaskets import forms
from workbaskets.models import WorkBasket
from workbaskets.session_store import SessionStore
@@ -346,6 +348,15 @@ def get_context_data(self, *args, **kwargs):
]
context["import_batch_pk"] = import_batch.pk
+ # notifications only relevant to a goods import
+ context["unsent_notification"] = (
+ import_batch.goods_import
+ and not Notification.objects.filter(
+ notified_object_pk=import_batch.pk,
+ notificaiton_type=NotificationTypeChoices.GOODS_REPORT,
+ ).exists()
+ )
+
return context
@@ -499,6 +510,19 @@ def get_context_data(self, **kwargs):
self.request.user.is_superuser
or self.request.user.has_perm("workbaskets.delete_workbasket")
)
+ # set to true if there is an associated goods import batch with an unsent notification
+ try:
+ import_batch = self.workbasket.importbatch
+ unsent_notifcation = (
+ import_batch
+ and import_batch.goods_import
+ and not Notification.objects.filter(
+ notified_object_pk=import_batch.pk,
+ notificaiton_type=NotificationTypeChoices.GOODS_REPORT,
+ ).exists()
+ )
+ except ObjectDoesNotExist:
+ unsent_notifcation = False
context.update(
{
"workbasket": self.workbasket,
@@ -506,6 +530,7 @@ def get_context_data(self, **kwargs):
"uploaded_envelope_dates": self.uploaded_envelope_dates,
"rule_check_in_progress": False,
"user_can_delete_workbasket": user_can_delete_workbasket,
+ "unsent_notification": unsent_notifcation,
},
)
if self.workbasket.rule_check_task_id:
From 30a5d759f9c4e16e3f7c8a88ad16279d6b570ab9 Mon Sep 17 00:00:00 2001
From: Anthoni Gleeson
Date: Mon, 11 Sep 2023 16:51:20 +0100
Subject: [PATCH 06/15] address type error and serialisation issues
---
.../send_goods_report_notification.py | 2 +-
importer/views.py | 5 +-
notifications/admin.py | 9 +-
...908_0850.py => 0003_auto_20230911_0924.py} | 36 ++------
notifications/models.py | 89 ++++++++++---------
notifications/tasks.py | 10 ++-
.../models/crown_dependencies_envelope.py | 4 +-
publishing/models/packaged_workbasket.py | 6 +-
workbaskets/views/ui.py | 4 +-
9 files changed, 75 insertions(+), 90 deletions(-)
rename notifications/migrations/{0003_auto_20230908_0850.py => 0003_auto_20230911_0924.py} (71%)
diff --git a/importer/management/commands/send_goods_report_notification.py b/importer/management/commands/send_goods_report_notification.py
index f08a03ddc..403a20d67 100644
--- a/importer/management/commands/send_goods_report_notification.py
+++ b/importer/management/commands/send_goods_report_notification.py
@@ -19,7 +19,7 @@ def send_notifcation(
)
notification = GoodsSuccessfulImportNotification(
- notificaiton_type=NotificationTypeChoices.GOODS_REPORT,
+ notification_type=NotificationTypeChoices.GOODS_REPORT,
notified_object_pk=import_batch.id,
)
notification.save()
diff --git a/importer/views.py b/importer/views.py
index bdfad1855..54c875c37 100644
--- a/importer/views.py
+++ b/importer/views.py
@@ -254,10 +254,7 @@ def get(self, request, *args, **kwargs):
# create notification
notification = GoodsSuccessfulImportNotification(
- notificaiton_type=(
- NotificationTypeChoices.GOODS_REPORT.value,
- NotificationTypeChoices.GOODS_REPORT.label,
- ),
+ notification_type=NotificationTypeChoices.GOODS_REPORT,
notified_object_pk=import_batch.id,
)
notification.save()
diff --git a/notifications/admin.py b/notifications/admin.py
index 4ffa133f1..077f33549 100644
--- a/notifications/admin.py
+++ b/notifications/admin.py
@@ -72,19 +72,22 @@ def changeform_view(self, request, object_id=None, form_url="", extra_context=No
class NotificationAdmin(admin.ModelAdmin):
"""Class providing read-only list and detail views for notification."""
+ # TODO add filters on notification_type
+
ordering = ["pk"]
list_display = (
"pk",
- "notificaiton_type",
+ "notification_type",
"notified_object_pk",
- # "display_users",
+ "display_users",
)
readonly_fields = []
def display_users(self, obj):
+ subclass_obj = obj.return_subclass_instance()
return ", ".join(
- [user.email for user in obj.notified_users().all()],
+ [user.email for user in subclass_obj.notified_users().all()],
)
display_users.short_description = "Recipients"
diff --git a/notifications/migrations/0003_auto_20230908_0850.py b/notifications/migrations/0003_auto_20230911_0924.py
similarity index 71%
rename from notifications/migrations/0003_auto_20230908_0850.py
rename to notifications/migrations/0003_auto_20230911_0924.py
index 750fe6324..523c7bbed 100644
--- a/notifications/migrations/0003_auto_20230908_0850.py
+++ b/notifications/migrations/0003_auto_20230911_0924.py
@@ -1,4 +1,4 @@
-# Generated by Django 3.2.20 on 2023-09-08 07:50
+# Generated by Django 3.2.20 on 2023-09-11 08:24
import django.db.models.deletion
from django.db import migrations
@@ -25,35 +25,17 @@ class Migration(migrations.Migration):
),
("notified_object_pk", models.IntegerField(default=None, null=True)),
(
- "notificaiton_type",
+ "notification_type",
models.CharField(
choices=[
- (
- "",
- "Goods Report",
- ),
- (
- "",
- "Packaging Notify Ready",
- ),
- (
- "",
- "Packaging Accepted",
- ),
- (
- "", # /PS-IGNORE
- "Packaging Rejected",
- ),
- (
- "",
- "Publishing Success",
- ),
- (
- "",
- "Publishing Failed",
- ),
+ ("goods_report", "Goods Report"),
+ ("packaging_notify_ready", "Packaging Notify Ready"),
+ ("packaging_accepted", "Packaging Accepted"),
+ ("packaging_rejected", "Packaging Rejected"),
+ ("publishing_success", "Publishing Successful"),
+ ("publishing_failed", "Publishing Failed"),
],
- max_length=150,
+ max_length=100,
),
),
],
diff --git a/notifications/models.py b/notifications/models.py
index a4bfb6b0c..1bd733b23 100644
--- a/notifications/models.py
+++ b/notifications/models.py
@@ -49,43 +49,33 @@ class NotificationLog(TimestampedMixin):
success = models.BooleanField(default=True)
-class CustomNotificationChoice:
- def __init__(self, value, label, template_id):
- self.value = value
- self.label = label
- self.template_id = template_id
+# class NotificationManager(models.Manager):
class NotificationTypeChoices(models.TextChoices):
- GOODS_REPORT = CustomNotificationChoice(
+ GOODS_REPORT = (
"goods_report",
"Goods Report",
- settings.GOODS_REPORT_TEMPLATE_ID,
)
- PACKAGING_NOTIFY_READY = CustomNotificationChoice(
+ PACKAGING_NOTIFY_READY = (
"packaging_notify_ready",
"Packaging Notify Ready",
- settings.READY_FOR_CDS_TEMPLATE_ID,
)
- PACKAGING_ACCEPTED = CustomNotificationChoice(
+ PACKAGING_ACCEPTED = (
"packaging_accepted",
"Packaging Accepted",
- settings.CDS_ACCEPTED_TEMPLATE_ID,
)
- PACKAGING_REJECTED = CustomNotificationChoice(
+ PACKAGING_REJECTED = (
"packaging_rejected",
"Packaging Rejected",
- settings.CDS_REJECTED_TEMPLATE_ID,
)
- PUBLISHING_SUCCESS = CustomNotificationChoice(
+ PUBLISHING_SUCCESS = (
"publishing_success",
"Publishing Successful",
- settings.API_PUBLISH_SUCCESS_TEMPLATE_ID,
)
- PUBLISHING_FAILED = CustomNotificationChoice(
+ PUBLISHING_FAILED = (
"publishing_failed",
"Publishing Failed",
- settings.API_PUBLISH_FAILED_TEMPLATE_ID,
)
@@ -110,11 +100,23 @@ class Notification(models.Model):
)
"""The primary key of the object being notified on."""
- notificaiton_type = models.CharField(
- max_length=150,
- choices=[(choice.value, choice.label) for choice in NotificationTypeChoices],
+ notification_type = models.CharField(
+ max_length=100,
+ choices=NotificationTypeChoices.choices,
)
+ def return_subclass_instance(self) -> "Notification":
+ subclasses = {
+ NotificationTypeChoices.GOODS_REPORT: GoodsSuccessfulImportNotification,
+ NotificationTypeChoices.PACKAGING_ACCEPTED: EnvelopeAcceptedNotification,
+ NotificationTypeChoices.PACKAGING_NOTIFY_READY: EnvelopeReadyForProcessingNotification,
+ NotificationTypeChoices.PACKAGING_REJECTED: EnvelopeRejectedNotification,
+ NotificationTypeChoices.PUBLISHING_SUCCESS: CrownDependenciesEnvelopeSuccessNotification,
+ NotificationTypeChoices.PUBLISHING_FAILED: CrownDependenciesEnvelopeFailedNotification,
+ }
+ self.__class__ = subclasses[self.notification_type]
+ return self
+
def get_personalisation(self) -> dict:
"""
Returns the personalisation of the notified object.
@@ -149,12 +151,12 @@ def notified_object(self) -> models.Model:
def synchronous_send_emails(self):
"""Synchronously call to send a notification email."""
- send_emails_task(self.pk, type(self))
+ send_emails_task(self.pk)
def schedule_send_emails(self, countdown=1):
"""Schedule a call to send a notification email, run as an asynchronous
background task."""
- send_emails_task.apply_sync(args=[self.pk, type(self)], countdown=countdown)
+ send_emails_task.apply_async(args=[self.pk], countdown=countdown)
def send_emails(self):
"""Send the notification emails to users via GOV.UK Notify."""
@@ -219,7 +221,7 @@ def get_personalisation(self) -> dict:
return personalisation
def notify_template_id(self) -> str:
- return NotificationTypeChoices.PACKAGING_NOTIFY_READY.template_id
+ return settings.READY_FOR_CDS_TEMPLATE_ID
def notified_users(self):
return NotifiedUser.objects.filter(
@@ -230,7 +232,7 @@ def notified_object(self) -> models.Model:
from publishing.models import PackagedWorkBasket
return (
- PackagedWorkBasket.objects.get(self.notified_object_pk)
+ PackagedWorkBasket.objects.get(pk=self.notified_object_pk)
if self.notified_object_pk
else None
)
@@ -265,7 +267,7 @@ def get_personalisation(self) -> dict:
return personalisation
def notify_template_id(self) -> str:
- return NotificationTypeChoices.PACKAGING_ACCEPTED.template_id
+ return settings.CDS_ACCEPTED_TEMPLATE_ID
def notified_users(self):
return NotifiedUser.objects.filter(
@@ -311,7 +313,7 @@ def get_personalisation(self) -> dict:
return personalisation
def notify_template_id(self) -> str:
- return NotificationTypeChoices.PACKAGING_REJECTED.template_id
+ return settings.CDS_REJECTED_TEMPLATE_ID
def notified_users(self):
return NotifiedUser.objects.filter(
@@ -322,7 +324,7 @@ def notified_object(self) -> models.Model:
from publishing.models import PackagedWorkBasket
return (
- PackagedWorkBasket.objects.get(self.notified_object_pk)
+ PackagedWorkBasket.objects.get(pk=self.notified_object_pk)
if self.notified_object_pk
else None
)
@@ -343,18 +345,18 @@ def get_personalisation(self) -> dict:
return personalisation
def notify_template_id(self) -> str:
- return NotificationTypeChoices.PUBLISHING_SUCCESS.template_id
+ return settings.API_PUBLISH_SUCCESS_TEMPLATE_ID
def notified_users(self):
return NotifiedUser.objects.filter(
- Q(enrole_api_publishing=True),
+ Q(enrol_api_publishing=True),
)
def notified_object(self) -> models.Model:
from publishing.models import CrownDependenciesEnvelope
return (
- CrownDependenciesEnvelope.objects.get(self.notified_object_pk)
+ CrownDependenciesEnvelope.objects.get(pk=self.notified_object_pk)
if self.notified_object_pk
else None
)
@@ -375,18 +377,18 @@ def get_personalisation(self) -> dict:
return personalisation
def notify_template_id(self) -> str:
- return NotificationTypeChoices.PUBLISHING_FAILED.template_id
+ return settings.API_PUBLISH_FAILED_TEMPLATE_ID
def notified_users(self):
return NotifiedUser.objects.filter(
- Q(enrole_api_publishing=True),
+ Q(enrol_api_publishing=True),
)
def notified_object(self) -> models.Model:
from publishing.models import CrownDependenciesEnvelope
return (
- CrownDependenciesEnvelope.objects.get(self.notified_object_pk)
+ CrownDependenciesEnvelope.objects.get(pk=self.notified_object_pk)
if self.notified_object_pk
else None
)
@@ -401,19 +403,17 @@ class Meta:
def get_personalisation(self) -> dict:
import_batch = self.notified_object()
- personalisation = (
- {
- "tgb_id": import_batch.name,
- "link_to_file": prepare_link_to_file(
- import_batch.taric_file,
- confirm_email_before_download=True,
- ),
- },
- )
+ personalisation = {
+ "tgb_id": import_batch.name,
+ "link_to_file": prepare_link_to_file(
+ import_batch.taric_file,
+ confirm_email_before_download=True,
+ ),
+ }
return personalisation
def notify_template_id(self) -> str:
- return NotificationTypeChoices.GOODS_REPORT.template_id
+ return settings.GOODS_REPORT_TEMPLATE_ID
def notified_users(self):
return NotifiedUser.objects.filter(
@@ -423,8 +423,9 @@ def notified_users(self):
def notified_object(self) -> models.Model:
from importer.models import ImportBatch
+ print(self.notified_object_pk)
return (
- ImportBatch.objects.get(self.notified_object_pk)
+ ImportBatch.objects.get(id=self.notified_object_pk)
if self.notified_object_pk
else None
)
diff --git a/notifications/tasks.py b/notifications/tasks.py
index cc7d292f2..da3a97d6b 100644
--- a/notifications/tasks.py
+++ b/notifications/tasks.py
@@ -10,9 +10,11 @@
# return NotificationsAPIClient(settings.NOTIFICATIONS_API_KEY)
@shared_task
@atomic
-def send_emails_task(notification_pk: int, notification_type: "Notification"):
+def send_emails_task(notification_pk: int):
"""Task for emailing all users signed up to receive packaging updates and
creating a log to record which users received which email template."""
- print(notification_type)
- notification = notification_type.objects.get(pk=notification_pk)
- notification.send_emails()
+ from notifications.models import Notification
+
+ notification = Notification.objects.get(pk=notification_pk)
+ sub_notification = notification.return_subclass_instance()
+ sub_notification.send_emails()
diff --git a/publishing/models/crown_dependencies_envelope.py b/publishing/models/crown_dependencies_envelope.py
index 4e49577ac..fd9647586 100644
--- a/publishing/models/crown_dependencies_envelope.py
+++ b/publishing/models/crown_dependencies_envelope.py
@@ -157,7 +157,7 @@ def notify_publishing_success(self):
"""Notify users that an envelope has successfully publishing to api."""
notification = CrownDependenciesEnvelopeSuccessNotification(
- notificaiton_type=NotificationTypeChoices.PUBLISHING_SUCCESS,
+ notification_type=NotificationTypeChoices.PUBLISHING_SUCCESS,
notified_object_pk=self.pk,
)
notification.save()
@@ -168,7 +168,7 @@ def notify_publishing_failed(self):
"""Notify users that an envelope has failed publishing to api."""
notification = CrownDependenciesEnvelopeFailedNotification(
- notificaiton_type=NotificationTypeChoices.PUBLISHING_FAILED,
+ notification_type=NotificationTypeChoices.PUBLISHING_FAILED,
notified_object_pk=self.pk,
)
notification.save()
diff --git a/publishing/models/packaged_workbasket.py b/publishing/models/packaged_workbasket.py
index 72783ffb1..21be98a9d 100644
--- a/publishing/models/packaged_workbasket.py
+++ b/publishing/models/packaged_workbasket.py
@@ -565,7 +565,7 @@ def notify_ready_for_processing(self):
(see `publishing.tasks.create_xml_envelope_file()`).
"""
notification = EnvelopeReadyForProcessingNotification(
- notificaiton_type=NotificationTypeChoices.PACKAGING_NOTIFY_READY,
+ notification_type=NotificationTypeChoices.PACKAGING_NOTIFY_READY,
notified_object_pk=self.pk,
)
notification.save()
@@ -576,7 +576,7 @@ def notify_processing_succeeded(self):
"""Notify users that envelope processing has succeeded (i.e. the
associated envelope was correctly ingested into HMRC systems)."""
notification = EnvelopeAcceptedNotification(
- notificaiton_type=NotificationTypeChoices.PACKAGING_ACCEPTED,
+ notification_type=NotificationTypeChoices.PACKAGING_ACCEPTED,
notified_object_pk=self.pk,
)
notification.save()
@@ -587,7 +587,7 @@ def notify_processing_failed(self):
"""Notify users that envelope processing has failed (i.e. HMRC systems
rejected this instance's associated envelope file)."""
notification = EnvelopeRejectedNotification(
- notificaiton_type=NotificationTypeChoices.PACKAGING_REJECTED,
+ notification_type=NotificationTypeChoices.PACKAGING_REJECTED,
notified_object_pk=self.pk,
)
notification.save()
diff --git a/workbaskets/views/ui.py b/workbaskets/views/ui.py
index 72bd43398..3e74c9d49 100644
--- a/workbaskets/views/ui.py
+++ b/workbaskets/views/ui.py
@@ -353,7 +353,7 @@ def get_context_data(self, *args, **kwargs):
import_batch.goods_import
and not Notification.objects.filter(
notified_object_pk=import_batch.pk,
- notificaiton_type=NotificationTypeChoices.GOODS_REPORT,
+ notification_type=NotificationTypeChoices.GOODS_REPORT,
).exists()
)
@@ -518,7 +518,7 @@ def get_context_data(self, **kwargs):
and import_batch.goods_import
and not Notification.objects.filter(
notified_object_pk=import_batch.pk,
- notificaiton_type=NotificationTypeChoices.GOODS_REPORT,
+ notification_type=NotificationTypeChoices.GOODS_REPORT,
).exists()
)
except ObjectDoesNotExist:
From 1b0ea79c116df5e5070bb29dd9955453e50d2d57 Mon Sep 17 00:00:00 2001
From: Anthoni Gleeson
Date: Mon, 11 Sep 2023 16:52:41 +0100
Subject: [PATCH 07/15] remove comment add todo
---
notifications/notify.py | 1 +
notifications/tasks.py | 2 --
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/notifications/notify.py b/notifications/notify.py
index 643181268..5c0f9f95e 100644
--- a/notifications/notify.py
+++ b/notifications/notify.py
@@ -74,6 +74,7 @@ def send_emails(template_id, personalisation: dict, email_addresses: List[str])
response_ids += f"{response['id']} \n"
recipients += f"{email} \n"
except Exception as e:
+ # TODO extract error info?
failed_recipients += f"{email} \n"
logger.error(
f"Failed to send email notification to {email}, with status code {e.status_code}.",
diff --git a/notifications/tasks.py b/notifications/tasks.py
index da3a97d6b..425bfdfa5 100644
--- a/notifications/tasks.py
+++ b/notifications/tasks.py
@@ -6,8 +6,6 @@
logger = logging.getLogger(__name__)
-# def get_notifications_client():
-# return NotificationsAPIClient(settings.NOTIFICATIONS_API_KEY)
@shared_task
@atomic
def send_emails_task(notification_pk: int):
From ddd73345408dff7f5869c8aa99e8df6be5c49d80 Mon Sep 17 00:00:00 2001
From: Anthoni Gleeson
Date: Mon, 11 Sep 2023 17:30:00 +0100
Subject: [PATCH 08/15] Refactor notification inits and fixed email list
---
.../send_goods_report_notification.py | 2 -
importer/views.py | 2 -
notifications/models.py | 39 ++++++++++++++++++-
.../models/crown_dependencies_envelope.py | 3 --
publishing/models/packaged_workbasket.py | 4 --
5 files changed, 37 insertions(+), 13 deletions(-)
diff --git a/importer/management/commands/send_goods_report_notification.py b/importer/management/commands/send_goods_report_notification.py
index 403a20d67..8db3368eb 100644
--- a/importer/management/commands/send_goods_report_notification.py
+++ b/importer/management/commands/send_goods_report_notification.py
@@ -3,7 +3,6 @@
from importer.models import ImportBatch
from notifications.models import GoodsSuccessfulImportNotification
-from notifications.models import NotificationTypeChoices
def send_notifcation(
@@ -19,7 +18,6 @@ def send_notifcation(
)
notification = GoodsSuccessfulImportNotification(
- notification_type=NotificationTypeChoices.GOODS_REPORT,
notified_object_pk=import_batch.id,
)
notification.save()
diff --git a/importer/views.py b/importer/views.py
index 54c875c37..78f8e9c9e 100644
--- a/importer/views.py
+++ b/importer/views.py
@@ -20,7 +20,6 @@
from importer.goods_report import GoodsReporter
from importer.models import ImportBatchStatus
from notifications.models import GoodsSuccessfulImportNotification
-from notifications.models import NotificationTypeChoices
from workbaskets.validators import WorkflowStatus
@@ -254,7 +253,6 @@ def get(self, request, *args, **kwargs):
# create notification
notification = GoodsSuccessfulImportNotification(
- notification_type=NotificationTypeChoices.GOODS_REPORT,
notified_object_pk=import_batch.id,
)
notification.save()
diff --git a/notifications/models.py b/notifications/models.py
index 1bd733b23..6d142661a 100644
--- a/notifications/models.py
+++ b/notifications/models.py
@@ -174,7 +174,7 @@ def send_emails(self):
result = send_emails(
self.notify_template_id(),
personalisation,
- notified_users,
+ [user.email for user in notified_users],
)
NotificationLog.objects.create(
@@ -199,6 +199,12 @@ class EnvelopeReadyForProcessingNotification(Notification):
class Meta:
proxy = True
+ def __init__(self, notified_object_pk: int):
+ super(EnvelopeReadyForProcessingNotification, self).__init__(
+ notified_object_pk=notified_object_pk,
+ notification_type=NotificationTypeChoices.PACKAGING_NOTIFY_READY,
+ )
+
def get_personalisation(self) -> dict:
packaged_workbasket = self.notified_object()
eif = "Immediately"
@@ -245,6 +251,12 @@ class EnvelopeAcceptedNotification(Notification):
class Meta:
proxy = True
+ def __init__(self, notified_object_pk: int):
+ super(EnvelopeAcceptedNotification, self).__init__(
+ notified_object_pk=notified_object_pk,
+ notification_type=NotificationTypeChoices.PACKAGING_ACCEPTED,
+ )
+
def get_personalisation(self) -> dict:
packaged_workbasket = self.notified_object()
loading_report_message = "Loading report: No loading report was provided."
@@ -291,6 +303,12 @@ class EnvelopeRejectedNotification(Notification):
class Meta:
proxy = True
+ def __init__(self, notified_object_pk: int):
+ super(EnvelopeRejectedNotification, self).__init__(
+ notified_object_pk=notified_object_pk,
+ notification_type=NotificationTypeChoices.PACKAGING_REJECTED,
+ )
+
def get_personalisation(self) -> dict:
packaged_workbasket = self.notified_object()
loading_report_message = "Loading report: No loading report was provided."
@@ -337,6 +355,12 @@ class CrownDependenciesEnvelopeSuccessNotification(Notification):
class Meta:
proxy = True
+ def __init__(self, notified_object_pk: int):
+ super(CrownDependenciesEnvelopeSuccessNotification, self).__init__(
+ notified_object_pk=notified_object_pk,
+ notification_type=NotificationTypeChoices.PUBLISHING_SUCCESS,
+ )
+
def get_personalisation(self) -> dict:
crown_dependicies_envelope = self.notified_object()
personalisation = {
@@ -369,6 +393,12 @@ class CrownDependenciesEnvelopeFailedNotification(Notification):
class Meta:
proxy = True
+ def __init__(self, notified_object_pk: int):
+ super(CrownDependenciesEnvelopeFailedNotification, self).__init__(
+ notified_object_pk=notified_object_pk,
+ notification_type=NotificationTypeChoices.PUBLISHING_FAILED,
+ )
+
def get_personalisation(self) -> dict:
self.notified_object()
personalisation = {
@@ -401,6 +431,12 @@ class GoodsSuccessfulImportNotification(Notification):
class Meta:
proxy = True
+ def __init__(self, notified_object_pk: int):
+ super(GoodsSuccessfulImportNotification, self).__init__(
+ notified_object_pk=notified_object_pk,
+ notification_type=NotificationTypeChoices.GOODS_REPORT,
+ )
+
def get_personalisation(self) -> dict:
import_batch = self.notified_object()
personalisation = {
@@ -423,7 +459,6 @@ def notified_users(self):
def notified_object(self) -> models.Model:
from importer.models import ImportBatch
- print(self.notified_object_pk)
return (
ImportBatch.objects.get(id=self.notified_object_pk)
if self.notified_object_pk
diff --git a/publishing/models/crown_dependencies_envelope.py b/publishing/models/crown_dependencies_envelope.py
index fd9647586..aab4640c7 100644
--- a/publishing/models/crown_dependencies_envelope.py
+++ b/publishing/models/crown_dependencies_envelope.py
@@ -11,7 +11,6 @@
from common.models.mixins import TimestampedMixin
from notifications.models import CrownDependenciesEnvelopeFailedNotification
from notifications.models import CrownDependenciesEnvelopeSuccessNotification
-from notifications.models import NotificationTypeChoices
from publishing.models.decorators import save_after
from publishing.models.decorators import skip_notifications_if_disabled
from publishing.models.packaged_workbasket import PackagedWorkBasket
@@ -157,7 +156,6 @@ def notify_publishing_success(self):
"""Notify users that an envelope has successfully publishing to api."""
notification = CrownDependenciesEnvelopeSuccessNotification(
- notification_type=NotificationTypeChoices.PUBLISHING_SUCCESS,
notified_object_pk=self.pk,
)
notification.save()
@@ -168,7 +166,6 @@ def notify_publishing_failed(self):
"""Notify users that an envelope has failed publishing to api."""
notification = CrownDependenciesEnvelopeFailedNotification(
- notification_type=NotificationTypeChoices.PUBLISHING_FAILED,
notified_object_pk=self.pk,
)
notification.save()
diff --git a/publishing/models/packaged_workbasket.py b/publishing/models/packaged_workbasket.py
index 21be98a9d..d6df65b07 100644
--- a/publishing/models/packaged_workbasket.py
+++ b/publishing/models/packaged_workbasket.py
@@ -27,7 +27,6 @@
from notifications.models import EnvelopeReadyForProcessingNotification
from notifications.models import EnvelopeRejectedNotification
from notifications.models import NotificationLog
-from notifications.models import NotificationTypeChoices
from publishing import models as publishing_models
from publishing.models.decorators import save_after
from publishing.models.decorators import skip_notifications_if_disabled
@@ -565,7 +564,6 @@ def notify_ready_for_processing(self):
(see `publishing.tasks.create_xml_envelope_file()`).
"""
notification = EnvelopeReadyForProcessingNotification(
- notification_type=NotificationTypeChoices.PACKAGING_NOTIFY_READY,
notified_object_pk=self.pk,
)
notification.save()
@@ -576,7 +574,6 @@ def notify_processing_succeeded(self):
"""Notify users that envelope processing has succeeded (i.e. the
associated envelope was correctly ingested into HMRC systems)."""
notification = EnvelopeAcceptedNotification(
- notification_type=NotificationTypeChoices.PACKAGING_ACCEPTED,
notified_object_pk=self.pk,
)
notification.save()
@@ -587,7 +584,6 @@ def notify_processing_failed(self):
"""Notify users that envelope processing has failed (i.e. HMRC systems
rejected this instance's associated envelope file)."""
notification = EnvelopeRejectedNotification(
- notification_type=NotificationTypeChoices.PACKAGING_REJECTED,
notified_object_pk=self.pk,
)
notification.save()
From 052db89bafc50785076452506cb88acb9b4713dd Mon Sep 17 00:00:00 2001
From: Anthoni Gleeson
Date: Wed, 13 Sep 2023 08:15:59 +0100
Subject: [PATCH 09/15] tests, mocks broke & goods report not working
---
common/tests/factories.py | 31 +++
conftest.py | 136 +++++++++++++
.../test_send_goods_report_notification.py | 52 +++++
importer/tests/test_views.py | 27 +++
notifications/tasks.py | 1 +
notifications/tests/conftest.py | 81 ++++++++
notifications/tests/test_models.py | 76 ++++++++
notifications/tests/test_tasks.py | 116 ++++++-----
publishing/tests/conftest.py | 126 ------------
publishing/tests/test_envelope_queue_views.py | 4 +-
.../test_model_crown_dependencies_envelope.py | 26 +--
publishing/tests/test_models.py | 27 +--
workbaskets/tests/test_views.py | 182 ++++++++++++++++++
13 files changed, 658 insertions(+), 227 deletions(-)
create mode 100644 importer/tests/management/commands/test_send_goods_report_notification.py
create mode 100644 notifications/tests/conftest.py
create mode 100644 notifications/tests/test_models.py
diff --git a/common/tests/factories.py b/common/tests/factories.py
index 278cf4b3d..febc7c3f7 100644
--- a/common/tests/factories.py
+++ b/common/tests/factories.py
@@ -1404,3 +1404,34 @@ class CrownDependenciesPublishingOperationalStatusFactory(
class Meta:
model = "publishing.CrownDependenciesPublishingOperationalStatus"
+
+
+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"
diff --git a/conftest.py b/conftest.py
index 0f69c106b..0ed5a2f27 100644
--- a/conftest.py
+++ b/conftest.py
@@ -9,6 +9,7 @@
from typing import Optional
from typing import Sequence
from typing import Type
+from unittest.mock import MagicMock
from unittest.mock import patch
import boto3
@@ -57,6 +58,7 @@
from importer.models import ImportBatchStatus
from importer.nursery import get_nursery
from importer.taric import process_taric_xml_stream
+from publishing.models import PackagedWorkBasket
from workbaskets.models import WorkBasket
from workbaskets.models import get_partition_scheme
from workbaskets.validators import WorkflowStatus
@@ -1547,3 +1549,137 @@ def empty_goods_import_batch():
status=ImportBatchStatus.SUCCEEDED,
workbasket_id=archived_workbasket.id,
)
+
+
+@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):
+ """
+ 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
diff --git a/importer/tests/management/commands/test_send_goods_report_notification.py b/importer/tests/management/commands/test_send_goods_report_notification.py
new file mode 100644
index 000000000..e6364f197
--- /dev/null
+++ b/importer/tests/management/commands/test_send_goods_report_notification.py
@@ -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.tasks.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()
diff --git a/importer/tests/test_views.py b/importer/tests/test_views.py
index 31fdcd9ba..1ecb4f301 100644
--- a/importer/tests/test_views.py
+++ b/importer/tests/test_views.py
@@ -1,4 +1,5 @@
from os import path
+from unittest.mock import MagicMock
from unittest.mock import patch
import pytest
@@ -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.tasks.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},
+ )
diff --git a/notifications/tasks.py b/notifications/tasks.py
index 425bfdfa5..88f622d37 100644
--- a/notifications/tasks.py
+++ b/notifications/tasks.py
@@ -13,6 +13,7 @@ def send_emails_task(notification_pk: int):
creating a log to record which users received which email template."""
from notifications.models import Notification
+ print("in here")
notification = Notification.objects.get(pk=notification_pk)
sub_notification = notification.return_subclass_instance()
sub_notification.send_emails()
diff --git a/notifications/tests/conftest.py b/notifications/tests/conftest.py
new file mode 100644
index 000000000..86fa305e6
--- /dev/null
+++ b/notifications/tests/conftest.py
@@ -0,0 +1,81 @@
+from unittest.mock import patch
+
+import pytest
+
+from common.tests import factories
+from importer.models import ImportBatchStatus
+
+
+@pytest.fixture()
+def goods_report_notification():
+ factories.NotifiedUserFactory(
+ email="goods_report@email.co.uk", # /PS-IGNORE
+ enrol_packaging=False,
+ enrol_goods_report=True,
+ )
+ factories.NotifiedUserFactory(
+ email="no_goods_report@email.co.uk", # /PS-IGNORE
+ )
+ import_batch = factories.ImportBatchFactory.create(
+ status=ImportBatchStatus.SUCCEEDED,
+ goods_import=True,
+ taric_file="goods.xml",
+ )
+
+ return factories.GoodsSuccessfulImportNotificationFactory(
+ notified_object_pk=import_batch.id,
+ )
+
+
+@pytest.fixture()
+def ready_for_packaging_notification(published_envelope_factory):
+ factories.NotifiedUserFactory(
+ email="packaging@email.co.uk", # /PS-IGNORE
+ )
+ factories.NotifiedUserFactory(
+ email="no_packaging@email.co.uk", # /PS-IGNORE
+ enrol_packaging=False,
+ )
+ packaged_wb = published_envelope_factory()
+ return factories.EnvelopeReadyForProcessingNotificationFactory(
+ notified_object_pk=packaged_wb.id,
+ )
+
+
+@pytest.fixture()
+def successful_publishing_notification(crown_dependencies_envelope_factory):
+ factories.NotifiedUserFactory(
+ email="publishing@email.co.uk", # /PS-IGNORE
+ enrol_packaging=False,
+ enrol_api_publishing=True,
+ )
+ factories.NotifiedUserFactory(
+ email="no_publishing@email.co.uk", # /PS-IGNORE
+ )
+ cde = crown_dependencies_envelope_factory()
+ return factories.CrownDependenciesEnvelopeSuccessNotificationFactory(
+ notified_object_pk=cde.id,
+ )
+
+
+@pytest.fixture()
+def mock_notify_send_emails():
+ with patch(
+ "notifications.notify.send_emails",
+ ) as mocked_send_emails:
+ yield mocked_send_emails
+
+
+@pytest.fixture()
+def mock_prepare_link():
+ return_value = {
+ "file": "VGVzdA==",
+ "is_csv": False,
+ "confirm_email_before_download": True,
+ "retention_period": None,
+ }
+ with patch(
+ "notifications.notify.prepare_link_to_file",
+ return_value=return_value,
+ ) as mocked_prepare_link_to_file:
+ yield mocked_prepare_link_to_file
diff --git a/notifications/tests/test_models.py b/notifications/tests/test_models.py
new file mode 100644
index 000000000..a318c5f44
--- /dev/null
+++ b/notifications/tests/test_models.py
@@ -0,0 +1,76 @@
+import pytest
+
+from importer.models import ImportBatch
+from publishing.models import CrownDependenciesEnvelope
+from publishing.models import PackagedWorkBasket
+
+pytestmark = pytest.mark.django_db
+
+
+def test_create_goods_report_notification(goods_report_notification):
+ """Test that the creating a notification correctly assigns users."""
+
+ expected_present_email = f"goods_report@email.co.uk" # /PS-IGNORE
+ expected_not_present_email = f"no_goods_report@email.co.uk" # /PS-IGNORE
+
+ users = goods_report_notification.notified_users()
+
+ for user in users:
+ assert user.email == expected_present_email
+ assert user.email != expected_not_present_email
+
+ assert isinstance(goods_report_notification.notified_object(), ImportBatch)
+ # TODO check goods_report_notification.get_personalisation()
+
+
+def test_create_packaging_notification(ready_for_packaging_notification):
+ """Test that the creating a notification correctly assigns users."""
+
+ expected_present_email = f"packaging@email.co.uk" # /PS-IGNORE
+ expected_not_present_email = f"no_packaging@email.co.uk" # /PS-IGNORE
+
+ users = ready_for_packaging_notification.notified_users()
+
+ for user in users:
+ assert user.email == expected_present_email
+ assert user.email != expected_not_present_email
+
+ assert isinstance(
+ ready_for_packaging_notification.notified_object(),
+ PackagedWorkBasket,
+ )
+
+ content = ready_for_packaging_notification.get_personalisation()
+ assert content == {
+ "envelope_id": "230001",
+ "description": "",
+ "download_url": "http://localhost/publishing/envelope-queue/",
+ "theme": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+ "eif": "Immediately",
+ "embargo": "None",
+ "jira_url": "www.fakejiraticket.com",
+ }
+
+
+def test_create_successful_publishing_notification(successful_publishing_notification):
+ """Test that the creating a notification correctly assigns users."""
+
+ expected_present_email = f"publishing@email.co.uk" # /PS-IGNORE
+ expected_not_present_email = f"no_publishing@email.co.uk" # /PS-IGNORE
+
+ users = successful_publishing_notification.notified_users()
+
+ for user in users:
+ assert user.email == expected_present_email
+ assert user.email != expected_not_present_email
+
+ assert isinstance(
+ successful_publishing_notification.notified_object(),
+ CrownDependenciesEnvelope,
+ )
+
+ content = successful_publishing_notification.get_personalisation()
+ assert content == {"envelope_id": "230001"}
+
+
+# TODO add test send_emails
diff --git a/notifications/tests/test_tasks.py b/notifications/tests/test_tasks.py
index b824cf6fc..53902b90b 100644
--- a/notifications/tests/test_tasks.py
+++ b/notifications/tests/test_tasks.py
@@ -1,93 +1,85 @@
-from unittest.mock import patch
-from uuid import uuid4
-
import pytest
-from common.tests import factories
from notifications import models
from notifications import tasks
pytestmark = pytest.mark.django_db
-@pytest.mark.skip(reason="TODO mock Notify client correctly")
-@patch("notifications.tasks.NotificationsAPIClient.send_email_notification")
-def test_send_emails_cds(send_email_notification, settings):
- """Tests that email notifications are only sent to users subscribed to
- packaging emails and that a log is created with this user's email and
- template id."""
- enrolled_user = factories.NotifiedUserFactory.create()
- unenrolled_user = factories.NotifiedUserFactory.create(enrol_packaging=False)
- template_id = uuid4()
-
- personalisation = {
- "envelope_id": "220001",
- "description": "description",
- "download_url": "https://example.com",
- "theme": "theme",
- "eif": None,
- "embargo": "None",
- "jira_url": "https://example.com",
- }
-
- tasks.send_emails.apply(
+def test_send_emails_goods_report_notification(
+ goods_report_notification,
+ mock_notify_send_emails,
+):
+ """Tests that email notifications are only sent to users subscribed to email
+ type and that a log is created with this user's email."""
+ expected_present_email = "goods_report@email.co.uk" # /PS-IGNORE
+ expected_unenrolled_email = "no_goods_report@email.co.uk" # /PS-IGNORE
+
+ tasks.send_emails_task.apply(
kwargs={
- "template_id": template_id,
- "personalisation": personalisation,
- "email_type": "packaging",
+ "notification_pk": goods_report_notification.id,
},
)
- send_email_notification.assert_called_once_with(
- email_address=enrolled_user.email,
- template_id=template_id,
- personalisation=personalisation,
- )
+ recipients = f"{expected_present_email} \n"
- recipients = f"{enrolled_user.email} \n"
log = models.NotificationLog.objects.get(
- template_id=template_id,
+ notification=goods_report_notification,
recipients=recipients,
+ success=True,
)
- assert unenrolled_user.email not in log.recipients
+ assert expected_unenrolled_email not in log.recipients
-@pytest.mark.skip(reason="TODO mock Notify client correctly")
-@patch("notifications.tasks.NotificationsAPIClient.send_email_notification")
-def test_send_emails_api(send_email_notification, settings):
- """Tests that email notifications are only sent to users subscribed to
- packaging emails and that a log is created with this user's email and
- template id."""
- enrolled_user = factories.NotifiedUserFactory.create(
- enrol_packaging=False,
- enrol_api_publishing=True,
- )
- unenrolled_user = factories.NotifiedUserFactory.create(enrol_packaging=False)
- template_id = uuid4()
+def test_send_emails_packaging_notification(
+ ready_for_packaging_notification,
+ mock_notify_send_emails,
+):
+ """Tests that email notifications are only sent to users subscribed to email
+ type and that a log is created with this user's email."""
- personalisation = {
- "envelope_id": "220001",
- }
+ expected_present_email = "packaging@email.co.uk" # /PS-IGNORE
+ expected_unenrolled_email = "no_packaging@email.co.uk" # /PS-IGNORE
- tasks.send_emails.apply(
+ tasks.send_emails_task.apply(
kwargs={
- "template_id": template_id,
- "personalisation": personalisation,
- "email_type": "publishing",
+ "notification_pk": ready_for_packaging_notification.id,
},
)
+ recipients = f"{expected_present_email} \n"
- send_email_notification.assert_called_once_with(
- email_address=enrolled_user.email,
- template_id=template_id,
- personalisation=personalisation,
+ log = models.NotificationLog.objects.get(
+ notification=ready_for_packaging_notification,
+ recipients=recipients,
+ success=True,
)
- recipients = f"{enrolled_user.email} \n"
+ assert expected_unenrolled_email not in log.recipients
+
+
+def test_send_emails_publishing_notification(
+ successful_publishing_notification,
+ mock_notify_send_emails,
+):
+ """Tests that email notifications are only sent to users subscribed to email
+ type and that a log is created with this user's email."""
+
+ expected_present_email = "publishing@email.co.uk" # /PS-IGNORE
+ expected_unenrolled_email = "no_publishing@email.co.uk" # /PS-IGNORE
+
+ tasks.send_emails_task.apply(
+ kwargs={
+ "notification_pk": successful_publishing_notification.id,
+ },
+ )
+
+ recipients = f"{expected_present_email} \n"
+
log = models.NotificationLog.objects.get(
- template_id=template_id,
+ notification=successful_publishing_notification,
recipients=recipients,
+ success=True,
)
- assert unenrolled_user.email not in log.recipients
+ assert expected_unenrolled_email not in log.recipients
diff --git a/publishing/tests/conftest.py b/publishing/tests/conftest.py
index 6ee53dbc8..8b49dee1f 100644
--- a/publishing/tests/conftest.py
+++ b/publishing/tests/conftest.py
@@ -13,7 +13,6 @@
import pytest
from common.tests import factories
-from publishing.models import PackagedWorkBasket
from publishing.models import QueueState
from publishing.models.state import CrownDependenciesPublishingState
@@ -50,15 +49,6 @@ def unpause_publishing():
)
-@pytest.fixture()
-def mocked_publishing_models_send_emails_delay():
- with patch(
- "notifications.tasks.send_emails.delay",
- return_value=MagicMock(id=factory.Faker("uuid4")),
- ) as mocked_delay:
- yield mocked_delay
-
-
@pytest.fixture(scope="module", autouse=True)
def mocked_create_xml_envelope_file_apply_sync():
"""Mock the Celery delay() function on
@@ -70,119 +60,3 @@ def mocked_create_xml_envelope_file_apply_sync():
return_value=MagicMock(id=factory.Faker("uuid4")),
) as mocked_apply_sync:
yield mocked_apply_sync
-
-
-@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):
- """
- 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
diff --git a/publishing/tests/test_envelope_queue_views.py b/publishing/tests/test_envelope_queue_views.py
index 35b10f53c..c88ab3c03 100644
--- a/publishing/tests/test_envelope_queue_views.py
+++ b/publishing/tests/test_envelope_queue_views.py
@@ -146,7 +146,7 @@ def test_start_processing(valid_user_client, unpause_queue):
def test_accept_envelope(
packaged_workbasket_factory,
published_envelope_factory,
- mocked_publishing_models_send_emails_delay,
+ mocked_send_emails_apply_async,
valid_user_client,
):
packaged_work_basket = packaged_workbasket_factory()
@@ -183,7 +183,7 @@ def test_accept_envelope(
def test_reject_envelope(
packaged_workbasket_factory,
published_envelope_factory,
- mocked_publishing_models_send_emails_delay,
+ mocked_send_emails_apply_async,
superuser_client,
):
packaged_work_basket_1 = packaged_workbasket_factory()
diff --git a/publishing/tests/test_model_crown_dependencies_envelope.py b/publishing/tests/test_model_crown_dependencies_envelope.py
index e25f92543..d9ed71d34 100644
--- a/publishing/tests/test_model_crown_dependencies_envelope.py
+++ b/publishing/tests/test_model_crown_dependencies_envelope.py
@@ -1,6 +1,6 @@
import pytest
-from django.conf import settings
+from notifications.models import Notification
from publishing.models import ApiPublishingState
from publishing.models import CrownDependenciesEnvelope
@@ -32,7 +32,7 @@ def test_create_crown_dependencies_envelope(
def test_notify_processing_succeeded(
- mocked_publishing_models_send_emails_delay,
+ mocked_send_emails_apply_async,
packaged_workbasket_factory,
successful_envelope_factory,
crown_dependencies_envelope_factory,
@@ -45,18 +45,14 @@ def test_notify_processing_succeeded(
cd_envelope.notify_publishing_success()
- personalisation = {
- "envelope_id": pwb.envelope.envelope_id,
- }
- mocked_publishing_models_send_emails_delay.assert_called_with(
- template_id=settings.API_PUBLISH_SUCCESS_TEMPLATE_ID,
- personalisation=personalisation,
- email_type="publishing",
+ notification = Notification.objects.last()
+ mocked_send_emails_apply_async.assert_called_with(
+ notification_id=notification.id,
)
def test_notify_processing_failed(
- mocked_publishing_models_send_emails_delay,
+ mocked_send_emails_apply_async,
packaged_workbasket_factory,
successful_envelope_factory,
crown_dependencies_envelope_factory,
@@ -69,11 +65,7 @@ def test_notify_processing_failed(
cd_envelope.notify_publishing_failed()
- personalisation = {
- "envelope_id": pwb.envelope.envelope_id,
- }
- mocked_publishing_models_send_emails_delay.assert_called_with(
- template_id=settings.API_PUBLISH_FAILED_TEMPLATE_ID,
- personalisation=personalisation,
- email_type="publishing",
+ notification = Notification.objects.last()
+ mocked_send_emails_apply_async.assert_called_with(
+ notification_id=notification.id,
)
diff --git a/publishing/tests/test_models.py b/publishing/tests/test_models.py
index 78c188feb..914023635 100644
--- a/publishing/tests/test_models.py
+++ b/publishing/tests/test_models.py
@@ -5,7 +5,6 @@
import factory
import freezegun
import pytest
-from django.conf import settings
from django_fsm import TransitionNotAllowed
from common.tests import factories
@@ -68,7 +67,7 @@ def test_create_from_invalid_status():
def test_notify_ready_for_processing(
packaged_workbasket_factory,
published_envelope_factory,
- mocked_publishing_models_send_emails_delay,
+ mocked_send_emails_apply_async,
settings,
):
packaged_wb = packaged_workbasket_factory()
@@ -83,15 +82,11 @@ def test_notify_ready_for_processing(
"embargo": str(packaged_wb.embargo),
"jira_url": packaged_wb.jira_url,
}
- mocked_publishing_models_send_emails_delay.assert_called_once_with(
- template_id=settings.READY_FOR_CDS_TEMPLATE_ID,
- personalisation=personalisation,
- email_type="packaging",
- )
+ mocked_send_emails_apply_async.assert_called_once()
def test_notify_processing_succeeded(
- mocked_publishing_models_send_emails_delay,
+ mocked_send_emails_apply_async,
packaged_workbasket_factory,
published_envelope_factory,
):
@@ -110,15 +105,11 @@ def test_notify_processing_succeeded(
"loading_report_message": f"Loading report(s): {loading_report.file_name}",
"comments": packaged_wb.loadingreports.first().comments,
}
- mocked_publishing_models_send_emails_delay.assert_called_once_with(
- template_id=settings.CDS_ACCEPTED_TEMPLATE_ID,
- personalisation=personalisation,
- email_type="packaging",
- )
+ mocked_send_emails_apply_async.assert_called_once()
def test_notify_processing_failed(
- mocked_publishing_models_send_emails_delay,
+ mocked_send_emails_apply_async,
packaged_workbasket_factory,
published_envelope_factory,
):
@@ -141,18 +132,14 @@ def test_notify_processing_failed(
"comments": packaged_wb.loadingreports.first().comments,
}
- mocked_publishing_models_send_emails_delay.assert_called_once_with(
- template_id=settings.CDS_REJECTED_TEMPLATE_ID,
- personalisation=personalisation,
- email_type="packaging",
- )
+ mocked_send_emails_apply_async.assert_called_once()
def test_success_processing_transition(
packaged_workbasket_factory,
published_envelope_factory,
envelope_storage,
- mocked_publishing_models_send_emails_delay,
+ mocked_send_emails_apply_async,
settings,
):
settings.ENABLE_PACKAGING_NOTIFICATIONS = False
diff --git a/workbaskets/tests/test_views.py b/workbaskets/tests/test_views.py
index 6a8559f80..ee6fea126 100644
--- a/workbaskets/tests/test_views.py
+++ b/workbaskets/tests/test_views.py
@@ -16,9 +16,12 @@
from common.tests.factories import GoodsNomenclatureFactory
from common.tests.factories import MeasureFactory
from exporter.tasks import upload_workbaskets
+from importer.models import ImportBatch
+from importer.models import ImportBatchStatus
from measures.models import Measure
from workbaskets import models
from workbaskets.forms import SelectableObjectsForm
+from workbaskets.tasks import check_workbasket_sync
from workbaskets.validators import WorkflowStatus
from workbaskets.views import ui
@@ -663,6 +666,109 @@ def test_submit_for_packaging(valid_user_client, session_workbasket):
assert response.url[: len(response_url)] == response_url
+@pytest.fixture
+def successful_business_rules_setup(session_workbasket, valid_user_client):
+ """Sets up data and runs business rules."""
+ with session_workbasket.new_transaction() as transaction:
+ good = GoodsNomenclatureFactory.create(transaction=transaction)
+ measure = MeasureFactory.create(transaction=transaction)
+ geo_area = GeographicalAreaFactory.create(transaction=transaction)
+ objects = [good, measure, geo_area]
+ for obj in objects:
+ TrackedModelCheckFactory.create(
+ transaction_check__transaction=transaction,
+ model=obj,
+ successful=True,
+ )
+ session = valid_user_client.session
+ session["workbasket"] = {
+ "id": session_workbasket.pk,
+ "status": session_workbasket.status,
+ "title": session_workbasket.title,
+ "error_count": session_workbasket.tracked_model_check_errors.count(),
+ }
+ session.save()
+
+ # run rule checks so unchecked_or_errored_transactions is set
+ check_workbasket_sync(session_workbasket)
+
+
+def import_batch_with_notification():
+ import_batch = factories.ImportBatchFactory.create(
+ status=ImportBatchStatus.SUCCEEDED,
+ goods_import=True,
+ taric_file="goods.xml",
+ )
+
+ return factories.GoodsSuccessfulImportNotificationFactory(
+ notified_object_pk=import_batch.id,
+ )
+
+
+@pytest.mark.parametrize(
+ "import_batch_factory,disabled",
+ [
+ (
+ lambda: factories.ImportBatchFactory.create(
+ status=ImportBatchStatus.SUCCEEDED,
+ goods_import=True,
+ ),
+ True,
+ ),
+ (
+ import_batch_with_notification,
+ False,
+ ),
+ (
+ lambda: factories.ImportBatchFactory.create(
+ status=ImportBatchStatus.SUCCEEDED,
+ ),
+ False,
+ ),
+ (
+ lambda: None,
+ False,
+ ),
+ ],
+ ids=(
+ "goods_import_no_notification",
+ "goods_import_with_notification",
+ "master_import",
+ "no_import",
+ ),
+)
+def test_submit_for_packaging_disabled(
+ successful_business_rules_setup,
+ valid_user_client,
+ session_workbasket,
+ import_batch_factory,
+ disabled,
+):
+ """Test that the submit-for-packaging button is disabled when a notification
+ has not been sent for a commodity code import (goods)"""
+
+ import_batch = import_batch_factory()
+
+ if import_batch:
+ import_batch.workbasket_id = session_workbasket.id
+ if isinstance(import_batch, ImportBatch):
+ import_batch.save()
+
+ response = valid_user_client.get(
+ reverse("workbaskets:current-workbasket"),
+ )
+
+ assert response.status_code == 200
+ soup = BeautifulSoup(str(response.content), "html.parser")
+
+ packaging_button = soup.find("a", href="/publishing/create/")
+
+ if disabled:
+ assert packaging_button.has_attr("disabled")
+ else:
+ assert not packaging_button.has_attr("disabled")
+
+
def test_terminate_rule_check(valid_user_client, session_workbasket):
session_workbasket.rule_check_task_id = 123
@@ -1193,3 +1299,79 @@ def test_application_access_after_workbasket_delete(
# workbasket deletion.
assert response.status_code == 200
assert not page.select("header nav a.workbasket-link")
+
+
+def make_goods_import_batch(importer_storage, **kwargs):
+ return factories.ImportBatchFactory.create(
+ status=ImportBatchStatus.SUCCEEDED,
+ goods_import=True,
+ taric_file="goods.xml",
+ **kwargs,
+ )
+
+
+@pytest.mark.parametrize(
+ "import_batch_factory,visable",
+ [
+ (
+ lambda: factories.ImportBatchFactory.create(
+ status=ImportBatchStatus.SUCCEEDED,
+ goods_import=True,
+ taric_file="goods.xml",
+ ),
+ True,
+ ),
+ (
+ import_batch_with_notification,
+ False,
+ ),
+ (
+ lambda: factories.ImportBatchFactory.create(
+ status=ImportBatchStatus.SUCCEEDED,
+ ),
+ False,
+ ),
+ (
+ lambda: None,
+ False,
+ ),
+ ],
+ ids=(
+ "goods_import_no_notification",
+ "goods_import_with_notification",
+ "master_import",
+ "no_import",
+ ),
+)
+def test_review_goods_notification_button(
+ successful_business_rules_setup,
+ valid_user_client,
+ session_workbasket,
+ import_batch_factory,
+ visable,
+):
+ """Test that the submit-for-packaging button is disabled when a notification
+ has not been sent for a commodity code import (goods)"""
+
+ import_batch = import_batch_factory()
+
+ if import_batch:
+ import_batch.workbasket_id = session_workbasket.id
+ if isinstance(import_batch, ImportBatch):
+ import_batch.save()
+
+ response = valid_user_client.get(
+ reverse("workbaskets:workbasket-ui-review-goods"),
+ )
+
+ assert response.status_code == 200
+ soup = BeautifulSoup(str(response.content), "html.parser")
+
+ # notify_button = soup.find("a", href=f"/notify-goods-report/{import_batch.id}/")
+ notify_button = soup.select(".govuk-body")
+
+ print(notify_button)
+ if visable:
+ assert notify_button
+ else:
+ assert not notify_button
From 6b059fc941e82d2e6fccc018b50a24c8c1e3cfbf Mon Sep 17 00:00:00 2001
From: Anthoni Gleeson
Date: Thu, 14 Sep 2023 15:38:00 +0100
Subject: [PATCH 10/15] Working tests!
---
.../test_send_goods_report_notification.py | 2 +-
importer/tests/test_views.py | 2 +-
notifications/models.py | 2 +-
notifications/notify.py | 1 -
notifications/tasks.py | 3 +-
notifications/tests/conftest.py | 24 ++++--
notifications/tests/test_tasks.py | 82 +++++++++++++------
.../models/crown_dependencies_envelope.py | 1 +
.../test_model_crown_dependencies_envelope.py | 17 ++--
publishing/tests/test_models.py | 7 ++
workbaskets/tests/test_views.py | 22 ++++-
11 files changed, 116 insertions(+), 47 deletions(-)
diff --git a/importer/tests/management/commands/test_send_goods_report_notification.py b/importer/tests/management/commands/test_send_goods_report_notification.py
index e6364f197..a64425197 100644
--- a/importer/tests/management/commands/test_send_goods_report_notification.py
+++ b/importer/tests/management/commands/test_send_goods_report_notification.py
@@ -41,7 +41,7 @@ def test_send_goods_report_notification(
notification."""
with patch(
- "notifications.tasks.send_emails_task",
+ "notifications.models.send_emails_task",
return_value=MagicMock(),
) as mocked_email_task:
call_command(
diff --git a/importer/tests/test_views.py b/importer/tests/test_views.py
index 1ecb4f301..87bf0e0ba 100644
--- a/importer/tests/test_views.py
+++ b/importer/tests/test_views.py
@@ -293,7 +293,7 @@ def test_notify_channel_islands_redirects(
conformation page on successful notification trigger."""
with patch(
- "notifications.tasks.send_emails_task",
+ "notifications.models.send_emails_task",
return_value=MagicMock(),
) as mocked_email_task:
response = valid_user_client.get(
diff --git a/notifications/models.py b/notifications/models.py
index 6d142661a..c6c085470 100644
--- a/notifications/models.py
+++ b/notifications/models.py
@@ -158,7 +158,7 @@ def schedule_send_emails(self, countdown=1):
background task."""
send_emails_task.apply_async(args=[self.pk], countdown=countdown)
- def send_emails(self):
+ def send_notification_emails(self):
"""Send the notification emails to users via GOV.UK Notify."""
notified_users = self.notified_users()
diff --git a/notifications/notify.py b/notifications/notify.py
index 5c0f9f95e..643181268 100644
--- a/notifications/notify.py
+++ b/notifications/notify.py
@@ -74,7 +74,6 @@ def send_emails(template_id, personalisation: dict, email_addresses: List[str])
response_ids += f"{response['id']} \n"
recipients += f"{email} \n"
except Exception as e:
- # TODO extract error info?
failed_recipients += f"{email} \n"
logger.error(
f"Failed to send email notification to {email}, with status code {e.status_code}.",
diff --git a/notifications/tasks.py b/notifications/tasks.py
index 88f622d37..a771c71b3 100644
--- a/notifications/tasks.py
+++ b/notifications/tasks.py
@@ -13,7 +13,6 @@ def send_emails_task(notification_pk: int):
creating a log to record which users received which email template."""
from notifications.models import Notification
- print("in here")
notification = Notification.objects.get(pk=notification_pk)
sub_notification = notification.return_subclass_instance()
- sub_notification.send_emails()
+ sub_notification.send_notification_emails()
diff --git a/notifications/tests/conftest.py b/notifications/tests/conftest.py
index 86fa305e6..2cc434884 100644
--- a/notifications/tests/conftest.py
+++ b/notifications/tests/conftest.py
@@ -1,5 +1,6 @@
from unittest.mock import patch
+import factory
import pytest
from common.tests import factories
@@ -59,11 +60,22 @@ def successful_publishing_notification(crown_dependencies_envelope_factory):
@pytest.fixture()
-def mock_notify_send_emails():
- with patch(
- "notifications.notify.send_emails",
- ) as mocked_send_emails:
- yield mocked_send_emails
+def notify_send_emails_return_value():
+ """
+ Factory fixture to create a mock for sending an email. This allows you to
+ override the response in the test.
+
+ params:
+ response_ids: [list of uuids],
+ recipients: [list of strings],
+ failed_recipients: [list of strings]
+ """
+
+ return {
+ "response_ids": " \n".join([factory.Faker("uuid")]),
+ "recipients": " \n".join([str(factory.Faker("email"))]),
+ "failed_recipients": "",
+ }
@pytest.fixture()
@@ -75,7 +87,7 @@ def mock_prepare_link():
"retention_period": None,
}
with patch(
- "notifications.notify.prepare_link_to_file",
+ "notifications.models.GoodsSuccessfulImportNotification.prepare_link_to_file",
return_value=return_value,
) as mocked_prepare_link_to_file:
yield mocked_prepare_link_to_file
diff --git a/notifications/tests/test_tasks.py b/notifications/tests/test_tasks.py
index 53902b90b..a0e82bd00 100644
--- a/notifications/tests/test_tasks.py
+++ b/notifications/tests/test_tasks.py
@@ -1,3 +1,6 @@
+from unittest.mock import patch
+
+import factory
import pytest
from notifications import models
@@ -8,24 +11,41 @@
def test_send_emails_goods_report_notification(
goods_report_notification,
- mock_notify_send_emails,
):
"""Tests that email notifications are only sent to users subscribed to email
type and that a log is created with this user's email."""
expected_present_email = "goods_report@email.co.uk" # /PS-IGNORE
expected_unenrolled_email = "no_goods_report@email.co.uk" # /PS-IGNORE
- tasks.send_emails_task.apply(
- kwargs={
- "notification_pk": goods_report_notification.id,
- },
- )
-
- recipients = f"{expected_present_email} \n"
+ return_value = {
+ "file": "VGVzdA==",
+ "is_csv": False,
+ "confirm_email_before_download": True,
+ "retention_period": None,
+ }
+ with patch(
+ "notifications.models.prepare_link_to_file",
+ return_value=return_value,
+ ) as mocked_prepare_link_to_file:
+ with patch(
+ "notifications.models.send_emails",
+ return_value={
+ "response_ids": " \n".join([str(factory.Faker("uuid"))]),
+ "recipients": " \n".join([expected_present_email]),
+ "failed_recipients": "",
+ },
+ ) as mocked_send_emails:
+ tasks.send_emails_task.apply(
+ kwargs={
+ "notification_pk": goods_report_notification.id,
+ },
+ )
+ mocked_send_emails.assert_called_once()
+ mocked_prepare_link_to_file.assert_called_once()
log = models.NotificationLog.objects.get(
notification=goods_report_notification,
- recipients=recipients,
+ recipients=expected_present_email,
success=True,
)
@@ -34,7 +54,6 @@ def test_send_emails_goods_report_notification(
def test_send_emails_packaging_notification(
ready_for_packaging_notification,
- mock_notify_send_emails,
):
"""Tests that email notifications are only sent to users subscribed to email
type and that a log is created with this user's email."""
@@ -42,16 +61,24 @@ def test_send_emails_packaging_notification(
expected_present_email = "packaging@email.co.uk" # /PS-IGNORE
expected_unenrolled_email = "no_packaging@email.co.uk" # /PS-IGNORE
- tasks.send_emails_task.apply(
- kwargs={
- "notification_pk": ready_for_packaging_notification.id,
+ with patch(
+ "notifications.models.send_emails",
+ return_value={
+ "response_ids": " \n".join([str(factory.Faker("uuid"))]),
+ "recipients": " \n".join([expected_present_email]),
+ "failed_recipients": "",
},
- )
- recipients = f"{expected_present_email} \n"
+ ) as mocked_send_emails:
+ tasks.send_emails_task.apply(
+ kwargs={
+ "notification_pk": ready_for_packaging_notification.id,
+ },
+ )
+ mocked_send_emails.assert_called_once()
log = models.NotificationLog.objects.get(
notification=ready_for_packaging_notification,
- recipients=recipients,
+ recipients=expected_present_email,
success=True,
)
@@ -60,7 +87,7 @@ def test_send_emails_packaging_notification(
def test_send_emails_publishing_notification(
successful_publishing_notification,
- mock_notify_send_emails,
+ # mock_notify_send_emails,
):
"""Tests that email notifications are only sent to users subscribed to email
type and that a log is created with this user's email."""
@@ -68,17 +95,24 @@ def test_send_emails_publishing_notification(
expected_present_email = "publishing@email.co.uk" # /PS-IGNORE
expected_unenrolled_email = "no_publishing@email.co.uk" # /PS-IGNORE
- tasks.send_emails_task.apply(
- kwargs={
- "notification_pk": successful_publishing_notification.id,
+ with patch(
+ "notifications.models.send_emails",
+ return_value={
+ "response_ids": " \n".join([str(factory.Faker("uuid"))]),
+ "recipients": " \n".join([expected_present_email]),
+ "failed_recipients": "",
},
- )
-
- recipients = f"{expected_present_email} \n"
+ ) as mocked_send_emails:
+ tasks.send_emails_task.apply(
+ kwargs={
+ "notification_pk": successful_publishing_notification.id,
+ },
+ )
+ mocked_send_emails.assert_called_once()
log = models.NotificationLog.objects.get(
notification=successful_publishing_notification,
- recipients=recipients,
+ recipients=expected_present_email,
success=True,
)
diff --git a/publishing/models/crown_dependencies_envelope.py b/publishing/models/crown_dependencies_envelope.py
index aab4640c7..487e58dee 100644
--- a/publishing/models/crown_dependencies_envelope.py
+++ b/publishing/models/crown_dependencies_envelope.py
@@ -158,6 +158,7 @@ def notify_publishing_success(self):
notification = CrownDependenciesEnvelopeSuccessNotification(
notified_object_pk=self.pk,
)
+ print(notification)
notification.save()
notification.schedule_send_emails()
diff --git a/publishing/tests/test_model_crown_dependencies_envelope.py b/publishing/tests/test_model_crown_dependencies_envelope.py
index d9ed71d34..b2ba46820 100644
--- a/publishing/tests/test_model_crown_dependencies_envelope.py
+++ b/publishing/tests/test_model_crown_dependencies_envelope.py
@@ -36,7 +36,10 @@ def test_notify_processing_succeeded(
packaged_workbasket_factory,
successful_envelope_factory,
crown_dependencies_envelope_factory,
+ settings,
):
+ settings.ENABLE_PACKAGING_NOTIFICATIONS = True
+
pwb = packaged_workbasket_factory()
crown_dependencies_envelope_factory(packaged_workbasket=pwb)
@@ -45,10 +48,8 @@ def test_notify_processing_succeeded(
cd_envelope.notify_publishing_success()
- notification = Notification.objects.last()
- mocked_send_emails_apply_async.assert_called_with(
- notification_id=notification.id,
- )
+ Notification.objects.last()
+ mocked_send_emails_apply_async.assert_called()
def test_notify_processing_failed(
@@ -56,7 +57,9 @@ def test_notify_processing_failed(
packaged_workbasket_factory,
successful_envelope_factory,
crown_dependencies_envelope_factory,
+ settings,
):
+ settings.ENABLE_PACKAGING_NOTIFICATIONS = True
pwb = packaged_workbasket_factory()
crown_dependencies_envelope_factory(packaged_workbasket=pwb)
@@ -65,7 +68,5 @@ def test_notify_processing_failed(
cd_envelope.notify_publishing_failed()
- notification = Notification.objects.last()
- mocked_send_emails_apply_async.assert_called_with(
- notification_id=notification.id,
- )
+ Notification.objects.last()
+ mocked_send_emails_apply_async.assert_called()
diff --git a/publishing/tests/test_models.py b/publishing/tests/test_models.py
index 914023635..287339f8a 100644
--- a/publishing/tests/test_models.py
+++ b/publishing/tests/test_models.py
@@ -70,6 +70,8 @@ def test_notify_ready_for_processing(
mocked_send_emails_apply_async,
settings,
):
+ settings.ENABLE_PACKAGING_NOTIFICATIONS = True
+
packaged_wb = packaged_workbasket_factory()
envelope = published_envelope_factory(packaged_workbasket=packaged_wb)
packaged_wb.notify_ready_for_processing()
@@ -89,7 +91,10 @@ def test_notify_processing_succeeded(
mocked_send_emails_apply_async,
packaged_workbasket_factory,
published_envelope_factory,
+ settings,
):
+ settings.ENABLE_PACKAGING_NOTIFICATIONS = True
+
packaged_wb = packaged_workbasket_factory()
loading_report = factories.LoadingReportFactory.create(
packaged_workbasket=packaged_wb,
@@ -112,7 +117,9 @@ def test_notify_processing_failed(
mocked_send_emails_apply_async,
packaged_workbasket_factory,
published_envelope_factory,
+ settings,
):
+ settings.ENABLE_PACKAGING_NOTIFICATIONS = True
packaged_wb = packaged_workbasket_factory()
loading_report1 = factories.LoadingReportFactory.create(
packaged_workbasket=packaged_wb,
diff --git a/workbaskets/tests/test_views.py b/workbaskets/tests/test_views.py
index ee6fea126..64f192507 100644
--- a/workbaskets/tests/test_views.py
+++ b/workbaskets/tests/test_views.py
@@ -1,5 +1,7 @@
+import os
import re
from unittest.mock import MagicMock
+from unittest.mock import mock_open
from unittest.mock import patch
import pytest
@@ -1310,6 +1312,7 @@ def make_goods_import_batch(importer_storage, **kwargs):
)
+@pytest.skip("Unable to mock s3 file read from within ET.parse currently")
@pytest.mark.parametrize(
"import_batch_factory,visable",
[
@@ -1360,9 +1363,22 @@ def test_review_goods_notification_button(
if isinstance(import_batch, ImportBatch):
import_batch.save()
- response = valid_user_client.get(
- reverse("workbaskets:workbasket-ui-review-goods"),
- )
+ def mock_xlsx_open(filename, mode):
+ if os.path.basename(filename) == "goods.xlsx":
+ return mock_open().return_value
+ return open(filename, mode)
+
+ with patch(
+ "importer.goods_report.GoodsReport.xlsx_file",
+ return_value="",
+ ) as mocked_xlsx_file:
+ # with patch(
+ # ".open",
+ # mock_xlsx_open,
+ # ):
+ response = valid_user_client.get(
+ reverse("workbaskets:workbasket-ui-review-goods"),
+ )
assert response.status_code == 200
soup = BeautifulSoup(str(response.content), "html.parser")
From 387cd2ac3859426fcfedc21c59ebec8713073546 Mon Sep 17 00:00:00 2001
From: Anthoni Gleeson
Date: Thu, 14 Sep 2023 16:23:08 +0100
Subject: [PATCH 11/15] removing unused fixtures
---
notifications/tests/conftest.py | 37 ---------------------------------
1 file changed, 37 deletions(-)
diff --git a/notifications/tests/conftest.py b/notifications/tests/conftest.py
index 2cc434884..d771afd97 100644
--- a/notifications/tests/conftest.py
+++ b/notifications/tests/conftest.py
@@ -1,6 +1,3 @@
-from unittest.mock import patch
-
-import factory
import pytest
from common.tests import factories
@@ -57,37 +54,3 @@ def successful_publishing_notification(crown_dependencies_envelope_factory):
return factories.CrownDependenciesEnvelopeSuccessNotificationFactory(
notified_object_pk=cde.id,
)
-
-
-@pytest.fixture()
-def notify_send_emails_return_value():
- """
- Factory fixture to create a mock for sending an email. This allows you to
- override the response in the test.
-
- params:
- response_ids: [list of uuids],
- recipients: [list of strings],
- failed_recipients: [list of strings]
- """
-
- return {
- "response_ids": " \n".join([factory.Faker("uuid")]),
- "recipients": " \n".join([str(factory.Faker("email"))]),
- "failed_recipients": "",
- }
-
-
-@pytest.fixture()
-def mock_prepare_link():
- return_value = {
- "file": "VGVzdA==",
- "is_csv": False,
- "confirm_email_before_download": True,
- "retention_period": None,
- }
- with patch(
- "notifications.models.GoodsSuccessfulImportNotification.prepare_link_to_file",
- return_value=return_value,
- ) as mocked_prepare_link_to_file:
- yield mocked_prepare_link_to_file
From 7512f20115aa88b0fcceb0a1e435ec2410ba3b08 Mon Sep 17 00:00:00 2001
From: Anthoni Gleeson
Date: Fri, 15 Sep 2023 08:49:12 +0100
Subject: [PATCH 12/15] test fixes and admin filter
---
conftest.py | 5 ++-
notifications/admin.py | 4 +-
notifications/tests/test_models.py | 61 +++++++++++++++++++++++++++++-
workbaskets/tests/test_views.py | 8 ++--
4 files changed, 70 insertions(+), 8 deletions(-)
diff --git a/conftest.py b/conftest.py
index b30cbf8f8..170f98c0e 100644
--- a/conftest.py
+++ b/conftest.py
@@ -1901,7 +1901,10 @@ def factory_method(packaged_workbasket=None, **kwargs):
@pytest.fixture(scope="function")
-def successful_envelope_factory(published_envelope_factory):
+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.
diff --git a/notifications/admin.py b/notifications/admin.py
index 077f33549..e7f4e8a42 100644
--- a/notifications/admin.py
+++ b/notifications/admin.py
@@ -72,8 +72,6 @@ def changeform_view(self, request, object_id=None, form_url="", extra_context=No
class NotificationAdmin(admin.ModelAdmin):
"""Class providing read-only list and detail views for notification."""
- # TODO add filters on notification_type
-
ordering = ["pk"]
list_display = (
"pk",
@@ -82,6 +80,8 @@ class NotificationAdmin(admin.ModelAdmin):
"display_users",
)
+ list_filter = ("notification_type",)
+
readonly_fields = []
def display_users(self, obj):
diff --git a/notifications/tests/test_models.py b/notifications/tests/test_models.py
index a318c5f44..c3f26497e 100644
--- a/notifications/tests/test_models.py
+++ b/notifications/tests/test_models.py
@@ -1,6 +1,10 @@
+from unittest.mock import patch
+
+import factory
import pytest
from importer.models import ImportBatch
+from notifications.models import NotificationLog
from publishing.models import CrownDependenciesEnvelope
from publishing.models import PackagedWorkBasket
@@ -20,7 +24,23 @@ def test_create_goods_report_notification(goods_report_notification):
assert user.email != expected_not_present_email
assert isinstance(goods_report_notification.notified_object(), ImportBatch)
- # TODO check goods_report_notification.get_personalisation()
+
+ return_value = {
+ "file": "VGVzdA==",
+ "is_csv": False,
+ "confirm_email_before_download": True,
+ "retention_period": None,
+ }
+ with patch(
+ "notifications.models.prepare_link_to_file",
+ return_value=return_value,
+ ) as mocked_prepare_link_to_file:
+ personalisation = goods_report_notification.get_personalisation()
+
+ assert personalisation == {
+ "tgb_id": "0",
+ "link_to_file": return_value,
+ }
def test_create_packaging_notification(ready_for_packaging_notification):
@@ -74,3 +94,42 @@ def test_create_successful_publishing_notification(successful_publishing_notific
# TODO add test send_emails
+def test_send_notification_emails(ready_for_packaging_notification):
+ expected_present_email = f"packaging@email.co.uk" # /PS-IGNORE
+ expected_not_present_email = f"no_packaging@email.co.uk" # /PS-IGNORE
+ with patch(
+ "notifications.models.send_emails",
+ return_value={
+ "response_ids": " \n".join([str(factory.Faker("uuid"))]),
+ "recipients": " \n".join([expected_present_email]),
+ "failed_recipients": "",
+ },
+ ) as mocked_send_emails:
+ ready_for_packaging_notification.send_notification_emails()
+ mocked_send_emails.assert_called_once()
+
+ log_success = NotificationLog.objects.get(
+ notification=ready_for_packaging_notification,
+ recipients=expected_present_email,
+ success=True,
+ )
+
+ assert expected_present_email in log_success.recipients
+
+ with patch(
+ "notifications.models.send_emails",
+ return_value={
+ "response_ids": "",
+ "recipients": "",
+ "failed_recipients": " \n".join([expected_present_email]),
+ },
+ ) as mocked_send_emails:
+ ready_for_packaging_notification.send_notification_emails()
+ mocked_send_emails.assert_called_once()
+
+ log_fail = NotificationLog.objects.get(
+ notification=ready_for_packaging_notification,
+ success=False,
+ )
+
+ assert expected_present_email in log_fail.recipients
diff --git a/workbaskets/tests/test_views.py b/workbaskets/tests/test_views.py
index 12f09ea20..e7b974fde 100644
--- a/workbaskets/tests/test_views.py
+++ b/workbaskets/tests/test_views.py
@@ -669,9 +669,9 @@ def test_submit_for_packaging(valid_user_client, session_workbasket):
def successful_business_rules_setup(session_workbasket, valid_user_client):
"""Sets up data and runs business rules."""
with session_workbasket.new_transaction() as transaction:
- good = GoodsNomenclatureFactory.create(transaction=transaction)
- measure = MeasureFactory.create(transaction=transaction)
- geo_area = GeographicalAreaFactory.create(transaction=transaction)
+ good = factories.GoodsNomenclatureFactory.create(transaction=transaction)
+ measure = factories.MeasureFactory.create(transaction=transaction)
+ geo_area = factories.GeographicalAreaFactory.create(transaction=transaction)
objects = [good, measure, geo_area]
for obj in objects:
TrackedModelCheckFactory.create(
@@ -1404,7 +1404,7 @@ def make_goods_import_batch(importer_storage, **kwargs):
)
-@pytest.skip("Unable to mock s3 file read from within ET.parse currently")
+@pytest.mark.skip(reason="Unable to mock s3 file read from within ET.parse currently")
@pytest.mark.parametrize(
"import_batch_factory,visable",
[
From 13849397f05e13178394a88d78bdbb7e8b118463 Mon Sep 17 00:00:00 2001
From: Anthoni Gleeson
Date: Fri, 15 Sep 2023 09:30:32 +0100
Subject: [PATCH 13/15] test fix
---
notifications/tests/test_models.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/notifications/tests/test_models.py b/notifications/tests/test_models.py
index c3f26497e..145981eb2 100644
--- a/notifications/tests/test_models.py
+++ b/notifications/tests/test_models.py
@@ -23,7 +23,8 @@ def test_create_goods_report_notification(goods_report_notification):
assert user.email == expected_present_email
assert user.email != expected_not_present_email
- assert isinstance(goods_report_notification.notified_object(), ImportBatch)
+ import_batch = goods_report_notification.notified_object()
+ assert isinstance(import_batch, ImportBatch)
return_value = {
"file": "VGVzdA==",
@@ -38,7 +39,7 @@ def test_create_goods_report_notification(goods_report_notification):
personalisation = goods_report_notification.get_personalisation()
assert personalisation == {
- "tgb_id": "0",
+ "tgb_id": import_batch.name,
"link_to_file": return_value,
}
@@ -93,7 +94,6 @@ def test_create_successful_publishing_notification(successful_publishing_notific
assert content == {"envelope_id": "230001"}
-# TODO add test send_emails
def test_send_notification_emails(ready_for_packaging_notification):
expected_present_email = f"packaging@email.co.uk" # /PS-IGNORE
expected_not_present_email = f"no_packaging@email.co.uk" # /PS-IGNORE
From 341c5c5f6f46d0ee0bb33277bf9d40d4b62f7d61 Mon Sep 17 00:00:00 2001
From: Anthoni Gleeson
Date: Mon, 18 Sep 2023 14:37:56 +0100
Subject: [PATCH 14/15] Confirm should be true by default
---
notifications/models.py | 1 -
notifications/notify.py | 4 ++--
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/notifications/models.py b/notifications/models.py
index c6c085470..36cb20ca8 100644
--- a/notifications/models.py
+++ b/notifications/models.py
@@ -443,7 +443,6 @@ def get_personalisation(self) -> dict:
"tgb_id": import_batch.name,
"link_to_file": prepare_link_to_file(
import_batch.taric_file,
- confirm_email_before_download=True,
),
}
return personalisation
diff --git a/notifications/notify.py b/notifications/notify.py
index 643181268..8d19c8e54 100644
--- a/notifications/notify.py
+++ b/notifications/notify.py
@@ -18,7 +18,7 @@ def get_notifications_client():
def prepare_link_to_file(
file,
is_csv=False,
- confirm_email_before_download=None,
+ confirm_email_before_download=True,
retention_period=None,
):
"""
@@ -28,7 +28,7 @@ def prepare_link_to_file(
params:
file: file which to generate report from
is_csv: if the file being attached is a csv set to True, default False
- confirm_email_before_download: security feature where user opening files must be on Gov Notify email list
+ confirm_email_before_download: security feature where user opening files must be on Gov Notify email list, True by default
retention_period: how long the file link is valid for, default 6 months
"""
From 6cf46d96765ad06d0c793cbbaae3cba50c1cfb1a Mon Sep 17 00:00:00 2001
From: Anthoni Gleeson
Date: Tue, 19 Sep 2023 08:22:45 +0100
Subject: [PATCH 15/15] PR comments
---
notifications/models.py | 9 +--------
1 file changed, 1 insertion(+), 8 deletions(-)
diff --git a/notifications/models.py b/notifications/models.py
index 36cb20ca8..c9faad050 100644
--- a/notifications/models.py
+++ b/notifications/models.py
@@ -22,7 +22,7 @@ class NotifiedUser(models.Model):
enrol_api_publishing = models.BooleanField(default=False)
enrol_goods_report = models.BooleanField(default=False)
- def __str__(self) -> str:
+ def __str__(self):
return str(self.email)
@@ -49,9 +49,6 @@ class NotificationLog(TimestampedMixin):
success = models.BooleanField(default=True)
-# class NotificationManager(models.Manager):
-
-
class NotificationTypeChoices(models.TextChoices):
GOODS_REPORT = (
"goods_report",
@@ -90,10 +87,6 @@ class Notification(models.Model):
https://docs.djangoproject.com/en/dev/topics/db/models/#proxy-models
"""
- # def __init__(self, notificaiton_type: NotificationTypeChoices, notified_object_pk: int = None, ):
- # self.notified_object_pk = notified_object_pk
- # self.notificaiton_type = notificaiton_type
-
notified_object_pk = models.IntegerField(
default=None,
null=True,