From 16979ae7790f6e6437dd576a25068362d2354d39 Mon Sep 17 00:00:00 2001 From: Antonio Rodriguez Date: Thu, 9 Jul 2026 20:28:30 +0200 Subject: [PATCH 1/2] Add TinyMCE WYSIWYG editor, HTML emails, template DRY refactor, DataTables fixes - Switch to EmailMultiAlternatives with HTML + plain text fallback - Add TinyMCE editor (self-hosted) for compose, template form, and resend pages - All body reads/writes go through getBodyContent()/setBodyContent() helpers - Variable insert uses TinyMCE API with plain textarea fallback - Preview modals render HTML body (.html() instead of .text(),
instead of
)
- Campaign detail sidebar and sent email modal render HTML body
- Auto-select template from ?template=ID URL param on compose page
- Extract 6 shared template includes: _email_preview_modal, _recipient_preview_modal,
  _delete_confirm_base, _status_badge_td, _stat_card, _danger_zone_card
- Fix nested form bug in variable_form danger zone card
- Fix DataTables crash on empty filtered results (skip init when no data rows)
- Fix DataTables layout: inline Show/Search controls, table outside card body,
  load Bootstrap4 theme CSS, pagination overflow handling
- Fix aria-hidden warnings on modal close (campaign_detail, resend_compose)
- Campaign detail log table: simple_numbers pagination, default 50 rows
---
 coldfront_notifications/campaign_sender.py    |    8 +-
 .../coldfront_notifications/css/compose.css   |    2 +-
 .../js/campaign_detail.js                     |    9 +-
 .../js/compose_draft.js                       |    4 +-
 .../js/compose_init.js                        |   16 +
 .../js/compose_preview.js                     |    8 +-
 .../js/compose_templates.js                   |   20 +-
 .../js/compose_validate.js                    |    2 +-
 .../js/datatables_init.js                     |   17 +
 .../js/template_form.js                       |   16 +-
 .../js/tinymce_init.js                        |   43 +
 .../vendor/tinymce/icons/default/icons.min.js |    1 +
 .../vendor/tinymce/langs/README.md            |    3 +
 .../vendor/tinymce/license.txt                |   21 +
 .../vendor/tinymce/models/dom/model.min.js    |    4 +
 .../tinymce/plugins/accordion/plugin.min.js   |    4 +
 .../tinymce/plugins/advlist/plugin.min.js     |    4 +
 .../tinymce/plugins/anchor/plugin.min.js      |    4 +
 .../tinymce/plugins/autolink/plugin.min.js    |    4 +
 .../tinymce/plugins/autoresize/plugin.min.js  |    4 +
 .../tinymce/plugins/autosave/plugin.min.js    |    4 +
 .../tinymce/plugins/charmap/plugin.min.js     |    4 +
 .../vendor/tinymce/plugins/code/plugin.min.js |    4 +
 .../tinymce/plugins/codesample/plugin.min.js  |    4 +
 .../plugins/directionality/plugin.min.js      |    4 +
 .../plugins/emoticons/js/emojiimages.js       |    1 +
 .../plugins/emoticons/js/emojiimages.min.js   |    3 +
 .../tinymce/plugins/emoticons/js/emojis.js    |    1 +
 .../plugins/emoticons/js/emojis.min.js        |    2 +
 .../tinymce/plugins/emoticons/plugin.min.js   |    4 +
 .../tinymce/plugins/fullscreen/plugin.min.js  |    4 +
 .../tinymce/plugins/help/js/i18n/keynav/ar.js |   90 +
 .../plugins/help/js/i18n/keynav/bg_BG.js      |   90 +
 .../tinymce/plugins/help/js/i18n/keynav/ca.js |   90 +
 .../tinymce/plugins/help/js/i18n/keynav/cs.js |   90 +
 .../tinymce/plugins/help/js/i18n/keynav/da.js |   90 +
 .../tinymce/plugins/help/js/i18n/keynav/de.js |   90 +
 .../tinymce/plugins/help/js/i18n/keynav/el.js |   90 +
 .../tinymce/plugins/help/js/i18n/keynav/en.js |   90 +
 .../tinymce/plugins/help/js/i18n/keynav/es.js |   90 +
 .../tinymce/plugins/help/js/i18n/keynav/eu.js |   90 +
 .../tinymce/plugins/help/js/i18n/keynav/fa.js |   90 +
 .../tinymce/plugins/help/js/i18n/keynav/fi.js |   90 +
 .../plugins/help/js/i18n/keynav/fr_FR.js      |   90 +
 .../plugins/help/js/i18n/keynav/he_IL.js      |   90 +
 .../tinymce/plugins/help/js/i18n/keynav/hi.js |   90 +
 .../tinymce/plugins/help/js/i18n/keynav/hr.js |   90 +
 .../plugins/help/js/i18n/keynav/hu_HU.js      |   90 +
 .../tinymce/plugins/help/js/i18n/keynav/id.js |   90 +
 .../tinymce/plugins/help/js/i18n/keynav/it.js |   90 +
 .../tinymce/plugins/help/js/i18n/keynav/ja.js |   90 +
 .../tinymce/plugins/help/js/i18n/keynav/kk.js |   90 +
 .../plugins/help/js/i18n/keynav/ko_KR.js      |   90 +
 .../tinymce/plugins/help/js/i18n/keynav/ms.js |   90 +
 .../plugins/help/js/i18n/keynav/nb_NO.js      |   90 +
 .../tinymce/plugins/help/js/i18n/keynav/nl.js |   90 +
 .../tinymce/plugins/help/js/i18n/keynav/pl.js |   90 +
 .../plugins/help/js/i18n/keynav/pt_BR.js      |   90 +
 .../plugins/help/js/i18n/keynav/pt_PT.js      |   90 +
 .../tinymce/plugins/help/js/i18n/keynav/ro.js |   90 +
 .../tinymce/plugins/help/js/i18n/keynav/ru.js |   90 +
 .../tinymce/plugins/help/js/i18n/keynav/sk.js |   90 +
 .../plugins/help/js/i18n/keynav/sl_SI.js      |   90 +
 .../plugins/help/js/i18n/keynav/sv_SE.js      |   90 +
 .../plugins/help/js/i18n/keynav/th_TH.js      |   90 +
 .../tinymce/plugins/help/js/i18n/keynav/tr.js |   90 +
 .../tinymce/plugins/help/js/i18n/keynav/uk.js |   90 +
 .../tinymce/plugins/help/js/i18n/keynav/vi.js |   90 +
 .../plugins/help/js/i18n/keynav/zh_CN.js      |   84 +
 .../plugins/help/js/i18n/keynav/zh_TW.js      |   90 +
 .../vendor/tinymce/plugins/help/plugin.min.js |    4 +
 .../tinymce/plugins/image/plugin.min.js       |    4 +
 .../tinymce/plugins/importcss/plugin.min.js   |    4 +
 .../plugins/insertdatetime/plugin.min.js      |    4 +
 .../vendor/tinymce/plugins/link/plugin.min.js |    4 +
 .../tinymce/plugins/lists/plugin.min.js       |    4 +
 .../tinymce/plugins/media/plugin.min.js       |    4 +
 .../tinymce/plugins/nonbreaking/plugin.min.js |    4 +
 .../tinymce/plugins/pagebreak/plugin.min.js   |    4 +
 .../tinymce/plugins/preview/plugin.min.js     |    4 +
 .../tinymce/plugins/quickbars/plugin.min.js   |    4 +
 .../vendor/tinymce/plugins/save/plugin.min.js |    4 +
 .../plugins/searchreplace/plugin.min.js       |    4 +
 .../tinymce/plugins/table/plugin.min.js       |    4 +
 .../tinymce/plugins/template/plugin.min.js    |    4 +
 .../plugins/visualblocks/plugin.min.js        |    4 +
 .../tinymce/plugins/visualchars/plugin.min.js |    4 +
 .../tinymce/plugins/wordcount/plugin.min.js   |    4 +
 .../tinymce/skins/content/dark/content.js     |    2 +
 .../skins/content/dark/content.min.css        |    1 +
 .../tinymce/skins/content/default/content.js  |    2 +
 .../skins/content/default/content.min.css     |    1 +
 .../tinymce/skins/content/document/content.js |    2 +
 .../skins/content/document/content.min.css    |    1 +
 .../skins/content/tinymce-5-dark/content.js   |    2 +
 .../content/tinymce-5-dark/content.min.css    |    1 +
 .../skins/content/tinymce-5/content.js        |    2 +
 .../skins/content/tinymce-5/content.min.css   |    1 +
 .../tinymce/skins/content/writer/content.js   |    2 +
 .../skins/content/writer/content.min.css      |    1 +
 .../skins/ui/oxide-dark/content.inline.js     |    2 +
 .../ui/oxide-dark/content.inline.min.css      |    1 +
 .../tinymce/skins/ui/oxide-dark/content.js    |    2 +
 .../skins/ui/oxide-dark/content.min.css       |    1 +
 .../tinymce/skins/ui/oxide-dark/skin.js       |    2 +
 .../tinymce/skins/ui/oxide-dark/skin.min.css  |    1 +
 .../skins/ui/oxide-dark/skin.shadowdom.js     |    2 +
 .../ui/oxide-dark/skin.shadowdom.min.css      |    1 +
 .../tinymce/skins/ui/oxide/content.inline.js  |    2 +
 .../skins/ui/oxide/content.inline.min.css     |    1 +
 .../vendor/tinymce/skins/ui/oxide/content.js  |    2 +
 .../tinymce/skins/ui/oxide/content.min.css    |    1 +
 .../vendor/tinymce/skins/ui/oxide/skin.js     |    2 +
 .../tinymce/skins/ui/oxide/skin.min.css       |    1 +
 .../tinymce/skins/ui/oxide/skin.shadowdom.js  |    2 +
 .../skins/ui/oxide/skin.shadowdom.min.css     |    1 +
 .../skins/ui/tinymce-5-dark/content.inline.js |    2 +
 .../ui/tinymce-5-dark/content.inline.min.css  |    1 +
 .../skins/ui/tinymce-5-dark/content.js        |    2 +
 .../skins/ui/tinymce-5-dark/content.min.css   |    1 +
 .../tinymce/skins/ui/tinymce-5-dark/skin.js   |    2 +
 .../skins/ui/tinymce-5-dark/skin.min.css      |    1 +
 .../skins/ui/tinymce-5-dark/skin.shadowdom.js |    2 +
 .../ui/tinymce-5-dark/skin.shadowdom.min.css  |    1 +
 .../skins/ui/tinymce-5/content.inline.js      |    2 +
 .../skins/ui/tinymce-5/content.inline.min.css |    1 +
 .../tinymce/skins/ui/tinymce-5/content.js     |    2 +
 .../skins/ui/tinymce-5/content.min.css        |    1 +
 .../vendor/tinymce/skins/ui/tinymce-5/skin.js |    2 +
 .../tinymce/skins/ui/tinymce-5/skin.min.css   |    1 +
 .../skins/ui/tinymce-5/skin.shadowdom.js      |    2 +
 .../skins/ui/tinymce-5/skin.shadowdom.min.css |    1 +
 .../vendor/tinymce/themes/silver/theme.min.js |  384 ++
 .../vendor/tinymce/tinymce.d.ts               | 3238 +++++++++++++++++
 .../vendor/tinymce/tinymce.min.js             |  384 ++
 .../_danger_zone_card.html                    |   21 +
 .../_delete_confirm_base.html                 |   41 +
 .../_email_preview_modal.html                 |   35 +
 .../_recipient_preview_modal.html             |   84 +
 .../coldfront_notifications/_stat_card.html   |    8 +
 .../_status_badge_td.html                     |    8 +
 .../campaign_detail.html                      |   85 +-
 .../campaign_list.html                        |   73 +-
 .../coldfront_notifications/compose.html      |  122 +-
 .../coldfront_notifications/dashboard.html    |   52 +-
 .../draft_delete_confirm.html                 |   74 +-
 .../resend_compose.html                       |  123 +-
 .../template_delete_confirm.html              |   72 +-
 .../template_form.html                        |   18 +-
 .../template_list.html                        |   50 +-
 .../variable_delete_confirm.html              |   99 +-
 .../variable_form.html                        |   20 +-
 .../variable_list.html                        |  110 +-
 153 files changed, 8328 insertions(+), 644 deletions(-)
 create mode 100644 coldfront_notifications/static/coldfront_notifications/js/tinymce_init.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/icons/default/icons.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/langs/README.md
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/license.txt
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/models/dom/model.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/accordion/plugin.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/advlist/plugin.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/anchor/plugin.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/autolink/plugin.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/autoresize/plugin.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/autosave/plugin.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/charmap/plugin.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/code/plugin.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/codesample/plugin.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/directionality/plugin.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/emoticons/js/emojiimages.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/emoticons/js/emojiimages.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/emoticons/js/emojis.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/emoticons/js/emojis.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/emoticons/plugin.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/fullscreen/plugin.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/ar.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/bg_BG.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/ca.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/cs.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/da.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/de.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/el.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/en.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/es.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/eu.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/fa.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/fi.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/fr_FR.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/he_IL.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/hi.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/hr.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/hu_HU.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/id.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/it.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/ja.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/kk.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/ko_KR.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/ms.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/nb_NO.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/nl.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/pl.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/pt_BR.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/pt_PT.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/ro.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/ru.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/sk.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/sl_SI.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/sv_SE.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/th_TH.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/tr.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/uk.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/vi.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/zh_CN.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/js/i18n/keynav/zh_TW.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/help/plugin.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/image/plugin.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/importcss/plugin.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/insertdatetime/plugin.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/link/plugin.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/lists/plugin.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/media/plugin.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/nonbreaking/plugin.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/pagebreak/plugin.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/preview/plugin.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/quickbars/plugin.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/save/plugin.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/searchreplace/plugin.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/table/plugin.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/template/plugin.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/visualblocks/plugin.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/visualchars/plugin.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/plugins/wordcount/plugin.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/content/dark/content.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/content/dark/content.min.css
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/content/default/content.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/content/default/content.min.css
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/content/document/content.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/content/document/content.min.css
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/content/tinymce-5-dark/content.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/content/tinymce-5-dark/content.min.css
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/content/tinymce-5/content.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/content/tinymce-5/content.min.css
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/content/writer/content.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/content/writer/content.min.css
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/oxide-dark/content.inline.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/oxide-dark/content.inline.min.css
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/oxide-dark/content.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/oxide-dark/content.min.css
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/oxide-dark/skin.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/oxide-dark/skin.min.css
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/oxide-dark/skin.shadowdom.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/oxide-dark/skin.shadowdom.min.css
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/oxide/content.inline.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/oxide/content.inline.min.css
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/oxide/content.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/oxide/content.min.css
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/oxide/skin.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/oxide/skin.min.css
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/oxide/skin.shadowdom.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/oxide/skin.shadowdom.min.css
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/tinymce-5-dark/content.inline.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/tinymce-5-dark/content.inline.min.css
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/tinymce-5-dark/content.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/tinymce-5-dark/content.min.css
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/tinymce-5-dark/skin.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/tinymce-5-dark/skin.min.css
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/tinymce-5-dark/skin.shadowdom.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/tinymce-5-dark/skin.shadowdom.min.css
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/tinymce-5/content.inline.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/tinymce-5/content.inline.min.css
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/tinymce-5/content.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/tinymce-5/content.min.css
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/tinymce-5/skin.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/tinymce-5/skin.min.css
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/tinymce-5/skin.shadowdom.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/skins/ui/tinymce-5/skin.shadowdom.min.css
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/themes/silver/theme.min.js
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/tinymce.d.ts
 create mode 100644 coldfront_notifications/static/coldfront_notifications/vendor/tinymce/tinymce.min.js
 create mode 100644 coldfront_notifications/templates/coldfront_notifications/_danger_zone_card.html
 create mode 100644 coldfront_notifications/templates/coldfront_notifications/_delete_confirm_base.html
 create mode 100644 coldfront_notifications/templates/coldfront_notifications/_email_preview_modal.html
 create mode 100644 coldfront_notifications/templates/coldfront_notifications/_recipient_preview_modal.html
 create mode 100644 coldfront_notifications/templates/coldfront_notifications/_stat_card.html
 create mode 100644 coldfront_notifications/templates/coldfront_notifications/_status_badge_td.html

