wbmailing 1.43.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 (64) 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/locale/de/LC_MESSAGES/django.po +1110 -0
  12. wbmailing/management/__init__.py +22 -0
  13. wbmailing/migrations/0001_initial_squashed_squashed_0008_alter_mail_bcc_email_alter_mail_cc_email_and_more.py +649 -0
  14. wbmailing/migrations/0002_delete_mailingsettings.py +16 -0
  15. wbmailing/migrations/0003_alter_mailinglistsubscriberchangerequest_options.py +25 -0
  16. wbmailing/migrations/__init__.py +0 -0
  17. wbmailing/models/__init__.py +6 -0
  18. wbmailing/models/mailing_lists.py +386 -0
  19. wbmailing/models/mails.py +895 -0
  20. wbmailing/serializers/__init__.py +19 -0
  21. wbmailing/serializers/mailing_lists.py +209 -0
  22. wbmailing/serializers/mails.py +251 -0
  23. wbmailing/tasks.py +37 -0
  24. wbmailing/templates/email_base_template.html +291 -0
  25. wbmailing/templates/mailing/maintain_mail_subsciptions.html +7 -0
  26. wbmailing/templates/mailing/manage_mailing_list_subscriptions.html +26 -0
  27. wbmailing/templates/template.html +295 -0
  28. wbmailing/templates/test.html +294 -0
  29. wbmailing/templates/workbench.html +24 -0
  30. wbmailing/templatetags/__init__.py +0 -0
  31. wbmailing/templatetags/mailing_tags.py +22 -0
  32. wbmailing/tests/__init__.py +0 -0
  33. wbmailing/tests/conftest.py +30 -0
  34. wbmailing/tests/models/__init__.py +0 -0
  35. wbmailing/tests/models/test_mailing_lists.py +297 -0
  36. wbmailing/tests/models/test_mails.py +205 -0
  37. wbmailing/tests/signals.py +124 -0
  38. wbmailing/tests/test_serializers.py +28 -0
  39. wbmailing/tests/test_tasks.py +49 -0
  40. wbmailing/tests/test_viewsets.py +216 -0
  41. wbmailing/tests/tests.py +142 -0
  42. wbmailing/urls.py +90 -0
  43. wbmailing/viewsets/__init__.py +32 -0
  44. wbmailing/viewsets/analytics.py +110 -0
  45. wbmailing/viewsets/buttons/__init__.py +10 -0
  46. wbmailing/viewsets/buttons/mailing_lists.py +91 -0
  47. wbmailing/viewsets/buttons/mails.py +98 -0
  48. wbmailing/viewsets/display/__init__.py +16 -0
  49. wbmailing/viewsets/display/mailing_lists.py +175 -0
  50. wbmailing/viewsets/display/mails.py +318 -0
  51. wbmailing/viewsets/endpoints/__init__.py +8 -0
  52. wbmailing/viewsets/endpoints/mailing_lists.py +86 -0
  53. wbmailing/viewsets/endpoints/mails.py +51 -0
  54. wbmailing/viewsets/mailing_lists.py +320 -0
  55. wbmailing/viewsets/mails.py +425 -0
  56. wbmailing/viewsets/menu/__init__.py +5 -0
  57. wbmailing/viewsets/menu/mailing_lists.py +37 -0
  58. wbmailing/viewsets/menu/mails.py +25 -0
  59. wbmailing/viewsets/titles/__init__.py +17 -0
  60. wbmailing/viewsets/titles/mailing_lists.py +63 -0
  61. wbmailing/viewsets/titles/mails.py +55 -0
  62. wbmailing-1.43.1.dist-info/METADATA +5 -0
  63. wbmailing-1.43.1.dist-info/RECORD +64 -0
  64. wbmailing-1.43.1.dist-info/WHEEL +5 -0
