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.
- codeforlife-0.22.10/LICENSE.md +3 -0
- codeforlife-0.22.10/PKG-INFO +192 -0
- codeforlife-0.22.10/README.md +75 -0
- codeforlife-0.22.10/codeforlife/__init__.py +12 -0
- codeforlife-0.22.10/codeforlife/_test.py +10 -0
- codeforlife-0.22.10/codeforlife/app.py +44 -0
- codeforlife-0.22.10/codeforlife/commands/__init__.py +7 -0
- codeforlife-0.22.10/codeforlife/commands/load_fixtures.py +40 -0
- codeforlife-0.22.10/codeforlife/commands/summarize_fixtures.py +86 -0
- codeforlife-0.22.10/codeforlife/data/.gitkeep +0 -0
- codeforlife-0.22.10/codeforlife/filters.py +33 -0
- codeforlife-0.22.10/codeforlife/forms.py +81 -0
- codeforlife-0.22.10/codeforlife/mail.py +312 -0
- codeforlife-0.22.10/codeforlife/middlewares/__init__.py +6 -0
- codeforlife-0.22.10/codeforlife/middlewares/session.py +34 -0
- codeforlife-0.22.10/codeforlife/mixins/__init__.py +6 -0
- codeforlife-0.22.10/codeforlife/mixins/cron.py +24 -0
- codeforlife-0.22.10/codeforlife/models/__init__.py +9 -0
- codeforlife-0.22.10/codeforlife/models/abstract_base_session.py +78 -0
- codeforlife-0.22.10/codeforlife/models/abstract_base_user.py +58 -0
- codeforlife-0.22.10/codeforlife/models/base.py +28 -0
- codeforlife-0.22.10/codeforlife/models/base_session_store.py +91 -0
- codeforlife-0.22.10/codeforlife/models/signals/__init__.py +10 -0
- codeforlife-0.22.10/codeforlife/models/signals/general.py +24 -0
- codeforlife-0.22.10/codeforlife/models/signals/post_save.py +91 -0
- codeforlife-0.22.10/codeforlife/models/signals/pre_save.py +113 -0
- codeforlife-0.22.10/codeforlife/models/signals/receiver.py +63 -0
- codeforlife-0.22.10/codeforlife/pagination.py +30 -0
- codeforlife-0.22.10/codeforlife/permissions/__init__.py +13 -0
- codeforlife-0.22.10/codeforlife/permissions/allow_any.py +12 -0
- codeforlife-0.22.10/codeforlife/permissions/allow_none.py +18 -0
- codeforlife-0.22.10/codeforlife/permissions/base.py +13 -0
- codeforlife-0.22.10/codeforlife/permissions/is_authenticated.py +12 -0
- codeforlife-0.22.10/codeforlife/permissions/is_cron_request_from_google.py +22 -0
- codeforlife-0.22.10/codeforlife/permissions/operators.py +51 -0
- codeforlife-0.22.10/codeforlife/py.typed +1 -0
- codeforlife-0.22.10/codeforlife/request/__init__.py +8 -0
- codeforlife-0.22.10/codeforlife/request/drf.py +142 -0
- codeforlife-0.22.10/codeforlife/request/http.py +36 -0
- codeforlife-0.22.10/codeforlife/request/wsgi.py +36 -0
- codeforlife-0.22.10/codeforlife/response.py +30 -0
- codeforlife-0.22.10/codeforlife/serializers/__init__.py +8 -0
- codeforlife-0.22.10/codeforlife/serializers/base.py +34 -0
- codeforlife-0.22.10/codeforlife/serializers/model.py +74 -0
- codeforlife-0.22.10/codeforlife/serializers/model_list.py +211 -0
- codeforlife-0.22.10/codeforlife/settings/__init__.py +19 -0
- codeforlife-0.22.10/codeforlife/settings/custom.py +46 -0
- codeforlife-0.22.10/codeforlife/settings/django.py +233 -0
- codeforlife-0.22.10/codeforlife/settings/third_party.py +25 -0
- codeforlife-0.22.10/codeforlife/tests/__init__.py +23 -0
- codeforlife-0.22.10/codeforlife/tests/api.py +66 -0
- codeforlife-0.22.10/codeforlife/tests/api_client.py +549 -0
- codeforlife-0.22.10/codeforlife/tests/api_request_factory.py +263 -0
- codeforlife-0.22.10/codeforlife/tests/cron.py +20 -0
- codeforlife-0.22.10/codeforlife/tests/model.py +96 -0
- codeforlife-0.22.10/codeforlife/tests/model_list_serializer.py +88 -0
- codeforlife-0.22.10/codeforlife/tests/model_serializer.py +414 -0
- codeforlife-0.22.10/codeforlife/tests/model_view_set.py +295 -0
- codeforlife-0.22.10/codeforlife/tests/model_view_set_client.py +638 -0
- codeforlife-0.22.10/codeforlife/tests/test.py +67 -0
- codeforlife-0.22.10/codeforlife/types.py +32 -0
- codeforlife-0.22.10/codeforlife/urls/__init__.py +7 -0
- codeforlife-0.22.10/codeforlife/urls/handlers.py +23 -0
- codeforlife-0.22.10/codeforlife/urls/patterns.py +103 -0
- codeforlife-0.22.10/codeforlife/user/__init__.py +8 -0
- codeforlife-0.22.10/codeforlife/user/admin.py +60 -0
- codeforlife-0.22.10/codeforlife/user/apps.py +16 -0
- codeforlife-0.22.10/codeforlife/user/auth/__init__.py +0 -0
- codeforlife-0.22.10/codeforlife/user/auth/backends/__init__.py +11 -0
- codeforlife-0.22.10/codeforlife/user/auth/backends/base.py +20 -0
- codeforlife-0.22.10/codeforlife/user/auth/backends/email.py +34 -0
- codeforlife-0.22.10/codeforlife/user/auth/backends/otp.py +56 -0
- codeforlife-0.22.10/codeforlife/user/auth/backends/otp_bypass_token.py +43 -0
- codeforlife-0.22.10/codeforlife/user/auth/backends/otp_bypass_token_test.py +37 -0
- codeforlife-0.22.10/codeforlife/user/auth/backends/student.py +41 -0
- codeforlife-0.22.10/codeforlife/user/auth/backends/student_auto.py +50 -0
- codeforlife-0.22.10/codeforlife/user/auth/password_validators/__init__.py +8 -0
- codeforlife-0.22.10/codeforlife/user/auth/password_validators/base.py +17 -0
- codeforlife-0.22.10/codeforlife/user/auth/password_validators/common.py +4 -0
- codeforlife-0.22.10/codeforlife/user/auth/password_validators/independent.py +48 -0
- codeforlife-0.22.10/codeforlife/user/auth/password_validators/independent_test.py +49 -0
- codeforlife-0.22.10/codeforlife/user/auth/password_validators/student.py +27 -0
- codeforlife-0.22.10/codeforlife/user/auth/password_validators/student_test.py +27 -0
- codeforlife-0.22.10/codeforlife/user/auth/password_validators/teacher.py +54 -0
- codeforlife-0.22.10/codeforlife/user/auth/password_validators/teacher_test.py +54 -0
- codeforlife-0.22.10/codeforlife/user/filters/__init__.py +7 -0
- codeforlife-0.22.10/codeforlife/user/filters/klass.py +31 -0
- codeforlife-0.22.10/codeforlife/user/filters/user.py +76 -0
- codeforlife-0.22.10/codeforlife/user/fixtures/independent.json +57 -0
- codeforlife-0.22.10/codeforlife/user/fixtures/non_school_teacher.json +56 -0
- codeforlife-0.22.10/codeforlife/user/fixtures/school_1.json +149 -0
- codeforlife-0.22.10/codeforlife/user/fixtures/school_2.json +184 -0
- codeforlife-0.22.10/codeforlife/user/fixtures/school_2_sessions.json +19 -0
- codeforlife-0.22.10/codeforlife/user/fixtures/school_3.json +68 -0
- codeforlife-0.22.10/codeforlife/user/fixtures/sites.json +18 -0
- codeforlife-0.22.10/codeforlife/user/management/__init__.py +4 -0
- codeforlife-0.22.10/codeforlife/user/management/commands/__init__.py +4 -0
- codeforlife-0.22.10/codeforlife/user/management/commands/load_fixtures.py +11 -0
- codeforlife-0.22.10/codeforlife/user/management/commands/summarize_fixtures.py +11 -0
- codeforlife-0.22.10/codeforlife/user/migrations/0001_initial.py +248 -0
- codeforlife-0.22.10/codeforlife/user/migrations/__init__.py +0 -0
- codeforlife-0.22.10/codeforlife/user/models/__init__.py +38 -0
- codeforlife-0.22.10/codeforlife/user/models/auth_factor.py +40 -0
- codeforlife-0.22.10/codeforlife/user/models/auth_factor_test.py +15 -0
- codeforlife-0.22.10/codeforlife/user/models/klass.py +7 -0
- codeforlife-0.22.10/codeforlife/user/models/otp_bypass_token.py +100 -0
- codeforlife-0.22.10/codeforlife/user/models/otp_bypass_token_test.py +58 -0
- codeforlife-0.22.10/codeforlife/user/models/school.py +7 -0
- codeforlife-0.22.10/codeforlife/user/models/session.py +51 -0
- codeforlife-0.22.10/codeforlife/user/models/session_auth_factor.py +31 -0
- codeforlife-0.22.10/codeforlife/user/models/session_auth_factor_test.py +18 -0
- codeforlife-0.22.10/codeforlife/user/models/session_test.py +29 -0
- codeforlife-0.22.10/codeforlife/user/models/student.py +34 -0
- codeforlife-0.22.10/codeforlife/user/models/student_test.py +16 -0
- codeforlife-0.22.10/codeforlife/user/models/teacher.py +206 -0
- codeforlife-0.22.10/codeforlife/user/models/user.py +630 -0
- codeforlife-0.22.10/codeforlife/user/permissions/__init__.py +8 -0
- codeforlife-0.22.10/codeforlife/user/permissions/is_independent.py +51 -0
- codeforlife-0.22.10/codeforlife/user/permissions/is_student.py +23 -0
- codeforlife-0.22.10/codeforlife/user/permissions/is_teacher.py +71 -0
- codeforlife-0.22.10/codeforlife/user/serializers/__init__.py +10 -0
- codeforlife-0.22.10/codeforlife/user/serializers/klass.py +50 -0
- codeforlife-0.22.10/codeforlife/user/serializers/school.py +31 -0
- codeforlife-0.22.10/codeforlife/user/serializers/student.py +28 -0
- codeforlife-0.22.10/codeforlife/user/serializers/teacher.py +30 -0
- codeforlife-0.22.10/codeforlife/user/serializers/user.py +103 -0
- codeforlife-0.22.10/codeforlife/user/serializers/user_test.py +71 -0
- codeforlife-0.22.10/codeforlife/user/signals/__init__.py +8 -0
- codeforlife-0.22.10/codeforlife/user/signals/teacher.py +9 -0
- codeforlife-0.22.10/codeforlife/user/signals/user.py +13 -0
- codeforlife-0.22.10/codeforlife/user/urls.py +15 -0
- codeforlife-0.22.10/codeforlife/user/views/__init__.py +8 -0
- codeforlife-0.22.10/codeforlife/user/views/klass.py +36 -0
- codeforlife-0.22.10/codeforlife/user/views/klass_test.py +135 -0
- codeforlife-0.22.10/codeforlife/user/views/school.py +46 -0
- codeforlife-0.22.10/codeforlife/user/views/school_test.py +93 -0
- codeforlife-0.22.10/codeforlife/user/views/user.py +79 -0
- codeforlife-0.22.10/codeforlife/user/views/user_test.py +248 -0
- codeforlife-0.22.10/codeforlife/version.py +8 -0
- codeforlife-0.22.10/codeforlife/views/__init__.py +11 -0
- codeforlife-0.22.10/codeforlife/views/api.py +63 -0
- codeforlife-0.22.10/codeforlife/views/base_login.py +106 -0
- codeforlife-0.22.10/codeforlife/views/common.py +35 -0
- codeforlife-0.22.10/codeforlife/views/decorators.py +70 -0
- codeforlife-0.22.10/codeforlife/views/health_check.py +126 -0
- codeforlife-0.22.10/codeforlife/views/model.py +396 -0
- codeforlife-0.22.10/codeforlife.egg-info/PKG-INFO +192 -0
- codeforlife-0.22.10/codeforlife.egg-info/SOURCES.txt +152 -0
- codeforlife-0.22.10/codeforlife.egg-info/dependency_links.txt +1 -0
- codeforlife-0.22.10/codeforlife.egg-info/requires.txt +153 -0
- codeforlife-0.22.10/codeforlife.egg-info/top_level.txt +1 -0
- codeforlife-0.22.10/pyproject.toml +47 -0
- codeforlife-0.22.10/setup.cfg +4 -0
- codeforlife-0.22.10/setup.py +100 -0
|
@@ -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,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,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()
|