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.

Files changed (57) hide show
  1. wbmailing/__init__.py +1 -0
  2. wbmailing/admin.py +74 -0
  3. wbmailing/apps.py +14 -0
  4. wbmailing/backend.py +131 -0
  5. wbmailing/celery.py +0 -0
  6. wbmailing/dynamic_preferences_registry.py +35 -0
  7. wbmailing/factories.py +211 -0
  8. wbmailing/filters/__init__.py +8 -0
  9. wbmailing/filters/mailing_lists.py +84 -0
  10. wbmailing/filters/mails.py +74 -0
  11. wbmailing/management/__init__.py +22 -0
  12. wbmailing/migrations/0001_initial_squashed_squashed_0008_alter_mail_bcc_email_alter_mail_cc_email_and_more.py +649 -0
  13. wbmailing/migrations/0002_delete_mailingsettings.py +16 -0
  14. wbmailing/migrations/0003_alter_mailinglistsubscriberchangerequest_options.py +25 -0
  15. wbmailing/migrations/__init__.py +0 -0
  16. wbmailing/models/__init__.py +6 -0
  17. wbmailing/models/mailing_lists.py +386 -0
  18. wbmailing/models/mails.py +895 -0
  19. wbmailing/serializers/__init__.py +19 -0
  20. wbmailing/serializers/mailing_lists.py +209 -0
  21. wbmailing/serializers/mails.py +251 -0
  22. wbmailing/tasks.py +37 -0
  23. wbmailing/templatetags/__init__.py +0 -0
  24. wbmailing/templatetags/mailing_tags.py +22 -0
  25. wbmailing/tests/__init__.py +0 -0
  26. wbmailing/tests/conftest.py +30 -0
  27. wbmailing/tests/models/__init__.py +0 -0
  28. wbmailing/tests/models/test_mailing_lists.py +297 -0
  29. wbmailing/tests/models/test_mails.py +205 -0
  30. wbmailing/tests/signals.py +124 -0
  31. wbmailing/tests/test_serializers.py +28 -0
  32. wbmailing/tests/test_tasks.py +49 -0
  33. wbmailing/tests/test_viewsets.py +216 -0
  34. wbmailing/tests/tests.py +142 -0
  35. wbmailing/urls.py +90 -0
  36. wbmailing/viewsets/__init__.py +32 -0
  37. wbmailing/viewsets/analytics.py +110 -0
  38. wbmailing/viewsets/buttons/__init__.py +10 -0
  39. wbmailing/viewsets/buttons/mailing_lists.py +91 -0
  40. wbmailing/viewsets/buttons/mails.py +98 -0
  41. wbmailing/viewsets/display/__init__.py +16 -0
  42. wbmailing/viewsets/display/mailing_lists.py +175 -0
  43. wbmailing/viewsets/display/mails.py +318 -0
  44. wbmailing/viewsets/endpoints/__init__.py +8 -0
  45. wbmailing/viewsets/endpoints/mailing_lists.py +86 -0
  46. wbmailing/viewsets/endpoints/mails.py +51 -0
  47. wbmailing/viewsets/mailing_lists.py +320 -0
  48. wbmailing/viewsets/mails.py +425 -0
  49. wbmailing/viewsets/menu/__init__.py +5 -0
  50. wbmailing/viewsets/menu/mailing_lists.py +37 -0
  51. wbmailing/viewsets/menu/mails.py +25 -0
  52. wbmailing/viewsets/titles/__init__.py +17 -0
  53. wbmailing/viewsets/titles/mailing_lists.py +63 -0
  54. wbmailing/viewsets/titles/mails.py +55 -0
  55. wbmailing-2.2.1.dist-info/METADATA +5 -0
  56. wbmailing-2.2.1.dist-info/RECORD +57 -0
  57. wbmailing-2.2.1.dist-info/WHEEL +5 -0
