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 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 = {"content": text_content, "title": title, "url_prefix": domain()}
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(f"(.*/verify_email/)(.*)", f"\\1", verify_url)
78
- html_body = re.sub(f"({verify_url})(.*){verify_url}", f"\\1\\2{verify_replace_url}", original_html_body)
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(subject, plaintext_body, sender, recipients)
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(from_address, to_addresses, "dummy_subject", "dummy_text_content", "dummy_title")
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, "Failed to send email." f" Reason: {response.reason}." f" Text: {response.text}."
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.0.0
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.0.0
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=Lmn7CepceKw1UqxjxTR_24fo7izecw0ihBJ3W_9tcco,6394
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=VgMOOqsYbyb60I1RmlZpqwqQ0C0IyT3R0c8_xX4pRGM,22
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=8HzxA7poBLVR_UyAYlWb7rGzdW-NVT9_tHOPIIuEeZE,17063
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=9Z3IQHOKLua_ItHHWf5lYn5pvY_5vpHH1UwWxxWMe8U,41487
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=N4slzEXqzp557LLPlwA6sD3HVzDu74NBf128uvtwKnM,6044
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.0.0.dist-info/LICENSE.md,sha256=9AbRlCDqD2D1tPibimysFv3zg3AIc49-eyv9aEsyq9w,115
632
- codeforlife_portal-7.0.0.dist-info/METADATA,sha256=PIxpIiA7ZA9AJ4Wc_-GvXnLMdRiWJ8RYyoqleSvTOZk,3447
633
- codeforlife_portal-7.0.0.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
634
- codeforlife_portal-7.0.0.dist-info/top_level.txt,sha256=8e5pdsuIoTqEAMqpelHBjGjLbffcBtgOoggmd2q7nMw,41
635
- codeforlife_portal-7.0.0.dist-info/RECORD,,
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.0.0"
1
+ __version__ = "7.1.0"
@@ -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(days: int, same_day: bool) -> (QuerySet[User], QuerySet[User]):
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(date_joined__gt=now - timedelta(days=days + 1))
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 cron jobs is HTTP
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(chunk_size=500):
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={"VERIFICATION_LINK": email_verification_url},
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(chunk_size=500):
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={"VERIFICATION_LINK": email_verification_url},
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(date=datetime.now().date())[0]
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") + teacher_count,
167
- anonymised_unverified_independents=F("anonymised_unverified_independents") + indy_count,
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()