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.

Files changed (52) hide show
  1. cfl_common/common/app_settings.py +3 -0
  2. cfl_common/common/email_messages.py +0 -58
  3. cfl_common/common/helpers/data_migration_loader.py +3 -4
  4. cfl_common/common/helpers/emails.py +27 -45
  5. cfl_common/common/helpers/generators.py +1 -1
  6. cfl_common/common/mail.py +116 -0
  7. cfl_common/common/migrations/0002_emailverification.py +1 -3
  8. cfl_common/common/migrations/0005_add_worksheets.py +1 -5
  9. cfl_common/common/migrations/0007_add_pdf_names_to_first_two_worksheets.py +1 -5
  10. cfl_common/common/migrations/0008_unlock_worksheet_3.py +1 -5
  11. cfl_common/common/migrations/0017_copy_email_to_username.py +2 -8
  12. cfl_common/common/migrations/0021_school_is_active.py +7 -7
  13. cfl_common/common/migrations/0022_school_cleanup.py +9 -9
  14. cfl_common/common/migrations/0023_userprofile_aimmo_badges.py +4 -4
  15. cfl_common/common/migrations/0025_schoolteacherinvitation.py +29 -13
  16. cfl_common/common/migrations/0026_teacher_remove_join_request.py +5 -5
  17. cfl_common/common/migrations/0027_class_created_by.py +10 -4
  18. cfl_common/common/migrations/0028_coding_club_downloads.py +5 -5
  19. cfl_common/common/migrations/0029_dynamicelement.py +6 -6
  20. cfl_common/common/migrations/0030_add_maintenance_banner.py +1 -3
  21. cfl_common/common/migrations/0031_improve_admin_panel.py +32 -14
  22. cfl_common/common/migrations/0032_dailyactivity_level_control_submits.py +3 -3
  23. cfl_common/common/migrations/0033_password_reset_tracking_fields.py +5 -5
  24. cfl_common/common/migrations/0034_dailyactivity_daily_school_student_lockout_reset.py +3 -3
  25. cfl_common/common/migrations/0035_rename_lockout_fields.py +10 -10
  26. cfl_common/common/migrations/0037_migrate_email_verification.py +2 -2
  27. cfl_common/common/migrations/0038_delete_emailverification.py +2 -2
  28. cfl_common/common/migrations/0039_copy_email_to_username.py +1 -6
  29. cfl_common/common/migrations/0040_school_county.py +3 -3
  30. cfl_common/common/migrations/0042_totalactivity.py +7 -7
  31. cfl_common/common/migrations/0044_update_activity_models.py +9 -9
  32. cfl_common/common/migrations/0045_otp.py +5 -5
  33. cfl_common/common/migrations/0046_alter_school_country.py +3 -3
  34. cfl_common/common/tests/utils/email.py +14 -34
  35. cfl_common/common/tests/utils/student.py +8 -8
  36. cfl_common/common/tests/utils/teacher.py +8 -8
  37. {codeforlife_portal-6.41.4.dist-info → codeforlife_portal-6.41.6.dist-info}/METADATA +2 -2
  38. {codeforlife_portal-6.41.4.dist-info → codeforlife_portal-6.41.6.dist-info}/RECORD +52 -51
  39. example_project/portal_test_settings.py +5 -1
  40. portal/__init__.py +1 -1
  41. portal/static/portal/img/logo_cfl.png +0 -0
  42. portal/static/portal/img/logo_cfl_reminder_cards.jpg +0 -0
  43. portal/tests/test_independent_student.py +30 -17
  44. portal/tests/test_ratelimit.py +15 -12
  45. portal/tests/test_teacher.py +35 -21
  46. portal/tests/test_teacher_student.py +13 -3
  47. portal/tests/test_views.py +55 -194
  48. portal/views/cron/user.py +12 -49
  49. portal/views/teacher/teach.py +6 -3
  50. {codeforlife_portal-6.41.4.dist-info → codeforlife_portal-6.41.6.dist-info}/LICENSE.md +0 -0
  51. {codeforlife_portal-6.41.4.dist-info → codeforlife_portal-6.41.6.dist-info}/WHEEL +0 -0
  52. {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. The file must be in the fixtures directory.
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.email_messages import (
11
- emailChangeNotificationEmail,
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 post, get, put, delete
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
- message = emailVerificationNeededEmail(request, verification)
127
- send_email(
128
- VERIFICATION_EMAIL,
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
- message = parentsEmailVerificationNeededEmail(request, user, verification)
142
- send_email(
143
- VERIFICATION_EMAIL,
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
- message["subject"],
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
- message = emailVerificationNeededEmail(request, verification)
152
- send_email(
153
- VERIFICATION_EMAIL,
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
- message = emailChangeVerificationEmail(request, verification)
168
- send_email(
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
- message = emailChangeNotificationEmail(request, new_email)
285
- send_email(VERIFICATION_EMAIL, [user.email], message["subject"], message["message"], message["subject"])
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
- message = emailChangeNotificationEmail(request, new_email)
303
- send_email(
304
- VERIFICATION_EMAIL, [user.new_user.email], message["subject"], message["message"], message["subject"]
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
@@ -1,6 +1,6 @@
1
+ import hashlib
1
2
  import random
2
3
  import string
3
- import hashlib
4
4
  from builtins import range, str
5
5
  from uuid import uuid4
6
6
 
@@ -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
- ('common', '0020_class_is_active_and_null_access_code'),
9
+ ("common", "0020_class_is_active_and_null_access_code"),
10
10
  ]
11
11
 
12
12
  operations = [
13
13
  migrations.AddField(
14
- model_name='school',
15
- name='is_active',
14
+ model_name="school",
15
+ name="is_active",
16
16
  field=models.BooleanField(default=True),
17
17
  ),
18
18
  migrations.AlterField(
19
- model_name='school',
20
- name='postcode',
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='school',
25
- name='town',
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
- ('common', '0021_school_is_active'),
9
+ ("common", "0021_school_is_active"),
10
10
  ]
11
11
 
12
12
  operations = [
13
13
  migrations.RemoveField(
14
- model_name='school',
15
- name='latitude',
14
+ model_name="school",
15
+ name="latitude",
16
16
  ),
17
17
  migrations.RemoveField(
18
- model_name='school',
19
- name='longitude',
18
+ model_name="school",
19
+ name="longitude",
20
20
  ),
21
21
  migrations.RemoveField(
22
- model_name='school',
23
- name='town',
22
+ model_name="school",
23
+ name="town",
24
24
  ),
25
25
  migrations.RemoveField(
26
- model_name='userprofile',
27
- name='can_view_aggregated_data',
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
- ('common', '0022_school_cleanup'),
9
+ ("common", "0022_school_cleanup"),
10
10
  ]
11
11
 
12
12
  operations = [
13
13
  migrations.AlterModelOptions(
14
- name='school',
14
+ name="school",
15
15
  options={},
16
16
  ),
17
17
  migrations.AddField(
18
- model_name='userprofile',
19
- name='aimmo_badges',
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
- ('common', '0024_teacher_invited_by'),
11
+ ("common", "0024_teacher_invited_by"),
12
12
  ]
13
13
 
14
14
  operations = [
15
15
  migrations.CreateModel(
16
- name='SchoolTeacherInvitation',
16
+ name="SchoolTeacherInvitation",
17
17
  fields=[
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
- ('from_teacher', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='school_invitations', to='common.teacher')),
28
- ('school', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='teacher_invitations', to='common.school')),
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
- ('common', '0025_schoolteacherinvitation'),
9
+ ("common", "0025_schoolteacherinvitation"),
10
10
  ]
11
11
 
12
12
  operations = [
13
13
  migrations.RemoveField(
14
- model_name='teacher',
15
- name='pending_join_request',
14
+ model_name="teacher",
15
+ name="pending_join_request",
16
16
  ),
17
17
  migrations.AlterField(
18
- model_name='teacher',
19
- name='blocked_time',
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
- ('common', '0026_teacher_remove_join_request'),
10
+ ("common", "0026_teacher_remove_join_request"),
11
11
  ]
12
12
 
13
13
  operations = [
14
14
  migrations.AddField(
15
- model_name='class',
16
- name='created_by',
17
- field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_classes', to='common.teacher'),
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
- ('common', '0027_class_created_by'),
9
+ ("common", "0027_class_created_by"),
10
10
  ]
11
11
 
12
12
  operations = [
13
13
  migrations.AddField(
14
- model_name='dailyactivity',
15
- name='primary_coding_club_downloads',
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='dailyactivity',
20
- name='python_coding_club_downloads',
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
- ('common', '0028_coding_club_downloads'),
9
+ ("common", "0028_coding_club_downloads"),
10
10
  ]
11
11
 
12
12
  operations = [
13
13
  migrations.CreateModel(
14
- name='DynamicElement',
14
+ name="DynamicElement",
15
15
  fields=[
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)),
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
  ]