wbfdm 2.2.1__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of wbfdm might be problematic. Click here for more details.
- wbfdm/__init__.py +2 -0
- wbfdm/admin/__init__.py +42 -0
- wbfdm/admin/classifications.py +39 -0
- wbfdm/admin/esg.py +23 -0
- wbfdm/admin/exchanges.py +53 -0
- wbfdm/admin/instrument_lists.py +23 -0
- wbfdm/admin/instrument_prices.py +62 -0
- wbfdm/admin/instrument_requests.py +33 -0
- wbfdm/admin/instruments.py +117 -0
- wbfdm/admin/instruments_relationships.py +25 -0
- wbfdm/admin/options.py +101 -0
- wbfdm/analysis/__init__.py +2 -0
- wbfdm/analysis/esg/__init__.py +0 -0
- wbfdm/analysis/esg/enums.py +82 -0
- wbfdm/analysis/esg/esg_analysis.py +217 -0
- wbfdm/analysis/esg/utils.py +13 -0
- wbfdm/analysis/financial_analysis/__init__.py +1 -0
- wbfdm/analysis/financial_analysis/financial_metric_analysis.py +88 -0
- wbfdm/analysis/financial_analysis/financial_ratio_analysis.py +125 -0
- wbfdm/analysis/financial_analysis/financial_statistics_analysis.py +271 -0
- wbfdm/analysis/financial_analysis/statement_with_estimates.py +558 -0
- wbfdm/analysis/financial_analysis/utils.py +316 -0
- wbfdm/analysis/technical_analysis/__init__.py +1 -0
- wbfdm/analysis/technical_analysis/technical_analysis.py +138 -0
- wbfdm/analysis/technical_analysis/traces.py +165 -0
- wbfdm/analysis/utils.py +32 -0
- wbfdm/apps.py +14 -0
- wbfdm/contrib/__init__.py +0 -0
- wbfdm/contrib/dsws/__init__.py +0 -0
- wbfdm/contrib/dsws/client.py +285 -0
- wbfdm/contrib/internal/__init__.py +0 -0
- wbfdm/contrib/internal/dataloaders/__init__.py +0 -0
- wbfdm/contrib/internal/dataloaders/market_data.py +87 -0
- wbfdm/contrib/metric/__init__.py +0 -0
- wbfdm/contrib/metric/admin/__init__.py +2 -0
- wbfdm/contrib/metric/admin/instruments.py +12 -0
- wbfdm/contrib/metric/admin/metrics.py +43 -0
- wbfdm/contrib/metric/apps.py +10 -0
- wbfdm/contrib/metric/backends/__init__.py +2 -0
- wbfdm/contrib/metric/backends/base.py +159 -0
- wbfdm/contrib/metric/backends/performances.py +265 -0
- wbfdm/contrib/metric/backends/statistics.py +182 -0
- wbfdm/contrib/metric/decorators.py +14 -0
- wbfdm/contrib/metric/dispatch.py +23 -0
- wbfdm/contrib/metric/dto.py +88 -0
- wbfdm/contrib/metric/exceptions.py +6 -0
- wbfdm/contrib/metric/factories.py +33 -0
- wbfdm/contrib/metric/filters.py +28 -0
- wbfdm/contrib/metric/migrations/0001_initial.py +88 -0
- wbfdm/contrib/metric/migrations/0002_remove_instrumentmetric_unique_instrument_metric_and_more.py +26 -0
- wbfdm/contrib/metric/migrations/__init__.py +0 -0
- wbfdm/contrib/metric/models.py +180 -0
- wbfdm/contrib/metric/orchestrators.py +94 -0
- wbfdm/contrib/metric/registry.py +80 -0
- wbfdm/contrib/metric/serializers.py +44 -0
- wbfdm/contrib/metric/tasks.py +27 -0
- wbfdm/contrib/metric/tests/__init__.py +0 -0
- wbfdm/contrib/metric/tests/backends/__init__.py +0 -0
- wbfdm/contrib/metric/tests/backends/test_performances.py +152 -0
- wbfdm/contrib/metric/tests/backends/test_statistics.py +48 -0
- wbfdm/contrib/metric/tests/conftest.py +92 -0
- wbfdm/contrib/metric/tests/test_dto.py +73 -0
- wbfdm/contrib/metric/tests/test_models.py +72 -0
- wbfdm/contrib/metric/tests/test_tasks.py +24 -0
- wbfdm/contrib/metric/tests/test_viewsets.py +79 -0
- wbfdm/contrib/metric/urls.py +19 -0
- wbfdm/contrib/metric/viewsets/__init__.py +1 -0
- wbfdm/contrib/metric/viewsets/configs/__init__.py +1 -0
- wbfdm/contrib/metric/viewsets/configs/display.py +92 -0
- wbfdm/contrib/metric/viewsets/configs/menus.py +11 -0
- wbfdm/contrib/metric/viewsets/configs/utils.py +137 -0
- wbfdm/contrib/metric/viewsets/mixins.py +245 -0
- wbfdm/contrib/metric/viewsets/viewsets.py +40 -0
- wbfdm/contrib/msci/__init__.py +0 -0
- wbfdm/contrib/msci/client.py +92 -0
- wbfdm/contrib/msci/dataloaders/__init__.py +0 -0
- wbfdm/contrib/msci/dataloaders/esg.py +87 -0
- wbfdm/contrib/msci/dataloaders/esg_controversies.py +81 -0
- wbfdm/contrib/msci/sync.py +58 -0
- wbfdm/contrib/msci/tests/__init__.py +0 -0
- wbfdm/contrib/msci/tests/conftest.py +1 -0
- wbfdm/contrib/msci/tests/test_client.py +70 -0
- wbfdm/contrib/qa/__init__.py +0 -0
- wbfdm/contrib/qa/apps.py +22 -0
- wbfdm/contrib/qa/database_routers.py +25 -0
- wbfdm/contrib/qa/dataloaders/__init__.py +0 -0
- wbfdm/contrib/qa/dataloaders/adjustments.py +56 -0
- wbfdm/contrib/qa/dataloaders/corporate_actions.py +59 -0
- wbfdm/contrib/qa/dataloaders/financials.py +83 -0
- wbfdm/contrib/qa/dataloaders/market_data.py +117 -0
- wbfdm/contrib/qa/dataloaders/officers.py +59 -0
- wbfdm/contrib/qa/dataloaders/reporting_dates.py +67 -0
- wbfdm/contrib/qa/dataloaders/statements.py +267 -0
- wbfdm/contrib/qa/tasks.py +0 -0
- wbfdm/dataloaders/__init__.py +0 -0
- wbfdm/dataloaders/cache.py +129 -0
- wbfdm/dataloaders/protocols.py +112 -0
- wbfdm/dataloaders/proxies.py +201 -0
- wbfdm/dataloaders/types.py +209 -0
- wbfdm/dynamic_preferences_registry.py +45 -0
- wbfdm/enums.py +657 -0
- wbfdm/factories/__init__.py +13 -0
- wbfdm/factories/classifications.py +56 -0
- wbfdm/factories/controversies.py +27 -0
- wbfdm/factories/exchanges.py +21 -0
- wbfdm/factories/instrument_list.py +22 -0
- wbfdm/factories/instrument_prices.py +79 -0
- wbfdm/factories/instruments.py +63 -0
- wbfdm/factories/instruments_relationships.py +31 -0
- wbfdm/factories/options.py +66 -0
- wbfdm/figures/__init__.py +1 -0
- wbfdm/figures/financials/__init__.py +1 -0
- wbfdm/figures/financials/financial_analysis_charts.py +469 -0
- wbfdm/figures/financials/financials_charts.py +711 -0
- wbfdm/filters/__init__.py +31 -0
- wbfdm/filters/classifications.py +100 -0
- wbfdm/filters/exchanges.py +22 -0
- wbfdm/filters/financials.py +95 -0
- wbfdm/filters/financials_analysis.py +119 -0
- wbfdm/filters/instrument_prices.py +112 -0
- wbfdm/filters/instruments.py +198 -0
- wbfdm/filters/utils.py +44 -0
- wbfdm/import_export/__init__.py +0 -0
- wbfdm/import_export/backends/__init__.py +0 -0
- wbfdm/import_export/backends/cbinsights/__init__.py +2 -0
- wbfdm/import_export/backends/cbinsights/deals.py +44 -0
- wbfdm/import_export/backends/cbinsights/equities.py +41 -0
- wbfdm/import_export/backends/cbinsights/mixin.py +15 -0
- wbfdm/import_export/backends/cbinsights/utils/__init__.py +0 -0
- wbfdm/import_export/backends/cbinsights/utils/classifications.py +4150 -0
- wbfdm/import_export/backends/cbinsights/utils/client.py +217 -0
- wbfdm/import_export/backends/refinitiv/__init__.py +5 -0
- wbfdm/import_export/backends/refinitiv/daily_fundamental.py +36 -0
- wbfdm/import_export/backends/refinitiv/fiscal_period.py +63 -0
- wbfdm/import_export/backends/refinitiv/forecast.py +178 -0
- wbfdm/import_export/backends/refinitiv/fundamental.py +103 -0
- wbfdm/import_export/backends/refinitiv/geographic_segment.py +32 -0
- wbfdm/import_export/backends/refinitiv/instrument.py +55 -0
- wbfdm/import_export/backends/refinitiv/instrument_price.py +77 -0
- wbfdm/import_export/backends/refinitiv/mixin.py +29 -0
- wbfdm/import_export/backends/refinitiv/utils/__init__.py +1 -0
- wbfdm/import_export/backends/refinitiv/utils/controller.py +182 -0
- wbfdm/import_export/handlers/__init__.py +0 -0
- wbfdm/import_export/handlers/instrument.py +253 -0
- wbfdm/import_export/handlers/instrument_list.py +101 -0
- wbfdm/import_export/handlers/instrument_price.py +71 -0
- wbfdm/import_export/handlers/option.py +54 -0
- wbfdm/import_export/handlers/private_equities.py +49 -0
- wbfdm/import_export/parsers/__init__.py +0 -0
- wbfdm/import_export/parsers/cbinsights/__init__.py +0 -0
- wbfdm/import_export/parsers/cbinsights/deals.py +39 -0
- wbfdm/import_export/parsers/cbinsights/equities.py +56 -0
- wbfdm/import_export/parsers/cbinsights/fundamentals.py +45 -0
- wbfdm/import_export/parsers/refinitiv/__init__.py +0 -0
- wbfdm/import_export/parsers/refinitiv/daily_fundamental.py +7 -0
- wbfdm/import_export/parsers/refinitiv/forecast.py +7 -0
- wbfdm/import_export/parsers/refinitiv/fundamental.py +9 -0
- wbfdm/import_export/parsers/refinitiv/geographic_segment.py +7 -0
- wbfdm/import_export/parsers/refinitiv/instrument.py +75 -0
- wbfdm/import_export/parsers/refinitiv/instrument_price.py +26 -0
- wbfdm/import_export/parsers/refinitiv/utils.py +96 -0
- wbfdm/import_export/resources/__init__.py +0 -0
- wbfdm/import_export/resources/classification.py +23 -0
- wbfdm/import_export/resources/instrument_prices.py +33 -0
- wbfdm/import_export/resources/instruments.py +176 -0
- wbfdm/jinja2.py +7 -0
- wbfdm/management/__init__.py +30 -0
- wbfdm/menu.py +11 -0
- wbfdm/migrations/0001_initial.py +71 -0
- wbfdm/migrations/0002_rename_statements_instrumentlookup_financials_and_more.py +144 -0
- wbfdm/migrations/0003_instrument_estimate_backend_and_more.py +34 -0
- wbfdm/migrations/0004_rename_financials_instrumentlookup_statements_and_more.py +86 -0
- wbfdm/migrations/0005_instrument_corporate_action_backend.py +29 -0
- wbfdm/migrations/0006_instrument_officer_backend.py +29 -0
- wbfdm/migrations/0007_instrument_country_instrument_currency_and_more.py +117 -0
- wbfdm/migrations/0008_controversy.py +75 -0
- wbfdm/migrations/0009_alter_controversy_flag_alter_controversy_initiated_and_more.py +85 -0
- wbfdm/migrations/0010_classification_classificationgroup_deal_exchange_and_more.py +1299 -0
- wbfdm/migrations/0011_delete_instrumentlookup_instrument_corporate_actions_and_more.py +169 -0
- wbfdm/migrations/0012_instrumentprice_created_instrumentprice_modified.py +564 -0
- wbfdm/migrations/0013_instrument_is_investable_universe_and_more.py +199 -0
- wbfdm/migrations/0014_alter_controversy_instrument.py +22 -0
- wbfdm/migrations/0015_instrument_instrument_investible_index.py +16 -0
- wbfdm/migrations/0016_instrumenttype_name_repr.py +18 -0
- wbfdm/migrations/0017_instrument_instrument_security_index.py +16 -0
- wbfdm/migrations/0018_instrument_instrument_level_index.py +20 -0
- wbfdm/migrations/0019_alter_controversy_source.py +17 -0
- wbfdm/migrations/0020_optionaggregate_option_and_more.py +249 -0
- wbfdm/migrations/0021_delete_instrumentdailystatistics.py +15 -0
- wbfdm/migrations/0022_instrument_cusip_option_open_interest_20d_and_more.py +91 -0
- wbfdm/migrations/0023_instrument_unique_ric_instrument_unique_rmc_and_more.py +53 -0
- wbfdm/migrations/0024_option_open_interest_10d_option_volume_10d_and_more.py +36 -0
- wbfdm/migrations/0025_instrument_is_primary_and_more.py +29 -0
- wbfdm/migrations/0026_instrument_is_cash_equivalent.py +30 -0
- wbfdm/migrations/0027_remove_instrument_unique_ric_and_more.py +100 -0
- wbfdm/migrations/__init__.py +0 -0
- wbfdm/models/__init__.py +4 -0
- wbfdm/models/esg/__init__.py +1 -0
- wbfdm/models/esg/controversies.py +81 -0
- wbfdm/models/exchanges/__init__.py +1 -0
- wbfdm/models/exchanges/exchanges.py +223 -0
- wbfdm/models/fields.py +117 -0
- wbfdm/models/fk_fields.py +403 -0
- wbfdm/models/indicators.py +0 -0
- wbfdm/models/instruments/__init__.py +19 -0
- wbfdm/models/instruments/classifications.py +265 -0
- wbfdm/models/instruments/instrument_lists.py +120 -0
- wbfdm/models/instruments/instrument_prices.py +540 -0
- wbfdm/models/instruments/instrument_relationships.py +251 -0
- wbfdm/models/instruments/instrument_requests.py +196 -0
- wbfdm/models/instruments/instruments.py +991 -0
- wbfdm/models/instruments/llm/__init__.py +1 -0
- wbfdm/models/instruments/llm/create_instrument_news_relationships.py +78 -0
- wbfdm/models/instruments/mixin/__init__.py +0 -0
- wbfdm/models/instruments/mixin/financials_computed.py +804 -0
- wbfdm/models/instruments/mixin/financials_serializer_fields.py +1407 -0
- wbfdm/models/instruments/mixin/instruments.py +294 -0
- wbfdm/models/instruments/options.py +225 -0
- wbfdm/models/instruments/private_equities.py +59 -0
- wbfdm/models/instruments/querysets.py +73 -0
- wbfdm/models/instruments/utils.py +41 -0
- wbfdm/preferences.py +21 -0
- wbfdm/serializers/__init__.py +4 -0
- wbfdm/serializers/esg.py +36 -0
- wbfdm/serializers/exchanges.py +39 -0
- wbfdm/serializers/instruments/__init__.py +37 -0
- wbfdm/serializers/instruments/classifications.py +139 -0
- wbfdm/serializers/instruments/instrument_lists.py +61 -0
- wbfdm/serializers/instruments/instrument_prices.py +73 -0
- wbfdm/serializers/instruments/instrument_relationships.py +170 -0
- wbfdm/serializers/instruments/instrument_requests.py +61 -0
- wbfdm/serializers/instruments/instruments.py +274 -0
- wbfdm/serializers/instruments/mixins.py +104 -0
- wbfdm/serializers/officers.py +20 -0
- wbfdm/signals.py +7 -0
- wbfdm/sync/__init__.py +0 -0
- wbfdm/sync/abstract.py +31 -0
- wbfdm/sync/runner.py +22 -0
- wbfdm/tasks.py +69 -0
- wbfdm/tests/__init__.py +0 -0
- wbfdm/tests/analysis/__init__.py +0 -0
- wbfdm/tests/analysis/financial_analysis/__init__.py +0 -0
- wbfdm/tests/analysis/financial_analysis/test_statement_with_estimates.py +392 -0
- wbfdm/tests/analysis/financial_analysis/test_utils.py +322 -0
- wbfdm/tests/analysis/test_esg.py +159 -0
- wbfdm/tests/conftest.py +92 -0
- wbfdm/tests/dataloaders/__init__.py +0 -0
- wbfdm/tests/dataloaders/test_cache.py +73 -0
- wbfdm/tests/models/__init__.py +0 -0
- wbfdm/tests/models/test_classifications.py +99 -0
- wbfdm/tests/models/test_exchanges.py +7 -0
- wbfdm/tests/models/test_instrument_list.py +117 -0
- wbfdm/tests/models/test_instrument_prices.py +306 -0
- wbfdm/tests/models/test_instruments.py +202 -0
- wbfdm/tests/models/test_merge.py +99 -0
- wbfdm/tests/models/test_options.py +69 -0
- wbfdm/tests/test_tasks.py +6 -0
- wbfdm/tests/tests.py +10 -0
- wbfdm/urls.py +222 -0
- wbfdm/utils.py +54 -0
- wbfdm/viewsets/__init__.py +10 -0
- wbfdm/viewsets/configs/__init__.py +5 -0
- wbfdm/viewsets/configs/buttons/__init__.py +8 -0
- wbfdm/viewsets/configs/buttons/classifications.py +23 -0
- wbfdm/viewsets/configs/buttons/exchanges.py +9 -0
- wbfdm/viewsets/configs/buttons/instrument_prices.py +49 -0
- wbfdm/viewsets/configs/buttons/instruments.py +283 -0
- wbfdm/viewsets/configs/display/__init__.py +22 -0
- wbfdm/viewsets/configs/display/classifications.py +138 -0
- wbfdm/viewsets/configs/display/esg.py +75 -0
- wbfdm/viewsets/configs/display/exchanges.py +42 -0
- wbfdm/viewsets/configs/display/instrument_lists.py +137 -0
- wbfdm/viewsets/configs/display/instrument_prices.py +199 -0
- wbfdm/viewsets/configs/display/instrument_requests.py +116 -0
- wbfdm/viewsets/configs/display/instruments.py +618 -0
- wbfdm/viewsets/configs/display/instruments_relationships.py +65 -0
- wbfdm/viewsets/configs/display/monthly_performances.py +72 -0
- wbfdm/viewsets/configs/display/officers.py +16 -0
- wbfdm/viewsets/configs/display/prices.py +21 -0
- wbfdm/viewsets/configs/display/statement_with_estimates.py +101 -0
- wbfdm/viewsets/configs/display/statements.py +48 -0
- wbfdm/viewsets/configs/endpoints/__init__.py +41 -0
- wbfdm/viewsets/configs/endpoints/classifications.py +87 -0
- wbfdm/viewsets/configs/endpoints/esg.py +20 -0
- wbfdm/viewsets/configs/endpoints/exchanges.py +6 -0
- wbfdm/viewsets/configs/endpoints/financials_analysis.py +65 -0
- wbfdm/viewsets/configs/endpoints/instrument_lists.py +38 -0
- wbfdm/viewsets/configs/endpoints/instrument_prices.py +51 -0
- wbfdm/viewsets/configs/endpoints/instrument_requests.py +20 -0
- wbfdm/viewsets/configs/endpoints/instruments.py +13 -0
- wbfdm/viewsets/configs/endpoints/instruments_relationships.py +31 -0
- wbfdm/viewsets/configs/endpoints/statements.py +6 -0
- wbfdm/viewsets/configs/menus/__init__.py +9 -0
- wbfdm/viewsets/configs/menus/classifications.py +19 -0
- wbfdm/viewsets/configs/menus/exchanges.py +10 -0
- wbfdm/viewsets/configs/menus/instrument_lists.py +10 -0
- wbfdm/viewsets/configs/menus/instruments.py +20 -0
- wbfdm/viewsets/configs/menus/instruments_relationships.py +33 -0
- wbfdm/viewsets/configs/titles/__init__.py +42 -0
- wbfdm/viewsets/configs/titles/classifications.py +79 -0
- wbfdm/viewsets/configs/titles/esg.py +11 -0
- wbfdm/viewsets/configs/titles/exchanges.py +12 -0
- wbfdm/viewsets/configs/titles/financial_ratio_analysis.py +6 -0
- wbfdm/viewsets/configs/titles/financials_analysis.py +50 -0
- wbfdm/viewsets/configs/titles/instrument_prices.py +50 -0
- wbfdm/viewsets/configs/titles/instrument_requests.py +16 -0
- wbfdm/viewsets/configs/titles/instruments.py +31 -0
- wbfdm/viewsets/configs/titles/instruments_relationships.py +21 -0
- wbfdm/viewsets/configs/titles/market_data.py +13 -0
- wbfdm/viewsets/configs/titles/prices.py +15 -0
- wbfdm/viewsets/configs/titles/statement_with_estimates.py +10 -0
- wbfdm/viewsets/esg.py +72 -0
- wbfdm/viewsets/exchanges.py +63 -0
- wbfdm/viewsets/financial_analysis/__init__.py +3 -0
- wbfdm/viewsets/financial_analysis/financial_metric_analysis.py +85 -0
- wbfdm/viewsets/financial_analysis/financial_ratio_analysis.py +85 -0
- wbfdm/viewsets/financial_analysis/statement_with_estimates.py +145 -0
- wbfdm/viewsets/instruments/__init__.py +80 -0
- wbfdm/viewsets/instruments/classifications.py +279 -0
- wbfdm/viewsets/instruments/financials_analysis.py +614 -0
- wbfdm/viewsets/instruments/instrument_lists.py +77 -0
- wbfdm/viewsets/instruments/instrument_prices.py +542 -0
- wbfdm/viewsets/instruments/instrument_requests.py +51 -0
- wbfdm/viewsets/instruments/instruments.py +106 -0
- wbfdm/viewsets/instruments/instruments_relationships.py +235 -0
- wbfdm/viewsets/instruments/utils.py +27 -0
- wbfdm/viewsets/market_data.py +172 -0
- wbfdm/viewsets/mixins.py +9 -0
- wbfdm/viewsets/officers.py +27 -0
- wbfdm/viewsets/prices.py +62 -0
- wbfdm/viewsets/statements/__init__.py +1 -0
- wbfdm/viewsets/statements/statements.py +100 -0
- wbfdm/viewsets/technical_analysis/__init__.py +1 -0
- wbfdm/viewsets/technical_analysis/monthly_performances.py +93 -0
- wbfdm-2.2.1.dist-info/METADATA +15 -0
- wbfdm-2.2.1.dist-info/RECORD +337 -0
- wbfdm-2.2.1.dist-info/WHEEL +5 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
import logging
|
|
4
|
+
from collections import OrderedDict
|
|
5
|
+
from functools import wraps
|
|
6
|
+
|
|
7
|
+
from django.core import checks
|
|
8
|
+
from django.core.exceptions import FieldDoesNotExist
|
|
9
|
+
from django.db.models import ForeignObjectRel
|
|
10
|
+
from django.db.models.fields.related import ForeignObject
|
|
11
|
+
from django.db.models.fields.related_descriptors import (
|
|
12
|
+
ForwardManyToOneDescriptor,
|
|
13
|
+
ReverseOneToOneDescriptor,
|
|
14
|
+
)
|
|
15
|
+
from django.db.models.sql.where import AND, WhereNode
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_cached_value(instance, descriptor, default=None):
|
|
19
|
+
return descriptor.field.get_cached_value(instance, default=default)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def set_cached_value_by_descriptor(instance, descriptor, value):
|
|
23
|
+
descriptor.field.set_cached_value(instance, value)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def set_cached_value_by_field(instance, field, value):
|
|
27
|
+
field.set_cached_value(instance, value)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# TODO: Figure out if nullable fields is a proper error
|
|
34
|
+
class CompositeForwardManyToOneDescriptor(ForwardManyToOneDescriptor):
|
|
35
|
+
def __set__(self, instance, value):
|
|
36
|
+
if value is not None or not self.field.nullable_fields:
|
|
37
|
+
super().__set__(instance, value)
|
|
38
|
+
else:
|
|
39
|
+
# we set only the asked fields to None, not all field as the default ForwardManyToOneDescriptor will
|
|
40
|
+
|
|
41
|
+
# ### taken from original ForwardManyToOneDescriptor
|
|
42
|
+
# Look up the previously-related object, which may still be available
|
|
43
|
+
# since we've not yet cleared out the related field.
|
|
44
|
+
# Use the cache directly, instead of the accessor; if we haven't
|
|
45
|
+
# populated the cache, then we don't care - we're only accessing
|
|
46
|
+
# the object to invalidate the accessor cache, so there's no
|
|
47
|
+
# need to populate the cache just to expire it again.
|
|
48
|
+
related = get_cached_value(instance, self, None)
|
|
49
|
+
|
|
50
|
+
# If we've got an old related object, we need to clear out its
|
|
51
|
+
# cache. This cache also might not exist if the related object
|
|
52
|
+
# hasn't been accessed yet.
|
|
53
|
+
if related is not None:
|
|
54
|
+
related_field = self.field.remote_field
|
|
55
|
+
set_cached_value_by_field(related, related_field, None)
|
|
56
|
+
|
|
57
|
+
# ##### only original part
|
|
58
|
+
for lh_field_name, none_value in self.field.nullable_fields.items():
|
|
59
|
+
setattr(instance, lh_field_name, none_value)
|
|
60
|
+
|
|
61
|
+
# Set the related instance cache used by __get__ to avoid a SQL query
|
|
62
|
+
# when accessing the attribute we just set.
|
|
63
|
+
set_cached_value_by_descriptor(instance, self, None)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class CompositeForeignKey(ForeignObject):
|
|
67
|
+
requires_unique_target = False
|
|
68
|
+
auto_created = False
|
|
69
|
+
empty_strings_allowed = False
|
|
70
|
+
|
|
71
|
+
many_to_many = False
|
|
72
|
+
many_to_one = False
|
|
73
|
+
one_to_many = True
|
|
74
|
+
one_to_one = False
|
|
75
|
+
|
|
76
|
+
rel_class = ForeignObjectRel
|
|
77
|
+
|
|
78
|
+
def __init__(self, to, **kwargs):
|
|
79
|
+
"""
|
|
80
|
+
create the ForeignObject, but use the to_fields as a dict which will later used as form_fields and to_fields
|
|
81
|
+
"""
|
|
82
|
+
to_fields = kwargs["to_fields"]
|
|
83
|
+
self.null_if_equal = kwargs.pop("null_if_equal", [])
|
|
84
|
+
nullable_fields = kwargs.pop("nullable_fields", {})
|
|
85
|
+
if not isinstance(nullable_fields, dict):
|
|
86
|
+
nullable_fields = {v: None for v in nullable_fields}
|
|
87
|
+
self.nullable_fields = nullable_fields
|
|
88
|
+
|
|
89
|
+
# a list of tuple : (fieldnaem, value) . if fielname = value, then the field react as if fieldnaem_id = None
|
|
90
|
+
self._raw_fields = self.compute_to_fields(to_fields)
|
|
91
|
+
# hiro nakamura should have said «very bad guy. you are vilain»
|
|
92
|
+
if "on_delete" in kwargs:
|
|
93
|
+
kwargs["on_delete"] = self.override_on_delete(kwargs["on_delete"])
|
|
94
|
+
|
|
95
|
+
kwargs["to_fields"], kwargs["from_fields"] = zip(
|
|
96
|
+
*((k, v.value) for k, v in self._raw_fields.items() if v.is_local_field)
|
|
97
|
+
)
|
|
98
|
+
super().__init__(to, **kwargs)
|
|
99
|
+
|
|
100
|
+
def override_on_delete(self, original):
|
|
101
|
+
@wraps(original)
|
|
102
|
+
def wrapper(collector, field, sub_objs, using):
|
|
103
|
+
res = original(collector, field, sub_objs, using)
|
|
104
|
+
# we make something nasty : we update the collector to
|
|
105
|
+
# skip the local field which does not have a dbcolumn
|
|
106
|
+
try:
|
|
107
|
+
del collector.field_updates[self.model][(self, None)]
|
|
108
|
+
except KeyError:
|
|
109
|
+
pass
|
|
110
|
+
return res
|
|
111
|
+
|
|
112
|
+
wrapper._original_fn = original
|
|
113
|
+
|
|
114
|
+
return wrapper
|
|
115
|
+
|
|
116
|
+
def check(self, **kwargs):
|
|
117
|
+
errors = super().check(**kwargs)
|
|
118
|
+
errors.extend(self._check_null_with_nullifequal())
|
|
119
|
+
errors.extend(self._check_nullifequal_fields_exists())
|
|
120
|
+
errors.extend(self._check_to_fields_local_valide())
|
|
121
|
+
errors.extend(self._check_to_fields_remote_valide())
|
|
122
|
+
errors.extend(self._check_recursion_field_dependecy())
|
|
123
|
+
errors.extend(self._check_bad_order_fields())
|
|
124
|
+
return errors
|
|
125
|
+
|
|
126
|
+
def _check_bad_order_fields(self):
|
|
127
|
+
res = []
|
|
128
|
+
try:
|
|
129
|
+
dependents = list(self.local_related_fields)
|
|
130
|
+
except FieldDoesNotExist:
|
|
131
|
+
return [] # the errors shall be raised befor by _check_recursion_field_dependecy
|
|
132
|
+
|
|
133
|
+
for field in self.model._meta.get_fields():
|
|
134
|
+
try:
|
|
135
|
+
dependents.remove(field)
|
|
136
|
+
except ValueError:
|
|
137
|
+
pass
|
|
138
|
+
if field == self:
|
|
139
|
+
if dependents:
|
|
140
|
+
# we met the current fields, but all dependent fields is not
|
|
141
|
+
# passed befor : we will have a problem in the init of some objects
|
|
142
|
+
# where the rest of dependents fields will override the
|
|
143
|
+
# values set by the current one (see Model.__init__)
|
|
144
|
+
res.append(
|
|
145
|
+
checks.Error(
|
|
146
|
+
"the field %s depend on the fields %s which is defined after. define them befor %s"
|
|
147
|
+
% (self.name, ",".join(f.name for f in dependents), self.name),
|
|
148
|
+
hint=None,
|
|
149
|
+
obj=self,
|
|
150
|
+
id="compositefk.E006",
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
break
|
|
154
|
+
return res
|
|
155
|
+
|
|
156
|
+
def _check_recursion_field_dependecy(self):
|
|
157
|
+
res = []
|
|
158
|
+
for local_field in self._raw_fields.values():
|
|
159
|
+
try:
|
|
160
|
+
f = self.model._meta.get_field(local_field.value)
|
|
161
|
+
if isinstance(f, CompositeForeignKey):
|
|
162
|
+
res.append(
|
|
163
|
+
checks.Error(
|
|
164
|
+
"the field %s depend on the field %s which is another CompositeForeignKey"
|
|
165
|
+
% (self.name, local_field),
|
|
166
|
+
hint=None,
|
|
167
|
+
obj=self,
|
|
168
|
+
id="compositefk.E005",
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
except FieldDoesNotExist:
|
|
172
|
+
pass # _check_to_fields_local_valide already raise errors for this
|
|
173
|
+
return res
|
|
174
|
+
|
|
175
|
+
def _check_to_fields_local_valide(self):
|
|
176
|
+
res = []
|
|
177
|
+
for local_field in self._raw_fields.values():
|
|
178
|
+
if isinstance(local_field, LocalFieldValue):
|
|
179
|
+
try:
|
|
180
|
+
self.model._meta.get_field(local_field.value)
|
|
181
|
+
except FieldDoesNotExist:
|
|
182
|
+
res.append(
|
|
183
|
+
checks.Error(
|
|
184
|
+
"the field %s does not exists on the model %s" % (local_field, self.model),
|
|
185
|
+
hint=None,
|
|
186
|
+
obj=self,
|
|
187
|
+
id="compositefk.E003",
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
return res
|
|
191
|
+
|
|
192
|
+
def _check_to_fields_remote_valide(self):
|
|
193
|
+
res = []
|
|
194
|
+
for remote_field in self._raw_fields.keys():
|
|
195
|
+
try:
|
|
196
|
+
self.related_model._meta.get_field(remote_field)
|
|
197
|
+
except FieldDoesNotExist:
|
|
198
|
+
res.append(
|
|
199
|
+
checks.Error(
|
|
200
|
+
"the field %s does not exists on the model %s" % (remote_field, self.model),
|
|
201
|
+
hint=None,
|
|
202
|
+
obj=self,
|
|
203
|
+
id="compositefk.E004",
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
return res
|
|
207
|
+
|
|
208
|
+
def _check_null_with_nullifequal(self):
|
|
209
|
+
if self.null_if_equal and not self.null:
|
|
210
|
+
return [
|
|
211
|
+
checks.Error(
|
|
212
|
+
"you must set null=True to field %s.%s if null_if_equal is given"
|
|
213
|
+
% (self.model.__class__.__name__, self.name),
|
|
214
|
+
hint=None,
|
|
215
|
+
obj=self,
|
|
216
|
+
id="compositefk.E001",
|
|
217
|
+
)
|
|
218
|
+
]
|
|
219
|
+
return []
|
|
220
|
+
|
|
221
|
+
def _check_nullifequal_fields_exists(self):
|
|
222
|
+
res = []
|
|
223
|
+
for field_name, value in self.null_if_equal:
|
|
224
|
+
try:
|
|
225
|
+
self.model._meta.get_field(field_name)
|
|
226
|
+
except FieldDoesNotExist:
|
|
227
|
+
res.append(
|
|
228
|
+
checks.Error(
|
|
229
|
+
"the field %s does not exists on the model %s" % (field_name, self.model),
|
|
230
|
+
hint=None,
|
|
231
|
+
obj=self,
|
|
232
|
+
id="compositefk.E002",
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
return res
|
|
236
|
+
|
|
237
|
+
def deconstruct(self):
|
|
238
|
+
name, path, args, kwargs = super(CompositeForeignKey, self).deconstruct()
|
|
239
|
+
del kwargs["from_fields"]
|
|
240
|
+
if "on_delete" in kwargs:
|
|
241
|
+
kwargs["on_delete"] = kwargs["on_delete"]._original_fn
|
|
242
|
+
kwargs["to_fields"] = self._raw_fields
|
|
243
|
+
kwargs["null_if_equal"] = self.null_if_equal
|
|
244
|
+
return name, path, args, kwargs
|
|
245
|
+
|
|
246
|
+
def get_extra_descriptor_filter(self, instance):
|
|
247
|
+
return {k: v.value for k, v in self._raw_fields.items() if isinstance(v, RawFieldValue)}
|
|
248
|
+
|
|
249
|
+
def get_extra_restriction(self, alias, related_alias):
|
|
250
|
+
constraint = WhereNode(connector=AND)
|
|
251
|
+
for remote, local in self._raw_fields.items():
|
|
252
|
+
lookup = local.get_lookup(self, self.related_model._meta.get_field(remote), alias)
|
|
253
|
+
if lookup:
|
|
254
|
+
constraint.add(lookup, AND)
|
|
255
|
+
if constraint.children:
|
|
256
|
+
return constraint
|
|
257
|
+
else:
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
def compute_to_fields(self, to_fields):
|
|
261
|
+
"""
|
|
262
|
+
compute the to_fields parameterse to make it uniformly a dict of CompositePart
|
|
263
|
+
:param set[unicode]|dict[unicode, unicode] to_fields: the list/dict of fields to match
|
|
264
|
+
:return: the well formated to_field containing only subclasses of CompositePart
|
|
265
|
+
:rtype: dict[str, CompositePart]
|
|
266
|
+
"""
|
|
267
|
+
# for problem in trim_join, we must try to give the fields in a consistent order with others models...
|
|
268
|
+
# see #26515 at https://code.djangoproject.com/ticket/26515
|
|
269
|
+
|
|
270
|
+
return OrderedDict(
|
|
271
|
+
(k, (v if isinstance(v, CompositePart) else LocalFieldValue(v)))
|
|
272
|
+
for k, v in (to_fields.items() if isinstance(to_fields, dict) else zip(to_fields, to_fields))
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
def db_type(self, connection):
|
|
276
|
+
# A CompositeForeignKey don't have a column in the database
|
|
277
|
+
# so return None.
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
def db_parameters(self, connection):
|
|
281
|
+
return {"type": None, "check": None}
|
|
282
|
+
|
|
283
|
+
def contribute_to_class(self, cls, name, **kwargs):
|
|
284
|
+
super().contribute_to_class(cls, name, **kwargs)
|
|
285
|
+
setattr(cls, self.name, CompositeForwardManyToOneDescriptor(self))
|
|
286
|
+
|
|
287
|
+
def get_instance_value_for_fields(self, instance, fields):
|
|
288
|
+
# we override this method to provide the feathur of converting
|
|
289
|
+
# some special values of teh composite local fields into a
|
|
290
|
+
# None pointing field.
|
|
291
|
+
# ie, if company is ' ' and it mean that the current field
|
|
292
|
+
# point to nothing (as if it was None) => we transform this
|
|
293
|
+
# ' ' into a true None to let django das as if it was None
|
|
294
|
+
res = super(CompositeForeignKey, self).get_instance_value_for_fields(instance, fields)
|
|
295
|
+
if self.null_if_equal:
|
|
296
|
+
for field_name, exception_value in self.null_if_equal:
|
|
297
|
+
val = getattr(instance, field_name)
|
|
298
|
+
if val == exception_value:
|
|
299
|
+
# we have field_name that is equal to the bad value
|
|
300
|
+
# currently, it is enouth since the django implementation check at first
|
|
301
|
+
# if there is a None in the result
|
|
302
|
+
return (None,)
|
|
303
|
+
return res
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class CompositeOneToOneField(CompositeForeignKey):
|
|
307
|
+
# Field flags
|
|
308
|
+
many_to_many = False
|
|
309
|
+
many_to_one = False
|
|
310
|
+
one_to_many = False
|
|
311
|
+
one_to_one = True
|
|
312
|
+
|
|
313
|
+
related_accessor_class = ReverseOneToOneDescriptor
|
|
314
|
+
|
|
315
|
+
description = "One-to-one relationship"
|
|
316
|
+
|
|
317
|
+
def __init__(self, to, **kwargs):
|
|
318
|
+
kwargs["unique"] = True
|
|
319
|
+
super().__init__(to, **kwargs)
|
|
320
|
+
self.remote_field.multiple = False
|
|
321
|
+
|
|
322
|
+
def deconstruct(self):
|
|
323
|
+
name, path, args, kwargs = super().deconstruct()
|
|
324
|
+
if "unique" in kwargs:
|
|
325
|
+
del kwargs["unique"]
|
|
326
|
+
return name, path, args, kwargs
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class CompositePart(object):
|
|
330
|
+
is_local_field = True
|
|
331
|
+
|
|
332
|
+
def __init__(self, value):
|
|
333
|
+
self.value = value
|
|
334
|
+
|
|
335
|
+
def deconstruct(self):
|
|
336
|
+
module_name = self.__module__
|
|
337
|
+
name = self.__class__.__name__
|
|
338
|
+
return ("%s.%s" % (module_name, name), (self.value,), {})
|
|
339
|
+
|
|
340
|
+
def __repr__(self):
|
|
341
|
+
return "%s(%r)" % (self.__class__.__name__, self.value)
|
|
342
|
+
|
|
343
|
+
def __eq__(self, other):
|
|
344
|
+
if self.__class__ != other.__class__:
|
|
345
|
+
return False
|
|
346
|
+
return self.value == other.value
|
|
347
|
+
|
|
348
|
+
def get_lookup(self, main_field, for_remote, alias):
|
|
349
|
+
"""
|
|
350
|
+
create a fake field for the lookup capability
|
|
351
|
+
:param CompositeForeignKey main_field: the local fk
|
|
352
|
+
:param Field for_remote: the remote field to match
|
|
353
|
+
:return:
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class RawFieldValue(CompositePart):
|
|
358
|
+
"""
|
|
359
|
+
represent a raw value for a field.
|
|
360
|
+
"""
|
|
361
|
+
|
|
362
|
+
is_local_field = False
|
|
363
|
+
|
|
364
|
+
def get_lookup(self, main_field, for_remote, alias):
|
|
365
|
+
"""
|
|
366
|
+
create a fake field for the lookup capability
|
|
367
|
+
:param CompositeForeignKey main_field: the local fk
|
|
368
|
+
:param Field for_remote: the remote field to match
|
|
369
|
+
:return:
|
|
370
|
+
"""
|
|
371
|
+
lookup_class = for_remote.get_lookup("exact")
|
|
372
|
+
return lookup_class(for_remote.get_col(alias), self.value)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
class FunctionBasedFieldValue(RawFieldValue):
|
|
376
|
+
def __init__(self, func):
|
|
377
|
+
self._func = func
|
|
378
|
+
|
|
379
|
+
def deconstruct(self):
|
|
380
|
+
module_name = self.__module__
|
|
381
|
+
name = self.__class__.__name__
|
|
382
|
+
return ("%s.%s" % (module_name, name), (self._func,), {})
|
|
383
|
+
|
|
384
|
+
def __eq__(self, other):
|
|
385
|
+
if self.__class__ != other.__class__:
|
|
386
|
+
return False
|
|
387
|
+
return self._func == other._func
|
|
388
|
+
|
|
389
|
+
@property
|
|
390
|
+
def value(self):
|
|
391
|
+
return self._func()
|
|
392
|
+
|
|
393
|
+
@value.setter
|
|
394
|
+
def value(self):
|
|
395
|
+
pass
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
class LocalFieldValue(CompositePart):
|
|
399
|
+
"""
|
|
400
|
+
implicitly used, represent the value of a local field
|
|
401
|
+
"""
|
|
402
|
+
|
|
403
|
+
is_local_field = True
|
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .llm import run_company_extraction_llm
|
|
2
|
+
from .instruments import Instrument, InstrumentType, Cash, Equity
|
|
3
|
+
from .instrument_prices import InstrumentPrice
|
|
4
|
+
from .instrument_relationships import (
|
|
5
|
+
InstrumentClassificationRelatedInstrument,
|
|
6
|
+
InstrumentClassificationThroughModel,
|
|
7
|
+
InstrumentFavoriteGroup,
|
|
8
|
+
RelatedInstrumentThroughModel,
|
|
9
|
+
)
|
|
10
|
+
from .classifications import (
|
|
11
|
+
Classification,
|
|
12
|
+
ClassificationGroup,
|
|
13
|
+
)
|
|
14
|
+
from .instrument_lists import InstrumentList, InstrumentListThroughModel
|
|
15
|
+
|
|
16
|
+
from .instrument_requests import InstrumentRequest
|
|
17
|
+
|
|
18
|
+
from .private_equities import Deal
|
|
19
|
+
from .options import Option, OptionAggregate
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
import roman
|
|
4
|
+
from django.contrib.postgres.expressions import ArraySubquery
|
|
5
|
+
from django.db import models
|
|
6
|
+
from django.db.models.functions import Cast
|
|
7
|
+
from django.db.models.signals import post_save
|
|
8
|
+
from django.dispatch import receiver
|
|
9
|
+
from mptt.models import MPTTModel, TreeForeignKey
|
|
10
|
+
from wbcore.models import WBModel
|
|
11
|
+
from wbcore.utils.models import ComplexToStringMixin
|
|
12
|
+
|
|
13
|
+
from .instrument_relationships import InstrumentClassificationThroughModel
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ClassificationGroup(WBModel):
|
|
17
|
+
name = models.CharField(max_length=128, verbose_name="Name")
|
|
18
|
+
is_primary = models.BooleanField(
|
|
19
|
+
default=False,
|
|
20
|
+
verbose_name="Primary",
|
|
21
|
+
help_text="Set to True if this " "classification must be used as " "default if not specified " "otherwise",
|
|
22
|
+
)
|
|
23
|
+
max_depth = models.IntegerField(default=0, verbose_name="Maximum Depth")
|
|
24
|
+
code_level_digits = models.IntegerField(default=2, verbose_name="The number of digits per code level")
|
|
25
|
+
|
|
26
|
+
def __str__(self) -> str:
|
|
27
|
+
return f'{self.name} ({"Primary" if self.is_primary else "Non Primary"})'
|
|
28
|
+
|
|
29
|
+
def save(self, *args, **kwargs):
|
|
30
|
+
qs = ClassificationGroup.objects.filter(is_primary=True).exclude(id=self.id)
|
|
31
|
+
if self.is_primary:
|
|
32
|
+
qs.update(is_primary=False)
|
|
33
|
+
elif not qs.exists():
|
|
34
|
+
self.is_primary = True
|
|
35
|
+
return super().save(*args, **kwargs)
|
|
36
|
+
|
|
37
|
+
def get_levels_representation(self) -> List[str]:
|
|
38
|
+
return [
|
|
39
|
+
self.classifications.filter(height=i).first().level_representation
|
|
40
|
+
for i in range(self.max_depth + 1)
|
|
41
|
+
if self.classifications.filter(height=i).exists()
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
def get_fields_names(self, sep: str = "__") -> list[str]:
|
|
45
|
+
return [f"parent{f'{sep}parent' * height}" for height in range(self.max_depth)]
|
|
46
|
+
|
|
47
|
+
def annotate_queryset(
|
|
48
|
+
self,
|
|
49
|
+
queryset: models.QuerySet,
|
|
50
|
+
classification_height: int,
|
|
51
|
+
instrument_label_key: str,
|
|
52
|
+
unique: bool = False,
|
|
53
|
+
annotation_label: str = "classifications",
|
|
54
|
+
):
|
|
55
|
+
ref_id = "classification__"
|
|
56
|
+
if classification_height:
|
|
57
|
+
ref_id += f"{'parent__' * classification_height}"
|
|
58
|
+
ref_id += "id"
|
|
59
|
+
if instrument_label_key:
|
|
60
|
+
instrument_label_key += "__"
|
|
61
|
+
base_subquery = InstrumentClassificationThroughModel.objects.filter(
|
|
62
|
+
classification__group=self,
|
|
63
|
+
instrument__tree_id=models.OuterRef(f"{instrument_label_key}tree_id"),
|
|
64
|
+
instrument__lft__lte=models.OuterRef(f"{instrument_label_key}lft"),
|
|
65
|
+
instrument__rght__gte=models.OuterRef(f"{instrument_label_key}rght"),
|
|
66
|
+
)
|
|
67
|
+
if unique:
|
|
68
|
+
expression = models.Subquery(base_subquery.values(ref_id)[:1])
|
|
69
|
+
else:
|
|
70
|
+
expression = ArraySubquery(
|
|
71
|
+
base_subquery.values(ref_id).distinct(ref_id)
|
|
72
|
+
) # we use distinct in order to remove duplicated classification (e.g. classification added to the parent as well as the children)
|
|
73
|
+
|
|
74
|
+
return queryset.annotate(**{annotation_label: expression})
|
|
75
|
+
|
|
76
|
+
class Meta:
|
|
77
|
+
verbose_name = "Classification Group"
|
|
78
|
+
verbose_name_plural = "Classification Groups"
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def get_representation_endpoint(cls) -> str:
|
|
82
|
+
return "wbfdm:classificationgrouprepresentation-list"
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def get_representation_value_key(cls) -> str:
|
|
86
|
+
return "id"
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def get_representation_label_key(cls) -> str:
|
|
90
|
+
return "{{name}}"
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def get_endpoint_basename(cls) -> str:
|
|
94
|
+
return "wbfdm:classificationgroup"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class Classification(MPTTModel, ComplexToStringMixin):
|
|
98
|
+
parent = TreeForeignKey(
|
|
99
|
+
"self",
|
|
100
|
+
related_name="children",
|
|
101
|
+
null=True,
|
|
102
|
+
blank=True,
|
|
103
|
+
on_delete=models.CASCADE,
|
|
104
|
+
verbose_name="Parent Classification",
|
|
105
|
+
)
|
|
106
|
+
height = models.PositiveIntegerField(
|
|
107
|
+
default=0,
|
|
108
|
+
verbose_name="The height (leaf node have height 0)",
|
|
109
|
+
)
|
|
110
|
+
group = models.ForeignKey(
|
|
111
|
+
ClassificationGroup,
|
|
112
|
+
related_name="classifications",
|
|
113
|
+
on_delete=models.CASCADE,
|
|
114
|
+
verbose_name="Classification Group",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
level_representation = models.CharField(max_length=256, verbose_name="Level Representation")
|
|
118
|
+
|
|
119
|
+
name = models.CharField(max_length=128, verbose_name="Name")
|
|
120
|
+
code_aggregated = models.CharField(max_length=64, verbose_name="Code Aggregated")
|
|
121
|
+
|
|
122
|
+
investable = models.BooleanField(default=True, help_text="Is this classification investable for us?")
|
|
123
|
+
|
|
124
|
+
description = models.TextField(
|
|
125
|
+
default="",
|
|
126
|
+
blank=True,
|
|
127
|
+
help_text="Give a basic definition and description",
|
|
128
|
+
verbose_name="Definition/Description",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def dict_to_model(cls, classification_data):
|
|
133
|
+
if isinstance(classification_data, int):
|
|
134
|
+
return cls.objects.filter(id=classification_data).first()
|
|
135
|
+
res = cls.objects.all()
|
|
136
|
+
if code_aggregated := classification_data.get("code_aggregated", None):
|
|
137
|
+
res = res.filter(code_aggregated=code_aggregated)
|
|
138
|
+
if group_id := classification_data.get("group", None):
|
|
139
|
+
res = res.filter(group=group_id)
|
|
140
|
+
if res.count() == 1:
|
|
141
|
+
return res.first()
|
|
142
|
+
|
|
143
|
+
def __str__(self):
|
|
144
|
+
if self.computed_str:
|
|
145
|
+
return self.computed_str
|
|
146
|
+
return f"{self.code_aggregated} {self.name}"
|
|
147
|
+
|
|
148
|
+
def get_classified_instruments(self, only_favorites: bool = False) -> models.QuerySet:
|
|
149
|
+
childs_classifications = self.get_descendants(include_self=True)
|
|
150
|
+
params = {"classifications__in": childs_classifications}
|
|
151
|
+
if only_favorites:
|
|
152
|
+
params["classifications_through__is_favorite"] = True
|
|
153
|
+
|
|
154
|
+
from wbfdm.models import Instrument
|
|
155
|
+
|
|
156
|
+
return Instrument.objects.filter(**params).distinct()
|
|
157
|
+
|
|
158
|
+
def save(self, *args, **kwargs):
|
|
159
|
+
if self.parent:
|
|
160
|
+
self.group = self.parent.group
|
|
161
|
+
if not self.code_aggregated:
|
|
162
|
+
self.code_aggregated = self.get_next_valid_code(self.group, self.parent)
|
|
163
|
+
if not self.level_representation:
|
|
164
|
+
self.level_representation = self.get_default_level_representation(self.group, self.parent)
|
|
165
|
+
return super().save(*args, **kwargs)
|
|
166
|
+
|
|
167
|
+
def compute_str(self) -> str:
|
|
168
|
+
if parent := self.parent:
|
|
169
|
+
tree_titles = parent.name
|
|
170
|
+
while parent and (parent := parent.parent):
|
|
171
|
+
tree_titles += f" - {parent.name}"
|
|
172
|
+
return f"{self.code_aggregated} {self.name} [{tree_titles}] ({self.group.name})"
|
|
173
|
+
return f"{self.code_aggregated} {self.name} ({self.group.name})"
|
|
174
|
+
|
|
175
|
+
class Meta:
|
|
176
|
+
verbose_name = "Classification"
|
|
177
|
+
verbose_name_plural = "Classifications"
|
|
178
|
+
constraints = [models.UniqueConstraint(fields=["group", "code_aggregated"], name="unique_classification")]
|
|
179
|
+
|
|
180
|
+
@classmethod
|
|
181
|
+
def get_next_valid_code(cls, group: "ClassificationGroup", parent: "Classification | None" = None) -> str:
|
|
182
|
+
"""
|
|
183
|
+
Return the next valid classification code given the classification parent and its group parameters
|
|
184
|
+
Args:
|
|
185
|
+
group: The classification group the estimated code belongs to
|
|
186
|
+
parent: The classification parent (if any)
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
The next valid and unused aggregated classification code
|
|
190
|
+
"""
|
|
191
|
+
if not group:
|
|
192
|
+
raise ValueError("This method needs a group")
|
|
193
|
+
siblings_classifications = (
|
|
194
|
+
Classification.objects.filter(parent=parent, group=group)
|
|
195
|
+
.annotate(casted_code=Cast("code_aggregated", models.BigIntegerField()))
|
|
196
|
+
.order_by("-casted_code")
|
|
197
|
+
)
|
|
198
|
+
parent_level = parent.level + 1 if parent else 0
|
|
199
|
+
code_aggregated_digits = group.code_level_digits * (parent_level + 1)
|
|
200
|
+
if last_classification := siblings_classifications.first():
|
|
201
|
+
for i in range(0, 100 - last_classification.casted_code % 10**group.code_level_digits):
|
|
202
|
+
if not siblings_classifications.filter(casted_code=last_classification.casted_code + i).exists():
|
|
203
|
+
last_valid_code = last_classification.casted_code + i
|
|
204
|
+
return f"{last_valid_code:0{code_aggregated_digits}}"
|
|
205
|
+
if parent:
|
|
206
|
+
return parent.code_aggregated + f"{1:0{group.code_level_digits}}"
|
|
207
|
+
return f"{1:0{group.code_level_digits}}"
|
|
208
|
+
|
|
209
|
+
@classmethod
|
|
210
|
+
def get_default_level_representation(
|
|
211
|
+
cls, group: "ClassificationGroup", parent: Optional["Classification"] = None
|
|
212
|
+
) -> str:
|
|
213
|
+
"""
|
|
214
|
+
Return the default level representation, extracted from the classification siblings
|
|
215
|
+
Args:
|
|
216
|
+
group: The classification group the estimated code belongs to
|
|
217
|
+
parent: The classification parent (if any)
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
A default level representation (e.g. Industry)
|
|
221
|
+
"""
|
|
222
|
+
level = parent.level + 1 if parent else None
|
|
223
|
+
siblings_classifications = cls.objects.filter(level=level, group=group).order_by("id")
|
|
224
|
+
if siblings_classifications.exists():
|
|
225
|
+
return siblings_classifications.last().level_representation
|
|
226
|
+
level = roman.toRoman(level) if level else 0
|
|
227
|
+
return f"Level {level}"
|
|
228
|
+
|
|
229
|
+
@classmethod
|
|
230
|
+
def get_representation_endpoint(cls) -> str:
|
|
231
|
+
return "wbfdm:classificationrepresentation-list"
|
|
232
|
+
|
|
233
|
+
@classmethod
|
|
234
|
+
def get_representation_value_key(cls) -> str:
|
|
235
|
+
return "id"
|
|
236
|
+
|
|
237
|
+
@classmethod
|
|
238
|
+
def get_endpoint_basename(cls) -> str:
|
|
239
|
+
return "wbfdm:classification"
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@receiver(post_save, sender="wbfdm.Classification")
|
|
243
|
+
def post_save_parent_classification(sender, instance, created, raw, **kwargs):
|
|
244
|
+
# Recursively call the parent save function to recompute its parameters
|
|
245
|
+
if not raw and instance.parent:
|
|
246
|
+
instance.parent.save()
|
|
247
|
+
if instance.level is not None:
|
|
248
|
+
update_fields = {"height": instance.group.max_depth - instance.level}
|
|
249
|
+
Classification.objects.filter(id=instance.id).update(**update_fields)
|
|
250
|
+
instance.refresh_from_db()
|
|
251
|
+
# # Ensure initial parent classifcation span the proper classification tree structure
|
|
252
|
+
if instance.group and not instance.get_descendants().exists() and instance.level < instance.group.max_depth:
|
|
253
|
+
Classification.objects.create(
|
|
254
|
+
parent=instance,
|
|
255
|
+
group=instance.group,
|
|
256
|
+
name=f"{instance.name} (Level {instance.level})",
|
|
257
|
+
)
|
|
258
|
+
"""
|
|
259
|
+
If a parent classification is not investable, therefore its "children" become non investable too, by cascade.
|
|
260
|
+
If a child classification becomes investable and one of its parent is non investable, therefore he cannot be
|
|
261
|
+
investable as long as its parent is non investable. (-> in the serializer)
|
|
262
|
+
"""
|
|
263
|
+
if not raw and not instance.investable and instance.get_descendants().exists():
|
|
264
|
+
descandants = instance.get_descendants()
|
|
265
|
+
descandants.update(investable=False)
|