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.
Files changed (78) hide show
  1. {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/PKG-INFO +3 -3
  2. {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/pyproject.toml +2 -2
  3. {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/src/wagtail_newsletter_django_backend/__init__.py +1 -1
  4. {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/src/wagtail_newsletter_django_backend/admin_viewsets.py +2 -2
  5. {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/src/wagtail_newsletter_django_backend/campaign_backend.py +32 -36
  6. wagtail_newsletter_django_backend-0.6.0/src/wagtail_newsletter_django_backend/migrations/0004_remove_audience_from_email_address_and_more.py +39 -0
  7. {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/src/wagtail_newsletter_django_backend/models.py +37 -64
  8. wagtail_newsletter_django_backend-0.5.9/.gitignore +0 -165
  9. wagtail_newsletter_django_backend-0.5.9/.readthedocs.yaml +0 -22
  10. wagtail_newsletter_django_backend-0.5.9/demo/manage.py +0 -22
  11. wagtail_newsletter_django_backend-0.5.9/demo/pyproject.toml +0 -35
  12. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/__init__.py +0 -4
  13. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/blog/apps.py +0 -11
  14. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/blog/migrations/0001_initial.py +0 -80
  15. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/blog/migrations/0002_newslettersignup.py +0 -45
  16. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/blog/models.py +0 -62
  17. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/blog/signals.py +0 -13
  18. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/blog/templates/blog/blog_index_page.html +0 -19
  19. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/blog/templates/blog/blog_page.html +0 -17
  20. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/blog/templates/blog/blog_page_newsletter.html +0 -19
  21. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/blog/templates/blog/newsletter_signup.html +0 -21
  22. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/blog/views.py +0 -3
  23. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/home/apps.py +0 -6
  24. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/home/migrations/0001_initial.py +0 -36
  25. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/home/models.py +0 -7
  26. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/home/static/css/welcome_page.css +0 -184
  27. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/home/templates/home/home_page.html +0 -21
  28. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/home/templates/home/welcome_page.html +0 -52
  29. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/home/tests.py +0 -43
  30. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/search/__init__.py +0 -0
  31. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/search/apps.py +0 -5
  32. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/search/templates/search/search.html +0 -38
  33. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/search/views.py +0 -46
  34. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/settings/__init__.py +0 -0
  35. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/settings/base.py +0 -195
  36. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/settings/dev.py +0 -24
  37. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/settings/production.py +0 -14
  38. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/static/css/demo.css +0 -0
  39. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/static/js/demo.js +0 -0
  40. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/templates/404.html +0 -11
  41. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/templates/500.html +0 -13
  42. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/templates/base.html +0 -54
  43. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/urls.py +0 -36
  44. wagtail_newsletter_django_backend-0.5.9/demo/src/demo/wsgi.py +0 -16
  45. wagtail_newsletter_django_backend-0.5.9/demo/uv.lock +0 -870
  46. wagtail_newsletter_django_backend-0.5.9/docs/Makefile +0 -20
  47. wagtail_newsletter_django_backend-0.5.9/docs/conf.py +0 -27
  48. wagtail_newsletter_django_backend-0.5.9/docs/index.rst +0 -19
  49. wagtail_newsletter_django_backend-0.5.9/docs/make.bat +0 -35
  50. wagtail_newsletter_django_backend-0.5.9/src/wagtail_newsletter_django_backend/admin.py +0 -3
  51. wagtail_newsletter_django_backend-0.5.9/src/wagtail_newsletter_django_backend/management/__init__.py +0 -0
  52. wagtail_newsletter_django_backend-0.5.9/src/wagtail_newsletter_django_backend/management/commands/__init__.py +0 -0
  53. wagtail_newsletter_django_backend-0.5.9/src/wagtail_newsletter_django_backend/migrations/__init__.py +0 -0
  54. wagtail_newsletter_django_backend-0.5.9/src/wagtail_newsletter_django_backend/templatetags/__init__.py +0 -0
  55. wagtail_newsletter_django_backend-0.5.9/src/wagtail_newsletter_django_backend/tests.py +0 -3
  56. wagtail_newsletter_django_backend-0.5.9/uv.lock +0 -681
  57. {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/LICENSE +0 -0
  58. {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/README.md +0 -0
  59. {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
  60. {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/src/wagtail_newsletter_django_backend/apps.py +0 -0
  61. {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/src/wagtail_newsletter_django_backend/forms.py +0 -0
  62. {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
  63. {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
  64. {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
  65. {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
  66. {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
  67. {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
  68. {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
  69. {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
  70. {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
  71. {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
  72. {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
  73. {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
  74. {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
  75. {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
  76. {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/src/wagtail_newsletter_django_backend/urls.py +0 -0
  77. {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/src/wagtail_newsletter_django_backend/views.py +0 -0
  78. {wagtail_newsletter_django_backend-0.5.9 → wagtail_newsletter_django_backend-0.6.0}/src/wagtail_newsletter_django_backend/wagtail_hooks.py +0 -0
@@ -1,14 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wagtail-newsletter-django-backend
3
- Version: 0.5.9
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, <5.3
11
- Requires-Dist: wagtail >=7.1, <7.3
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/
@@ -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, <5.3',
10
- 'wagtail >=7.1, <7.3',
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",
@@ -3,4 +3,4 @@
3
3
  This is a simple, basic internal backend for wagtail-newsletter, allowing you to manage your mailing lists internally and send them out through Django's standard email capabilities.
4
4
  '''
5
5
 
6
- __version__ = '0.5.9'
6
+ __version__ = '0.6.0'
@@ -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', 'smtp_user', 'smtp_password', 'from_email_address', 'from_email_name']
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\]\]|\\[\\[unsubscribe\\]\\]')
15
- _manage_re = re.compile(r'\[\[manage\]\]|\\[\\[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
- audience: models.Audience
98
- if campaign.audience_segment is not None:
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 = audience.from_address()
138
- user, password = audience.smtp_auth()
137
+ from_address = campaign.from_address()
139
138
 
140
- connection: Any = get_connection( # pyright: ignore[reportAny]
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 | None]:
22
- email = cast(str | None, settings.DEFAULT_FROM_EMAIL)
23
- name: str | None = None
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 }
@@ -1,4 +0,0 @@
1
- '''A demo site for wagtail-newsletter-django-backend.
2
- '''
3
-
4
- __version__ = '0.0.1'
@@ -1,11 +0,0 @@
1
- from typing import override
2
- from django.apps import AppConfig
3
-
4
-
5
- class BlogConfig(AppConfig):
6
- default_auto_field = "django.db.models.BigAutoField"
7
- name = "demo.blog"
8
-
9
- @override
10
- def ready(self):
11
- from . import signals