codeforlife-portal 6.41.4__py2.py3-none-any.whl → 6.41.6__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of codeforlife-portal might be problematic. Click here for more details.
- cfl_common/common/app_settings.py +3 -0
- cfl_common/common/email_messages.py +0 -58
- cfl_common/common/helpers/data_migration_loader.py +3 -4
- cfl_common/common/helpers/emails.py +27 -45
- cfl_common/common/helpers/generators.py +1 -1
- cfl_common/common/mail.py +116 -0
- cfl_common/common/migrations/0002_emailverification.py +1 -3
- cfl_common/common/migrations/0005_add_worksheets.py +1 -5
- cfl_common/common/migrations/0007_add_pdf_names_to_first_two_worksheets.py +1 -5
- cfl_common/common/migrations/0008_unlock_worksheet_3.py +1 -5
- cfl_common/common/migrations/0017_copy_email_to_username.py +2 -8
- cfl_common/common/migrations/0021_school_is_active.py +7 -7
- cfl_common/common/migrations/0022_school_cleanup.py +9 -9
- cfl_common/common/migrations/0023_userprofile_aimmo_badges.py +4 -4
- cfl_common/common/migrations/0025_schoolteacherinvitation.py +29 -13
- cfl_common/common/migrations/0026_teacher_remove_join_request.py +5 -5
- cfl_common/common/migrations/0027_class_created_by.py +10 -4
- cfl_common/common/migrations/0028_coding_club_downloads.py +5 -5
- cfl_common/common/migrations/0029_dynamicelement.py +6 -6
- cfl_common/common/migrations/0030_add_maintenance_banner.py +1 -3
- cfl_common/common/migrations/0031_improve_admin_panel.py +32 -14
- cfl_common/common/migrations/0032_dailyactivity_level_control_submits.py +3 -3
- cfl_common/common/migrations/0033_password_reset_tracking_fields.py +5 -5
- cfl_common/common/migrations/0034_dailyactivity_daily_school_student_lockout_reset.py +3 -3
- cfl_common/common/migrations/0035_rename_lockout_fields.py +10 -10
- cfl_common/common/migrations/0037_migrate_email_verification.py +2 -2
- cfl_common/common/migrations/0038_delete_emailverification.py +2 -2
- cfl_common/common/migrations/0039_copy_email_to_username.py +1 -6
- cfl_common/common/migrations/0040_school_county.py +3 -3
- cfl_common/common/migrations/0042_totalactivity.py +7 -7
- cfl_common/common/migrations/0044_update_activity_models.py +9 -9
- cfl_common/common/migrations/0045_otp.py +5 -5
- cfl_common/common/migrations/0046_alter_school_country.py +3 -3
- cfl_common/common/tests/utils/email.py +14 -34
- cfl_common/common/tests/utils/student.py +8 -8
- cfl_common/common/tests/utils/teacher.py +8 -8
- {codeforlife_portal-6.41.4.dist-info → codeforlife_portal-6.41.6.dist-info}/METADATA +2 -2
- {codeforlife_portal-6.41.4.dist-info → codeforlife_portal-6.41.6.dist-info}/RECORD +52 -51
- example_project/portal_test_settings.py +5 -1
- portal/__init__.py +1 -1
- portal/static/portal/img/logo_cfl.png +0 -0
- portal/static/portal/img/logo_cfl_reminder_cards.jpg +0 -0
- portal/tests/test_independent_student.py +30 -17
- portal/tests/test_ratelimit.py +15 -12
- portal/tests/test_teacher.py +35 -21
- portal/tests/test_teacher_student.py +13 -3
- portal/tests/test_views.py +55 -194
- portal/views/cron/user.py +12 -49
- portal/views/teacher/teach.py +6 -3
- {codeforlife_portal-6.41.4.dist-info → codeforlife_portal-6.41.6.dist-info}/LICENSE.md +0 -0
- {codeforlife_portal-6.41.4.dist-info → codeforlife_portal-6.41.6.dist-info}/WHEEL +0 -0
- {codeforlife_portal-6.41.4.dist-info → codeforlife_portal-6.41.6.dist-info}/top_level.txt +0 -0
|
@@ -3,6 +3,9 @@ from django.conf import settings
|
|
|
3
3
|
# Email address to source notifications from
|
|
4
4
|
EMAIL_ADDRESS = getattr(settings, "EMAIL_ADDRESS", "no-reply@codeforlife.education")
|
|
5
5
|
|
|
6
|
+
# Dotdigital authorization details
|
|
7
|
+
DOTDIGITAL_AUTH = getattr(settings, "DOTDIGITAL_AUTH", "")
|
|
8
|
+
|
|
6
9
|
# Dotmailer URLs for adding users to the newsletter address book
|
|
7
10
|
DOTMAILER_CREATE_CONTACT_URL = getattr(settings, "DOTMAILER_CREATE_CONTACT_URL", "")
|
|
8
11
|
DOTMAILER_MAIN_ADDRESS_BOOK_URL = getattr(settings, "DOTMAILER_MAIN_ADDRESS_BOOK_URL", "")
|
|
@@ -14,64 +14,6 @@ def resetEmailPasswordMessage(request, domain, uid, token, protocol):
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
def emailVerificationNeededEmail(request, token):
|
|
18
|
-
url = f"{request.build_absolute_uri(reverse('verify_email', kwargs={'token': token}))}"
|
|
19
|
-
privacy_notice_url = f"{request.build_absolute_uri(reverse('privacy_notice'))}"
|
|
20
|
-
terms_url = f"{request.build_absolute_uri(reverse('terms'))}"
|
|
21
|
-
return {
|
|
22
|
-
"subject": f"Email verification ",
|
|
23
|
-
"message": (
|
|
24
|
-
f"Please go to {url} to verify your email address.\n\nBy activating the account you confirm that you have "
|
|
25
|
-
f"read and agreed to our terms ({terms_url}) and our privacy notice ({privacy_notice_url})."
|
|
26
|
-
),
|
|
27
|
-
"url": {"verify_url": url},
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def parentsEmailVerificationNeededEmail(request, user, token):
|
|
32
|
-
url = f"{request.build_absolute_uri(reverse('verify_email', kwargs={'token': token}))}"
|
|
33
|
-
privacy_notice_url = f"{request.build_absolute_uri(reverse('privacy_notice'))}"
|
|
34
|
-
terms_url = f"{request.build_absolute_uri(reverse('terms'))}"
|
|
35
|
-
return {
|
|
36
|
-
"subject": f"Code for Life account request",
|
|
37
|
-
"message": (
|
|
38
|
-
f"{user.first_name} has requested to create a Code for Life account so that they can learn how to code for "
|
|
39
|
-
f"FREE! 🎉\n\n"
|
|
40
|
-
f"{user.first_name} provided your email address as a guardian that is able to read the privacy notice "
|
|
41
|
-
f"documents and agree to the terms and conditions related to our website on their behalf.\n\n"
|
|
42
|
-
f"If you also wish to receive communication from us, you can sign up for newsletters on our website here. 📧\n\n"
|
|
43
|
-
f"Please activate the account for {user.first_name} by following this link: {url}.\n\nBy activating the "
|
|
44
|
-
f"account you confirm that you have read and agreed to our terms ({terms_url}) and our privacy notice "
|
|
45
|
-
f"({privacy_notice_url})."
|
|
46
|
-
),
|
|
47
|
-
"url": {"verify_url": url},
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def emailChangeVerificationEmail(request, token):
|
|
52
|
-
url = f"{request.build_absolute_uri(reverse('verify_email', kwargs={'token': token}))}"
|
|
53
|
-
return {
|
|
54
|
-
"subject": f"Email verification needed",
|
|
55
|
-
"message": (
|
|
56
|
-
f"You are changing your email, please go to "
|
|
57
|
-
f"{url} "
|
|
58
|
-
f"to verify your new email address. If you are not part of Code for Life "
|
|
59
|
-
f"then please ignore this email."
|
|
60
|
-
),
|
|
61
|
-
"url": {"verify_url": url},
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def emailChangeNotificationEmail(request, new_email_address):
|
|
66
|
-
return {
|
|
67
|
-
"subject": f"Email address update",
|
|
68
|
-
"message": (
|
|
69
|
-
f"There is a request to change the email address of your account to "
|
|
70
|
-
f"{new_email_address}. If this was not you, please get in contact with us."
|
|
71
|
-
),
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
17
|
def userAlreadyRegisteredEmail(request, email, is_independent_student=False):
|
|
76
18
|
if is_independent_student:
|
|
77
19
|
login_url = reverse("independent_student_login")
|
|
@@ -10,7 +10,8 @@ def load_data_from_file(file_name) -> Callable:
|
|
|
10
10
|
For use with migrations.RunPython
|
|
11
11
|
|
|
12
12
|
Args:
|
|
13
|
-
file_name (str): The name of the file containing the data you want to load. Include `.json` at the end.
|
|
13
|
+
file_name (str): The name of the file containing the data you want to load. Include `.json` at the end.
|
|
14
|
+
The file must be in the fixtures directory.
|
|
14
15
|
"""
|
|
15
16
|
absolute_file_path = Path(__file__).resolve().parent.parent / "fixtures" / file_name
|
|
16
17
|
|
|
@@ -26,9 +27,7 @@ def load_data_from_file(file_name) -> Callable:
|
|
|
26
27
|
try:
|
|
27
28
|
return apps.get_model(model_identifier)
|
|
28
29
|
except (LookupError, TypeError):
|
|
29
|
-
raise base.DeserializationError(
|
|
30
|
-
"Invalid model identifier: '%s'" % model_identifier
|
|
31
|
-
)
|
|
30
|
+
raise base.DeserializationError("Invalid model identifier: '%s'" % model_identifier)
|
|
32
31
|
|
|
33
32
|
# Replace the _get_model() function on the module, so loaddata can utilize it.
|
|
34
33
|
python._get_model = _get_model
|
|
@@ -7,20 +7,16 @@ from uuid import uuid4
|
|
|
7
7
|
import jwt
|
|
8
8
|
from common import app_settings
|
|
9
9
|
from common.app_settings import domain
|
|
10
|
-
from common.
|
|
11
|
-
|
|
12
|
-
emailChangeVerificationEmail,
|
|
13
|
-
emailVerificationNeededEmail,
|
|
14
|
-
parentsEmailVerificationNeededEmail,
|
|
15
|
-
)
|
|
16
|
-
from common.models import Teacher, Student
|
|
10
|
+
from common.mail import campaign_ids, send_dotdigital_email
|
|
11
|
+
from common.models import Student, Teacher
|
|
17
12
|
from django.conf import settings
|
|
18
13
|
from django.contrib.auth.models import User
|
|
19
14
|
from django.core.mail import EmailMultiAlternatives
|
|
20
15
|
from django.http import HttpResponse
|
|
21
16
|
from django.template import loader
|
|
17
|
+
from django.urls import reverse
|
|
22
18
|
from django.utils import timezone
|
|
23
|
-
from requests import
|
|
19
|
+
from requests import delete, get, post, put
|
|
24
20
|
from requests.exceptions import RequestException
|
|
25
21
|
|
|
26
22
|
NOTIFICATION_EMAIL = "Code For Life Notification <" + app_settings.EMAIL_ADDRESS + ">"
|
|
@@ -123,14 +119,10 @@ def send_verification_email(request, user, data, new_email=None, age=None):
|
|
|
123
119
|
|
|
124
120
|
# if the user is a teacher
|
|
125
121
|
if age is None:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
[user.email],
|
|
130
|
-
message["subject"],
|
|
131
|
-
message["message"],
|
|
132
|
-
message["subject"],
|
|
133
|
-
replace_url=message["url"],
|
|
122
|
+
url = f"{request.build_absolute_uri(reverse('verify_email', kwargs={'token': verification}))}"
|
|
123
|
+
|
|
124
|
+
send_dotdigital_email(
|
|
125
|
+
campaign_ids["verify_new_user"], [user.email], personalization_values={"VERIFICATION_LINK": url}
|
|
134
126
|
)
|
|
135
127
|
|
|
136
128
|
if _newsletter_ticked(data):
|
|
@@ -138,24 +130,16 @@ def send_verification_email(request, user, data, new_email=None, age=None):
|
|
|
138
130
|
# if the user is an independent student
|
|
139
131
|
else:
|
|
140
132
|
if age < 13:
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
133
|
+
url = f"{request.build_absolute_uri(reverse('verify_email', kwargs={'token': verification}))}"
|
|
134
|
+
send_dotdigital_email(
|
|
135
|
+
campaign_ids["verify_new_user_via_parent"],
|
|
144
136
|
[user.email],
|
|
145
|
-
|
|
146
|
-
message["message"],
|
|
147
|
-
message["subject"],
|
|
148
|
-
replace_url=message["url"],
|
|
137
|
+
personalization_values={"FIRST_NAME": user.first_name, "ACTIVATION_LINK": url},
|
|
149
138
|
)
|
|
150
139
|
else:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
[user.email],
|
|
155
|
-
message["subject"],
|
|
156
|
-
message["message"],
|
|
157
|
-
message["subject"],
|
|
158
|
-
replace_url=message["url"],
|
|
140
|
+
url = f"{request.build_absolute_uri(reverse('verify_email', kwargs={'token': verification}))}"
|
|
141
|
+
send_dotdigital_email(
|
|
142
|
+
campaign_ids["verify_new_user"], [user.email], personalization_values={"VERIFICATION_LINK": url}
|
|
159
143
|
)
|
|
160
144
|
|
|
161
145
|
if _newsletter_ticked(data):
|
|
@@ -163,15 +147,9 @@ def send_verification_email(request, user, data, new_email=None, age=None):
|
|
|
163
147
|
# verifying change of email address.
|
|
164
148
|
else:
|
|
165
149
|
verification = generate_token(user, new_email)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
VERIFICATION_EMAIL,
|
|
170
|
-
[new_email],
|
|
171
|
-
message["subject"],
|
|
172
|
-
message["message"],
|
|
173
|
-
message["subject"],
|
|
174
|
-
replace_url=message["url"],
|
|
150
|
+
url = f"{request.build_absolute_uri(reverse('verify_email', kwargs={'token': verification}))}"
|
|
151
|
+
send_dotdigital_email(
|
|
152
|
+
campaign_ids["email_change_verification"], [new_email], personalization_values={"VERIFICATION_LINK": url}
|
|
175
153
|
)
|
|
176
154
|
|
|
177
155
|
|
|
@@ -281,8 +259,11 @@ def update_indy_email(user, request, data):
|
|
|
281
259
|
changing_email = True
|
|
282
260
|
users_with_email = User.objects.filter(email=new_email)
|
|
283
261
|
|
|
284
|
-
|
|
285
|
-
|
|
262
|
+
send_dotdigital_email(
|
|
263
|
+
campaign_ids["email_change_notification"],
|
|
264
|
+
[user.email],
|
|
265
|
+
personalization_values={"NEW_EMAIL_ADDRESS": new_email},
|
|
266
|
+
)
|
|
286
267
|
|
|
287
268
|
# email is available
|
|
288
269
|
if not users_with_email.exists():
|
|
@@ -299,9 +280,10 @@ def update_email(user: Teacher or Student, request, data):
|
|
|
299
280
|
changing_email = True
|
|
300
281
|
users_with_email = User.objects.filter(email=new_email)
|
|
301
282
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
283
|
+
send_dotdigital_email(
|
|
284
|
+
campaign_ids["email_change_notification"],
|
|
285
|
+
[user.new_user.email],
|
|
286
|
+
personalization_values={"NEW_EMAIL_ADDRESS": new_email},
|
|
305
287
|
)
|
|
306
288
|
|
|
307
289
|
# email is available
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
from common import app_settings
|
|
6
|
+
|
|
7
|
+
campaign_ids = {
|
|
8
|
+
"email_change_notification": 1551600,
|
|
9
|
+
"email_change_verification": 1551594,
|
|
10
|
+
"verify_new_user": 1551577,
|
|
11
|
+
"verify_new_user_first_reminder": 1557170,
|
|
12
|
+
"verify_new_user_second_reminder": 1557173,
|
|
13
|
+
"verify_new_user_via_parent": 1551587,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def add_contact(email: str):
|
|
18
|
+
"""Add a new contact to Dotdigital."""
|
|
19
|
+
# TODO: implement
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def remove_contact(email: str):
|
|
23
|
+
"""Remove an existing contact from Dotdigital."""
|
|
24
|
+
# TODO: implement
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class EmailAttachment:
|
|
29
|
+
"""An email attachment for a Dotdigital triggered campaign."""
|
|
30
|
+
|
|
31
|
+
file_name: str
|
|
32
|
+
mime_type: str
|
|
33
|
+
content: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# pylint: disable-next=too-many-arguments
|
|
37
|
+
def send_dotdigital_email(
|
|
38
|
+
campaign_id: int,
|
|
39
|
+
to_addresses: t.List[str],
|
|
40
|
+
cc_addresses: t.Optional[t.List[str]] = None,
|
|
41
|
+
bcc_addresses: t.Optional[t.List[str]] = None,
|
|
42
|
+
from_address: t.Optional[str] = None,
|
|
43
|
+
personalization_values: t.Optional[t.Dict[str, str]] = None,
|
|
44
|
+
metadata: t.Optional[str] = None,
|
|
45
|
+
attachments: t.Optional[t.List[EmailAttachment]] = None,
|
|
46
|
+
region: str = "r1",
|
|
47
|
+
auth: t.Optional[str] = None,
|
|
48
|
+
timeout: int = 30,
|
|
49
|
+
):
|
|
50
|
+
# pylint: disable=line-too-long
|
|
51
|
+
"""Send a triggered email campaign using DotDigital's API.
|
|
52
|
+
|
|
53
|
+
https://developer.dotdigital.com/reference/send-transactional-email-using-a-triggered-campaign
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
campaign_id: The ID of the triggered campaign, which needs to be included within the request body.
|
|
57
|
+
to_addresses: The email address(es) to send to.
|
|
58
|
+
cc_addresses: The CC email address or address to to send to. separate email addresses with a comma. Maximum: 100.
|
|
59
|
+
bcc_addresses: The BCC email address or address to to send to. separate email addresses with a comma. Maximum: 100.
|
|
60
|
+
from_address: The From address for your email. Note: The From address must already be added to your account. Otherwise, your account's default From address is used.
|
|
61
|
+
personalization_values: Each personalisation value is a key-value pair; the placeholder name of the personalization value needs to be included in the request body.
|
|
62
|
+
metadata: The metadata for your email. It can be either a single value or a series of values in a JSON object.
|
|
63
|
+
attachments: A Base64 encoded string. All attachment types are supported. Maximum file size: 15 MB.
|
|
64
|
+
region: The Dotdigital region id your account belongs to e.g. r1, r2 or r3.
|
|
65
|
+
auth: The authorization header used to enable API access. If None, the value will be retrieved from the DOTDIGITAL_AUTH environment variable.
|
|
66
|
+
timeout: Send timeout to avoid hanging.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
AssertionError: If failed to send email.
|
|
70
|
+
"""
|
|
71
|
+
# pylint: enable=line-too-long
|
|
72
|
+
|
|
73
|
+
if auth is None:
|
|
74
|
+
auth = app_settings.DOTDIGITAL_AUTH
|
|
75
|
+
|
|
76
|
+
body = {
|
|
77
|
+
"campaignId": campaign_id,
|
|
78
|
+
"toAddresses": to_addresses,
|
|
79
|
+
}
|
|
80
|
+
if cc_addresses is not None:
|
|
81
|
+
body["ccAddresses"] = cc_addresses
|
|
82
|
+
if bcc_addresses is not None:
|
|
83
|
+
body["bccAddresses"] = bcc_addresses
|
|
84
|
+
if from_address is not None:
|
|
85
|
+
body["fromAddress"] = from_address
|
|
86
|
+
if personalization_values is not None:
|
|
87
|
+
body["personalizationValues"] = [
|
|
88
|
+
{
|
|
89
|
+
"name": key,
|
|
90
|
+
"value": value,
|
|
91
|
+
}
|
|
92
|
+
for key, value in personalization_values.items()
|
|
93
|
+
]
|
|
94
|
+
if metadata is not None:
|
|
95
|
+
body["metadata"] = metadata
|
|
96
|
+
if attachments is not None:
|
|
97
|
+
body["attachments"] = [
|
|
98
|
+
{
|
|
99
|
+
"fileName": attachment.file_name,
|
|
100
|
+
"mimeType": attachment.mime_type,
|
|
101
|
+
"content": attachment.content,
|
|
102
|
+
}
|
|
103
|
+
for attachment in attachments
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
response = requests.post(
|
|
107
|
+
url=f"https://{region}-api.dotdigital.com/v2/email/triggered-campaign",
|
|
108
|
+
json=body,
|
|
109
|
+
headers={
|
|
110
|
+
"accept": "text/plain",
|
|
111
|
+
"authorization": auth,
|
|
112
|
+
},
|
|
113
|
+
timeout=timeout,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
assert response.ok, "Failed to send email." f" Reason: {response.reason}." f" Text: {response.text}."
|
|
@@ -32,9 +32,7 @@ class Migration(migrations.Migration):
|
|
|
32
32
|
("token", models.CharField(max_length=30)),
|
|
33
33
|
(
|
|
34
34
|
"email",
|
|
35
|
-
models.CharField(
|
|
36
|
-
blank=True, default=None, max_length=200, null=True
|
|
37
|
-
),
|
|
35
|
+
models.CharField(blank=True, default=None, max_length=200, null=True),
|
|
38
36
|
),
|
|
39
37
|
("expiry", models.DateTimeField()),
|
|
40
38
|
("verified", models.BooleanField(default=False)),
|
|
@@ -9,8 +9,4 @@ class Migration(migrations.Migration):
|
|
|
9
9
|
("aimmo", "0020_add_info_to_worksheet"),
|
|
10
10
|
]
|
|
11
11
|
|
|
12
|
-
operations = [
|
|
13
|
-
migrations.RunPython(
|
|
14
|
-
migrations.RunPython.noop, reverse_code=migrations.RunPython.noop
|
|
15
|
-
)
|
|
16
|
-
]
|
|
12
|
+
operations = [migrations.RunPython(migrations.RunPython.noop, reverse_code=migrations.RunPython.noop)]
|
|
@@ -9,8 +9,4 @@ class Migration(migrations.Migration):
|
|
|
9
9
|
("aimmo", "0021_add_pdf_names_to_worksheet"),
|
|
10
10
|
]
|
|
11
11
|
|
|
12
|
-
operations = [
|
|
13
|
-
migrations.RunPython(
|
|
14
|
-
migrations.RunPython.noop, reverse_code=migrations.RunPython.noop
|
|
15
|
-
)
|
|
16
|
-
]
|
|
12
|
+
operations = [migrations.RunPython(migrations.RunPython.noop, reverse_code=migrations.RunPython.noop)]
|
|
@@ -8,8 +8,4 @@ class Migration(migrations.Migration):
|
|
|
8
8
|
("common", "0007_add_pdf_names_to_first_two_worksheets"),
|
|
9
9
|
]
|
|
10
10
|
|
|
11
|
-
operations = [
|
|
12
|
-
migrations.RunPython(
|
|
13
|
-
migrations.RunPython.noop, reverse_code=migrations.RunPython.noop
|
|
14
|
-
)
|
|
15
|
-
]
|
|
11
|
+
operations = [migrations.RunPython(migrations.RunPython.noop, reverse_code=migrations.RunPython.noop)]
|
|
@@ -3,9 +3,7 @@ from django.db import migrations
|
|
|
3
3
|
|
|
4
4
|
def copy_email_to_username(apps, schema):
|
|
5
5
|
Student = apps.get_model("common", "Student")
|
|
6
|
-
independent_students = Student.objects.filter(
|
|
7
|
-
class_field__isnull=True, new_user__is_active=True
|
|
8
|
-
)
|
|
6
|
+
independent_students = Student.objects.filter(class_field__isnull=True, new_user__is_active=True)
|
|
9
7
|
for student in independent_students:
|
|
10
8
|
student.new_user.username = student.new_user.email
|
|
11
9
|
student.new_user.save()
|
|
@@ -17,8 +15,4 @@ class Migration(migrations.Migration):
|
|
|
17
15
|
("common", "0016_joinreleasestudent"),
|
|
18
16
|
]
|
|
19
17
|
|
|
20
|
-
operations = [
|
|
21
|
-
migrations.RunPython(
|
|
22
|
-
code=copy_email_to_username, reverse_code=migrations.RunPython.noop
|
|
23
|
-
)
|
|
24
|
-
]
|
|
18
|
+
operations = [migrations.RunPython(code=copy_email_to_username, reverse_code=migrations.RunPython.noop)]
|
|
@@ -6,23 +6,23 @@ from django.db import migrations, models
|
|
|
6
6
|
class Migration(migrations.Migration):
|
|
7
7
|
|
|
8
8
|
dependencies = [
|
|
9
|
-
(
|
|
9
|
+
("common", "0020_class_is_active_and_null_access_code"),
|
|
10
10
|
]
|
|
11
11
|
|
|
12
12
|
operations = [
|
|
13
13
|
migrations.AddField(
|
|
14
|
-
model_name=
|
|
15
|
-
name=
|
|
14
|
+
model_name="school",
|
|
15
|
+
name="is_active",
|
|
16
16
|
field=models.BooleanField(default=True),
|
|
17
17
|
),
|
|
18
18
|
migrations.AlterField(
|
|
19
|
-
model_name=
|
|
20
|
-
name=
|
|
19
|
+
model_name="school",
|
|
20
|
+
name="postcode",
|
|
21
21
|
field=models.CharField(max_length=10, null=True),
|
|
22
22
|
),
|
|
23
23
|
migrations.AlterField(
|
|
24
|
-
model_name=
|
|
25
|
-
name=
|
|
24
|
+
model_name="school",
|
|
25
|
+
name="town",
|
|
26
26
|
field=models.CharField(max_length=200, null=True),
|
|
27
27
|
),
|
|
28
28
|
]
|
|
@@ -6,24 +6,24 @@ from django.db import migrations
|
|
|
6
6
|
class Migration(migrations.Migration):
|
|
7
7
|
|
|
8
8
|
dependencies = [
|
|
9
|
-
(
|
|
9
|
+
("common", "0021_school_is_active"),
|
|
10
10
|
]
|
|
11
11
|
|
|
12
12
|
operations = [
|
|
13
13
|
migrations.RemoveField(
|
|
14
|
-
model_name=
|
|
15
|
-
name=
|
|
14
|
+
model_name="school",
|
|
15
|
+
name="latitude",
|
|
16
16
|
),
|
|
17
17
|
migrations.RemoveField(
|
|
18
|
-
model_name=
|
|
19
|
-
name=
|
|
18
|
+
model_name="school",
|
|
19
|
+
name="longitude",
|
|
20
20
|
),
|
|
21
21
|
migrations.RemoveField(
|
|
22
|
-
model_name=
|
|
23
|
-
name=
|
|
22
|
+
model_name="school",
|
|
23
|
+
name="town",
|
|
24
24
|
),
|
|
25
25
|
migrations.RemoveField(
|
|
26
|
-
model_name=
|
|
27
|
-
name=
|
|
26
|
+
model_name="userprofile",
|
|
27
|
+
name="can_view_aggregated_data",
|
|
28
28
|
),
|
|
29
29
|
]
|
|
@@ -6,17 +6,17 @@ from django.db import migrations, models
|
|
|
6
6
|
class Migration(migrations.Migration):
|
|
7
7
|
|
|
8
8
|
dependencies = [
|
|
9
|
-
(
|
|
9
|
+
("common", "0022_school_cleanup"),
|
|
10
10
|
]
|
|
11
11
|
|
|
12
12
|
operations = [
|
|
13
13
|
migrations.AlterModelOptions(
|
|
14
|
-
name=
|
|
14
|
+
name="school",
|
|
15
15
|
options={},
|
|
16
16
|
),
|
|
17
17
|
migrations.AddField(
|
|
18
|
-
model_name=
|
|
19
|
-
name=
|
|
18
|
+
model_name="userprofile",
|
|
19
|
+
name="aimmo_badges",
|
|
20
20
|
field=models.CharField(blank=True, max_length=200, null=True),
|
|
21
21
|
),
|
|
22
22
|
]
|
|
@@ -8,24 +8,40 @@ import django.utils.timezone
|
|
|
8
8
|
class Migration(migrations.Migration):
|
|
9
9
|
|
|
10
10
|
dependencies = [
|
|
11
|
-
(
|
|
11
|
+
("common", "0024_teacher_invited_by"),
|
|
12
12
|
]
|
|
13
13
|
|
|
14
14
|
operations = [
|
|
15
15
|
migrations.CreateModel(
|
|
16
|
-
name=
|
|
16
|
+
name="SchoolTeacherInvitation",
|
|
17
17
|
fields=[
|
|
18
|
-
(
|
|
19
|
-
(
|
|
20
|
-
(
|
|
21
|
-
(
|
|
22
|
-
(
|
|
23
|
-
(
|
|
24
|
-
(
|
|
25
|
-
(
|
|
26
|
-
(
|
|
27
|
-
(
|
|
28
|
-
|
|
18
|
+
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
|
19
|
+
("token", models.CharField(max_length=32)),
|
|
20
|
+
("invited_teacher_first_name", models.CharField(max_length=150)),
|
|
21
|
+
("invited_teacher_last_name", models.CharField(max_length=150)),
|
|
22
|
+
("invited_teacher_email", models.EmailField(max_length=254)),
|
|
23
|
+
("invited_teacher_is_admin", models.BooleanField(default=False)),
|
|
24
|
+
("expiry", models.DateTimeField()),
|
|
25
|
+
("creation_time", models.DateTimeField(default=django.utils.timezone.now, null=True)),
|
|
26
|
+
("is_active", models.BooleanField(default=True)),
|
|
27
|
+
(
|
|
28
|
+
"from_teacher",
|
|
29
|
+
models.ForeignKey(
|
|
30
|
+
null=True,
|
|
31
|
+
on_delete=django.db.models.deletion.SET_NULL,
|
|
32
|
+
related_name="school_invitations",
|
|
33
|
+
to="common.teacher",
|
|
34
|
+
),
|
|
35
|
+
),
|
|
36
|
+
(
|
|
37
|
+
"school",
|
|
38
|
+
models.ForeignKey(
|
|
39
|
+
null=True,
|
|
40
|
+
on_delete=django.db.models.deletion.SET_NULL,
|
|
41
|
+
related_name="teacher_invitations",
|
|
42
|
+
to="common.school",
|
|
43
|
+
),
|
|
44
|
+
),
|
|
29
45
|
],
|
|
30
46
|
),
|
|
31
47
|
]
|
|
@@ -6,17 +6,17 @@ from django.db import migrations, models
|
|
|
6
6
|
class Migration(migrations.Migration):
|
|
7
7
|
|
|
8
8
|
dependencies = [
|
|
9
|
-
(
|
|
9
|
+
("common", "0025_schoolteacherinvitation"),
|
|
10
10
|
]
|
|
11
11
|
|
|
12
12
|
operations = [
|
|
13
13
|
migrations.RemoveField(
|
|
14
|
-
model_name=
|
|
15
|
-
name=
|
|
14
|
+
model_name="teacher",
|
|
15
|
+
name="pending_join_request",
|
|
16
16
|
),
|
|
17
17
|
migrations.AlterField(
|
|
18
|
-
model_name=
|
|
19
|
-
name=
|
|
18
|
+
model_name="teacher",
|
|
19
|
+
name="blocked_time",
|
|
20
20
|
field=models.DateTimeField(blank=True, null=True),
|
|
21
21
|
),
|
|
22
22
|
]
|
|
@@ -7,13 +7,19 @@ import django.db.models.deletion
|
|
|
7
7
|
class Migration(migrations.Migration):
|
|
8
8
|
|
|
9
9
|
dependencies = [
|
|
10
|
-
(
|
|
10
|
+
("common", "0026_teacher_remove_join_request"),
|
|
11
11
|
]
|
|
12
12
|
|
|
13
13
|
operations = [
|
|
14
14
|
migrations.AddField(
|
|
15
|
-
model_name=
|
|
16
|
-
name=
|
|
17
|
-
field=models.ForeignKey(
|
|
15
|
+
model_name="class",
|
|
16
|
+
name="created_by",
|
|
17
|
+
field=models.ForeignKey(
|
|
18
|
+
blank=True,
|
|
19
|
+
null=True,
|
|
20
|
+
on_delete=django.db.models.deletion.SET_NULL,
|
|
21
|
+
related_name="created_classes",
|
|
22
|
+
to="common.teacher",
|
|
23
|
+
),
|
|
18
24
|
),
|
|
19
25
|
]
|
|
@@ -6,18 +6,18 @@ from django.db import migrations, models
|
|
|
6
6
|
class Migration(migrations.Migration):
|
|
7
7
|
|
|
8
8
|
dependencies = [
|
|
9
|
-
(
|
|
9
|
+
("common", "0027_class_created_by"),
|
|
10
10
|
]
|
|
11
11
|
|
|
12
12
|
operations = [
|
|
13
13
|
migrations.AddField(
|
|
14
|
-
model_name=
|
|
15
|
-
name=
|
|
14
|
+
model_name="dailyactivity",
|
|
15
|
+
name="primary_coding_club_downloads",
|
|
16
16
|
field=models.PositiveIntegerField(default=0),
|
|
17
17
|
),
|
|
18
18
|
migrations.AddField(
|
|
19
|
-
model_name=
|
|
20
|
-
name=
|
|
19
|
+
model_name="dailyactivity",
|
|
20
|
+
name="python_coding_club_downloads",
|
|
21
21
|
field=models.PositiveIntegerField(default=0),
|
|
22
22
|
),
|
|
23
23
|
]
|
|
@@ -6,17 +6,17 @@ from django.db import migrations, models
|
|
|
6
6
|
class Migration(migrations.Migration):
|
|
7
7
|
|
|
8
8
|
dependencies = [
|
|
9
|
-
(
|
|
9
|
+
("common", "0028_coding_club_downloads"),
|
|
10
10
|
]
|
|
11
11
|
|
|
12
12
|
operations = [
|
|
13
13
|
migrations.CreateModel(
|
|
14
|
-
name=
|
|
14
|
+
name="DynamicElement",
|
|
15
15
|
fields=[
|
|
16
|
-
(
|
|
17
|
-
(
|
|
18
|
-
(
|
|
19
|
-
(
|
|
16
|
+
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
|
17
|
+
("name", models.CharField(max_length=64, unique=True, editable=False)),
|
|
18
|
+
("active", models.BooleanField(default=False)),
|
|
19
|
+
("text", models.TextField(blank=True, null=True)),
|
|
20
20
|
],
|
|
21
21
|
),
|
|
22
22
|
]
|