codeforlife 0.22.10__tar.gz

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.
Files changed (154) hide show
  1. codeforlife-0.22.10/LICENSE.md +3 -0
  2. codeforlife-0.22.10/PKG-INFO +192 -0
  3. codeforlife-0.22.10/README.md +75 -0
  4. codeforlife-0.22.10/codeforlife/__init__.py +12 -0
  5. codeforlife-0.22.10/codeforlife/_test.py +10 -0
  6. codeforlife-0.22.10/codeforlife/app.py +44 -0
  7. codeforlife-0.22.10/codeforlife/commands/__init__.py +7 -0
  8. codeforlife-0.22.10/codeforlife/commands/load_fixtures.py +40 -0
  9. codeforlife-0.22.10/codeforlife/commands/summarize_fixtures.py +86 -0
  10. codeforlife-0.22.10/codeforlife/data/.gitkeep +0 -0
  11. codeforlife-0.22.10/codeforlife/filters.py +33 -0
  12. codeforlife-0.22.10/codeforlife/forms.py +81 -0
  13. codeforlife-0.22.10/codeforlife/mail.py +312 -0
  14. codeforlife-0.22.10/codeforlife/middlewares/__init__.py +6 -0
  15. codeforlife-0.22.10/codeforlife/middlewares/session.py +34 -0
  16. codeforlife-0.22.10/codeforlife/mixins/__init__.py +6 -0
  17. codeforlife-0.22.10/codeforlife/mixins/cron.py +24 -0
  18. codeforlife-0.22.10/codeforlife/models/__init__.py +9 -0
  19. codeforlife-0.22.10/codeforlife/models/abstract_base_session.py +78 -0
  20. codeforlife-0.22.10/codeforlife/models/abstract_base_user.py +58 -0
  21. codeforlife-0.22.10/codeforlife/models/base.py +28 -0
  22. codeforlife-0.22.10/codeforlife/models/base_session_store.py +91 -0
  23. codeforlife-0.22.10/codeforlife/models/signals/__init__.py +10 -0
  24. codeforlife-0.22.10/codeforlife/models/signals/general.py +24 -0
  25. codeforlife-0.22.10/codeforlife/models/signals/post_save.py +91 -0
  26. codeforlife-0.22.10/codeforlife/models/signals/pre_save.py +113 -0
  27. codeforlife-0.22.10/codeforlife/models/signals/receiver.py +63 -0
  28. codeforlife-0.22.10/codeforlife/pagination.py +30 -0
  29. codeforlife-0.22.10/codeforlife/permissions/__init__.py +13 -0
  30. codeforlife-0.22.10/codeforlife/permissions/allow_any.py +12 -0
  31. codeforlife-0.22.10/codeforlife/permissions/allow_none.py +18 -0
  32. codeforlife-0.22.10/codeforlife/permissions/base.py +13 -0
  33. codeforlife-0.22.10/codeforlife/permissions/is_authenticated.py +12 -0
  34. codeforlife-0.22.10/codeforlife/permissions/is_cron_request_from_google.py +22 -0
  35. codeforlife-0.22.10/codeforlife/permissions/operators.py +51 -0
  36. codeforlife-0.22.10/codeforlife/py.typed +1 -0
  37. codeforlife-0.22.10/codeforlife/request/__init__.py +8 -0
  38. codeforlife-0.22.10/codeforlife/request/drf.py +142 -0
  39. codeforlife-0.22.10/codeforlife/request/http.py +36 -0
  40. codeforlife-0.22.10/codeforlife/request/wsgi.py +36 -0
  41. codeforlife-0.22.10/codeforlife/response.py +30 -0
  42. codeforlife-0.22.10/codeforlife/serializers/__init__.py +8 -0
  43. codeforlife-0.22.10/codeforlife/serializers/base.py +34 -0
  44. codeforlife-0.22.10/codeforlife/serializers/model.py +74 -0
  45. codeforlife-0.22.10/codeforlife/serializers/model_list.py +211 -0
  46. codeforlife-0.22.10/codeforlife/settings/__init__.py +19 -0
  47. codeforlife-0.22.10/codeforlife/settings/custom.py +46 -0
  48. codeforlife-0.22.10/codeforlife/settings/django.py +233 -0
  49. codeforlife-0.22.10/codeforlife/settings/third_party.py +25 -0
  50. codeforlife-0.22.10/codeforlife/tests/__init__.py +23 -0
  51. codeforlife-0.22.10/codeforlife/tests/api.py +66 -0
  52. codeforlife-0.22.10/codeforlife/tests/api_client.py +549 -0
  53. codeforlife-0.22.10/codeforlife/tests/api_request_factory.py +263 -0
  54. codeforlife-0.22.10/codeforlife/tests/cron.py +20 -0
  55. codeforlife-0.22.10/codeforlife/tests/model.py +96 -0
  56. codeforlife-0.22.10/codeforlife/tests/model_list_serializer.py +88 -0
  57. codeforlife-0.22.10/codeforlife/tests/model_serializer.py +414 -0
  58. codeforlife-0.22.10/codeforlife/tests/model_view_set.py +295 -0
  59. codeforlife-0.22.10/codeforlife/tests/model_view_set_client.py +638 -0
  60. codeforlife-0.22.10/codeforlife/tests/test.py +67 -0
  61. codeforlife-0.22.10/codeforlife/types.py +32 -0
  62. codeforlife-0.22.10/codeforlife/urls/__init__.py +7 -0
  63. codeforlife-0.22.10/codeforlife/urls/handlers.py +23 -0
  64. codeforlife-0.22.10/codeforlife/urls/patterns.py +103 -0
  65. codeforlife-0.22.10/codeforlife/user/__init__.py +8 -0
  66. codeforlife-0.22.10/codeforlife/user/admin.py +60 -0
  67. codeforlife-0.22.10/codeforlife/user/apps.py +16 -0
  68. codeforlife-0.22.10/codeforlife/user/auth/__init__.py +0 -0
  69. codeforlife-0.22.10/codeforlife/user/auth/backends/__init__.py +11 -0
  70. codeforlife-0.22.10/codeforlife/user/auth/backends/base.py +20 -0
  71. codeforlife-0.22.10/codeforlife/user/auth/backends/email.py +34 -0
  72. codeforlife-0.22.10/codeforlife/user/auth/backends/otp.py +56 -0
  73. codeforlife-0.22.10/codeforlife/user/auth/backends/otp_bypass_token.py +43 -0
  74. codeforlife-0.22.10/codeforlife/user/auth/backends/otp_bypass_token_test.py +37 -0
  75. codeforlife-0.22.10/codeforlife/user/auth/backends/student.py +41 -0
  76. codeforlife-0.22.10/codeforlife/user/auth/backends/student_auto.py +50 -0
  77. codeforlife-0.22.10/codeforlife/user/auth/password_validators/__init__.py +8 -0
  78. codeforlife-0.22.10/codeforlife/user/auth/password_validators/base.py +17 -0
  79. codeforlife-0.22.10/codeforlife/user/auth/password_validators/common.py +4 -0
  80. codeforlife-0.22.10/codeforlife/user/auth/password_validators/independent.py +48 -0
  81. codeforlife-0.22.10/codeforlife/user/auth/password_validators/independent_test.py +49 -0
  82. codeforlife-0.22.10/codeforlife/user/auth/password_validators/student.py +27 -0
  83. codeforlife-0.22.10/codeforlife/user/auth/password_validators/student_test.py +27 -0
  84. codeforlife-0.22.10/codeforlife/user/auth/password_validators/teacher.py +54 -0
  85. codeforlife-0.22.10/codeforlife/user/auth/password_validators/teacher_test.py +54 -0
  86. codeforlife-0.22.10/codeforlife/user/filters/__init__.py +7 -0
  87. codeforlife-0.22.10/codeforlife/user/filters/klass.py +31 -0
  88. codeforlife-0.22.10/codeforlife/user/filters/user.py +76 -0
  89. codeforlife-0.22.10/codeforlife/user/fixtures/independent.json +57 -0
  90. codeforlife-0.22.10/codeforlife/user/fixtures/non_school_teacher.json +56 -0
  91. codeforlife-0.22.10/codeforlife/user/fixtures/school_1.json +149 -0
  92. codeforlife-0.22.10/codeforlife/user/fixtures/school_2.json +184 -0
  93. codeforlife-0.22.10/codeforlife/user/fixtures/school_2_sessions.json +19 -0
  94. codeforlife-0.22.10/codeforlife/user/fixtures/school_3.json +68 -0
  95. codeforlife-0.22.10/codeforlife/user/fixtures/sites.json +18 -0
  96. codeforlife-0.22.10/codeforlife/user/management/__init__.py +4 -0
  97. codeforlife-0.22.10/codeforlife/user/management/commands/__init__.py +4 -0
  98. codeforlife-0.22.10/codeforlife/user/management/commands/load_fixtures.py +11 -0
  99. codeforlife-0.22.10/codeforlife/user/management/commands/summarize_fixtures.py +11 -0
  100. codeforlife-0.22.10/codeforlife/user/migrations/0001_initial.py +248 -0
  101. codeforlife-0.22.10/codeforlife/user/migrations/__init__.py +0 -0
  102. codeforlife-0.22.10/codeforlife/user/models/__init__.py +38 -0
  103. codeforlife-0.22.10/codeforlife/user/models/auth_factor.py +40 -0
  104. codeforlife-0.22.10/codeforlife/user/models/auth_factor_test.py +15 -0
  105. codeforlife-0.22.10/codeforlife/user/models/klass.py +7 -0
  106. codeforlife-0.22.10/codeforlife/user/models/otp_bypass_token.py +100 -0
  107. codeforlife-0.22.10/codeforlife/user/models/otp_bypass_token_test.py +58 -0
  108. codeforlife-0.22.10/codeforlife/user/models/school.py +7 -0
  109. codeforlife-0.22.10/codeforlife/user/models/session.py +51 -0
  110. codeforlife-0.22.10/codeforlife/user/models/session_auth_factor.py +31 -0
  111. codeforlife-0.22.10/codeforlife/user/models/session_auth_factor_test.py +18 -0
  112. codeforlife-0.22.10/codeforlife/user/models/session_test.py +29 -0
  113. codeforlife-0.22.10/codeforlife/user/models/student.py +34 -0
  114. codeforlife-0.22.10/codeforlife/user/models/student_test.py +16 -0
  115. codeforlife-0.22.10/codeforlife/user/models/teacher.py +206 -0
  116. codeforlife-0.22.10/codeforlife/user/models/user.py +630 -0
  117. codeforlife-0.22.10/codeforlife/user/permissions/__init__.py +8 -0
  118. codeforlife-0.22.10/codeforlife/user/permissions/is_independent.py +51 -0
  119. codeforlife-0.22.10/codeforlife/user/permissions/is_student.py +23 -0
  120. codeforlife-0.22.10/codeforlife/user/permissions/is_teacher.py +71 -0
  121. codeforlife-0.22.10/codeforlife/user/serializers/__init__.py +10 -0
  122. codeforlife-0.22.10/codeforlife/user/serializers/klass.py +50 -0
  123. codeforlife-0.22.10/codeforlife/user/serializers/school.py +31 -0
  124. codeforlife-0.22.10/codeforlife/user/serializers/student.py +28 -0
  125. codeforlife-0.22.10/codeforlife/user/serializers/teacher.py +30 -0
  126. codeforlife-0.22.10/codeforlife/user/serializers/user.py +103 -0
  127. codeforlife-0.22.10/codeforlife/user/serializers/user_test.py +71 -0
  128. codeforlife-0.22.10/codeforlife/user/signals/__init__.py +8 -0
  129. codeforlife-0.22.10/codeforlife/user/signals/teacher.py +9 -0
  130. codeforlife-0.22.10/codeforlife/user/signals/user.py +13 -0
  131. codeforlife-0.22.10/codeforlife/user/urls.py +15 -0
  132. codeforlife-0.22.10/codeforlife/user/views/__init__.py +8 -0
  133. codeforlife-0.22.10/codeforlife/user/views/klass.py +36 -0
  134. codeforlife-0.22.10/codeforlife/user/views/klass_test.py +135 -0
  135. codeforlife-0.22.10/codeforlife/user/views/school.py +46 -0
  136. codeforlife-0.22.10/codeforlife/user/views/school_test.py +93 -0
  137. codeforlife-0.22.10/codeforlife/user/views/user.py +79 -0
  138. codeforlife-0.22.10/codeforlife/user/views/user_test.py +248 -0
  139. codeforlife-0.22.10/codeforlife/version.py +8 -0
  140. codeforlife-0.22.10/codeforlife/views/__init__.py +11 -0
  141. codeforlife-0.22.10/codeforlife/views/api.py +63 -0
  142. codeforlife-0.22.10/codeforlife/views/base_login.py +106 -0
  143. codeforlife-0.22.10/codeforlife/views/common.py +35 -0
  144. codeforlife-0.22.10/codeforlife/views/decorators.py +70 -0
  145. codeforlife-0.22.10/codeforlife/views/health_check.py +126 -0
  146. codeforlife-0.22.10/codeforlife/views/model.py +396 -0
  147. codeforlife-0.22.10/codeforlife.egg-info/PKG-INFO +192 -0
  148. codeforlife-0.22.10/codeforlife.egg-info/SOURCES.txt +152 -0
  149. codeforlife-0.22.10/codeforlife.egg-info/dependency_links.txt +1 -0
  150. codeforlife-0.22.10/codeforlife.egg-info/requires.txt +153 -0
  151. codeforlife-0.22.10/codeforlife.egg-info/top_level.txt +1 -0
  152. codeforlife-0.22.10/pyproject.toml +47 -0
  153. codeforlife-0.22.10/setup.cfg +4 -0
  154. codeforlife-0.22.10/setup.py +100 -0
