generated from westfarn/web_django_template
Add campaign UTM links and piha.li shortener as a core helper.
Keep mint/UTM in always-on core so email_sms and directmail stay optional, and brand utm_source from SITE_NAME (or UTM_SOURCE) instead of a hardcoded client name. Closes westfarn/web_django_template#3
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
{% extends "portal_base.html" %}
|
||||
{% load static %}
|
||||
{% block title %}{{ campaign.name }} · Campaign{% endblock %}
|
||||
{% block topbar_title %}{{ campaign.name }}{% endblock %}
|
||||
{% block portal_content %}
|
||||
@@ -51,6 +52,13 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="panel" style="margin-bottom:16px">
|
||||
<div class="panel-h"><h2>Tracked site link</h2></div>
|
||||
<div class="panel-b">
|
||||
{% include "core/_utm_link_panel.html" with utm_live=False utm_medium=campaign.channel utm_campaign_name=campaign.name %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-row" id="campaign-stats">
|
||||
<div class="stat-card">
|
||||
<div class="label">Messages</div>
|
||||
@@ -208,7 +216,10 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block extra_js %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.4/build/qrcode.min.js"></script>
|
||||
<script src="{% static 'js/campaign-utm.js' %}"></script>
|
||||
<script>
|
||||
window.CampaignUtm.bindPanel(document.getElementById("utm-link-panel"));
|
||||
(function () {
|
||||
var panel = document.getElementById("recipients-panel");
|
||||
var page = (panel && panel.getAttribute("data-page")) || "1";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends "portal_base.html" %}
|
||||
{% load static %}
|
||||
{% block title %}Campaigns · Portal{% endblock %}
|
||||
{% block topbar_title %}Campaign composer{% endblock %}
|
||||
{% block extra_head %}
|
||||
@@ -64,6 +65,7 @@
|
||||
<label for="id_name">Campaign name</label>
|
||||
<input id="id_name" name="name" type="text" required
|
||||
placeholder="Spring seller tips" value="{{ form_data.name }}">
|
||||
<div class="hint">Used as <code>utm_campaign</code> on the tracked site link.</div>
|
||||
</div>
|
||||
<div class="field" id="subject-field">
|
||||
<label for="id_subject">Subject</label>
|
||||
@@ -128,6 +130,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint-block">Saves a draft campaign and recipient stubs. Send from the campaign report when ready. You’ll get an email when the send finishes.</p>
|
||||
{% include "core/_utm_link_panel.html" with utm_live=True utm_medium="postcard" %}
|
||||
<button class="btn btn-primary" type="submit">Save draft</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -179,6 +182,8 @@
|
||||
{% endblock %}
|
||||
{% block extra_js %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.4/build/qrcode.min.js"></script>
|
||||
<script src="{% static 'js/campaign-utm.js' %}"></script>
|
||||
<script>
|
||||
(function () {
|
||||
var uploadUrl = "{{ image_upload_url|escapejs }}";
|
||||
@@ -283,6 +288,8 @@
|
||||
var active = (ch === 'email' && isEmail) || (ch === 'sms' && isSms) || (ch === 'postcard' && isPostcard);
|
||||
a.classList.toggle('active', active);
|
||||
});
|
||||
var panel = document.getElementById('utm-link-panel');
|
||||
if (panel && panel._utmApply) panel._utmApply();
|
||||
syncCampaignPreview();
|
||||
};
|
||||
|
||||
@@ -370,6 +377,13 @@
|
||||
if (tmplSelect) tmplSelect.addEventListener('change', syncCampaignPreview);
|
||||
|
||||
initQuill();
|
||||
window.CampaignUtm.bindPanel(document.getElementById('utm-link-panel'), {
|
||||
getName: function () {
|
||||
return (document.getElementById('id_name') || {}).value || '';
|
||||
},
|
||||
getMedium: function () { return 'postcard'; },
|
||||
csrfToken: csrfToken
|
||||
});
|
||||
syncComposeChannel();
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -279,3 +279,65 @@ class PostcardDesignPickTests(TestCase):
|
||||
self.assertEqual(campaign.messages.count(), 1)
|
||||
|
||||
|
||||
class PostcardUtmPanelTests(TestCase):
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
self.user = User.objects.create_user(
|
||||
username="utm-mail", password="test-pass-123"
|
||||
)
|
||||
self.client = Client()
|
||||
self.client.login(username="utm-mail", password="test-pass-123")
|
||||
self.contact = Contact.objects.create(
|
||||
email="mail@example.com",
|
||||
first_name="Pat",
|
||||
postal_address=Contact.make_postal_address(line1="9 Main"),
|
||||
)
|
||||
|
||||
def test_composer_shows_tracked_link_panel(self):
|
||||
with patch(
|
||||
"directmail.views._fetch_pcm_designs",
|
||||
return_value=([], ""),
|
||||
):
|
||||
url = reverse("directmail:campaign_list")
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Tracked site link")
|
||||
self.assertContains(response, "campaign-utm.js")
|
||||
self.assertContains(response, "qrcode.min.js")
|
||||
self.assertContains(response, "data-utm-qr")
|
||||
self.assertContains(response, reverse("campaign_short_link"))
|
||||
|
||||
def test_postcard_does_not_inject_into_body(self):
|
||||
campaign = create_campaign_draft(
|
||||
name="March mailer",
|
||||
audience=Campaign.Audience.POSTCARD_OPT_IN,
|
||||
body="Internal note only",
|
||||
created_by=self.user,
|
||||
)
|
||||
self.assertEqual(campaign.body_override, "Internal note only")
|
||||
self.assertNotIn("utm_source", campaign.body_override)
|
||||
|
||||
@override_settings(
|
||||
PUBLIC_SITE_URL="https://acmehvac.com",
|
||||
SITE_NAME="Acme HVAC",
|
||||
UTM_SOURCE="",
|
||||
SHORTENER_BASE_URL="",
|
||||
SHORTENER_API_TOKEN="",
|
||||
)
|
||||
def test_detail_shows_tracked_url(self):
|
||||
campaign = create_campaign_draft(
|
||||
name="March mailer",
|
||||
audience=Campaign.Audience.POSTCARD_OPT_IN,
|
||||
body="Internal note only",
|
||||
created_by=self.user,
|
||||
)
|
||||
url = reverse("directmail:campaign_detail", kwargs={"pk": campaign.pk})
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "utm_campaign=march-mailer")
|
||||
self.assertContains(response, "utm_medium=postcard")
|
||||
self.assertContains(response, "qrcode.min.js")
|
||||
self.assertNotContains(response, "Internal note onlyutm")
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,11 @@ urlpatterns = [
|
||||
name="campaign_status_json",
|
||||
),
|
||||
path("campaigns/<uuid:pk>/send/", views.campaign_send, name="campaign_send"),
|
||||
path(
|
||||
"campaigns/upload-image/",
|
||||
views.campaign_image_upload,
|
||||
name="campaign_image_upload",
|
||||
),
|
||||
path(
|
||||
"campaigns/<uuid:pk>/messages/<uuid:message_id>/remove/",
|
||||
views.campaign_message_remove,
|
||||
|
||||
@@ -27,6 +27,7 @@ from directmail.providers.postcard.pcm import (
|
||||
list_designs,
|
||||
)
|
||||
from contacts.consent import opted_in_contacts
|
||||
from core.campaign_utm import utm_panel_context
|
||||
from core.scheduling import parse_scheduled_for
|
||||
from directmail.services import (
|
||||
create_campaign_draft,
|
||||
@@ -58,8 +59,6 @@ _MAX_IMAGE_BYTES = 5 * 1024 * 1024
|
||||
def _audience_choices() -> list[tuple[str, str]]:
|
||||
"""Labeled audience options with live opted-in counts."""
|
||||
rows = [
|
||||
(Campaign.Audience.POSTCARD_OPT_IN, Channel.EMAIL, "email"),
|
||||
(Campaign.Audience.SMS_OPT_IN, Channel.SMS, "SMS"),
|
||||
(Campaign.Audience.POSTCARD_OPT_IN, Channel.POSTCARD, "postcard"),
|
||||
]
|
||||
choices = []
|
||||
@@ -453,21 +452,13 @@ def campaign_list(request):
|
||||
form_errors.append("Campaign name is required.")
|
||||
if audience not in Campaign.Audience.values:
|
||||
form_errors.append("Choose a recipient list.")
|
||||
if audience == Campaign.Audience.POSTCARD_OPT_IN:
|
||||
elif audience == Campaign.Audience.POSTCARD_OPT_IN:
|
||||
if not template or template.channel != Channel.POSTCARD:
|
||||
form_errors.append(
|
||||
"Choose a postcard design (create one under Postcard design)."
|
||||
)
|
||||
if not body:
|
||||
body = "Postcard mailing"
|
||||
else:
|
||||
if not body:
|
||||
form_errors.append("Body is required.")
|
||||
if (
|
||||
audience == Campaign.Audience.POSTCARD_OPT_IN
|
||||
and not subject
|
||||
):
|
||||
form_errors.append("Subject is required for email campaigns.")
|
||||
|
||||
scheduled_for = None
|
||||
try:
|
||||
@@ -505,6 +496,7 @@ def campaign_list(request):
|
||||
"form_data": form_data,
|
||||
"form_errors": form_errors,
|
||||
"image_upload_url": reverse("directmail:campaign_image_upload"),
|
||||
**utm_panel_context(live=True, medium=Channel.POSTCARD),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -528,6 +520,7 @@ def campaign_detail(request, pk):
|
||||
"recent_events": ctx["recent_events"],
|
||||
"events_title": ctx["events_title"],
|
||||
"events_empty": ctx["events_empty"],
|
||||
**utm_panel_context(campaign=campaign, live=False),
|
||||
"can_send": campaign.status
|
||||
in {
|
||||
Campaign.Status.DRAFT,
|
||||
|
||||
Reference in New Issue
Block a user