#6383 Add support for bulk sending notification emails

Open Tom Usher tomusher
Coverage Reach
wagtail/admin/views/pages/edit.py wagtail/admin/views/pages/create.py wagtail/admin/views/pages/revisions.py wagtail/admin/views/pages/workflow.py wagtail/admin/views/pages/preview.py wagtail/admin/views/pages/search.py wagtail/admin/views/pages/history.py wagtail/admin/views/pages/moderation.py wagtail/admin/views/pages/move.py wagtail/admin/views/pages/copy.py wagtail/admin/views/pages/listing.py wagtail/admin/views/pages/lock.py wagtail/admin/views/pages/unpublish.py wagtail/admin/views/pages/delete.py wagtail/admin/views/pages/ordering.py wagtail/admin/views/pages/usage.py wagtail/admin/views/pages/utils.py wagtail/admin/views/workflows.py wagtail/admin/views/generic.py wagtail/admin/views/account.py wagtail/admin/views/mixins.py wagtail/admin/views/chooser.py wagtail/admin/views/reports.py wagtail/admin/views/collections.py wagtail/admin/views/home.py wagtail/admin/views/page_privacy.py wagtail/admin/views/collection_privacy.py wagtail/admin/views/userbar.py wagtail/admin/views/tags.py wagtail/admin/rich_text/converters/html_to_contentstate.py wagtail/admin/rich_text/converters/editor_html.py wagtail/admin/rich_text/converters/html_ruleset.py wagtail/admin/rich_text/converters/contentstate_models.py wagtail/admin/rich_text/converters/contentstate.py wagtail/admin/rich_text/editors/hallo.py wagtail/admin/rich_text/editors/draftail/__init__.py wagtail/admin/rich_text/editors/draftail/features.py wagtail/admin/rich_text/__init__.py wagtail/admin/edit_handlers.py wagtail/admin/forms/collections.py wagtail/admin/forms/workflows.py wagtail/admin/forms/pages.py wagtail/admin/forms/models.py wagtail/admin/forms/choosers.py wagtail/admin/forms/view_restrictions.py wagtail/admin/forms/tags.py wagtail/admin/forms/auth.py wagtail/admin/forms/search.py wagtail/admin/forms/__init__.py wagtail/admin/compare.py wagtail/admin/widgets/chooser.py wagtail/admin/widgets/datetime.py wagtail/admin/widgets/button.py wagtail/admin/widgets/filtered_select.py wagtail/admin/widgets/tags.py wagtail/admin/widgets/workflows.py wagtail/admin/widgets/button_select.py wagtail/admin/widgets/auto_height_text.py wagtail/admin/widgets/__init__.py wagtail/admin/wagtail_hooks.py wagtail/admin/templatetags/wagtailadmin_tags.py wagtail/admin/templatetags/wagtailuserbar.py wagtail/admin/static_src/wagtailadmin/js/page-editor.js wagtail/admin/static_src/wagtailadmin/js/vendor/urlify.js wagtail/admin/static_src/wagtailadmin/js/vendor/jquery-3.5.1.min.js wagtail/admin/static_src/wagtailadmin/app/wagtailadmin.entry.js wagtail/admin/static_src/wagtailadmin/app/draftail.entry.js wagtail/admin/mail.py wagtail/admin/action_menu.py wagtail/admin/api/serializers.py wagtail/admin/api/views.py wagtail/admin/api/filters.py wagtail/admin/api/urls.py wagtail/admin/filters.py wagtail/admin/auth.py wagtail/admin/menu.py wagtail/admin/userbar.py wagtail/admin/viewsets/model.py wagtail/admin/viewsets/__init__.py wagtail/admin/viewsets/base.py wagtail/admin/checks.py wagtail/admin/search.py wagtail/admin/urls/__init__.py wagtail/admin/urls/pages.py wagtail/admin/urls/collections.py wagtail/admin/urls/workflows.py wagtail/admin/urls/reports.py wagtail/admin/urls/password_reset.py wagtail/admin/site_summary.py wagtail/admin/messages.py wagtail/admin/log_action_registry.py wagtail/admin/staticfiles.py wagtail/admin/navigation.py wagtail/admin/models.py wagtail/admin/signal_handlers.py wagtail/admin/modal_workflow.py wagtail/admin/localization.py wagtail/admin/apps.py wagtail/admin/jinja2tags.py wagtail/admin/datetimepicker.py wagtail/admin/blocks.py wagtail/admin/signals.py wagtail/admin/__init__.py wagtail/contrib/modeladmin/views.py wagtail/contrib/modeladmin/options.py wagtail/contrib/modeladmin/helpers/button.py wagtail/contrib/modeladmin/helpers/permission.py wagtail/contrib/modeladmin/helpers/url.py wagtail/contrib/modeladmin/helpers/search.py wagtail/contrib/modeladmin/helpers/__init__.py wagtail/contrib/modeladmin/templatetags/modeladmin_tags.py wagtail/contrib/modeladmin/mixins.py wagtail/contrib/modeladmin/menus.py wagtail/contrib/modeladmin/forms.py wagtail/contrib/modeladmin/apps.py wagtail/contrib/modeladmin/__init__.py wagtail/contrib/redirects/views.py wagtail/contrib/redirects/base_formats.py wagtail/contrib/redirects/management/commands/import_redirects.py wagtail/contrib/redirects/models.py wagtail/contrib/redirects/forms.py wagtail/contrib/redirects/tmp_storages.py wagtail/contrib/redirects/middleware.py wagtail/contrib/redirects/utils.py wagtail/contrib/redirects/wagtail_hooks.py wagtail/contrib/redirects/apps.py wagtail/contrib/redirects/urls.py wagtail/contrib/redirects/permissions.py wagtail/contrib/redirects/__init__.py wagtail/contrib/postgres_search/backend.py wagtail/contrib/postgres_search/query.py wagtail/contrib/postgres_search/utils.py wagtail/contrib/postgres_search/models.py wagtail/contrib/postgres_search/apps.py wagtail/contrib/postgres_search/__init__.py wagtail/contrib/forms/views.py wagtail/contrib/forms/models.py wagtail/contrib/forms/forms.py wagtail/contrib/forms/utils.py wagtail/contrib/forms/edit_handlers.py wagtail/contrib/forms/wagtail_hooks.py wagtail/contrib/forms/apps.py wagtail/contrib/forms/urls.py wagtail/contrib/forms/__init__.py wagtail/contrib/frontend_cache/tests.py wagtail/contrib/frontend_cache/backends.py wagtail/contrib/frontend_cache/utils.py wagtail/contrib/frontend_cache/signal_handlers.py wagtail/contrib/frontend_cache/apps.py wagtail/contrib/frontend_cache/__init__.py wagtail/contrib/search_promotions/tests.py wagtail/contrib/search_promotions/views.py wagtail/contrib/search_promotions/forms.py wagtail/contrib/search_promotions/wagtail_hooks.py wagtail/contrib/search_promotions/models.py wagtail/contrib/search_promotions/templatetags/wagtailsearchpromotions_tags.py wagtail/contrib/search_promotions/apps.py wagtail/contrib/search_promotions/admin_urls.py wagtail/contrib/search_promotions/__init__.py wagtail/contrib/routable_page/tests.py wagtail/contrib/routable_page/models.py wagtail/contrib/routable_page/templatetags/wagtailroutablepage_tags.py wagtail/contrib/routable_page/apps.py wagtail/contrib/routable_page/__init__.py wagtail/contrib/settings/views.py wagtail/contrib/settings/models.py wagtail/contrib/settings/jinja2tags.py wagtail/contrib/settings/registry.py wagtail/contrib/settings/context_processors.py wagtail/contrib/settings/forms.py wagtail/contrib/settings/templatetags/wagtailsettings_tags.py wagtail/contrib/settings/wagtail_hooks.py wagtail/contrib/settings/apps.py wagtail/contrib/settings/urls.py wagtail/contrib/settings/permissions.py wagtail/contrib/settings/__init__.py wagtail/contrib/table_block/tests.py wagtail/contrib/table_block/blocks.py wagtail/contrib/table_block/templatetags/table_block_tags.py wagtail/contrib/table_block/apps.py wagtail/contrib/table_block/__init__.py wagtail/contrib/sitemaps/tests.py wagtail/contrib/sitemaps/sitemap_generator.py wagtail/contrib/sitemaps/views.py wagtail/contrib/sitemaps/apps.py wagtail/contrib/sitemaps/__init__.py wagtail/contrib/styleguide/views.py wagtail/contrib/styleguide/wagtail_hooks.py wagtail/contrib/styleguide/tests.py wagtail/contrib/styleguide/apps.py wagtail/contrib/styleguide/__init__.py wagtail/core/models.py wagtail/core/blocks/field_block.py wagtail/core/blocks/stream_block.py wagtail/core/blocks/base.py wagtail/core/blocks/struct_block.py wagtail/core/blocks/list_block.py wagtail/core/blocks/static_block.py wagtail/core/blocks/__init__.py wagtail/core/blocks/utils.py wagtail/core/management/commands/fixtree.py wagtail/core/management/commands/publish_scheduled_pages.py wagtail/core/management/commands/create_log_entries_from_revisions.py wagtail/core/management/commands/replace_text.py wagtail/core/management/commands/purge_revisions.py wagtail/core/management/commands/move_pages.py wagtail/core/management/commands/set_url_paths.py wagtail/core/permission_policies/collections.py wagtail/core/permission_policies/base.py wagtail/core/permission_policies/__init__.py wagtail/core/rich_text/rewriters.py wagtail/core/rich_text/feature_registry.py wagtail/core/rich_text/__init__.py wagtail/core/rich_text/pages.py wagtail/core/query.py wagtail/core/templatetags/wagtailcore_tags.py wagtail/core/fields.py wagtail/core/whitelist.py wagtail/core/utils.py wagtail/core/treebeard.py wagtail/core/wagtail_hooks.py wagtail/core/hooks.py wagtail/core/jinja2tags.py wagtail/core/views.py wagtail/core/signal_handlers.py wagtail/core/forms.py wagtail/core/workflows.py wagtail/core/sites.py wagtail/core/signals.py wagtail/core/urls.py wagtail/core/apps.py wagtail/core/url_routing.py wagtail/core/compat.py wagtail/core/__init__.py wagtail/core/permissions.py wagtail/core/exceptions.py wagtail/images/views/images.py wagtail/images/views/multiple.py wagtail/images/views/chooser.py wagtail/images/views/serve.py wagtail/images/models.py wagtail/images/image_operations.py wagtail/images/rect.py wagtail/images/templatetags/wagtailimages_tags.py wagtail/images/wagtail_hooks.py wagtail/images/fields.py wagtail/images/rich_text/contentstate.py wagtail/images/rich_text/editor_html.py wagtail/images/rich_text/__init__.py wagtail/images/formats.py wagtail/images/api/v2/views.py wagtail/images/api/v2/serializers.py wagtail/images/api/fields.py wagtail/images/api/admin/views.py wagtail/images/api/admin/serializers.py wagtail/images/forms.py wagtail/images/checks.py wagtail/images/utils.py wagtail/images/blocks.py wagtail/images/widgets.py wagtail/images/jinja2tags.py wagtail/images/signal_handlers.py wagtail/images/edit_handlers.py wagtail/images/__init__.py wagtail/images/apps.py wagtail/images/shortcuts.py wagtail/images/admin.py wagtail/images/admin_urls.py wagtail/images/permissions.py wagtail/images/urls.py wagtail/images/exceptions.py wagtail/search/backends/elasticsearch2.py wagtail/search/backends/base.py wagtail/search/backends/db.py wagtail/search/backends/elasticsearch7.py wagtail/search/backends/__init__.py wagtail/search/backends/elasticsearch5.py wagtail/search/backends/elasticsearch6.py wagtail/search/index.py wagtail/search/management/commands/update_index.py wagtail/search/management/commands/search_garbage_collect.py wagtail/search/management/commands/wagtail_update_index.py wagtail/search/utils.py wagtail/search/models.py wagtail/search/query.py wagtail/search/views/queries.py wagtail/search/signal_handlers.py wagtail/search/apps.py wagtail/search/queryset.py wagtail/search/wagtail_hooks.py wagtail/search/urls/admin.py wagtail/search/forms.py wagtail/search/__init__.py wagtail/users/tests.py wagtail/users/forms.py wagtail/users/views/users.py wagtail/users/views/groups.py wagtail/users/wagtail_hooks.py wagtail/users/templatetags/wagtailusers_tags.py wagtail/users/models.py wagtail/users/utils.py wagtail/users/apps.py wagtail/users/widgets.py wagtail/users/urls/users.py wagtail/users/__init__.py wagtail/snippets/tests.py wagtail/snippets/views/snippets.py wagtail/snippets/views/chooser.py wagtail/snippets/wagtail_hooks.py wagtail/snippets/widgets.py wagtail/snippets/models.py wagtail/snippets/blocks.py wagtail/snippets/permissions.py wagtail/snippets/templatetags/wagtailsnippets_admin_tags.py wagtail/snippets/edit_handlers.py wagtail/snippets/apps.py wagtail/snippets/urls.py wagtail/snippets/__init__.py wagtail/api/v2/views.py wagtail/api/v2/serializers.py wagtail/api/v2/utils.py wagtail/api/v2/filters.py wagtail/api/v2/router.py wagtail/api/v2/signal_handlers.py wagtail/api/v2/pagination.py wagtail/api/v2/apps.py wagtail/api/v2/__init__.py wagtail/api/conf.py wagtail/api/__init__.py wagtail/documents/views/documents.py wagtail/documents/views/chooser.py wagtail/documents/views/serve.py wagtail/documents/views/multiple.py wagtail/documents/models.py wagtail/documents/wagtail_hooks.py wagtail/documents/rich_text/editor_html.py wagtail/documents/rich_text/contentstate.py wagtail/documents/rich_text/__init__.py wagtail/documents/api/v2/views.py wagtail/documents/api/v2/serializers.py wagtail/documents/api/admin/views.py wagtail/documents/forms.py wagtail/documents/widgets.py wagtail/documents/blocks.py wagtail/documents/__init__.py wagtail/documents/apps.py wagtail/documents/signal_handlers.py wagtail/documents/admin.py wagtail/documents/edit_handlers.py wagtail/documents/admin_urls.py wagtail/documents/permissions.py wagtail/documents/urls.py wagtail/embeds/finders/oembed.py wagtail/embeds/finders/embedly.py wagtail/embeds/finders/__init__.py wagtail/embeds/finders/base.py wagtail/embeds/oembed_providers.py wagtail/embeds/rich_text/contentstate.py wagtail/embeds/rich_text/__init__.py wagtail/embeds/rich_text/editor_html.py wagtail/embeds/blocks.py wagtail/embeds/models.py wagtail/embeds/views/chooser.py wagtail/embeds/embeds.py wagtail/embeds/wagtail_hooks.py wagtail/embeds/templatetags/wagtailembeds_tags.py wagtail/embeds/format.py wagtail/embeds/apps.py wagtail/embeds/exceptions.py wagtail/embeds/forms.py wagtail/embeds/urls.py wagtail/embeds/__init__.py wagtail/utils/setup.py wagtail/utils/sendfile.py wagtail/utils/sendfile_streaming_backend.py wagtail/utils/deprecation.py wagtail/utils/decorators.py wagtail/utils/version.py wagtail/utils/widgets.py wagtail/utils/apps.py wagtail/utils/loading.py wagtail/utils/urlpatterns.py wagtail/utils/utils.py wagtail/sites/tests.py wagtail/sites/views.py wagtail/sites/wagtail_hooks.py wagtail/sites/forms.py wagtail/sites/apps.py wagtail/sites/__init__.py wagtail/bin/wagtail.py wagtail/__init__.py client/src/components/Draftail/sources/ModalWorkflowSource.js client/src/components/Draftail/decorators/TooltipEntity.js client/src/components/Draftail/decorators/Link.js client/src/components/Draftail/decorators/Document.js client/src/components/Draftail/blocks/MediaBlock.js client/src/components/Draftail/blocks/ImageBlock.js client/src/components/Draftail/blocks/EmbedBlock.js client/src/components/Draftail/EditorFallback/EditorFallback.js client/src/components/Draftail/index.js client/src/components/Draftail/DraftUtils.js client/src/components/Draftail/Tooltip/Tooltip.js client/src/components/Explorer/ExplorerPanel.js client/src/components/Explorer/actions.js client/src/components/Explorer/reducers/nodes.js client/src/components/Explorer/reducers/explorer.js client/src/components/Explorer/Explorer.js client/src/components/Explorer/ExplorerItem.js client/src/components/Explorer/index.js client/src/components/Explorer/ExplorerHeader.js client/src/components/Explorer/ExplorerToggle.js client/src/components/Explorer/PageCount.js client/src/components/Portal/Portal.js client/src/components/Button/Button.js client/src/components/UpgradeNotification/index.js client/src/components/Transition/Transition.js client/src/components/Icon/Icon.js client/src/components/PublicationStatus/PublicationStatus.js client/src/components/LoadingSpinner/LoadingSpinner.js client/src/includes/initSubmenus.js client/src/includes/initSkipLink.js client/src/utils/focus.js client/src/utils/version.js client/src/utils/actions.js client/src/api/client.js client/src/api/admin.js client/src/config/wagtailConfig.js