@@ -0,0 +1,3 @@
1
+ # License
2
+
3
+ Find our license [here](https://github.com/ocadotechnology/codeforlife-workspace/blob/main/LICENSE.md).
@@ -0,0 +1,192 @@
1
+ Metadata-Version: 2.1
2
+ Name: codeforlife
3
+ Version: 0.22.10
4
+ Summary: Code for Life's common code.
5
+ Home-page: https://github.com/ocadotechnology/codeforlife-package-python
6
+ Author: Ocado
7
+ Author-email: code-for-life-full-time-xd@ocado.com
8
+ Requires-Python: ==3.12.*
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE.md
11
+ Requires-Dist: asgiref==3.8.1; python_version >= "3.8"
12
+ Requires-Dist: certifi==2024.8.30; python_version >= "3.6"
13
+ Requires-Dist: cfl-common==7.3.5
14
+ Requires-Dist: charset-normalizer==3.4.0; python_full_version >= "3.7.0"
15
+ Requires-Dist: click==8.1.7; python_version >= "3.7"
16
+ Requires-Dist: codeforlife-portal==7.3.5
17
+ Requires-Dist: diff-match-patch==20241021; python_version >= "3.7"
18
+ Requires-Dist: django==3.2.25; python_version >= "3.6"
19
+ Requires-Dist: django-classy-tags==2.0.0
20
+ Requires-Dist: django-cors-headers==4.1.0; python_version >= "3.7"
21
+ Requires-Dist: django-countries==7.3.1
22
+ Requires-Dist: django-csp==3.7
23
+ Requires-Dist: django-filter==23.2; python_version >= "3.7"
24
+ Requires-Dist: django-formtools==2.2
25
+ Requires-Dist: django-import-export==4.0.3; python_version >= "3.8"
26
+ Requires-Dist: django-otp==1.0.2
27
+ Requires-Dist: django-phonenumber-field==6.4.0; python_version >= "3.7"
28
+ Requires-Dist: django-pipeline==2.0.8
29
+ Requires-Dist: django-preventconcurrentlogins==0.8.2
30
+ Requires-Dist: django-ratelimit==3.0.1; python_version >= "3.4"
31
+ Requires-Dist: django-recaptcha==2.0.6
32
+ Requires-Dist: django-reverse-js==0.1.7; python_version >= "3.10"
33
+ Requires-Dist: django-sekizai==2.0.0
34
+ Requires-Dist: django-treebeard==4.3.1
35
+ Requires-Dist: django-two-factor-auth==1.13.2
36
+ Requires-Dist: djangorestframework==3.13.1; python_version >= "3.6"
37
+ Requires-Dist: gunicorn==23.0.0; python_version >= "3.7"
38
+ Requires-Dist: h11==0.14.0; python_version >= "3.7"
39
+ Requires-Dist: idna==3.10; python_version >= "3.6"
40
+ Requires-Dist: importlib-metadata==4.13.0; python_version >= "3.7"
41
+ Requires-Dist: libsass==0.23.0; python_version >= "3.8"
42
+ Requires-Dist: more-itertools==8.7.0; python_version >= "3.5"
43
+ Requires-Dist: numpy==2.1.2; python_version >= "3.10"
44
+ Requires-Dist: packaging==24.1; python_version >= "3.8"
45
+ Requires-Dist: pandas==2.2.3; python_version >= "3.9"
46
+ Requires-Dist: pgeocode==0.4.0; python_version >= "3.8"
47
+ Requires-Dist: phonenumbers==8.12.12
48
+ Requires-Dist: pillow==11.0.0; python_version >= "3.9"
49
+ Requires-Dist: psycopg2-binary==2.9.9; python_version >= "3.7"
50
+ Requires-Dist: pyhamcrest==2.0.2; python_version >= "3.5"
51
+ Requires-Dist: pyjwt==2.6.0; python_version >= "3.7"
52
+ Requires-Dist: pyotp==2.9.0; python_version >= "3.7"
53
+ Requires-Dist: pypng==0.20220715.0
54
+ Requires-Dist: python-dateutil==2.9.0.post0; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2"
55
+ Requires-Dist: pytz==2024.2
56
+ Requires-Dist: pyyaml==6.0.2; python_version >= "3.8"
57
+ Requires-Dist: qrcode==7.4.2; python_version >= "3.7"
58
+ Requires-Dist: rapid-router==6.5.3
59
+ Requires-Dist: reportlab==3.6.13; python_version >= "3.7" and python_version < "4"
60
+ Requires-Dist: requests==2.32.2; python_version >= "3.8"
61
+ Requires-Dist: setuptools==74.0.0; python_version >= "3.8"
62
+ Requires-Dist: six==1.16.0; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2"
63
+ Requires-Dist: sqlparse==0.5.1; python_version >= "3.8"
64
+ Requires-Dist: tablib==3.5.0; python_version >= "3.8"
65
+ Requires-Dist: typing-extensions==4.12.2; python_version >= "3.8"
66
+ Requires-Dist: tzdata==2024.2; python_version >= "2"
67
+ Requires-Dist: urllib3==2.2.3; python_version >= "3.8"
68
+ Requires-Dist: uvicorn==0.32.0; python_version >= "3.8"
69
+ Requires-Dist: uvicorn-worker==0.2.0; python_version >= "3.8"
70
+ Requires-Dist: zipp==3.20.2; python_version >= "3.8"
71
+ Provides-Extra: dev
72
+ Requires-Dist: asgiref==3.8.1; python_version >= "3.8" and extra == "dev"
73
+ Requires-Dist: astroid==3.2.4; python_full_version >= "3.8.0" and extra == "dev"
74
+ Requires-Dist: black==24.8.0; python_version >= "3.8" and extra == "dev"
75
+ Requires-Dist: certifi==2024.8.30; python_version >= "3.6" and extra == "dev"
76
+ Requires-Dist: charset-normalizer==3.4.0; python_full_version >= "3.7.0" and extra == "dev"
77
+ Requires-Dist: click==8.1.7; python_version >= "3.7" and extra == "dev"
78
+ Requires-Dist: coverage[toml]==7.6.4; python_version >= "3.9" and extra == "dev"
79
+ Requires-Dist: dill==0.3.9; python_version >= "3.11" and extra == "dev"
80
+ Requires-Dist: django==3.2.25; python_version >= "3.6" and extra == "dev"
81
+ Requires-Dist: django-extensions==3.2.1; python_version >= "3.6" and extra == "dev"
82
+ Requires-Dist: django-stubs[compatible-mypy]==4.2.6; python_version >= "3.8" and extra == "dev"
83
+ Requires-Dist: django-stubs-ext==5.1.1; python_version >= "3.8" and extra == "dev"
84
+ Requires-Dist: django-test-migrations==1.2.0; (python_version >= "3.6" and python_version < "4.0") and extra == "dev"
85
+ Requires-Dist: djangorestframework-stubs[compatible-mypy]==3.14.4; python_version >= "3.8" and extra == "dev"
86
+ Requires-Dist: execnet==2.1.1; python_version >= "3.8" and extra == "dev"
87
+ Requires-Dist: idna==3.10; python_version >= "3.6" and extra == "dev"
88
+ Requires-Dist: iniconfig==2.0.0; python_version >= "3.7" and extra == "dev"
89
+ Requires-Dist: isort==5.13.2; python_full_version >= "3.8.0" and extra == "dev"
90
+ Requires-Dist: mccabe==0.7.0; python_version >= "3.6" and extra == "dev"
91
+ Requires-Dist: mypy==1.6.1; python_version >= "3.8" and extra == "dev"
92
+ Requires-Dist: mypy-extensions==1.0.0; python_version >= "3.5" and extra == "dev"
93
+ Requires-Dist: packaging==24.1; python_version >= "3.8" and extra == "dev"
94
+ Requires-Dist: pathspec==0.12.1; python_version >= "3.8" and extra == "dev"
95
+ Requires-Dist: platformdirs==4.3.6; python_version >= "3.8" and extra == "dev"
96
+ Requires-Dist: pluggy==1.5.0; python_version >= "3.8" and extra == "dev"
97
+ Requires-Dist: psutil==6.1.0; extra == "dev"
98
+ Requires-Dist: pydot==1.4.2; (python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3") and extra == "dev"
99
+ Requires-Dist: pylint==3.2.7; python_full_version >= "3.8.0" and extra == "dev"
100
+ Requires-Dist: pylint-django==2.5.5; (python_version >= "3.7" and python_version < "4.0") and extra == "dev"
101
+ Requires-Dist: pylint-plugin-utils==0.8.2; (python_version >= "3.7" and python_version < "4.0") and extra == "dev"
102
+ Requires-Dist: pyparsing==3.0.9; python_full_version >= "3.6.8" and extra == "dev"
103
+ Requires-Dist: pytest==8.3.3; python_version >= "3.8" and extra == "dev"
104
+ Requires-Dist: pytest-cov==5.0.0; python_version >= "3.8" and extra == "dev"
105
+ Requires-Dist: pytest-django==4.5.2; python_version >= "3.5" and extra == "dev"
106
+ Requires-Dist: pytest-env==0.8.1; python_version >= "3.7" and extra == "dev"
107
+ Requires-Dist: pytest-xdist[psutil]==3.5.0; python_version >= "3.7" and extra == "dev"
108
+ Requires-Dist: pytz==2024.2; extra == "dev"
109
+ Requires-Dist: requests==2.32.2; python_version >= "3.8" and extra == "dev"
110
+ Requires-Dist: sqlparse==0.5.1; python_version >= "3.8" and extra == "dev"
111
+ Requires-Dist: tomlkit==0.13.2; python_version >= "3.8" and extra == "dev"
112
+ Requires-Dist: types-pytz==2024.2.0.20241003; python_version >= "3.8" and extra == "dev"
113
+ Requires-Dist: types-pyyaml==6.0.12.20240917; python_version >= "3.8" and extra == "dev"
114
+ Requires-Dist: types-requests==2.32.0.20241016; python_version >= "3.8" and extra == "dev"
115
+ Requires-Dist: typing-extensions==4.12.2; python_version >= "3.8" and extra == "dev"
116
+ Requires-Dist: urllib3==2.2.3; python_version >= "3.8" and extra == "dev"
117
+
118
+ # codeforlife-package-python
119
+
120
+ This repo contains CFL's python package. This will be installed into all backend services.
121
+
122
+ ## LICENCE
123
+ In accordance with the [Terms of Use](https://www.codeforlife.education/terms#terms)
124
+ of the Code for Life website, all copyright, trademarks, and other
125
+ intellectual property rights in and relating to Code for Life (including all
126
+ content of the Code for Life website, the Rapid Router application, the
127
+ Kurono application, related software (including any drawn and/or animated
128
+ avatars, whether such avatars have any modifications) and any other games,
129
+ applications or any other content that we make available from time to time) are
130
+ owned by Ocado Innovation Limited.
131
+
132
+ The source code of the Code for Life portal, the Rapid Router application
133
+ and the Kurono/aimmo application are [licensed under the GNU Affero General
134
+ Public License](https://github.com/ocadotechnology/codeforlife-workspace/blob/main/LICENSE.md).
135
+ All other assets including images, logos, sounds etc., are not covered by
136
+ this licence and no-one may copy, modify, distribute, show in public or
137
+ create any derivative work from these assets.
138
+
139
+ ## Installation
140
+
141
+ To install this package, do one of the following options.
142
+
143
+ *Ensure you're installing the package with the required python version. See [setup.py](setup.py).*
144
+
145
+ *Remember to replace the version number ("0.0.0") with your [desired version](https://github.com/ocadotechnology/codeforlife-package-python/releases).*
146
+
147
+ **Option 1:** Run `pipenv install` command:
148
+
149
+ ```bash
150
+ pipenv install git+https://github.com/ocadotechnology/codeforlife-package-python.git@v0.0.0#egg=codeforlife
151
+ ```
152
+
153
+ **Option 2:** Add a row to `[packages]` in `Pipfile`:
154
+
155
+ ```toml
156
+ [packages]
157
+ codeforlife = {ref = "v0.0.0", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"}
158
+ ```
159
+
160
+ ## Making Changes
161
+
162
+ To make changes, you must:
163
+
164
+ 1. Branch off of main.
165
+ 1. Push your changes on your branch.
166
+ 1. Ensure the pipeline runs successfully on your branch.
167
+ 1. Have your changes reviewed and approved by a peer.
168
+ 1. Merge your branch into the `main` branch.
169
+ 1. [Manually trigger](https://github.com/ocadotechnology/codeforlife-package-python/actions/workflows/main.yml)
170
+ the `Main` pipeline for the `main` branch.
171
+
172
+ ### Installing your branch
173
+
174
+ You may wish to install and integrate your changes into a CFL backend before it's been peer-reviewed.
175
+
176
+ *Remember to replace the branch name ("my-branch") with your
177
+ [branch](https://github.com/ocadotechnology/codeforlife-package-python/branches)*.
178
+
179
+ ```toml
180
+ [packages]
181
+ codeforlife = {ref = "my-branch", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"}
182
+ ```
183
+
184
+ ## Version Release
185
+
186
+ New versions of this package are automatically created via a GitHub Actions [workflow](.github/workflows/python-package.yml). Versions are determined using the [semantic-release commit message format](https://semantic-release.gitbook.io/semantic-release/#commit-message-format).
187
+
188
+ A new package may only be released if:
189
+
190
+ 1. there are no formatting errors;
191
+ 1. all unit tests pass;
192
+ 1. (TODO) test/code coverage is acceptable.
@@ -0,0 +1,75 @@
1
+ # codeforlife-package-python
2
+
3
+ This repo contains CFL's python package. This will be installed into all backend services.
4
+
5
+ ## LICENCE
6
+ In accordance with the [Terms of Use](https://www.codeforlife.education/terms#terms)
7
+ of the Code for Life website, all copyright, trademarks, and other
8
+ intellectual property rights in and relating to Code for Life (including all
9
+ content of the Code for Life website, the Rapid Router application, the
10
+ Kurono application, related software (including any drawn and/or animated
11
+ avatars, whether such avatars have any modifications) and any other games,
12
+ applications or any other content that we make available from time to time) are
13
+ owned by Ocado Innovation Limited.
14
+
15
+ The source code of the Code for Life portal, the Rapid Router application
16
+ and the Kurono/aimmo application are [licensed under the GNU Affero General
17
+ Public License](https://github.com/ocadotechnology/codeforlife-workspace/blob/main/LICENSE.md).
18
+ All other assets including images, logos, sounds etc., are not covered by
19
+ this licence and no-one may copy, modify, distribute, show in public or
20
+ create any derivative work from these assets.
21
+
22
+ ## Installation
23
+
24
+ To install this package, do one of the following options.
25
+
26
+ *Ensure you're installing the package with the required python version. See [setup.py](setup.py).*
27
+
28
+ *Remember to replace the version number ("0.0.0") with your [desired version](https://github.com/ocadotechnology/codeforlife-package-python/releases).*
29
+
30
+ **Option 1:** Run `pipenv install` command:
31
+
32
+ ```bash
33
+ pipenv install git+https://github.com/ocadotechnology/codeforlife-package-python.git@v0.0.0#egg=codeforlife
34
+ ```
35
+
36
+ **Option 2:** Add a row to `[packages]` in `Pipfile`:
37
+
38
+ ```toml
39
+ [packages]
40
+ codeforlife = {ref = "v0.0.0", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"}
41
+ ```
42
+
43
+ ## Making Changes
44
+
45
+ To make changes, you must:
46
+
47
+ 1. Branch off of main.
48
+ 1. Push your changes on your branch.
49
+ 1. Ensure the pipeline runs successfully on your branch.
50
+ 1. Have your changes reviewed and approved by a peer.
51
+ 1. Merge your branch into the `main` branch.
52
+ 1. [Manually trigger](https://github.com/ocadotechnology/codeforlife-package-python/actions/workflows/main.yml)
53
+ the `Main` pipeline for the `main` branch.
54
+
55
+ ### Installing your branch
56
+
57
+ You may wish to install and integrate your changes into a CFL backend before it's been peer-reviewed.
58
+
59
+ *Remember to replace the branch name ("my-branch") with your
60
+ [branch](https://github.com/ocadotechnology/codeforlife-package-python/branches)*.
61
+
62
+ ```toml
63
+ [packages]
64
+ codeforlife = {ref = "my-branch", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"}
65
+ ```
66
+
67
+ ## Version Release
68
+
69
+ New versions of this package are automatically created via a GitHub Actions [workflow](.github/workflows/python-package.yml). Versions are determined using the [semantic-release commit message format](https://semantic-release.gitbook.io/semantic-release/#commit-message-format).
70
+
71
+ A new package may only be released if:
72
+
73
+ 1. there are no formatting errors;
74
+ 1. all unit tests pass;
75
+ 1. (TODO) test/code coverage is acceptable.
@@ -0,0 +1,12 @@
1
+ """
2
+ © Ocado Group
3
+ Created on 20/02/2024 at 09:28:27(+00:00).
4
+ """
5
+
6
+ from pathlib import Path
7
+
8
+ from .version import __version__
9
+
10
+ BASE_DIR = Path(__file__).resolve().parent
11
+ DATA_DIR = BASE_DIR.joinpath("data")
12
+ USER_DIR = BASE_DIR.joinpath("user")
@@ -0,0 +1,10 @@
1
+ """
2
+ © Ocado Group
3
+ Created on 12/04/2024 at 14:31:10(+01:00).
4
+ """
5
+
6
+
7
+ def test_import():
8
+ """Basic test to ensure importing the package does not raise an error."""
9
+ # pylint: disable-next=unused-import,import-outside-toplevel
10
+ import codeforlife
@@ -0,0 +1,44 @@
1
+ """
2
+ © Ocado Group
3
+ Created on 28/10/2024 at 16:19:47(+00:00).
4
+ """
5
+
6
+ import multiprocessing
7
+ import typing as t
8
+
9
+ from django.core.management import call_command
10
+ from gunicorn.app.base import BaseApplication # type: ignore[import-untyped]
11
+
12
+
13
+ # pylint: disable-next=abstract-method
14
+ class StandaloneApplication(BaseApplication):
15
+ """A server for an app in a live environment.
16
+
17
+ Based off of:
18
+ https://gist.github.com/Kludex/c98ed6b06f5c0f89fd78dd75ef58b424
19
+ https://docs.gunicorn.org/en/stable/custom.html
20
+ """
21
+
22
+ def __init__(self, app: t.Callable):
23
+ call_command("migrate", interactive=False)
24
+
25
+ self.options = {
26
+ "bind": "0.0.0.0:8080",
27
+ # https://docs.gunicorn.org/en/stable/design.html#how-many-workers
28
+ "workers": (multiprocessing.cpu_count() * 2) + 1,
29
+ "worker_class": "uvicorn.workers.UvicornWorker",
30
+ }
31
+ self.application = app
32
+ super().__init__()
33
+
34
+ def load_config(self):
35
+ config = {
36
+ key: value
37
+ for key, value in self.options.items()
38
+ if key in self.cfg.settings and value is not None
39
+ }
40
+ for key, value in config.items():
41
+ self.cfg.set(key.lower(), value)
42
+
43
+ def load(self):
44
+ return self.application
@@ -0,0 +1,7 @@
1
+ """
2
+ © Ocado Group
3
+ Created on 15/11/2024 at 12:18:24(+00:00).
4
+ """
5
+
6
+ from .load_fixtures import LoadFixtures
7
+ from .summarize_fixtures import SummarizeFixtures
@@ -0,0 +1,40 @@
1
+ """
2
+ © Ocado Group
3
+ Created on 10/06/2024 at 10:44:45(+01:00).
4
+ """
5
+
6
+ import os
7
+ import typing as t
8
+
9
+ from django.apps import apps
10
+ from django.core.management import call_command
11
+ from django.core.management.base import BaseCommand
12
+
13
+
14
+ # pylint: disable-next=missing-class-docstring
15
+ class LoadFixtures(BaseCommand):
16
+ help = "Loads all the fixtures of the specified apps."
17
+
18
+ required_app_labels: t.Set[str] = set()
19
+
20
+ def add_arguments(self, parser):
21
+ parser.add_argument("app_labels", nargs="*", type=str)
22
+
23
+ def handle(self, *args, **options):
24
+ fixture_labels: t.List[str] = []
25
+ for app_label in {*options["app_labels"], *self.required_app_labels}:
26
+ app_config = apps.app_configs[app_label]
27
+ fixtures_path = os.path.join(app_config.path, "fixtures")
28
+
29
+ self.stdout.write(f"{app_label} fixtures ({fixtures_path}):")
30
+ for fixture_label in os.listdir(fixtures_path):
31
+ if fixture_label in fixture_labels:
32
+ self.stderr.write(f"Duplicate fixture: {fixture_label}")
33
+ return
34
+
35
+ self.stdout.write(f" - {fixture_label}")
36
+ fixture_labels.append(fixture_label)
37
+
38
+ self.stdout.write()
39
+
40
+ call_command("loaddata", *fixture_labels)
@@ -0,0 +1,86 @@
1
+ """
2
+ © Ocado Group
3
+ Created on 22/02/2024 at 09:24:27(+00:00).
4
+ """
5
+
6
+ import json
7
+ import os
8
+ import typing as t
9
+ from dataclasses import dataclass
10
+ from itertools import groupby
11
+
12
+ from django.apps import apps
13
+ from django.core.management.base import BaseCommand
14
+
15
+ from ..types import JsonDict
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class Fixture:
20
+ """A data model fixture."""
21
+
22
+ model: str
23
+ pk: t.Any
24
+ fields: JsonDict
25
+
26
+
27
+ FixtureDict = t.Dict[str, t.List[Fixture]]
28
+
29
+
30
+ # pylint: disable-next=missing-class-docstring
31
+ class SummarizeFixtures(BaseCommand):
32
+ help = "Summarizes all the listed fixtures."
33
+
34
+ required_app_labels: t.Set[str] = set()
35
+
36
+ def add_arguments(self, parser):
37
+ parser.add_argument("app_labels", nargs="*", type=str)
38
+
39
+ def _write_pks_per_model(self, fixtures: t.List[Fixture], indents: int = 0):
40
+ def get_model(fixture: Fixture):
41
+ return fixture.model.lower()
42
+
43
+ fixtures.sort(key=get_model)
44
+
45
+ self.stdout.write(f'{" " * indents}Primary keys per model:')
46
+
47
+ for model, group in groupby(fixtures, key=get_model):
48
+ pks = [fixture.pk for fixture in group]
49
+ pks.sort()
50
+
51
+ self.stdout.write(f'{" " * (indents + 1)}- {model}: {pks}')
52
+
53
+ def write_pks_per_model(self, fixtures: FixtureDict):
54
+ """Write all the sorted primary keys per model."""
55
+ self._write_pks_per_model(
56
+ [
57
+ fixture
58
+ for file_fixtures in fixtures.values()
59
+ for fixture in file_fixtures
60
+ ]
61
+ )
62
+
63
+ def write_pks_per_file(self, fixtures: FixtureDict):
64
+ """Write all the sorted primary keys per file, per model."""
65
+ self.stdout.write("Primary keys per file:")
66
+
67
+ for file, file_fixtures in fixtures.items():
68
+ self.stdout.write(f" - {file}")
69
+ self._write_pks_per_model(file_fixtures, indents=2)
70
+
71
+ def handle(self, *args, **options):
72
+ fixtures: FixtureDict = {}
73
+ for app_label in {*options["app_labels"], *self.required_app_labels}:
74
+ app_config = apps.app_configs[app_label]
75
+ fixtures_path = os.path.join(app_config.path, "fixtures")
76
+
77
+ for fixture_name in os.listdir(fixtures_path):
78
+ fixture_path = os.path.join(fixtures_path, fixture_name)
79
+ with open(fixture_path, "r", encoding="utf-8") as fixture:
80
+ fixtures[fixture_path] = [
81
+ Fixture(**fixture) for fixture in json.load(fixture)
82
+ ]
83
+
84
+ self.write_pks_per_model(fixtures)
85
+ self.stdout.write()
86
+ self.write_pks_per_file(fixtures)
File without changes
@@ -0,0 +1,33 @@
1
+ """
2
+ © Ocado Group
3
+ Created on 26/07/2024 at 11:26:14(+01:00).
4
+ """
5
+
6
+ from django.db.models.query import QuerySet
7
+
8
+ # pylint: disable-next=line-too-long
9
+ from django_filters.rest_framework import ( # type: ignore[import-untyped] # isort: skip
10
+ FilterSet as _FilterSet,
11
+ )
12
+
13
+
14
+ class FilterSet(_FilterSet):
15
+ """Base filter set all other filter sets must inherit."""
16
+
17
+ @staticmethod
18
+ def make_exclude_field_list_method(field: str):
19
+ """Make a class-method that excludes a list of values for a field.
20
+
21
+ Args:
22
+ field: The field to exclude a list of values for.
23
+
24
+ Returns:
25
+ A class-method.
26
+ """
27
+
28
+ def method(self: FilterSet, queryset: QuerySet, name: str, *args):
29
+ return queryset.exclude(
30
+ **{f"{field}__in": self.request.GET.getlist(name)}
31
+ )
32
+
33
+ return method
@@ -0,0 +1,81 @@
1
+ """
2
+ © Ocado Group
3
+ Created on 07/11/2024 at 15:08:33(+00:00).
4
+ """
5
+
6
+ import typing as t
7
+
8
+ from django import forms
9
+ from django.contrib.auth import authenticate
10
+ from django.core.exceptions import ValidationError
11
+ from django.core.handlers.wsgi import WSGIRequest
12
+
13
+ from .models import AbstractBaseUser
14
+ from .types import get_arg
15
+
16
+ AnyAbstractBaseUser = t.TypeVar("AnyAbstractBaseUser", bound=AbstractBaseUser)
17
+
18
+
19
+ class BaseLoginForm(forms.Form, t.Generic[AnyAbstractBaseUser]):
20
+ """Base login form that all other login forms must inherit."""
21
+
22
+ user: AnyAbstractBaseUser
23
+
24
+ @classmethod
25
+ def get_user_class(cls) -> t.Type[AnyAbstractBaseUser]:
26
+ """Get the user class."""
27
+ return get_arg(cls, 0)
28
+
29
+ def __init__(self, request: WSGIRequest, *args, **kwargs):
30
+ self.request = request
31
+ super().__init__(*args, **kwargs)
32
+
33
+ def clean(self):
34
+ """Authenticates a user.
35
+
36
+ Raises:
37
+ ValidationError: If there are form errors.
38
+ ValidationError: If the user's credentials were incorrect.
39
+ ValidationError: If the user's account is deactivated.
40
+
41
+ Returns:
42
+ The cleaned form data.
43
+ """
44
+
45
+ if self.errors:
46
+ raise ValidationError(
47
+ "Found form errors. Skipping authentication.",
48
+ code="form_errors",
49
+ )
50
+
51
+ user = authenticate(
52
+ self.request,
53
+ **{key: self.cleaned_data[key] for key in self.fields.keys()}
54
+ )
55
+ if user is None:
56
+ raise ValidationError(
57
+ self.get_invalid_login_error_message(),
58
+ code="invalid_login",
59
+ )
60
+ if not isinstance(user, self.get_user_class()):
61
+ raise ValidationError(
62
+ "Incorrect user class.",
63
+ code="incorrect_user_class",
64
+ )
65
+ if not user.is_active:
66
+ raise ValidationError(
67
+ "User is not active",
68
+ code="user_not_active",
69
+ )
70
+
71
+ self.user = user
72
+
73
+ return self.cleaned_data
74
+
75
+ def get_invalid_login_error_message(self) -> str:
76
+ """Returns the error message if the user failed to login.
77
+
78
+ Raises:
79
+ NotImplementedError: If message is not set.
80
+ """
81
+ raise NotImplementedError()