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.
- 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/locale/de/LC_MESSAGES/django.po +1110 -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/templates/email_base_template.html +291 -0
- wbmailing/templates/mailing/maintain_mail_subsciptions.html +7 -0
- wbmailing/templates/mailing/manage_mailing_list_subscriptions.html +26 -0
- wbmailing/templates/template.html +295 -0
- wbmailing/templates/test.html +294 -0
- wbmailing/templates/workbench.html +24 -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-1.43.1.dist-info/METADATA +5 -0
- wbmailing-1.43.1.dist-info/RECORD +64 -0
- 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}
|