wagtail-newsletter-django-backend 0.5.9__tar.gz → 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/PKG-INFO +3 -3
- {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/pyproject.toml +2 -2
- {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/src/wagtail_newsletter_django_backend/__init__.py +1 -1
- {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/src/wagtail_newsletter_django_backend/admin_viewsets.py +2 -2
- {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/src/wagtail_newsletter_django_backend/campaign_backend.py +32 -36
- wagtail_newsletter_django_backend-0.6.0/src/wagtail_newsletter_django_backend/migrations/0004_remove_audience_from_email_address_and_more.py +39 -0
- {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/src/wagtail_newsletter_django_backend/models.py +37 -64
- wagtail_newsletter_django_backend-0.5.9/.gitignore +0 -165
- wagtail_newsletter_django_backend-0.5.9/.readthedocs.yaml +0 -22
- wagtail_newsletter_django_backend-0.5.9/demo/manage.py +0 -22
- wagtail_newsletter_django_backend-0.5.9/demo/pyproject.toml +0 -35
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/__init__.py +0 -4
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/blog/apps.py +0 -11
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/blog/migrations/0001_initial.py +0 -80
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/blog/migrations/0002_newslettersignup.py +0 -45
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/blog/models.py +0 -62
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/blog/signals.py +0 -13
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/blog/templates/blog/blog_index_page.html +0 -19
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/blog/templates/blog/blog_page.html +0 -17
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/blog/templates/blog/blog_page_newsletter.html +0 -19
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/blog/templates/blog/newsletter_signup.html +0 -21
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/blog/views.py +0 -3
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/home/apps.py +0 -6
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/home/migrations/0001_initial.py +0 -36
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/home/models.py +0 -7
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/home/static/css/welcome_page.css +0 -184
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/home/templates/home/home_page.html +0 -21
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/home/templates/home/welcome_page.html +0 -52
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/home/tests.py +0 -43
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/search/__init__.py +0 -0
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/search/apps.py +0 -5
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/search/templates/search/search.html +0 -38
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/search/views.py +0 -46
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/settings/__init__.py +0 -0
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/settings/base.py +0 -195
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/settings/dev.py +0 -24
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/settings/production.py +0 -14
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/static/css/demo.css +0 -0
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/static/js/demo.js +0 -0
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/templates/404.html +0 -11
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/templates/500.html +0 -13
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/templates/base.html +0 -54
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/urls.py +0 -36
- wagtail_newsletter_django_backend-0.5.9/demo/src/demo/wsgi.py +0 -16
- wagtail_newsletter_django_backend-0.5.9/demo/uv.lock +0 -870
- wagtail_newsletter_django_backend-0.5.9/docs/Makefile +0 -20
- wagtail_newsletter_django_backend-0.5.9/docs/conf.py +0 -27
- wagtail_newsletter_django_backend-0.5.9/docs/index.rst +0 -19
- wagtail_newsletter_django_backend-0.5.9/docs/make.bat +0 -35
- wagtail_newsletter_django_backend-0.5.9/src/wagtail_newsletter_django_backend/admin.py +0 -3
- wagtail_newsletter_django_backend-0.5.9/src/wagtail_newsletter_django_backend/management/__init__.py +0 -0
- wagtail_newsletter_django_backend-0.5.9/src/wagtail_newsletter_django_backend/management/commands/__init__.py +0 -0
- wagtail_newsletter_django_backend-0.5.9/src/wagtail_newsletter_django_backend/migrations/__init__.py +0 -0
- wagtail_newsletter_django_backend-0.5.9/src/wagtail_newsletter_django_backend/templatetags/__init__.py +0 -0
- wagtail_newsletter_django_backend-0.5.9/src/wagtail_newsletter_django_backend/tests.py +0 -3
- wagtail_newsletter_django_backend-0.5.9/uv.lock +0 -681
- {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/LICENSE +0 -0
- {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/README.md +0 -0
- {wagtail_newsletter_django_backend-0.5.9/demo/src/demo/blog → wagtail_newsletter_django_backend-0.6.0/src/wagtail_newsletter_django_backend}/admin.py +0 -0
- {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/src/wagtail_newsletter_django_backend/apps.py +0 -0
- {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/src/wagtail_newsletter_django_backend/forms.py +0 -0
- {wagtail_newsletter_django_backend-0.5.9/demo/src/demo/blog → wagtail_newsletter_django_backend-0.6.0/src/wagtail_newsletter_django_backend/management}/__init__.py +0 -0
- {wagtail_newsletter_django_backend-0.5.9/demo/src/demo/blog/migrations → wagtail_newsletter_django_backend-0.6.0/src/wagtail_newsletter_django_backend/management/commands}/__init__.py +0 -0
- {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/src/wagtail_newsletter_django_backend/management/commands/send_scheduled_campaigns.py +0 -0
- {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/src/wagtail_newsletter_django_backend/migrations/0001_initial.py +0 -0
- {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/src/wagtail_newsletter_django_backend/migrations/0002_alter_audience_name_alter_audiencesegment_name_and_more.py +0 -0
- {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/src/wagtail_newsletter_django_backend/migrations/0003_alter_campaign_audience_segment.py +0 -0
- {wagtail_newsletter_django_backend-0.5.9/demo/src/demo/home → wagtail_newsletter_django_backend-0.6.0/src/wagtail_newsletter_django_backend/migrations}/__init__.py +0 -0
- {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/src/wagtail_newsletter_django_backend/templates/wagtail_newsletter_django_backend/manage.html +0 -0
- {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/src/wagtail_newsletter_django_backend/templates/wagtail_newsletter_django_backend/manage_tag.html +0 -0
- {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/src/wagtail_newsletter_django_backend/templates/wagtail_newsletter_django_backend/unsubscribe.html +0 -0
- {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/src/wagtail_newsletter_django_backend/templates/wagtail_newsletter_django_backend/unsubscribe_tag.html +0 -0
- {wagtail_newsletter_django_backend-0.5.9/demo/src/demo/home/migrations → wagtail_newsletter_django_backend-0.6.0/src/wagtail_newsletter_django_backend/templatetags}/__init__.py +0 -0
- {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/src/wagtail_newsletter_django_backend/templatetags/wagtail_newsletter_django_backend_tags.py +0 -0
- {wagtail_newsletter_django_backend-0.5.9/demo/src/demo/blog → wagtail_newsletter_django_backend-0.6.0/src/wagtail_newsletter_django_backend}/tests.py +0 -0
- {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/src/wagtail_newsletter_django_backend/urls.py +0 -0
- {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/src/wagtail_newsletter_django_backend/views.py +0 -0
- {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/src/wagtail_newsletter_django_backend/wagtail_hooks.py +0 -0
{wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/PKG-INFO
RENAMED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wagtail-newsletter-django-backend
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: An internal Django backend to wagtail-newsletter.
|
|
5
5
|
Author-email: "Taylor C. Richberger" <taylor@axfive.net>
|
|
6
6
|
Maintainer-email: "Taylor C. Richberger" <taylor@axfive.net>
|
|
7
7
|
Requires-Python: >= 3.10
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
License-File: LICENSE
|
|
10
|
-
Requires-Dist: Django >=5.2, <
|
|
11
|
-
Requires-Dist: wagtail >=7.1, <7.
|
|
10
|
+
Requires-Dist: Django >=5.2, <6.1
|
|
11
|
+
Requires-Dist: wagtail >=7.1, <7.4
|
|
12
12
|
Requires-Dist: wagtail-newsletter >=0.2.2, <0.2.4
|
|
13
13
|
Requires-Dist: html2text >= 2025.4.15
|
|
14
14
|
Project-URL: documentation, https://wagtail-newsletter-django-backend.readthedocs.io/en/latest/
|
{wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/pyproject.toml
RENAMED
|
@@ -6,8 +6,8 @@ requires-python = '>= 3.10'
|
|
|
6
6
|
license = {text = 'AGPL-3.0-or-later'}
|
|
7
7
|
|
|
8
8
|
dependencies = [
|
|
9
|
-
'Django >=5.2, <
|
|
10
|
-
'wagtail >=7.1, <7.
|
|
9
|
+
'Django >=5.2, <6.1',
|
|
10
|
+
'wagtail >=7.1, <7.4',
|
|
11
11
|
# The wagtail newsletter folks pushed a breaking change with 0.2.4, unfortunately
|
|
12
12
|
"wagtail-newsletter >=0.2.2, <0.2.4",
|
|
13
13
|
"html2text >= 2025.4.15",
|
|
@@ -5,7 +5,7 @@ from .models import Audience, AudienceSegment, Subscriber, Campaign
|
|
|
5
5
|
@final
|
|
6
6
|
class AudienceViewSet(ModelViewSet):
|
|
7
7
|
model = Audience
|
|
8
|
-
form_fields = ['site', 'name', 'description'
|
|
8
|
+
form_fields = ['site', 'name', 'description']
|
|
9
9
|
list_display = ['site', 'name', 'description'] # pyright: ignore[reportAssignmentType]
|
|
10
10
|
icon = 'group'
|
|
11
11
|
add_to_admin_menu = False
|
|
@@ -35,7 +35,7 @@ class SubscriberViewSet(ModelViewSet):
|
|
|
35
35
|
@final
|
|
36
36
|
class CampaignViewSet(ModelViewSet):
|
|
37
37
|
model = Campaign
|
|
38
|
-
form_fields = ['subject', 'audience_segment', 'send_at', 'sent_at', 'html']
|
|
38
|
+
form_fields = ['subject', 'audience_segment', 'send_at', 'sent_at', 'html', 'from_email_address', 'from_email_name']
|
|
39
39
|
list_display = ['subject', 'audience_segment'] # pyright: ignore[reportAssignmentType]
|
|
40
40
|
icon = 'mail'
|
|
41
41
|
add_to_admin_menu = False
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
1
2
|
from datetime import datetime
|
|
2
3
|
from typing import Any, cast, override
|
|
3
4
|
from django.db import transaction
|
|
@@ -8,11 +9,12 @@ from html2text import HTML2Text
|
|
|
8
9
|
from django.core.mail import get_connection
|
|
9
10
|
from django.core.mail.message import EmailMessage, EmailMultiAlternatives
|
|
10
11
|
from wagtail_newsletter import campaign_backends as wncb, audiences as wna, models as wnm
|
|
12
|
+
from wagtail.models import Site
|
|
11
13
|
import re
|
|
12
14
|
from . import models
|
|
13
15
|
|
|
14
|
-
_unsubscribe_re = re.compile(r'\[\[unsubscribe\]\]
|
|
15
|
-
_manage_re = re.compile(r'\[\[manage\]\]
|
|
16
|
+
_unsubscribe_re = re.compile(r'\[\[unsubscribe\]\]')
|
|
17
|
+
_manage_re = re.compile(r'\[\[manage\]\]')
|
|
16
18
|
|
|
17
19
|
h2t = HTML2Text()
|
|
18
20
|
class CampaignBackend(wncb.CampaignBackend):
|
|
@@ -27,7 +29,7 @@ class CampaignBackend(wncb.CampaignBackend):
|
|
|
27
29
|
name=audience.name,
|
|
28
30
|
member_count=audience.subscriber_set.filter(verified=True).count(),
|
|
29
31
|
)
|
|
30
|
-
for audience in models.Audience.objects.all()
|
|
32
|
+
for audience in cast(Iterable[models.Audience], models.Audience.objects.all())
|
|
31
33
|
]
|
|
32
34
|
|
|
33
35
|
@override
|
|
@@ -35,7 +37,7 @@ class CampaignBackend(wncb.CampaignBackend):
|
|
|
35
37
|
self,
|
|
36
38
|
audience_id: str
|
|
37
39
|
) -> "list[wna.AudienceSegment]":
|
|
38
|
-
audience = models.Audience.objects.get(id=int(audience_id))
|
|
40
|
+
audience = cast(models.Audience, models.Audience.objects.get(id=int(audience_id)))
|
|
39
41
|
return [
|
|
40
42
|
wna.AudienceSegment(
|
|
41
43
|
id=f'{audience_id}/{audience_segment.id}',
|
|
@@ -43,7 +45,7 @@ class CampaignBackend(wncb.CampaignBackend):
|
|
|
43
45
|
name=audience_segment.name,
|
|
44
46
|
member_count=audience_segment.subscribers.filter(verified=True).count(),
|
|
45
47
|
)
|
|
46
|
-
for audience_segment in audience.audience_segment_set.all()
|
|
48
|
+
for audience_segment in cast(Iterable[models.AudienceSegment], audience.audience_segment_set.all())
|
|
47
49
|
]
|
|
48
50
|
|
|
49
51
|
@override
|
|
@@ -54,37 +56,45 @@ class CampaignBackend(wncb.CampaignBackend):
|
|
|
54
56
|
recipients: "wnm.NewsletterRecipientsBase | None",
|
|
55
57
|
subject: str,
|
|
56
58
|
html: str,
|
|
59
|
+
from_name: str,
|
|
60
|
+
reply_to: str,
|
|
57
61
|
) -> str:
|
|
58
62
|
with transaction.atomic():
|
|
59
63
|
audience_segment: models.AudienceSegment | None = None
|
|
60
64
|
if recipients is not None and recipients.segment:
|
|
61
|
-
audience_segment = models.AudienceSegment.objects.get(id=int(recipients.segment.split('/')[-1]))
|
|
65
|
+
audience_segment = cast(models.AudienceSegment, models.AudienceSegment.objects.get(id=int(recipients.segment.split('/')[-1])))
|
|
62
66
|
|
|
63
67
|
campaign: models.Campaign | None
|
|
64
68
|
if campaign_id:
|
|
65
69
|
int_campaign_id = int(campaign_id)
|
|
66
|
-
campaign = models.Campaign.objects.filter(id=int_campaign_id).first()
|
|
70
|
+
campaign = cast(models.Campaign | None, models.Campaign.objects.filter(id=int_campaign_id).first())
|
|
67
71
|
if campaign is None:
|
|
68
72
|
# No campaign exists with that ID anymore; recreate it.
|
|
69
|
-
campaign = models.Campaign.objects.create(
|
|
73
|
+
campaign = cast(models.Campaign, models.Campaign.objects.create(
|
|
70
74
|
id=int_campaign_id,
|
|
71
75
|
subject=subject,
|
|
72
76
|
html=html.strip(),
|
|
73
77
|
audience_segment=audience_segment,
|
|
74
|
-
|
|
78
|
+
from_email_address=reply_to,
|
|
79
|
+
from_email_name=from_name,
|
|
80
|
+
))
|
|
75
81
|
else:
|
|
76
82
|
campaign.subject = subject
|
|
77
83
|
campaign.html = html.strip()
|
|
84
|
+
campaign.from_email_address = reply_to
|
|
85
|
+
campaign.from_email_name = from_name
|
|
78
86
|
if audience_segment is not None:
|
|
79
87
|
campaign.audience_segment = audience_segment
|
|
80
88
|
campaign.full_clean()
|
|
81
89
|
campaign.save()
|
|
82
90
|
else:
|
|
83
|
-
campaign = models.Campaign.objects.create(
|
|
91
|
+
campaign = cast(models.Campaign, models.Campaign.objects.create(
|
|
84
92
|
subject=subject,
|
|
85
93
|
html=html.strip(),
|
|
86
94
|
audience_segment=audience_segment,
|
|
87
|
-
|
|
95
|
+
from_email_address=reply_to,
|
|
96
|
+
from_email_name=from_name,
|
|
97
|
+
))
|
|
88
98
|
return str(campaign.id)
|
|
89
99
|
|
|
90
100
|
@override
|
|
@@ -93,17 +103,9 @@ class CampaignBackend(wncb.CampaignBackend):
|
|
|
93
103
|
|
|
94
104
|
@override
|
|
95
105
|
def send_test_email(self, *, campaign_id: str, email: str) -> None:
|
|
96
|
-
campaign = models.Campaign.objects.get(id=int(campaign_id))
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
audience = campaign.audience_segment.audience
|
|
100
|
-
else:
|
|
101
|
-
# Build empty audience. Will have no subscribers, and no smtp data.
|
|
102
|
-
# This will not work without default data in the config.
|
|
103
|
-
audience = models.Audience()
|
|
104
|
-
|
|
105
|
-
from_address = audience.from_address()
|
|
106
|
-
user, password = audience.smtp_auth()
|
|
106
|
+
campaign = cast(models.Campaign, models.Campaign.objects.get(id=int(campaign_id)))
|
|
107
|
+
|
|
108
|
+
from_address = campaign.from_address()
|
|
107
109
|
|
|
108
110
|
_ = send_mail(
|
|
109
111
|
subject=campaign.subject,
|
|
@@ -111,21 +113,19 @@ class CampaignBackend(wncb.CampaignBackend):
|
|
|
111
113
|
from_email=from_address,
|
|
112
114
|
recipient_list=[email],
|
|
113
115
|
html_message=campaign.html,
|
|
114
|
-
auth_user=user,
|
|
115
|
-
auth_password=password,
|
|
116
116
|
)
|
|
117
117
|
|
|
118
118
|
@override
|
|
119
119
|
def send_campaign(self, campaign_id: str) -> None:
|
|
120
120
|
with transaction.atomic():
|
|
121
|
-
campaign = models.Campaign.objects.get(id=int(campaign_id))
|
|
121
|
+
campaign = cast(models.Campaign, models.Campaign.objects.get(id=int(campaign_id)))
|
|
122
122
|
if campaign.audience_segment is None:
|
|
123
123
|
raise RuntimeError("Campaign can't be sent without an audience segment")
|
|
124
124
|
|
|
125
|
-
audience = campaign.audience_segment.audience
|
|
125
|
+
audience = cast(models.Audience, campaign.audience_segment.audience)
|
|
126
126
|
|
|
127
|
-
site = audience.site
|
|
128
|
-
hostname = site.hostname
|
|
127
|
+
site = cast(Site, audience.site)
|
|
128
|
+
hostname = cast(str, site.hostname)
|
|
129
129
|
match site.port:
|
|
130
130
|
case 80:
|
|
131
131
|
url_base = f'http://{hostname}'
|
|
@@ -134,13 +134,9 @@ class CampaignBackend(wncb.CampaignBackend):
|
|
|
134
134
|
case port:
|
|
135
135
|
url_base = f'http://{hostname}:{port}'
|
|
136
136
|
|
|
137
|
-
from_address =
|
|
138
|
-
user, password = audience.smtp_auth()
|
|
137
|
+
from_address = campaign.from_address()
|
|
139
138
|
|
|
140
|
-
connection: Any = get_connection(
|
|
141
|
-
username=user,
|
|
142
|
-
password=password,
|
|
143
|
-
)
|
|
139
|
+
connection: Any = get_connection()
|
|
144
140
|
|
|
145
141
|
def subscriber_message(subscriber: models.Subscriber) -> EmailMessage:
|
|
146
142
|
unsubscribe_url = url_base + reverse('wagtail_newsletter_django_backend:newsletter_subscriber_unsubscribe', kwargs={'key': subscriber.key})
|
|
@@ -185,7 +181,7 @@ class CampaignBackend(wncb.CampaignBackend):
|
|
|
185
181
|
@override
|
|
186
182
|
def schedule_campaign(self, campaign_id: str, schedule_time: datetime) -> None:
|
|
187
183
|
with transaction.atomic():
|
|
188
|
-
campaign = models.Campaign.objects.get(id=int(campaign_id))
|
|
184
|
+
campaign = cast(models.Campaign, models.Campaign.objects.get(id=int(campaign_id)))
|
|
189
185
|
campaign.send_at = schedule_time
|
|
190
186
|
campaign.full_clean()
|
|
191
187
|
campaign.save()
|
|
@@ -193,7 +189,7 @@ class CampaignBackend(wncb.CampaignBackend):
|
|
|
193
189
|
@override
|
|
194
190
|
def unschedule_campaign(self, campaign_id: str) -> None:
|
|
195
191
|
with transaction.atomic():
|
|
196
|
-
campaign = models.Campaign.objects.get(id=int(campaign_id))
|
|
192
|
+
campaign = cast(models.Campaign, models.Campaign.objects.get(id=int(campaign_id)))
|
|
197
193
|
campaign.send_at = None
|
|
198
194
|
campaign.full_clean()
|
|
199
195
|
campaign.save()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Generated by Django 5.2.5 on 2026-02-04 04:16
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('wagtail_newsletter_django_backend', '0003_alter_campaign_audience_segment'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.RemoveField(
|
|
14
|
+
model_name='audience',
|
|
15
|
+
name='from_email_address',
|
|
16
|
+
),
|
|
17
|
+
migrations.RemoveField(
|
|
18
|
+
model_name='audience',
|
|
19
|
+
name='from_email_name',
|
|
20
|
+
),
|
|
21
|
+
migrations.RemoveField(
|
|
22
|
+
model_name='audience',
|
|
23
|
+
name='smtp_password',
|
|
24
|
+
),
|
|
25
|
+
migrations.RemoveField(
|
|
26
|
+
model_name='audience',
|
|
27
|
+
name='smtp_user',
|
|
28
|
+
),
|
|
29
|
+
migrations.AddField(
|
|
30
|
+
model_name='campaign',
|
|
31
|
+
name='from_email_address',
|
|
32
|
+
field=models.EmailField(blank=True, default=None, help_text='The From address for the email. If not present, this will default to WAGTAIL_NEWSLETTER_REPLY_TO.', max_length=254, null=True),
|
|
33
|
+
),
|
|
34
|
+
migrations.AddField(
|
|
35
|
+
model_name='campaign',
|
|
36
|
+
name='from_email_name',
|
|
37
|
+
field=models.CharField(blank=True, default=None, help_text='The display name portion of the from email. If not present, this will default to WAGTAIL_NEWSLETTER_FROM_NAME.', max_length=256, null=True),
|
|
38
|
+
),
|
|
39
|
+
]
|
|
@@ -15,16 +15,12 @@ import string
|
|
|
15
15
|
from wagtail_newsletter import campaign_backends as wncb
|
|
16
16
|
|
|
17
17
|
if TYPE_CHECKING:
|
|
18
|
-
from django.db.models.manager import RelatedManager
|
|
18
|
+
from django.db.models.manager import RelatedManager, Manager
|
|
19
19
|
|
|
20
20
|
@lru_cache
|
|
21
|
-
def _default_from_parts() -> tuple[str | None, str
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if email:
|
|
25
|
-
name, email = parseaddr(email)
|
|
26
|
-
email = email or None
|
|
27
|
-
name = name or None
|
|
21
|
+
def _default_from_parts() -> tuple[str | None, str]:
|
|
22
|
+
name = cast(str | None, getattr(settings, 'WAGTAIL_NEWSLETTER_FROM_NAME', None))
|
|
23
|
+
email = cast(str, settings.WAGTAIL_NEWSLETTER_REPLY_TO)
|
|
28
24
|
return name, email
|
|
29
25
|
|
|
30
26
|
class SaveDict(TypedDict):
|
|
@@ -56,33 +52,6 @@ class Audience(models.Model):
|
|
|
56
52
|
site: 'models.ForeignKey[Site]' = models.ForeignKey(Site, blank=False, null=False, on_delete=models.CASCADE)
|
|
57
53
|
name: 'models.CharField[str]' = models.CharField(max_length=64, blank=False, null=False)
|
|
58
54
|
description: 'models.TextField[str]' = models.TextField(blank=True, null=False)
|
|
59
|
-
smtp_user: 'models.CharField[str | None]' = models.CharField(
|
|
60
|
-
max_length=256,
|
|
61
|
-
blank=True,
|
|
62
|
-
null=True,
|
|
63
|
-
default=None,
|
|
64
|
-
help_text='The SMTP login user. If absent, this will not be supplied to send_mail, and EMAIL_HOST_USER will be used instead',
|
|
65
|
-
)
|
|
66
|
-
smtp_password: 'models.CharField[str | None]' = models.CharField(
|
|
67
|
-
max_length=256,
|
|
68
|
-
blank=True,
|
|
69
|
-
null=True,
|
|
70
|
-
default=None,
|
|
71
|
-
help_text='The SMTP login password. If absent, this will not be supplied to send_mail, and EMAIL_HOST_PASSWORD will be used instead',
|
|
72
|
-
)
|
|
73
|
-
from_email_address: 'models.EmailField[str | None]' = models.EmailField(
|
|
74
|
-
blank=True,
|
|
75
|
-
null=True,
|
|
76
|
-
default=None,
|
|
77
|
-
help_text='The From address for the email. If not present, this will not be supplied to send_mail, and DEFAULT_FROM_EMAIL will be used instead',
|
|
78
|
-
)
|
|
79
|
-
from_email_name: 'models.CharField[str | None]' = models.CharField(
|
|
80
|
-
max_length=256,
|
|
81
|
-
blank=True,
|
|
82
|
-
null=True,
|
|
83
|
-
default=None,
|
|
84
|
-
help_text='The display name portion of the from email. This will be wrapped around the from email address.',
|
|
85
|
-
)
|
|
86
55
|
|
|
87
56
|
created_at: 'models.DateTimeField[datetime]' = models.DateTimeField(auto_now_add=True)
|
|
88
57
|
updated_at: 'models.DateTimeField[datetime]' = models.DateTimeField(auto_now=True)
|
|
@@ -99,35 +68,6 @@ class Audience(models.Model):
|
|
|
99
68
|
def __str__(self) -> str:
|
|
100
69
|
return self.name
|
|
101
70
|
|
|
102
|
-
_NULLABLE_FIELDS: Sequence[str] = ('smtp_user', 'smtp_password', 'from_email_address', 'from_email_name')
|
|
103
|
-
|
|
104
|
-
@override
|
|
105
|
-
def save(self, **kwargs: Unpack[SaveDict]) -> None: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
106
|
-
patch_blanks(self, kwargs.get('update_fields'), Audience._NULLABLE_FIELDS)
|
|
107
|
-
return super().save(**kwargs)
|
|
108
|
-
|
|
109
|
-
def from_address(self) -> str:
|
|
110
|
-
default_from_name, default_from_email = _default_from_parts()
|
|
111
|
-
from_email = self.from_email_address or default_from_email
|
|
112
|
-
if from_email is None:
|
|
113
|
-
raise RuntimeError('from email address must be set')
|
|
114
|
-
from_name = self.from_email_name or default_from_name
|
|
115
|
-
if from_name:
|
|
116
|
-
return formataddr((from_name, from_email))
|
|
117
|
-
else:
|
|
118
|
-
return from_email
|
|
119
|
-
|
|
120
|
-
def smtp_auth(self) -> tuple[str, str]:
|
|
121
|
-
smtp_user = self.smtp_user or cast(str | None, settings.EMAIL_HOST_USER)
|
|
122
|
-
smtp_password = self.smtp_password or cast(str | None, settings.EMAIL_HOST_PASSWORD)
|
|
123
|
-
if smtp_user is None:
|
|
124
|
-
raise RuntimeError('smtp_user must be set')
|
|
125
|
-
|
|
126
|
-
if smtp_password is None:
|
|
127
|
-
raise RuntimeError('smtp_password must be set')
|
|
128
|
-
|
|
129
|
-
return smtp_user, smtp_password
|
|
130
|
-
|
|
131
71
|
class AudienceSegment(models.Model):
|
|
132
72
|
pk: int
|
|
133
73
|
id: int # pyright: ignore[reportUninitializedInstanceVariable]
|
|
@@ -245,6 +185,8 @@ class Subscription(models.Model):
|
|
|
245
185
|
raise ValidationError('The audience_segment and subscriber must have the same audience')
|
|
246
186
|
|
|
247
187
|
class Campaign(models.Model):
|
|
188
|
+
objects: 'Manager[Campaign]'
|
|
189
|
+
|
|
248
190
|
pk: int
|
|
249
191
|
id: int # pyright: ignore[reportUninitializedInstanceVariable]
|
|
250
192
|
|
|
@@ -254,9 +196,40 @@ class Campaign(models.Model):
|
|
|
254
196
|
html: 'models.TextField[str]' = models.TextField(blank=False, null=False)
|
|
255
197
|
audience_segment: 'models.ForeignKey[AudienceSegment | None]' = models.ForeignKey(AudienceSegment, blank=False, null=True, on_delete=models.SET_NULL)
|
|
256
198
|
|
|
199
|
+
from_email_address: 'models.EmailField[str | None]' = models.EmailField(
|
|
200
|
+
blank=True,
|
|
201
|
+
null=True,
|
|
202
|
+
default=None,
|
|
203
|
+
help_text='The From address for the email. If not present, this will default to WAGTAIL_NEWSLETTER_REPLY_TO.',
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
from_email_name: 'models.CharField[str | None]' = models.CharField(
|
|
207
|
+
max_length=256,
|
|
208
|
+
blank=True,
|
|
209
|
+
null=True,
|
|
210
|
+
default=None,
|
|
211
|
+
help_text='The display name portion of the from email. If not present, this will default to WAGTAIL_NEWSLETTER_FROM_NAME.',
|
|
212
|
+
)
|
|
213
|
+
|
|
257
214
|
created_at: 'models.DateTimeField[datetime]' = models.DateTimeField(auto_now_add=True)
|
|
258
215
|
updated_at: 'models.DateTimeField[datetime]' = models.DateTimeField(auto_now=True)
|
|
259
216
|
|
|
217
|
+
_NULLABLE_FIELDS: Sequence[str] = ('from_email_address', 'from_email_name')
|
|
218
|
+
|
|
219
|
+
@override
|
|
220
|
+
def save(self, **kwargs: Unpack[SaveDict]) -> None: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
221
|
+
patch_blanks(self, kwargs.get('update_fields'), Campaign._NULLABLE_FIELDS)
|
|
222
|
+
return super().save(**kwargs)
|
|
223
|
+
|
|
224
|
+
def from_address(self) -> str:
|
|
225
|
+
default_from_name, default_from_email = _default_from_parts()
|
|
226
|
+
from_email = cast(str | None, self.from_email_address) or default_from_email
|
|
227
|
+
from_name = cast(str | None, self.from_email_name) or default_from_name
|
|
228
|
+
if from_name:
|
|
229
|
+
return formataddr((from_name, from_email))
|
|
230
|
+
else:
|
|
231
|
+
return from_email
|
|
232
|
+
|
|
260
233
|
@property
|
|
261
234
|
def is_sent(self) -> bool:
|
|
262
235
|
return self.sent_at is not None
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
# ---> Python
|
|
2
|
-
# Byte-compiled / optimized / DLL files
|
|
3
|
-
__pycache__/
|
|
4
|
-
*.py[cod]
|
|
5
|
-
*$py.class
|
|
6
|
-
|
|
7
|
-
# C extensions
|
|
8
|
-
*.so
|
|
9
|
-
|
|
10
|
-
# Distribution / packaging
|
|
11
|
-
.Python
|
|
12
|
-
build/
|
|
13
|
-
develop-eggs/
|
|
14
|
-
dist/
|
|
15
|
-
downloads/
|
|
16
|
-
eggs/
|
|
17
|
-
.eggs/
|
|
18
|
-
lib/
|
|
19
|
-
lib64/
|
|
20
|
-
parts/
|
|
21
|
-
sdist/
|
|
22
|
-
var/
|
|
23
|
-
wheels/
|
|
24
|
-
share/python-wheels/
|
|
25
|
-
*.egg-info/
|
|
26
|
-
.installed.cfg
|
|
27
|
-
*.egg
|
|
28
|
-
MANIFEST
|
|
29
|
-
|
|
30
|
-
# PyInstaller
|
|
31
|
-
# Usually these files are written by a python script from a template
|
|
32
|
-
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
33
|
-
*.manifest
|
|
34
|
-
*.spec
|
|
35
|
-
|
|
36
|
-
# Installer logs
|
|
37
|
-
pip-log.txt
|
|
38
|
-
pip-delete-this-directory.txt
|
|
39
|
-
|
|
40
|
-
# Unit test / coverage reports
|
|
41
|
-
htmlcov/
|
|
42
|
-
.tox/
|
|
43
|
-
.nox/
|
|
44
|
-
.coverage
|
|
45
|
-
.coverage.*
|
|
46
|
-
.cache
|
|
47
|
-
nosetests.xml
|
|
48
|
-
coverage.xml
|
|
49
|
-
*.cover
|
|
50
|
-
*.py,cover
|
|
51
|
-
.hypothesis/
|
|
52
|
-
.pytest_cache/
|
|
53
|
-
cover/
|
|
54
|
-
|
|
55
|
-
# Translations
|
|
56
|
-
*.mo
|
|
57
|
-
*.pot
|
|
58
|
-
|
|
59
|
-
# Django stuff:
|
|
60
|
-
*.log
|
|
61
|
-
local_settings.py
|
|
62
|
-
db.sqlite3
|
|
63
|
-
db.sqlite3-*
|
|
64
|
-
|
|
65
|
-
# Flask stuff:
|
|
66
|
-
instance/
|
|
67
|
-
.webassets-cache
|
|
68
|
-
|
|
69
|
-
# Scrapy stuff:
|
|
70
|
-
.scrapy
|
|
71
|
-
|
|
72
|
-
# Sphinx documentation
|
|
73
|
-
docs/_build/
|
|
74
|
-
|
|
75
|
-
# PyBuilder
|
|
76
|
-
.pybuilder/
|
|
77
|
-
target/
|
|
78
|
-
|
|
79
|
-
# Jupyter Notebook
|
|
80
|
-
.ipynb_checkpoints
|
|
81
|
-
|
|
82
|
-
# IPython
|
|
83
|
-
profile_default/
|
|
84
|
-
ipython_config.py
|
|
85
|
-
|
|
86
|
-
# pyenv
|
|
87
|
-
# For a library or package, you might want to ignore these files since the code is
|
|
88
|
-
# intended to run in multiple environments; otherwise, check them in:
|
|
89
|
-
# .python-version
|
|
90
|
-
|
|
91
|
-
# pipenv
|
|
92
|
-
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
93
|
-
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
94
|
-
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
95
|
-
# install all needed dependencies.
|
|
96
|
-
#Pipfile.lock
|
|
97
|
-
|
|
98
|
-
# poetry
|
|
99
|
-
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
100
|
-
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
101
|
-
# commonly ignored for libraries.
|
|
102
|
-
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
103
|
-
#poetry.lock
|
|
104
|
-
|
|
105
|
-
# pdm
|
|
106
|
-
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
107
|
-
#pdm.lock
|
|
108
|
-
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
109
|
-
# in version control.
|
|
110
|
-
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
111
|
-
.pdm.toml
|
|
112
|
-
.pdm-python
|
|
113
|
-
.pdm-build/
|
|
114
|
-
|
|
115
|
-
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
116
|
-
__pypackages__/
|
|
117
|
-
|
|
118
|
-
# Celery stuff
|
|
119
|
-
celerybeat-schedule
|
|
120
|
-
celerybeat.pid
|
|
121
|
-
|
|
122
|
-
# SageMath parsed files
|
|
123
|
-
*.sage.py
|
|
124
|
-
|
|
125
|
-
# Environments
|
|
126
|
-
.env
|
|
127
|
-
.venv
|
|
128
|
-
env/
|
|
129
|
-
venv/
|
|
130
|
-
ENV/
|
|
131
|
-
env.bak/
|
|
132
|
-
venv.bak/
|
|
133
|
-
|
|
134
|
-
# Spyder project settings
|
|
135
|
-
.spyderproject
|
|
136
|
-
.spyproject
|
|
137
|
-
|
|
138
|
-
# Rope project settings
|
|
139
|
-
.ropeproject
|
|
140
|
-
|
|
141
|
-
# mkdocs documentation
|
|
142
|
-
/site
|
|
143
|
-
|
|
144
|
-
# mypy
|
|
145
|
-
.mypy_cache/
|
|
146
|
-
.dmypy.json
|
|
147
|
-
dmypy.json
|
|
148
|
-
|
|
149
|
-
# Pyre type checker
|
|
150
|
-
.pyre/
|
|
151
|
-
|
|
152
|
-
# pytype static type analyzer
|
|
153
|
-
.pytype/
|
|
154
|
-
|
|
155
|
-
# Cython debug symbols
|
|
156
|
-
cython_debug/
|
|
157
|
-
|
|
158
|
-
# PyCharm
|
|
159
|
-
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
160
|
-
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
161
|
-
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
162
|
-
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
163
|
-
#.idea/
|
|
164
|
-
|
|
165
|
-
demo/media/
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# Read the Docs configuration file
|
|
2
|
-
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
|
3
|
-
|
|
4
|
-
# Required
|
|
5
|
-
version: 2
|
|
6
|
-
|
|
7
|
-
# Set the OS, Python version, and other tools you might need
|
|
8
|
-
build:
|
|
9
|
-
os: ubuntu-24.04
|
|
10
|
-
tools:
|
|
11
|
-
python: "3.13"
|
|
12
|
-
|
|
13
|
-
# Build documentation in the "docs/" directory with Sphinx
|
|
14
|
-
sphinx:
|
|
15
|
-
configuration: docs/conf.py
|
|
16
|
-
|
|
17
|
-
# Optionally, but recommended,
|
|
18
|
-
# declare the Python requirements required to build your documentation
|
|
19
|
-
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
|
|
20
|
-
# python:
|
|
21
|
-
# install:
|
|
22
|
-
# - requirements: docs/requirements.txt
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python
|
|
2
|
-
"""Django's command-line utility for administrative tasks."""
|
|
3
|
-
import os
|
|
4
|
-
import sys
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def main():
|
|
8
|
-
"""Run administrative tasks."""
|
|
9
|
-
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings.dev")
|
|
10
|
-
try:
|
|
11
|
-
from django.core.management import execute_from_command_line
|
|
12
|
-
except ImportError as exc:
|
|
13
|
-
raise ImportError(
|
|
14
|
-
"Couldn't import Django. Are you sure it's installed and "
|
|
15
|
-
"available on your PYTHONPATH environment variable? Did you "
|
|
16
|
-
"forget to activate a virtual environment?"
|
|
17
|
-
) from exc
|
|
18
|
-
execute_from_command_line(sys.argv)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if __name__ == '__main__':
|
|
22
|
-
main()
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ['flit_core >= 3.2, <4']
|
|
3
|
-
build-backend = 'flit_core.buildapi'
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "demo"
|
|
7
|
-
dynamic = ['version', 'description']
|
|
8
|
-
dependencies = [
|
|
9
|
-
'Django>=5.2,<5.3',
|
|
10
|
-
'wagtail>=7.1,<7.2',
|
|
11
|
-
"wagtail-newsletter[mrml]>=0.2.2,<0.3.0",
|
|
12
|
-
"wagtail-newsletter-django-backend",
|
|
13
|
-
'python-dotenv',
|
|
14
|
-
"django-extensions",
|
|
15
|
-
'requests >= 2.32.5, < 3',
|
|
16
|
-
]
|
|
17
|
-
requires-python = ">=3.10"
|
|
18
|
-
|
|
19
|
-
[dependency-groups]
|
|
20
|
-
dev = [
|
|
21
|
-
"basedpyright>=1.31.2",
|
|
22
|
-
"django-types>=0.22.0",
|
|
23
|
-
"ptpython",
|
|
24
|
-
]
|
|
25
|
-
|
|
26
|
-
[[project.authors]]
|
|
27
|
-
name = 'Taylor C. Richberger'
|
|
28
|
-
email = 'taylor@axfive.net'
|
|
29
|
-
|
|
30
|
-
[[project.maintainers]]
|
|
31
|
-
name = 'Taylor C. Richberger'
|
|
32
|
-
email = 'taylor@axfive.net'
|
|
33
|
-
|
|
34
|
-
[tool.uv.sources]
|
|
35
|
-
wagtail-newsletter-django-backend = { path = "../", editable = true }
|