Flags

Flags have been temporarily removed from this view while the flagging feature is refactored for better performance and user experience.

You can still use flags when viewing individual files. Flag-level thresholds will also remain on pull and merge requests in your repository provider.

More information can be found in our documentation.


@@ -52,7 +52,8 @@
Loading
52 52
        'connection': connection,
53 53
        'headers': {
54 54
            'Auto-Submitted': 'auto-generated',
55 -
        }
55 +
        },
56 +
        'bcc': kwargs.get('bcc', [])
56 57
    }
57 58
    mail = EmailMultiAlternatives(subject, message, from_email, recipient_list, **multi_alt_kwargs)
58 59
    html_message = kwargs.get('html_message', None)
@@ -223,7 +224,9 @@
Loading
223 224
            'html': template_html,
224 225
        }
225 226
226 -
    def send_emails(self, template_set, context, recipients, **kwargs):
227 +
    def send_personalised_emails(self, template_set, context, recipients, **kwargs):
228 +
        """ Send emails in a personalised form, including the user in the email context and
229 +
        translating to the recipient's preferred language. """
227 230
228 231
        connection = get_connection()
229 232
        sent_count = 0
@@ -233,7 +236,6 @@
Loading
233 236
                # Send emails
234 237
                for recipient in recipients:
235 238
                    try:
