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.
- codeforlife_portal-6.44.8.dist-info/METADATA +73 -0
- {codeforlife_portal-6.44.4.dist-info → codeforlife_portal-6.44.8.dist-info}/RECORD +11 -11
- portal/__init__.py +1 -1
- portal/static/portal/img/gitbook.png +0 -0
- portal/templates/portal/contribute.html +9 -7
- portal/tests/test_emails.py +90 -34
- portal/urls.py +237 -55
- portal/views/email.py +26 -51
- codeforlife_portal-6.44.4.dist-info/METADATA +0 -33
- {codeforlife_portal-6.44.4.dist-info → codeforlife_portal-6.44.8.dist-info}/LICENSE.md +0 -0
- {codeforlife_portal-6.44.4.dist-info → codeforlife_portal-6.44.8.dist-info}/WHEEL +0 -0
- {codeforlife_portal-6.44.4.dist-info → codeforlife_portal-6.44.8.dist-info}/top_level.txt +0 -0
|
@@ -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
|
+
[](https://github.com/ocadotechnology/codeforlife-portal/actions/workflows/ci.yml)
|
|
38
|
+
[](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
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
646
|
-
codeforlife_portal-6.44.
|
|
647
|
-
codeforlife_portal-6.44.
|
|
648
|
-
codeforlife_portal-6.44.
|
|
649
|
-
codeforlife_portal-6.44.
|
|
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.
|
|
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
|
-
<
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
81
|
-
|
|
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
|
portal/tests/test_emails.py
CHANGED
|
@@ -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(
|
|
45
|
+
mocked_add_to_address_book = mocker.patch(
|
|
46
|
+
"common.helpers.emails.add_contact_to_address_book"
|
|
47
|
+
)
|
|
55
48
|
|
|
56
|
-
add_to_dotmailer(
|
|
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(
|
|
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(
|
|
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": [
|
|
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,
|
|
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(
|
|
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,
|
|
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,
|
|
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(
|
|
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,
|
|
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,
|
|
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(
|
|
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,
|
|
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,
|
|
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(
|
|
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": [
|
|
215
|
+
"consentFields": [
|
|
216
|
+
{
|
|
217
|
+
"fields": [
|
|
218
|
+
{"key": "DATETIMECONSENTED", "value": FAKE_TIME.__str__()}
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
],
|
|
185
222
|
}
|
|
186
223
|
|
|
187
|
-
expected_body2 = {
|
|
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,
|
|
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,
|
|
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(
|
|
216
|
-
|
|
217
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
23
|
-
|
|
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
|
|
33
|
-
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
url(
|
|
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(
|
|
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(
|
|
185
|
-
|
|
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(
|
|
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(
|
|
284
|
+
TemplateView.as_view(
|
|
285
|
+
template_name="portal/email_verification_needed.html"
|
|
286
|
+
),
|
|
205
287
|
name="email_verification",
|
|
206
288
|
),
|
|
207
|
-
url(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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(
|
|
217
|
-
|
|
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(
|
|
225
|
-
|
|
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(
|
|
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(
|
|
244
|
-
|
|
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(
|
|
259
|
-
|
|
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(
|
|
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(
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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/
|
|
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(
|
|
291
|
-
|
|
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/
|
|
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+)/",
|
|
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(
|
|
349
|
-
|
|
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.
|
|
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.
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|