codeforlife-portal 6.44.4__py2.py3-none-any.whl → 6.44.8__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.

@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.1
2
+ Name: codeforlife-portal
3
+ Version: 6.44.8
4
+ Classifier: Programming Language :: Python
5
+ Classifier: Programming Language :: Python :: 3.8
6
+ Classifier: Framework :: Django
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE.md
9
+ Requires-Dist: django ==3.2.25
10
+ Requires-Dist: django-countries ==7.3.1
11
+ Requires-Dist: djangorestframework ==3.13.1
12
+ Requires-Dist: django-pipeline ==2.0.8
13
+ Requires-Dist: django-recaptcha ==2.0.6
14
+ Requires-Dist: pyyaml ==5.4.1
15
+ Requires-Dist: importlib-metadata ==4.13.0
16
+ Requires-Dist: rapid-router >=4
17
+ Requires-Dist: aimmo >=2
18
+ Requires-Dist: reportlab ==3.6.13
19
+ Requires-Dist: django-formtools ==2.2
20
+ Requires-Dist: django-otp ==1.0.2
21
+ Requires-Dist: requests ==2.32.0
22
+ Requires-Dist: django-treebeard ==4.3.1
23
+ Requires-Dist: django-sekizai ==2.0.0
24
+ Requires-Dist: django-classy-tags ==2.0.0
25
+ Requires-Dist: libsass ==0.23.0
26
+ Requires-Dist: phonenumbers ==8.12.12
27
+ Requires-Dist: more-itertools ==8.7.0
28
+ Requires-Dist: cfl-common ==6.44.8
29
+ Requires-Dist: django-ratelimit ==3.0.1
30
+ Requires-Dist: django-preventconcurrentlogins ==0.8.2
31
+ Requires-Dist: django-csp ==3.7
32
+ Requires-Dist: setuptools ==65.5.1
33
+ Requires-Dist: django-import-export
34
+
35
+ # Code for Life Portal
36
+
37
+ [![Workflow Status](https://github.com/ocadotechnology/codeforlife-portal/actions/workflows/ci.yml/badge.svg)](https://github.com/ocadotechnology/codeforlife-portal/actions/workflows/ci.yml)
38
+ [![codecov](https://codecov.io/gh/ocadotechnology/codeforlife-portal/branch/master/graph/badge.svg)](https://codecov.io/gh/ocadotechnology/codeforlife-portal)
39
+
40
+ ## LICENCE
41
+ In accordance with the [Terms of Use](https://www.codeforlife.education/terms#terms)
42
+ of the Code for Life website, all copyright, trademarks, and other
43
+ intellectual property rights in and relating to Code for Life (including all
44
+ content of the Code for Life website, the Rapid Router application, the
45
+ Kurono application, related software (including any drawn and/or animated
46
+ avatars, whether or not such avatars have any modifications) and any other
47
+ games, applications or any other content that we make available from time to
48
+ time) are owned by Ocado Innovation Limited.
49
+
50
+ The source code of the Code for Life portal, the Rapid Router application
51
+ and the Kurono/aimmo application are [licensed under the GNU Affero General
52
+ Public License](https://github.com/ocadotechnology/codeforlife-workspace/blob/main/LICENSE.md).
53
+ All other assets including images, logos, sounds etc., are not covered by
54
+ this licence and no-one may copy, modify, distribute, show in public or
55
+ create any derivative work from these assets.
56
+
57
+ ## Code for Life
58
+
59
+ [Code for Life](https://www.codeforlife.education/) has been developed by Ocado Technology as a **free, open-source** project to inspire the next generation of computer scientists and to help teachers deliver the computing curriculum.
60
+
61
+ This repository hosts the source code of the [main website](https://www.codeforlife.education/), which includes the registration, log in, teacher and student dashboards, the teaching materials, etc.
62
+
63
+ We are open to contributors from anywhere around the world. Please read ahead if you'd like to get involved.
64
+
65
+ ## To get started
66
+
67
+ - [Developer Guide](https://docs.codeforlife.education/developer-guide)
68
+
69
+ - [Good First Issues](https://github.com/ocadotechnology/codeforlife-portal/contribute)
70
+
71
+ - [How to set up your work environment](https://docs.codeforlife.education/git/common-setup)
72
+
73
+ - [Testing](https://docs.codeforlife.education/git/testing)
@@ -105,7 +105,7 @@ example_project/portal_test_settings.py,sha256=frp_XMpd-z1g3VFCRxB2w7AaFW2ivRVKn
105
105
  example_project/settings.py,sha256=XRZZvASoIl5a9xe3masTq_CUBleuJq9ByHx8f_e2UFc,5613
106
106
  example_project/urls.py,sha256=OVeRQ-TCpzHISBRuzqD0yd3ewF7H5U3c-f2p2alfUD0,430
107
107
  example_project/wsgi.py,sha256=U1W6WzZxZaIdYZ5tks7w9fqp5WS5qvn2iThsVcskrWw,829
108
- portal/__init__.py,sha256=-lI-ZgjvQrCkpiM7hjvY9ScoT2ABAjdCrwMGjYbzxpk,23
108
+ portal/__init__.py,sha256=csffnuSwbnjo5LqcdMo349n5YXfQyFDxeX6usxjUqww,23
109
109
  portal/admin.py,sha256=on1-zNRnZvf2cwBN6GVRVYRhkaksrCgfzX8XPWtkvz8,6062
110
110
  portal/app_settings.py,sha256=DhWLQOwM0zVOXE3O5TNKbMM9K6agfLuCsHOdr1J7xEI,651
111
111
  portal/backends.py,sha256=2Dss6_WoQwPuDzJUF1yEaTQTNG4eUrD12ujJQ5cp5Tc,812
@@ -113,7 +113,7 @@ portal/beta.py,sha256=0TCC-9_KZoM1nuzJ9FiuKR5n9JITdMYenHGQtRvn9UU,255
113
113
  portal/context_processors.py,sha256=1TrUZqnMqGa5f7ERph9EpBqojSMJvOrcpnJzTdeCLDI,133
114
114
  portal/handlers.py,sha256=gF99OfQrGcIGDnUyONhvylZNU8sl6XHYEurwu0fuiss,422
115
115
  portal/models.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
116
- portal/urls.py,sha256=6kuMw36OQ8gRQDQjN0QCK9th15_I3nl1Z7H9za_Susg,16206
116
+ portal/urls.py,sha256=ROGvyAo1pw1hHRLupxcbIVGjaGhZXY7AeyafxXYoa7Q,17464
117
117
  portal/wsgi.py,sha256=3yRcNxBQG30NhzrVi93bX-DrbXtsIQBc70HiW5wbOyE,401
118
118
  portal/forms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
119
119
  portal/forms/add_game.py,sha256=Z9pjNiAU3W2HthSsbz_OWzl2RuLpZ0GtA6Xe1tg7H5I,1008
@@ -185,7 +185,7 @@ portal/static/portal/img/dee.png,sha256=GQHNpaEiQt9M3qjb3wy6KOzlpKB1fNkESPe6yFZ7
185
185
  portal/static/portal/img/facebook.png,sha256=qaQdtT9dOg-7-XWnRxz8dW5Wwm6CEWq-S0hQDiLCWus,6543
186
186
  portal/static/portal/img/favicon.ico,sha256=vZMUaL5b47WxGRxlRMzp1fRlZQXevGHxBq5vOTV_tdg,481
187
187
  portal/static/portal/img/get_involved_hero.png,sha256=TD52SemoUt0xILs_WMxtoaKaSaXQVEQhSr2MgMNW8NY,176315
188
- portal/static/portal/img/gitbook.png,sha256=xnh_535-b64zq2O-Z1hEqTGkyYL0O74LvokV1RE1gFQ,177664
188
+ portal/static/portal/img/gitbook.png,sha256=JgswtPQZWoIL8eY2gPEKq2RTXr6IBHVGYjzUQqgFO4o,248105
189
189
  portal/static/portal/img/gitbook_space.png,sha256=d1krFtJM36UgBu_aXW0LmGKG7QyIvp4zC6kOXBTjon0,62646
190
190
  portal/static/portal/img/github.png,sha256=NaZ4dyTsDgbTaBLJQW9hRo49uuGw6KOXZ4p7BInrVsI,63596
191
191
  portal/static/portal/img/github_hero.png,sha256=HQHyoT5IRnCGzCklqc6fJM6BOoN9ksrBN0WJD7k50ss,47014
@@ -438,7 +438,7 @@ portal/templates/portal/about.html,sha256=vXsSYlPV-3OF2fv9eHp-ErEWXwcL-HwroocE4B
438
438
  portal/templates/portal/base.html,sha256=DhZ1BC9AANRofocQeNtU6wSFMI-5WmQ-4eXemUGAuQo,11689
439
439
  portal/templates/portal/base_no_userprofile.html,sha256=PlRufyYmUUGWBZ6CvbYhJWOMTqKqdcee4xnO5--AogA,447
440
440
  portal/templates/portal/coding_club.html,sha256=aaLx9UQ8p_7Vvy3Vlh1rOufeNN0KhJ6fj4oqI6QqJcM,4518
441
- portal/templates/portal/contribute.html,sha256=BebXjBAMkW1IQrCcJ5syHmMpuTbBKiqT9szdZnIEan8,4001
441
+ portal/templates/portal/contribute.html,sha256=9_sUT_RqPZspURQ_uxHylYaTsF0MvAKw5wMk7X1JiIU,4253
442
442
  portal/templates/portal/dotmailer_consent_form.html,sha256=UDdizPoKYZGybr6z9nzDV4WPhJPa-S3bIKywvVgFrxg,918
443
443
  portal/templates/portal/email_invitation_sent.html,sha256=hAMzQXE3NFGnOsQlCGuo3Aps-vazSJb5BhAN7bWT48M,641
444
444
  portal/templates/portal/email_style_template.html,sha256=VodOGssxRS9aJTjSaU7iv5qJgYe66C_gPL_98ZX_cIQ,11935
@@ -542,7 +542,7 @@ portal/tests/test_api.py,sha256=Yo5s_nEGOoG35jA39yZ6nuDOUZvuCZ8o8o8XhZos61w,1381
542
542
  portal/tests/test_captcha_forms.py,sha256=lirhIli-sHovun8VdrF0he7KRFTAd8DMCpkJ8cQNotg,1015
543
543
  portal/tests/test_class.py,sha256=V6Fkc6PqdisefKD3xs9PbfE2pKp-9e0gwQVkPUiu6bk,14150
544
544
  portal/tests/test_daily_activities.py,sha256=-siDCMGBD1ijjccHVk7eEmrk4bgTsvbh0B6hDoj2fo0,1803
545
- portal/tests/test_emails.py,sha256=Y26VjhPOzc2aptHBSmOW9R-do3k1QaRqUc5MiwgsAQk,9156
545
+ portal/tests/test_emails.py,sha256=xNgOt592r2nrQu7VeBdc8kvSnw6Z5fPJqMx2-UMcyyk,9482
546
546
  portal/tests/test_helper_methods.py,sha256=-SQCDZm2XUtyXGEp0CHIb_SSC9CPD-XOSnpnY8QclHk,890
547
547
  portal/tests/test_independent_student.py,sha256=jyNpHyisCfkD-CDsjIfSxWnWTxDmkReJ-BdXEBdJoMo,27081
548
548
  portal/tests/test_invite_teacher.py,sha256=oeOaoJV1IqJSYPlaPFjnhVXdB2mq8otCTLp_lfjuCfk,12224
@@ -617,7 +617,7 @@ portal/views/about.py,sha256=-muXy17UhxCSKkjnMAkSLXiCvT_pBPlf2ykTYr794dI,443
617
617
  portal/views/admin.py,sha256=4Xt3zEyQH7sUwQSrwuRtoCodWidjOzd7gJUwWU96pXY,957
618
618
  portal/views/api.py,sha256=lCwiclR98G-yTgK55u8IjkueIH8iremeiZSa3jAvO-M,6990
619
619
  portal/views/dotmailer.py,sha256=_subSoy5f1j5sAcRrjE_xMejdarjHIY1d_jwSrf7_o0,2299
620
- portal/views/email.py,sha256=dSgTp0kEZRTBRwPbtCrMoWhiOqZ1dPZT1YMvR6f1GQU,3899
620
+ portal/views/email.py,sha256=V3wXRxIjeZ4OJBVqGCQrPn-GQWKZK1PCXbR1f2Zpa_4,2174
621
621
  portal/views/home.py,sha256=J4KY66p2nRgRu1O_0IYwdgfZoTYuhQS0qpfiA1b8PTE,9855
622
622
  portal/views/legal.py,sha256=nUunsTHnhMcXBcDlg1GmUal86k9Vhinne4A2FWfq78M,342
623
623
  portal/views/organisation.py,sha256=sPDbiM7hdtpF8GKyh_4n4VPl2a-WnAgnF4q9aSvQCVI,3341
@@ -642,8 +642,8 @@ portal/views/two_factor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG
642
642
  portal/views/two_factor/core.py,sha256=O_wcBeFqdPYSGNGv-pT_vbs5-Dj1Z-Jfkd6f9-E5yZI,760
643
643
  portal/views/two_factor/form.py,sha256=lnHNKI-BMlpncTuW3zUzjPaJJNuEra2I_nOam0eOKFY,257
644
644
  portal/views/two_factor/profile.py,sha256=tkl_ludo8arMtd5LKNmohM66vpC_YQiP-0nspTSJiJ4,383
645
- codeforlife_portal-6.44.4.dist-info/LICENSE.md,sha256=9AbRlCDqD2D1tPibimysFv3zg3AIc49-eyv9aEsyq9w,115
646
- codeforlife_portal-6.44.4.dist-info/METADATA,sha256=01n40MUX_ICmWOiQHo0u_C_EAyOK8arKbaQ0mldDZgU,1137
647
- codeforlife_portal-6.44.4.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
648
- codeforlife_portal-6.44.4.dist-info/top_level.txt,sha256=8e5pdsuIoTqEAMqpelHBjGjLbffcBtgOoggmd2q7nMw,41
649
- codeforlife_portal-6.44.4.dist-info/RECORD,,
645
+ codeforlife_portal-6.44.8.dist-info/LICENSE.md,sha256=9AbRlCDqD2D1tPibimysFv3zg3AIc49-eyv9aEsyq9w,115
646
+ codeforlife_portal-6.44.8.dist-info/METADATA,sha256=d1GGfu9CPQSdWhpujDFB9VZs3tGbSaCGwC2a4l1P_nY,3474
647
+ codeforlife_portal-6.44.8.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
648
+ codeforlife_portal-6.44.8.dist-info/top_level.txt,sha256=8e5pdsuIoTqEAMqpelHBjGjLbffcBtgOoggmd2q7nMw,41
649
+ codeforlife_portal-6.44.8.dist-info/RECORD,,
portal/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "6.44.4"
1
+ __version__ = "6.44.8"
Binary file
@@ -10,7 +10,7 @@
10
10
  {% block content %}
11
11
  <div class="background container">
12
12
  <section>
13
- <h4>How to get involved and gain experience</h4>
13
+ <h3>How to get involved and gain experience</h3>
14
14
  </section>
15
15
  <div class="row">
16
16
  <div class="col-sm-6">
@@ -25,7 +25,7 @@
25
25
  Ocado Technology deployed an army of internal volunteers who worked after hours, fuelled by
26
26
  free pizzas and fizzy drinks.
27
27
  </p>
28
- <p>What came out of this, is what you see today, used by educators and learners from over 150
28
+ <p>What came out of this, is what you see today, used by educators and learners from over 180
29
29
  countries. Our products are open-source and free forever.
30
30
  </p>
31
31
  </div>
@@ -50,14 +50,14 @@
50
50
  </p>
51
51
 
52
52
  <p><b>Rapid Router</b></p>
53
- <p>An introduction to coding that is aimed at Key Stages 1-3 (age 5 to 14).
53
+ <p>An introduction to coding that is aimed at Key Stages 1-3 (age 5 to 15).
54
54
  Built on Blockly, it's a visual programming language similar to Scratch.
55
- The levels start off with Blockly and gradually progress to Python. With 109 levels,
55
+ The levels start off with Blockly and gradually progress to Python. With over 100 levels,
56
56
  Rapid Router is our flagship game with the biggest user base.
57
57
  </p>
58
58
 
59
59
  <p><b>Kurono</b></p>
60
- <p>A multiplayer game that is aimed at older students of Key Stages 3 and up,
60
+ <p>A multiplayer game that is aimed at students at Key Stages 3 and up,
61
61
  it is primarily for use in a class or a club setting. Students code in Python to
62
62
  move their avatar around in order to complete tasks. Parts of Kurono are still in development.
63
63
  </p>
@@ -77,8 +77,10 @@
77
77
  <div class="col-sm-6">
78
78
  <h4>How you can contribute</h4>
79
79
  <p>Today, there is a small dedicated team working full time on Code for Life.
80
- In 2021, we're expecting to reach over 100,000 newly registered teachers and students.
81
- We need your help to do even more.
80
+ Our community, made up of dedicated teachers, tutors, students, and volunteers,
81
+ has played a vital part in Code For Life's growth. The resources and games are
82
+ aligned to the UK National Curriculum and have reached over 265,000 active users
83
+ across 180 countries, and over 600,000 users since the launch in 2014. We need your help to grow even more.
82
84
  </p>
83
85
  <p>
84
86
  If contributing to open-source projects to support education in coding and technology
@@ -10,7 +10,6 @@ from common.helpers.emails import (
10
10
  send_dotmailer_consent_confirmation_email_to_user,
11
11
  DotmailerUserType,
12
12
  )
13
- from django.core import mail
14
13
  from django.test import Client
15
14
  from django.urls import reverse
16
15
 
@@ -41,19 +40,15 @@ def patch_datetime_now(monkeypatch):
41
40
  monkeypatch.setattr(datetime, "datetime", mydatetime)
42
41
 
43
42
 
44
- @pytest.mark.django_db
45
- def test_send_new_users_numbers_email():
46
- client = Client()
47
- response = client.get(reverse("send_new_users_report"))
48
- assert response.status_code == 200
49
- assert len(mail.outbox) == 1
50
-
51
-
52
43
  def test_newsletter_calls_correct_requests(mocker, monkeypatch):
53
44
  mocked_create_contact = mocker.patch("common.helpers.emails.create_contact")
54
- mocked_add_to_address_book = mocker.patch("common.helpers.emails.add_contact_to_address_book")
45
+ mocked_add_to_address_book = mocker.patch(
46
+ "common.helpers.emails.add_contact_to_address_book"
47
+ )
55
48
 
56
- add_to_dotmailer("Ray", "Charles", "ray.charles@example.com", DotmailerUserType.TEACHER)
49
+ add_to_dotmailer(
50
+ "Ray", "Charles", "ray.charles@example.com", DotmailerUserType.TEACHER
51
+ )
57
52
 
58
53
  mocked_create_contact.assert_called_once()
59
54
  mocked_add_to_address_book.assert_called_once()
@@ -74,10 +69,15 @@ def test_delete_account(mocker):
74
69
 
75
70
  delete_contact("example@mail.com")
76
71
 
77
- mocked_delete.assert_called_once_with(DOTMAILER_DELETE_USER_BY_ID_URL, auth=(DOTMAILER_USER, DOTMAILER_PASSWORD))
72
+ mocked_delete.assert_called_once_with(
73
+ DOTMAILER_DELETE_USER_BY_ID_URL,
74
+ auth=(DOTMAILER_USER, DOTMAILER_PASSWORD),
75
+ )
78
76
 
79
77
 
80
- def test_newsletter_sends_correct_request_data(mocker, monkeypatch, patch_datetime_now):
78
+ def test_newsletter_sends_correct_request_data(
79
+ mocker, monkeypatch, patch_datetime_now
80
+ ):
81
81
  mocked_post = mocker.patch("common.helpers.emails.post")
82
82
 
83
83
  expected_body1 = {
@@ -91,7 +91,13 @@ def test_newsletter_sends_correct_request_data(mocker, monkeypatch, patch_dateti
91
91
  {"key": "FULLNAME", "value": "Ray Charles"},
92
92
  ],
93
93
  },
94
- "consentFields": [{"fields": [{"key": "DATETIMECONSENTED", "value": FAKE_TIME.__str__()}]}],
94
+ "consentFields": [
95
+ {
96
+ "fields": [
97
+ {"key": "DATETIMECONSENTED", "value": FAKE_TIME.__str__()}
98
+ ]
99
+ }
100
+ ],
95
101
  "preferences": [{"trout": True}],
96
102
  }
97
103
 
@@ -109,51 +115,76 @@ def test_newsletter_sends_correct_request_data(mocker, monkeypatch, patch_dateti
109
115
  create_contact("Ray", "Charles", "ray.charles@example.com")
110
116
 
111
117
  mocked_post.assert_called_once_with(
112
- DOTMAILER_CREATE_CONTACT_URL, auth=(DOTMAILER_USER, DOTMAILER_PASSWORD), json=expected_body1
118
+ DOTMAILER_CREATE_CONTACT_URL,
119
+ auth=(DOTMAILER_USER, DOTMAILER_PASSWORD),
120
+ json=expected_body1,
113
121
  )
114
122
 
115
- add_contact_to_address_book("Ray", "Charles", "ray.charles@example.com", DotmailerUserType.TEACHER)
123
+ add_contact_to_address_book(
124
+ "Ray", "Charles", "ray.charles@example.com", DotmailerUserType.TEACHER
125
+ )
116
126
 
117
127
  assert mocked_post.call_count == 3
118
128
 
119
129
  mocked_post.assert_any_call(
120
- DOTMAILER_MAIN_ADDRESS_BOOK_URL, auth=(DOTMAILER_USER, DOTMAILER_PASSWORD), json=expected_body2
130
+ DOTMAILER_MAIN_ADDRESS_BOOK_URL,
131
+ auth=(DOTMAILER_USER, DOTMAILER_PASSWORD),
132
+ json=expected_body2,
121
133
  )
122
134
 
123
135
  mocked_post.assert_any_call(
124
- DOTMAILER_TEACHER_ADDRESS_BOOK_URL, auth=(DOTMAILER_USER, DOTMAILER_PASSWORD), json=expected_body2
136
+ DOTMAILER_TEACHER_ADDRESS_BOOK_URL,
137
+ auth=(DOTMAILER_USER, DOTMAILER_PASSWORD),
138
+ json=expected_body2,
125
139
  )
126
140
 
127
141
  mocked_post.reset_mock()
128
142
 
129
- add_contact_to_address_book("Ray", "Charles", "ray.charles@example.com", DotmailerUserType.STUDENT)
143
+ add_contact_to_address_book(
144
+ "Ray", "Charles", "ray.charles@example.com", DotmailerUserType.STUDENT
145
+ )
130
146
 
131
147
  assert mocked_post.call_count == 2
132
148
 
133
149
  mocked_post.assert_any_call(
134
- DOTMAILER_MAIN_ADDRESS_BOOK_URL, auth=(DOTMAILER_USER, DOTMAILER_PASSWORD), json=expected_body2
150
+ DOTMAILER_MAIN_ADDRESS_BOOK_URL,
151
+ auth=(DOTMAILER_USER, DOTMAILER_PASSWORD),
152
+ json=expected_body2,
135
153
  )
136
154
 
137
155
  mocked_post.assert_any_call(
138
- DOTMAILER_STUDENT_ADDRESS_BOOK_URL, auth=(DOTMAILER_USER, DOTMAILER_PASSWORD), json=expected_body2
156
+ DOTMAILER_STUDENT_ADDRESS_BOOK_URL,
157
+ auth=(DOTMAILER_USER, DOTMAILER_PASSWORD),
158
+ json=expected_body2,
139
159
  )
140
160
 
141
161
  mocked_post.reset_mock()
142
162
 
143
- add_contact_to_address_book("Ray", "Charles", "ray.charles@example.com", DotmailerUserType.NO_ACCOUNT)
163
+ add_contact_to_address_book(
164
+ "Ray",
165
+ "Charles",
166
+ "ray.charles@example.com",
167
+ DotmailerUserType.NO_ACCOUNT,
168
+ )
144
169
 
145
170
  assert mocked_post.call_count == 2
146
171
 
147
172
  mocked_post.assert_any_call(
148
- DOTMAILER_MAIN_ADDRESS_BOOK_URL, auth=(DOTMAILER_USER, DOTMAILER_PASSWORD), json=expected_body2
173
+ DOTMAILER_MAIN_ADDRESS_BOOK_URL,
174
+ auth=(DOTMAILER_USER, DOTMAILER_PASSWORD),
175
+ json=expected_body2,
149
176
  )
150
177
 
151
178
  mocked_post.assert_any_call(
152
- DOTMAILER_NO_ACCOUNT_ADDRESS_BOOK_URL, auth=(DOTMAILER_USER, DOTMAILER_PASSWORD), json=expected_body2
179
+ DOTMAILER_NO_ACCOUNT_ADDRESS_BOOK_URL,
180
+ auth=(DOTMAILER_USER, DOTMAILER_PASSWORD),
181
+ json=expected_body2,
153
182
  )
154
183
 
155
184
 
156
- def test_consent_calls_send_correct_request_data(mocker, monkeypatch, patch_datetime_now):
185
+ def test_consent_calls_send_correct_request_data(
186
+ mocker, monkeypatch, patch_datetime_now
187
+ ):
157
188
  mocked_post = mocker.patch("common.helpers.emails.post")
158
189
  mocked_put = mocker.patch("common.helpers.emails.put")
159
190
 
@@ -181,21 +212,34 @@ def test_consent_calls_send_correct_request_data(mocker, monkeypatch, patch_date
181
212
  {"key": "FULLNAME", "value": "Ray Charles"},
182
213
  ],
183
214
  },
184
- "consentFields": [{"fields": [{"key": "DATETIMECONSENTED", "value": FAKE_TIME.__str__()}]}],
215
+ "consentFields": [
216
+ {
217
+ "fields": [
218
+ {"key": "DATETIMECONSENTED", "value": FAKE_TIME.__str__()}
219
+ ]
220
+ }
221
+ ],
185
222
  }
186
223
 
187
- expected_body2 = {"campaignID": DOTMAILER_THANKS_FOR_STAYING_CAMPAIGN_ID, "contactIds": ["1"]}
224
+ expected_body2 = {
225
+ "campaignID": DOTMAILER_THANKS_FOR_STAYING_CAMPAIGN_ID,
226
+ "contactIds": ["1"],
227
+ }
188
228
 
189
229
  add_consent_record_to_dotmailer_user(user)
190
230
 
191
231
  mocked_put.assert_called_once_with(
192
- DOTMAILER_PUT_CONSENT_DATA_URL, auth=(DOTMAILER_USER, DOTMAILER_PASSWORD), json=expected_body1
232
+ DOTMAILER_PUT_CONSENT_DATA_URL,
233
+ auth=(DOTMAILER_USER, DOTMAILER_PASSWORD),
234
+ json=expected_body1,
193
235
  )
194
236
 
195
237
  send_dotmailer_consent_confirmation_email_to_user(user)
196
238
 
197
239
  mocked_post.assert_called_with(
198
- DOTMAILER_SEND_CAMPAIGN_URL, auth=(DOTMAILER_USER, DOTMAILER_PASSWORD), json=expected_body2
240
+ DOTMAILER_SEND_CAMPAIGN_URL,
241
+ auth=(DOTMAILER_USER, DOTMAILER_PASSWORD),
242
+ json=expected_body2,
199
243
  )
200
244
 
201
245
 
@@ -212,9 +256,15 @@ def test_dotmailer_consent_form(mocker, monkeypatch):
212
256
  c = Client()
213
257
  consent_form_url = reverse("consent_form")
214
258
 
215
- mocked_get_user_success = mocker.patch("portal.views.dotmailer.get_dotmailer_user_by_email")
216
- mocked_add_consent = mocker.patch("portal.views.dotmailer.add_consent_record_to_dotmailer_user")
217
- mocked_send_campaign = mocker.patch("portal.views.dotmailer.send_dotmailer_consent_confirmation_email_to_user")
259
+ mocked_get_user_success = mocker.patch(
260
+ "portal.views.dotmailer.get_dotmailer_user_by_email"
261
+ )
262
+ mocked_add_consent = mocker.patch(
263
+ "portal.views.dotmailer.add_consent_record_to_dotmailer_user"
264
+ )
265
+ mocked_send_campaign = mocker.patch(
266
+ "portal.views.dotmailer.send_dotmailer_consent_confirmation_email_to_user"
267
+ )
218
268
 
219
269
  get_consent_form_response = c.get(consent_form_url)
220
270
 
@@ -244,7 +294,10 @@ def test_dotmailer_consent_form(mocker, monkeypatch):
244
294
  mocked_add_consent.assert_called_once()
245
295
  mocked_send_campaign.assert_called_once()
246
296
 
247
- mocker.patch("portal.views.dotmailer.add_consent_record_to_dotmailer_user", side_effect=KeyError)
297
+ mocker.patch(
298
+ "portal.views.dotmailer.add_consent_record_to_dotmailer_user",
299
+ side_effect=KeyError,
300
+ )
248
301
 
249
302
  wrong_email_response = c.post(consent_form_url, data=good_request_data)
250
303
 
@@ -255,4 +308,7 @@ def test_dotmailer_consent_form(mocker, monkeypatch):
255
308
 
256
309
  def _is_warning_message_showing(response):
257
310
  messages = list(response.wsgi_request._messages)
258
- assert messages[0].message == "Valid email address and consent required. Please try again."
311
+ assert (
312
+ messages[0].message
313
+ == "Valid email address and consent required. Please try again."
314
+ )
portal/urls.py CHANGED
@@ -1,13 +1,18 @@
1
1
  from aimmo.urls import HOMEPAGE_REGEX
2
2
  from common.permissions import teacher_verified
3
3
  from django.conf.urls import include, url
4
- from django.urls import path
5
4
  from django.http import HttpResponse
5
+ from django.urls import path
6
6
  from django.views.generic import RedirectView
7
7
  from django.views.generic.base import TemplateView
8
8
  from django.views.i18n import JavaScriptCatalog
9
9
  from game.views.level import play_default_level
10
- from two_factor.views import BackupTokensView, ProfileView, QRGeneratorView, SetupCompleteView
10
+ from two_factor.views import (
11
+ BackupTokensView,
12
+ ProfileView,
13
+ QRGeneratorView,
14
+ SetupCompleteView,
15
+ )
11
16
 
12
17
  from portal.helpers.decorators import ratelimit
13
18
  from portal.helpers.ratelimit import (
@@ -18,9 +23,16 @@ from portal.helpers.ratelimit import (
18
23
  )
19
24
  from portal.helpers.ratelimit import school_student_key
20
25
  from portal.helpers.regexes import ACCESS_CODE_REGEX, JWT_REGEX
26
+ from portal.views import cron
21
27
  from portal.views.about import about, getinvolved, contribute
22
- from portal.views.admin import AdminChangePasswordDoneView, AdminChangePasswordView
23
- from portal.views.aimmo.dashboard import StudentAimmoDashboard, TeacherAimmoDashboard
28
+ from portal.views.admin import (
29
+ AdminChangePasswordDoneView,
30
+ AdminChangePasswordView,
31
+ )
32
+ from portal.views.aimmo.dashboard import (
33
+ StudentAimmoDashboard,
34
+ TeacherAimmoDashboard,
35
+ )
24
36
  from portal.views.api import (
25
37
  AnonymiseOrphanSchoolsView,
26
38
  InactiveUsersView,
@@ -29,8 +41,11 @@ from portal.views.api import (
29
41
  number_users_per_country,
30
42
  registered_users,
31
43
  )
32
- from portal.views.dotmailer import dotmailer_consent_form, process_newsletter_form
33
- from portal.views.email import send_new_users_report, verify_email
44
+ from portal.views.dotmailer import (
45
+ dotmailer_consent_form,
46
+ process_newsletter_form,
47
+ )
48
+ from portal.views.email import verify_email
34
49
  from portal.views.home import (
35
50
  coding_club,
36
51
  download_student_pack,
@@ -43,7 +58,11 @@ from portal.views.home import (
43
58
  from portal.views.legal import privacy_notice, terms
44
59
  from portal.views.login import old_login_form_redirect
45
60
  from portal.views.login.independent_student import IndependentStudentLoginView
46
- from portal.views.login.student import StudentLoginView, StudentClassCodeView, student_direct_login
61
+ from portal.views.login.student import (
62
+ StudentLoginView,
63
+ StudentClassCodeView,
64
+ student_direct_login,
65
+ )
47
66
  from portal.views.login.teacher import TeacherLoginView
48
67
  from portal.views.organisation import organisation_leave, organisation_manage
49
68
  from portal.views.play_landing_page import play_landing_page
@@ -59,7 +78,11 @@ from portal.views.student.edit_account_details import (
59
78
  SchoolStudentEditAccountView,
60
79
  student_edit_account,
61
80
  )
62
- from portal.views.student.play import SchoolStudentDashboard, IndependentStudentDashboard, student_join_organisation
81
+ from portal.views.student.play import (
82
+ SchoolStudentDashboard,
83
+ IndependentStudentDashboard,
84
+ student_join_organisation,
85
+ )
63
86
  from portal.views.teach import teach
64
87
  from portal.views.teacher.dashboard import (
65
88
  dashboard_manage,
@@ -88,20 +111,36 @@ from portal.views.teacher.teach import (
88
111
  teacher_download_csv,
89
112
  teacher_view_class,
90
113
  )
91
- from portal.views import cron
92
-
93
114
  from portal.views.two_factor.core import CustomSetupView
94
115
  from portal.views.two_factor.profile import CustomDisableView
95
116
 
96
117
  js_info_dict = {"packages": ("conf.locale",)}
97
118
 
98
119
  two_factor_patterns = [
99
- url(r"^account/two_factor/setup/$", CustomSetupView.as_view(), name="setup"),
120
+ url(
121
+ r"^account/two_factor/setup/$", CustomSetupView.as_view(), name="setup"
122
+ ),
100
123
  url(r"^account/two_factor/qrcode/$", QRGeneratorView.as_view(), name="qr"),
101
- url(r"^account/two_factor/setup/complete/$", SetupCompleteView.as_view(), name="setup_complete"),
102
- url(r"^account/two_factor/backup/tokens/$", teacher_verified(BackupTokensView.as_view()), name="backup_tokens"),
103
- url(r"^account/two_factor/$", teacher_verified(ProfileView.as_view()), name="profile"),
104
- url(r"^account/two_factor/disable/$", teacher_verified(CustomDisableView.as_view()), name="disable"),
124
+ url(
125
+ r"^account/two_factor/setup/complete/$",
126
+ SetupCompleteView.as_view(),
127
+ name="setup_complete",
128
+ ),
129
+ url(
130
+ r"^account/two_factor/backup/tokens/$",
131
+ teacher_verified(BackupTokensView.as_view()),
132
+ name="backup_tokens",
133
+ ),
134
+ url(
135
+ r"^account/two_factor/$",
136
+ teacher_verified(ProfileView.as_view()),
137
+ name="profile",
138
+ ),
139
+ url(
140
+ r"^account/two_factor/disable/$",
141
+ teacher_verified(CustomDisableView.as_view()),
142
+ name="disable",
143
+ ),
105
144
  ]
106
145
 
107
146
 
@@ -136,22 +175,51 @@ urlpatterns = [
136
175
  ),
137
176
  ),
138
177
  url(HOMEPAGE_REGEX, include("aimmo.urls")),
139
- url(r"^teach/kurono/dashboard/$", TeacherAimmoDashboard.as_view(), name="teacher_aimmo_dashboard"),
140
- url(r"^play/kurono/dashboard/$", StudentAimmoDashboard.as_view(), name="student_aimmo_dashboard"),
141
- url(r"^favicon\.ico$", RedirectView.as_view(url="/static/portal/img/favicon.ico", permanent=True)),
142
- url(r"^administration/password_change/$", AdminChangePasswordView.as_view(), name="administration_password_change"),
178
+ url(
179
+ r"^teach/kurono/dashboard/$",
180
+ TeacherAimmoDashboard.as_view(),
181
+ name="teacher_aimmo_dashboard",
182
+ ),
183
+ url(
184
+ r"^play/kurono/dashboard/$",
185
+ StudentAimmoDashboard.as_view(),
186
+ name="student_aimmo_dashboard",
187
+ ),
188
+ url(
189
+ r"^favicon\.ico$",
190
+ RedirectView.as_view(
191
+ url="/static/portal/img/favicon.ico", permanent=True
192
+ ),
193
+ ),
194
+ url(
195
+ r"^administration/password_change/$",
196
+ AdminChangePasswordView.as_view(),
197
+ name="administration_password_change",
198
+ ),
143
199
  url(
144
200
  r"^administration/password_change_done/$",
145
201
  AdminChangePasswordDoneView.as_view(),
146
202
  name="administration_password_change_done",
147
203
  ),
148
- url(r"^mail/weekly/", send_new_users_report, name="send_new_users_report"),
149
- url(r"^users/inactive/", InactiveUsersView.as_view(), name="inactive_users"),
150
- url(r"^locked_out/$", TemplateView.as_view(template_name="portal/locked_out.html"), name="locked_out"),
151
- url(r"^", include((two_factor_patterns, "two_factor"), namespace="two_factor")),
204
+ url(
205
+ r"^users/inactive/", InactiveUsersView.as_view(), name="inactive_users"
206
+ ),
207
+ url(
208
+ r"^locked_out/$",
209
+ TemplateView.as_view(template_name="portal/locked_out.html"),
210
+ name="locked_out",
211
+ ),
212
+ url(
213
+ r"^",
214
+ include((two_factor_patterns, "two_factor"), namespace="two_factor"),
215
+ ),
152
216
  url(r"^i18n/", include("django.conf.urls.i18n")),
153
217
  url(r"^jsi18n/$", JavaScriptCatalog.as_view(), js_info_dict),
154
- url(r"^(?P<levelName>[A-Z0-9]+)/$", play_default_level, name="play_default_level"),
218
+ url(
219
+ r"^(?P<levelName>[A-Z0-9]+)/$",
220
+ play_default_level,
221
+ name="play_default_level",
222
+ ),
155
223
  url(r"^$", home, name="home"),
156
224
  url(r"^home-learning", home_learning, name="home-learning"),
157
225
  url(r"^register_form", register_view, name="register"),
@@ -181,8 +249,16 @@ urlpatterns = [
181
249
  )(StudentLoginView.as_view()),
182
250
  name="student_login",
183
251
  ),
184
- url(r"^login/student/$", StudentClassCodeView.as_view(), name="student_login_access_code"),
185
- url(r"^u/(?P<user_id>[0-9]+)/(?P<login_id>[a-z0-9]+)/$", student_direct_login, name="student_direct_login"),
252
+ url(
253
+ r"^login/student/$",
254
+ StudentClassCodeView.as_view(),
255
+ name="student_login_access_code",
256
+ ),
257
+ url(
258
+ r"^u/(?P<user_id>[0-9]+)/(?P<login_id>[a-z0-9]+)/$",
259
+ student_direct_login,
260
+ name="student_direct_login",
261
+ ),
186
262
  url(
187
263
  r"^login/independent/$",
188
264
  ratelimit(
@@ -197,32 +273,70 @@ urlpatterns = [
197
273
  ),
198
274
  url(r"^login_form", old_login_form_redirect, name="old_login_form"),
199
275
  url(r"^logout/$", logout_view, name="logout_view"),
200
- url(r"^news_signup/$", process_newsletter_form, name="process_newsletter_form"),
276
+ url(
277
+ r"^news_signup/$",
278
+ process_newsletter_form,
279
+ name="process_newsletter_form",
280
+ ),
201
281
  url(r"^consent_form/$", dotmailer_consent_form, name="consent_form"),
202
282
  url(
203
283
  r"^verify_email/$",
204
- TemplateView.as_view(template_name="portal/email_verification_needed.html"),
284
+ TemplateView.as_view(
285
+ template_name="portal/email_verification_needed.html"
286
+ ),
205
287
  name="email_verification",
206
288
  ),
207
- url(rf"^verify_email/(?P<token>{JWT_REGEX})/$", verify_email, name="verify_email"),
208
- url(r"^user/password/reset/student/$", student_password_reset, name="student_password_reset"),
209
- url(r"^user/password/reset/teacher/$", teacher_password_reset, name="teacher_password_reset"),
210
- url(r"^user/password/reset/done/$", password_reset_done, name="reset_password_email_sent"),
289
+ url(
290
+ rf"^verify_email/(?P<token>{JWT_REGEX})/$",
291
+ verify_email,
292
+ name="verify_email",
293
+ ),
294
+ url(
295
+ r"^user/password/reset/student/$",
296
+ student_password_reset,
297
+ name="student_password_reset",
298
+ ),
299
+ url(
300
+ r"^user/password/reset/teacher/$",
301
+ teacher_password_reset,
302
+ name="teacher_password_reset",
303
+ ),
304
+ url(
305
+ r"^user/password/reset/done/$",
306
+ password_reset_done,
307
+ name="reset_password_email_sent",
308
+ ),
211
309
  url(
212
310
  r"^user/password/reset/(?P<uidb64>[0-9A-Za-z]+)-(?P<token>.+)/$",
213
311
  password_reset_check_and_confirm,
214
312
  name="password_reset_check_and_confirm",
215
313
  ),
216
- url(r"^user/reset_screentime_warning/$", reset_screentime_warning, name="reset_screentime_warning"),
217
- url(r"^user/reset_session_time/$", lambda _: HttpResponse(status=204), name="reset_session_time"),
314
+ url(
315
+ r"^user/reset_screentime_warning/$",
316
+ reset_screentime_warning,
317
+ name="reset_screentime_warning",
318
+ ),
319
+ url(
320
+ r"^user/reset_session_time/$",
321
+ lambda _: HttpResponse(status=204),
322
+ name="reset_session_time",
323
+ ),
218
324
  url(
219
325
  r"^teacher/password/reset/complete/$",
220
326
  TemplateView.as_view(template_name="portal/reset_password_done.html"),
221
327
  name="password_reset_complete",
222
328
  ),
223
329
  url(r"^teach/$", teach, name="teach"),
224
- url(r"^teach/onboarding-organisation/$", organisation_manage, name="onboarding-organisation"),
225
- url(r"^teach/onboarding-classes", teacher_onboarding_create_class, name="onboarding-classes"),
330
+ url(
331
+ r"^teach/onboarding-organisation/$",
332
+ organisation_manage,
333
+ name="onboarding-organisation",
334
+ ),
335
+ url(
336
+ r"^teach/onboarding-classes",
337
+ teacher_onboarding_create_class,
338
+ name="onboarding-classes",
339
+ ),
226
340
  url(
227
341
  rf"^teach/onboarding-class/(?P<access_code>{ACCESS_CODE_REGEX})$",
228
342
  teacher_onboarding_edit_class,
@@ -238,10 +352,22 @@ urlpatterns = [
238
352
  teacher_download_csv,
239
353
  name="teacher_download_csv",
240
354
  ),
241
- url(r"^invited_teacher/(?P<token>[0-9a-f]+)/$", invited_teacher, name="invited_teacher"),
355
+ url(
356
+ r"^invited_teacher/(?P<token>[0-9a-f]+)/$",
357
+ invited_teacher,
358
+ name="invited_teacher",
359
+ ),
242
360
  url(r"^play/$", play_landing_page, name="play"),
243
- url(r"^play/details/$", SchoolStudentDashboard.as_view(), name="student_details"),
244
- url(r"^play/details/independent$", IndependentStudentDashboard.as_view(), name="independent_student_details"),
361
+ url(
362
+ r"^play/details/$",
363
+ SchoolStudentDashboard.as_view(),
364
+ name="student_details",
365
+ ),
366
+ url(
367
+ r"^play/details/independent$",
368
+ IndependentStudentDashboard.as_view(),
369
+ name="independent_student_details",
370
+ ),
245
371
  url(r"^play/account/$", student_edit_account, name="student_edit_account"),
246
372
  url(
247
373
  r"^play/account/independent/$",
@@ -255,19 +381,45 @@ urlpatterns = [
255
381
  )(independentStudentEditAccountView),
256
382
  name="independent_edit_account",
257
383
  ),
258
- url(r"^play/account/school_student/$", SchoolStudentEditAccountView.as_view(), name="school_student_edit_account"),
259
- url(r"^play/join/$", student_join_organisation, name="student_join_organisation"),
384
+ url(
385
+ r"^play/account/school_student/$",
386
+ SchoolStudentEditAccountView.as_view(),
387
+ name="school_student_edit_account",
388
+ ),
389
+ url(
390
+ r"^play/join/$",
391
+ student_join_organisation,
392
+ name="student_join_organisation",
393
+ ),
260
394
  url(r"^about", about, name="about"),
261
395
  url(r"^getinvolved", getinvolved, name="getinvolved"),
262
396
  url(r"^contribute", contribute, name="contribute"),
263
397
  url(r"^terms", terms, name="terms"),
264
398
  url(r"^privacy-notice/$", privacy_notice, name="privacy_notice"),
265
- url(r"^privacy-policy/$", privacy_notice, name="privacy_policy"), # Keeping this to route from old URL
399
+ url(
400
+ r"^privacy-policy/$", privacy_notice, name="privacy_policy"
401
+ ), # Keeping this to route from old URL
266
402
  url(r"^teach/dashboard/$", dashboard_manage, name="dashboard"),
267
- url(r"^teach/dashboard/kick/(?P<pk>[0-9]+)/$", organisation_kick, name="organisation_kick"),
268
- url(r"^teach/dashboard/toggle_admin/(?P<pk>[0-9]+)/$", organisation_toggle_admin, name="organisation_toggle_admin"),
269
- url(r"^teach/dashboard/disable_2FA/(?P<pk>[0-9]+)/$", teacher_disable_2FA, name="teacher_disable_2FA"),
270
- url(r"^teach/dashboard/school/leave/$", organisation_leave, name="organisation_leave"),
403
+ url(
404
+ r"^teach/dashboard/kick/(?P<pk>[0-9]+)/$",
405
+ organisation_kick,
406
+ name="organisation_kick",
407
+ ),
408
+ url(
409
+ r"^teach/dashboard/toggle_admin/(?P<pk>[0-9]+)/$",
410
+ organisation_toggle_admin,
411
+ name="organisation_toggle_admin",
412
+ ),
413
+ url(
414
+ r"^teach/dashboard/disable_2FA/(?P<pk>[0-9]+)/$",
415
+ teacher_disable_2FA,
416
+ name="teacher_disable_2FA",
417
+ ),
418
+ url(
419
+ r"^teach/dashboard/school/leave/$",
420
+ organisation_leave,
421
+ name="organisation_leave",
422
+ ),
271
423
  url(
272
424
  r"^teach/dashboard/student/accept/(?P<pk>[0-9]+)/$",
273
425
  teacher_accept_student_request,
@@ -278,17 +430,31 @@ urlpatterns = [
278
430
  teacher_reject_student_request,
279
431
  name="teacher_reject_student_request",
280
432
  ),
281
- url(rf"^teach/class/(?P<access_code>{ACCESS_CODE_REGEX})$", teacher_view_class, name="view_class"),
282
433
  url(
283
- rf"^teach/class/delete/(?P<access_code>{ACCESS_CODE_REGEX})$", teacher_delete_class, name="teacher_delete_class"
434
+ rf"^teach/class/(?P<access_code>{ACCESS_CODE_REGEX})$",
435
+ teacher_view_class,
436
+ name="view_class",
437
+ ),
438
+ url(
439
+ rf"^teach/class/delete/(?P<access_code>{ACCESS_CODE_REGEX})$",
440
+ teacher_delete_class,
441
+ name="teacher_delete_class",
284
442
  ),
285
443
  url(
286
444
  rf"^teach/class/(?P<access_code>{ACCESS_CODE_REGEX})/students/delete/$",
287
445
  teacher_delete_students,
288
446
  name="teacher_delete_students",
289
447
  ),
290
- url(rf"^teach/class/edit/(?P<access_code>{ACCESS_CODE_REGEX})$", teacher_edit_class, name="teacher_edit_class"),
291
- url(r"^teach/class/student/edit/(?P<pk>[0-9]+)/$", teacher_edit_student, name="teacher_edit_student"),
448
+ url(
449
+ rf"^teach/class/edit/(?P<access_code>{ACCESS_CODE_REGEX})$",
450
+ teacher_edit_class,
451
+ name="teacher_edit_class",
452
+ ),
453
+ url(
454
+ r"^teach/class/student/edit/(?P<pk>[0-9]+)/$",
455
+ teacher_edit_student,
456
+ name="teacher_edit_student",
457
+ ),
292
458
  url(
293
459
  rf"^teach/class/(?P<access_code>{ACCESS_CODE_REGEX})/password_reset/$",
294
460
  teacher_class_password_reset,
@@ -304,9 +470,15 @@ urlpatterns = [
304
470
  teacher_move_students,
305
471
  name="teacher_move_students",
306
472
  ),
307
- url(r"^teach/dashboard/resend_invite/(?P<token>[0-9a-f]+)/$", resend_invite_teacher, name="resend_invite_teacher"),
308
473
  url(
309
- r"^teach/dashboard/toggle_admin_invite/(?P<invite_id>[0-9]+)/$", invite_toggle_admin, name="invite_toggle_admin"
474
+ r"^teach/dashboard/resend_invite/(?P<token>[0-9a-f]+)/$",
475
+ resend_invite_teacher,
476
+ name="resend_invite_teacher",
477
+ ),
478
+ url(
479
+ r"^teach/dashboard/toggle_admin_invite/(?P<invite_id>[0-9]+)/$",
480
+ invite_toggle_admin,
481
+ name="invite_toggle_admin",
310
482
  ),
311
483
  url(
312
484
  r"^teach/dashboard/delete_teacher_invite/(?P<token>[0-9a-f]+)$",
@@ -320,7 +492,9 @@ urlpatterns = [
320
492
  ),
321
493
  url(r"^delete/account/$", delete_account, name="delete_account"),
322
494
  url(
323
- r"^schools/anonymise/(?P<start_id>\d+)/", AnonymiseOrphanSchoolsView.as_view(), name="anonymise_orphan_schools"
495
+ r"^schools/anonymise/(?P<start_id>\d+)/",
496
+ AnonymiseOrphanSchoolsView.as_view(),
497
+ name="anonymise_orphan_schools",
324
498
  ),
325
499
  url(
326
500
  r"^api/",
@@ -345,6 +519,14 @@ urlpatterns = [
345
519
  ),
346
520
  ),
347
521
  url(r"^codingClub/$", coding_club, name="codingClub"),
348
- url(r"^codingClub/(?P<student_pack_type>[3-4])/", download_student_pack, name="download_student_pack"),
349
- url(r"^removeFakeAccounts/", RemoveFakeAccounts.as_view(), name="remove_fake_accounts"),
522
+ url(
523
+ r"^codingClub/(?P<student_pack_type>[3-4])/",
524
+ download_student_pack,
525
+ name="download_student_pack",
526
+ ),
527
+ url(
528
+ r"^removeFakeAccounts/",
529
+ RemoveFakeAccounts.as_view(),
530
+ name="remove_fake_accounts",
531
+ ),
350
532
  ]
portal/views/email.py CHANGED
@@ -1,38 +1,47 @@
1
1
  from datetime import timedelta
2
2
 
3
3
  import jwt
4
- from common.helpers.emails import NOTIFICATION_EMAIL, send_email
5
- from common.models import School, Student, Teacher
4
+ from common.models import Teacher
6
5
  from common.permissions import logged_in_as_independent_student
7
6
  from django.conf import settings
8
7
  from django.contrib import messages as messages
9
8
  from django.contrib.auth.models import User
10
- from django.db.models import Count
11
- from django.http import HttpResponse, HttpResponseRedirect
9
+ from django.http import HttpResponseRedirect
12
10
  from django.shortcuts import render
13
11
  from django.urls import reverse_lazy
14
12
  from django.utils import timezone
15
- from django_countries import countries
16
-
17
- from portal.app_settings import CONTACT_FORM_EMAILS
18
13
 
19
14
 
20
15
  def verify_email(request, token):
21
16
  decoded_jwt = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
22
17
 
23
18
  user_found = User.objects.filter(email=decoded_jwt["email"]).first()
24
- usertype = "TEACHER" if Teacher.objects.filter(new_user=user_found).exists() else "INDEP_STUDENT"
19
+ usertype = (
20
+ "TEACHER"
21
+ if Teacher.objects.filter(new_user=user_found).exists()
22
+ else "INDEP_STUDENT"
23
+ )
25
24
  is_expired = decoded_jwt["expires"] < timezone.now().timestamp()
26
25
 
27
26
  is_changing_email = decoded_jwt["new_email"] != ""
28
- updated_email = decoded_jwt["new_email"] if is_changing_email else decoded_jwt["email"]
27
+ updated_email = (
28
+ decoded_jwt["new_email"] if is_changing_email else decoded_jwt["email"]
29
+ )
29
30
 
30
31
  if not user_found or is_expired:
31
- return render(request, "portal/email_verification_failed.html", {"usertype": usertype})
32
+ return render(
33
+ request,
34
+ "portal/email_verification_failed.html",
35
+ {"usertype": usertype},
36
+ )
32
37
 
33
38
  is_user_verified = user_found.userprofile.is_verified
34
39
  if is_user_verified and not is_changing_email:
35
- return render(request, "portal/email_verification_failed.html", {"usertype": usertype})
40
+ return render(
41
+ request,
42
+ "portal/email_verification_failed.html",
43
+ {"usertype": usertype},
44
+ )
36
45
 
37
46
  if not is_user_verified or is_changing_email:
38
47
  user_found.email = updated_email
@@ -41,7 +50,9 @@ def verify_email(request, token):
41
50
  user_found.userprofile.save()
42
51
  user_found.save()
43
52
 
44
- messages.success(request, "Your email address was successfully verified, please log in.")
53
+ messages.success(
54
+ request, "Your email address was successfully verified, please log in."
55
+ )
45
56
 
46
57
  if logged_in_as_independent_student(user_found):
47
58
  login_url = "independent_student_login"
@@ -53,43 +64,7 @@ def verify_email(request, token):
53
64
 
54
65
  def has_verification_failed(verifications):
55
66
  return (
56
- len(verifications) > 1 or verifications[0].verified or (verifications[0].expiry - timezone.now()) < timedelta()
57
- )
58
-
59
-
60
- def send_new_users_report(request):
61
- new_users_count = User.objects.filter(date_joined__gte=timezone.now() - timedelta(days=7)).count()
62
- users_count = User.objects.count()
63
- active_users = User.objects.filter(last_login__gte=timezone.now() - timedelta(days=7)).count()
64
- school_count = School.objects.count()
65
- teacher_count = Teacher.objects.count()
66
- student_count = Student.objects.count()
67
- schools_countries = School.objects.values("country").annotate(nb_countries=Count("id")).order_by("-nb_countries")
68
- nb_countries = schools_countries.count()
69
- countries_count = "\n".join(
70
- "{}: {}".format(dict(countries)[k["country"]], k["nb_countries"]) for k in schools_countries[:3]
71
- )
72
- send_email(
73
- NOTIFICATION_EMAIL,
74
- CONTACT_FORM_EMAILS,
75
- "new users",
76
- "There are {new_users} new users this week!\n"
77
- "The total number of registered users is now: {users}\n"
78
- "Current number of schools: {schools}\n"
79
- "Current number of teachers: {teachers}\n"
80
- "Current number of students: {students}\n"
81
- "Number of users that last logged in during the last week: {active_users}\n"
82
- "Number of countries with registered schools: {countries}\n"
83
- "Top 3 - schools per country:\n{countries_counter}".format(
84
- new_users=new_users_count,
85
- users=users_count,
86
- schools=school_count,
87
- teachers=teacher_count,
88
- students=student_count,
89
- countries=nb_countries,
90
- active_users=active_users,
91
- countries_counter=countries_count,
92
- ),
93
- "new users",
67
+ len(verifications) > 1
68
+ or verifications[0].verified
69
+ or (verifications[0].expiry - timezone.now()) < timedelta()
94
70
  )
95
- return HttpResponse("success")
@@ -1,33 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: codeforlife-portal
3
- Version: 6.44.4
4
- Classifier: Programming Language :: Python
5
- Classifier: Programming Language :: Python :: 3.8
6
- Classifier: Framework :: Django
7
- License-File: LICENSE.md
8
- Requires-Dist: django ==3.2.25
9
- Requires-Dist: django-countries ==7.3.1
10
- Requires-Dist: djangorestframework ==3.13.1
11
- Requires-Dist: django-pipeline ==2.0.8
12
- Requires-Dist: django-recaptcha ==2.0.6
13
- Requires-Dist: pyyaml ==5.4.1
14
- Requires-Dist: importlib-metadata ==4.13.0
15
- Requires-Dist: rapid-router >=4
16
- Requires-Dist: aimmo >=2
17
- Requires-Dist: reportlab ==3.6.13
18
- Requires-Dist: django-formtools ==2.2
19
- Requires-Dist: django-otp ==1.0.2
20
- Requires-Dist: requests ==2.31.0
21
- Requires-Dist: django-treebeard ==4.3.1
22
- Requires-Dist: django-sekizai ==2.0.0
23
- Requires-Dist: django-classy-tags ==2.0.0
24
- Requires-Dist: libsass ==0.23.0
25
- Requires-Dist: phonenumbers ==8.12.12
26
- Requires-Dist: more-itertools ==8.7.0
27
- Requires-Dist: cfl-common ==6.44.4
28
- Requires-Dist: django-ratelimit ==3.0.1
29
- Requires-Dist: django-preventconcurrentlogins ==0.8.2
30
- Requires-Dist: django-csp ==3.7
31
- Requires-Dist: setuptools ==65.5.1
32
- Requires-Dist: django-import-export
33
-