diff --git a/coldfront_notifications/campaign_sender.py b/coldfront_notifications/campaign_sender.py
index abf70eb..a84dc9b 100644
--- a/coldfront_notifications/campaign_sender.py
+++ b/coldfront_notifications/campaign_sender.py
@@ -13,8 +13,9 @@
 import time
 
 from django.conf import settings
-from django.core.mail import EmailMessage, get_connection
+from django.core.mail import EmailMultiAlternatives, get_connection
 from django.utils import timezone
+from django.utils.html import strip_tags
 
 from .conf import BATCH_SIZE, BATCH_DELAY, MAX_RETRIES, RETRY_DELAY
 from .filters import RecipientResolver
@@ -239,14 +240,15 @@ def _deliver_emails(self, resolved_emails, snapshot):
                         if bcc:
                             bcc = list(dev_list)
 
-                message = EmailMessage(
+                message = EmailMultiAlternatives(
                     subject=rendered_subject,
-                    body=rendered_body,
+                    body=strip_tags(rendered_body),
                     from_email=campaign.sender,
                     to=to,
                     bcc=bcc,
                     reply_to=[campaign.reply_to] if campaign.reply_to else [],
                 )
+                message.attach_alternative(rendered_body, "text/html")
 
                 send_error = self.delivery.send_with_retry(message, connection)
                 if send_error is None:
diff --git a/coldfront_notifications/static/coldfront_notifications/css/compose.css b/coldfront_notifications/static/coldfront_notifications/css/compose.css
index dc5fe0c..cbda566 100644
--- a/coldfront_notifications/static/coldfront_notifications/css/compose.css
+++ b/coldfront_notifications/static/coldfront_notifications/css/compose.css
@@ -175,7 +175,7 @@
   display: inline-flex; align-items: center; gap: 4px; margin: 0; white-space: nowrap;
 }
 .recip-page-size-select {
-  width: auto !important; display: inline-block; padding: 2px 6px;
+  width: 52px !important; display: inline-block; padding: 2px 4px;
   font-size: .78rem; height: auto !important;
 }
 