236 -
237 239
                        # update context with this recipient
238 240
                        context["user"] = recipient
239 241
@@ -260,8 +262,38 @@
Loading
260 262
261 263
        return sent_count == len(recipients)
262 264
265 +
    def send_bulk_emails(self, template_set, context, recipients, **kwargs):
266 +
        """ Send emails in a bulk form by sending the same email to all recipients using the BCC field. """
267 +
268 +
        connection = get_connection()
269 +
        try:
270 +
            with OpenedConnection(connection) as open_connection:
271 +
                try:
272 +
                    email_subject = render_to_string(template_set['subject'], context).strip()
273 +
                    email_content = render_to_string(template_set['text'], context).strip()
274 +
275 +
                    kwargs = {}
276 +
                    if getattr(settings, 'WAGTAILADMIN_NOTIFICATION_USE_HTML', False):
277 +
                        kwargs['html_message'] = render_to_string(template_set['html'], context)
278 +
279 +
                    bcc_addresses = [recipient.email for recipient in recipients]
280 +
281 +
                    # Send email
282 +
                    send_mail(email_subject, email_content, [], connection=open_connection, bcc=[bcc_addresses], **kwargs)
283 +
                except Exception:
284 +
                    logger.exception(
285 +
                        "Failed to send notification email '%s'",
286 +
                        email_subject
287 +
                    )
288 +
        except (TimeoutError, ConnectionError):
289 +
            logger.exception("Mail connection error, notification sending skipped")
290 +
263 291
    def send_notifications(self, template_set, context, recipients, **kwargs):
264 -
        return self.send_emails(template_set, context, recipients, **kwargs)
292 +
        DEFAULT_BULK_SEND_THRESHOLD = 30
293 +
        if getattr(settings, 'WAGTAILADMIN_NOTIFICATION_BULK_SEND_THRESHOLD', DEFAULT_BULK_SEND_THRESHOLD) > len(recipients):
294 +
            return self.send_personalised_emails(template_set, context, recipients, **kwargs)
295 +
        else:
296 +
            return self.send_bulk_emails(template_set, context, recipients, **kwargs)
265 297
266 298
267 299
class BaseWorkflowStateEmailNotifier(EmailNotificationMixin, Notifier):

Everything is accounted for!

No changes detected that need to be reviewed.
What changes does Codecov check for?
Lines, not adjusted in diff, that have changed coverage data.
Files that introduced coverage data that had none before.
Files that have missing coverage data that once were tracked.
Files Coverage
client/src 92.85%
wagtail -0.02% 89.89%
Project Totals (427 files) 89.98%
Loading