@@ -0,0 +1,294 @@
1
+ {% load i18n %}
2
+
3
+ <html>
4
+
5
+ <head>
6
+ <meta charset="utf-8">
7
+ <meta http-equiv="x-ua-compatible" content="ie=edge">
8
+ <title>{% translate "Atonra Research Newsletter" %}</title>
9
+ <meta name="viewport" content="width=device-width, initial-scale=1">
10
+ <style type="text/css">
11
+ /**
12
+ * Google webfonts. Recommended to include the .woff version for cross-client compatibility.
13
+ */
14
+ @media screen {
15
+ @font-face {
16
+ font-family: 'Source Sans Pro';
17
+ font-style: normal;
18
+ font-weight: 400;
19
+ src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(https://fonts.gstatic.com/s/sourcesanspro/v10/ODelI1aHBYDBqgeIAH2zlBM0YzuT7MdOe03otPbuUS0.woff) format('woff');
20
+ }
21
+
22
+ @font-face {
23
+ font-family: 'Source Sans Pro';
24
+ font-style: normal;
25
+ font-weight: 700;
26
+ src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(https://fonts.gstatic.com/s/sourcesanspro/v10/toadOcfmlt9b38dHJxOBGFkQc6VGVFSmCnC_l7QZG60.woff) format('woff');
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Avoid browser level font resizing.
32
+ * 1. Windows Mobile
33
+ * 2. iOS / OSX
34
+ */
35
+ body,
36
+ table,
37
+ td,
38
+ a {
39
+ -ms-text-size-adjust: 100%;
40
+ /* 1 */
41
+ -webkit-text-size-adjust: 100%;
42
+ /* 2 */
43
+ }
44
+
45
+ /**
46
+ * Remove extra space added to tables and cells in Outlook.
47
+ */
48
+ table,
49
+ td {
50
+ mso-table-rspace: 0pt;
51
+ mso-table-lspace: 0pt;
52
+ }
53
+
54
+ /**
55
+ * Better fluid images in Internet Explorer.
56
+ */
57
+ img {
58
+ -ms-interpolation-mode: bicubic;
59
+ }
60
+
61
+ /**
62
+ * Remove blue links for iOS devices.
63
+ */
64
+ a[x-apple-data-detectors] {
65
+ font-family: inherit !important;
66
+ font-size: inherit !important;
67
+ font-weight: inherit !important;
68
+ line-height: inherit !important;
69
+ color: inherit !important;
70
+ text-decoration: none !important;
71
+ }
72
+
73
+ /**
74
+ * Fix centering issues in Android 4.4.
75
+ */
76
+ div[style*="margin: 16px 0;"] {
77
+ margin: 0 !important;
78
+ }
79
+
80
+ body {
81
+ width: 100% !important;
82
+ height: 100% !important;
83
+ padding: 0 !important;
84
+ margin: 0 !important;
85
+ }
86
+
87
+ /**
88
+ * Collapse table borders to avoid space between cells.
89
+ */
90
+ table {
91
+ border-collapse: collapse !important;
92
+ }
93
+
94
+ a {
95
+ color: #1a82e2;
96
+ }
97
+
98
+ img {
99
+ height: auto;
100
+ line-height: 100%;
101
+ text-decoration: none;
102
+ border: 0;
103
+ outline: none;
104
+ }
105
+ </style>
106
+
107
+ </head>
108
+
109
+ <body style="background-color: #eaf2ff;">
110
+ <!-- start body -->
111
+ <table border="0" cellpadding="0" cellspacing="0" width="100%">
112
+
113
+ <!-- start logo -->
114
+ <tr>
115
+ <td align="center" bgcolor="#eaf2ff">
116
+ <!--[if (gte mso 9)|(IE)]>
117
+ <table align="center" border="0" cellpadding="0" cellspacing="0" width="800">
118
+ <tr>
119
+ <td align="center" valign="top" width="800">
120
+ <![endif]-->
121
+ <table border="0" cellpadding="0" cellspacing="0" width="100%" style="width:800px;max-width:800px;">
122
+ <tr>
123
+ <td align="center" valign="top" style="padding-top: 36px;">
124
+ <img src="https://atonra.fra1.digitaloceanspaces.com/public/atonra_banderol.png" border="0"
125
+ width="800" style="display: block; width:800px; max-width:800px;height: auto;">
126
+ </td>
127
+ </tr>
128
+ </table>
129
+ <!--[if (gte mso 9)|(IE)]>
130
+ </td>
131
+ </tr>
132
+ </table>
133
+ <![endif]-->
134
+ </td>
135
+ </tr>
136
+ <!-- end logo -->
137
+
138
+ <!-- start hero -->
139
+ <tr>
140
+ <td align="center" bgcolor="#eaf2ff">
141
+ <!--[if (gte mso 9)|(IE)]>
142
+ <table align="center" border="0" cellpadding="0" cellspacing="0" width="800">
143
+ <tr>
144
+ <td align="center" valign="top" width="800">
145
+ <![endif]-->
146
+ <table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width:800px;">
147
+ <tr>
148
+ <td align="left" bgcolor="#ffffff"
149
+ style="padding: 26px 24px 0; font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif;">
150
+ <h1
151
+ style="margin: 0; font-size: 22px; font-weight: 800; letter-spacing: -1px; line-height: 48px;">
152
+ {{salutation}},
153
+ </h1>
154
+ <p>{% blocktranslate %}The monthly factsheets for our various equity portfolios have just been updated. Please find a quick summary below.</p>
155
+ <p>If you want more detailed data or the factsheet as a pdf, please visit our interactive factsheets {% endblocktranslate %}<a href="https://atonra.stainly-bench.com/public/factsheet">{% translate "here" %}</a>.</p>
156
+ </td>
157
+ </tr>
158
+ </table>
159
+ <!--[if (gte mso 9)|(IE)]>
160
+ </td>
161
+ </tr>
162
+ </table>
163
+ <![endif]-->
164
+ </td>
165
+ </tr>
166
+ <!-- end hero -->
167
+
168
+ <!-- start copy block -->
169
+ <tr>
170
+ <td align="center" bgcolor="#eaf2ff">
171
+ <!--[if (gte mso 9)|(IE)]>
172
+ <table align="center" border="0" cellpadding="0" cellspacing="0" width="800">
173
+ <tr>
174
+ <td align="center" valign="top" width="800">
175
+ <![endif]-->
176
+ <table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width:800px;">
177
+
178
+ <!-- start copy -->
179
+ <tr>
180
+ <td align="left" bgcolor="#ffffff"
181
+ style="padding: 24px; font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 24px;">
182
+ <a href="https://atonra.stainly-bench.com/public/factsheet">
183
+ <img src={{content|safe}}
184
+ border="0" width="752"
185
+ style="display: block; width:752px; max-width:752px;height: auto;">
186
+ </a>
187
+ </td>
188
+ </tr>
189
+ <!-- end copy -->
190
+
191
+ <!-- start copy -->
192
+ <tr>
193
+ <td align="left" bgcolor="#ffffff"
194
+ style="padding: 0 24px 24px 24px; font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 24px; border-bottom: 3px solid #d4dadf">
195
+ <p style="margin: 0;">{% translate "Best Regards," %}</p>
196
+ <p style="margin: 0;">{% translate "The AtonRâ Partners Team." %}</p>
197
+ </td>
198
+ </tr>
199
+ <!-- end copy -->
200
+
201
+ </table>
202
+ <!--[if (gte mso 9)|(IE)]>
203
+ </td>
204
+ </tr>
205
+ </table>
206
+ <![endif]-->
207
+ </td>
208
+ </tr>
209
+ <!-- end copy block -->
210
+
211
+ <!-- start footer -->
212
+ <tr>
213
+ <td align="center" bgcolor="#eaf2ff" style="padding: 24px;">
214
+ <!--[if (gte mso 9)|(IE)]>
215
+ <table align="center" border="0" cellpadding="0" cellspacing="0" width="800">
216
+ <tr>
217
+ <td align="center" valign="top" width="800">
218
+ <![endif]-->
219
+ <table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width:800px;">
220
+
221
+ <!-- start permission -->
222
+ <tr>
223
+ <td align="center" bgcolor="#eaf2ff"
224
+ style="padding: 12px 24px; font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 20px; color: #666;">
225
+ <p style="margin: 0;" style="text-align: center">
226
+ <b>{% blocktranslate %}AtonRâ Partners SA</b><br />
227
+ 7, rue de la Croix d’Or, 1204 Geneva, Switzerland{% endblocktranslate %}<br />
228
+ </p>
229
+ <table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width:800px;">
230
+ <tr>
231
+ <td align="center" bgcolor="#eaf2ff"
232
+ style="width:33%; font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 20px; color: #666;">
233
+ 🌍: <a href="https://www.atonra.ch" target="_blank">https://www.atonra.ch</a>
234
+ </td>
235
+ <td align="center" bgcolor="#eaf2ff"
236
+ style="width:33%; font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 20px; color: #666;">
237
+ @: <a href="mailto:team@atonra.ch">team@atonra.ch</a>
238
+ </td>
239
+ <td align="center" bgcolor="#eaf2ff"
240
+ style="width:33%; font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 20px; color: #666;">
241
+ ☎️: <a href="tel:+41-229-061-616">+41 22 906 16 16</a>
242
+ </td>
243
+ </tr>
244
+ </table>
245
+ </td>
246
+ </tr>
247
+ <tr>
248
+ <td align="center" bgcolor="#eaf2ff"
249
+ style="padding: 12px 24px; font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 20px; color: #666;">
250
+ <p style="margin: 0;" style="text-align: center">{% blocktranslate %}<b>About AtonRâ Partners:</b> AtonRâ
251
+ Partners is an asset management
252
+ company, founded in
253
+ 2004 with head office in Geneva, incorporated under Swiss law, duly approved by the
254
+ Swiss Financial
255
+ Market Supervisory Authority (FINMA) under the Swiss Collective Investment Schemes Act.
256
+ AtonRâ
257
+ Partners is a conviction-driven asset manager combining industrial and scientific
258
+ research with
259
+ financial analysis. AtonRâ Partners focuses on long-term trends powerful enough to be
260
+ turned into
261
+ thematic equity portfolios.</p>{% endblocktranslate %}
262
+ </td>
263
+ </tr>
264
+ <!-- end permission -->
265
+
266
+ <!-- start unsubscribe -->
267
+ <tr>
268
+ <td align="center" bgcolor="#eaf2ff"
269
+ style="padding: 12px 24px; font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 20px; color: #666;">
270
+ {% if unsubscribe %}
271
+ <p style="margin: 0;" style="text-align: center">{% translate "If you receive this email because you are
272
+ subscribed to our mailing list:
273
+ Please feel free to " %}{{unsubscribe|safe}} </p>
274
+ {% endif %}
275
+ </td>
276
+ </tr>
277
+ <!-- end unsubscribe -->
278
+
279
+ </table>
280
+ <!--[if (gte mso 9)|(IE)]>
281
+ </td>
282
+ </tr>
283
+ </table>
284
+ <![endif]-->
285
+ </td>
286
+ </tr>
287
+ <!-- end footer -->
288
+
289
+ </table>
290
+ <!-- end body -->
291
+
292
+ </body>
293
+
294
+ </html>
@@ -0,0 +1,24 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>{{ title }}</title>
6
+ <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet">
7
+ </head>
8
+ <body>
9
+ <div>
10
+ {% block content %}
11
+ {% endblock content %}
12
+ </div>
13
+ <script
14
+ src="https://code.jquery.com/jquery-3.4.1.slim.min.js"
15
+ integrity="sha256-pasqAKBDmFT4eHoN2ndd6lN370kFiGUFyTiUHWhU7k8="
16
+ crossorigin="anonymous">
17
+ </script>
18
+ <script
19
+ src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.bundle.min.js"
20
+ integrity="sha384-xrRywqdh3PHs8keKZN+8zzc5TX0GRTLCcmivcbNJWm2rs5C8PRhcEn3czEjhAO9o"
21
+ crossorigin="anonymous">
22
+ </script>
23
+ </body>
24
+ </html>
File without changes
@@ -0,0 +1,22 @@
1
+ from django import template
2
+ from django.utils.html import strip_tags
3
+
4
+ register = template.Library()
5
+
6
+
7
+ @register.filter
8
+ def stripAndsplit(string, sep):
9
+ """Return the string split by sep.
10
+
11
+ Example usage: {{ value|split:"/" }}
12
+ """
13
+ return strip_tags(string).split(sep)
14
+
15
+
16
+ @register.filter
17
+ def strip(string):
18
+ """Return the string split by sep.
19
+
20
+ Example usage: {{ value|split:"/" }}
21
+ """
22
+ return strip_tags(string)
File without changes
@@ -0,0 +1,30 @@
1
+ from django.apps import apps
2
+ from django.db.models.signals import pre_migrate
3
+ from pytest_factoryboy import register
4
+ from wbcore.contrib.authentication.factories import UserFactory
5
+ from wbcore.contrib.directory.factories import EmailContactFactory, PersonFactory
6
+ from wbcore.contrib.geography.tests.signals import app_pre_migration
7
+ from wbmailing.factories import (
8
+ MailEventFactory,
9
+ MailFactory,
10
+ MailingListEmailContactThroughModelFactory,
11
+ MailingListFactory,
12
+ MailingListSubscriberChangeRequestFactory,
13
+ MailTemplateFactory,
14
+ MassMailFactory,
15
+ )
16
+
17
+ register(EmailContactFactory)
18
+ register(PersonFactory)
19
+ register(UserFactory)
20
+ register(MailingListSubscriberChangeRequestFactory)
21
+ register(MailingListEmailContactThroughModelFactory)
22
+ register(MailingListFactory)
23
+ register(MassMailFactory)
24
+ register(MailFactory)
25
+ register(MailEventFactory)
26
+ register(MailTemplateFactory)
27
+
28
+ from .signals import *
29
+
30
+ pre_migrate.connect(app_pre_migration, sender=apps.get_app_config("wbmailing"))
File without changes
@@ -0,0 +1,297 @@
1
+ import pytest
2
+ from django.contrib.auth.models import Permission
3
+ from faker import Faker
4
+ from wbcore.contrib.authentication.factories import UserFactory
5
+ from wbmailing.models.mailing_lists import (
6
+ MailingList,
7
+ MailingListEmailContactThroughModel,
8
+ MailingListSubscriberChangeRequest,
9
+ )
10
+
11
+ fake = Faker()
12
+
13
+
14
+ @pytest.mark.django_db
15
+ class TestMailingListSubscriberChangeRequest:
16
+ @pytest.fixture
17
+ def user_admin(self):
18
+ user = UserFactory.create()
19
+ perm = Permission.objects.get(codename="administrate_mailinglistsubscriberchangerequest")
20
+ user.user_permissions.add(perm)
21
+ return user
22
+
23
+ def test_init(self, mailing_list_subscriber_change_request):
24
+ """
25
+ Test basics creation logic:
26
+ - relationship creation from email contact and mailing list
27
+ """
28
+ assert mailing_list_subscriber_change_request
29
+ assert mailing_list_subscriber_change_request.status == MailingListSubscriberChangeRequest.Status.PENDING
30
+ rel = mailing_list_subscriber_change_request.relationship
31
+ assert rel.email_contact == mailing_list_subscriber_change_request.email_contact
32
+ assert rel.mailing_list == mailing_list_subscriber_change_request.mailing_list
33
+ assert rel.status == MailingListEmailContactThroughModel.Status.UNSUBSCRIBED
34
+
35
+ @pytest.mark.parametrize("mailing_list__is_public", [True, False])
36
+ def test_init_public_mailing_list(self, mailing_list_subscriber_change_request_factory, mailing_list):
37
+ """
38
+ Test if a request subscription creation to a public mailing list is automatically approved
39
+ """
40
+ request = mailing_list_subscriber_change_request_factory.create(mailing_list=mailing_list)
41
+ assert request.type == MailingListSubscriberChangeRequest.Type.SUBSCRIBING
42
+ if mailing_list.is_public:
43
+ assert request.status == MailingListSubscriberChangeRequest.Status.APPROVED
44
+ else:
45
+ assert request.status == MailingListSubscriberChangeRequest.Status.PENDING
46
+
47
+ @pytest.mark.parametrize(
48
+ "mailing_list_subscriber_change_request__type, description",
49
+ [
50
+ (MailingListSubscriberChangeRequest.Type.SUBSCRIBING, fake.sentence()),
51
+ (MailingListSubscriberChangeRequest.Type.UNSUBSCRIBING, fake.sentence()),
52
+ ],
53
+ )
54
+ def test_approve(self, mailing_list_subscriber_change_request, user, description):
55
+ mailing_list_subscriber_change_request.approve(by=user, description=description)
56
+ if mailing_list_subscriber_change_request.type == MailingListSubscriberChangeRequest.Type.SUBSCRIBING:
57
+ assert (
58
+ mailing_list_subscriber_change_request.relationship.status
59
+ == MailingListEmailContactThroughModel.Status.SUBSCRIBED
60
+ )
61
+ else:
62
+ assert (
63
+ mailing_list_subscriber_change_request.relationship.status
64
+ == MailingListEmailContactThroughModel.Status.UNSUBSCRIBED
65
+ )
66
+ assert mailing_list_subscriber_change_request.approver == user.profile
67
+ assert mailing_list_subscriber_change_request.reason == description
68
+
69
+ @pytest.mark.parametrize("description", [fake.sentence()])
70
+ def test_deny(self, mailing_list_subscriber_change_request, user, description):
71
+ """
72
+ Coverage test and unit test on description and approver from action trigerer
73
+ """
74
+ mailing_list_subscriber_change_request.deny(by=user, description=description)
75
+ mailing_list_subscriber_change_request.save()
76
+ assert mailing_list_subscriber_change_request.approver == user.profile
77
+ assert mailing_list_subscriber_change_request.reason == description
78
+
79
+ @pytest.mark.parametrize(
80
+ "mailing_list_email_contact_through_model__status, type, res",
81
+ [
82
+ (
83
+ MailingListEmailContactThroughModel.Status.SUBSCRIBED,
84
+ MailingListSubscriberChangeRequest.Type.UNSUBSCRIBING,
85
+ False,
86
+ ),
87
+ (
88
+ MailingListEmailContactThroughModel.Status.SUBSCRIBED,
89
+ MailingListSubscriberChangeRequest.Type.SUBSCRIBING,
90
+ False,
91
+ ),
92
+ (
93
+ MailingListEmailContactThroughModel.Status.UNSUBSCRIBED,
94
+ MailingListSubscriberChangeRequest.Type.UNSUBSCRIBING,
95
+ False,
96
+ ),
97
+ (
98
+ MailingListEmailContactThroughModel.Status.UNSUBSCRIBED,
99
+ MailingListSubscriberChangeRequest.Type.SUBSCRIBING,
100
+ True,
101
+ ),
102
+ ],
103
+ )
104
+ def test_subscribing(
105
+ self, mailing_list_email_contact_through_model, mailing_list_subscriber_change_request_factory, type, res
106
+ ):
107
+ """
108
+ Basic property result check
109
+ """
110
+ request = mailing_list_subscriber_change_request_factory.create(
111
+ type=type,
112
+ relationship=mailing_list_email_contact_through_model,
113
+ email_contact=mailing_list_email_contact_through_model.email_contact,
114
+ mailing_list=mailing_list_email_contact_through_model.mailing_list,
115
+ )
116
+ assert request.subscribing == res
117
+
118
+ def test_get_expired_date_subquery(
119
+ self, email_contact, mailing_list, mailing_list_subscriber_change_request_factory
120
+ ):
121
+ """
122
+ Check that expired date is the last approved and subscribing mailing change request
123
+ """
124
+ mailing_list_subscriber_change_request_factory.create(
125
+ type=MailingListSubscriberChangeRequest.Type.SUBSCRIBING,
126
+ status=MailingListSubscriberChangeRequest.Status.APPROVED,
127
+ mailing_list=mailing_list,
128
+ email_contact=email_contact,
129
+ expiration_date=fake.date_object(),
130
+ ) # oldest valid request
131
+ req = mailing_list_subscriber_change_request_factory.create(
132
+ type=MailingListSubscriberChangeRequest.Type.SUBSCRIBING,
133
+ status=MailingListSubscriberChangeRequest.Status.APPROVED,
134
+ mailing_list=mailing_list,
135
+ email_contact=email_contact,
136
+ expiration_date=fake.date_object(),
137
+ ) # expected expiration time request
138
+ mailing_list_subscriber_change_request_factory.create(
139
+ type=MailingListSubscriberChangeRequest.Type.UNSUBSCRIBING,
140
+ status=MailingListSubscriberChangeRequest.Status.APPROVED,
141
+ mailing_list=mailing_list,
142
+ email_contact=email_contact,
143
+ expiration_date=fake.date_object(),
144
+ ) # Unvalid request because not subscribing
145
+ mailing_list_subscriber_change_request_factory.create(
146
+ type=MailingListSubscriberChangeRequest.Type.SUBSCRIBING,
147
+ status=MailingListSubscriberChangeRequest.Status.PENDING,
148
+ mailing_list=mailing_list,
149
+ email_contact=email_contact,
150
+ expiration_date=fake.date_object(),
151
+ ) # Unvalid request because pending
152
+ assert (
153
+ MailingListEmailContactThroughModel.objects.annotate(
154
+ expiration_date=MailingListEmailContactThroughModel.get_expired_date_subquery()
155
+ )
156
+ .filter(mailing_list=mailing_list, email_contact=email_contact)
157
+ .values_list("expiration_date", flat=True)[0]
158
+ == req.expiration_date
159
+ )
160
+
161
+ def test_get_approvers(self, user, user_admin):
162
+ """
163
+ Test that approvers are the proper user with admin rights
164
+ """
165
+ assert set(MailingListSubscriberChangeRequest.get_approvers()) == {user_admin}
166
+ assert not MailingListSubscriberChangeRequest.get_approvers().filter(id=user.id).exists()
167
+
168
+
169
+ @pytest.mark.django_db
170
+ class TestMailingListEmailContactThroughModel:
171
+ def test_init(self, mailing_list_email_contact_through_model):
172
+ assert mailing_list_email_contact_through_model
173
+
174
+ def test_change_state(self, mailing_list_email_contact_through_model):
175
+ initial_status = mailing_list_email_contact_through_model.status
176
+ mailing_list_email_contact_through_model.change_state()
177
+ assert mailing_list_email_contact_through_model.status == initial_status
178
+ assert (
179
+ mailing_list_email_contact_through_model.requests.filter(
180
+ status=MailingListSubscriberChangeRequest.Status.PENDING
181
+ ).count()
182
+ == 1
183
+ )
184
+ mailing_list_email_contact_through_model.change_state(automatically_approve=True)
185
+ mailing_list_email_contact_through_model.refresh_from_db()
186
+ assert not mailing_list_email_contact_through_model.requests.filter(
187
+ status=MailingListSubscriberChangeRequest.Status.PENDING
188
+ ).exists()
189
+ assert mailing_list_email_contact_through_model.status != initial_status
190
+
191
+
192
+ @pytest.mark.django_db
193
+ class TestMailingList:
194
+ def test_init(self, mailing_list):
195
+ assert mailing_list
196
+
197
+ @pytest.mark.parametrize(
198
+ "mailing_list_email_contact_through_model__status", [MailingListEmailContactThroughModel.Status.SUBSCRIBED]
199
+ )
200
+ def test_unsubscription(self, mailing_list_email_contact_through_model):
201
+ """
202
+ Test unsubscription
203
+ """
204
+ email_contact = mailing_list_email_contact_through_model.email_contact
205
+ mailing_list = mailing_list_email_contact_through_model.mailing_list
206
+
207
+ mailing_list.unsubscribe(email_contact)
208
+ req = mailing_list_email_contact_through_model.requests.get(
209
+ status=MailingListSubscriberChangeRequest.Status.PENDING
210
+ )
211
+ assert (
212
+ mailing_list_email_contact_through_model.status == MailingListEmailContactThroughModel.Status.SUBSCRIBED
213
+ ) # We expect the contact to still be subscribed because the unsubscription request is still pending
214
+ req.approve()
215
+ mailing_list_email_contact_through_model.refresh_from_db()
216
+ assert (
217
+ mailing_list_email_contact_through_model.status == MailingListEmailContactThroughModel.Status.UNSUBSCRIBED
218
+ )
219
+
220
+ @pytest.mark.parametrize(
221
+ "mailing_list_email_contact_through_model__status", [MailingListEmailContactThroughModel.Status.SUBSCRIBED]
222
+ )
223
+ def test_unsubscription_automatically_approve(self, mailing_list_email_contact_through_model):
224
+ """
225
+ Test automatically approved unsubscription change request
226
+ """
227
+ email_contact = mailing_list_email_contact_through_model.email_contact
228
+ mailing_list = mailing_list_email_contact_through_model.mailing_list
229
+
230
+ mailing_list.unsubscribe(email_contact, automatically_approve=True)
231
+ mailing_list_email_contact_through_model.refresh_from_db()
232
+ assert (
233
+ mailing_list_email_contact_through_model.status == MailingListEmailContactThroughModel.Status.UNSUBSCRIBED
234
+ )
235
+
236
+ @pytest.mark.parametrize(
237
+ "mailing_list_email_contact_through_model__status", [MailingListEmailContactThroughModel.Status.UNSUBSCRIBED]
238
+ )
239
+ def test_subscription(self, mailing_list_email_contact_through_model):
240
+ """
241
+ Test subscription change request
242
+ """
243
+ email_contact = mailing_list_email_contact_through_model.email_contact
244
+ mailing_list = mailing_list_email_contact_through_model.mailing_list
245
+
246
+ mailing_list.subscribe(email_contact)
247
+ req = mailing_list_email_contact_through_model.requests.get(
248
+ status=MailingListSubscriberChangeRequest.Status.PENDING
249
+ )
250
+ assert (
251
+ mailing_list_email_contact_through_model.status == MailingListEmailContactThroughModel.Status.UNSUBSCRIBED
252
+ ) # We expect the contact to still be subscribed because the unsubscription request is still pending
253
+ req.approve()
254
+ mailing_list_email_contact_through_model.refresh_from_db()
255
+ assert mailing_list_email_contact_through_model.status == MailingListEmailContactThroughModel.Status.SUBSCRIBED
256
+
257
+ @pytest.mark.parametrize(
258
+ "mailing_list_email_contact_through_model__status", [MailingListEmailContactThroughModel.Status.UNSUBSCRIBED]
259
+ )
260
+ def test_subscription_automatically_approve(self, mailing_list_email_contact_through_model):
261
+ """
262
+ Test automatically approved subscription change request
263
+ """
264
+ email_contact = mailing_list_email_contact_through_model.email_contact
265
+ mailing_list = mailing_list_email_contact_through_model.mailing_list
266
+
267
+ mailing_list.subscribe(email_contact, automatically_approve=True)
268
+ mailing_list_email_contact_through_model.refresh_from_db()
269
+ assert mailing_list_email_contact_through_model.status == MailingListEmailContactThroughModel.Status.SUBSCRIBED
270
+
271
+ def test_get_subscribed_mailing_lists(self, mailing_list_email_contact_through_model_factory):
272
+ """
273
+ Test subscribed mailing list for a email contact.
274
+ """
275
+ rel_e1_ml1_subscribed = mailing_list_email_contact_through_model_factory.create(
276
+ status=MailingListEmailContactThroughModel.Status.SUBSCRIBED
277
+ )
278
+ e1 = rel_e1_ml1_subscribed.email_contact
279
+ m1 = rel_e1_ml1_subscribed.mailing_list
280
+
281
+ rel_e1_rel_ml2_unsubscribed = mailing_list_email_contact_through_model_factory.create(
282
+ email_contact=e1, status=MailingListEmailContactThroughModel.Status.UNSUBSCRIBED
283
+ ) # We expect e1 to not show up in the valid emails queryset
284
+ m2 = rel_e1_rel_ml2_unsubscribed.mailing_list
285
+
286
+ rel_e2_ml1_subscribed = mailing_list_email_contact_through_model_factory.create(
287
+ mailing_list=m1, status=MailingListEmailContactThroughModel.Status.SUBSCRIBED
288
+ )
289
+ e2 = rel_e2_ml1_subscribed.email_contact
290
+ mailing_list_email_contact_through_model_factory.create(
291
+ email_contact=e2, mailing_list=m2, status=MailingListEmailContactThroughModel.Status.SUBSCRIBED
292
+ )
293
+
294
+ assert set(MailingList.get_subscribed_mailing_lists(e1)) == {
295
+ m1,
296
+ }
297
+ assert set(MailingList.get_subscribed_mailing_lists(e2)) == {m1, m2}