@@ -0,0 +1,6 @@
1
+ from .mailing_lists import (
2
+ MailingList,
3
+ MailingListEmailContactThroughModel,
4
+ MailingListSubscriberChangeRequest,
5
+ )
6
+ from .mails import Mail, MailEvent, MailTemplate, MassMail
@@ -0,0 +1,386 @@
1
+ from django.contrib.auth import get_user_model
2
+ from django.db import models
3
+ from django.db.models import Exists, OuterRef, Subquery
4
+ from django.db.models.signals import post_delete, post_save
5
+ from django.dispatch import receiver
6
+ from django.utils.translation import gettext
7
+ from django.utils.translation import gettext_lazy as _
8
+ from django_fsm import FSMField, transition
9
+ from wbcore.contrib.directory.models import EmailContact
10
+ from wbcore.contrib.directory.signals import deactivate_profile
11
+ from wbcore.contrib.icons import WBIcon
12
+ from wbcore.contrib.notifications.dispatch import send_notification
13
+ from wbcore.contrib.notifications.utils import create_notification_type
14
+ from wbcore.enums import RequestType
15
+ from wbcore.metadata.configs.buttons import ActionButton, ButtonDefaultColor
16
+ from wbcore.models import WBModel
17
+
18
+
19
+ def can_administrate_change_request(mail, user):
20
+ return user.has_perm("wbmailing.administrate_mailinglistsubscriberchangerequest") or user.is_superuser
21
+
22
+
23
+ class MailingListSubscriberChangeRequest(models.Model):
24
+ class Type(models.TextChoices):
25
+ SUBSCRIBING = "SUBSCRIBING", _("Subscribing")
26
+ UNSUBSCRIBING = "UNSUBSCRIBING", _("Unsubscribing")
27
+
28
+ class Status(models.TextChoices):
29
+ PENDING = "PENDING", _("Pending")
30
+ APPROVED = "APPROVED", _("Approved")
31
+ DENIED = "DENIED", _("Denied")
32
+
33
+ status = FSMField(default=Status.PENDING, choices=Status.choices, verbose_name=_("Status"))
34
+ type = models.CharField(max_length=32, choices=Type.choices, verbose_name=_("Type"))
35
+
36
+ @transition(
37
+ field=status,
38
+ source=[Status.PENDING],
39
+ target=Status.APPROVED,
40
+ permission=can_administrate_change_request,
41
+ custom={
42
+ "_transition_button": ActionButton(
43
+ method=RequestType.PATCH,
44
+ identifiers=("wbmailing:mailinglistsubscriberchangerequest",),
45
+ color=ButtonDefaultColor.SUCCESS,
46
+ icon=WBIcon.APPROVE.icon,
47
+ key="approve",
48
+ label=_("Approve"),
49
+ action_label=_("Approving"),
50
+ description_fields=_(
51
+ "<p>Address: {{_email_contact.address}}</p><p>Mailing list: {{_mailing_list.title}}</p>"
52
+ ),
53
+ )
54
+ },
55
+ )
56
+ def approve(self, by=None, description=None, **kwargs):
57
+ if profile := getattr(by, "profile", None):
58
+ self.approver = profile
59
+ if self.subscribing:
60
+ self.relationship.status = MailingListEmailContactThroughModel.Status.SUBSCRIBED
61
+ else:
62
+ self.relationship.status = MailingListEmailContactThroughModel.Status.UNSUBSCRIBED
63
+ self.relationship.save()
64
+ if description:
65
+ self.reason = description
66
+ if self.requester != self.approver and (user := getattr(self.requester, "user_account", None)):
67
+ approver_repr = self.approver.full_name if self.approver else "Unknown"
68
+ send_notification(
69
+ code="wbmailing.mailinglistsubscriberchangerequest.notify",
70
+ title=f"{self.type} change request for {self.email_contact.address} to {self.mailing_list.title} approved by {approver_repr}",
71
+ body=self.reason,
72
+ user=user,
73
+ )
74
+
75
+ @transition(
76
+ field=status,
77
+ source=[Status.PENDING],
78
+ target=Status.DENIED,
79
+ permission=can_administrate_change_request,
80
+ custom={
81
+ "_transition_button": ActionButton(
82
+ method=RequestType.PATCH,
83
+ color=ButtonDefaultColor.ERROR,
84
+ identifiers=("wbmailing:mailinglistsubscriberchangerequest",),
85
+ icon=WBIcon.DENY.icon,
86
+ key="deny",
87
+ label=_("Deny"),
88
+ action_label=_("Denial"),
89
+ description_fields=_(
90
+ "<p>Mail: {{_email_contact.address}}</p><p>Mailing list: {{_mailing_list.title}}</p>"
91
+ ),
92
+ )
93
+ },
94
+ )
95
+ def deny(self, by=None, description=None, **kwargs):
96
+ if profile := getattr(by, "profile", None):
97
+ self.approver = profile
98
+ if description:
99
+ self.reason = description
100
+
101
+ email_contact = models.ForeignKey(
102
+ "directory.EmailContact",
103
+ related_name="change_requests",
104
+ on_delete=models.CASCADE,
105
+ verbose_name=_("Subscriber"),
106
+ )
107
+ mailing_list = models.ForeignKey(
108
+ "MailingList", related_name="change_requests", on_delete=models.CASCADE, verbose_name=_("Mailing List")
109
+ )
110
+ relationship = models.ForeignKey(
111
+ "wbmailing.MailingListEmailContactThroughModel", on_delete=models.CASCADE, related_name="requests"
112
+ )
113
+
114
+ requester = models.ForeignKey(
115
+ "directory.Person", null=True, blank=True, on_delete=models.SET_NULL, verbose_name=_("Requester")
116
+ )
117
+ approver = models.ForeignKey(
118
+ "directory.Person",
119
+ null=True,
120
+ blank=True,
121
+ on_delete=models.SET_NULL,
122
+ related_name="approved_requests",
123
+ verbose_name=_("Approver"),
124
+ )
125
+
126
+ expiration_date = models.DateField(
127
+ null=True,
128
+ blank=True,
129
+ verbose_name=_("Expiration Date"),
130
+ help_text=_(
131
+ "If set, this email will be removed automatically from the mailing list after the set expiration time"
132
+ ),
133
+ )
134
+
135
+ reason = models.TextField(blank=True, null=True, verbose_name=_("Reason"))
136
+ created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
137
+ updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
138
+
139
+ class Meta:
140
+ verbose_name = _("Mailing List Subscriber Change Request")
141
+ verbose_name_plural = _("Mailing List Subscriber Change Requests")
142
+ permissions = (
143
+ (
144
+ "administrate_mailinglistsubscriberchangerequest",
145
+ "Can Administrate Mailing List Subscriber Change Requests",
146
+ ),
147
+ )
148
+ constraints = [
149
+ models.UniqueConstraint(
150
+ fields=["mailing_list", "email_contact"],
151
+ condition=models.Q(status="PENDING"),
152
+ name="unique_pending_request",
153
+ )
154
+ ]
155
+
156
+ notification_types = [
157
+ create_notification_type(
158
+ code="wbmailing.mailinglistsubscriberchangerequest.notify",
159
+ title="Subscriber Notification",
160
+ help_text="Sends out a notification when a recipient subscribes or unsubscribes",
161
+ )
162
+ ]
163
+
164
+ def save(self, **kwargs):
165
+ if not hasattr(self, "relationship"):
166
+ self.relationship = MailingListEmailContactThroughModel.objects.get_or_create(
167
+ mailing_list=self.mailing_list, email_contact=self.email_contact
168
+ )[0]
169
+ if not self.type:
170
+ self.type = (
171
+ self.Type.SUBSCRIBING
172
+ if self.relationship.status == MailingListEmailContactThroughModel.Status.UNSUBSCRIBED
173
+ else self.Type.UNSUBSCRIBING
174
+ )
175
+ if self.status == MailingListSubscriberChangeRequest.Status.PENDING:
176
+ if self.mailing_list.is_public or (
177
+ (user := getattr(self.requester, "user_account", None)) and can_administrate_change_request(self, user)
178
+ ):
179
+ if self.approver is None:
180
+ self.approver = self.requester
181
+ self.approve(description=gettext("Automatically approved.") if not self.reason else self.reason)
182
+
183
+ super().save(**kwargs)
184
+
185
+ @property
186
+ def subscribing(self) -> bool:
187
+ """
188
+ True if the state is unsubscribed and the change will subscribe it
189
+ """
190
+ return (
191
+ self.relationship.status == MailingListEmailContactThroughModel.Status.UNSUBSCRIBED
192
+ and self.type == self.Type.SUBSCRIBING
193
+ )
194
+
195
+ def __str__(self) -> str:
196
+ return f"{self.type} {self.email_contact.address} {self.mailing_list.title}"
197
+
198
+ @classmethod
199
+ def get_endpoint_basename(cls) -> str:
200
+ return "wbmailing:mailinglistsubscriberchangerequest"
201
+
202
+ @classmethod
203
+ def get_representation_value_key(cls) -> str:
204
+ return "id"
205
+
206
+ @classmethod
207
+ def get_representation_label_key(cls) -> str:
208
+ return "{{email_contact__address}} to {{mailing_list__title}}"
209
+
210
+ @classmethod
211
+ def get_approvers(cls):
212
+ return (
213
+ get_user_model()
214
+ .objects.filter(
215
+ models.Q(groups__permissions__codename="administrate_mailinglistsubscriberchangerequest")
216
+ | models.Q(user_permissions__codename="administrate_mailinglistsubscriberchangerequest")
217
+ )
218
+ .distinct()
219
+ )
220
+
221
+
222
+ class MailingListEmailContactThroughModel(models.Model):
223
+ class Status(models.TextChoices):
224
+ SUBSCRIBED = "SUBSCRIBED", _("Subscribed")
225
+ UNSUBSCRIBED = "UNSUBSCRIBED", _("Unsubscribed")
226
+
227
+ mailing_list = models.ForeignKey(
228
+ "wbmailing.MailingList", on_delete=models.CASCADE, related_name="through_mailinglists"
229
+ )
230
+ email_contact = models.ForeignKey(
231
+ "directory.EmailContact", on_delete=models.CASCADE, related_name="through_mailinglists"
232
+ )
233
+ status = models.CharField(
234
+ max_length=32, default=Status.UNSUBSCRIBED, choices=Status.choices, verbose_name=_("Status")
235
+ )
236
+
237
+ class Meta:
238
+ unique_together = ("mailing_list", "email_contact")
239
+
240
+ def change_state(self, automatically_approve: bool = False, **kwargs):
241
+ """
242
+ When called, change the state of the relationship from subscribe to unsubscribe or unsubscribe to subscribe
243
+ Args:
244
+ reason: Text field explaining the reason
245
+ requester: The subscription change state requester
246
+ automatically_approve: True if the change request needs to be automatically approved.
247
+ """
248
+ request = MailingListSubscriberChangeRequest.objects.get_or_create(
249
+ email_contact=self.email_contact,
250
+ mailing_list=self.mailing_list,
251
+ status=MailingListSubscriberChangeRequest.Status.PENDING,
252
+ defaults={
253
+ "relationship": self,
254
+ "type": MailingListSubscriberChangeRequest.Type.SUBSCRIBING
255
+ if self.status == self.Status.UNSUBSCRIBED
256
+ else MailingListSubscriberChangeRequest.Type.UNSUBSCRIBING,
257
+ **kwargs,
258
+ },
259
+ )[0]
260
+ if automatically_approve and request.status == MailingListSubscriberChangeRequest.Status.PENDING:
261
+ request.approve()
262
+ request.save()
263
+
264
+ @classmethod
265
+ def get_expired_date_subquery(
266
+ cls, mailing_list_label_field: str = "mailing_list", email_contact_label_field: str = "email_contact"
267
+ ) -> Subquery:
268
+ return Subquery(
269
+ MailingListSubscriberChangeRequest.objects.filter(
270
+ mailing_list=OuterRef(mailing_list_label_field),
271
+ email_contact=OuterRef(email_contact_label_field),
272
+ status=MailingListSubscriberChangeRequest.Status.APPROVED,
273
+ type=MailingListSubscriberChangeRequest.Type.SUBSCRIBING,
274
+ )
275
+ .order_by("-created")
276
+ .values("expiration_date")[:1],
277
+ )
278
+
279
+ @classmethod
280
+ def get_endpoint_basename(cls) -> str:
281
+ return "wbmailing:mailinglistemailcontact"
282
+
283
+
284
+ class MailingList(WBModel):
285
+ class Meta:
286
+ verbose_name = _("Mailing List")
287
+ verbose_name_plural = _("Mailing Lists")
288
+
289
+ title = models.CharField(max_length=255, verbose_name=_("Title"))
290
+ is_public = models.BooleanField(
291
+ default=False, verbose_name=_("Public"), help_text=_("If true, the factsheet is automatically subscribable")
292
+ )
293
+ email_contacts = models.ManyToManyField(
294
+ "directory.EmailContact",
295
+ through=MailingListEmailContactThroughModel,
296
+ related_name="mailing_lists",
297
+ blank=True,
298
+ verbose_name=_("Subcribers"),
299
+ )
300
+
301
+ def __str__(self) -> str:
302
+ return self.title
303
+
304
+ def unsubscribe(self, email_contact: EmailContact, **kwargs):
305
+ """
306
+ Wrapper around the corresponding method in MailingListEmailContactThroughModel. Keyword argument matches the underlying signature
307
+ """
308
+ rel = MailingListEmailContactThroughModel.objects.get_or_create(
309
+ email_contact=email_contact, mailing_list=self
310
+ )[0]
311
+ if rel.status == MailingListEmailContactThroughModel.Status.SUBSCRIBED:
312
+ rel.change_state(**kwargs)
313
+
314
+ def subscribe(self, email_contact: EmailContact, **kwargs):
315
+ """
316
+ Wrapper around the corresponding method in MailingListEmailContactThroughModel. Keyword argument matches the underlying signature
317
+ """
318
+ rel = MailingListEmailContactThroughModel.objects.get_or_create(
319
+ email_contact=email_contact, mailing_list=self
320
+ )[0]
321
+ if rel.status == MailingListEmailContactThroughModel.Status.UNSUBSCRIBED:
322
+ rel.change_state(**kwargs)
323
+
324
+ @classmethod
325
+ def get_subscribed_mailing_lists(cls, email_contact):
326
+ return cls.objects.annotate(
327
+ is_subscribe=Exists(
328
+ MailingListEmailContactThroughModel.objects.filter(
329
+ mailing_list=OuterRef("id"),
330
+ email_contact=email_contact,
331
+ status=MailingListEmailContactThroughModel.Status.SUBSCRIBED,
332
+ )
333
+ )
334
+ ).filter(is_subscribe=True)
335
+
336
+ @classmethod
337
+ def get_endpoint_basename(cls) -> str:
338
+ return "wbmailing:mailinglist"
339
+
340
+ @classmethod
341
+ def get_representation_endpoint(cls) -> str:
342
+ return "wbmailing:mailinglistrepresentation-list"
343
+
344
+ @classmethod
345
+ def get_representation_value_key(cls) -> str:
346
+ return "id"
347
+
348
+ @classmethod
349
+ def get_representation_label_key(cls) -> str:
350
+ return "{{title}}"
351
+
352
+
353
+ @receiver(post_save, sender=MailingListSubscriberChangeRequest)
354
+ def post_save_mailing_request(sender, instance, created, **kwargs):
355
+ """
356
+ MailingListSubscriberChangeRequest post_save signal: Send the notification email if needed
357
+ """
358
+
359
+ if created and instance.status == MailingListSubscriberChangeRequest.Status.PENDING.name:
360
+ for user in MailingListSubscriberChangeRequest.get_approvers():
361
+ entry_name = instance.email_contact.address
362
+ if instance.email_contact.entry:
363
+ entry_name += f" ({instance.email_contact.entry.computed_str})"
364
+
365
+ send_notification(
366
+ code="wbmailing.mailinglistsubscriberchangerequest.notify",
367
+ title=_("New in Mailing Subscription Request Change for {}").format(entry_name),
368
+ body=_("User requested to {} {} to the mailing list {}").format(
369
+ instance.type, entry_name, instance.mailing_list.title
370
+ ),
371
+ user=user,
372
+ reverse_name="wbmailing:mailinglistsubscriberchangerequest-detail",
373
+ reverse_args=[instance.id],
374
+ )
375
+
376
+
377
+ @receiver(deactivate_profile)
378
+ @receiver(post_delete, sender="directory.Entry")
379
+ @receiver(post_delete, sender="directory.Person")
380
+ @receiver(post_delete, sender="directory.Company")
381
+ def handle_user_deactivation(sender, instance, substitute_profile=None, **kwargs):
382
+ for email_contact in EmailContact.objects.filter(entry_id=instance.id):
383
+ for rel in MailingListEmailContactThroughModel.objects.filter(
384
+ email_contact=email_contact, status=MailingListEmailContactThroughModel.Status.SUBSCRIBED
385
+ ):
386
+ rel.change_state(reason=gettext("User's deactivation"), automatically_approve=True)