codeforlife-portal 7.0.0__py2.py3-none-any.whl → 7.1.0__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 codeforlife-portal might be problematic. Click here for more details.
- cfl_common/common/mail.py +31 -6
- {codeforlife_portal-7.0.0.dist-info → codeforlife_portal-7.1.0.dist-info}/METADATA +2 -2
- {codeforlife_portal-7.0.0.dist-info → codeforlife_portal-7.1.0.dist-info}/RECORD +10 -10
- portal/__init__.py +1 -1
- portal/tests/test_views.py +101 -0
- portal/urls.py +15 -0
- portal/views/cron/user.py +158 -15
- {codeforlife_portal-7.0.0.dist-info → codeforlife_portal-7.1.0.dist-info}/LICENSE.md +0 -0
- {codeforlife_portal-7.0.0.dist-info → codeforlife_portal-7.1.0.dist-info}/WHEEL +0 -0
- {codeforlife_portal-7.0.0.dist-info → codeforlife_portal-7.1.0.dist-info}/top_level.txt +0 -0
cfl_common/common/mail.py
CHANGED
|
@@ -27,6 +27,9 @@ campaign_ids = {
|
|
|
27
27
|
"verify_new_user_second_reminder": 1557173,
|
|
28
28
|
"verify_new_user_via_parent": 1551587,
|
|
29
29
|
"verify_released_student": 1580574,
|
|
30
|
+
"inactive_users_on_website_first_reminder": 1604381,
|
|
31
|
+
"inactive_users_on_website_second_reminder": 1606208,
|
|
32
|
+
"inactive_users_on_website_final_reminder": 1606215,
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
|
|
@@ -65,7 +68,11 @@ def django_send_email(
|
|
|
65
68
|
plaintext = loader.get_template(plaintext_template)
|
|
66
69
|
html = loader.get_template(html_template)
|
|
67
70
|
plaintext_email_context = {"content": text_content}
|
|
68
|
-
html_email_context = {
|
|
71
|
+
html_email_context = {
|
|
72
|
+
"content": text_content,
|
|
73
|
+
"title": title,
|
|
74
|
+
"url_prefix": domain(),
|
|
75
|
+
}
|
|
69
76
|
|
|
70
77
|
# render templates
|
|
71
78
|
plaintext_body = plaintext.render(plaintext_email_context)
|
|
@@ -74,11 +81,19 @@ def django_send_email(
|
|
|
74
81
|
|
|
75
82
|
if replace_url:
|
|
76
83
|
verify_url = replace_url["verify_url"]
|
|
77
|
-
verify_replace_url = re.sub(
|
|
78
|
-
|
|
84
|
+
verify_replace_url = re.sub(
|
|
85
|
+
f"(.*/verify_email/)(.*)", f"\\1", verify_url
|
|
86
|
+
)
|
|
87
|
+
html_body = re.sub(
|
|
88
|
+
f"({verify_url})(.*){verify_url}",
|
|
89
|
+
f"\\1\\2{verify_replace_url}",
|
|
90
|
+
original_html_body,
|
|
91
|
+
)
|
|
79
92
|
|
|
80
93
|
# make message using templates
|
|
81
|
-
message = EmailMultiAlternatives(
|
|
94
|
+
message = EmailMultiAlternatives(
|
|
95
|
+
subject, plaintext_body, sender, recipients
|
|
96
|
+
)
|
|
82
97
|
message.attach_alternative(html_body, "text/html")
|
|
83
98
|
|
|
84
99
|
message.send()
|
|
@@ -123,7 +138,13 @@ def send_dotdigital_email(
|
|
|
123
138
|
|
|
124
139
|
# Dotdigital emails don't work locally, so if testing emails locally use Django to send a dummy email instead
|
|
125
140
|
if MODULE_NAME == "local":
|
|
126
|
-
django_send_email(
|
|
141
|
+
django_send_email(
|
|
142
|
+
from_address,
|
|
143
|
+
to_addresses,
|
|
144
|
+
"dummy_subject",
|
|
145
|
+
"dummy_text_content",
|
|
146
|
+
"dummy_title",
|
|
147
|
+
)
|
|
127
148
|
else:
|
|
128
149
|
if auth is None:
|
|
129
150
|
auth = app_settings.DOTDIGITAL_AUTH
|
|
@@ -168,4 +189,8 @@ def send_dotdigital_email(
|
|
|
168
189
|
timeout=timeout,
|
|
169
190
|
)
|
|
170
191
|
|
|
171
|
-
assert response.ok,
|
|
192
|
+
assert response.ok, (
|
|
193
|
+
"Failed to send email."
|
|
194
|
+
f" Reason: {response.reason}."
|
|
195
|
+
f" Text: {response.text}."
|
|
196
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: codeforlife-portal
|
|
3
|
-
Version: 7.
|
|
3
|
+
Version: 7.1.0
|
|
4
4
|
Classifier: Programming Language :: Python
|
|
5
5
|
Classifier: Programming Language :: Python :: 3.8
|
|
6
6
|
Classifier: Framework :: Django
|
|
@@ -24,7 +24,7 @@ Requires-Dist: django-classy-tags ==2.0.0
|
|
|
24
24
|
Requires-Dist: libsass ==0.23.0
|
|
25
25
|
Requires-Dist: phonenumbers ==8.12.12
|
|
26
26
|
Requires-Dist: more-itertools ==8.7.0
|
|
27
|
-
Requires-Dist: cfl-common ==7.
|
|
27
|
+
Requires-Dist: cfl-common ==7.1.0
|
|
28
28
|
Requires-Dist: django-ratelimit ==3.0.1
|
|
29
29
|
Requires-Dist: django-preventconcurrentlogins ==0.8.2
|
|
30
30
|
Requires-Dist: django-csp ==3.7
|
|
@@ -5,7 +5,7 @@ cfl_common/common/app_settings.py,sha256=x2ROLY5Xl5LgqjxyTiChZvQorZYUXpFzEkaLsjh
|
|
|
5
5
|
cfl_common/common/apps.py,sha256=49UXZ3bSkFKvIEOL4zM7y1sAhccQJyRtsoOg5XVd_8Y,129
|
|
6
6
|
cfl_common/common/context_processors.py,sha256=X0iuX5qu9kMWa7q8osE9CJ2LgM7pPOYQFGdjm8X3rk0,236
|
|
7
7
|
cfl_common/common/csp_config.py,sha256=9ECOLnp60ENRFAYEEIoYOMhqQzLgfKA-wkWxeUBwDrQ,2824
|
|
8
|
-
cfl_common/common/mail.py,sha256=
|
|
8
|
+
cfl_common/common/mail.py,sha256=nCY5aRiyiBCudonewpHOQ3GnXhQu4HLJRaqx1vOYhfI,6799
|
|
9
9
|
cfl_common/common/models.py,sha256=FB34xkpmTpYkvypgrDHv3QSRWnds69JnjHFw0X0fjrI,15989
|
|
10
10
|
cfl_common/common/permissions.py,sha256=gC6RQGZI2QDBbglx-xr_V4Hl2C2nf1V2_uPmEuoEcJo,2416
|
|
11
11
|
cfl_common/common/utils.py,sha256=Nn2Npao9Uqad5Js_IdHwF-ow6wrPNpBLW4AO1LxoEBc,1727
|
|
@@ -107,7 +107,7 @@ example_project/portal_test_settings.py,sha256=NfLY72mt1LR2c0_kxF-Yg5pCm2vQ52ece
|
|
|
107
107
|
example_project/settings.py,sha256=vOGZyxsWfV_G28X3XnSGSE65BUSU7mIGKOd0Z4mSkaE,5600
|
|
108
108
|
example_project/urls.py,sha256=6nYfzu2pSVAjkAm2ZyzniZl-VzxYuDyaAZTObVX7Jjg,350
|
|
109
109
|
example_project/wsgi.py,sha256=U1W6WzZxZaIdYZ5tks7w9fqp5WS5qvn2iThsVcskrWw,829
|
|
110
|
-
portal/__init__.py,sha256=
|
|
110
|
+
portal/__init__.py,sha256=vrQk6cOG_uk0gOqN4qh7NRCxezXfvhv9tT7bwYHyTck,22
|
|
111
111
|
portal/admin.py,sha256=on1-zNRnZvf2cwBN6GVRVYRhkaksrCgfzX8XPWtkvz8,6062
|
|
112
112
|
portal/app_settings.py,sha256=DhWLQOwM0zVOXE3O5TNKbMM9K6agfLuCsHOdr1J7xEI,651
|
|
113
113
|
portal/backends.py,sha256=2Dss6_WoQwPuDzJUF1yEaTQTNG4eUrD12ujJQ5cp5Tc,812
|
|
@@ -115,7 +115,7 @@ portal/beta.py,sha256=0TCC-9_KZoM1nuzJ9FiuKR5n9JITdMYenHGQtRvn9UU,255
|
|
|
115
115
|
portal/context_processors.py,sha256=1TrUZqnMqGa5f7ERph9EpBqojSMJvOrcpnJzTdeCLDI,133
|
|
116
116
|
portal/handlers.py,sha256=gF99OfQrGcIGDnUyONhvylZNU8sl6XHYEurwu0fuiss,422
|
|
117
117
|
portal/models.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
118
|
-
portal/urls.py,sha256=
|
|
118
|
+
portal/urls.py,sha256=FKO46TayhzGnyzQkDXABzfiFdlnoF6C0DqJ_lm21JlI,17897
|
|
119
119
|
portal/wsgi.py,sha256=3yRcNxBQG30NhzrVi93bX-DrbXtsIQBc70HiW5wbOyE,401
|
|
120
120
|
portal/forms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
121
121
|
portal/forms/admin.py,sha256=Cdl8-wvasAzvMfgUlFYzQjYeuyC7gIsSiy8V_-jMp7w,2080
|
|
@@ -545,7 +545,7 @@ portal/tests/test_school_student.py,sha256=bFZwY4twaFHQLp0cltMq8cLNDZGgCHTZBCZHK
|
|
|
545
545
|
portal/tests/test_security.py,sha256=FGrlRfnzi-Xx2_bn4fTZlYORKm7w_GhGkD3havvplwc,3239
|
|
546
546
|
portal/tests/test_teacher.py,sha256=vjnJi_aj_x48OJOMMRIBr0JTCxy4tFxqrLfCgw0fRxQ,29315
|
|
547
547
|
portal/tests/test_teacher_student.py,sha256=NWITbUw1kijqu3c8eRHLHJKaYQMOsOMvl7PAVx5QghI,21567
|
|
548
|
-
portal/tests/test_views.py,sha256=
|
|
548
|
+
portal/tests/test_views.py,sha256=x4veABtBucGRgsO6rACPLmeTqKLHTs-j4nyjyq1E4H8,44626
|
|
549
549
|
portal/tests/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
550
550
|
portal/tests/migrations/test_migration_make_portaladmin_teacher.py,sha256=ekMRb6cU97oT0k9gCKW7IUB7oPuGmv4uWJCqInQN7x8,2589
|
|
551
551
|
portal/tests/migrations/test_migration_preview_user_remove.py,sha256=K6D-FZT9YFEA8oMxHz9VTglVV6MZOTRYVlvwWwXc2vU,555
|
|
@@ -613,7 +613,7 @@ portal/views/play_landing_page.py,sha256=FFmjUFub3ZdlbMqkB8yX3jAImCzqrUqgb8AZcpK
|
|
|
613
613
|
portal/views/registration.py,sha256=L9AzIG2nOU946cSOXmUMQRtDo3uxApHX-0ceXopbOCw,10888
|
|
614
614
|
portal/views/teach.py,sha256=nzlyTcgq9ImAjnqrF3esqi212qBLH5Ww1LKE2gSjoRY,210
|
|
615
615
|
portal/views/cron/__init__.py,sha256=5rxXyhJmLOExRdrYZ1VJttTsyRIPRybzdftbUDwFByI,20
|
|
616
|
-
portal/views/cron/user.py,sha256=
|
|
616
|
+
portal/views/cron/user.py,sha256=IP4hngpHg1GrKqh6dyyW13dr3R6617MenAcObWH9zbY,10287
|
|
617
617
|
portal/views/login/__init__.py,sha256=xSCtyFPSI87BRUybBgqa86ekFEolX5gUDbBSfBUMTyI,399
|
|
618
618
|
portal/views/login/independent_student.py,sha256=3dFULhwMAlX4VDrJl-Znril6a9M5xKBSHO1eWvujfS0,2662
|
|
619
619
|
portal/views/login/student.py,sha256=dt6cMfWepBJsVCRcADltfYSHVpyeP1WGLKSogMJ22E0,5539
|
|
@@ -628,8 +628,8 @@ portal/views/two_factor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG
|
|
|
628
628
|
portal/views/two_factor/core.py,sha256=O_wcBeFqdPYSGNGv-pT_vbs5-Dj1Z-Jfkd6f9-E5yZI,760
|
|
629
629
|
portal/views/two_factor/form.py,sha256=lnHNKI-BMlpncTuW3zUzjPaJJNuEra2I_nOam0eOKFY,257
|
|
630
630
|
portal/views/two_factor/profile.py,sha256=tkl_ludo8arMtd5LKNmohM66vpC_YQiP-0nspTSJiJ4,383
|
|
631
|
-
codeforlife_portal-7.
|
|
632
|
-
codeforlife_portal-7.
|
|
633
|
-
codeforlife_portal-7.
|
|
634
|
-
codeforlife_portal-7.
|
|
635
|
-
codeforlife_portal-7.
|
|
631
|
+
codeforlife_portal-7.1.0.dist-info/LICENSE.md,sha256=9AbRlCDqD2D1tPibimysFv3zg3AIc49-eyv9aEsyq9w,115
|
|
632
|
+
codeforlife_portal-7.1.0.dist-info/METADATA,sha256=2TBqrL6DvpPsSR_nWyqE_jVPNmZoi0LljuVeQoDmO-8,3447
|
|
633
|
+
codeforlife_portal-7.1.0.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
|
|
634
|
+
codeforlife_portal-7.1.0.dist-info/top_level.txt,sha256=8e5pdsuIoTqEAMqpelHBjGjLbffcBtgOoggmd2q7nMw,41
|
|
635
|
+
codeforlife_portal-7.1.0.dist-info/RECORD,,
|
portal/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "7.
|
|
1
|
+
__version__ = "7.1.0"
|
portal/tests/test_views.py
CHANGED
|
@@ -16,6 +16,7 @@ from common.models import (
|
|
|
16
16
|
UserProfile,
|
|
17
17
|
UserSession,
|
|
18
18
|
)
|
|
19
|
+
from common.mail import campaign_ids
|
|
19
20
|
from common.tests.utils.classes import create_class_directly
|
|
20
21
|
from common.tests.utils.organisation import (
|
|
21
22
|
create_organisation_directly,
|
|
@@ -1197,3 +1198,103 @@ class TestUser(CronTestCase):
|
|
|
1197
1198
|
is_verified=False,
|
|
1198
1199
|
assert_active=False,
|
|
1199
1200
|
)
|
|
1201
|
+
|
|
1202
|
+
@patch("portal.views.cron.user.send_dotdigital_email")
|
|
1203
|
+
def send_inactivity_reminder(
|
|
1204
|
+
self,
|
|
1205
|
+
days: int,
|
|
1206
|
+
view_name: str,
|
|
1207
|
+
assert_called: bool,
|
|
1208
|
+
campaign_name: str,
|
|
1209
|
+
mock_send_dotdigital_email: Mock,
|
|
1210
|
+
):
|
|
1211
|
+
self.teacher_user.date_joined = timezone.now() - timedelta(
|
|
1212
|
+
days=days, hours=12
|
|
1213
|
+
)
|
|
1214
|
+
self.teacher_user.save()
|
|
1215
|
+
self.student_user.date_joined = timezone.now() - timedelta(
|
|
1216
|
+
days=days, hours=12
|
|
1217
|
+
)
|
|
1218
|
+
self.student_user.save()
|
|
1219
|
+
self.indy_user.last_login = timezone.now() - timedelta(
|
|
1220
|
+
days=days, hours=12
|
|
1221
|
+
)
|
|
1222
|
+
self.indy_user.save()
|
|
1223
|
+
|
|
1224
|
+
self.client.get(reverse(view_name))
|
|
1225
|
+
|
|
1226
|
+
if assert_called:
|
|
1227
|
+
mock_send_dotdigital_email.assert_any_call(
|
|
1228
|
+
campaign_ids[campaign_name], [self.teacher_user.email]
|
|
1229
|
+
)
|
|
1230
|
+
|
|
1231
|
+
mock_send_dotdigital_email.assert_any_call(
|
|
1232
|
+
campaign_ids[campaign_name], [self.indy_user.email]
|
|
1233
|
+
)
|
|
1234
|
+
|
|
1235
|
+
# Check only two emails are sent - the student should never be included.
|
|
1236
|
+
assert mock_send_dotdigital_email.call_count == 2
|
|
1237
|
+
else:
|
|
1238
|
+
mock_send_dotdigital_email.assert_not_called()
|
|
1239
|
+
|
|
1240
|
+
mock_send_dotdigital_email.reset_mock()
|
|
1241
|
+
|
|
1242
|
+
def test_first_inactivity_reminder_view(self):
|
|
1243
|
+
self.send_inactivity_reminder(
|
|
1244
|
+
729,
|
|
1245
|
+
"first-inactivity-reminder",
|
|
1246
|
+
False,
|
|
1247
|
+
"inactive_users_on_website_first_reminder",
|
|
1248
|
+
)
|
|
1249
|
+
self.send_inactivity_reminder(
|
|
1250
|
+
730,
|
|
1251
|
+
"first-inactivity-reminder",
|
|
1252
|
+
True,
|
|
1253
|
+
"inactive_users_on_website_first_reminder",
|
|
1254
|
+
)
|
|
1255
|
+
self.send_inactivity_reminder(
|
|
1256
|
+
731,
|
|
1257
|
+
"first-inactivity-reminder",
|
|
1258
|
+
False,
|
|
1259
|
+
"inactive_users_on_website_first_reminder",
|
|
1260
|
+
)
|
|
1261
|
+
|
|
1262
|
+
def test_second_inactivity_reminder_view(self):
|
|
1263
|
+
self.send_inactivity_reminder(
|
|
1264
|
+
972,
|
|
1265
|
+
"second-inactivity-reminder",
|
|
1266
|
+
False,
|
|
1267
|
+
"inactive_users_on_website_second_reminder",
|
|
1268
|
+
)
|
|
1269
|
+
self.send_inactivity_reminder(
|
|
1270
|
+
973,
|
|
1271
|
+
"second-inactivity-reminder",
|
|
1272
|
+
True,
|
|
1273
|
+
"inactive_users_on_website_second_reminder",
|
|
1274
|
+
)
|
|
1275
|
+
self.send_inactivity_reminder(
|
|
1276
|
+
974,
|
|
1277
|
+
"second-inactivity-reminder",
|
|
1278
|
+
False,
|
|
1279
|
+
"inactive_users_on_website_second_reminder",
|
|
1280
|
+
)
|
|
1281
|
+
|
|
1282
|
+
def test_final_inactivity_reminder_view(self):
|
|
1283
|
+
self.send_inactivity_reminder(
|
|
1284
|
+
1064,
|
|
1285
|
+
"final-inactivity-reminder",
|
|
1286
|
+
False,
|
|
1287
|
+
"inactive_users_on_website_final_reminder",
|
|
1288
|
+
)
|
|
1289
|
+
self.send_inactivity_reminder(
|
|
1290
|
+
1065,
|
|
1291
|
+
"final-inactivity-reminder",
|
|
1292
|
+
True,
|
|
1293
|
+
"inactive_users_on_website_final_reminder",
|
|
1294
|
+
)
|
|
1295
|
+
self.send_inactivity_reminder(
|
|
1296
|
+
1066,
|
|
1297
|
+
"final-inactivity-reminder",
|
|
1298
|
+
False,
|
|
1299
|
+
"inactive_users_on_website_final_reminder",
|
|
1300
|
+
)
|
portal/urls.py
CHANGED
|
@@ -164,6 +164,21 @@ urlpatterns = [
|
|
|
164
164
|
cron.user.AnonymiseUnverifiedAccounts.as_view(),
|
|
165
165
|
name="anonymise-unverified-accounts",
|
|
166
166
|
),
|
|
167
|
+
path(
|
|
168
|
+
"inactive/send-first-reminder/",
|
|
169
|
+
cron.user.FirstInactivityReminderView.as_view(),
|
|
170
|
+
name="first-inactivity-reminder",
|
|
171
|
+
),
|
|
172
|
+
path(
|
|
173
|
+
"inactive/send-second-reminder/",
|
|
174
|
+
cron.user.SecondInactivityReminderView.as_view(),
|
|
175
|
+
name="second-inactivity-reminder",
|
|
176
|
+
),
|
|
177
|
+
path(
|
|
178
|
+
"inactive/send-final-reminder/",
|
|
179
|
+
cron.user.FinalInactivityReminderView.as_view(),
|
|
180
|
+
name="final-inactivity-reminder",
|
|
181
|
+
),
|
|
167
182
|
]
|
|
168
183
|
),
|
|
169
184
|
),
|
portal/views/cron/user.py
CHANGED
|
@@ -5,24 +5,28 @@ from common.helpers.emails import generate_token_for_email
|
|
|
5
5
|
from common.mail import campaign_ids, send_dotdigital_email
|
|
6
6
|
from common.models import DailyActivity, TotalActivity
|
|
7
7
|
from django.contrib.auth.models import User
|
|
8
|
-
from django.db.models import F
|
|
8
|
+
from django.db.models import F, Q
|
|
9
9
|
from django.db.models.query import QuerySet
|
|
10
10
|
from django.urls import reverse
|
|
11
11
|
from django.utils import timezone
|
|
12
12
|
from rest_framework.response import Response
|
|
13
13
|
from rest_framework.views import APIView
|
|
14
14
|
|
|
15
|
-
from portal.views.api import anonymise
|
|
16
|
-
|
|
17
15
|
from ...mixins import CronMixin
|
|
16
|
+
from ...views.api import anonymise
|
|
18
17
|
|
|
19
|
-
# TODO: move email templates to DotDigital.
|
|
20
18
|
USER_1ST_VERIFY_EMAIL_REMINDER_DAYS = 7
|
|
21
19
|
USER_2ND_VERIFY_EMAIL_REMINDER_DAYS = 14
|
|
22
20
|
USER_DELETE_UNVERIFIED_ACCOUNT_DAYS = 19
|
|
23
21
|
|
|
22
|
+
USER_1ST_INACTIVE_REMINDER_DAYS = 730 # 2 years
|
|
23
|
+
USER_2ND_INACTIVE_REMINDER_DAYS = 973 # roughly 2 years and 8 months
|
|
24
|
+
USER_FINAL_INACTIVE_REMINDER_DAYS = 1065 # 2 years and 11 months
|
|
25
|
+
|
|
24
26
|
|
|
25
|
-
def get_unverified_users(
|
|
27
|
+
def get_unverified_users(
|
|
28
|
+
days: int, same_day: bool
|
|
29
|
+
) -> (QuerySet[User], QuerySet[User]):
|
|
26
30
|
now = timezone.now()
|
|
27
31
|
|
|
28
32
|
# All expired unverified users.
|
|
@@ -31,7 +35,9 @@ def get_unverified_users(days: int, same_day: bool) -> (QuerySet[User], QuerySet
|
|
|
31
35
|
userprofile__is_verified=False,
|
|
32
36
|
)
|
|
33
37
|
if same_day:
|
|
34
|
-
user_queryset = user_queryset.filter(
|
|
38
|
+
user_queryset = user_queryset.filter(
|
|
39
|
+
date_joined__gt=now - timedelta(days=days + 1)
|
|
40
|
+
)
|
|
35
41
|
|
|
36
42
|
teacher_queryset = user_queryset.filter(
|
|
37
43
|
new_teacher__isnull=False,
|
|
@@ -45,10 +51,31 @@ def get_unverified_users(days: int, same_day: bool) -> (QuerySet[User], QuerySet
|
|
|
45
51
|
return teacher_queryset, independent_student_queryset
|
|
46
52
|
|
|
47
53
|
|
|
54
|
+
def get_inactive_users(days: int) -> QuerySet[User]:
|
|
55
|
+
now = timezone.now()
|
|
56
|
+
|
|
57
|
+
# All users who haven't logged in in X days OR who've never logged in and
|
|
58
|
+
# registered over X days ago.
|
|
59
|
+
user_queryset = User.objects.filter(
|
|
60
|
+
Q(
|
|
61
|
+
last_login__isnull=False,
|
|
62
|
+
last_login__lte=now - timedelta(days=days),
|
|
63
|
+
last_login__gt=now - timedelta(days=days + 1),
|
|
64
|
+
)
|
|
65
|
+
| Q(
|
|
66
|
+
last_login__isnull=True,
|
|
67
|
+
date_joined__lte=now - timedelta(days=days),
|
|
68
|
+
date_joined__gt=now - timedelta(days=days + 1),
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return user_queryset.exclude(email__isnull=True).exclude(email="")
|
|
73
|
+
|
|
74
|
+
|
|
48
75
|
def build_absolute_google_uri(request, location: str) -> str:
|
|
49
76
|
"""
|
|
50
|
-
This is needed specifically for emails sent by cron jobs as the protocol for
|
|
51
|
-
and the service name is wrongly parsed.
|
|
77
|
+
This is needed specifically for emails sent by cron jobs as the protocol for
|
|
78
|
+
cron jobs is HTTP and the service name is wrongly parsed.
|
|
52
79
|
"""
|
|
53
80
|
url = request.build_absolute_uri(location)
|
|
54
81
|
url = url.replace("http", "https")
|
|
@@ -70,7 +97,9 @@ class FirstVerifyEmailReminderView(CronMixin, APIView):
|
|
|
70
97
|
|
|
71
98
|
if user_count > 0:
|
|
72
99
|
sent_email_count = 0
|
|
73
|
-
for email in user_queryset.values_list("email", flat=True).iterator(
|
|
100
|
+
for email in user_queryset.values_list("email", flat=True).iterator(
|
|
101
|
+
chunk_size=500
|
|
102
|
+
):
|
|
74
103
|
email_verification_url = build_absolute_google_uri(
|
|
75
104
|
request,
|
|
76
105
|
reverse(
|
|
@@ -83,7 +112,9 @@ class FirstVerifyEmailReminderView(CronMixin, APIView):
|
|
|
83
112
|
send_dotdigital_email(
|
|
84
113
|
campaign_ids["verify_new_user_first_reminder"],
|
|
85
114
|
[email],
|
|
86
|
-
personalization_values={
|
|
115
|
+
personalization_values={
|
|
116
|
+
"VERIFICATION_LINK": email_verification_url
|
|
117
|
+
},
|
|
87
118
|
)
|
|
88
119
|
|
|
89
120
|
sent_email_count += 1
|
|
@@ -109,7 +140,9 @@ class SecondVerifyEmailReminderView(CronMixin, APIView):
|
|
|
109
140
|
if user_count > 0:
|
|
110
141
|
|
|
111
142
|
sent_email_count = 0
|
|
112
|
-
for email in user_queryset.values_list("email", flat=True).iterator(
|
|
143
|
+
for email in user_queryset.values_list("email", flat=True).iterator(
|
|
144
|
+
chunk_size=500
|
|
145
|
+
):
|
|
113
146
|
email_verification_url = build_absolute_google_uri(
|
|
114
147
|
request,
|
|
115
148
|
reverse(
|
|
@@ -122,7 +155,9 @@ class SecondVerifyEmailReminderView(CronMixin, APIView):
|
|
|
122
155
|
send_dotdigital_email(
|
|
123
156
|
campaign_ids["verify_new_user_second_reminder"],
|
|
124
157
|
[email],
|
|
125
|
-
personalization_values={
|
|
158
|
+
personalization_values={
|
|
159
|
+
"VERIFICATION_LINK": email_verification_url
|
|
160
|
+
},
|
|
126
161
|
)
|
|
127
162
|
|
|
128
163
|
sent_email_count += 1
|
|
@@ -157,14 +192,122 @@ class AnonymiseUnverifiedAccounts(CronMixin, APIView):
|
|
|
157
192
|
user_count -= User.objects.filter(is_active=True).count()
|
|
158
193
|
logging.info(f"{user_count} unverified users anonymised.")
|
|
159
194
|
|
|
160
|
-
activity_today = DailyActivity.objects.get_or_create(
|
|
195
|
+
activity_today = DailyActivity.objects.get_or_create(
|
|
196
|
+
date=datetime.now().date()
|
|
197
|
+
)[0]
|
|
161
198
|
activity_today.anonymised_unverified_teachers = teacher_count
|
|
162
199
|
activity_today.anonymised_unverified_independents = indy_count
|
|
163
200
|
activity_today.save()
|
|
164
201
|
|
|
165
202
|
TotalActivity.objects.update(
|
|
166
|
-
anonymised_unverified_teachers=F("anonymised_unverified_teachers")
|
|
167
|
-
|
|
203
|
+
anonymised_unverified_teachers=F("anonymised_unverified_teachers")
|
|
204
|
+
+ teacher_count,
|
|
205
|
+
anonymised_unverified_independents=F(
|
|
206
|
+
"anonymised_unverified_independents"
|
|
207
|
+
)
|
|
208
|
+
+ indy_count,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
return Response()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class FirstInactivityReminderView(CronMixin, APIView):
|
|
215
|
+
def get(self, request):
|
|
216
|
+
user_queryset = get_inactive_users(USER_1ST_INACTIVE_REMINDER_DAYS)
|
|
217
|
+
user_count = user_queryset.count()
|
|
218
|
+
|
|
219
|
+
logging.info(
|
|
220
|
+
f"{user_count} inactive users after "
|
|
221
|
+
f"{USER_1ST_INACTIVE_REMINDER_DAYS} days."
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
if user_count > 0:
|
|
225
|
+
sent_email_count = 0
|
|
226
|
+
for email in user_queryset.values_list("email", flat=True).iterator(
|
|
227
|
+
chunk_size=500
|
|
228
|
+
):
|
|
229
|
+
try:
|
|
230
|
+
send_dotdigital_email(
|
|
231
|
+
campaign_ids[
|
|
232
|
+
"inactive_users_on_website_first_reminder"
|
|
233
|
+
],
|
|
234
|
+
[email],
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
sent_email_count += 1
|
|
238
|
+
except Exception as ex:
|
|
239
|
+
logging.exception(ex)
|
|
240
|
+
|
|
241
|
+
logging.info(
|
|
242
|
+
f"Reminded {sent_email_count}/{user_count} inactive users."
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
return Response()
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class SecondInactivityReminderView(CronMixin, APIView):
|
|
249
|
+
def get(self, request):
|
|
250
|
+
user_queryset = get_inactive_users(USER_2ND_INACTIVE_REMINDER_DAYS)
|
|
251
|
+
user_count = user_queryset.count()
|
|
252
|
+
|
|
253
|
+
logging.info(
|
|
254
|
+
f"{user_count} inactive users after "
|
|
255
|
+
f"{USER_2ND_INACTIVE_REMINDER_DAYS} days."
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
if user_count > 0:
|
|
259
|
+
sent_email_count = 0
|
|
260
|
+
for email in user_queryset.values_list("email", flat=True).iterator(
|
|
261
|
+
chunk_size=500
|
|
262
|
+
):
|
|
263
|
+
try:
|
|
264
|
+
send_dotdigital_email(
|
|
265
|
+
campaign_ids[
|
|
266
|
+
"inactive_users_on_website_second_reminder"
|
|
267
|
+
],
|
|
268
|
+
[email],
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
sent_email_count += 1
|
|
272
|
+
except Exception as ex:
|
|
273
|
+
logging.exception(ex)
|
|
274
|
+
|
|
275
|
+
logging.info(
|
|
276
|
+
f"Reminded {sent_email_count}/{user_count} inactive users."
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
return Response()
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class FinalInactivityReminderView(CronMixin, APIView):
|
|
283
|
+
def get(self, request):
|
|
284
|
+
user_queryset = get_inactive_users(USER_FINAL_INACTIVE_REMINDER_DAYS)
|
|
285
|
+
user_count = user_queryset.count()
|
|
286
|
+
|
|
287
|
+
logging.info(
|
|
288
|
+
f"{user_count} inactive users after "
|
|
289
|
+
f"{USER_FINAL_INACTIVE_REMINDER_DAYS} days."
|
|
168
290
|
)
|
|
169
291
|
|
|
292
|
+
if user_count > 0:
|
|
293
|
+
sent_email_count = 0
|
|
294
|
+
for email in user_queryset.values_list("email", flat=True).iterator(
|
|
295
|
+
chunk_size=500
|
|
296
|
+
):
|
|
297
|
+
try:
|
|
298
|
+
send_dotdigital_email(
|
|
299
|
+
campaign_ids[
|
|
300
|
+
"inactive_users_on_website_final_reminder"
|
|
301
|
+
],
|
|
302
|
+
[email],
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
sent_email_count += 1
|
|
306
|
+
except Exception as ex:
|
|
307
|
+
logging.exception(ex)
|
|
308
|
+
|
|
309
|
+
logging.info(
|
|
310
|
+
f"Reminded {sent_email_count}/{user_count} inactive users."
|
|
311
|
+
)
|
|
312
|
+
|
|
170
313
|
return Response()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|