clear-skies 1.22.10__py3-none-any.whl → 2.0.23__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.
- clear_skies-2.0.23.dist-info/METADATA +76 -0
- clear_skies-2.0.23.dist-info/RECORD +265 -0
- {clear_skies-1.22.10.dist-info → clear_skies-2.0.23.dist-info}/WHEEL +1 -1
- clearskies/__init__.py +37 -21
- clearskies/action.py +7 -0
- clearskies/authentication/__init__.py +8 -39
- clearskies/authentication/authentication.py +44 -0
- clearskies/authentication/authorization.py +14 -8
- clearskies/authentication/authorization_pass_through.py +14 -10
- clearskies/authentication/jwks.py +135 -58
- clearskies/authentication/public.py +3 -26
- clearskies/authentication/secret_bearer.py +515 -44
- clearskies/autodoc/formats/oai3_json/__init__.py +2 -2
- clearskies/autodoc/formats/oai3_json/oai3_json.py +11 -9
- clearskies/autodoc/formats/oai3_json/parameter.py +6 -3
- clearskies/autodoc/formats/oai3_json/request.py +7 -5
- clearskies/autodoc/formats/oai3_json/response.py +7 -4
- clearskies/autodoc/formats/oai3_json/schema/object.py +10 -1
- clearskies/autodoc/request/__init__.py +2 -0
- clearskies/autodoc/request/header.py +4 -6
- clearskies/autodoc/request/json_body.py +4 -6
- clearskies/autodoc/request/parameter.py +8 -0
- clearskies/autodoc/request/request.py +16 -4
- clearskies/autodoc/request/url_parameter.py +4 -6
- clearskies/autodoc/request/url_path.py +4 -6
- clearskies/autodoc/schema/__init__.py +4 -2
- clearskies/autodoc/schema/array.py +5 -6
- clearskies/autodoc/schema/boolean.py +4 -10
- clearskies/autodoc/schema/date.py +0 -3
- clearskies/autodoc/schema/datetime.py +1 -4
- clearskies/autodoc/schema/double.py +0 -3
- clearskies/autodoc/schema/enum.py +4 -2
- clearskies/autodoc/schema/integer.py +4 -9
- clearskies/autodoc/schema/long.py +0 -3
- clearskies/autodoc/schema/number.py +4 -9
- clearskies/autodoc/schema/object.py +5 -7
- clearskies/autodoc/schema/password.py +0 -3
- clearskies/autodoc/schema/schema.py +11 -0
- clearskies/autodoc/schema/string.py +4 -10
- clearskies/backends/__init__.py +55 -20
- clearskies/backends/api_backend.py +1118 -280
- clearskies/backends/backend.py +54 -85
- clearskies/backends/cursor_backend.py +246 -191
- clearskies/backends/memory_backend.py +514 -208
- clearskies/backends/secrets_backend.py +68 -31
- clearskies/column.py +1221 -0
- clearskies/columns/__init__.py +71 -0
- clearskies/columns/audit.py +306 -0
- clearskies/columns/belongs_to_id.py +478 -0
- clearskies/columns/belongs_to_model.py +129 -0
- clearskies/columns/belongs_to_self.py +109 -0
- clearskies/columns/boolean.py +110 -0
- clearskies/columns/category_tree.py +273 -0
- clearskies/columns/category_tree_ancestors.py +51 -0
- clearskies/columns/category_tree_children.py +126 -0
- clearskies/columns/category_tree_descendants.py +48 -0
- clearskies/columns/created.py +92 -0
- clearskies/columns/created_by_authorization_data.py +114 -0
- clearskies/columns/created_by_header.py +103 -0
- clearskies/columns/created_by_ip.py +90 -0
- clearskies/columns/created_by_routing_data.py +102 -0
- clearskies/columns/created_by_user_agent.py +89 -0
- clearskies/columns/date.py +232 -0
- clearskies/columns/datetime.py +284 -0
- clearskies/columns/email.py +78 -0
- clearskies/columns/float.py +149 -0
- clearskies/columns/has_many.py +529 -0
- clearskies/columns/has_many_self.py +62 -0
- clearskies/columns/has_one.py +21 -0
- clearskies/columns/integer.py +158 -0
- clearskies/columns/json.py +126 -0
- clearskies/columns/many_to_many_ids.py +335 -0
- clearskies/columns/many_to_many_ids_with_data.py +274 -0
- clearskies/columns/many_to_many_models.py +156 -0
- clearskies/columns/many_to_many_pivots.py +132 -0
- clearskies/columns/phone.py +162 -0
- clearskies/columns/select.py +95 -0
- clearskies/columns/string.py +102 -0
- clearskies/columns/timestamp.py +164 -0
- clearskies/columns/updated.py +107 -0
- clearskies/columns/uuid.py +83 -0
- clearskies/configs/README.md +105 -0
- clearskies/configs/__init__.py +170 -0
- clearskies/configs/actions.py +43 -0
- clearskies/configs/any.py +15 -0
- clearskies/configs/any_dict.py +24 -0
- clearskies/configs/any_dict_or_callable.py +25 -0
- clearskies/configs/authentication.py +23 -0
- clearskies/configs/authorization.py +23 -0
- clearskies/configs/boolean.py +18 -0
- clearskies/configs/boolean_or_callable.py +20 -0
- clearskies/configs/callable_config.py +20 -0
- clearskies/configs/columns.py +34 -0
- clearskies/configs/conditions.py +30 -0
- clearskies/configs/config.py +26 -0
- clearskies/configs/datetime.py +20 -0
- clearskies/configs/datetime_or_callable.py +21 -0
- clearskies/configs/email.py +10 -0
- clearskies/configs/email_list.py +17 -0
- clearskies/configs/email_list_or_callable.py +17 -0
- clearskies/configs/email_or_email_list_or_callable.py +59 -0
- clearskies/configs/endpoint.py +23 -0
- clearskies/configs/endpoint_list.py +29 -0
- clearskies/configs/float.py +18 -0
- clearskies/configs/float_or_callable.py +20 -0
- clearskies/configs/headers.py +28 -0
- clearskies/configs/integer.py +18 -0
- clearskies/configs/integer_or_callable.py +20 -0
- clearskies/configs/joins.py +30 -0
- clearskies/configs/list_any_dict.py +32 -0
- clearskies/configs/list_any_dict_or_callable.py +33 -0
- clearskies/configs/model_class.py +35 -0
- clearskies/configs/model_column.py +67 -0
- clearskies/configs/model_columns.py +58 -0
- clearskies/configs/model_destination_name.py +26 -0
- clearskies/configs/model_to_id_column.py +45 -0
- clearskies/configs/readable_model_column.py +11 -0
- clearskies/configs/readable_model_columns.py +11 -0
- clearskies/configs/schema.py +23 -0
- clearskies/configs/searchable_model_columns.py +11 -0
- clearskies/configs/security_headers.py +39 -0
- clearskies/configs/select.py +28 -0
- clearskies/configs/select_list.py +49 -0
- clearskies/configs/string.py +31 -0
- clearskies/configs/string_dict.py +34 -0
- clearskies/configs/string_list.py +47 -0
- clearskies/configs/string_list_or_callable.py +48 -0
- clearskies/configs/string_or_callable.py +18 -0
- clearskies/configs/timedelta.py +20 -0
- clearskies/configs/timezone.py +20 -0
- clearskies/configs/url.py +25 -0
- clearskies/configs/validators.py +45 -0
- clearskies/configs/writeable_model_column.py +11 -0
- clearskies/configs/writeable_model_columns.py +11 -0
- clearskies/configurable.py +78 -0
- clearskies/contexts/__init__.py +8 -8
- clearskies/contexts/cli.py +129 -43
- clearskies/contexts/context.py +93 -56
- clearskies/contexts/wsgi.py +79 -33
- clearskies/contexts/wsgi_ref.py +87 -0
- clearskies/cursors/__init__.py +7 -0
- clearskies/cursors/cursor.py +166 -0
- clearskies/cursors/from_environment/__init__.py +5 -0
- clearskies/cursors/from_environment/mysql.py +51 -0
- clearskies/cursors/from_environment/postgresql.py +49 -0
- clearskies/cursors/from_environment/sqlite.py +35 -0
- clearskies/cursors/mysql.py +61 -0
- clearskies/cursors/postgresql.py +61 -0
- clearskies/cursors/sqlite.py +62 -0
- clearskies/decorators.py +33 -0
- clearskies/decorators.pyi +10 -0
- clearskies/di/__init__.py +11 -7
- clearskies/di/additional_config.py +115 -4
- clearskies/di/additional_config_auto_import.py +12 -0
- clearskies/di/di.py +714 -125
- clearskies/di/inject/__init__.py +23 -0
- clearskies/di/inject/akeyless_sdk.py +16 -0
- clearskies/di/inject/by_class.py +24 -0
- clearskies/di/inject/by_name.py +22 -0
- clearskies/di/inject/di.py +16 -0
- clearskies/di/inject/environment.py +15 -0
- clearskies/di/inject/input_output.py +19 -0
- clearskies/di/inject/now.py +16 -0
- clearskies/di/inject/requests.py +16 -0
- clearskies/di/inject/secrets.py +15 -0
- clearskies/di/inject/utcnow.py +16 -0
- clearskies/di/inject/uuid.py +16 -0
- clearskies/di/injectable.py +32 -0
- clearskies/di/injectable_properties.py +131 -0
- clearskies/end.py +219 -0
- clearskies/endpoint.py +1303 -0
- clearskies/endpoint_group.py +333 -0
- clearskies/endpoints/__init__.py +25 -0
- clearskies/endpoints/advanced_search.py +519 -0
- clearskies/endpoints/callable.py +382 -0
- clearskies/endpoints/create.py +201 -0
- clearskies/endpoints/delete.py +133 -0
- clearskies/endpoints/get.py +267 -0
- clearskies/endpoints/health_check.py +181 -0
- clearskies/endpoints/list.py +567 -0
- clearskies/endpoints/restful_api.py +417 -0
- clearskies/endpoints/schema.py +185 -0
- clearskies/endpoints/simple_search.py +279 -0
- clearskies/endpoints/update.py +188 -0
- clearskies/environment.py +7 -3
- clearskies/exceptions/__init__.py +19 -0
- clearskies/{handlers/exceptions/input_error.py → exceptions/input_errors.py} +1 -1
- clearskies/exceptions/missing_dependency.py +2 -0
- clearskies/exceptions/moved_permanently.py +3 -0
- clearskies/exceptions/moved_temporarily.py +3 -0
- clearskies/functional/__init__.py +2 -2
- clearskies/functional/json.py +47 -0
- clearskies/functional/routing.py +92 -0
- clearskies/functional/string.py +19 -11
- clearskies/functional/validations.py +61 -9
- clearskies/input_outputs/__init__.py +9 -7
- clearskies/input_outputs/cli.py +135 -160
- clearskies/input_outputs/exceptions/__init__.py +6 -1
- clearskies/input_outputs/headers.py +54 -0
- clearskies/input_outputs/input_output.py +77 -123
- clearskies/input_outputs/programmatic.py +62 -0
- clearskies/input_outputs/wsgi.py +36 -48
- clearskies/model.py +1874 -193
- clearskies/query/__init__.py +12 -0
- clearskies/query/condition.py +228 -0
- clearskies/query/join.py +136 -0
- clearskies/query/query.py +193 -0
- clearskies/query/sort.py +27 -0
- clearskies/schema.py +82 -0
- clearskies/secrets/__init__.py +4 -31
- clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py +15 -4
- clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +11 -5
- clearskies/secrets/akeyless.py +421 -155
- clearskies/secrets/exceptions/__init__.py +7 -1
- clearskies/secrets/exceptions/not_found_error.py +2 -0
- clearskies/secrets/exceptions/permissions_error.py +2 -0
- clearskies/secrets/secrets.py +12 -11
- clearskies/security_header.py +17 -0
- clearskies/security_headers/__init__.py +8 -8
- clearskies/security_headers/cache_control.py +47 -109
- clearskies/security_headers/cors.py +38 -92
- clearskies/security_headers/csp.py +76 -150
- clearskies/security_headers/hsts.py +14 -15
- clearskies/typing.py +11 -0
- clearskies/validator.py +36 -0
- clearskies/validators/__init__.py +33 -0
- clearskies/validators/after_column.py +61 -0
- clearskies/validators/before_column.py +15 -0
- clearskies/validators/in_the_future.py +29 -0
- clearskies/validators/in_the_future_at_least.py +13 -0
- clearskies/validators/in_the_future_at_most.py +12 -0
- clearskies/validators/in_the_past.py +29 -0
- clearskies/validators/in_the_past_at_least.py +12 -0
- clearskies/validators/in_the_past_at_most.py +12 -0
- clearskies/validators/maximum_length.py +25 -0
- clearskies/validators/maximum_value.py +28 -0
- clearskies/validators/minimum_length.py +25 -0
- clearskies/validators/minimum_value.py +28 -0
- clearskies/{input_requirements → validators}/required.py +18 -9
- clearskies/validators/timedelta.py +58 -0
- clearskies/validators/unique.py +28 -0
- clear_skies-1.22.10.dist-info/METADATA +0 -47
- clear_skies-1.22.10.dist-info/RECORD +0 -213
- clearskies/application.py +0 -29
- clearskies/authentication/auth0_jwks.py +0 -118
- clearskies/authentication/auth_exception.py +0 -2
- clearskies/authentication/jwks_jwcrypto.py +0 -51
- clearskies/backends/api_get_only_backend.py +0 -48
- clearskies/backends/example_backend.py +0 -43
- clearskies/backends/file_backend.py +0 -48
- clearskies/backends/json_backend.py +0 -7
- clearskies/backends/restful_api_advanced_search_backend.py +0 -103
- clearskies/binding_config.py +0 -16
- clearskies/column_types/__init__.py +0 -203
- clearskies/column_types/audit.py +0 -249
- clearskies/column_types/belongs_to.py +0 -271
- clearskies/column_types/boolean.py +0 -60
- clearskies/column_types/category_tree.py +0 -304
- clearskies/column_types/column.py +0 -373
- clearskies/column_types/created.py +0 -26
- clearskies/column_types/created_by_authorization_data.py +0 -26
- clearskies/column_types/created_by_header.py +0 -24
- clearskies/column_types/created_by_ip.py +0 -17
- clearskies/column_types/created_by_routing_data.py +0 -25
- clearskies/column_types/created_by_user_agent.py +0 -17
- clearskies/column_types/created_micro.py +0 -26
- clearskies/column_types/datetime.py +0 -109
- clearskies/column_types/datetime_micro.py +0 -13
- clearskies/column_types/email.py +0 -18
- clearskies/column_types/float.py +0 -43
- clearskies/column_types/has_many.py +0 -179
- clearskies/column_types/has_one.py +0 -58
- clearskies/column_types/integer.py +0 -41
- clearskies/column_types/json.py +0 -25
- clearskies/column_types/many_to_many.py +0 -278
- clearskies/column_types/many_to_many_with_data.py +0 -162
- clearskies/column_types/phone.py +0 -48
- clearskies/column_types/select.py +0 -11
- clearskies/column_types/string.py +0 -24
- clearskies/column_types/timestamp.py +0 -73
- clearskies/column_types/updated.py +0 -26
- clearskies/column_types/updated_micro.py +0 -26
- clearskies/column_types/uuid.py +0 -25
- clearskies/columns.py +0 -123
- clearskies/condition_parser.py +0 -172
- clearskies/contexts/build_context.py +0 -54
- clearskies/contexts/convert_to_application.py +0 -190
- clearskies/contexts/extract_handler.py +0 -37
- clearskies/contexts/test.py +0 -94
- clearskies/decorators/__init__.py +0 -39
- clearskies/decorators/auth0_jwks.py +0 -22
- clearskies/decorators/authorization.py +0 -10
- clearskies/decorators/binding_classes.py +0 -9
- clearskies/decorators/binding_modules.py +0 -9
- clearskies/decorators/bindings.py +0 -9
- clearskies/decorators/create.py +0 -10
- clearskies/decorators/delete.py +0 -10
- clearskies/decorators/docs.py +0 -14
- clearskies/decorators/get.py +0 -10
- clearskies/decorators/jwks.py +0 -26
- clearskies/decorators/merge.py +0 -124
- clearskies/decorators/patch.py +0 -10
- clearskies/decorators/post.py +0 -10
- clearskies/decorators/public.py +0 -11
- clearskies/decorators/response_headers.py +0 -10
- clearskies/decorators/return_raw_response.py +0 -9
- clearskies/decorators/schema.py +0 -10
- clearskies/decorators/secret_bearer.py +0 -24
- clearskies/decorators/security_headers.py +0 -10
- clearskies/di/standard_dependencies.py +0 -151
- clearskies/di/test_module/__init__.py +0 -6
- clearskies/di/test_module/another_module/__init__.py +0 -2
- clearskies/di/test_module/module_class.py +0 -5
- clearskies/handlers/__init__.py +0 -41
- clearskies/handlers/advanced_search.py +0 -271
- clearskies/handlers/base.py +0 -479
- clearskies/handlers/callable.py +0 -191
- clearskies/handlers/create.py +0 -35
- clearskies/handlers/crud_by_method.py +0 -18
- clearskies/handlers/database_connector.py +0 -32
- clearskies/handlers/delete.py +0 -61
- clearskies/handlers/exceptions/__init__.py +0 -5
- clearskies/handlers/exceptions/not_found.py +0 -3
- clearskies/handlers/get.py +0 -156
- clearskies/handlers/health_check.py +0 -59
- clearskies/handlers/input_processing.py +0 -79
- clearskies/handlers/list.py +0 -530
- clearskies/handlers/mygrations.py +0 -82
- clearskies/handlers/request_method_routing.py +0 -47
- clearskies/handlers/restful_api.py +0 -218
- clearskies/handlers/routing.py +0 -62
- clearskies/handlers/schema_helper.py +0 -128
- clearskies/handlers/simple_routing.py +0 -206
- clearskies/handlers/simple_routing_route.py +0 -192
- clearskies/handlers/simple_search.py +0 -136
- clearskies/handlers/update.py +0 -96
- clearskies/handlers/write.py +0 -193
- clearskies/input_requirements/__init__.py +0 -78
- clearskies/input_requirements/after.py +0 -36
- clearskies/input_requirements/before.py +0 -36
- clearskies/input_requirements/in_the_future_at_least.py +0 -19
- clearskies/input_requirements/in_the_future_at_most.py +0 -19
- clearskies/input_requirements/in_the_past_at_least.py +0 -19
- clearskies/input_requirements/in_the_past_at_most.py +0 -19
- clearskies/input_requirements/maximum_length.py +0 -19
- clearskies/input_requirements/maximum_value.py +0 -19
- clearskies/input_requirements/minimum_length.py +0 -22
- clearskies/input_requirements/minimum_value.py +0 -19
- clearskies/input_requirements/requirement.py +0 -25
- clearskies/input_requirements/time_delta.py +0 -38
- clearskies/input_requirements/unique.py +0 -18
- clearskies/mocks/__init__.py +0 -7
- clearskies/mocks/input_output.py +0 -124
- clearskies/mocks/models.py +0 -142
- clearskies/models.py +0 -350
- clearskies/security_headers/base.py +0 -12
- clearskies/tests/simple_api/models/__init__.py +0 -2
- clearskies/tests/simple_api/models/status.py +0 -23
- clearskies/tests/simple_api/models/user.py +0 -21
- clearskies/tests/simple_api/users_api.py +0 -64
- {clear_skies-1.22.10.dist-info → clear_skies-2.0.23.dist-info/licenses}/LICENSE +0 -0
- /clearskies/{contexts/bash.py → autodoc/py.typed} +0 -0
- /clearskies/{handlers/exceptions → exceptions}/authentication.py +0 -0
- /clearskies/{handlers/exceptions → exceptions}/authorization.py +0 -0
- /clearskies/{handlers/exceptions → exceptions}/client_error.py +0 -0
- /clearskies/{secrets/exceptions → exceptions}/not_found.py +0 -0
- /clearskies/{tests/__init__.py → input_outputs/py.typed} +0 -0
- /clearskies/{tests/simple_api/__init__.py → py.typed} +0 -0
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
from .string import String
|
|
3
|
-
from ..autodoc.schema import Array as AutoDocArray
|
|
4
|
-
from ..autodoc.schema import Object as AutoDocObject
|
|
5
|
-
from ..autodoc.schema import String as AutoDocString
|
|
6
|
-
from collections import OrderedDict
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class BelongsTo(String):
|
|
10
|
-
"""
|
|
11
|
-
Controls a belongs to relationship.
|
|
12
|
-
|
|
13
|
-
This column should be named something like 'parent_id', e.g. user_id, column_id, etc... It expects the actual
|
|
14
|
-
database column to be an integer. It also provides an additional property on the model which returns the
|
|
15
|
-
related model, instead of the id, with a name given by dropping `_id` from the column name. In other words,
|
|
16
|
-
if you have a column called user_id and a particular model has a user_id of 5, then:
|
|
17
|
-
|
|
18
|
-
```
|
|
19
|
-
print(model.user_id)
|
|
20
|
-
# prints 5
|
|
21
|
-
print(model.user.id)
|
|
22
|
-
# prints 5
|
|
23
|
-
print(model.user.name)
|
|
24
|
-
# prints the name of the user with an id of 5.
|
|
25
|
-
```
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
wants_n_plus_one = True
|
|
29
|
-
required_configs = [
|
|
30
|
-
"parent_models_class",
|
|
31
|
-
]
|
|
32
|
-
|
|
33
|
-
my_configs = [
|
|
34
|
-
"model_column_name",
|
|
35
|
-
"readable_parent_columns",
|
|
36
|
-
"join_type",
|
|
37
|
-
"where",
|
|
38
|
-
]
|
|
39
|
-
|
|
40
|
-
def __init__(self, di):
|
|
41
|
-
super().__init__(di)
|
|
42
|
-
|
|
43
|
-
def _check_configuration(self, configuration):
|
|
44
|
-
super()._check_configuration(configuration)
|
|
45
|
-
self.validate_models_class(configuration["parent_models_class"])
|
|
46
|
-
|
|
47
|
-
error_prefix = f"Configuration error for '{self.name}' in '{self.model_class.__name__}':"
|
|
48
|
-
if not configuration.get("model_column_name") and self.name[-3:] != "_id":
|
|
49
|
-
raise ValueError(
|
|
50
|
-
f"Invalid name for column '{self.name}' in '{self.model_class.__name__}' - "
|
|
51
|
-
+ "BelongsTo column names must end in '_id', or you must set 'model_column_name' to specify the name of the column "
|
|
52
|
-
+ "that the parent model can be fetched from."
|
|
53
|
-
)
|
|
54
|
-
if configuration.get("model_column_name") and type(configuration.get("model_column_name")) != str:
|
|
55
|
-
raise ValueError(f"{error_prefix} 'model_column_name' must be a string.")
|
|
56
|
-
|
|
57
|
-
join_type = configuration.get("join_type")
|
|
58
|
-
if join_type and join_type.upper() not in ["LEFT", "INNER"]:
|
|
59
|
-
raise ValueError(f"{error_prefix} join_type must be INNER or LEFT")
|
|
60
|
-
|
|
61
|
-
if configuration.get("readable_parent_columns"):
|
|
62
|
-
parent_columns = self.di.build(configuration["parent_models_class"], cache=True).raw_columns_configuration()
|
|
63
|
-
readable_parent_columns = configuration["readable_parent_columns"]
|
|
64
|
-
if not hasattr(readable_parent_columns, "__iter__"):
|
|
65
|
-
raise ValueError(
|
|
66
|
-
f"{error_prefix} 'readable_parent_columns' should be an iterable "
|
|
67
|
-
+ "with the list of child columns to output."
|
|
68
|
-
)
|
|
69
|
-
if isinstance(readable_parent_columns, str):
|
|
70
|
-
raise ValueError(
|
|
71
|
-
f"{error_prefix} 'readable_parent_columns' should be an iterable "
|
|
72
|
-
+ "with the list of child columns to output."
|
|
73
|
-
)
|
|
74
|
-
for column_name in readable_parent_columns:
|
|
75
|
-
if column_name not in parent_columns:
|
|
76
|
-
raise ValueError(
|
|
77
|
-
f"{error_prefix} 'readable_parent_columns' references column named '{column_name}' but this"
|
|
78
|
-
+ "column does not exist in the model class."
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
wheres = configuration.get("where")
|
|
82
|
-
if wheres:
|
|
83
|
-
if not isinstance(wheres, list):
|
|
84
|
-
raise ValueError(
|
|
85
|
-
f"{error_prefix} 'where' must be a list of where conditions or callables that return where conditions"
|
|
86
|
-
)
|
|
87
|
-
for index, where in enumerate(wheres):
|
|
88
|
-
if callable(where) or isinstance(where, str):
|
|
89
|
-
continue
|
|
90
|
-
raise ValueError(
|
|
91
|
-
f"{error_prefix} 'where' must be a list of where conditions or callables that return where conditions, but the item in entry #${index+1} was neither a string nor a callable"
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
def _finalize_configuration(self, configuration):
|
|
95
|
-
return {
|
|
96
|
-
**super()._finalize_configuration(configuration),
|
|
97
|
-
**{
|
|
98
|
-
"model_column_name": configuration.get("model_column_name")
|
|
99
|
-
if configuration.get("model_column_name")
|
|
100
|
-
else self.name[:-3],
|
|
101
|
-
"join_type": configuration.get("join_type", "INNER").upper(),
|
|
102
|
-
"where": configuration.get("where", []),
|
|
103
|
-
},
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
def input_error_for_value(self, value, operator=None):
|
|
107
|
-
integer_check = super().input_error_for_value(value)
|
|
108
|
-
if integer_check:
|
|
109
|
-
return integer_check
|
|
110
|
-
parent_models = self.parent_models
|
|
111
|
-
id_column_name = parent_models.get_id_column_name()
|
|
112
|
-
matching_parents = parent_models.where(f"{id_column_name}={value}")
|
|
113
|
-
input_output = self.di.build("input_output", cache=True)
|
|
114
|
-
matching_parents = matching_parents.where_for_request(
|
|
115
|
-
matching_parents,
|
|
116
|
-
input_output.routing_data(),
|
|
117
|
-
input_output.get_authorization_data(),
|
|
118
|
-
input_output,
|
|
119
|
-
)
|
|
120
|
-
if not len(matching_parents):
|
|
121
|
-
return f"Invalid selection for {self.name}: record does not exist"
|
|
122
|
-
return ""
|
|
123
|
-
|
|
124
|
-
def can_provide(self, column_name):
|
|
125
|
-
return column_name == self.config("model_column_name")
|
|
126
|
-
|
|
127
|
-
def provide(self, data, column_name):
|
|
128
|
-
# did we have data parent data loaded up with a query?
|
|
129
|
-
alias = self.join_table_alias()
|
|
130
|
-
parent_id_column_name = self.parent_models.get_id_column_name()
|
|
131
|
-
if f"{alias}_{parent_id_column_name}" in data:
|
|
132
|
-
parent_data = {parent_id_column_name: data[f"{alias}_{parent_id_column_name}"]}
|
|
133
|
-
for column_name in self.parent_columns.keys():
|
|
134
|
-
select_alias = f"{alias}_{column_name}"
|
|
135
|
-
parent_data[column_name] = data[select_alias] if select_alias in data else None
|
|
136
|
-
return self.parent_models.model(parent_data)
|
|
137
|
-
|
|
138
|
-
# if not, just look it up from the id
|
|
139
|
-
parent_id = data.get(self.name)
|
|
140
|
-
if parent_id:
|
|
141
|
-
parent_id_column_name = self.parent_models.get_id_column_name()
|
|
142
|
-
return self.parent_models.where(f"{parent_id_column_name}={parent_id}").first()
|
|
143
|
-
return self.parent_models.empty_model()
|
|
144
|
-
|
|
145
|
-
def join_table_alias(self):
|
|
146
|
-
return self.parent_models.table_name() + "_" + self.name
|
|
147
|
-
|
|
148
|
-
def configure_n_plus_one(self, models, columns=None):
|
|
149
|
-
if columns is None:
|
|
150
|
-
columns = self.config("readable_parent_columns", silent=True)
|
|
151
|
-
if not columns:
|
|
152
|
-
return models
|
|
153
|
-
|
|
154
|
-
models = self.add_join(models)
|
|
155
|
-
alias = self.join_table_alias()
|
|
156
|
-
parent_id_column_name = self.parent_models.get_id_column_name()
|
|
157
|
-
select_parts = [f"{alias}.{column_name} AS {alias}_{column_name}" for column_name in columns]
|
|
158
|
-
select_parts.append(f"{alias}.{parent_id_column_name} AS {alias}_{parent_id_column_name}")
|
|
159
|
-
return models.select(", ".join(select_parts))
|
|
160
|
-
|
|
161
|
-
@property
|
|
162
|
-
def parent_models(self):
|
|
163
|
-
parents = self.di.build(self.config("parent_models_class"), cache=True)
|
|
164
|
-
for where in self.config("where"):
|
|
165
|
-
if callable(where):
|
|
166
|
-
parents = self.di.call_function(where, model=parents)
|
|
167
|
-
if not parents:
|
|
168
|
-
raise ValueError(
|
|
169
|
-
f"Configuration error for column '{self.name}' in model '{self.model_class.__name__}': when 'where' is a callable, it must return a models class, but when the callable in where entry #{index+1} was called, it did not return the models class"
|
|
170
|
-
)
|
|
171
|
-
else:
|
|
172
|
-
parents = parents.where(where)
|
|
173
|
-
return parents
|
|
174
|
-
|
|
175
|
-
@property
|
|
176
|
-
def parent_columns(self):
|
|
177
|
-
return self.parent_models.model_columns
|
|
178
|
-
|
|
179
|
-
def to_json(self, model):
|
|
180
|
-
# if we don't have readable parent columns specified, then just return the id
|
|
181
|
-
if not self.config("readable_parent_columns", silent=True):
|
|
182
|
-
return super().to_json(model)
|
|
183
|
-
|
|
184
|
-
# otherwise return an object with the readable parent columns
|
|
185
|
-
columns = self.parent_columns
|
|
186
|
-
parent = model.__getattr__(self.config("model_column_name"))
|
|
187
|
-
json = OrderedDict()
|
|
188
|
-
if parent.id_column_name not in self.config("readable_parent_columns"):
|
|
189
|
-
json[parent.id_column_name] = list(columns[parent.id_column_name].to_json(parent).values())[0]
|
|
190
|
-
for column_name in self.config("readable_parent_columns"):
|
|
191
|
-
json = {**json, **columns[column_name].to_json(parent)}
|
|
192
|
-
id_less_name = self.config("model_column_name")
|
|
193
|
-
return {
|
|
194
|
-
**super().to_json(model),
|
|
195
|
-
id_less_name: json,
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
def documentation(self, name=None, example=None, value=None):
|
|
199
|
-
columns = self.parent_columns
|
|
200
|
-
parent_id_column_name = self.parent_models.get_id_column_name()
|
|
201
|
-
parent_properties = [columns[parent_id_column_name].documentation()]
|
|
202
|
-
|
|
203
|
-
parent_columns = self.config("readable_parent_columns", silent=True)
|
|
204
|
-
parent_id_doc = AutoDocString(name if name is not None else self.name)
|
|
205
|
-
if not parent_columns:
|
|
206
|
-
return parent_id_doc
|
|
207
|
-
|
|
208
|
-
for column_name in self.config("readable_parent_columns"):
|
|
209
|
-
if column_name == parent_id_column_name:
|
|
210
|
-
continue
|
|
211
|
-
parent_properties.append(columns[column_name].documentation())
|
|
212
|
-
|
|
213
|
-
return [
|
|
214
|
-
parent_id_doc,
|
|
215
|
-
AutoDocObject(
|
|
216
|
-
self.config("model_column_name"),
|
|
217
|
-
parent_properties,
|
|
218
|
-
),
|
|
219
|
-
]
|
|
220
|
-
|
|
221
|
-
def is_allowed_operator(self, operator, relationship_reference=None):
|
|
222
|
-
"""
|
|
223
|
-
This is called when processing user data to decide if the end-user is specifying an allowed operator
|
|
224
|
-
"""
|
|
225
|
-
if not relationship_reference:
|
|
226
|
-
return "="
|
|
227
|
-
parent_columns = self.parent_columns
|
|
228
|
-
if relationship_reference not in self.parent_columns:
|
|
229
|
-
raise ValueError(
|
|
230
|
-
"I was asked to search on a related column that doens't exist. This shouldn't have happened :("
|
|
231
|
-
)
|
|
232
|
-
return self.parent_columns[relationship_reference].is_allowed_operator(operator)
|
|
233
|
-
|
|
234
|
-
def check_search_value(self, value, operator=None, relationship_reference=None):
|
|
235
|
-
if not relationship_reference:
|
|
236
|
-
return self.input_error_for_value(value, operator=operator)
|
|
237
|
-
parent_columns = self.parent_columns
|
|
238
|
-
if relationship_reference not in self.parent_columns:
|
|
239
|
-
raise ValueError(
|
|
240
|
-
"I was asked to search on a related column that doens't exist. This shouldn't have happened :("
|
|
241
|
-
)
|
|
242
|
-
return self.parent_columns[relationship_reference].check_search_value(value, operator=operator)
|
|
243
|
-
|
|
244
|
-
def add_join(self, models):
|
|
245
|
-
parent_table = self.parent_models.table_name()
|
|
246
|
-
alias = self.join_table_alias()
|
|
247
|
-
|
|
248
|
-
if models.is_joined(parent_table, alias=alias):
|
|
249
|
-
return models
|
|
250
|
-
|
|
251
|
-
join_type = "LEFT " if self.config("join_type") == "LEFT" else ""
|
|
252
|
-
own_table_name = models.table_name()
|
|
253
|
-
parent_id_column_name = self.parent_models.get_id_column_name()
|
|
254
|
-
return models.join(
|
|
255
|
-
f"{join_type}JOIN {parent_table} as {alias} on {alias}.{parent_id_column_name}={own_table_name}.{self.name}"
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
def add_search(self, models, value, operator=None, relationship_reference=None):
|
|
259
|
-
if not relationship_reference:
|
|
260
|
-
return super().add_search(models, value, operator=operator)
|
|
261
|
-
|
|
262
|
-
parent_columns = self.parent_columns
|
|
263
|
-
if relationship_reference not in self.parent_columns:
|
|
264
|
-
raise ValueError(
|
|
265
|
-
"I was asked to search on a related column that doens't exist. This shouldn't have happened :("
|
|
266
|
-
)
|
|
267
|
-
|
|
268
|
-
models = self.add_join(models)
|
|
269
|
-
related_column = self.parent_columns[relationship_reference]
|
|
270
|
-
alias = self.join_table_alias()
|
|
271
|
-
return models.where(related_column.build_condition(value, operator=operator, column_prefix=f"{alias}."))
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
from .column import Column
|
|
2
|
-
from ..autodoc.schema import Boolean as AutoDocBoolean
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class Boolean(Column):
|
|
6
|
-
_auto_doc_class = AutoDocBoolean
|
|
7
|
-
|
|
8
|
-
my_configs = {
|
|
9
|
-
"on_true": None,
|
|
10
|
-
"on_false": None,
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
def __init__(self, di):
|
|
14
|
-
super().__init__(di)
|
|
15
|
-
|
|
16
|
-
def _check_configuration(self, configuration):
|
|
17
|
-
"""Check the configuration and throw exceptions as needed"""
|
|
18
|
-
super()._check_configuration(configuration)
|
|
19
|
-
for trigger in ["on_true", "on_false"]:
|
|
20
|
-
if configuration.get(trigger):
|
|
21
|
-
self._check_actions(configuration[trigger], trigger)
|
|
22
|
-
|
|
23
|
-
def to_backend(self, data):
|
|
24
|
-
if self.name not in data or data[self.name] is None:
|
|
25
|
-
return data
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
**data,
|
|
29
|
-
self.name: bool(data[self.name]),
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
def from_backend(self, value):
|
|
33
|
-
return bool(value)
|
|
34
|
-
|
|
35
|
-
def input_error_for_value(self, value, operator=None):
|
|
36
|
-
return f"{self.name} must be a boolean" if type(value) != bool else ""
|
|
37
|
-
|
|
38
|
-
def build_condition(self, value, operator=None, column_prefix=""):
|
|
39
|
-
condition_value = "1" if value else "0"
|
|
40
|
-
if not operator:
|
|
41
|
-
operator = "="
|
|
42
|
-
return f"{column_prefix}{self.name}{operator}{condition_value}"
|
|
43
|
-
|
|
44
|
-
def save_finished(self, model):
|
|
45
|
-
"""
|
|
46
|
-
Make any necessary changes needed after a save has completely finished.
|
|
47
|
-
"""
|
|
48
|
-
super().save_finished(model)
|
|
49
|
-
|
|
50
|
-
on_true = self.config("on_true", silent=True)
|
|
51
|
-
on_false = self.config("on_false", silent=True)
|
|
52
|
-
if not on_true and not on_false:
|
|
53
|
-
return
|
|
54
|
-
if not model.was_changed(self.name):
|
|
55
|
-
return
|
|
56
|
-
|
|
57
|
-
if model.get(self.name) and on_true:
|
|
58
|
-
self.execute_actions(on_true, model)
|
|
59
|
-
if not model.get(self.name) and on_false:
|
|
60
|
-
self.execute_actions(on_false, model)
|
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
from .belongs_to import BelongsTo
|
|
3
|
-
from ..autodoc.schema import Array as AutoDocArray
|
|
4
|
-
from ..autodoc.schema import Object as AutoDocObject
|
|
5
|
-
from ..autodoc.schema import String as AutoDocString
|
|
6
|
-
from collections import OrderedDict
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class CategoryTree(BelongsTo):
|
|
10
|
-
"""
|
|
11
|
-
Builds a tree table for quick lookups in a category heirarchy.
|
|
12
|
-
|
|
13
|
-
Imagine you have a model that represents a category heirarchy:
|
|
14
|
-
|
|
15
|
-
```
|
|
16
|
-
CREATE TABLE categories (
|
|
17
|
-
id varchar(255),
|
|
18
|
-
parent_id varchar(255),
|
|
19
|
-
name varchar(255)
|
|
20
|
-
)
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Where `parent_id` references a record in the same categories table - a category tree! This works
|
|
24
|
-
fine but it gets tricky when you want to answer the question "what are all the parent categories
|
|
25
|
-
of X category?" or "what are all the child categories of Y category?". This column class solves that
|
|
26
|
-
by building a tree table that caches this data as the categories are updated. That table should look
|
|
27
|
-
like this:
|
|
28
|
-
|
|
29
|
-
```
|
|
30
|
-
CREATE TABLE category_tree (
|
|
31
|
-
id varchar(255),
|
|
32
|
-
parent_id varchar(255),
|
|
33
|
-
child_id varchar(255),
|
|
34
|
-
is_parent tinyint(1),
|
|
35
|
-
level tinyint(1),
|
|
36
|
-
)
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
(add indexes as desired). You then you have your corresponding models:
|
|
40
|
-
|
|
41
|
-
```
|
|
42
|
-
import clearskies
|
|
43
|
-
|
|
44
|
-
class CategoryTree(clearskies.Model):
|
|
45
|
-
def __init__(self, cursor_backend, columns):
|
|
46
|
-
super().__init__(cursor_backend, columns)
|
|
47
|
-
|
|
48
|
-
def columns_configuration(self):
|
|
49
|
-
return OrderedDict([
|
|
50
|
-
clearskies.column_types.string('parent_id'),
|
|
51
|
-
clearskies.column_types.string('child_id'),
|
|
52
|
-
clearskies.column_types.integer('is_parent'),
|
|
53
|
-
clearskies.column_types.integer('level'),
|
|
54
|
-
])
|
|
55
|
-
|
|
56
|
-
class Category(clearskies.Model):
|
|
57
|
-
def __init__(self, cursor_backend, columns):
|
|
58
|
-
super().__init__(cursor_backend, columns)
|
|
59
|
-
|
|
60
|
-
def columns_configuration(self):
|
|
61
|
-
return OrderedDict([
|
|
62
|
-
clearskies.column_types.string('name'),
|
|
63
|
-
clearskies.column_types.category_tree('parent_id', tree_models_class=CategoryTree),
|
|
64
|
-
])
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
You would then build your cateogry tree normally:
|
|
68
|
-
|
|
69
|
-
```
|
|
70
|
-
# categories object comes in from dependency injection
|
|
71
|
-
root_category = categories.create({'name': 'my root category'})
|
|
72
|
-
sub_category = categories.create({'name': 'my sub category', parent_id=root_category.id})
|
|
73
|
-
alt_sub_category = categories.create({'name': 'my alternate sub category', parent_id=root_category.id})
|
|
74
|
-
sub_sub_category = categories.create({'name': 'my sub-sub category', parent_id=sub_category.id})
|
|
75
|
-
sub_sub_sub_category = categories.create({'name': 'my sub-sub-sub category', parent_id=sub_sub_category.id})
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
and your database would look like this (using auto-incrementing ids for hopeful clarity)
|
|
79
|
-
|
|
80
|
-
```
|
|
81
|
-
$ SELECT * FROM category_tree
|
|
82
|
-
|
|
83
|
-
parent_id | child_id | is_parent | level
|
|
84
|
-
1 | 2 | 1 | 0 # sub category
|
|
85
|
-
1 | 3 | 1 | 0 # alt sub category
|
|
86
|
-
1 | 4 | 0 | 0 # sub sub category referencing the root category
|
|
87
|
-
2 | 4 | 1 | 1 # sub sub category referencing the sub category
|
|
88
|
-
1 | 5 | 0 | 0 # sub sub sub category referencing the root category
|
|
89
|
-
2 | 5 | 0 | 1 # sub sub sub category referencing the sub category
|
|
90
|
-
4 | 5 | 1 | 2 # sub sub sub category referencing the sub sub category
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
You can then use various SQL statements to efficiently fetch various pieces of data.
|
|
94
|
-
|
|
95
|
-
```
|
|
96
|
-
# the category tree for a specific category
|
|
97
|
-
SELECT parent_id FROM category_tree WHERE child_id=5 ORDER BY level DESC;
|
|
98
|
-
# all the children of a parent (excluding sub-children)
|
|
99
|
-
SELECT child_id FROM category_tree WHERE parent_id=1 AND is_parent=1
|
|
100
|
-
# All the children of a parent (including sub-children)
|
|
101
|
-
SELECT child_id FROM category_tree WHERE parent_id=1;
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
Of course other kinds of databases are better at this (such as graph databases), but it isn't
|
|
105
|
-
always worth managing another database unless performance is becoming a problem.
|
|
106
|
-
"""
|
|
107
|
-
|
|
108
|
-
required_configs = [
|
|
109
|
-
"tree_models_class",
|
|
110
|
-
]
|
|
111
|
-
|
|
112
|
-
my_configs = [
|
|
113
|
-
"model_column_name",
|
|
114
|
-
"readable_parent_columns",
|
|
115
|
-
"join_type",
|
|
116
|
-
"tree_parent_id_column_name",
|
|
117
|
-
"tree_child_id_column_name",
|
|
118
|
-
"tree_is_parent_column_name",
|
|
119
|
-
"tree_level_column_name",
|
|
120
|
-
"max_iterations",
|
|
121
|
-
"parent_models_class",
|
|
122
|
-
"children_column_name",
|
|
123
|
-
"descendents_column_name",
|
|
124
|
-
"ancestors_column_name",
|
|
125
|
-
"load_relatives_strategy",
|
|
126
|
-
]
|
|
127
|
-
|
|
128
|
-
def __init__(self, di):
|
|
129
|
-
super().__init__(di)
|
|
130
|
-
|
|
131
|
-
def _check_configuration(self, configuration):
|
|
132
|
-
# our parent class is the BelongsTo which needs to know the parent model class.
|
|
133
|
-
# with a category tree, we _are_ our own parent model class, so no need to ask for it.
|
|
134
|
-
super()._check_configuration(
|
|
135
|
-
{
|
|
136
|
-
**configuration,
|
|
137
|
-
"parent_models_class": configuration.get("parent_models_class", self.model_class),
|
|
138
|
-
}
|
|
139
|
-
)
|
|
140
|
-
self.validate_models_class(
|
|
141
|
-
configuration["tree_models_class"],
|
|
142
|
-
config_name="tree_models_class",
|
|
143
|
-
)
|
|
144
|
-
load_relatives_strategy = configuration.get("load_relatives_strategy", None)
|
|
145
|
-
if load_relatives_strategy and load_relatives_strategy not in ["join", "where_in", "individual"]:
|
|
146
|
-
raise ValueError(
|
|
147
|
-
f"Configuration error for category_tree column '{self.name} in model class '{self.model_class.__name__}': load_relatives_strategy must be one of ['join', 'where_in', or 'individual']"
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
def _finalize_configuration(self, configuration):
|
|
151
|
-
return {
|
|
152
|
-
**super()._finalize_configuration(
|
|
153
|
-
{
|
|
154
|
-
**configuration,
|
|
155
|
-
"parent_models_class": configuration.get("parent_models_class", self.model_class),
|
|
156
|
-
}
|
|
157
|
-
),
|
|
158
|
-
**{
|
|
159
|
-
"tree_parent_id_column_name": configuration.get("tree_parent_id_column_name", "parent_id"),
|
|
160
|
-
"tree_child_id_column_name": configuration.get("tree_child_id_column_name", "child_id"),
|
|
161
|
-
"tree_is_parent_column_name": configuration.get("tree_is_parent_column_name", "is_parent"),
|
|
162
|
-
"tree_level_column_name": configuration.get("tree_level_column_name", "level"),
|
|
163
|
-
"max_iterations": configuration.get("max_iterations", 100),
|
|
164
|
-
"children_column_name": configuration.get("children_column_name", "children"),
|
|
165
|
-
"descendents_column_name": configuration.get("descendents_column_name", "descendents"),
|
|
166
|
-
"ancestors_column_name": configuration.get("ancestors_column_name", "ancestors"),
|
|
167
|
-
"load_relatives_strategy": configuration.get("load_relatives_strategy", "join"),
|
|
168
|
-
},
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
@property
|
|
172
|
-
def tree_models(self):
|
|
173
|
-
return self.di.build(self.config("tree_models_class"), cache=True)
|
|
174
|
-
|
|
175
|
-
def post_save(self, data, model, id):
|
|
176
|
-
if not model.is_changing(self.name, data):
|
|
177
|
-
return data
|
|
178
|
-
|
|
179
|
-
self.update_tree_table(model, id, model.latest(self.name, data))
|
|
180
|
-
return data
|
|
181
|
-
|
|
182
|
-
def force_tree_update(self, model):
|
|
183
|
-
self.update_tree_table(model, model.get(self.id_column_name), model.__getattr__(self.name))
|
|
184
|
-
|
|
185
|
-
def update_tree_table(self, model, child_id, direct_parent_id):
|
|
186
|
-
tree_models = self.tree_models
|
|
187
|
-
parent_models = self.parent_models
|
|
188
|
-
model_column_name = self.config("model_column_name")
|
|
189
|
-
tree_parent_id_column_name = self.config("tree_parent_id_column_name")
|
|
190
|
-
tree_child_id_column_name = self.config("tree_child_id_column_name")
|
|
191
|
-
tree_is_parent_column_name = self.config("tree_is_parent_column_name")
|
|
192
|
-
tree_level_column_name = self.config("tree_level_column_name")
|
|
193
|
-
max_iterations = self.config("max_iterations")
|
|
194
|
-
|
|
195
|
-
# we're going to be lazy and just delete the data for the current record in the tree table,
|
|
196
|
-
# and then re-insert everything (but we can skip this if creating a new record)
|
|
197
|
-
if model.exists:
|
|
198
|
-
for tree in tree_models.where(f"{tree_child_id_column_name}={child_id}"):
|
|
199
|
-
tree.delete()
|
|
200
|
-
|
|
201
|
-
# if we are a root category then we don't have a tree
|
|
202
|
-
if not direct_parent_id:
|
|
203
|
-
return
|
|
204
|
-
|
|
205
|
-
is_root = False
|
|
206
|
-
id_column_name = parent_models.id_column_name
|
|
207
|
-
next_parent = parent_models.find(f"{id_column_name}={direct_parent_id}")
|
|
208
|
-
tree = []
|
|
209
|
-
c = 0
|
|
210
|
-
while not is_root:
|
|
211
|
-
c += 1
|
|
212
|
-
if c > max_iterations:
|
|
213
|
-
self._circular(max_iterations)
|
|
214
|
-
|
|
215
|
-
tree.append(next_parent.__getattr__(next_parent.id_column_name))
|
|
216
|
-
if not next_parent.__getattr__(self.name):
|
|
217
|
-
is_root = True
|
|
218
|
-
else:
|
|
219
|
-
next_next_parent_id = next_parent.__getattr__(self.name)
|
|
220
|
-
next_parent = parent_models.find(f"{id_column_name}={next_next_parent_id}")
|
|
221
|
-
|
|
222
|
-
tree.reverse()
|
|
223
|
-
for index, parent_id in enumerate(tree):
|
|
224
|
-
tree_models.create(
|
|
225
|
-
{
|
|
226
|
-
tree_parent_id_column_name: parent_id,
|
|
227
|
-
tree_child_id_column_name: child_id,
|
|
228
|
-
tree_is_parent_column_name: 1 if parent_id == direct_parent_id else 0,
|
|
229
|
-
tree_level_column_name: index,
|
|
230
|
-
}
|
|
231
|
-
)
|
|
232
|
-
|
|
233
|
-
def _circular(self, max_iterations):
|
|
234
|
-
raise ValueError(
|
|
235
|
-
f"Error for column '{self.name}' for model class '{self.model_class.__name__}': "
|
|
236
|
-
+ f"I've climbed through {max_iterations} parents and haven't found the root yet."
|
|
237
|
-
+ "You may have accidentally created a circular cateogry tree. If not, and your category tree "
|
|
238
|
-
+ "really _is_ that deep, then adjust the 'max_iterations' configuration for this column accordingly. "
|
|
239
|
-
)
|
|
240
|
-
|
|
241
|
-
def can_provide(self, column_name):
|
|
242
|
-
return column_name in [
|
|
243
|
-
self.config("model_column_name"),
|
|
244
|
-
self.config("children_column_name"),
|
|
245
|
-
self.config("descendents_column_name"),
|
|
246
|
-
self.config("ancestors_column_name"),
|
|
247
|
-
]
|
|
248
|
-
|
|
249
|
-
def provide(self, data, column_name):
|
|
250
|
-
if column_name == self.config("model_column_name"):
|
|
251
|
-
return super().provide(data, column_name)
|
|
252
|
-
|
|
253
|
-
if column_name == self.config("children_column_name"):
|
|
254
|
-
return self.relatives(data)
|
|
255
|
-
|
|
256
|
-
if column_name == self.config("descendents_column_name"):
|
|
257
|
-
return self.relatives(data, include_all=True)
|
|
258
|
-
|
|
259
|
-
if column_name == self.config("ancestors_column_name"):
|
|
260
|
-
return self.relatives(data, find_parents=True, include_all=True)
|
|
261
|
-
|
|
262
|
-
def relatives(self, data, include_all=False, find_parents=False):
|
|
263
|
-
id_column_name = self.model_class.id_column_name
|
|
264
|
-
model_id = data[self.model_class.id_column_name]
|
|
265
|
-
model_table_name = self.model_class.table_name()
|
|
266
|
-
tree_table_name = self.config("tree_models_class").table_name()
|
|
267
|
-
parent_id_column_name = self.config("tree_parent_id_column_name")
|
|
268
|
-
child_id_column_name = self.config("tree_child_id_column_name")
|
|
269
|
-
is_parent_column_name = self.config("tree_is_parent_column_name")
|
|
270
|
-
level_column_name = self.config("tree_level_column_name")
|
|
271
|
-
|
|
272
|
-
if find_parents:
|
|
273
|
-
join_on = parent_id_column_name
|
|
274
|
-
search_on = child_id_column_name
|
|
275
|
-
else:
|
|
276
|
-
join_on = child_id_column_name
|
|
277
|
-
search_on = parent_id_column_name
|
|
278
|
-
|
|
279
|
-
# if we can join then use a join.
|
|
280
|
-
if self.config("load_relatives_strategy") == "join":
|
|
281
|
-
relatives = self.parent_models.join(
|
|
282
|
-
f"{tree_table_name} as tree on tree.{join_on}={model_table_name}.{id_column_name}"
|
|
283
|
-
)
|
|
284
|
-
relatives = relatives.where(f"tree.{search_on}={model_id}")
|
|
285
|
-
if not include_all:
|
|
286
|
-
relatives = relatives.where(f"tree.{is_parent_column_name}=1")
|
|
287
|
-
if find_parents:
|
|
288
|
-
relatives = relatives.sort_by(level_column_name, "asc")
|
|
289
|
-
return relatives
|
|
290
|
-
|
|
291
|
-
# joins only work for SQL-like backends. Otherwise, we have to pull out our list of ids
|
|
292
|
-
branches = self.tree_models.where(f"{search_on}={model_id}")
|
|
293
|
-
if not include_all:
|
|
294
|
-
branches = branches.where(f"{is_parent_column_name}=1")
|
|
295
|
-
if find_parents:
|
|
296
|
-
branches = branches.sort_by(level_column_name, "asc")
|
|
297
|
-
ids = [str(branch.get(join_on)) for branch in branches]
|
|
298
|
-
|
|
299
|
-
# Can we search with a WHERE IN() clause? If the backend supports it, it is probably faster
|
|
300
|
-
if self.config("load_relatives_strategy") == "where_in":
|
|
301
|
-
return self.parent_models.where(f"{id_column_name} IN ('" + "','".join(ids) + "')")
|
|
302
|
-
|
|
303
|
-
# otherwise we have to load each model individually which is SLOW....
|
|
304
|
-
return [self.parent_models.find(f"{id_column_name}={id}") for id in ids]
|