wbmailing 2.2.1__py2.py3-none-any.whl
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.
Potentially problematic release.
This version of wbmailing might be problematic. Click here for more details.
- wbmailing/__init__.py +1 -0
- wbmailing/admin.py +74 -0
- wbmailing/apps.py +14 -0
- wbmailing/backend.py +131 -0
- wbmailing/celery.py +0 -0
- wbmailing/dynamic_preferences_registry.py +35 -0
- wbmailing/factories.py +211 -0
- wbmailing/filters/__init__.py +8 -0
- wbmailing/filters/mailing_lists.py +84 -0
- wbmailing/filters/mails.py +74 -0
- wbmailing/management/__init__.py +22 -0
- wbmailing/migrations/0001_initial_squashed_squashed_0008_alter_mail_bcc_email_alter_mail_cc_email_and_more.py +649 -0
- wbmailing/migrations/0002_delete_mailingsettings.py +16 -0
- wbmailing/migrations/0003_alter_mailinglistsubscriberchangerequest_options.py +25 -0
- wbmailing/migrations/__init__.py +0 -0
- wbmailing/models/__init__.py +6 -0
- wbmailing/models/mailing_lists.py +386 -0
- wbmailing/models/mails.py +895 -0
- wbmailing/serializers/__init__.py +19 -0
- wbmailing/serializers/mailing_lists.py +209 -0
- wbmailing/serializers/mails.py +251 -0
- wbmailing/tasks.py +37 -0
- wbmailing/templatetags/__init__.py +0 -0
- wbmailing/templatetags/mailing_tags.py +22 -0
- wbmailing/tests/__init__.py +0 -0
- wbmailing/tests/conftest.py +30 -0
- wbmailing/tests/models/__init__.py +0 -0
- wbmailing/tests/models/test_mailing_lists.py +297 -0
- wbmailing/tests/models/test_mails.py +205 -0
- wbmailing/tests/signals.py +124 -0
- wbmailing/tests/test_serializers.py +28 -0
- wbmailing/tests/test_tasks.py +49 -0
- wbmailing/tests/test_viewsets.py +216 -0
- wbmailing/tests/tests.py +142 -0
- wbmailing/urls.py +90 -0
- wbmailing/viewsets/__init__.py +32 -0
- wbmailing/viewsets/analytics.py +110 -0
- wbmailing/viewsets/buttons/__init__.py +10 -0
- wbmailing/viewsets/buttons/mailing_lists.py +91 -0
- wbmailing/viewsets/buttons/mails.py +98 -0
- wbmailing/viewsets/display/__init__.py +16 -0
- wbmailing/viewsets/display/mailing_lists.py +175 -0
- wbmailing/viewsets/display/mails.py +318 -0
- wbmailing/viewsets/endpoints/__init__.py +8 -0
- wbmailing/viewsets/endpoints/mailing_lists.py +86 -0
- wbmailing/viewsets/endpoints/mails.py +51 -0
- wbmailing/viewsets/mailing_lists.py +320 -0
- wbmailing/viewsets/mails.py +425 -0
- wbmailing/viewsets/menu/__init__.py +5 -0
- wbmailing/viewsets/menu/mailing_lists.py +37 -0
- wbmailing/viewsets/menu/mails.py +25 -0
- wbmailing/viewsets/titles/__init__.py +17 -0
- wbmailing/viewsets/titles/mailing_lists.py +63 -0
- wbmailing/viewsets/titles/mails.py +55 -0
- wbmailing-2.2.1.dist-info/METADATA +5 -0
- wbmailing-2.2.1.dist-info/RECORD +57 -0
- wbmailing-2.2.1.dist-info/WHEEL +5 -0
wbmailing/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
wbmailing/admin.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
from django.utils.translation import gettext_lazy as _
|
|
3
|
+
|
|
4
|
+
from .models import (
|
|
5
|
+
Mail,
|
|
6
|
+
MailEvent,
|
|
7
|
+
MailingList,
|
|
8
|
+
MailingListEmailContactThroughModel,
|
|
9
|
+
MailingListSubscriberChangeRequest,
|
|
10
|
+
MailTemplate,
|
|
11
|
+
MassMail,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MailingListEmailContactThroughInlineAdmin(admin.TabularInline):
|
|
16
|
+
model = MailingListEmailContactThroughModel
|
|
17
|
+
fk_name = "mailing_list"
|
|
18
|
+
autocomplete_fields = ["email_contact"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@admin.register(MailingList)
|
|
22
|
+
class MailingListAdmin(admin.ModelAdmin):
|
|
23
|
+
autocomplete_fields = ["email_contacts"]
|
|
24
|
+
search_fields = ["title"]
|
|
25
|
+
inlines = [
|
|
26
|
+
MailingListEmailContactThroughInlineAdmin,
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@admin.register(MassMail)
|
|
31
|
+
class MassMailAdmin(admin.ModelAdmin):
|
|
32
|
+
autocomplete_fields = ["creator"]
|
|
33
|
+
|
|
34
|
+
def send_test_mail(self, request, queryset):
|
|
35
|
+
for mass_mail in queryset:
|
|
36
|
+
mass_mail.send_test_mail(request.user)
|
|
37
|
+
|
|
38
|
+
actions = [send_test_mail]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@admin.register(MailingListSubscriberChangeRequest)
|
|
42
|
+
class MailingListSubscriberChangeRequestAdmin(admin.ModelAdmin):
|
|
43
|
+
list_display = ("email_contact", "mailing_list", "status")
|
|
44
|
+
autocomplete_fields = ["email_contact", "requester"]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
admin.site.register(MailEvent)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class MailEventInline(admin.TabularInline):
|
|
51
|
+
model = MailEvent
|
|
52
|
+
fields = ("timestamp", "event_type", "reject_reason", "recipient", "user_agent", "tags", "description")
|
|
53
|
+
readonly_fields = ("timestamp", "event_type", "reject_reason", "recipient", "user_agent", "tags", "description")
|
|
54
|
+
extra = 0
|
|
55
|
+
can_delete = False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@admin.register(Mail)
|
|
59
|
+
class MailAdmin(admin.ModelAdmin):
|
|
60
|
+
def send_mails(self, request, queryset):
|
|
61
|
+
for mail in queryset:
|
|
62
|
+
mail.resend()
|
|
63
|
+
|
|
64
|
+
send_mails.short_description = _("Send Emails")
|
|
65
|
+
actions = [send_mails]
|
|
66
|
+
search_fields = ["mass_mail__subject", "from_email", "to_email__address", "subject", "message_ids"]
|
|
67
|
+
autocomplete_fields = ["to_email", "cc_email", "bcc_email"]
|
|
68
|
+
|
|
69
|
+
inlines = [MailEventInline]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@admin.register(MailTemplate)
|
|
73
|
+
class MailTemplateAdmin(admin.ModelAdmin):
|
|
74
|
+
pass
|
wbmailing/apps.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from django.apps import AppConfig
|
|
2
|
+
from django.db.models.signals import post_migrate
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class WbmailingConfig(AppConfig):
|
|
6
|
+
name = "wbmailing"
|
|
7
|
+
|
|
8
|
+
def ready(self) -> None:
|
|
9
|
+
from wbmailing.management import initialize_task
|
|
10
|
+
|
|
11
|
+
post_migrate.connect(
|
|
12
|
+
initialize_task,
|
|
13
|
+
dispatch_uid="wbmailing.initialize_task",
|
|
14
|
+
)
|
wbmailing/backend.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from datetime import timedelta
|
|
2
|
+
|
|
3
|
+
from anymail.backends.mailgun import EmailBackend as AnymailMailgunBackend
|
|
4
|
+
from anymail.backends.mailjet import EmailBackend as AnymailMailjetBackend
|
|
5
|
+
from anymail.backends.mandrill import EmailBackend as AnymailMandrillBackend
|
|
6
|
+
from anymail.backends.postmark import EmailBackend as AnymailPostmarkBackend
|
|
7
|
+
from anymail.backends.sendgrid import EmailBackend as AnymailSendgridBackend
|
|
8
|
+
from anymail.backends.sendinblue import EmailBackend as AnymailSendinblueBackend
|
|
9
|
+
from anymail.exceptions import AnymailError
|
|
10
|
+
from django.conf import settings
|
|
11
|
+
from django.core.mail.backends.console import EmailBackend as ConsoleBackend
|
|
12
|
+
from django.utils import timezone
|
|
13
|
+
from sentry_sdk import capture_message
|
|
14
|
+
from wbcore.utils.html import convert_html2text
|
|
15
|
+
from wbmailing.models import Mail, MailEvent
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SendMessagesMixin:
|
|
19
|
+
def _process_msg(self, message):
|
|
20
|
+
mail = message.mail if hasattr(message, "mail") else None
|
|
21
|
+
mass_mail = message.mass_mail if hasattr(message, "mass_mail") else None
|
|
22
|
+
if mail:
|
|
23
|
+
event_type = MailEvent.EventType.RESENT
|
|
24
|
+
else:
|
|
25
|
+
if not hasattr(message, "silent_mail") or (hasattr(message, "silent_mail") and not message.silent_mail):
|
|
26
|
+
mail = Mail.create_mail_from_mailmessage(message, user=getattr(message, "user", None))
|
|
27
|
+
event_type = MailEvent.EventType.CREATED
|
|
28
|
+
if mass_mail:
|
|
29
|
+
message.tags = [f"massmail-{mass_mail.id}"]
|
|
30
|
+
else:
|
|
31
|
+
message.tags = [f"mail-{mail.id}"]
|
|
32
|
+
if mail and mail.body:
|
|
33
|
+
# We reset the body text and html field with what might have been computed in create_mail_from_mailmessage
|
|
34
|
+
message.body = convert_html2text(mail.body)
|
|
35
|
+
message.alternatives = []
|
|
36
|
+
message.attach_alternative(mail.body, "text/html")
|
|
37
|
+
return event_type, mail
|
|
38
|
+
|
|
39
|
+
def send_messages(self, email_messages):
|
|
40
|
+
"""
|
|
41
|
+
Sends one or more EmailMessage objects and returns the number of email
|
|
42
|
+
messages sent.
|
|
43
|
+
"""
|
|
44
|
+
# This API is specified by Django's core BaseEmailBackend
|
|
45
|
+
# (so you can't change it to, e.g., return detailed status).
|
|
46
|
+
# Subclasses shouldn't need to override.
|
|
47
|
+
from wbmailing.models import MailEvent
|
|
48
|
+
|
|
49
|
+
num_sent = 0
|
|
50
|
+
if not email_messages:
|
|
51
|
+
return num_sent
|
|
52
|
+
|
|
53
|
+
created_session = self.open()
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
for message in email_messages:
|
|
57
|
+
try:
|
|
58
|
+
event_type, mail = self._process_msg(message)
|
|
59
|
+
sent = self._send(message)
|
|
60
|
+
if mail:
|
|
61
|
+
MailEvent.objects.create(
|
|
62
|
+
mail=mail, event_type=event_type, timestamp=timezone.now() - timedelta(seconds=1)
|
|
63
|
+
)
|
|
64
|
+
mail.message_ids.append(message.anymail_status.message_id)
|
|
65
|
+
mail.last_send = timezone.now()
|
|
66
|
+
mail.save()
|
|
67
|
+
except AnymailError as e:
|
|
68
|
+
capture_message(e)
|
|
69
|
+
if self.fail_silently:
|
|
70
|
+
sent = False
|
|
71
|
+
else:
|
|
72
|
+
raise
|
|
73
|
+
if sent:
|
|
74
|
+
num_sent += 1
|
|
75
|
+
finally:
|
|
76
|
+
if created_session:
|
|
77
|
+
self.close()
|
|
78
|
+
|
|
79
|
+
return num_sent
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class PostmarkEmailBackend(SendMessagesMixin, AnymailPostmarkBackend):
|
|
83
|
+
def send_messages(self, email_messages):
|
|
84
|
+
if settings.WBMAILING_POSTMARK_BROADCAST_STREAM_ID:
|
|
85
|
+
for msg in email_messages:
|
|
86
|
+
if hasattr(msg, "mass_mail") and msg.mass_mail:
|
|
87
|
+
msg.esp_extra = {"MessageStream": settings.WBMAILING_POSTMARK_BROADCAST_STREAM_ID}
|
|
88
|
+
|
|
89
|
+
return super().send_messages(email_messages)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class SendgridEmailBackend(SendMessagesMixin, AnymailSendgridBackend):
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class MailgunEmailBackend(SendMessagesMixin, AnymailMailgunBackend):
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class MailjetEmailBackend(SendMessagesMixin, AnymailMailjetBackend):
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class MandrillEmailBackend(SendMessagesMixin, AnymailMandrillBackend):
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class SendinblueEmailBackend(SendMessagesMixin, AnymailSendinblueBackend):
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ConsoleEmailBackend(SendMessagesMixin, ConsoleBackend):
|
|
113
|
+
def send_messages(self, email_messages):
|
|
114
|
+
"""Write all messages to the stream in a thread-safe way."""
|
|
115
|
+
if not email_messages:
|
|
116
|
+
return
|
|
117
|
+
msg_count = 0
|
|
118
|
+
with self._lock:
|
|
119
|
+
try:
|
|
120
|
+
stream_created = self.open()
|
|
121
|
+
for message in email_messages:
|
|
122
|
+
self._process_msg(message)
|
|
123
|
+
self.write_message(message)
|
|
124
|
+
self.stream.flush() # flush after each message
|
|
125
|
+
msg_count += 1
|
|
126
|
+
if stream_created:
|
|
127
|
+
self.close()
|
|
128
|
+
except Exception:
|
|
129
|
+
if not self.fail_silently:
|
|
130
|
+
raise
|
|
131
|
+
return msg_count
|
wbmailing/celery.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from re import fullmatch
|
|
2
|
+
|
|
3
|
+
from django.forms import ValidationError
|
|
4
|
+
from django.utils.translation import gettext as _
|
|
5
|
+
from dynamic_preferences.preferences import Section
|
|
6
|
+
from dynamic_preferences.registries import global_preferences_registry
|
|
7
|
+
from dynamic_preferences.types import BooleanPreference, StringPreference
|
|
8
|
+
|
|
9
|
+
mailing_section = Section("wbmailing")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@global_preferences_registry.register
|
|
13
|
+
class DefaultSourceMailPreference(StringPreference):
|
|
14
|
+
section = mailing_section
|
|
15
|
+
name = "default_source_mail"
|
|
16
|
+
default = "info@stainly-bench.com"
|
|
17
|
+
|
|
18
|
+
verbose_name = _("Default Source Mail Preference")
|
|
19
|
+
help_text = _("The default address used to send emails from")
|
|
20
|
+
|
|
21
|
+
def validate(self, value):
|
|
22
|
+
if not fullmatch(r"[^@]+@[^@]+\.[^@]+", value):
|
|
23
|
+
raise ValidationError(_("Not a valid email format"))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@global_preferences_registry.register
|
|
27
|
+
class AutomaticallyApproveUnsubscriptionRequestFromHardBound(BooleanPreference):
|
|
28
|
+
section = mailing_section
|
|
29
|
+
name = "automatically_approve_unsubscription_request_from_hard_bounce"
|
|
30
|
+
default = False
|
|
31
|
+
|
|
32
|
+
verbose_name = _("Automatically approve unsubscription request from hard bounce")
|
|
33
|
+
help_text = _(
|
|
34
|
+
"Automatically approve unsubscription request from hard bounce received from the ESP tracking system"
|
|
35
|
+
)
|
wbmailing/factories.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import factory
|
|
2
|
+
import pytz
|
|
3
|
+
from wbcore.contrib.directory.factories import EmailContactFactory
|
|
4
|
+
from wbmailing.models import (
|
|
5
|
+
Mail,
|
|
6
|
+
MailEvent,
|
|
7
|
+
MailingList,
|
|
8
|
+
MailingListEmailContactThroughModel,
|
|
9
|
+
MailingListSubscriberChangeRequest,
|
|
10
|
+
MailTemplate,
|
|
11
|
+
MassMail,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MailingListSubscriberChangeRequestFactory(factory.django.DjangoModelFactory):
|
|
16
|
+
class Meta:
|
|
17
|
+
model = MailingListSubscriberChangeRequest
|
|
18
|
+
|
|
19
|
+
expiration_date = factory.Faker("date_object")
|
|
20
|
+
status = MailingListSubscriberChangeRequest.Status.PENDING
|
|
21
|
+
type = MailingListSubscriberChangeRequest.Type.SUBSCRIBING
|
|
22
|
+
email_contact = factory.SubFactory("wbcore.contrib.directory.factories.EmailContactFactory")
|
|
23
|
+
mailing_list = factory.SubFactory("wbmailing.factories.MailingListFactory")
|
|
24
|
+
requester = factory.SubFactory("wbcore.contrib.authentication.factories.AuthenticatedPersonFactory")
|
|
25
|
+
approver = factory.SubFactory("wbcore.contrib.authentication.factories.AuthenticatedPersonFactory")
|
|
26
|
+
reason = factory.Faker("text", max_nb_chars=256)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ApprovedMailingListSubscriberChangeRequest(MailingListSubscriberChangeRequestFactory):
|
|
30
|
+
status = MailingListSubscriberChangeRequest.Status.APPROVED
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MailingListFactory(factory.django.DjangoModelFactory):
|
|
34
|
+
class Meta:
|
|
35
|
+
model = MailingList
|
|
36
|
+
|
|
37
|
+
title = factory.Faker("text", max_nb_chars=64)
|
|
38
|
+
is_public = False
|
|
39
|
+
|
|
40
|
+
@factory.post_generation
|
|
41
|
+
def email_contacts(self, create, extracted, **kwargs):
|
|
42
|
+
if not create:
|
|
43
|
+
return
|
|
44
|
+
if extracted:
|
|
45
|
+
for email_contact in extracted:
|
|
46
|
+
MailingListEmailContactThroughModel.objects.create(
|
|
47
|
+
mailing_list=self,
|
|
48
|
+
email_contact=email_contact,
|
|
49
|
+
status=MailingListEmailContactThroughModel.Status.SUBSCRIBED,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class EmailContactMailingListFactory(MailingListFactory):
|
|
54
|
+
@factory.post_generation
|
|
55
|
+
def email_contacts(self, create, extracted, **kwargs):
|
|
56
|
+
mlscr = MailingListSubscriberChangeRequestFactory()
|
|
57
|
+
self.email_contacts.add(mlscr.email_contact)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class MailingListEmailContactFactory(EmailContactFactory):
|
|
61
|
+
@factory.post_generation
|
|
62
|
+
def subscriptions(self, create, extracted, **kwargs):
|
|
63
|
+
ml = MailingListFactory()
|
|
64
|
+
MailingListEmailContactThroughModel.objects.create(
|
|
65
|
+
mailing_list=ml, email_contact=self, status=MailingListEmailContactThroughModel.Status.SUBSCRIBED
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class UnsubscribedMailingListEmailContactFactory(EmailContactFactory):
|
|
70
|
+
@factory.post_generation
|
|
71
|
+
def subscriptions(self, create, extracted, **kwargs):
|
|
72
|
+
ml = MailingListFactory()
|
|
73
|
+
MailingListEmailContactThroughModel.objects.create(
|
|
74
|
+
mailing_list=ml, email_contact=self, status=MailingListEmailContactThroughModel.Status.UNSUBSCRIBED
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class MailingListEmailContactThroughModelFactory(factory.django.DjangoModelFactory):
|
|
79
|
+
email_contact = factory.SubFactory("wbcore.contrib.directory.factories.EmailContactFactory")
|
|
80
|
+
mailing_list = factory.SubFactory("wbmailing.factories.MailingListFactory")
|
|
81
|
+
status = MailingListEmailContactThroughModel.Status.SUBSCRIBED
|
|
82
|
+
|
|
83
|
+
class Meta:
|
|
84
|
+
model = MailingListEmailContactThroughModel
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class MassMailFactory(factory.django.DjangoModelFactory):
|
|
88
|
+
class Meta:
|
|
89
|
+
model = MassMail
|
|
90
|
+
|
|
91
|
+
# status = #defaut = DRAFT
|
|
92
|
+
from_email = factory.Faker("email")
|
|
93
|
+
template = factory.SubFactory("wbmailing.factories.MailTemplateFactory")
|
|
94
|
+
|
|
95
|
+
@factory.post_generation
|
|
96
|
+
def mailing_lists(self, create, extracted, **kwargs):
|
|
97
|
+
if not create:
|
|
98
|
+
return
|
|
99
|
+
if extracted:
|
|
100
|
+
for mailing_list in extracted:
|
|
101
|
+
self.mailing_lists.add(mailing_list)
|
|
102
|
+
|
|
103
|
+
subject = factory.Faker("text", max_nb_chars=64)
|
|
104
|
+
body = factory.Faker("paragraph", nb_sentences=5)
|
|
105
|
+
|
|
106
|
+
@factory.post_generation
|
|
107
|
+
def attachments(self, create, extracted, **kwargs):
|
|
108
|
+
if not create:
|
|
109
|
+
return
|
|
110
|
+
if extracted:
|
|
111
|
+
for attachment in extracted:
|
|
112
|
+
self.attach_document(attachment)
|
|
113
|
+
|
|
114
|
+
# body_json = #JSONField(null=True, blank=True)
|
|
115
|
+
created = factory.Faker("date_time", tzinfo=pytz.utc)
|
|
116
|
+
creator = factory.SubFactory("wbcore.contrib.directory.factories.PersonFactory")
|
|
117
|
+
send_at = factory.Faker("date_time_between", start_date="now", end_date="+30y", tzinfo=pytz.utc)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class CustomMassMailFactory(MassMailFactory):
|
|
121
|
+
@factory.post_generation
|
|
122
|
+
def mailing_lists(self, create, extracted, **kwargs):
|
|
123
|
+
ml = MailingListFactory()
|
|
124
|
+
self.mailing_lists.add(ml)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class CustomMassMailEmailContactFactory(EmailContactFactory):
|
|
128
|
+
@factory.post_generation
|
|
129
|
+
def subscriptions(self, create, extracted, **kwargs):
|
|
130
|
+
ml = MailingListFactory.create()
|
|
131
|
+
ml.add_to_mailinglist(self)
|
|
132
|
+
MassMailFactory.create(mailing_lists=[ml])
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class MailFactory(factory.django.DjangoModelFactory):
|
|
136
|
+
class Meta:
|
|
137
|
+
model = Mail
|
|
138
|
+
|
|
139
|
+
created = factory.Faker("date_time", tzinfo=pytz.utc)
|
|
140
|
+
last_send = factory.Faker("date_time", tzinfo=pytz.utc)
|
|
141
|
+
template = factory.SubFactory("wbmailing.factories.MailTemplateFactory")
|
|
142
|
+
message_ids = factory.List([factory.Faker("pystr")])
|
|
143
|
+
mass_mail = factory.SubFactory("wbmailing.factories.MassMailFactory")
|
|
144
|
+
from_email = factory.Faker("email")
|
|
145
|
+
|
|
146
|
+
@factory.post_generation
|
|
147
|
+
def to_email(self, create, extracted, **kwargs):
|
|
148
|
+
if not create:
|
|
149
|
+
return
|
|
150
|
+
if extracted:
|
|
151
|
+
for email in extracted:
|
|
152
|
+
self.to_email.add(email)
|
|
153
|
+
|
|
154
|
+
@factory.post_generation
|
|
155
|
+
def cc_email(self, create, extracted, **kwargs):
|
|
156
|
+
if not create:
|
|
157
|
+
return
|
|
158
|
+
if extracted:
|
|
159
|
+
for email in extracted:
|
|
160
|
+
self.cc_email.add(email)
|
|
161
|
+
|
|
162
|
+
@factory.post_generation
|
|
163
|
+
def bcc_email(self, create, extracted, **kwargs):
|
|
164
|
+
if not create:
|
|
165
|
+
return
|
|
166
|
+
if extracted:
|
|
167
|
+
for email in extracted:
|
|
168
|
+
self.bcc_email.add(email)
|
|
169
|
+
|
|
170
|
+
subject = factory.Faker("text", max_nb_chars=64)
|
|
171
|
+
body = factory.Faker("paragraph", nb_sentences=5)
|
|
172
|
+
# body_json = # JSONField(null=True, blank=True)
|
|
173
|
+
|
|
174
|
+
@factory.post_generation
|
|
175
|
+
def attachments(self, create, extracted, **kwargs):
|
|
176
|
+
if not create:
|
|
177
|
+
return
|
|
178
|
+
if extracted:
|
|
179
|
+
for attachment in extracted:
|
|
180
|
+
self.attach_document(attachment)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class ToEmailMailFactory(MailFactory):
|
|
184
|
+
@factory.post_generation
|
|
185
|
+
def to_email(self, create, extracted, **kwargs):
|
|
186
|
+
ec = EmailContactFactory.create()
|
|
187
|
+
self.to_email.add(ec)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class MailEventFactory(factory.django.DjangoModelFactory):
|
|
191
|
+
class Meta:
|
|
192
|
+
model = MailEvent
|
|
193
|
+
|
|
194
|
+
mail = factory.SubFactory("wbmailing.factories.MailFactory")
|
|
195
|
+
timestamp = factory.Faker("date_time", tzinfo=pytz.utc)
|
|
196
|
+
# event_type = # default=EventType.CREATED
|
|
197
|
+
reject_reason = "" # default=null # RejectReason.choices
|
|
198
|
+
description = factory.Faker("paragraph", nb_sentences=2)
|
|
199
|
+
recipient = factory.Faker("email")
|
|
200
|
+
click_url = factory.Faker("image_url")
|
|
201
|
+
ip = factory.Faker("ipv4")
|
|
202
|
+
user_agent = factory.Faker("first_name")
|
|
203
|
+
# raw_data = JSONField(default=dict, null=True, blank=True, verbose_name="Raw Data")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class MailTemplateFactory(factory.django.DjangoModelFactory):
|
|
207
|
+
class Meta:
|
|
208
|
+
model = MailTemplate
|
|
209
|
+
|
|
210
|
+
title = factory.Faker("text", max_nb_chars=64)
|
|
211
|
+
template = factory.Faker("paragraph", nb_sentences=5)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from .mailing_lists import (
|
|
2
|
+
EmailContactMailingListFilterSet,
|
|
3
|
+
MailingListEmailContactThroughModelModelFilterSet,
|
|
4
|
+
MailingListFilterSet,
|
|
5
|
+
MailingListSubscriberChangeRequestFilterSet,
|
|
6
|
+
MailStatusMassMailFilterSet,
|
|
7
|
+
)
|
|
8
|
+
from .mails import MailFilter, MassMailFilterSet
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from django.apps import apps
|
|
2
|
+
from django.db.models import Exists, OuterRef
|
|
3
|
+
from django.utils.translation import gettext_lazy as _
|
|
4
|
+
from wbcore import filters as wb_filters
|
|
5
|
+
from wbcore.contrib.directory.models import EmailContact, Entry
|
|
6
|
+
from wbmailing import models
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MailingListFilterSet(wb_filters.FilterSet):
|
|
10
|
+
not_factsheet_mailinglist = wb_filters.BooleanFilter(
|
|
11
|
+
label=_("No Factsheet Mailing Lists"), method="boolean_not_factsheet_mailinglist", default=True
|
|
12
|
+
)
|
|
13
|
+
negative_entry = wb_filters.ModelChoiceFilter(
|
|
14
|
+
label=_("Unsubscribed mailing lists for user"),
|
|
15
|
+
queryset=Entry.objects.all(),
|
|
16
|
+
endpoint=Entry.get_representation_endpoint(),
|
|
17
|
+
value_key=Entry.get_representation_value_key(),
|
|
18
|
+
label_key=Entry.get_representation_label_key(),
|
|
19
|
+
method="get_notsubscribed_mailing_list_for_entry",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def get_notsubscribed_mailing_list_for_entry(self, queryset, name, value):
|
|
23
|
+
if value:
|
|
24
|
+
already_subscribed_subquery = models.MailingListEmailContactThroughModel.objects.filter(
|
|
25
|
+
mailing_list=OuterRef("pk"),
|
|
26
|
+
email_contact__in=value.emails.all(),
|
|
27
|
+
status__in=[models.MailingListEmailContactThroughModel.Status.SUBSCRIBED],
|
|
28
|
+
)
|
|
29
|
+
return queryset.annotate(already_subscribed_subquery=Exists(already_subscribed_subquery)).filter(
|
|
30
|
+
already_subscribed_subquery=False
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return queryset
|
|
34
|
+
|
|
35
|
+
def boolean_not_factsheet_mailinglist(self, queryset, name, value):
|
|
36
|
+
if apps.is_installed("wbreport"):
|
|
37
|
+
if value is False:
|
|
38
|
+
return queryset.filter(reports__isnull=False).distinct()
|
|
39
|
+
elif value is True:
|
|
40
|
+
return queryset.filter(reports__isnull=True).distinct()
|
|
41
|
+
return queryset
|
|
42
|
+
|
|
43
|
+
class Meta:
|
|
44
|
+
model = models.MailingList
|
|
45
|
+
fields = {"email_contacts": ["exact"], "is_public": ["exact"]}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class MailingListSubscriberChangeRequestFilterSet(wb_filters.FilterSet):
|
|
49
|
+
class Meta:
|
|
50
|
+
model = models.MailingListSubscriberChangeRequest
|
|
51
|
+
fields = {
|
|
52
|
+
"email_contact": ["exact"],
|
|
53
|
+
"mailing_list": ["exact"],
|
|
54
|
+
"requester": ["exact"],
|
|
55
|
+
"created": ["gte", "exact", "lte"],
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class MailStatusMassMailFilterSet(wb_filters.FilterSet):
|
|
60
|
+
status = wb_filters.ChoiceFilter(label=_("Status"), choices=models.MailEvent.EventType.choices)
|
|
61
|
+
|
|
62
|
+
class Meta:
|
|
63
|
+
model = EmailContact
|
|
64
|
+
fields = {"entry": ["exact"]}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class MailingListEmailContactThroughModelModelFilterSet(wb_filters.FilterSet):
|
|
68
|
+
expiration_date = wb_filters.DateTimeRangeFilter(
|
|
69
|
+
label=_("Expiration Date"),
|
|
70
|
+
method=wb_filters.DateRangeFilter.base_date_range_filter_method,
|
|
71
|
+
)
|
|
72
|
+
is_pending_request_change = wb_filters.BooleanFilter(
|
|
73
|
+
label=_("Pending Change"), lookup_expr="exact", field_name="is_pending_request_change"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
class Meta:
|
|
77
|
+
model = models.MailingListEmailContactThroughModel
|
|
78
|
+
fields = {"status": ["exact"]}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class EmailContactMailingListFilterSet(MailingListEmailContactThroughModelModelFilterSet):
|
|
82
|
+
class Meta:
|
|
83
|
+
model = models.MailingListEmailContactThroughModel
|
|
84
|
+
fields = {"email_contact": ["exact"], "status": ["exact"]}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from django.apps import apps
|
|
2
|
+
from django.utils.translation import gettext_lazy as _
|
|
3
|
+
from wbcore import filters as wb_filters
|
|
4
|
+
from wbmailing import models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MassMailFilterSet(wb_filters.FilterSet):
|
|
8
|
+
is_factsheet_massmail = wb_filters.BooleanFilter(
|
|
9
|
+
label=_("Is Factsheet Mass Mail"), method="boolean_is_factsheet_massmail", default=False
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
def boolean_is_factsheet_massmail(self, queryset, name, value):
|
|
13
|
+
if apps.is_installed("wbreport"):
|
|
14
|
+
if value is True:
|
|
15
|
+
return queryset.filter(mailing_lists__reports__isnull=False).distinct()
|
|
16
|
+
elif value is False:
|
|
17
|
+
return queryset.filter(mailing_lists__reports__isnull=True).distinct()
|
|
18
|
+
return queryset
|
|
19
|
+
|
|
20
|
+
class Meta:
|
|
21
|
+
model = models.MassMail
|
|
22
|
+
fields = {
|
|
23
|
+
"subject": ["exact", "icontains"],
|
|
24
|
+
"from_email": ["exact", "icontains"],
|
|
25
|
+
"template": ["exact"],
|
|
26
|
+
"mailing_lists": ["exact"],
|
|
27
|
+
"created": ["gte", "exact", "lte"],
|
|
28
|
+
"creator": ["exact"],
|
|
29
|
+
"status": ["exact"],
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MailFilter(wb_filters.FilterSet):
|
|
34
|
+
never_open = wb_filters.BooleanFilter(label=_("Never Opened"), method="boolean_never_open")
|
|
35
|
+
|
|
36
|
+
event_type = wb_filters.MultipleChoiceFilter(
|
|
37
|
+
label=_("Events"), method="filter_events", choices=models.MailEvent.EventType.choices
|
|
38
|
+
)
|
|
39
|
+
rejected_reason = wb_filters.MultipleChoiceFilter(
|
|
40
|
+
label=_("Rejection Reasons"), method="filter_rejected_reason", choices=models.MailEvent.RejectReason.choices
|
|
41
|
+
)
|
|
42
|
+
is_massmail_mail = wb_filters.BooleanFilter(label=_("Mass Mail"), method="boolean_is_massmail")
|
|
43
|
+
status = wb_filters.ChoiceFilter(label=_("Status"), choices=models.MailEvent.EventType.choices)
|
|
44
|
+
|
|
45
|
+
class Meta:
|
|
46
|
+
model = models.Mail
|
|
47
|
+
fields = {
|
|
48
|
+
"mass_mail": ["exact"],
|
|
49
|
+
"from_email": ["exact"],
|
|
50
|
+
"to_email": ["exact"],
|
|
51
|
+
"created": ["gte", "exact", "lte"],
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
def boolean_is_massmail(self, queryset, name, value):
|
|
55
|
+
if value is True:
|
|
56
|
+
return queryset.filter(mass_mail__isnull=False)
|
|
57
|
+
elif value is False:
|
|
58
|
+
return queryset.filter(mass_mail__isnull=True)
|
|
59
|
+
return queryset
|
|
60
|
+
|
|
61
|
+
def boolean_never_open(self, queryset, name, value):
|
|
62
|
+
if value:
|
|
63
|
+
return queryset.exclude(events__event_type=models.MailEvent.EventType.OPENED)
|
|
64
|
+
return queryset
|
|
65
|
+
|
|
66
|
+
def filter_events(self, queryset, name, value):
|
|
67
|
+
if value:
|
|
68
|
+
return queryset.filter(events__event_type=value)
|
|
69
|
+
return queryset
|
|
70
|
+
|
|
71
|
+
def filter_rejected_reason(self, queryset, name, value):
|
|
72
|
+
if value:
|
|
73
|
+
return queryset.filter(events__reject_reason=value)
|
|
74
|
+
return queryset
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from django_celery_beat.models import IntervalSchedule, PeriodicTask
|
|
2
|
+
from django.db import DEFAULT_DB_ALIAS
|
|
3
|
+
from django.apps import apps as global_apps
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def initialize_task(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, apps=global_apps, **kwargs):
|
|
7
|
+
PeriodicTask.objects.update_or_create(
|
|
8
|
+
task="wbmailing.tasks.periodic_send_mass_mail_as_tasks",
|
|
9
|
+
defaults={
|
|
10
|
+
"name": "Mailing: Periodically send scheduled mass mails",
|
|
11
|
+
"interval": IntervalSchedule.objects.get_or_create(every=120, period=IntervalSchedule.SECONDS)[0],
|
|
12
|
+
"crontab": None,
|
|
13
|
+
},
|
|
14
|
+
)
|
|
15
|
+
PeriodicTask.objects.update_or_create(
|
|
16
|
+
task="wbmailing.tasks.check_and_remove_expired_mailinglist_subscription",
|
|
17
|
+
defaults={
|
|
18
|
+
"name": "Mailing: Remove expired contact from mailing list",
|
|
19
|
+
"interval": IntervalSchedule.objects.get_or_create(every=1, period=IntervalSchedule.DAYS)[0],
|
|
20
|
+
"crontab": None,
|
|
21
|
+
},
|
|
22
|
+
)
|