diff --git a/coldfront_notifications/static/coldfront_notifications/js/campaign_detail.js b/coldfront_notifications/static/coldfront_notifications/js/campaign_detail.js
index 2ff440c..5c6a555 100644
--- a/coldfront_notifications/static/coldfront_notifications/js/campaign_detail.js
+++ b/coldfront_notifications/static/coldfront_notifications/js/campaign_detail.js
@@ -3,6 +3,11 @@
 //   CAMPAIGN_STATUS       - string ('queued', 'sending', 'sent', etc.)
 //   CAMPAIGN_PROGRESS_URL - URL for the progress JSON endpoint
 
+// Blur focused element before modal hides to avoid aria-hidden warning.
+$('#emailViewModal').on('hide.bs.modal', function() {
+  if (document.activeElement) document.activeElement.blur();
+});
+
 var STATUS_CLASSES = {
   sent:    'badge-success',
   sending: 'badge-primary',
@@ -49,7 +54,7 @@ $(function(){
   // Only init DataTable when there are real data rows.
   var realRows = $('#logTable tbody tr td:not([colspan])').length;
   if (realRows > 0) {
-    $('#logTable').DataTable({ order:[[2,'asc']], columnDefs:[{orderable:false,targets:[3]}] });
+    $('#logTable').DataTable({ order:[[2,'asc']], columnDefs:[{orderable:false,targets:[3]}], pagingType:'simple_numbers', pageLength:50, lengthMenu:[[50,100,250,-1],[50,100,250,'All']] });
   }
   // Start polling if campaign is in-flight.
   if (CAMPAIGN_STATUS === 'sending' || CAMPAIGN_STATUS === 'queued') {
@@ -62,6 +67,6 @@ $(document).on('click', '.view-email-btn', function() {
   $('#em-replyto').text($(this).attr('data-replyto') || '—');
   $('#em-to').text($(this).attr('data-email'));
   $('#em-subject').text($(this).attr('data-subject'));
-  $('#em-body').text($(this).attr('data-body'));
+  $('#em-body').html($(this).attr('data-body'));
   $('#emailViewModal').modal('show');
 });
diff --git a/coldfront_notifications/static/coldfront_notifications/js/compose_draft.js b/coldfront_notifications/static/coldfront_notifications/js/compose_draft.js
index 4c84a3c..0bee2fc 100644
--- a/coldfront_notifications/static/coldfront_notifications/js/compose_draft.js
+++ b/coldfront_notifications/static/coldfront_notifications/js/compose_draft.js
@@ -31,7 +31,7 @@ function _collectDraftData() {
     csrfmiddlewaretoken: $('input[name=csrfmiddlewaretoken]').val(),
     draft_pk: DRAFT_PK || '',
     subject: $('#id_subject').val() || '',
-    body: $('#id_body').val() || '',
+    body: getBodyContent() || '',
     sender: $('#id_sender').val() || '',
     reply_to: $('#id_reply_to').val() || '',
     template_id: $('#hidden_template_id').val() || '',
@@ -91,7 +91,7 @@ function restoreDraft(draftData) {
 
   // Restore form fields
   if (draftData.subject) $('#id_subject').val(draftData.subject);
-  if (draftData.body) $('#id_body').val(draftData.body);
+  if (draftData.body) setBodyContent(draftData.body);
   if (draftData.sender) $('#id_sender').val(draftData.sender);
   if (draftData.reply_to) $('#id_reply_to').val(draftData.reply_to);
   if (draftData.template_id) $('#hidden_template_id').val(draftData.template_id);
diff --git a/coldfront_notifications/static/coldfront_notifications/js/compose_init.js b/coldfront_notifications/static/coldfront_notifications/js/compose_init.js
index d120283..957a8ab 100644
--- a/coldfront_notifications/static/coldfront_notifications/js/compose_init.js
+++ b/coldfront_notifications/static/coldfront_notifications/js/compose_init.js
@@ -8,6 +8,7 @@
 
 // ── Serialize filter hidden fields before submit ─────────────────
 $('#notifForm').on('submit', function() {
+  if (typeof tinymce !== 'undefined') tinymce.triggerSave();
   $('#h_projects').val(JSON.stringify($('#f_project').val() || []));
   $('#h_allocations').val(JSON.stringify($('#f_allocation').val() || []));
   $('#h_depts').val(JSON.stringify($('#f_dept').val() || []));
@@ -26,6 +27,21 @@ $(document).ready(function() {
     width: '100%',
     placeholder: function() { return $(this).data('placeholder'); }
   });
+
+  // Auto-select template if ?template=ID is in the URL
+  var params = new URLSearchParams(window.location.search);
+  var templateId = params.get('template');
+  if (templateId) {
+    // Delay slightly to ensure TinyMCE is initialized
+    setTimeout(function() {
+      var $item = $('.tmpl-item[data-id="' + templateId + '"]');
+      if ($item.length) {
+        $('.tmpl-item').removeClass('active');
+        $item.addClass('active');
+        loadTemplate($item);
+      }
+    }, 500);
+  }
 });
 
 // ── pageshow: reset validation state + refresh active template ────
diff --git a/coldfront_notifications/static/coldfront_notifications/js/compose_preview.js b/coldfront_notifications/static/coldfront_notifications/js/compose_preview.js
index e0557a8..aa22aaa 100644
--- a/coldfront_notifications/static/coldfront_notifications/js/compose_preview.js
+++ b/coldfront_notifications/static/coldfront_notifications/js/compose_preview.js
@@ -22,14 +22,14 @@ function renderPreviewSample(idx) {
     // "Template" — no recipient, raw tokens
     $('#prev-to').text('—');
     $('#prev-subject').text($('#id_subject').val() || '—');
-    $('#prev-body').text($('#id_body').val() || '—');
+    $('#prev-body').html(getBodyContent() || '—');
     $('#prev-error').hide();
     $('#prev-footer').text('Showing template with literal placeholders');
     return;
   }
   $('#prev-to').text((s.recipient.name || s.recipient.username) + ' <' + s.recipient.email + '>');
   $('#prev-subject').text(s.subject || '—');
-  $('#prev-body').text(s.body || '—');
+  $('#prev-body').html(s.body || '—');
   if (s.error) {
     $('#prev-error').text('Resolution error: ' + s.error).show();
   } else {
@@ -58,7 +58,7 @@ $(document).on('click', '#previewBtn', function() {
     data: {
       csrfmiddlewaretoken: $('input[name=csrfmiddlewaretoken]').val(),
       subject:      $('#id_subject').val() || '',
-      body:         $('#id_body').val()    || '',
+      body:         getBodyContent()       || '',
       filters:      JSON.stringify(collectFilters()),
       dedupe_users: JSON.stringify(getDedupeUsers()),
     },
@@ -212,7 +212,7 @@ function fetchPreviewPage(page) {
       alloc_status: $('#f_status').val()     || [],
       roles:        $('#f_role').val()       || [],
       subject:      $('#id_subject').val()   || '',
-      body:         $('#id_body').val()      || '',
+      body:         getBodyContent()         || '',
       dedupe_users: JSON.stringify(getDedupeUsers()),
       preview:      'true',
       page:         PREVIEW_PAGE,
diff --git a/coldfront_notifications/static/coldfront_notifications/js/compose_templates.js b/coldfront_notifications/static/coldfront_notifications/js/compose_templates.js
index fcbb648..32e9a74 100644
--- a/coldfront_notifications/static/coldfront_notifications/js/compose_templates.js
+++ b/coldfront_notifications/static/coldfront_notifications/js/compose_templates.js
@@ -40,7 +40,7 @@ function renderVars(vars, filter) {
 // ── Extract tokens from subject + body ───────────────────────────
 var TOKEN_RE = /\{\{(\w+)\}\}/g;
 function extractTokens() {
-  var text = ($('#id_subject').val() || '') + '\n' + ($('#id_body').val() || '');
+  var text = ($('#id_subject').val() || '') + '\n' + (getBodyContent() || '');
   var seen = {};
   var out = [];
   var m;
@@ -66,7 +66,7 @@ function loadTemplate($el) {
   if (!id) { ACTIVE_VARS = []; renderVars([], ''); return; }
   $.getJSON(URLS.templateJson.replace('{id}', id), function(t) {
     $('#id_subject').val(t.subject || '');
-    $('#id_body').val(t.body || '');
+    setBodyContent(t.body || '');
     ACTIVE_VARS = t.variables || [];
     renderVars(ACTIVE_VARS, '');
     if (HAS_VALIDATED) runValidation();
@@ -99,10 +99,16 @@ $(document).on('input', '#varSearch', function() {
 
 // ── Variable insert at cursor ────────────────────────────────────
 $(document).on('click', '.var-btn', function() {
-  var ta    = document.getElementById('id_body');
   var token = $(this).attr('data-var');
-  var s = ta.selectionStart, e = ta.selectionEnd;
-  ta.value = ta.value.substring(0, s) + token + ta.value.substring(e);
-  ta.selectionStart = ta.selectionEnd = s + token.length;
-  ta.focus();
+  var editor = typeof tinymce !== 'undefined' && tinymce.get('id_body');
+  if (editor) {
+    editor.insertContent(token);
+    editor.focus();
+  } else {
+    var ta = document.getElementById('id_body');
+    var s = ta.selectionStart, e = ta.selectionEnd;
+    ta.value = ta.value.substring(0, s) + token + ta.value.substring(e);
+    ta.selectionStart = ta.selectionEnd = s + token.length;
+    ta.focus();
+  }
 });
diff --git a/coldfront_notifications/static/coldfront_notifications/js/compose_validate.js b/coldfront_notifications/static/coldfront_notifications/js/compose_validate.js
index 48f1947..c0bac51 100644
--- a/coldfront_notifications/static/coldfront_notifications/js/compose_validate.js
+++ b/coldfront_notifications/static/coldfront_notifications/js/compose_validate.js
@@ -67,7 +67,7 @@ function runValidation() {
     data: {
       csrfmiddlewaretoken: $('input[name=csrfmiddlewaretoken]').val(),
       subject: $('#id_subject').val() || '',
-      body: $('#id_body').val() || '',
+      body: getBodyContent() || '',
       filters: JSON.stringify(collectFilters()),
       dedupe_users: JSON.stringify(getDedupeUsers()),
     },
diff --git a/coldfront_notifications/static/coldfront_notifications/js/datatables_init.js b/coldfront_notifications/static/coldfront_notifications/js/datatables_init.js
index 269b49b..858e1f6 100644
--- a/coldfront_notifications/static/coldfront_notifications/js/datatables_init.js
+++ b/coldfront_notifications/static/coldfront_notifications/js/datatables_init.js
@@ -4,9 +4,26 @@
 //   DT_CONFIG - object with: { tableId, order, nonOrderableTargets, paging, searching, info }
 // All fields except tableId are optional.
 
+// Fix DataTables "Show entries" dropdown and search layout
+$('");const y=r(e),u=c(e),v='
+
 
 {% endblock %}
diff --git a/coldfront_notifications/templates/coldfront_notifications/campaign_list.html b/coldfront_notifications/templates/coldfront_notifications/campaign_list.html
index 7ec4ee4..2be6cd2 100644
--- a/coldfront_notifications/templates/coldfront_notifications/campaign_list.html
+++ b/coldfront_notifications/templates/coldfront_notifications/campaign_list.html
@@ -26,46 +26,39 @@ 

All Notifications

-
- - - - - - - - - - {% for c in campaigns %} - - - - - - - - - - - - {% empty %} - - {% endfor %} - -
#SubjectTemplateRecipientsDeliveredFailedSent AtStatus
{{ c.pk|stringformat:"03d" }}{{ c.display_subject }}{{ c.template.slug|default:"—" }}{{ c.recipient_count }}{{ c.delivered_count }}{{ c.failed_count|default:"—" }}{{ c.sent_at|date:"Y-m-d H:i"|default:"—" }} - {% if c.status == 'sent' %}Sent - {% elif c.status == 'sending' %}Sending… - {% elif c.status == 'partial' %}Partial - {% elif c.status == 'failed' %}Failed - {% elif c.status == 'draft' %}Draft - {% else %}{{ c.status }}{% endif %} - - {% if c.status == 'draft' %} - - - {% endif %} -
No notifications yet. Send one →
-
+ +
+ + + + + + + + + + {% for c in campaigns %} + + + + + + + + + {% include "coldfront_notifications/_status_badge_td.html" with campaign=c %} + + + {% empty %} + + {% endfor %} + +
#SubjectTemplateRecipientsDeliveredFailedSent AtStatus
{{ c.pk|stringformat:"03d" }}{{ c.display_subject }}{{ c.template.slug|default:"—" }}{{ c.recipient_count }}{{ c.delivered_count }}{{ c.failed_count|default:"—" }}{{ c.sent_at|date:"Y-m-d H:i"|default:"—" }} + {% if c.status == 'draft' %} + + + {% endif %} +
No notifications yet. Send one →
{% endblock %} {% block javascript %} diff --git a/coldfront_notifications/templates/coldfront_notifications/compose.html b/coldfront_notifications/templates/coldfront_notifications/compose.html index 033c091..4ee502c 100644 --- a/coldfront_notifications/templates/coldfront_notifications/compose.html +++ b/coldfront_notifications/templates/coldfront_notifications/compose.html @@ -282,124 +282,8 @@

- - - - - +{% include "coldfront_notifications/_email_preview_modal.html" %} +{% include "coldfront_notifications/_recipient_preview_modal.html" with show_dedupe=True info_text="Users with multiple emails show a dedupe checkbox — tick it to send them only their first email." %} {% endblock %} @@ -419,6 +303,8 @@

draftSave: '{% url "notifications:api-draft-save" %}' }; + + diff --git a/coldfront_notifications/templates/coldfront_notifications/dashboard.html b/coldfront_notifications/templates/coldfront_notifications/dashboard.html index 13afc83..89f9e3c 100644 --- a/coldfront_notifications/templates/coldfront_notifications/dashboard.html +++ b/coldfront_notifications/templates/coldfront_notifications/dashboard.html @@ -11,38 +11,10 @@

Notifications Dashboard

-
-
-
-
Total Delivered
-
{{ total_sent|default:"0" }}
-
-
-
-
-
-
-
Sending Now
-
{{ sending_now|default:"0" }}
-
-
-
-
-
-
-
Failed (30d)
-
{{ total_failed_30|default:"0" }}
-
-
-
-
-
-
-
Total Notifications
-
{{ total_campaigns|default:"0" }}
-
-
-
+ {% include "coldfront_notifications/_stat_card.html" with label="Total Delivered" value=total_sent|default:"0" color="#A51C30" %} + {% include "coldfront_notifications/_stat_card.html" with label="Sending Now" value=sending_now|default:"0" color="#28a745" value_color="#28a745" %} + {% include "coldfront_notifications/_stat_card.html" with label="Failed (30d)" value=total_failed_30|default:"0" color="#ffc107" value_color="#e67e22" %} + {% include "coldfront_notifications/_stat_card.html" with label="Total Notifications" value=total_campaigns|default:"0" color="#17a2b8" %}
@@ -92,21 +64,7 @@

{{ campaign.sender }} {{ campaign.recipient_count }} {{ campaign.sent_at|date:"Y-m-d H:i"|default:"—" }} - - {% if campaign.status == 'sent' %} - Sent - {% elif campaign.status == 'sending' %} - Sending… - {% elif campaign.status == 'partial' %} - Partial - {% elif campaign.status == 'failed' %} - Failed - {% elif campaign.status == 'draft' %} - Draft - {% else %} - {{ campaign.status }} - {% endif %} - + {% include "coldfront_notifications/_status_badge_td.html" with campaign=campaign %} {% empty %} diff --git a/coldfront_notifications/templates/coldfront_notifications/draft_delete_confirm.html b/coldfront_notifications/templates/coldfront_notifications/draft_delete_confirm.html index 01aa3da..5c754b0 100644 --- a/coldfront_notifications/templates/coldfront_notifications/draft_delete_confirm.html +++ b/coldfront_notifications/templates/coldfront_notifications/draft_delete_confirm.html @@ -1,52 +1,32 @@ -{% extends "common/base.html" %} -{% block title %}Delete Draft | FAS Research Computing{% endblock %} -{% block content %} -
-

Delete Draft

-
-
+{% extends "coldfront_notifications/_delete_confirm_base.html" %} -
-
-
-
-
Confirm Deletion
-
-
-

Are you sure you want to delete this draft notification?

+{% block delete_title %}Delete Draft{% endblock %} +{% block delete_heading %}Delete Draft{% endblock %} - - - {% if campaign.template %} - - {% endif %} - - {% if campaign.filter_summary %} - - {% endif %} -
Subject{{ campaign.subject|default:"(no subject)" }}
Template{{ campaign.template.slug }}
Created{{ campaign.created_at|date:"Y-m-d H:i" }}
Filters - {% for badge in campaign.filter_summary %} - {{ badge }} - {% endfor %} -
+{% block confirm_message %} +

Are you sure you want to delete this draft notification?

+{% endblock %} -

- This draft has never been sent. Deleting it is permanent and cannot be undone. -

+{% block details_rows %} +Subject{{ campaign.subject|default:"(no subject)" }} +{% if campaign.template %} +Template{{ campaign.template.slug }} +{% endif %} +Created{{ campaign.created_at|date:"Y-m-d H:i" }} +{% if campaign.filter_summary %} +Filters + {% for badge in campaign.filter_summary %} + {{ badge }} + {% endfor %} + +{% endif %} +{% endblock %} -
- {% csrf_token %} -
- - Go Back - - -
-
-
-
-
-
+{% block explanation %} +

+ This draft has never been sent. Deleting it is permanent and cannot be undone. +

{% endblock %} + +{% block cancel_url %}{% url 'notifications:campaign-list' %}{% endblock %} +{% block delete_button_label %}Delete Draft{% endblock %} diff --git a/coldfront_notifications/templates/coldfront_notifications/resend_compose.html b/coldfront_notifications/templates/coldfront_notifications/resend_compose.html index dd21cd2..eda9dbd 100644 --- a/coldfront_notifications/templates/coldfront_notifications/resend_compose.html +++ b/coldfront_notifications/templates/coldfront_notifications/resend_compose.html @@ -160,114 +160,14 @@

Review & Edit

- - - - - +{% include "coldfront_notifications/_email_preview_modal.html" %} +{% include "coldfront_notifications/_recipient_preview_modal.html" with show_dedupe=False info_text="Recipients that will receive this resend." %} {% endblock %} {% block javascript %} + + + + {% endblock %} \ No newline at end of file diff --git a/coldfront_notifications/templates/coldfront_notifications/template_list.html b/coldfront_notifications/templates/coldfront_notifications/template_list.html index 8a882cc..d096f1c 100644 --- a/coldfront_notifications/templates/coldfront_notifications/template_list.html +++ b/coldfront_notifications/templates/coldfront_notifications/template_list.html @@ -13,32 +13,30 @@

Templates


-
-
- - - - - - {% for t in templates %} - - - - - - - - - {% empty %} - - {% endfor %} - -
NameSlugSubjectUpdatedUsed
{{ t.name }}{{ t.slug }}{{ t.subject }}{{ t.updated_at|date:"Y-m-d" }}{{ t.use_count }}× - - - -
No templates yet.
-
+
+ + + + + + {% for t in templates %} + + + + + + + + + {% empty %} + + {% endfor %} + +
NameSlugSubjectUpdatedUsed
{{ t.name }}{{ t.slug }}{{ t.subject }}{{ t.updated_at|date:"Y-m-d" }}{{ t.use_count }}× + + + +
No templates yet.
{% endblock %} {% block javascript %} diff --git a/coldfront_notifications/templates/coldfront_notifications/variable_delete_confirm.html b/coldfront_notifications/templates/coldfront_notifications/variable_delete_confirm.html index de6b77b..3c7df82 100644 --- a/coldfront_notifications/templates/coldfront_notifications/variable_delete_confirm.html +++ b/coldfront_notifications/templates/coldfront_notifications/variable_delete_confirm.html @@ -1,63 +1,48 @@ -{% extends "common/base.html" %} -{% block title %}Delete Variable | FAS Research Computing{% endblock %} -{% block content %} -
-

Delete Variable

-
-
+{% extends "coldfront_notifications/_delete_confirm_base.html" %} -
-
-
-
-
Confirm Deletion
-
-
-

Are you sure you want to delete the variable {% templatetag openvariable %}{{ variable.key }}{% templatetag closevariable %}?

+{% block delete_title %}Delete Variable{% endblock %} +{% block delete_heading %}Delete Variable{% endblock %} - - - - {% if variable.resolver_key %} - - {% endif %} -
Label{{ variable.label }}
Source{{ variable.get_source_display }}
Query{{ variable.resolver_key }}
+{% block confirm_message %} +

Are you sure you want to delete the variable {% templatetag openvariable %}{{ variable.key }}{% templatetag closevariable %}?

+{% endblock %} - {% if affected_templates %} -
- - {{ affected_templates|length }} template{{ affected_templates|length|pluralize }} use{{ affected_templates|length|pluralize:"s," }} this variable: -
    - {% for t in affected_templates %} -
  • - {{ t.name }} - ({{ t.slug }}) -
  • - {% endfor %} -
-
-

- These templates will fail validation until the {% templatetag openvariable %}{{ variable.key }}{% templatetag closevariable %} token is removed from their subject/body. -

- {% else %} -

- No active templates reference this variable. -

- {% endif %} +{% block details_rows %} +Label{{ variable.label }} +Source{{ variable.get_source_display }} +{% if variable.resolver_key %} +Query{{ variable.resolver_key }} +{% endif %} +{% endblock %} -
- {% csrf_token %} -
- - Go Back - - -
-
-
-
-
+{% block warning %} +{% if affected_templates %} +
+ + {{ affected_templates|length }} template{{ affected_templates|length|pluralize }} use{{ affected_templates|length|pluralize:"s," }} this variable: +
    + {% for t in affected_templates %} +
  • + {{ t.name }} + ({{ t.slug }}) +
  • + {% endfor %} +
+{% endif %} {% endblock %} + +{% block explanation %} +{% if affected_templates %} +

+ These templates will fail validation until the {% templatetag openvariable %}{{ variable.key }}{% templatetag closevariable %} token is removed from their subject/body. +

+{% else %} +

+ No active templates reference this variable. +

+{% endif %} +{% endblock %} + +{% block cancel_url %}{% url 'notifications:variable-list' %}{% endblock %} +{% block delete_button_label %}Delete Variable{% endblock %} diff --git a/coldfront_notifications/templates/coldfront_notifications/variable_form.html b/coldfront_notifications/templates/coldfront_notifications/variable_form.html index b810933..5cc4351 100644 --- a/coldfront_notifications/templates/coldfront_notifications/variable_form.html +++ b/coldfront_notifications/templates/coldfront_notifications/variable_form.html @@ -163,24 +163,8 @@

{% if variable %} -
-
-
Danger Zone
-
-
-

- Deleting {% templatetag openvariable %}{{ variable.key }}{% templatetag closevariable %} will cause templates that use it - to fail pre-flight validation. -

-
- {% csrf_token %} - -
-
-
+ {% url 'notifications:variable-delete' variable.pk as dz_url %} + {% include "coldfront_notifications/_danger_zone_card.html" with message="Deleting this variable will cause templates that use it to fail pre-flight validation." delete_url=dz_url delete_label="Delete Variable" %} {% endif %}
diff --git a/coldfront_notifications/templates/coldfront_notifications/variable_list.html b/coldfront_notifications/templates/coldfront_notifications/variable_list.html index 5897519..bd32d34 100644 --- a/coldfront_notifications/templates/coldfront_notifications/variable_list.html +++ b/coldfront_notifications/templates/coldfront_notifications/variable_list.html @@ -19,62 +19,60 @@

Template Variables

-
-
- - - - - - - - - - - - - - {% for v in variables %} - - - - - - - - - - {% empty %} - - {% endfor %} - -
TokenLabelSourceQuery / WidgetExampleRequired
{% templatetag openvariable %}{{ v.key }}{% templatetag closevariable %}{{ v.label }} - {% if v.description %}
{{ v.description }}{% endif %} -
- {% if v.source == "query" %} - Query - {% else %} - Manual - {% endif %} - - {% if v.source == "query" %} - {{ v.resolver_key }} - {% else %} - {{ v.get_input_widget_display }} - {% endif %} - {{ v.example|default:"—" }} - {% if v.is_required %}{% else %}—{% endif %} - - - - - - - -
No variables defined yet. - Create the first one → -
-
+
+ + + + + + + + + + + + + + {% for v in variables %} + + + + + + + + + + {% empty %} + + {% endfor %} + +
TokenLabelSourceQuery / WidgetExampleRequired
{% templatetag openvariable %}{{ v.key }}{% templatetag closevariable %}{{ v.label }} + {% if v.description %}
{{ v.description }}{% endif %} +
+ {% if v.source == "query" %} + Query + {% else %} + Manual + {% endif %} + + {% if v.source == "query" %} + {{ v.resolver_key }} + {% else %} + {{ v.get_input_widget_display }} + {% endif %} + {{ v.example|default:"—" }} + {% if v.is_required %}{% else %}—{% endif %} + + + + + + + +
No variables defined yet. + Create the first one → +
{% endblock %} {% block javascript %} From d5475612e47c4d6c2015146c54451f0a484d368b Mon Sep 17 00:00:00 2001 From: Antonio Rodriguez Date: Thu, 9 Jul 2026 20:38:14 +0200 Subject: [PATCH 2/2] Fix flake8: restore EmailMessage import for type hint --- coldfront_notifications/campaign_sender.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coldfront_notifications/campaign_sender.py b/coldfront_notifications/campaign_sender.py index a84dc9b..b653a67 100644 --- a/coldfront_notifications/campaign_sender.py +++ b/coldfront_notifications/campaign_sender.py @@ -13,7 +13,7 @@ import time from django.conf import settings -from django.core.mail import EmailMultiAlternatives, get_connection +from django.core.mail import EmailMessage, EmailMultiAlternatives, get_connection from django.utils import timezone from django.utils.html import strip_tags