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,320 @@
1
+ from django.contrib.messages import warning
2
+ from django.contrib.postgres.aggregates import StringAgg
3
+ from django.db.models import Count, Exists, F, OuterRef, Q, Subquery
4
+ from django.db.models.functions import Coalesce
5
+ from django.shortcuts import get_object_or_404, redirect, render
6
+ from django.utils.functional import cached_property
7
+ from django.utils.translation import gettext_lazy as _
8
+ from django.views.generic import View
9
+ from rest_framework.decorators import action
10
+ from rest_framework.response import Response
11
+ from wbcore import serializers as wb_serializers
12
+ from wbcore import viewsets
13
+ from wbcore.contrib.directory.models import Company, EmailContact, Entry
14
+ from wbmailing import models, serializers
15
+ from wbmailing.filters import (
16
+ EmailContactMailingListFilterSet,
17
+ MailingListEmailContactThroughModelModelFilterSet,
18
+ MailingListFilterSet,
19
+ )
20
+ from wbmailing.models import (
21
+ MailingListEmailContactThroughModel,
22
+ MailingListSubscriberChangeRequest,
23
+ )
24
+ from wbmailing.viewsets.buttons import (
25
+ MailingListButtonConfig,
26
+ MailingListEmailContactThroughModelButtonConfig,
27
+ MailingListSubcriptionRequestButtonConfig,
28
+ )
29
+ from wbmailing.viewsets.display import (
30
+ EmailContactMailingListDisplayConfig,
31
+ MailingListDisplayConfig,
32
+ MailingListEntryDisplayConfig,
33
+ MailingListSubscriberChangeRequestDisplayConfig,
34
+ MailingListSubscriberRequestEntryDisplayConfig,
35
+ MailingListSubscriberRequestMailingListDisplayConfig,
36
+ )
37
+ from wbmailing.viewsets.endpoints import (
38
+ EmailContactMailingListEndpointConfig,
39
+ MailingListEntryEndpointConfig,
40
+ MailingListSubscriberRequestEntryEndpointConfig,
41
+ MailingListSubscriberRequestMailingListEndpointConfig,
42
+ )
43
+ from wbmailing.viewsets.titles import (
44
+ EmailContactMailingListTitleConfig,
45
+ MailingListEntryTitleConfig,
46
+ MailingListSubscriberChangeRequestTitleConfig,
47
+ MailingListSubscriberRequestEntryTitleConfig,
48
+ MailingListTitleConfig,
49
+ )
50
+
51
+
52
+ class MailingListRepresentationViewSet(viewsets.RepresentationViewSet):
53
+ IDENTIFIER = "wbmailing:mailinglist"
54
+
55
+ ordering_fields = ("title",)
56
+ search_fields = ("title",)
57
+ queryset = models.MailingList.objects.all()
58
+ serializer_class = serializers.MailingListRepresentationSerializer
59
+ filterset_class = MailingListFilterSet
60
+
61
+
62
+ class MailingListModelViewSet(viewsets.ModelViewSet):
63
+ IDENTIFIER = "wbmailing:mailinglist"
64
+
65
+ queryset = models.MailingList.objects.all()
66
+
67
+ def get_serializer_class(self):
68
+ if getattr(self, "action", None) == "list":
69
+ return serializers.MailingListListSerializer
70
+ return serializers.MailingListModelSerializer
71
+
72
+ filterset_class = MailingListFilterSet
73
+
74
+ ordering_fields = ordering = ("title",)
75
+ search_fields = ("title",)
76
+
77
+ display_config_class = MailingListDisplayConfig
78
+ title_config_class = MailingListTitleConfig
79
+ button_config_class = MailingListButtonConfig
80
+
81
+ def get_queryset(self):
82
+ qs = self.filter_queryset(
83
+ models.MailingList.objects.annotate(
84
+ nb_subscribers=Coalesce(
85
+ Subquery(
86
+ MailingListEmailContactThroughModel.objects.filter(
87
+ mailing_list=OuterRef("pk"), status=MailingListEmailContactThroughModel.Status.SUBSCRIBED
88
+ )
89
+ .values("mailing_list")
90
+ .annotate(c=Count("mailing_list"))
91
+ .values("c")[:1]
92
+ ),
93
+ 0,
94
+ )
95
+ )
96
+ )
97
+ qs = qs.prefetch_related("email_contacts")
98
+ return qs
99
+
100
+
101
+ class MailingListSubscriberChangeRequestModelViewSet(viewsets.ModelViewSet):
102
+ queryset = (
103
+ models.MailingListSubscriberChangeRequest.objects.select_related("email_contact")
104
+ .select_related("mailing_list")
105
+ .select_related("requester")
106
+ .select_related("approver")
107
+ ).annotate(entry_repr=F("email_contact__entry__computed_str"))
108
+
109
+ serializer_class = serializers.MailingListSubscriberChangeRequestModelSerializer
110
+
111
+ search_fields = ("email_contact__address", "mailing_list__title")
112
+ ordering_fields = ("email_contact__address", "mailing_list__title", "created")
113
+ ordering = ("-created",)
114
+ filterset_fields = {
115
+ "email_contact": ["exact"],
116
+ "mailing_list": ["exact"],
117
+ "requester": ["exact"],
118
+ "created": ["gte", "exact", "lte"],
119
+ "status": ["exact"],
120
+ }
121
+
122
+ display_config_class = MailingListSubscriberChangeRequestDisplayConfig
123
+ title_config_class = MailingListSubscriberChangeRequestTitleConfig
124
+ button_config_class = MailingListSubcriptionRequestButtonConfig
125
+
126
+ def add_messages(self, request, instance=None, **kwargs):
127
+ if instance:
128
+ _type = (
129
+ "subscribed"
130
+ if instance.type == MailingListSubscriberChangeRequest.Type.SUBSCRIBING
131
+ else "unsubscribed"
132
+ )
133
+ warning(
134
+ request,
135
+ f"Upon approval, This change request will {_type} {instance.email_contact.address} to {instance.mailing_list.title}",
136
+ )
137
+
138
+ @action(detail=False, methods=["GET"])
139
+ def approveall(self, request, pk=None):
140
+ for request in models.MailingListSubscriberChangeRequest.objects.filter(
141
+ status=models.MailingListSubscriberChangeRequest.Status.PENDING
142
+ ).all():
143
+ request.approve()
144
+ request.save()
145
+ return Response({"send": True})
146
+
147
+
148
+ class MailingListSubscriberRequestMailingListModelViewSet(MailingListSubscriberChangeRequestModelViewSet):
149
+ IDENTIFIER = "wbmailing:mailing_list-mailinglistsubscriberchangerequest"
150
+
151
+ display_config_class = MailingListSubscriberRequestMailingListDisplayConfig
152
+ endpoint_config_class = MailingListSubscriberRequestMailingListEndpointConfig
153
+
154
+ def get_queryset(self):
155
+ return super().get_queryset().filter(mailing_list__id=self.kwargs["mailing_list_id"])
156
+
157
+
158
+ class MailingListEmailContactThroughModelModelViewSet(viewsets.ModelViewSet):
159
+ queryset = MailingListEmailContactThroughModel.objects.select_related(
160
+ "email_contact",
161
+ "mailing_list",
162
+ )
163
+ search_fields = ["email_contact__address", "email_contact__entry__computed_str"]
164
+ button_config_class = MailingListEmailContactThroughModelButtonConfig
165
+ serializer_class = serializers.MailingListEmailContactThroughModelModelSerializer
166
+
167
+ ordering = ["email_contact__address", "mailing_list__title"]
168
+ ordering_fields = ["email_contact__address", "status", "mailing_list__title"]
169
+
170
+ filterset_class = MailingListEmailContactThroughModelModelFilterSet
171
+
172
+ @action(detail=True, methods=["GET", "PATCH"])
173
+ def removeexpirationdate(self, request, mailing_list_id=None, pk=None, **kwargs):
174
+ through = get_object_or_404(MailingListEmailContactThroughModel, id=pk)
175
+ if (qs := through.requests.filter(expiration_date__isnull=False)).exists():
176
+ last_request = qs.latest("created")
177
+ last_request.expiration_date = None
178
+ last_request.save()
179
+ return Response({"send": True})
180
+
181
+ @action(detail=True, methods=["GET", "PATCH"])
182
+ def delete(self, request, mailing_list_id=None, pk=None, **kwargs):
183
+ through = get_object_or_404(MailingListEmailContactThroughModel, id=pk)
184
+ through.delete()
185
+ return Response({"send": True})
186
+
187
+ @action(detail=True, methods=["GET", "PATCH"])
188
+ def unsubscribe(self, request, pk=None, **kwargs):
189
+ through = get_object_or_404(MailingListEmailContactThroughModel, id=pk)
190
+ if through.status == MailingListEmailContactThroughModel.Status.SUBSCRIBED:
191
+ through.change_state(
192
+ reason=_("Unsubscribed by {}").format(str(request.user)),
193
+ requester=request.user.profile,
194
+ approver=request.user.profile,
195
+ automatically_approve=True,
196
+ )
197
+ return Response({"send": True})
198
+
199
+ def get_queryset(self):
200
+ return (
201
+ super()
202
+ .get_queryset()
203
+ .annotate(
204
+ expiration_date=MailingListEmailContactThroughModel.get_expired_date_subquery(),
205
+ in_charge=StringAgg(F("email_contact__entry__relationship_managers__computed_str"), delimiter=", "),
206
+ is_pending_request_change=Exists(
207
+ MailingListSubscriberChangeRequest.objects.filter(
208
+ relationship=OuterRef("pk"), status=MailingListSubscriberChangeRequest.Status.PENDING
209
+ )
210
+ ),
211
+ is_public=F("mailing_list__is_public"),
212
+ )
213
+ )
214
+
215
+
216
+ class EmailContactMailingListModelViewSet(MailingListEmailContactThroughModelModelViewSet):
217
+ filterset_class = EmailContactMailingListFilterSet
218
+
219
+ display_config_class = EmailContactMailingListDisplayConfig
220
+ title_config_class = EmailContactMailingListTitleConfig
221
+ endpoint_config_class = EmailContactMailingListEndpointConfig
222
+
223
+ def get_queryset(self):
224
+ return super().get_queryset().filter(mailing_list=self.kwargs["mailing_list_id"])
225
+
226
+
227
+ class MailingListEntryModelViewSet(MailingListEmailContactThroughModelModelViewSet):
228
+ filterset_fields = {"mailing_list": ["exact"], "status": ["exact"]}
229
+
230
+ display_config_class = MailingListEntryDisplayConfig
231
+ title_config_class = MailingListEntryTitleConfig
232
+ endpoint_config_class = MailingListEntryEndpointConfig
233
+
234
+ @cached_property
235
+ def casted_entry(self):
236
+ return get_object_or_404(Entry, id=self.kwargs["entry_id"]).get_casted_entry()
237
+
238
+ @cached_property
239
+ def primary_email(self):
240
+ return EmailContact.objects.filter(entry=self.kwargs["entry_id"], primary=True).first()
241
+
242
+ def add_messages(self, request, instance=None, **kwargs):
243
+ if not self.primary_email:
244
+ warning(
245
+ request,
246
+ "This person does not have a primary email. Adds one first before adding them to a mailing list.",
247
+ )
248
+
249
+ def get_queryset(self):
250
+ qs = super().get_queryset()
251
+ entry = self.casted_entry
252
+ if isinstance(entry, Company):
253
+ email_contacts = EmailContact.objects.filter(
254
+ Q(entry=entry) | Q(entry__in=entry.employees.all())
255
+ ).distinct()
256
+ qs = qs.filter(email_contact__in=email_contacts)
257
+ else:
258
+ qs = qs.filter(email_contact__entry=entry)
259
+ return qs
260
+
261
+
262
+ class MailingListSubscriberRequestEntryModelViewSet(MailingListSubscriberChangeRequestModelViewSet):
263
+ IDENTIFIER = "wbmailing:entry-mailinglistsubscriberchangerequest"
264
+ display_config_class = MailingListSubscriberRequestEntryDisplayConfig
265
+ title_config_class = MailingListSubscriberRequestEntryTitleConfig
266
+ endpoint_config_class = MailingListSubscriberRequestEntryEndpointConfig
267
+
268
+ @cached_property
269
+ def primary_email(self):
270
+ return EmailContact.objects.filter(entry=self.kwargs["entry_id"], primary=True).first()
271
+
272
+ def get_serializer_class(self):
273
+ if self.primary_email:
274
+
275
+ class Serializer(serializers.MailingListSubscriberChangeRequestModelSerializer):
276
+ email_contact = wb_serializers.PrimaryKeyRelatedField(
277
+ queryset=EmailContact.objects.filter(entry=self.kwargs["entry_id"]),
278
+ label=_("Email"),
279
+ many=False,
280
+ default=self.primary_email,
281
+ )
282
+
283
+ else:
284
+
285
+ class Serializer(serializers.MailingListSubscriberChangeRequestModelSerializer):
286
+ email_contact = wb_serializers.PrimaryKeyRelatedField(label=_("No Emails"), read_only=True)
287
+
288
+ return Serializer
289
+
290
+ def get_queryset(self):
291
+ return super().get_queryset().filter(email_contact__entry__id=self.kwargs["entry_id"])
292
+
293
+
294
+ #################
295
+ # TODO Old system
296
+ #################
297
+
298
+
299
+ class ManageMailingListSubscriptions(View):
300
+ def get(self, request, email_contact_id, *args, **kwargs):
301
+ email_contact = get_object_or_404(EmailContact, id=email_contact_id)
302
+ context = {
303
+ "title": _("Manage Mailing List Subscriptions"),
304
+ "email_contact": email_contact,
305
+ "mailing_lists": models.MailingList.get_subscribed_mailing_lists(email_contact),
306
+ }
307
+ return render(request, "mailing/manage_mailing_list_subscriptions.html", context=context)
308
+
309
+
310
+ class UnsubscribeView(View):
311
+ def get(self, request, email_contact_id, mailing_list_id, *args, **kwargs):
312
+ email_contact = get_object_or_404(EmailContact, id=email_contact_id)
313
+ mailing_list = get_object_or_404(models.MailingList, id=mailing_list_id)
314
+ mailing_list.unsubscribe(
315
+ email_contact, reason=_("The user requested to be unsubscribed."), automatically_approve=True
316
+ )
317
+ return redirect(
318
+ "wbmailing:manage_mailing_list_subscriptions",
319
+ email_contact_id=email_contact_id,
320
+ )