Template
Add campaign UTM links and piha.li shortener (#4)
## Summary - Port Monica campaign UTM + piha.li minting into always-on `core` (`shortener.py`, `campaign_utm.py`) so `email_sms` and `directmail` stay optional and never import each other. - `utm_source` is a slug of `SITE_NAME` (override with `UTM_SOURCE`). Email gets an HTML `data-campaign-utm` link; SMS gets a plain URL; postcard QR only — no body inject. - Live composer mints through login+CSRF `POST /portal/campaigns/short-link/` (registered only when an outreach app is installed). Empty `SHORTENER_*` falls back to the long UTM URL. Reference: [monica_site PR #10](ai_ml_operations/monica_site#10) Closes #3 ## Test plan - [x] `cd site && uv run python manage.py test` (125 tests) - [ ] Email composer: type a name, confirm HTML link + `utm_campaign` updates, save draft - [ ] SMS composer: plain `piha.li` (or long UTM if shortener unset) in the body - [ ] Postcard composer: QR copies the tracked URL; campaign body has no `utm_source` - [ ] Campaign report pages show the same panel - [ ] With `FEATURE_EMAIL_SMS` and `FEATURE_DIRECT_MAIL` off, no extra nav and no `/portal/campaigns/short-link/` route Reviewed-on: #4
This commit was merged in pull request #4.
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 %}
|
||||
@@ -63,6 +64,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>
|
||||
@@ -102,6 +104,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 %}
|
||||
<button class="btn btn-primary" type="submit">Save draft</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -153,6 +156,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 }}";
|
||||
@@ -160,11 +165,38 @@
|
||||
var bodyField = document.getElementById('id_body');
|
||||
var smsField = document.getElementById('id_body_sms');
|
||||
var quill = null;
|
||||
var utmLock = false;
|
||||
var utmLinkLabel = "{{ utm_link_label|escapejs }}";
|
||||
var utmSource = "{{ utm_source|escapejs }}";
|
||||
|
||||
function csrfHeader() {
|
||||
return { 'X-CSRFToken': csrfToken };
|
||||
}
|
||||
|
||||
function audienceChannel() {
|
||||
var audience = (document.getElementById('id_audience') || {}).value || '';
|
||||
if (audience === 'sms_opt_in') return 'sms';
|
||||
if (audience === 'postcard_opt_in') return 'postcard';
|
||||
return 'email';
|
||||
}
|
||||
|
||||
function applyUtmToBodies(url, channel, label) {
|
||||
if (utmLock) return;
|
||||
utmLock = true;
|
||||
try {
|
||||
if (channel === 'email' && quill) {
|
||||
window.CampaignUtm.ensureQuillLink(quill, url, label || utmLinkLabel, utmSource);
|
||||
syncBodyFromQuill();
|
||||
} else if (channel === 'sms' && smsField) {
|
||||
smsField.value = window.CampaignUtm.replaceTextUrl(smsField.value, url, utmSource);
|
||||
if (bodyField) bodyField.value = smsField.value;
|
||||
syncCampaignPreview();
|
||||
}
|
||||
} finally {
|
||||
utmLock = false;
|
||||
}
|
||||
}
|
||||
|
||||
function syncBodyFromQuill() {
|
||||
if (!quill || !bodyField) return;
|
||||
var html = quill.root.innerHTML;
|
||||
@@ -188,6 +220,7 @@
|
||||
var subject = (document.getElementById('id_subject') || {}).value || '';
|
||||
var audience = (document.getElementById('id_audience') || {}).value || '';
|
||||
var isEmail = audience === 'email_opt_in';
|
||||
var isSms = audience === 'sms_opt_in';
|
||||
var isPostcard = audience === 'postcard_opt_in';
|
||||
var body = '';
|
||||
if (isEmail && quill) {
|
||||
@@ -218,6 +251,9 @@
|
||||
if (isEmail) {
|
||||
bodyEl.style.whiteSpace = 'normal';
|
||||
bodyEl.innerHTML = body;
|
||||
} else if (isSms) {
|
||||
bodyEl.style.whiteSpace = 'pre-wrap';
|
||||
bodyEl.innerHTML = window.CampaignUtm.linkify(body);
|
||||
} else {
|
||||
bodyEl.style.whiteSpace = 'pre-wrap';
|
||||
bodyEl.textContent = body;
|
||||
@@ -257,6 +293,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();
|
||||
};
|
||||
|
||||
@@ -315,7 +353,10 @@
|
||||
} else if (initial) {
|
||||
quill.setText(initial);
|
||||
}
|
||||
quill.on('text-change', syncBodyFromQuill);
|
||||
quill.on('text-change', function () {
|
||||
if (utmLock) return;
|
||||
syncBodyFromQuill();
|
||||
});
|
||||
document.getElementById('campaign-compose').addEventListener('submit', function () {
|
||||
var audience = (document.getElementById('id_audience') || {}).value || '';
|
||||
if (audience === 'email_opt_in') syncBodyFromQuill();
|
||||
@@ -344,6 +385,14 @@
|
||||
if (tmplSelect) tmplSelect.addEventListener('change', syncCampaignPreview);
|
||||
|
||||
initQuill();
|
||||
window.CampaignUtm.bindPanel(document.getElementById('utm-link-panel'), {
|
||||
getName: function () {
|
||||
return (document.getElementById('id_name') || {}).value || '';
|
||||
},
|
||||
getMedium: audienceChannel,
|
||||
csrfToken: csrfToken,
|
||||
onUrlChange: applyUtmToBodies
|
||||
});
|
||||
syncComposeChannel();
|
||||
})();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user