wbcore 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.
- wbcore/__init__.py +1 -0
- wbcore/admin.py +197 -0
- wbcore/apps.py +17 -0
- wbcore/cache/__init__.py +0 -0
- wbcore/cache/buttons.py +22 -0
- wbcore/cache/decorators.py +29 -0
- wbcore/cache/mixins.py +48 -0
- wbcore/cache/registry.py +90 -0
- wbcore/cache/views.py +18 -0
- wbcore/configs/__init__.py +11 -0
- wbcore/configs/configs.py +51 -0
- wbcore/configs/decorators.py +11 -0
- wbcore/configs/registry.py +35 -0
- wbcore/configs/views.py +11 -0
- wbcore/configurations/__init__.py +1 -0
- wbcore/configurations/base.py +45 -0
- wbcore/configurations/configurations/__init__.py +16 -0
- wbcore/configurations/configurations/apps.py +54 -0
- wbcore/configurations/configurations/authentication.py +43 -0
- wbcore/configurations/configurations/base.py +15 -0
- wbcore/configurations/configurations/cache.py +20 -0
- wbcore/configurations/configurations/celery.py +26 -0
- wbcore/configurations/configurations/i18nl10n.py +10 -0
- wbcore/configurations/configurations/mail.py +2 -0
- wbcore/configurations/configurations/maintenance.py +53 -0
- wbcore/configurations/configurations/media.py +22 -0
- wbcore/configurations/configurations/middleware.py +27 -0
- wbcore/configurations/configurations/network.py +19 -0
- wbcore/configurations/configurations/rest_framework.py +42 -0
- wbcore/configurations/configurations/static.py +28 -0
- wbcore/configurations/configurations/templates.py +17 -0
- wbcore/configurations/configurations/uvicorn.py +9 -0
- wbcore/configurations/configurations/wbcore.py +68 -0
- wbcore/content_type/__init__.py +0 -0
- wbcore/content_type/admin.py +8 -0
- wbcore/content_type/filters.py +19 -0
- wbcore/content_type/serializers.py +89 -0
- wbcore/content_type/utils.py +30 -0
- wbcore/content_type/viewsets.py +80 -0
- wbcore/contrib/__init__.py +0 -0
- wbcore/contrib/agenda/__init__.py +0 -0
- wbcore/contrib/agenda/admin/__init__.py +2 -0
- wbcore/contrib/agenda/admin/calendar_item.py +13 -0
- wbcore/contrib/agenda/admin/conference_room.py +18 -0
- wbcore/contrib/agenda/apps.py +6 -0
- wbcore/contrib/agenda/configurations.py +11 -0
- wbcore/contrib/agenda/factories/__init__.py +2 -0
- wbcore/contrib/agenda/factories/calendar_item.py +47 -0
- wbcore/contrib/agenda/factories/conference_room.py +19 -0
- wbcore/contrib/agenda/filters/__init__.py +2 -0
- wbcore/contrib/agenda/filters/calendar_item.py +64 -0
- wbcore/contrib/agenda/filters/conference_room.py +41 -0
- wbcore/contrib/agenda/migrations/0001_initial.py +84 -0
- wbcore/contrib/agenda/migrations/0002_initial.py +26 -0
- wbcore/contrib/agenda/migrations/0003_calendaritem_endpoint_basename.py +42 -0
- wbcore/contrib/agenda/migrations/0004_alter_calendaritem_item_type.py +17 -0
- wbcore/contrib/agenda/migrations/0005_building_and_more.py +94 -0
- wbcore/contrib/agenda/migrations/0006_calendaritem_is_deletable.py +17 -0
- wbcore/contrib/agenda/migrations/0007_alter_calendaritem_options.py +21 -0
- wbcore/contrib/agenda/migrations/0008_alter_calendaritem_item_type.py +17 -0
- wbcore/contrib/agenda/migrations/0009_alter_calendaritem_icon.py +18 -0
- wbcore/contrib/agenda/migrations/__init__.py +0 -0
- wbcore/contrib/agenda/models/__init__.py +2 -0
- wbcore/contrib/agenda/models/calendar_item.py +238 -0
- wbcore/contrib/agenda/models/conference_room.py +95 -0
- wbcore/contrib/agenda/release_notes/1_0_0.md +13 -0
- wbcore/contrib/agenda/release_notes/__init__.py +0 -0
- wbcore/contrib/agenda/serializers/__init__.py +10 -0
- wbcore/contrib/agenda/serializers/calendar_item.py +76 -0
- wbcore/contrib/agenda/serializers/conference_room.py +98 -0
- wbcore/contrib/agenda/signals.py +8 -0
- wbcore/contrib/agenda/tests/__init__.py +0 -0
- wbcore/contrib/agenda/tests/conftest.py +14 -0
- wbcore/contrib/agenda/tests/signals.py +16 -0
- wbcore/contrib/agenda/tests/test_models.py +34 -0
- wbcore/contrib/agenda/tests/test_viewsets.py +171 -0
- wbcore/contrib/agenda/tests/tests.py +25 -0
- wbcore/contrib/agenda/typings.py +19 -0
- wbcore/contrib/agenda/urls.py +25 -0
- wbcore/contrib/agenda/viewsets/__init__.py +13 -0
- wbcore/contrib/agenda/viewsets/buttons/__init__.py +1 -0
- wbcore/contrib/agenda/viewsets/buttons/conference_room.py +20 -0
- wbcore/contrib/agenda/viewsets/calendar_items.py +168 -0
- wbcore/contrib/agenda/viewsets/conference_room.py +48 -0
- wbcore/contrib/agenda/viewsets/display/__init__.py +2 -0
- wbcore/contrib/agenda/viewsets/display/calendar_items.py +17 -0
- wbcore/contrib/agenda/viewsets/display/conference_room.py +41 -0
- wbcore/contrib/agenda/viewsets/endpoints/__init__.py +1 -0
- wbcore/contrib/agenda/viewsets/endpoints/calendar_items.py +18 -0
- wbcore/contrib/agenda/viewsets/menu/__init__.py +2 -0
- wbcore/contrib/agenda/viewsets/menu/calendar_items.py +17 -0
- wbcore/contrib/agenda/viewsets/menu/conference_room.py +37 -0
- wbcore/contrib/agenda/viewsets/titles/__init__.py +2 -0
- wbcore/contrib/agenda/viewsets/titles/calendar_items.py +7 -0
- wbcore/contrib/agenda/viewsets/titles/conference_room.py +24 -0
- wbcore/contrib/ai/__init__.py +0 -0
- wbcore/contrib/ai/apps.py +5 -0
- wbcore/contrib/ai/exceptions.py +42 -0
- wbcore/contrib/ai/llm/__init__.py +0 -0
- wbcore/contrib/ai/llm/config.py +107 -0
- wbcore/contrib/ai/llm/decorators.py +10 -0
- wbcore/contrib/ai/llm/mixins.py +35 -0
- wbcore/contrib/ai/llm/utils.py +45 -0
- wbcore/contrib/authentication/__init__.py +9 -0
- wbcore/contrib/authentication/admin.py +247 -0
- wbcore/contrib/authentication/apps.py +14 -0
- wbcore/contrib/authentication/authentication.py +162 -0
- wbcore/contrib/authentication/configs.py +9 -0
- wbcore/contrib/authentication/configurations.py +53 -0
- wbcore/contrib/authentication/dynamic_preferences_registry.py +26 -0
- wbcore/contrib/authentication/factories/__init__.py +3 -0
- wbcore/contrib/authentication/factories/tokens.py +15 -0
- wbcore/contrib/authentication/factories/users.py +82 -0
- wbcore/contrib/authentication/factories/users_activities.py +19 -0
- wbcore/contrib/authentication/filters.py +18 -0
- wbcore/contrib/authentication/management/__init__.py +14 -0
- wbcore/contrib/authentication/migrations/0001_initial_squashed.py +174 -0
- wbcore/contrib/authentication/migrations/0002_profile.py +26 -0
- wbcore/contrib/authentication/migrations/0003_alter_user_profile.py +34 -0
- wbcore/contrib/authentication/migrations/0004_token.py +51 -0
- wbcore/contrib/authentication/migrations/0005_user_external_calendar_settings.py +17 -0
- wbcore/contrib/authentication/migrations/0006_auto_20231206_1422.py +13 -0
- wbcore/contrib/authentication/migrations/__init__.py +0 -0
- wbcore/contrib/authentication/models/__init__.py +3 -0
- wbcore/contrib/authentication/models/tokens.py +140 -0
- wbcore/contrib/authentication/models/users.py +225 -0
- wbcore/contrib/authentication/models/users_activities.py +112 -0
- wbcore/contrib/authentication/release_notes/1_0_0.md +13 -0
- wbcore/contrib/authentication/release_notes/__init__.py +0 -0
- wbcore/contrib/authentication/serializers/__init__.py +14 -0
- wbcore/contrib/authentication/serializers/users.py +350 -0
- wbcore/contrib/authentication/serializers/users_activites.py +37 -0
- wbcore/contrib/authentication/tasks.py +29 -0
- wbcore/contrib/authentication/tests/__init__.py +0 -0
- wbcore/contrib/authentication/tests/conftest.py +18 -0
- wbcore/contrib/authentication/tests/e2e/__init__.py +1 -0
- wbcore/contrib/authentication/tests/e2e/e2e_auth_utility.py +20 -0
- wbcore/contrib/authentication/tests/signals.py +17 -0
- wbcore/contrib/authentication/tests/test_configs.py +6 -0
- wbcore/contrib/authentication/tests/test_serializers.py +6 -0
- wbcore/contrib/authentication/tests/test_tasks.py +33 -0
- wbcore/contrib/authentication/tests/test_tokens.py +126 -0
- wbcore/contrib/authentication/tests/test_users.py +338 -0
- wbcore/contrib/authentication/tests/test_viewsets.py +6 -0
- wbcore/contrib/authentication/tests/tests.py +15 -0
- wbcore/contrib/authentication/urls.py +97 -0
- wbcore/contrib/authentication/utils.py +10 -0
- wbcore/contrib/authentication/viewsets/__init__.py +19 -0
- wbcore/contrib/authentication/viewsets/buttons/__init__.py +1 -0
- wbcore/contrib/authentication/viewsets/buttons/users.py +55 -0
- wbcore/contrib/authentication/viewsets/display/__init__.py +6 -0
- wbcore/contrib/authentication/viewsets/display/user_activities.py +85 -0
- wbcore/contrib/authentication/viewsets/display/users.py +72 -0
- wbcore/contrib/authentication/viewsets/endpoints/__init__.py +6 -0
- wbcore/contrib/authentication/viewsets/endpoints/user_activities.py +22 -0
- wbcore/contrib/authentication/viewsets/endpoints/users.py +20 -0
- wbcore/contrib/authentication/viewsets/menu/__init__.py +6 -0
- wbcore/contrib/authentication/viewsets/menu/user_activities.py +39 -0
- wbcore/contrib/authentication/viewsets/menu/users.py +16 -0
- wbcore/contrib/authentication/viewsets/titles/__init__.py +12 -0
- wbcore/contrib/authentication/viewsets/titles/user_activities.py +31 -0
- wbcore/contrib/authentication/viewsets/titles/users.py +26 -0
- wbcore/contrib/authentication/viewsets/user_activities.py +222 -0
- wbcore/contrib/authentication/viewsets/users.py +328 -0
- wbcore/contrib/color/migrations/0001_initial.py +33 -0
- wbcore/contrib/color/migrations/0002_alter_colorgradient_colors.py +25 -0
- wbcore/contrib/color/migrations/__init__.py +0 -0
- wbcore/contrib/currency/__init__.py +0 -0
- wbcore/contrib/currency/admin.py +32 -0
- wbcore/contrib/currency/apps.py +5 -0
- wbcore/contrib/currency/dynamic_preferences_registry.py +39 -0
- wbcore/contrib/currency/factories.py +39 -0
- wbcore/contrib/currency/import_export/__init__.py +0 -0
- wbcore/contrib/currency/import_export/backends/__init__.py +1 -0
- wbcore/contrib/currency/import_export/backends/fixerio/__init__.py +1 -0
- wbcore/contrib/currency/import_export/backends/fixerio/currency_fx_rates.py +66 -0
- wbcore/contrib/currency/import_export/backends/utils.py +5 -0
- wbcore/contrib/currency/import_export/handlers/__init__.py +2 -0
- wbcore/contrib/currency/import_export/handlers/currency.py +24 -0
- wbcore/contrib/currency/import_export/handlers/currency_fx_rates.py +28 -0
- wbcore/contrib/currency/import_export/parsers/__init__.py +0 -0
- wbcore/contrib/currency/import_export/parsers/fixerio/__init__.py +0 -0
- wbcore/contrib/currency/import_export/parsers/fixerio/currency_fx_rates.py +34 -0
- wbcore/contrib/currency/migrations/0001_initial.py +89 -0
- wbcore/contrib/currency/migrations/__init__.py +0 -0
- wbcore/contrib/currency/models.py +176 -0
- wbcore/contrib/currency/release_notes/1_0_0.md +13 -0
- wbcore/contrib/currency/release_notes/__init__.py +0 -0
- wbcore/contrib/currency/serializers.py +40 -0
- wbcore/contrib/currency/tests/__init__.py +0 -0
- wbcore/contrib/currency/tests/conftest.py +7 -0
- wbcore/contrib/currency/tests/test_models.py +104 -0
- wbcore/contrib/currency/tests/test_serializers.py +62 -0
- wbcore/contrib/currency/tests/test_viewsets.py +197 -0
- wbcore/contrib/currency/urls.py +19 -0
- wbcore/contrib/currency/viewsets/__init__.py +2 -0
- wbcore/contrib/currency/viewsets/buttons/__init__.py +0 -0
- wbcore/contrib/currency/viewsets/currency.py +56 -0
- wbcore/contrib/currency/viewsets/currency_fx_rates.py +32 -0
- wbcore/contrib/currency/viewsets/display/__init__.py +2 -0
- wbcore/contrib/currency/viewsets/display/currency.py +37 -0
- wbcore/contrib/currency/viewsets/display/currency_fx_rates.py +9 -0
- wbcore/contrib/currency/viewsets/endpoints/__init__.py +1 -0
- wbcore/contrib/currency/viewsets/endpoints/currency_fx_rates.py +14 -0
- wbcore/contrib/currency/viewsets/menu/__init__.py +1 -0
- wbcore/contrib/currency/viewsets/menu/currency.py +8 -0
- wbcore/contrib/currency/viewsets/preview/__init__.py +1 -0
- wbcore/contrib/currency/viewsets/preview/currency.py +10 -0
- wbcore/contrib/currency/viewsets/titles/__init__.py +2 -0
- wbcore/contrib/currency/viewsets/titles/currency.py +6 -0
- wbcore/contrib/currency/viewsets/titles/currency_fx_rates.py +8 -0
- wbcore/contrib/dataloader/__init__.py +1 -0
- wbcore/contrib/dataloader/apps.py +5 -0
- wbcore/contrib/dataloader/dataloaders/__init__.py +2 -0
- wbcore/contrib/dataloader/dataloaders/dataloaders.py +24 -0
- wbcore/contrib/dataloader/dataloaders/proxies.py +81 -0
- wbcore/contrib/dataloader/models/__init__.py +1 -0
- wbcore/contrib/dataloader/models/entities.py +29 -0
- wbcore/contrib/dataloader/models/querysets/__init__.py +1 -0
- wbcore/contrib/dataloader/models/querysets/entities.py +27 -0
- wbcore/contrib/dataloader/tests/__init__.py +0 -0
- wbcore/contrib/dataloader/tests/conftest.py +26 -0
- wbcore/contrib/dataloader/tests/test/__init__.py +0 -0
- wbcore/contrib/dataloader/tests/test/dataloaders/__init__.py +0 -0
- wbcore/contrib/dataloader/tests/test/dataloaders/dataloaders.py +18 -0
- wbcore/contrib/dataloader/tests/test/dataloaders/protocols.py +6 -0
- wbcore/contrib/dataloader/tests/test/dataloaders/proxies.py +11 -0
- wbcore/contrib/dataloader/tests/test/factories.py +13 -0
- wbcore/contrib/dataloader/tests/test/models.py +10 -0
- wbcore/contrib/dataloader/tests/test_dataloaders.py +37 -0
- wbcore/contrib/dataloader/tests/test_entities.py +12 -0
- wbcore/contrib/dataloader/utils.py +17 -0
- wbcore/contrib/directory/__init__.py +0 -0
- wbcore/contrib/directory/admin/__init__.py +14 -0
- wbcore/contrib/directory/admin/contacts.py +54 -0
- wbcore/contrib/directory/admin/entries.py +230 -0
- wbcore/contrib/directory/admin/relationships.py +73 -0
- wbcore/contrib/directory/apps.py +6 -0
- wbcore/contrib/directory/configs.py +8 -0
- wbcore/contrib/directory/configurations.py +69 -0
- wbcore/contrib/directory/dynamic_preferences_registry.py +61 -0
- wbcore/contrib/directory/factories/__init__.py +34 -0
- wbcore/contrib/directory/factories/contacts.py +71 -0
- wbcore/contrib/directory/factories/entries.py +169 -0
- wbcore/contrib/directory/factories/relationships.py +77 -0
- wbcore/contrib/directory/filters/__init__.py +22 -0
- wbcore/contrib/directory/filters/contacts.py +233 -0
- wbcore/contrib/directory/filters/entries.py +247 -0
- wbcore/contrib/directory/filters/relationships.py +89 -0
- wbcore/contrib/directory/migrations/0001_initial.py +871 -0
- wbcore/contrib/directory/migrations/0002_auto_20230414_1553.py +42 -0
- wbcore/contrib/directory/migrations/0003_remove_entry_last_event.py +16 -0
- wbcore/contrib/directory/migrations/0004_entry_is_draft_entry.py +21 -0
- wbcore/contrib/directory/migrations/0005_entry_salutation.py +17 -0
- wbcore/contrib/directory/migrations/0006_employeremployeerelationship_position_name.py +23 -0
- wbcore/contrib/directory/migrations/0007_alter_bankingcontact_options.py +20 -0
- wbcore/contrib/directory/migrations/0008_bankingcontact_access.py +17 -0
- wbcore/contrib/directory/migrations/0009_remove_entry_external_identfier_and_more.py +22 -0
- wbcore/contrib/directory/migrations/__init__.py +0 -0
- wbcore/contrib/directory/models/__init__.py +27 -0
- wbcore/contrib/directory/models/contacts.py +554 -0
- wbcore/contrib/directory/models/entries.py +889 -0
- wbcore/contrib/directory/models/relationships.py +634 -0
- wbcore/contrib/directory/preferences.py +10 -0
- wbcore/contrib/directory/release_notes/1_0_0.md +13 -0
- wbcore/contrib/directory/release_notes/1_0_1.md +13 -0
- wbcore/contrib/directory/release_notes/__init__.py +0 -0
- wbcore/contrib/directory/serializers/__init__.py +63 -0
- wbcore/contrib/directory/serializers/companies.py +158 -0
- wbcore/contrib/directory/serializers/contacts.py +404 -0
- wbcore/contrib/directory/serializers/entries.py +344 -0
- wbcore/contrib/directory/serializers/entry_representations.py +35 -0
- wbcore/contrib/directory/serializers/persons.py +209 -0
- wbcore/contrib/directory/serializers/relationships.py +332 -0
- wbcore/contrib/directory/signals.py +3 -0
- wbcore/contrib/directory/tests/__init__.py +0 -0
- wbcore/contrib/directory/tests/conftest.py +60 -0
- wbcore/contrib/directory/tests/disable_signals.py +52 -0
- wbcore/contrib/directory/tests/e2e/__init__.py +7 -0
- wbcore/contrib/directory/tests/e2e/e2e_directory_utility.py +162 -0
- wbcore/contrib/directory/tests/signals.py +90 -0
- wbcore/contrib/directory/tests/test_configs.py +6 -0
- wbcore/contrib/directory/tests/test_filters.py +59 -0
- wbcore/contrib/directory/tests/test_models.py +285 -0
- wbcore/contrib/directory/tests/test_permissions.py +122 -0
- wbcore/contrib/directory/tests/test_serializers.py +201 -0
- wbcore/contrib/directory/tests/test_viewsets.py +677 -0
- wbcore/contrib/directory/tests/tests.py +130 -0
- wbcore/contrib/directory/typings.py +17 -0
- wbcore/contrib/directory/urls.py +135 -0
- wbcore/contrib/directory/viewsets/__init__.py +58 -0
- wbcore/contrib/directory/viewsets/buttons/__init__.py +7 -0
- wbcore/contrib/directory/viewsets/buttons/contacts.py +16 -0
- wbcore/contrib/directory/viewsets/buttons/entries.py +133 -0
- wbcore/contrib/directory/viewsets/buttons/relationships.py +31 -0
- wbcore/contrib/directory/viewsets/contacts.py +393 -0
- wbcore/contrib/directory/viewsets/display/__init__.py +36 -0
- wbcore/contrib/directory/viewsets/display/contacts.py +380 -0
- wbcore/contrib/directory/viewsets/display/entries.py +501 -0
- wbcore/contrib/directory/viewsets/display/relationships.py +315 -0
- wbcore/contrib/directory/viewsets/display/utils.py +33 -0
- wbcore/contrib/directory/viewsets/endpoints/__init__.py +22 -0
- wbcore/contrib/directory/viewsets/endpoints/contacts.py +54 -0
- wbcore/contrib/directory/viewsets/endpoints/entries.py +30 -0
- wbcore/contrib/directory/viewsets/endpoints/relationships.py +105 -0
- wbcore/contrib/directory/viewsets/entries.py +212 -0
- wbcore/contrib/directory/viewsets/menu/__init__.py +16 -0
- wbcore/contrib/directory/viewsets/menu/contacts.py +25 -0
- wbcore/contrib/directory/viewsets/menu/entries.py +61 -0
- wbcore/contrib/directory/viewsets/menu/relationships.py +31 -0
- wbcore/contrib/directory/viewsets/menu/utils.py +72 -0
- wbcore/contrib/directory/viewsets/mixins.py +11 -0
- wbcore/contrib/directory/viewsets/previews/__init__.py +2 -0
- wbcore/contrib/directory/viewsets/previews/contacts.py +18 -0
- wbcore/contrib/directory/viewsets/previews/entries.py +31 -0
- wbcore/contrib/directory/viewsets/relationships.py +241 -0
- wbcore/contrib/directory/viewsets/titles/__init__.py +30 -0
- wbcore/contrib/directory/viewsets/titles/contacts.py +122 -0
- wbcore/contrib/directory/viewsets/titles/entries.py +29 -0
- wbcore/contrib/directory/viewsets/titles/relationships.py +86 -0
- wbcore/contrib/directory/viewsets/titles/utils.py +46 -0
- wbcore/contrib/directory/viewsets/utils.py +101 -0
- wbcore/contrib/documents/__init__.py +0 -0
- wbcore/contrib/documents/admin.py +79 -0
- wbcore/contrib/documents/apps.py +6 -0
- wbcore/contrib/documents/factories.py +43 -0
- wbcore/contrib/documents/filters.py +82 -0
- wbcore/contrib/documents/migrations/0001_initial.py +189 -0
- wbcore/contrib/documents/migrations/0002_documentmodelrelationship_primary_and_more.py +22 -0
- wbcore/contrib/documents/migrations/0003_alter_documentmodelrelationship_unique_together_and_more.py +30 -0
- wbcore/contrib/documents/migrations/0004_auto_20240103_0958.py +44 -0
- wbcore/contrib/documents/migrations/0005_document_valid_from_document_valid_until_and_more.py +32 -0
- wbcore/contrib/documents/migrations/__init__.py +0 -0
- wbcore/contrib/documents/models/__init__.py +4 -0
- wbcore/contrib/documents/models/document_model_relationships.py +57 -0
- wbcore/contrib/documents/models/document_types.py +35 -0
- wbcore/contrib/documents/models/documents.py +309 -0
- wbcore/contrib/documents/models/mixins.py +10 -0
- wbcore/contrib/documents/models/shareable_links.py +90 -0
- wbcore/contrib/documents/release_notes/1_0_0.md +13 -0
- wbcore/contrib/documents/release_notes/__init__.py +0 -0
- wbcore/contrib/documents/serializers/__init__.py +12 -0
- wbcore/contrib/documents/serializers/document_model_relationships.py +31 -0
- wbcore/contrib/documents/serializers/document_types.py +40 -0
- wbcore/contrib/documents/serializers/documents.py +64 -0
- wbcore/contrib/documents/serializers/shareable_links.py +90 -0
- wbcore/contrib/documents/urls.py +42 -0
- wbcore/contrib/documents/viewsets/__init__.py +10 -0
- wbcore/contrib/documents/viewsets/buttons/__init__.py +3 -0
- wbcore/contrib/documents/viewsets/buttons/documents.py +53 -0
- wbcore/contrib/documents/viewsets/buttons/shareable_links.py +20 -0
- wbcore/contrib/documents/viewsets/buttons/signals.py +13 -0
- wbcore/contrib/documents/viewsets/display/__init__.py +4 -0
- wbcore/contrib/documents/viewsets/display/document_model_relationships.py +18 -0
- wbcore/contrib/documents/viewsets/display/document_types.py +23 -0
- wbcore/contrib/documents/viewsets/display/documents.py +101 -0
- wbcore/contrib/documents/viewsets/display/shareable_links.py +65 -0
- wbcore/contrib/documents/viewsets/document_model_relationships.py +25 -0
- wbcore/contrib/documents/viewsets/document_types.py +37 -0
- wbcore/contrib/documents/viewsets/documents.py +107 -0
- wbcore/contrib/documents/viewsets/endpoints/__init__.py +5 -0
- wbcore/contrib/documents/viewsets/endpoints/documents.py +19 -0
- wbcore/contrib/documents/viewsets/endpoints/documents_model_relationships.py +13 -0
- wbcore/contrib/documents/viewsets/endpoints/shareable_links.py +45 -0
- wbcore/contrib/documents/viewsets/menu/__init__.py +1 -0
- wbcore/contrib/documents/viewsets/menu/documents.py +24 -0
- wbcore/contrib/documents/viewsets/previews/__init__.py +1 -0
- wbcore/contrib/documents/viewsets/previews/documents.py +10 -0
- wbcore/contrib/documents/viewsets/shareable_links.py +109 -0
- wbcore/contrib/documents/viewsets/titles/__init__.py +3 -0
- wbcore/contrib/documents/viewsets/titles/document_types.py +13 -0
- wbcore/contrib/documents/viewsets/titles/documents.py +26 -0
- wbcore/contrib/documents/viewsets/titles/shareable_links.py +13 -0
- wbcore/contrib/dynamic_preferences/__init__.py +0 -0
- wbcore/contrib/dynamic_preferences/types.py +19 -0
- wbcore/contrib/example_app/__init__.py +0 -0
- wbcore/contrib/example_app/admin.py +117 -0
- wbcore/contrib/example_app/apps.py +6 -0
- wbcore/contrib/example_app/factories/__init__.py +8 -0
- wbcore/contrib/example_app/factories/event.py +26 -0
- wbcore/contrib/example_app/factories/league.py +15 -0
- wbcore/contrib/example_app/factories/match.py +25 -0
- wbcore/contrib/example_app/factories/person.py +32 -0
- wbcore/contrib/example_app/factories/role.py +9 -0
- wbcore/contrib/example_app/factories/sport.py +12 -0
- wbcore/contrib/example_app/factories/stadium.py +12 -0
- wbcore/contrib/example_app/factories/team.py +35 -0
- wbcore/contrib/example_app/filters/__init__.py +9 -0
- wbcore/contrib/example_app/filters/event.py +80 -0
- wbcore/contrib/example_app/filters/league.py +74 -0
- wbcore/contrib/example_app/filters/match.py +92 -0
- wbcore/contrib/example_app/filters/person.py +77 -0
- wbcore/contrib/example_app/filters/role.py +17 -0
- wbcore/contrib/example_app/filters/sport.py +26 -0
- wbcore/contrib/example_app/filters/stadium.py +30 -0
- wbcore/contrib/example_app/filters/team.py +77 -0
- wbcore/contrib/example_app/filters/teamresult.py +47 -0
- wbcore/contrib/example_app/migrations/0001_initial.py +498 -0
- wbcore/contrib/example_app/migrations/0002_sportperson_profile.py +49 -0
- wbcore/contrib/example_app/migrations/0003_change_stadium_capacity.py +31 -0
- wbcore/contrib/example_app/migrations/0004_alter_player_transfer_value.py +21 -0
- wbcore/contrib/example_app/migrations/0005_sportperson_profile_image.py +23 -0
- wbcore/contrib/example_app/migrations/0006_league_season_period_player_is_active_and_more.py +116 -0
- wbcore/contrib/example_app/migrations/0007_alter_player_options_alter_team_options_and_more.py +40 -0
- wbcore/contrib/example_app/migrations/__init__.py +0 -0
- wbcore/contrib/example_app/models.py +906 -0
- wbcore/contrib/example_app/serializers/__init__.py +31 -0
- wbcore/contrib/example_app/serializers/league.py +111 -0
- wbcore/contrib/example_app/serializers/match_event.py +304 -0
- wbcore/contrib/example_app/serializers/person_team.py +280 -0
- wbcore/contrib/example_app/serializers/role.py +16 -0
- wbcore/contrib/example_app/serializers/season.py +53 -0
- wbcore/contrib/example_app/serializers/sport.py +37 -0
- wbcore/contrib/example_app/serializers/stadium.py +58 -0
- wbcore/contrib/example_app/serializers/teamresult.py +43 -0
- wbcore/contrib/example_app/tests/__init__.py +0 -0
- wbcore/contrib/example_app/tests/conftest.py +13 -0
- wbcore/contrib/example_app/tests/e2e/__init__.py +1 -0
- wbcore/contrib/example_app/tests/e2e/e2e_example_app_utility.py +51 -0
- wbcore/contrib/example_app/tests/e2e/test_league.py +69 -0
- wbcore/contrib/example_app/tests/e2e/test_person.py +67 -0
- wbcore/contrib/example_app/tests/e2e/test_teams.py +56 -0
- wbcore/contrib/example_app/tests/signals.py +7 -0
- wbcore/contrib/example_app/tests/test_displays.py +40 -0
- wbcore/contrib/example_app/tests/test_filters.py +70 -0
- wbcore/contrib/example_app/tests/test_utils.py +25 -0
- wbcore/contrib/example_app/urls.py +79 -0
- wbcore/contrib/example_app/utils.py +20 -0
- wbcore/contrib/example_app/viewsets/__init__.py +29 -0
- wbcore/contrib/example_app/viewsets/buttons/__init__.py +2 -0
- wbcore/contrib/example_app/viewsets/buttons/person.py +18 -0
- wbcore/contrib/example_app/viewsets/buttons/team.py +23 -0
- wbcore/contrib/example_app/viewsets/displays/__init__.py +23 -0
- wbcore/contrib/example_app/viewsets/displays/event.py +186 -0
- wbcore/contrib/example_app/viewsets/displays/league.py +390 -0
- wbcore/contrib/example_app/viewsets/displays/match.py +264 -0
- wbcore/contrib/example_app/viewsets/displays/person.py +251 -0
- wbcore/contrib/example_app/viewsets/displays/role.py +16 -0
- wbcore/contrib/example_app/viewsets/displays/season.py +65 -0
- wbcore/contrib/example_app/viewsets/displays/sport.py +115 -0
- wbcore/contrib/example_app/viewsets/displays/stadium.py +149 -0
- wbcore/contrib/example_app/viewsets/displays/team.py +230 -0
- wbcore/contrib/example_app/viewsets/displays/teamresult.py +30 -0
- wbcore/contrib/example_app/viewsets/endpoints/__init__.py +12 -0
- wbcore/contrib/example_app/viewsets/endpoints/endpoints.py +79 -0
- wbcore/contrib/example_app/viewsets/event.py +287 -0
- wbcore/contrib/example_app/viewsets/league.py +72 -0
- wbcore/contrib/example_app/viewsets/match.py +119 -0
- wbcore/contrib/example_app/viewsets/menu/__init__.py +1 -0
- wbcore/contrib/example_app/viewsets/menu/menus.py +102 -0
- wbcore/contrib/example_app/viewsets/menus.py +63 -0
- wbcore/contrib/example_app/viewsets/person.py +133 -0
- wbcore/contrib/example_app/viewsets/role.py +25 -0
- wbcore/contrib/example_app/viewsets/season.py +23 -0
- wbcore/contrib/example_app/viewsets/sport.py +26 -0
- wbcore/contrib/example_app/viewsets/stadium.py +30 -0
- wbcore/contrib/example_app/viewsets/team.py +68 -0
- wbcore/contrib/example_app/viewsets/teamresult.py +106 -0
- wbcore/contrib/example_app/viewsets/titles/__init__.py +9 -0
- wbcore/contrib/example_app/viewsets/titles/event.py +41 -0
- wbcore/contrib/example_app/viewsets/titles/league.py +24 -0
- wbcore/contrib/example_app/viewsets/titles/match.py +34 -0
- wbcore/contrib/example_app/viewsets/titles/person.py +35 -0
- wbcore/contrib/example_app/viewsets/titles/role.py +13 -0
- wbcore/contrib/example_app/viewsets/titles/sport.py +13 -0
- wbcore/contrib/example_app/viewsets/titles/stadium.py +13 -0
- wbcore/contrib/example_app/viewsets/titles/team.py +24 -0
- wbcore/contrib/example_app/viewsets/titles/teamresult.py +12 -0
- wbcore/contrib/geography/__init__.py +0 -0
- wbcore/contrib/geography/admin.py +30 -0
- wbcore/contrib/geography/apps.py +5 -0
- wbcore/contrib/geography/factories.py +46 -0
- wbcore/contrib/geography/import_export/__init__.py +0 -0
- wbcore/contrib/geography/import_export/resources/__init__.py +0 -0
- wbcore/contrib/geography/import_export/resources/geography.py +10 -0
- wbcore/contrib/geography/migrations/0001_initial.py +110 -0
- wbcore/contrib/geography/migrations/__init__.py +0 -0
- wbcore/contrib/geography/models.py +147 -0
- wbcore/contrib/geography/release_notes/1_0_0.md +13 -0
- wbcore/contrib/geography/release_notes/__init__.py +0 -0
- wbcore/contrib/geography/serializers.py +32 -0
- wbcore/contrib/geography/tests/__init__.py +0 -0
- wbcore/contrib/geography/tests/conftest.py +12 -0
- wbcore/contrib/geography/tests/signals.py +7 -0
- wbcore/contrib/geography/tests/test_models.py +12 -0
- wbcore/contrib/geography/tests/test_serializers.py +7 -0
- wbcore/contrib/geography/tests/test_viewsets.py +6 -0
- wbcore/contrib/geography/tests/tests.py +12 -0
- wbcore/contrib/geography/urls.py +12 -0
- wbcore/contrib/geography/viewsets/__init__.py +1 -0
- wbcore/contrib/geography/viewsets/buttons/__init__.py +0 -0
- wbcore/contrib/geography/viewsets/display/__init__.py +1 -0
- wbcore/contrib/geography/viewsets/display/geography.py +32 -0
- wbcore/contrib/geography/viewsets/endpoints/__init__.py +0 -0
- wbcore/contrib/geography/viewsets/geography.py +57 -0
- wbcore/contrib/geography/viewsets/menu/__init__.py +1 -0
- wbcore/contrib/geography/viewsets/menu/geography.py +8 -0
- wbcore/contrib/geography/viewsets/preview/__init__.py +1 -0
- wbcore/contrib/geography/viewsets/preview/geography.py +19 -0
- wbcore/contrib/geography/viewsets/titles/__init__.py +0 -0
- wbcore/contrib/geography/viewsets/titles/geography.py +14 -0
- wbcore/contrib/gleap/__init__.py +0 -0
- wbcore/contrib/gleap/apps.py +5 -0
- wbcore/contrib/gleap/configs.py +11 -0
- wbcore/contrib/gleap/configurations.py +6 -0
- wbcore/contrib/gleap/hashes.py +13 -0
- wbcore/contrib/gleap/tests/__init__.py +0 -0
- wbcore/contrib/gleap/tests/conftest.py +1 -0
- wbcore/contrib/gleap/tests/tests.py +29 -0
- wbcore/contrib/gleap/urls.py +7 -0
- wbcore/contrib/gleap/views.py +31 -0
- wbcore/contrib/guardian/migrations/0001_initial.py +103 -0
- wbcore/contrib/guardian/migrations/__init__.py +0 -0
- wbcore/contrib/guardian/models/__init__.py +1 -0
- wbcore/contrib/guardian/models/mixins.py +138 -0
- wbcore/contrib/guardian/models/models.py +29 -0
- wbcore/contrib/guardian/tests/__init__.py +0 -0
- wbcore/contrib/guardian/tests/conftest.py +1 -0
- wbcore/contrib/guardian/tests/test_model_mixins.py +93 -0
- wbcore/contrib/guardian/tests/test_tasks.py +77 -0
- wbcore/contrib/guardian/tests/test_utils.py +196 -0
- wbcore/contrib/guardian/tests/test_viewsets.py +48 -0
- wbcore/contrib/guardian/viewsets/__init__.py +1 -0
- wbcore/contrib/guardian/viewsets/configs/__init__.py +4 -0
- wbcore/contrib/guardian/viewsets/configs/buttons.py +27 -0
- wbcore/contrib/guardian/viewsets/configs/displays.py +43 -0
- wbcore/contrib/guardian/viewsets/configs/endpoints.py +18 -0
- wbcore/contrib/guardian/viewsets/configs/titles.py +16 -0
- wbcore/contrib/guardian/viewsets/mixins.py +6 -0
- wbcore/contrib/guardian/viewsets/viewsets.py +139 -0
- wbcore/contrib/icons/__init__.py +1 -0
- wbcore/contrib/icons/apps.py +5 -0
- wbcore/contrib/icons/backends/__init__.py +5 -0
- wbcore/contrib/icons/backends/default.py +374 -0
- wbcore/contrib/icons/backends/material.py +135 -0
- wbcore/contrib/icons/icons.py +169 -0
- wbcore/contrib/icons/models.py +11 -0
- wbcore/contrib/icons/serializers.py +18 -0
- wbcore/contrib/io/__init__.py +0 -0
- wbcore/contrib/io/admin.py +150 -0
- wbcore/contrib/io/apps.py +27 -0
- wbcore/contrib/io/backends/__init__.py +5 -0
- wbcore/contrib/io/backends/abstract.py +60 -0
- wbcore/contrib/io/backends/mail.py +85 -0
- wbcore/contrib/io/backends/sftp.py +63 -0
- wbcore/contrib/io/backends/stream.py +92 -0
- wbcore/contrib/io/backends/utils.py +42 -0
- wbcore/contrib/io/configs/__init__.py +0 -0
- wbcore/contrib/io/configs/endpoints.py +12 -0
- wbcore/contrib/io/configurations/__init__.py +1 -0
- wbcore/contrib/io/configurations/base.py +7 -0
- wbcore/contrib/io/dynamic_preferences_registry.py +15 -0
- wbcore/contrib/io/enums.py +15 -0
- wbcore/contrib/io/exceptions.py +16 -0
- wbcore/contrib/io/factories.py +202 -0
- wbcore/contrib/io/imports.py +305 -0
- wbcore/contrib/io/management/__init__.py +13 -0
- wbcore/contrib/io/migrations/0001_initial_squashed.py +319 -0
- wbcore/contrib/io/migrations/0002_importsource_creator.py +26 -0
- wbcore/contrib/io/migrations/0003_auto_20240103_1000.py +46 -0
- wbcore/contrib/io/migrations/0004_alter_importsource_status_exportsource.py +134 -0
- wbcore/contrib/io/migrations/0005_exportsource_data_alter_exportsource_query_str_and_more.py +67 -0
- wbcore/contrib/io/migrations/0006_alter_exportsource_query_params.py +20 -0
- wbcore/contrib/io/migrations/0007_alter_exportsource_query_params.py +23 -0
- wbcore/contrib/io/migrations/__init__.py +0 -0
- wbcore/contrib/io/mixins.py +37 -0
- wbcore/contrib/io/models.py +1005 -0
- wbcore/contrib/io/release_notes/1_0_0.md +13 -0
- wbcore/contrib/io/release_notes/__init__.py +0 -0
- wbcore/contrib/io/resources.py +177 -0
- wbcore/contrib/io/serializers.py +136 -0
- wbcore/contrib/io/tests/__init__.py +0 -0
- wbcore/contrib/io/tests/conftest.py +42 -0
- wbcore/contrib/io/tests/test_backends.py +128 -0
- wbcore/contrib/io/tests/test_exports.py +130 -0
- wbcore/contrib/io/tests/test_imports.py +167 -0
- wbcore/contrib/io/tests/test_models.py +363 -0
- wbcore/contrib/io/tests/tests.py +18 -0
- wbcore/contrib/io/urls.py +28 -0
- wbcore/contrib/io/utils.py +44 -0
- wbcore/contrib/io/viewset_mixins.py +301 -0
- wbcore/contrib/io/viewsets.py +138 -0
- wbcore/contrib/notifications/__init__.py +0 -0
- wbcore/contrib/notifications/admin.py +61 -0
- wbcore/contrib/notifications/apps.py +38 -0
- wbcore/contrib/notifications/backends/__init__.py +0 -0
- wbcore/contrib/notifications/backends/abstract_backend.py +13 -0
- wbcore/contrib/notifications/backends/console/__init__.py +1 -0
- wbcore/contrib/notifications/backends/console/backends.py +25 -0
- wbcore/contrib/notifications/backends/firebase/__init__.py +1 -0
- wbcore/contrib/notifications/backends/firebase/backends.py +102 -0
- wbcore/contrib/notifications/configs.py +12 -0
- wbcore/contrib/notifications/configurations.py +9 -0
- wbcore/contrib/notifications/dispatch.py +71 -0
- wbcore/contrib/notifications/factories/__init__.py +0 -0
- wbcore/contrib/notifications/factories/notification_types.py +22 -0
- wbcore/contrib/notifications/factories/notifications.py +16 -0
- wbcore/contrib/notifications/factories/tokens.py +11 -0
- wbcore/contrib/notifications/migrations/0001_initial.py +116 -0
- wbcore/contrib/notifications/migrations/0002_notificationusertoken_unique_user_token_device.py +18 -0
- wbcore/contrib/notifications/migrations/0003_notificationusertoken_updated.py +17 -0
- wbcore/contrib/notifications/migrations/0004_alter_notification_body.py +17 -0
- wbcore/contrib/notifications/migrations/0005_alter_notification_endpoint.py +17 -0
- wbcore/contrib/notifications/migrations/0006_notification_created.py +25 -0
- wbcore/contrib/notifications/migrations/__init__.py +0 -0
- wbcore/contrib/notifications/models/__init__.py +3 -0
- wbcore/contrib/notifications/models/notification_types.py +112 -0
- wbcore/contrib/notifications/models/notifications.py +81 -0
- wbcore/contrib/notifications/models/tokens.py +45 -0
- wbcore/contrib/notifications/release_notes/1_0_0.md +13 -0
- wbcore/contrib/notifications/release_notes/__init__.py +0 -0
- wbcore/contrib/notifications/serializers/__init__.py +5 -0
- wbcore/contrib/notifications/serializers/notification_types.py +36 -0
- wbcore/contrib/notifications/serializers/notifications.py +33 -0
- wbcore/contrib/notifications/tasks.py +55 -0
- wbcore/contrib/notifications/tests/__init__.py +0 -0
- wbcore/contrib/notifications/tests/conftest.py +47 -0
- wbcore/contrib/notifications/tests/test_backends/__init__.py +0 -0
- wbcore/contrib/notifications/tests/test_backends/test_firebase.py +78 -0
- wbcore/contrib/notifications/tests/test_configs.py +7 -0
- wbcore/contrib/notifications/tests/test_models/__init__.py +0 -0
- wbcore/contrib/notifications/tests/test_models/test_notification_types.py +84 -0
- wbcore/contrib/notifications/tests/test_models/test_notifications.py +45 -0
- wbcore/contrib/notifications/tests/test_models/test_tokens.py +21 -0
- wbcore/contrib/notifications/tests/test_serializers/__init__.py +0 -0
- wbcore/contrib/notifications/tests/test_serializers/test_notification_types.py +53 -0
- wbcore/contrib/notifications/tests/test_serializers/test_notifications.py +23 -0
- wbcore/contrib/notifications/tests/test_tasks.py +71 -0
- wbcore/contrib/notifications/tests/test_utils.py +0 -0
- wbcore/contrib/notifications/tests/test_viewsets/__init__.py +0 -0
- wbcore/contrib/notifications/tests/test_viewsets/test_notification_types.py +121 -0
- wbcore/contrib/notifications/tests/test_viewsets/test_notifications.py +120 -0
- wbcore/contrib/notifications/urls.py +26 -0
- wbcore/contrib/notifications/utils.py +20 -0
- wbcore/contrib/notifications/views.py +62 -0
- wbcore/contrib/notifications/viewsets/__init__.py +5 -0
- wbcore/contrib/notifications/viewsets/menus.py +14 -0
- wbcore/contrib/notifications/viewsets/notification_types.py +40 -0
- wbcore/contrib/notifications/viewsets/notifications.py +57 -0
- wbcore/contrib/tags/__init__.py +0 -0
- wbcore/contrib/tags/admin.py +17 -0
- wbcore/contrib/tags/apps.py +9 -0
- wbcore/contrib/tags/factories.py +27 -0
- wbcore/contrib/tags/filters.py +42 -0
- wbcore/contrib/tags/migrations/0001_initial.py +62 -0
- wbcore/contrib/tags/migrations/__init__.py +0 -0
- wbcore/contrib/tags/models/__init__.py +2 -0
- wbcore/contrib/tags/models/mixins.py +27 -0
- wbcore/contrib/tags/models/tags.py +103 -0
- wbcore/contrib/tags/release_notes/1_0_0.md +13 -0
- wbcore/contrib/tags/release_notes/__init__.py +0 -0
- wbcore/contrib/tags/serializers.py +87 -0
- wbcore/contrib/tags/signals.py +17 -0
- wbcore/contrib/tags/tests/__init__.py +0 -0
- wbcore/contrib/tags/tests/conftest.py +7 -0
- wbcore/contrib/tags/tests/tests.py +17 -0
- wbcore/contrib/tags/urls.py +14 -0
- wbcore/contrib/tags/viewsets/__init__.py +8 -0
- wbcore/contrib/tags/viewsets/display.py +50 -0
- wbcore/contrib/tags/viewsets/menu.py +15 -0
- wbcore/contrib/tags/viewsets/viewsets.py +58 -0
- wbcore/contrib/workflow/__init__.py +1 -0
- wbcore/contrib/workflow/admin/__init__.py +15 -0
- wbcore/contrib/workflow/admin/condition.py +8 -0
- wbcore/contrib/workflow/admin/data.py +14 -0
- wbcore/contrib/workflow/admin/display.py +8 -0
- wbcore/contrib/workflow/admin/process.py +36 -0
- wbcore/contrib/workflow/admin/step.py +91 -0
- wbcore/contrib/workflow/admin/transition.py +8 -0
- wbcore/contrib/workflow/admin/workflow.py +8 -0
- wbcore/contrib/workflow/apps.py +26 -0
- wbcore/contrib/workflow/configs.py +10 -0
- wbcore/contrib/workflow/decorators.py +25 -0
- wbcore/contrib/workflow/dispatch.py +23 -0
- wbcore/contrib/workflow/factories/__init__.py +18 -0
- wbcore/contrib/workflow/factories/condition.py +15 -0
- wbcore/contrib/workflow/factories/data.py +22 -0
- wbcore/contrib/workflow/factories/display.py +27 -0
- wbcore/contrib/workflow/factories/process.py +69 -0
- wbcore/contrib/workflow/factories/step.py +155 -0
- wbcore/contrib/workflow/factories/transition.py +25 -0
- wbcore/contrib/workflow/factories/workflow.py +19 -0
- wbcore/contrib/workflow/filters/__init__.py +24 -0
- wbcore/contrib/workflow/filters/condition.py +23 -0
- wbcore/contrib/workflow/filters/data.py +24 -0
- wbcore/contrib/workflow/filters/process.py +164 -0
- wbcore/contrib/workflow/filters/step.py +226 -0
- wbcore/contrib/workflow/filters/transition.py +41 -0
- wbcore/contrib/workflow/filters/workflow.py +43 -0
- wbcore/contrib/workflow/migrations/0001_initial.py +788 -0
- wbcore/contrib/workflow/migrations/0002_alter_step_step_type.py +31 -0
- wbcore/contrib/workflow/migrations/0003_alter_condition_attribute_name_and_more.py +75 -0
- wbcore/contrib/workflow/migrations/0004_alter_userstep_assignee_method.py +27 -0
- wbcore/contrib/workflow/migrations/0005_alter_userstep_assignee_method.py +17 -0
- wbcore/contrib/workflow/migrations/__init__.py +0 -0
- wbcore/contrib/workflow/models/__init__.py +19 -0
- wbcore/contrib/workflow/models/condition.py +123 -0
- wbcore/contrib/workflow/models/data.py +238 -0
- wbcore/contrib/workflow/models/display.py +33 -0
- wbcore/contrib/workflow/models/process.py +243 -0
- wbcore/contrib/workflow/models/step.py +735 -0
- wbcore/contrib/workflow/models/transition.py +70 -0
- wbcore/contrib/workflow/models/workflow.py +307 -0
- wbcore/contrib/workflow/serializers/__init__.py +37 -0
- wbcore/contrib/workflow/serializers/condition.py +64 -0
- wbcore/contrib/workflow/serializers/data.py +135 -0
- wbcore/contrib/workflow/serializers/display.py +25 -0
- wbcore/contrib/workflow/serializers/process.py +182 -0
- wbcore/contrib/workflow/serializers/signals.py +25 -0
- wbcore/contrib/workflow/serializers/step.py +364 -0
- wbcore/contrib/workflow/serializers/transition.py +80 -0
- wbcore/contrib/workflow/serializers/workflow.py +124 -0
- wbcore/contrib/workflow/sites.py +43 -0
- wbcore/contrib/workflow/tests/__init__.py +0 -0
- wbcore/contrib/workflow/tests/conftest.py +57 -0
- wbcore/contrib/workflow/tests/test_configs.py +6 -0
- wbcore/contrib/workflow/tests/test_dispatch.py +88 -0
- wbcore/contrib/workflow/tests/test_displays.py +119 -0
- wbcore/contrib/workflow/tests/test_filters.py +86 -0
- wbcore/contrib/workflow/tests/test_serializers.py +177 -0
- wbcore/contrib/workflow/tests/test_viewsets.py +358 -0
- wbcore/contrib/workflow/tests/test_workflow_assignees.py +225 -0
- wbcore/contrib/workflow/tests/tests.py +24 -0
- wbcore/contrib/workflow/urls.py +66 -0
- wbcore/contrib/workflow/utils.py +13 -0
- wbcore/contrib/workflow/viewsets/__init__.py +17 -0
- wbcore/contrib/workflow/viewsets/buttons/__init__.py +1 -0
- wbcore/contrib/workflow/viewsets/buttons/step.py +67 -0
- wbcore/contrib/workflow/viewsets/condition.py +32 -0
- wbcore/contrib/workflow/viewsets/data.py +33 -0
- wbcore/contrib/workflow/viewsets/display/__init__.py +15 -0
- wbcore/contrib/workflow/viewsets/display/condition.py +50 -0
- wbcore/contrib/workflow/viewsets/display/data.py +50 -0
- wbcore/contrib/workflow/viewsets/display/process.py +185 -0
- wbcore/contrib/workflow/viewsets/display/step.py +453 -0
- wbcore/contrib/workflow/viewsets/display/transition.py +75 -0
- wbcore/contrib/workflow/viewsets/display/workflow.py +167 -0
- wbcore/contrib/workflow/viewsets/endpoints/__init__.py +5 -0
- wbcore/contrib/workflow/viewsets/endpoints/condition.py +9 -0
- wbcore/contrib/workflow/viewsets/endpoints/data.py +9 -0
- wbcore/contrib/workflow/viewsets/endpoints/process.py +11 -0
- wbcore/contrib/workflow/viewsets/endpoints/step.py +24 -0
- wbcore/contrib/workflow/viewsets/endpoints/transition.py +18 -0
- wbcore/contrib/workflow/viewsets/menu/__init__.py +15 -0
- wbcore/contrib/workflow/viewsets/menu/condition.py +18 -0
- wbcore/contrib/workflow/viewsets/menu/data.py +18 -0
- wbcore/contrib/workflow/viewsets/menu/process.py +18 -0
- wbcore/contrib/workflow/viewsets/menu/step.py +123 -0
- wbcore/contrib/workflow/viewsets/menu/transition.py +18 -0
- wbcore/contrib/workflow/viewsets/menu/workflow.py +18 -0
- wbcore/contrib/workflow/viewsets/process.py +171 -0
- wbcore/contrib/workflow/viewsets/step.py +229 -0
- wbcore/contrib/workflow/viewsets/titles/__init__.py +16 -0
- wbcore/contrib/workflow/viewsets/titles/condition.py +13 -0
- wbcore/contrib/workflow/viewsets/titles/data.py +13 -0
- wbcore/contrib/workflow/viewsets/titles/process.py +26 -0
- wbcore/contrib/workflow/viewsets/titles/step.py +101 -0
- wbcore/contrib/workflow/viewsets/titles/transition.py +13 -0
- wbcore/contrib/workflow/viewsets/titles/workflow.py +13 -0
- wbcore/contrib/workflow/viewsets/transition.py +57 -0
- wbcore/contrib/workflow/viewsets/workflow.py +36 -0
- wbcore/contrib/workflow/workflows/__init__.py +1 -0
- wbcore/contrib/workflow/workflows/assignees.py +82 -0
- wbcore/crontab/__init__.py +0 -0
- wbcore/crontab/serializers.py +23 -0
- wbcore/crontab/viewsets.py +9 -0
- wbcore/dispatch.py +55 -0
- wbcore/docs/__init__.py +25 -0
- wbcore/docs/orderable.md +29 -0
- wbcore/docs/reparent.md +13 -0
- wbcore/dynamic_preferences_registry.py +29 -0
- wbcore/enums.py +98 -0
- wbcore/filters/__init__.py +21 -0
- wbcore/filters/backends.py +19 -0
- wbcore/filters/defaults.py +69 -0
- wbcore/filters/fields/__init__.py +15 -0
- wbcore/filters/fields/booleans.py +6 -0
- wbcore/filters/fields/choices.py +61 -0
- wbcore/filters/fields/content_type.py +33 -0
- wbcore/filters/fields/datetime.py +91 -0
- wbcore/filters/fields/models.py +113 -0
- wbcore/filters/fields/multiple_lookups.py +19 -0
- wbcore/filters/fields/numbers.py +46 -0
- wbcore/filters/fields/text.py +6 -0
- wbcore/filters/filterset.py +217 -0
- wbcore/filters/lookups.py +41 -0
- wbcore/filters/mixins.py +112 -0
- wbcore/filters/utils.py +21 -0
- wbcore/forms.py +125 -0
- wbcore/frontend.py +23 -0
- wbcore/frontend_user_configuration.py +96 -0
- wbcore/fsm/__init__.py +0 -0
- wbcore/fsm/markdown_extensions.py +31 -0
- wbcore/fsm/mixins.py +114 -0
- wbcore/management/__init__.py +88 -0
- wbcore/management/commands/__init__.py +0 -0
- wbcore/management/commands/bootstrap.py +18 -0
- wbcore/management/commands/clean_obsolete_object.py +21 -0
- wbcore/management/commands/handle_release_notes.py +53 -0
- wbcore/markdown/__init__.py +1 -0
- wbcore/markdown/admin.py +9 -0
- wbcore/markdown/dynamic_preferences_registry.py +16 -0
- wbcore/markdown/models.py +38 -0
- wbcore/markdown/template.py +38 -0
- wbcore/markdown/utils.py +36 -0
- wbcore/markdown/views.py +67 -0
- wbcore/menus/__init__.py +2 -0
- wbcore/menus/menus.py +96 -0
- wbcore/menus/registry.py +28 -0
- wbcore/menus/views.py +41 -0
- wbcore/messages.py +51 -0
- wbcore/metadata/__init__.py +0 -0
- wbcore/metadata/configs/__init__.py +0 -0
- wbcore/metadata/configs/base.py +86 -0
- wbcore/metadata/configs/buttons/__init__.py +4 -0
- wbcore/metadata/configs/buttons/bases.py +91 -0
- wbcore/metadata/configs/buttons/buttons.py +142 -0
- wbcore/metadata/configs/buttons/enums.py +25 -0
- wbcore/metadata/configs/buttons/metadata.py +8 -0
- wbcore/metadata/configs/buttons/view_config.py +176 -0
- wbcore/metadata/configs/display/__init__.py +4 -0
- wbcore/metadata/configs/display/configs.py +15 -0
- wbcore/metadata/configs/display/display.py +234 -0
- wbcore/metadata/configs/display/formatting.py +50 -0
- wbcore/metadata/configs/display/instance_display/__init__.py +15 -0
- wbcore/metadata/configs/display/instance_display/display.py +37 -0
- wbcore/metadata/configs/display/instance_display/enums.py +7 -0
- wbcore/metadata/configs/display/instance_display/layouts/__init__.py +3 -0
- wbcore/metadata/configs/display/instance_display/layouts/inlines.py +58 -0
- wbcore/metadata/configs/display/instance_display/layouts/layouts.py +77 -0
- wbcore/metadata/configs/display/instance_display/layouts/sections.py +43 -0
- wbcore/metadata/configs/display/instance_display/operators.py +22 -0
- wbcore/metadata/configs/display/instance_display/pages.py +40 -0
- wbcore/metadata/configs/display/instance_display/shortcuts.py +70 -0
- wbcore/metadata/configs/display/instance_display/signals.py +3 -0
- wbcore/metadata/configs/display/instance_display/styles.py +42 -0
- wbcore/metadata/configs/display/instance_display/utils.py +74 -0
- wbcore/metadata/configs/display/list_display.py +230 -0
- wbcore/metadata/configs/display/models.py +20 -0
- wbcore/metadata/configs/display/view_config.py +82 -0
- wbcore/metadata/configs/display/views.py +48 -0
- wbcore/metadata/configs/display/windows.py +28 -0
- wbcore/metadata/configs/documentations.py +11 -0
- wbcore/metadata/configs/endpoints.py +176 -0
- wbcore/metadata/configs/fields.py +17 -0
- wbcore/metadata/configs/filter_fields.py +42 -0
- wbcore/metadata/configs/identifiers.py +25 -0
- wbcore/metadata/configs/ordering_fields.py +24 -0
- wbcore/metadata/configs/paginations.py +11 -0
- wbcore/metadata/configs/preview.py +41 -0
- wbcore/metadata/configs/primary_keys.py +9 -0
- wbcore/metadata/configs/search_fields.py +9 -0
- wbcore/metadata/configs/titles.py +48 -0
- wbcore/metadata/configs/window_types.py +13 -0
- wbcore/metadata/exceptions.py +0 -0
- wbcore/metadata/metadata.py +33 -0
- wbcore/metadata/mixins.py +105 -0
- wbcore/metadata/tests/__init__.py +0 -0
- wbcore/metadata/tests/test_buttons.py +179 -0
- wbcore/metadata/utils.py +4 -0
- wbcore/migrations/0001_initial_squashed_squashed_0010_preset_appliedpreset.py +398 -0
- wbcore/migrations/0011_genericmodel.py +22 -0
- wbcore/migrations/0012_delete_notification.py +15 -0
- wbcore/migrations/0013_delete_colorgradient.py +14 -0
- wbcore/migrations/0014_biguserobjectpermission_system.py +44 -0
- wbcore/migrations/__init__.py +0 -0
- wbcore/models/__init__.py +6 -0
- wbcore/models/base.py +182 -0
- wbcore/models/fields.py +29 -0
- wbcore/models/orderable.py +6 -0
- wbcore/pagination.py +65 -0
- wbcore/pandas/__init__.py +0 -0
- wbcore/pandas/fields.py +136 -0
- wbcore/pandas/filters.py +113 -0
- wbcore/pandas/filterset.py +26 -0
- wbcore/pandas/metadata.py +14 -0
- wbcore/pandas/utils.py +129 -0
- wbcore/pandas/views.py +157 -0
- wbcore/permissions/__init__.py +0 -0
- wbcore/permissions/backend.py +35 -0
- wbcore/permissions/mixins.py +72 -0
- wbcore/permissions/permissions.py +50 -0
- wbcore/permissions/registry.py +32 -0
- wbcore/permissions/shortcuts.py +37 -0
- wbcore/permissions/utils.py +26 -0
- wbcore/release_notes/__init__.py +0 -0
- wbcore/release_notes/admin.py +30 -0
- wbcore/release_notes/buttons.py +25 -0
- wbcore/release_notes/display.py +57 -0
- wbcore/release_notes/filters.py +35 -0
- wbcore/release_notes/models.py +65 -0
- wbcore/release_notes/serializers.py +19 -0
- wbcore/release_notes/utils.py +14 -0
- wbcore/release_notes/viewsets.py +45 -0
- wbcore/reversion/__init__.py +0 -0
- wbcore/reversion/filters.py +38 -0
- wbcore/reversion/serializers.py +80 -0
- wbcore/reversion/viewsets/__init__.py +6 -0
- wbcore/reversion/viewsets/buttons.py +94 -0
- wbcore/reversion/viewsets/displays.py +44 -0
- wbcore/reversion/viewsets/endpoints.py +29 -0
- wbcore/reversion/viewsets/titles.py +17 -0
- wbcore/reversion/viewsets/viewsets.py +111 -0
- wbcore/routers.py +63 -0
- wbcore/search/__init__.py +62 -0
- wbcore/serializers/__init__.py +66 -0
- wbcore/serializers/fields/__init__.py +58 -0
- wbcore/serializers/fields/boolean.py +51 -0
- wbcore/serializers/fields/choice.py +36 -0
- wbcore/serializers/fields/datetime.py +143 -0
- wbcore/serializers/fields/fields.py +190 -0
- wbcore/serializers/fields/file.py +20 -0
- wbcore/serializers/fields/fsm.py +20 -0
- wbcore/serializers/fields/json.py +56 -0
- wbcore/serializers/fields/list.py +105 -0
- wbcore/serializers/fields/mixins.py +175 -0
- wbcore/serializers/fields/number.py +103 -0
- wbcore/serializers/fields/other.py +42 -0
- wbcore/serializers/fields/primary_key.py +22 -0
- wbcore/serializers/fields/related.py +152 -0
- wbcore/serializers/fields/text.py +136 -0
- wbcore/serializers/fields/types.py +41 -0
- wbcore/serializers/mixins.py +24 -0
- wbcore/serializers/serializers.py +415 -0
- wbcore/serializers/utils.py +142 -0
- wbcore/shares/__init__.py +1 -0
- wbcore/shares/config.py +63 -0
- wbcore/shares/decorator.py +13 -0
- wbcore/shares/signals.py +3 -0
- wbcore/shares/sites.py +28 -0
- wbcore/shares/views.py +24 -0
- wbcore/signals/__init__.py +6 -0
- wbcore/signals/filters.py +3 -0
- wbcore/signals/instance_buttons.py +5 -0
- wbcore/signals/merge.py +4 -0
- wbcore/signals/models.py +8 -0
- wbcore/signals/permissions.py +3 -0
- wbcore/signals/serializers.py +5 -0
- wbcore/tasks.py +83 -0
- wbcore/templates/reversion/__init__.py +0 -0
- wbcore/templates/reversion/compare_detail.html +19 -0
- wbcore/test/__init__.py +30 -0
- wbcore/test/mixins.py +709 -0
- wbcore/test/signals.py +6 -0
- wbcore/test/tests.py +131 -0
- wbcore/test/utils.py +227 -0
- wbcore/tests/__init__.py +0 -0
- wbcore/tests/conftest.py +55 -0
- wbcore/tests/e2e/__init__.py +0 -0
- wbcore/tests/e2e/test_e2e.py +24 -0
- wbcore/tests/models.py +6 -0
- wbcore/tests/test_cache/__init__.py +0 -0
- wbcore/tests/test_cache/test_decorators.py +30 -0
- wbcore/tests/test_cache/test_mixins.py +29 -0
- wbcore/tests/test_cache/test_registry.py +57 -0
- wbcore/tests/test_configs.py +65 -0
- wbcore/tests/test_enums.py +54 -0
- wbcore/tests/test_fields/__init__.py +0 -0
- wbcore/tests/test_fields/test_boolean_fields.py +48 -0
- wbcore/tests/test_fields/test_choice_fields.py +48 -0
- wbcore/tests/test_fields/test_datetime_fields.py +151 -0
- wbcore/tests/test_fields/test_fields.py +23 -0
- wbcore/tests/test_fields/test_file_fields.py +52 -0
- wbcore/tests/test_fields/test_json_fields.py +28 -0
- wbcore/tests/test_fields/test_list_fields.py +27 -0
- wbcore/tests/test_fields/test_mixins.py +111 -0
- wbcore/tests/test_fields/test_number_fields.py +190 -0
- wbcore/tests/test_fields/test_other_fields.py +61 -0
- wbcore/tests/test_fields/test_primary_key_fields.py +52 -0
- wbcore/tests/test_fields/test_related.py +80 -0
- wbcore/tests/test_fields/test_text_fields.py +117 -0
- wbcore/tests/test_filters/__init__.py +0 -0
- wbcore/tests/test_filters/test_mixins.py +108 -0
- wbcore/tests/test_filters/test_pandas.py +114 -0
- wbcore/tests/test_list_display.py +31 -0
- wbcore/tests/test_models/__init__.py +0 -0
- wbcore/tests/test_models/test_fields.py +25 -0
- wbcore/tests/test_models/test_mixins.py +31 -0
- wbcore/tests/test_new_display/__init__.py +0 -0
- wbcore/tests/test_new_display/test_inlines.py +13 -0
- wbcore/tests/test_new_display/test_layouts.py +15 -0
- wbcore/tests/test_new_display/test_operators.py +15 -0
- wbcore/tests/test_new_display/test_pages.py +8 -0
- wbcore/tests/test_new_display/test_sections.py +0 -0
- wbcore/tests/test_new_display/test_shortcuts.py +38 -0
- wbcore/tests/test_new_display/test_utils.py +48 -0
- wbcore/tests/test_pagination.py +31 -0
- wbcore/tests/test_serializers/__init__.py +0 -0
- wbcore/tests/test_serializers/test_fields.py +140 -0
- wbcore/tests/test_serializers/test_mixins.py +53 -0
- wbcore/tests/test_serializers/test_related.py +77 -0
- wbcore/tests/test_something.py +39 -0
- wbcore/tests/test_utils/__init__.py +0 -0
- wbcore/tests/test_utils/test_date.py +49 -0
- wbcore/tests/test_utils/test_date_builder.py +99 -0
- wbcore/tests/test_utils/test_primary.py +79 -0
- wbcore/tests/test_utils/test_signals.py +38 -0
- wbcore/tests/test_viewsets.py +20 -0
- wbcore/urls.py +114 -0
- wbcore/utils/__init__.py +13 -0
- wbcore/utils/cache.py +8 -0
- wbcore/utils/date.py +220 -0
- wbcore/utils/date_builder/__init__.py +18 -0
- wbcore/utils/date_builder/components.py +42 -0
- wbcore/utils/date_builder/offsets.py +27 -0
- wbcore/utils/deprecations.py +11 -0
- wbcore/utils/enum.py +23 -0
- wbcore/utils/figures.py +290 -0
- wbcore/utils/html.py +8 -0
- wbcore/utils/importlib.py +13 -0
- wbcore/utils/itertools.py +40 -0
- wbcore/utils/models.py +275 -0
- wbcore/utils/numbers.py +69 -0
- wbcore/utils/prettytable.py +35 -0
- wbcore/utils/print.py +29 -0
- wbcore/utils/renderers.py +13 -0
- wbcore/utils/rrules.py +37 -0
- wbcore/utils/serializers.py +8 -0
- wbcore/utils/settings.py +5 -0
- wbcore/utils/signals.py +36 -0
- wbcore/utils/string_loader.py +42 -0
- wbcore/utils/strings.py +77 -0
- wbcore/utils/task.py +6 -0
- wbcore/utils/urls.py +70 -0
- wbcore/utils/views.py +201 -0
- wbcore/views.py +26 -0
- wbcore/viewsets/__init__.py +10 -0
- wbcore/viewsets/encoders.py +18 -0
- wbcore/viewsets/generics.py +5 -0
- wbcore/viewsets/mixins.py +290 -0
- wbcore/viewsets/utils.py +26 -0
- wbcore/viewsets/viewsets.py +142 -0
- wbcore-2.2.1.dist-info/METADATA +62 -0
- wbcore-2.2.1.dist-info/RECORD +1035 -0
- wbcore-2.2.1.dist-info/WHEEL +5 -0
|
@@ -0,0 +1,1005 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import traceback
|
|
7
|
+
import uuid
|
|
8
|
+
from contextlib import suppress
|
|
9
|
+
from copy import deepcopy
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from importlib import import_module
|
|
12
|
+
from typing import Any, Callable, Dict, Optional
|
|
13
|
+
|
|
14
|
+
import magic
|
|
15
|
+
from celery import chain, shared_task
|
|
16
|
+
from croniter import croniter
|
|
17
|
+
from django.apps import apps
|
|
18
|
+
from django.conf import settings
|
|
19
|
+
from django.contrib.contenttypes.fields import GenericForeignKey
|
|
20
|
+
from django.contrib.contenttypes.models import ContentType
|
|
21
|
+
from django.core.exceptions import ValidationError
|
|
22
|
+
from django.core.files.base import ContentFile
|
|
23
|
+
from django.db import models, transaction
|
|
24
|
+
from django.db.models import Q
|
|
25
|
+
from django.db.models.signals import post_delete, pre_delete
|
|
26
|
+
from django.db.utils import IntegrityError
|
|
27
|
+
from django.dispatch import receiver
|
|
28
|
+
from django.utils import timezone
|
|
29
|
+
from django.utils.functional import cached_property
|
|
30
|
+
from django.utils.translation import gettext_lazy as _
|
|
31
|
+
from django_celery_beat.models import CrontabSchedule, PeriodicTask, cronexp
|
|
32
|
+
from import_export.resources import Resource
|
|
33
|
+
from picklefield.fields import PickledObjectField
|
|
34
|
+
from tablib import Dataset
|
|
35
|
+
from tqdm import tqdm
|
|
36
|
+
from wbcore.contrib.notifications.dispatch import send_notification
|
|
37
|
+
from wbcore.contrib.notifications.utils import create_notification_type
|
|
38
|
+
from wbcore.utils.importlib import import_from_dotted_path
|
|
39
|
+
from wbcore.utils.models import ComplexToStringMixin
|
|
40
|
+
|
|
41
|
+
from .enums import ExportFormat, get_django_import_export_format
|
|
42
|
+
from .exceptions import ImportError
|
|
43
|
+
|
|
44
|
+
logger = logging.getLogger("io")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ParserHandler(models.Model):
|
|
48
|
+
class MimeTypeChoices(models.TextChoices):
|
|
49
|
+
CSV = "text/csv", "CSV"
|
|
50
|
+
JSON = "application/json", "JSON"
|
|
51
|
+
XLS = "application/vnd.ms-excel", "XLS"
|
|
52
|
+
XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "XLSX"
|
|
53
|
+
|
|
54
|
+
parser = models.CharField(max_length=255)
|
|
55
|
+
handler = models.CharField(max_length=255)
|
|
56
|
+
|
|
57
|
+
allow_file_type = models.CharField(max_length=68, blank=True, null=True, choices=MimeTypeChoices.choices)
|
|
58
|
+
|
|
59
|
+
def __str__(self) -> str:
|
|
60
|
+
return f"{self.parser}::{self.handler}"
|
|
61
|
+
|
|
62
|
+
def parse(self, import_source: "ImportSource") -> Dict[str, Any]:
|
|
63
|
+
"""
|
|
64
|
+
call the parser on the provided data
|
|
65
|
+
Args:
|
|
66
|
+
import_source: The import source containing the data
|
|
67
|
+
Returns:
|
|
68
|
+
The parsed data
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
parser = importlib.import_module(self.parser)
|
|
72
|
+
return parser.parse(import_source)
|
|
73
|
+
|
|
74
|
+
def handle(self, import_source: "ImportSource", parsed_data: Dict[str, Any], **kwargs):
|
|
75
|
+
"""
|
|
76
|
+
Call the Handler on the parsed data
|
|
77
|
+
Args:
|
|
78
|
+
import_source: The initial import source
|
|
79
|
+
parsed_data: The parsed_data
|
|
80
|
+
"""
|
|
81
|
+
if parsed_data:
|
|
82
|
+
model: Any = apps.get_model(self.handler)
|
|
83
|
+
if handler_class := getattr(model, "import_export_handler_class", None):
|
|
84
|
+
handler = handler_class(import_source)
|
|
85
|
+
handler.process(parsed_data, **kwargs)
|
|
86
|
+
|
|
87
|
+
class Meta:
|
|
88
|
+
unique_together = ("parser", "handler")
|
|
89
|
+
verbose_name = "Parser-Handler"
|
|
90
|
+
verbose_name_plural = "Parsers-Handlers"
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def get_representation_value_key(self) -> str:
|
|
94
|
+
return "id"
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def get_representation_label_key(self) -> str:
|
|
98
|
+
return "{{parser}}::{{handler}}"
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def get_representation_endpoint(self) -> str:
|
|
102
|
+
return "wbcore:io:parserhandlerrepresentation-list"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class ImportedObjectProviderRelationship(ComplexToStringMixin, models.Model):
|
|
106
|
+
"""
|
|
107
|
+
A model that represent the relationship/link between the imported object and a provider.
|
|
108
|
+
|
|
109
|
+
This model can be used to define different identifier in case the object is imported from difference providers
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
provider = models.ForeignKey(
|
|
113
|
+
to="io.Provider", related_name="imported_object_relationships", on_delete=models.CASCADE
|
|
114
|
+
)
|
|
115
|
+
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
|
116
|
+
object_id = models.PositiveIntegerField()
|
|
117
|
+
content_object = GenericForeignKey("content_type", "object_id")
|
|
118
|
+
provider_identifier = models.CharField(max_length=255)
|
|
119
|
+
|
|
120
|
+
def compute_str(self) -> str:
|
|
121
|
+
try:
|
|
122
|
+
return f"{self.provider.title} -> {self.content_object}: {self.provider_identifier}"
|
|
123
|
+
except AttributeError:
|
|
124
|
+
return f"{self.provider.title} ({self.provider_identifier})"
|
|
125
|
+
|
|
126
|
+
class Meta:
|
|
127
|
+
verbose_name = "Content object Provider Identifier relationship"
|
|
128
|
+
verbose_name_plural = "Content object Provider Identifier relationships"
|
|
129
|
+
unique_together = [
|
|
130
|
+
("content_type", "object_id", "provider"),
|
|
131
|
+
("content_type", "provider_identifier", "provider"),
|
|
132
|
+
]
|
|
133
|
+
indexes = [
|
|
134
|
+
models.Index(fields=["content_type", "object_id", "provider"]),
|
|
135
|
+
models.Index(fields=["content_type", "provider_identifier", "provider"]),
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class Provider(models.Model):
|
|
140
|
+
"""
|
|
141
|
+
Represent a data vendor/provider. Every source is linked to a provider and are represented by a unique identifier
|
|
142
|
+
extracted from the data backend root folder
|
|
143
|
+
|
|
144
|
+
Example:
|
|
145
|
+
io/backend/refinitiv/instrument_prices.py => Provider.objects.get(key="refinitiv")
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
title = models.CharField(max_length=255)
|
|
149
|
+
key = models.CharField(max_length=255, unique=True)
|
|
150
|
+
|
|
151
|
+
def __str__(self) -> str:
|
|
152
|
+
return f"{self.title} ({self.key})"
|
|
153
|
+
|
|
154
|
+
class Meta:
|
|
155
|
+
verbose_name = "Provider"
|
|
156
|
+
verbose_name_plural = "Providers"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class DataBackend(models.Model):
|
|
160
|
+
"""
|
|
161
|
+
Represents the instantiated backend imported through the specified dotted path and the passed parameters
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
title = models.CharField(max_length=255)
|
|
165
|
+
|
|
166
|
+
save_data_in_import_source = models.BooleanField(
|
|
167
|
+
default=True, help_text="If true, save the data in the import_source json field"
|
|
168
|
+
)
|
|
169
|
+
passive_only = models.BooleanField(
|
|
170
|
+
default=True, help_text="If True, this data backend is allowed to be called only from the import source."
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
backend_class_path = models.CharField(max_length=512)
|
|
174
|
+
backend_class_name = models.CharField(max_length=128, default="DataBackend")
|
|
175
|
+
|
|
176
|
+
provider = models.ForeignKey(
|
|
177
|
+
"io.Provider",
|
|
178
|
+
on_delete=models.SET_NULL,
|
|
179
|
+
null=True,
|
|
180
|
+
blank=True,
|
|
181
|
+
verbose_name=_("Provider"),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def get_internal_object(self, object_content_type: ContentType, provider_external_id: str) -> models.Model | None:
|
|
185
|
+
"""
|
|
186
|
+
Internal facing function that returns the internal id of the object with the given external provider identifier. If the relationship already exists, we simply returns the stored id. Otherwise we call the appropriate function to get
|
|
187
|
+
Args:
|
|
188
|
+
object_content_type: ContentType object representing the seeked object id
|
|
189
|
+
provider_external_id: The object identifier as given by this backend provider
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
The object as stored in the importer object-provider relationship. None if it doesn't exist yet
|
|
193
|
+
"""
|
|
194
|
+
if rel := ImportedObjectProviderRelationship.objects.filter(
|
|
195
|
+
provider=self.provider, provider_identifier=provider_external_id, content_type=object_content_type
|
|
196
|
+
).first():
|
|
197
|
+
return rel.content_object
|
|
198
|
+
|
|
199
|
+
class Meta:
|
|
200
|
+
verbose_name = "Data Backend"
|
|
201
|
+
verbose_name_plural = "Data Backends"
|
|
202
|
+
|
|
203
|
+
@cached_property
|
|
204
|
+
def backend_class(self) -> Any:
|
|
205
|
+
"""
|
|
206
|
+
Return the imported backend class
|
|
207
|
+
Returns:
|
|
208
|
+
The backend class
|
|
209
|
+
"""
|
|
210
|
+
return getattr(import_module(self.backend_class_path), self.backend_class_name)
|
|
211
|
+
|
|
212
|
+
def __str__(self) -> str:
|
|
213
|
+
return self.title
|
|
214
|
+
|
|
215
|
+
@classmethod
|
|
216
|
+
def get_representation_value_key(self) -> str:
|
|
217
|
+
return "id"
|
|
218
|
+
|
|
219
|
+
@classmethod
|
|
220
|
+
def get_representation_label_key(self) -> str:
|
|
221
|
+
return "{{title}}"
|
|
222
|
+
|
|
223
|
+
@classmethod
|
|
224
|
+
def get_representation_endpoint(self) -> str:
|
|
225
|
+
return "wbcore:io:databackendrepresentation-list"
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class Source(models.Model):
|
|
229
|
+
title = models.CharField(max_length=128)
|
|
230
|
+
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
|
|
231
|
+
parser_handler = models.ManyToManyField(
|
|
232
|
+
"io.ParserHandler",
|
|
233
|
+
verbose_name="Parser/Handler",
|
|
234
|
+
related_name="sources",
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
is_active = models.BooleanField(default=True)
|
|
238
|
+
|
|
239
|
+
connection_parameters = models.JSONField(default=dict, null=True, blank=True)
|
|
240
|
+
import_parameters = models.JSONField(default=dict, null=True, blank=True)
|
|
241
|
+
|
|
242
|
+
credentials = models.ManyToManyField("io.ImportCredential", blank=True, related_name="sources")
|
|
243
|
+
|
|
244
|
+
data_backend = models.ForeignKey("io.DataBackend", on_delete=models.CASCADE, verbose_name=_("Data Backend"))
|
|
245
|
+
|
|
246
|
+
crontab = models.ForeignKey(
|
|
247
|
+
CrontabSchedule,
|
|
248
|
+
on_delete=models.SET_NULL,
|
|
249
|
+
null=True,
|
|
250
|
+
blank=True,
|
|
251
|
+
verbose_name=_("Crontab Schedule"),
|
|
252
|
+
help_text=_("Crontab Schedule to run the task on. " "Set only one schedule type, leave the others null."),
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
periodic_task = models.ForeignKey(
|
|
256
|
+
PeriodicTask,
|
|
257
|
+
on_delete=models.SET_NULL,
|
|
258
|
+
null=True,
|
|
259
|
+
blank=True,
|
|
260
|
+
verbose_name="Periodic Task",
|
|
261
|
+
related_name="sources",
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
import_timedelta_interval = models.IntegerField(default=0)
|
|
265
|
+
|
|
266
|
+
def __init__(self, *args, **kwargs):
|
|
267
|
+
super().__init__(*args, **kwargs)
|
|
268
|
+
self.instantiated_backend: Any = None
|
|
269
|
+
|
|
270
|
+
def init_backend(self, execution_datetime: datetime):
|
|
271
|
+
"""
|
|
272
|
+
Singleton that instantiate the backend attribute
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
execution_datetime: Datetime at which the backend is supposed to be initialize
|
|
276
|
+
|
|
277
|
+
"""
|
|
278
|
+
if not self.instantiated_backend:
|
|
279
|
+
self.instantiated_backend = self.data_backend.backend_class(
|
|
280
|
+
import_credential=self.get_valid_credential(execution_datetime),
|
|
281
|
+
data_backend=self.data_backend,
|
|
282
|
+
**self.connection_parameters,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
def save(self, *args, **kwargs):
|
|
286
|
+
super().save(*args, **kwargs)
|
|
287
|
+
self.update_periodic_task()
|
|
288
|
+
|
|
289
|
+
def __str__(self) -> str:
|
|
290
|
+
return self.title if self.title else f"{self.uuid}"
|
|
291
|
+
|
|
292
|
+
class Meta:
|
|
293
|
+
verbose_name = "Source"
|
|
294
|
+
verbose_name_plural = "Sources"
|
|
295
|
+
|
|
296
|
+
@property
|
|
297
|
+
def current_valid_credential(self) -> "ImportCredential":
|
|
298
|
+
"""
|
|
299
|
+
Property that returns the current valid credential
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
The valid ImportCredential (as of now)
|
|
303
|
+
"""
|
|
304
|
+
return self.get_valid_credential(timezone.now())
|
|
305
|
+
|
|
306
|
+
@property
|
|
307
|
+
def crontab_repr(self) -> str:
|
|
308
|
+
"""
|
|
309
|
+
Returns the crontab string representation.
|
|
310
|
+
:return: crontab string representation
|
|
311
|
+
"""
|
|
312
|
+
if not self.crontab:
|
|
313
|
+
return ""
|
|
314
|
+
|
|
315
|
+
return "{0} {1} {2} {3} {4}".format(
|
|
316
|
+
cronexp(self.crontab.minute),
|
|
317
|
+
cronexp(self.crontab.hour),
|
|
318
|
+
cronexp(self.crontab.day_of_month),
|
|
319
|
+
cronexp(self.crontab.month_of_year),
|
|
320
|
+
cronexp(self.crontab.day_of_week),
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
def update_periodic_task(self):
|
|
324
|
+
"""
|
|
325
|
+
Utility function to find and update the link periodic task given the current source state
|
|
326
|
+
"""
|
|
327
|
+
if self.crontab:
|
|
328
|
+
if task := self.periodic_task:
|
|
329
|
+
task.crontab = self.crontab
|
|
330
|
+
task.enabled = self.is_active
|
|
331
|
+
task.save()
|
|
332
|
+
else:
|
|
333
|
+
task = PeriodicTask.objects.update_or_create(
|
|
334
|
+
name=f"Import-Export: {self}",
|
|
335
|
+
defaults={
|
|
336
|
+
"crontab": self.crontab,
|
|
337
|
+
"args": json.dumps([self.pk]),
|
|
338
|
+
"task": "wbcore.contrib.io.models.trigger_workflow_as_task",
|
|
339
|
+
},
|
|
340
|
+
)[0]
|
|
341
|
+
Source.objects.filter(pk=self.pk).update(periodic_task=task)
|
|
342
|
+
self.refresh_from_db()
|
|
343
|
+
|
|
344
|
+
def get_valid_credential(self, val_datetime: datetime) -> "ImportCredential":
|
|
345
|
+
"""
|
|
346
|
+
Get the valid credential
|
|
347
|
+
Args:
|
|
348
|
+
val_datetime: The date at which the credentials need to be valid
|
|
349
|
+
Returns:
|
|
350
|
+
A valid credential
|
|
351
|
+
"""
|
|
352
|
+
return (
|
|
353
|
+
self.credentials.filter(
|
|
354
|
+
(Q(validity_start__lte=val_datetime) | Q(validity_start__isnull=True))
|
|
355
|
+
& (Q(validity_end__gte=val_datetime) | Q(validity_end__isnull=True))
|
|
356
|
+
)
|
|
357
|
+
.order_by("-validity_end")
|
|
358
|
+
.first()
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
def get_or_create_provider_id(self, obj: models.Model) -> str | None:
|
|
362
|
+
"""
|
|
363
|
+
Workflow method that try to get or fetch the provider identifier for the passed object.
|
|
364
|
+
|
|
365
|
+
If the relationship already exists, we directly returns the stored external id. Otherwise we call the appropriate function on the already instantiated backend to get it from the provider itself. If the value is returned, we store the relationship
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
obj: The object to get the external provider identifier from
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
If it exists, the external identifier (from this source provider). None otherwise.
|
|
372
|
+
"""
|
|
373
|
+
|
|
374
|
+
if (provider := self.data_backend.provider) and self.instantiated_backend is not None:
|
|
375
|
+
if self.instantiated_backend.is_object_valid(obj):
|
|
376
|
+
if rel := ImportedObjectProviderRelationship.objects.filter(
|
|
377
|
+
provider=provider, content_type=ContentType.objects.get_for_model(obj), object_id=obj.pk
|
|
378
|
+
).first():
|
|
379
|
+
return rel.provider_identifier
|
|
380
|
+
else:
|
|
381
|
+
if provider_identifier := self.instantiated_backend.get_provider_id(obj):
|
|
382
|
+
with transaction.atomic():
|
|
383
|
+
ImportedObjectProviderRelationship.objects.create(
|
|
384
|
+
provider=provider,
|
|
385
|
+
content_type=ContentType.objects.get_for_model(obj),
|
|
386
|
+
object_id=obj.pk,
|
|
387
|
+
provider_identifier=provider_identifier,
|
|
388
|
+
)
|
|
389
|
+
return provider_identifier
|
|
390
|
+
obj.save()
|
|
391
|
+
else:
|
|
392
|
+
raise ValueError(
|
|
393
|
+
f"You can't create a provider relationship with {obj} with for backend {self.data_backend} without a provider assigned"
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
def is_valid_date(self, sync_datetime: datetime) -> bool:
|
|
397
|
+
"""
|
|
398
|
+
check wether a date is valid given the stored crontab schedule
|
|
399
|
+
Args:
|
|
400
|
+
sync_datetime: The datetime at which validity needs to be checked
|
|
401
|
+
Returns:
|
|
402
|
+
True if the given date is valid given the source crontab
|
|
403
|
+
"""
|
|
404
|
+
if not self.crontab:
|
|
405
|
+
return False
|
|
406
|
+
return croniter.match(self.crontab_repr, sync_datetime)
|
|
407
|
+
|
|
408
|
+
def trigger_workflow(
|
|
409
|
+
self,
|
|
410
|
+
execution_time: Optional[datetime] = None,
|
|
411
|
+
callback_signature: Callable | None = None,
|
|
412
|
+
callback_args: list | None = None,
|
|
413
|
+
callback_kwargs: Dict | None = None,
|
|
414
|
+
synchronous: bool = False,
|
|
415
|
+
**kwargs,
|
|
416
|
+
):
|
|
417
|
+
"""
|
|
418
|
+
The entry point function of the whole import source workflow.
|
|
419
|
+
Loop over all files return by the attached backend and save the result into a serie of import sources.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
execution_time: The time at which the worfklow was triggerd
|
|
423
|
+
callback_signature: The celery canvas signature to be called upon completion (if any)
|
|
424
|
+
callback_args: the potential positional arguments to be passed to the callback function (if any)
|
|
425
|
+
callback_kwargs: Potential keyword arguments to be passed to the callback function (if any)
|
|
426
|
+
synchronous: If true, will execute the workflow synchronously. Default to False
|
|
427
|
+
**kwargs: keyword arguments to be passed down along the workflow
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
A generator of import sources
|
|
431
|
+
"""
|
|
432
|
+
if not execution_time:
|
|
433
|
+
execution_time = timezone.now()
|
|
434
|
+
# If a queryset is provided, we need to convert it into a list of valid external id that we will get from the relationship through model or from the backend directly if not existing
|
|
435
|
+
|
|
436
|
+
chained_tasks = [
|
|
437
|
+
generate_import_sources_as_task.s(self.pk, execution_time, **kwargs),
|
|
438
|
+
process_import_sources_as_task.s(),
|
|
439
|
+
]
|
|
440
|
+
if not callback_args:
|
|
441
|
+
callback_args = []
|
|
442
|
+
if not callback_kwargs:
|
|
443
|
+
callback_kwargs = {}
|
|
444
|
+
if callback_signature:
|
|
445
|
+
chained_tasks.append(callback_signature.si(*callback_args, **callback_kwargs))
|
|
446
|
+
if synchronous:
|
|
447
|
+
import_source_ids = chained_tasks[0].apply().get()
|
|
448
|
+
chained_tasks[1].apply((import_source_ids,))
|
|
449
|
+
if len(chained_tasks) > 2:
|
|
450
|
+
chained_tasks[2].apply(tuple(callback_args), callback_kwargs)
|
|
451
|
+
else:
|
|
452
|
+
chain(*chained_tasks).apply_async(eta=execution_time)
|
|
453
|
+
|
|
454
|
+
def generate_import_sources(
|
|
455
|
+
self,
|
|
456
|
+
execution_time: datetime,
|
|
457
|
+
only_handler: str | None = None,
|
|
458
|
+
**kwargs,
|
|
459
|
+
) -> list["ImportSource"]:
|
|
460
|
+
"""
|
|
461
|
+
Process the source based on its attached backend (returns an error if such backend does not exist)
|
|
462
|
+
The backend is first initialize through constructor and then, The generator `get_files` is called, passing the
|
|
463
|
+
execution time, the expected data time, and some keyword arguments.
|
|
464
|
+
|
|
465
|
+
For each file returns by the generator, we create a ImportSource objects and trigger its import task
|
|
466
|
+
Args:
|
|
467
|
+
execution_time: The time at which this task was triggered
|
|
468
|
+
**kwargs: keyword arguments to be passed down along the workflow
|
|
469
|
+
Returns:
|
|
470
|
+
Return a generator of import sources
|
|
471
|
+
"""
|
|
472
|
+
if not self.data_backend:
|
|
473
|
+
raise ValueError("Data Backend not specied for this source")
|
|
474
|
+
|
|
475
|
+
# Import the data backend module and extract its class_name
|
|
476
|
+
res = []
|
|
477
|
+
# Get the expected date at which this source should return its data (e.g. T-1)
|
|
478
|
+
|
|
479
|
+
# Loop over the backend returned generator
|
|
480
|
+
self.init_backend(execution_time)
|
|
481
|
+
errors = []
|
|
482
|
+
queryset = kwargs.pop("queryset", self.instantiated_backend.get_default_queryset())
|
|
483
|
+
if self.data_backend.provider and queryset is not None:
|
|
484
|
+
obj_external_ids = []
|
|
485
|
+
for obj in tqdm(queryset, total=queryset.count()):
|
|
486
|
+
try:
|
|
487
|
+
if external_id := self.get_or_create_provider_id(obj):
|
|
488
|
+
obj_external_ids.append(external_id)
|
|
489
|
+
except IntegrityError:
|
|
490
|
+
errors.append(str(obj))
|
|
491
|
+
kwargs["obj_external_ids"] = obj_external_ids
|
|
492
|
+
else:
|
|
493
|
+
kwargs["queryset"] = queryset
|
|
494
|
+
streams = self.instantiated_backend.get_files(
|
|
495
|
+
execution_time,
|
|
496
|
+
**{
|
|
497
|
+
**self.import_parameters,
|
|
498
|
+
**kwargs,
|
|
499
|
+
},
|
|
500
|
+
)
|
|
501
|
+
if streams:
|
|
502
|
+
for file_name, _file in streams:
|
|
503
|
+
content_file = ContentFile(_file.getvalue())
|
|
504
|
+
content_file.name = file_name
|
|
505
|
+
parser_handlers = self.parser_handler.all()
|
|
506
|
+
if only_handler:
|
|
507
|
+
parser_handlers = parser_handlers.filter(handler__iexact=only_handler)
|
|
508
|
+
for parser_handler in parser_handlers:
|
|
509
|
+
with transaction.atomic():
|
|
510
|
+
import_source = ImportSource.objects.create(
|
|
511
|
+
file=content_file,
|
|
512
|
+
parser_handler=parser_handler,
|
|
513
|
+
save_data=self.data_backend.save_data_in_import_source,
|
|
514
|
+
source=self,
|
|
515
|
+
)
|
|
516
|
+
res.append(import_source)
|
|
517
|
+
return res
|
|
518
|
+
|
|
519
|
+
@classmethod
|
|
520
|
+
def load_sources_from_settings(self, settings: list[tuple[list[tuple[str, str]], str, dict[str, Any]]]):
|
|
521
|
+
"""
|
|
522
|
+
Utility classmethod to parser sources from the settings.
|
|
523
|
+
|
|
524
|
+
We assume data structure as follow
|
|
525
|
+
|
|
526
|
+
[
|
|
527
|
+
(
|
|
528
|
+
[("module1.HandlerModel1", "module1.io.parsers.provider1.handler1")],
|
|
529
|
+
"module1.io.backends.provider1.handler1.DataBackend",
|
|
530
|
+
{
|
|
531
|
+
"credentials": [credential_dict],
|
|
532
|
+
"crontab": "10 9,17,1 * * *",
|
|
533
|
+
},
|
|
534
|
+
),
|
|
535
|
+
(
|
|
536
|
+
[("module2.HandlerModel2", "module2.io.parsers.provider2.handler2")],
|
|
537
|
+
"module2.io.backends.provider2.handler2.DataBackend",
|
|
538
|
+
)
|
|
539
|
+
]
|
|
540
|
+
Args:
|
|
541
|
+
settings: The source settings to parse
|
|
542
|
+
|
|
543
|
+
"""
|
|
544
|
+
for parser_handlers, data_backend_module, config in settings:
|
|
545
|
+
with suppress(Exception):
|
|
546
|
+
# This method is loaded when server boots. If this fail, it can prevent the server from starting.
|
|
547
|
+
backend_class_path, _, backend_class_name = data_backend_module.rpartition(".")
|
|
548
|
+
|
|
549
|
+
data_backend = DataBackend.objects.get(
|
|
550
|
+
backend_class_path=backend_class_path, backend_class_name=backend_class_name
|
|
551
|
+
)
|
|
552
|
+
crontab = None
|
|
553
|
+
credentials = config.pop("credentials", [])
|
|
554
|
+
if crontab_data := config.pop("crontab", None):
|
|
555
|
+
minute, hour, day_of_month, month_of_year, day_of_week = crontab_data.split(" ")
|
|
556
|
+
crontab_kwargs = {
|
|
557
|
+
"minute": minute,
|
|
558
|
+
"hour": hour,
|
|
559
|
+
"day_of_week": day_of_week,
|
|
560
|
+
"day_of_month": day_of_month,
|
|
561
|
+
"month_of_year": month_of_year,
|
|
562
|
+
}
|
|
563
|
+
crontabs = CrontabSchedule.objects.filter(**crontab_kwargs)
|
|
564
|
+
if crontabs.exists():
|
|
565
|
+
crontab = crontabs.first()
|
|
566
|
+
else:
|
|
567
|
+
crontab = CrontabSchedule.objects.create(**crontab_kwargs)
|
|
568
|
+
|
|
569
|
+
source, source_created = Source.objects.get_or_create(
|
|
570
|
+
data_backend=data_backend,
|
|
571
|
+
defaults={"crontab": crontab, **config},
|
|
572
|
+
)
|
|
573
|
+
for handler, parser in parser_handlers:
|
|
574
|
+
parser_handler = ParserHandler.objects.get_or_create(parser=parser, handler=handler)[0]
|
|
575
|
+
if parser_handler not in source.parser_handler.all():
|
|
576
|
+
source.parser_handler.add(parser_handler)
|
|
577
|
+
for credential_data in credentials:
|
|
578
|
+
credential, created = ImportCredential.objects.get_or_create(
|
|
579
|
+
key=credential_data["key"], defaults=credential_data
|
|
580
|
+
)
|
|
581
|
+
if credential not in source.credentials.all():
|
|
582
|
+
source.credentials.add(credential)
|
|
583
|
+
source.save()
|
|
584
|
+
|
|
585
|
+
@classmethod
|
|
586
|
+
def get_representation_value_key(self) -> str:
|
|
587
|
+
return "id"
|
|
588
|
+
|
|
589
|
+
@classmethod
|
|
590
|
+
def get_representation_label_key(self) -> str:
|
|
591
|
+
return "{{title}} ({{id}})"
|
|
592
|
+
|
|
593
|
+
@classmethod
|
|
594
|
+
def get_representation_endpoint(self) -> str:
|
|
595
|
+
return "wbcore:io:sourcerepresentation-list"
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
@receiver(post_delete, sender="io.Source")
|
|
599
|
+
def post_delete_source(sender, instance, **kwargs):
|
|
600
|
+
# Delete the attached period task
|
|
601
|
+
if instance.periodic_task:
|
|
602
|
+
instance.periodic_task.delete()
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
@receiver(pre_delete, sender="io.Source")
|
|
606
|
+
def pre_delete_source(sender, instance, **kwargs):
|
|
607
|
+
# Unassign import source from the deleting source
|
|
608
|
+
ImportSource.objects.filter(source=instance).update(source=None)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
@shared_task(queue="importexport")
|
|
612
|
+
def trigger_workflow_as_task(source_id: int, execution_time: datetime | None = None, **kwargs):
|
|
613
|
+
"""
|
|
614
|
+
Call the `import_source` as a celery task
|
|
615
|
+
"""
|
|
616
|
+
source = Source.objects.get(id=source_id)
|
|
617
|
+
source.trigger_workflow(execution_time=execution_time, **kwargs)
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
@shared_task(queue="importexport")
|
|
621
|
+
def generate_import_sources_as_task(source_id: int, execution_time: datetime, **kwargs) -> list[int]:
|
|
622
|
+
"""
|
|
623
|
+
Call the `import_source` as a celery task
|
|
624
|
+
"""
|
|
625
|
+
source = Source.objects.get(id=source_id)
|
|
626
|
+
import_source_ids = [
|
|
627
|
+
import_source.pk for import_source in source.generate_import_sources(execution_time, **kwargs)
|
|
628
|
+
] # convert to list to be json serializable
|
|
629
|
+
return import_source_ids
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
@shared_task(queue="importexport")
|
|
633
|
+
def process_import_sources_as_task(import_source_ids: list[int]):
|
|
634
|
+
"""
|
|
635
|
+
Call the `import_source` as a celery task
|
|
636
|
+
"""
|
|
637
|
+
errors = []
|
|
638
|
+
for import_source_id in import_source_ids:
|
|
639
|
+
import_source = ImportSource.objects.get(id=import_source_id)
|
|
640
|
+
try:
|
|
641
|
+
import_source.import_data(force_reimport=True)
|
|
642
|
+
except ImportError:
|
|
643
|
+
errors.append(import_source_id)
|
|
644
|
+
if len(errors) > 0:
|
|
645
|
+
raise ImportError(f"Error while processing import source {errors}")
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
class ImportExportSource(models.Model):
|
|
649
|
+
class Status(models.TextChoices):
|
|
650
|
+
PENDING = "PENDING", "Pending"
|
|
651
|
+
PROCESSED = "PROCESSED", "Processed"
|
|
652
|
+
WARNING = "WARNING", "Warning"
|
|
653
|
+
ERROR = "ERROR", "Error"
|
|
654
|
+
IGNORED = "IGNORED", "Ignore"
|
|
655
|
+
|
|
656
|
+
created = models.DateTimeField(auto_now_add=True)
|
|
657
|
+
last_updated = models.DateTimeField(auto_now=True)
|
|
658
|
+
|
|
659
|
+
status = models.CharField(
|
|
660
|
+
max_length=16,
|
|
661
|
+
choices=Status.choices,
|
|
662
|
+
default=Status.PENDING.value,
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
log = models.TextField(null=True, blank=True)
|
|
666
|
+
origin = models.CharField(max_length=255, null=True, blank=True)
|
|
667
|
+
creator = models.ForeignKey(
|
|
668
|
+
"authentication.User", null=True, blank=True, verbose_name="Creator", on_delete=models.SET_NULL
|
|
669
|
+
)
|
|
670
|
+
data = models.JSONField(default=dict, null=True, blank=True)
|
|
671
|
+
|
|
672
|
+
class Meta:
|
|
673
|
+
abstract = True
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
class ExportSource(ImportExportSource):
|
|
677
|
+
file = models.FileField(max_length=256, upload_to="io/export_source/files", null=True, blank=True)
|
|
678
|
+
format = models.IntegerField(
|
|
679
|
+
choices=ExportFormat.choices,
|
|
680
|
+
default=ExportFormat.CSV.value,
|
|
681
|
+
verbose_name=_("Format of file to be exported"),
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
content_type = models.ForeignKey(ContentType, verbose_name=_("Export job Content Type"), on_delete=models.CASCADE)
|
|
685
|
+
|
|
686
|
+
resource_path = models.CharField(
|
|
687
|
+
verbose_name=_("Resource path to use when exporting"), max_length=255, default="", blank=True, null=True
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
resource_kwargs = models.JSONField(default=dict, blank=True, null=False)
|
|
691
|
+
|
|
692
|
+
query_str = models.TextField(verbose_name=_("SQL query to be executed"), blank=True, null=False)
|
|
693
|
+
query_params = PickledObjectField( # we have to use picklefield because the sql parameters are given as python object. However, no django model should be stored in this field, which should be the case as these objects are given through their IDs
|
|
694
|
+
blank=True, verbose_name=_("SQL query parameters to be used with the sql query"), default=list
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
class Meta:
|
|
698
|
+
verbose_name = _("Export Source")
|
|
699
|
+
verbose_name_plural = _("Export Sources")
|
|
700
|
+
notification_types = [
|
|
701
|
+
create_notification_type(
|
|
702
|
+
code="io.export_done",
|
|
703
|
+
title="File Export has finished",
|
|
704
|
+
help_text="Notifies user when their submitted export files is done and available",
|
|
705
|
+
web=True,
|
|
706
|
+
mobile=True,
|
|
707
|
+
email=True,
|
|
708
|
+
)
|
|
709
|
+
]
|
|
710
|
+
constraints = [
|
|
711
|
+
models.CheckConstraint(
|
|
712
|
+
check=Q(query_str__isnull=False, resource_path__isnull=False) | ~Q(data__exact=dict()),
|
|
713
|
+
name="check_either_data_or_resource_isnotnull",
|
|
714
|
+
)
|
|
715
|
+
]
|
|
716
|
+
|
|
717
|
+
@property
|
|
718
|
+
def file_format(self):
|
|
719
|
+
return get_django_import_export_format(self.format)()
|
|
720
|
+
|
|
721
|
+
@property
|
|
722
|
+
def resource(self) -> Resource:
|
|
723
|
+
"""
|
|
724
|
+
Load into an attribute the instantiated resource loaded from the resource path
|
|
725
|
+
"""
|
|
726
|
+
resource_class = import_from_dotted_path(self.resource_path)
|
|
727
|
+
return resource_class(**self.resource_kwargs)
|
|
728
|
+
|
|
729
|
+
@property
|
|
730
|
+
def queryset(self) -> models.QuerySet:
|
|
731
|
+
"""
|
|
732
|
+
Recreate the base queryset from the content type model class base manager and the saved query string and parameters
|
|
733
|
+
"""
|
|
734
|
+
|
|
735
|
+
return self.content_type.model_class().objects.raw(self.query_str, self.query_params)
|
|
736
|
+
|
|
737
|
+
def get_export_filename(self):
|
|
738
|
+
date_str = timezone.now().strftime("%Y-%m-%d")
|
|
739
|
+
ts = datetime.timestamp(timezone.now())
|
|
740
|
+
filename = "%s-%s_%s.%s" % (
|
|
741
|
+
self.content_type.model_class().__name__,
|
|
742
|
+
date_str,
|
|
743
|
+
ts,
|
|
744
|
+
self.file_format.get_extension(),
|
|
745
|
+
)
|
|
746
|
+
return filename
|
|
747
|
+
|
|
748
|
+
def notify(self):
|
|
749
|
+
"""
|
|
750
|
+
Notify the user if the export job is done and accessible
|
|
751
|
+
"""
|
|
752
|
+
if self.file and self.status == self.Status.PROCESSED:
|
|
753
|
+
send_notification(
|
|
754
|
+
code="io.export_done",
|
|
755
|
+
title=_("Your export file is available"),
|
|
756
|
+
body=_("<p>The export job you requested is finished and available for one hour.</p>").format(
|
|
757
|
+
url=self.file.url
|
|
758
|
+
),
|
|
759
|
+
endpoint=self.file.url,
|
|
760
|
+
user=self.creator,
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
def export_data(self, **kwargs):
|
|
764
|
+
self.log = ""
|
|
765
|
+
self.status = self.Status.PENDING.name
|
|
766
|
+
self.save()
|
|
767
|
+
try:
|
|
768
|
+
file_format = get_django_import_export_format(self.format)()
|
|
769
|
+
if self.data:
|
|
770
|
+
# Data was saved as a dictionary from a tablib.dataset, we need to recreate it
|
|
771
|
+
data = Dataset()
|
|
772
|
+
data.headers = self.data["headers"]
|
|
773
|
+
data.extend(self.data["data"])
|
|
774
|
+
else:
|
|
775
|
+
data = self.resource.export(queryset=self.queryset)
|
|
776
|
+
|
|
777
|
+
export_data = file_format.export_data(data)
|
|
778
|
+
if not file_format.is_binary():
|
|
779
|
+
export_data = export_data.encode("utf-8")
|
|
780
|
+
self.file.save(self.get_export_filename(), ContentFile(export_data))
|
|
781
|
+
self.status = self.Status.PROCESSED.name
|
|
782
|
+
self.save()
|
|
783
|
+
self.notify()
|
|
784
|
+
|
|
785
|
+
except Exception:
|
|
786
|
+
ex_type, ex_value, _ = sys.exc_info()
|
|
787
|
+
self.status = self.Status.ERROR.name
|
|
788
|
+
self.log = f"{ex_type}: {ex_value}\n"
|
|
789
|
+
self.log += traceback.format_exc()
|
|
790
|
+
self.save()
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
class ImportSource(ImportExportSource):
|
|
794
|
+
file = models.FileField(max_length=256, upload_to="io/import_source/files", null=True, blank=True)
|
|
795
|
+
|
|
796
|
+
save_data = models.BooleanField(default=True, help_text="If True, will save the raw data in a field")
|
|
797
|
+
progress_index = models.PositiveIntegerField(default=0)
|
|
798
|
+
|
|
799
|
+
parser_handler = models.ForeignKey("io.ParserHandler", on_delete=models.CASCADE)
|
|
800
|
+
source = models.ForeignKey("io.Source", null=True, blank=True, verbose_name="Source", on_delete=models.SET_NULL)
|
|
801
|
+
errors_log = models.TextField(null=True, blank=True)
|
|
802
|
+
|
|
803
|
+
class Meta:
|
|
804
|
+
verbose_name = "Import Source"
|
|
805
|
+
verbose_name_plural = "Import Sources"
|
|
806
|
+
notification_types = [
|
|
807
|
+
create_notification_type(
|
|
808
|
+
code="io.import_done",
|
|
809
|
+
title="File Import has finished",
|
|
810
|
+
help_text="Notifies user when their submitted import files has finished",
|
|
811
|
+
)
|
|
812
|
+
]
|
|
813
|
+
|
|
814
|
+
def __str__(self) -> str:
|
|
815
|
+
return f"{self.pk}"
|
|
816
|
+
|
|
817
|
+
def _validate_file_type(self):
|
|
818
|
+
"""
|
|
819
|
+
Valid the file type based on the allowed types specified by the parser
|
|
820
|
+
Returns:
|
|
821
|
+
True if the filetype is valid
|
|
822
|
+
"""
|
|
823
|
+
if (upload := self.file) and (allow_file_type := self.parser_handler.allow_file_type):
|
|
824
|
+
file_type = magic.from_buffer(upload.file.read(1024), mime=True)
|
|
825
|
+
if file_type != allow_file_type:
|
|
826
|
+
raise ValidationError("File type not supported.")
|
|
827
|
+
|
|
828
|
+
def save(self, *args, **kwargs):
|
|
829
|
+
# Check here company name, modify ...
|
|
830
|
+
self._validate_file_type()
|
|
831
|
+
super().save(*args, **kwargs)
|
|
832
|
+
|
|
833
|
+
def _parse_data(self) -> dict[str, Any]:
|
|
834
|
+
"""
|
|
835
|
+
Given the ParserHandler linked object, we import its related module and call its defined `parse` function
|
|
836
|
+
Returns:
|
|
837
|
+
Dict The parsed data as a python dictionary
|
|
838
|
+
"""
|
|
839
|
+
if not self.file:
|
|
840
|
+
raise ValueError("This import source does not include a valid file")
|
|
841
|
+
parsed_data = self.parser_handler.parse(self)
|
|
842
|
+
if self.save_data:
|
|
843
|
+
self.data = parsed_data
|
|
844
|
+
self.save()
|
|
845
|
+
return parsed_data
|
|
846
|
+
|
|
847
|
+
def _process_data(self, parsed_data: dict[str, Any], **kwargs):
|
|
848
|
+
"""
|
|
849
|
+
Given the parsed data (as a python dictionary), import the corresponding handler and calls its defined `process
|
|
850
|
+
Args:
|
|
851
|
+
data: The deserialized data to be passed down to the parser
|
|
852
|
+
"""
|
|
853
|
+
self.status = self.Status.PROCESSED.name
|
|
854
|
+
if parsed_data:
|
|
855
|
+
parsed_data_copy = deepcopy(parsed_data)
|
|
856
|
+
if data := parsed_data_copy.pop("data", None):
|
|
857
|
+
self.parser_handler.handle(self, {"data": data[self.progress_index :], **parsed_data_copy}, **kwargs)
|
|
858
|
+
if self.errors_log:
|
|
859
|
+
self.status = self.Status.WARNING.name
|
|
860
|
+
self.save()
|
|
861
|
+
|
|
862
|
+
def import_data(self, force_reimport: Optional[bool] = False, **kwargs):
|
|
863
|
+
"""
|
|
864
|
+
General workflow method:
|
|
865
|
+
* Parse the data given the linked import file
|
|
866
|
+
* Handle the parsed data
|
|
867
|
+
* Change the import_source status based on sucess
|
|
868
|
+
Args:
|
|
869
|
+
silent: False if this method is not suppose to silently catch the exceptions
|
|
870
|
+
"""
|
|
871
|
+
if force_reimport:
|
|
872
|
+
self.progress_index = 0
|
|
873
|
+
self.log = ""
|
|
874
|
+
self.errors_log = ""
|
|
875
|
+
# self.data = {}
|
|
876
|
+
self.status = self.Status.PENDING.name
|
|
877
|
+
self.save()
|
|
878
|
+
debug = kwargs.get("debug", settings.DEBUG)
|
|
879
|
+
try:
|
|
880
|
+
data = self._parse_data()
|
|
881
|
+
self._process_data(data, debug=debug)
|
|
882
|
+
except Exception as e:
|
|
883
|
+
ex_type, ex_value, _ = sys.exc_info()
|
|
884
|
+
self.status = self.Status.ERROR.name
|
|
885
|
+
self.log = f"{ex_type}: {ex_value}\n"
|
|
886
|
+
self.log += traceback.format_exc()
|
|
887
|
+
self.save()
|
|
888
|
+
if not debug:
|
|
889
|
+
raise ImportError(
|
|
890
|
+
f"Could not import file {self.file.name} for backend {self.source.data_backend.title} and parser/handler {self.parser_handler}"
|
|
891
|
+
)
|
|
892
|
+
else:
|
|
893
|
+
raise e
|
|
894
|
+
|
|
895
|
+
@classmethod
|
|
896
|
+
def get_representation_value_key(self) -> str:
|
|
897
|
+
return "id"
|
|
898
|
+
|
|
899
|
+
@classmethod
|
|
900
|
+
def get_representation_label_key(self) -> str:
|
|
901
|
+
return "{{file}}"
|
|
902
|
+
|
|
903
|
+
@classmethod
|
|
904
|
+
def get_representation_endpoint(self) -> str:
|
|
905
|
+
return "wbcore:io:importsourcerepresentation-list"
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
@shared_task(queue="importexport")
|
|
909
|
+
def import_data_as_task(import_source_id: int, **kwargs):
|
|
910
|
+
"""
|
|
911
|
+
Call `import_data` as a celery task
|
|
912
|
+
:param Int The import_source id
|
|
913
|
+
"""
|
|
914
|
+
import_source = ImportSource.objects.get(id=import_source_id)
|
|
915
|
+
import_source.import_data(**kwargs)
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
@shared_task(queue="importexport")
|
|
919
|
+
def export_data_as_task(export_source_id: int, **kwargs):
|
|
920
|
+
"""
|
|
921
|
+
Call `import_data` as a celery task
|
|
922
|
+
:param Int The import_source id
|
|
923
|
+
"""
|
|
924
|
+
export_source = ExportSource.objects.get(id=export_source_id)
|
|
925
|
+
export_source.export_data(**kwargs)
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
@receiver(models.signals.post_save, sender=ExportSource)
|
|
929
|
+
def post_save_export_source(sender, instance: ExportSource, created: bool, raw: bool, **kwargs):
|
|
930
|
+
"""Triggers the export task on creation"""
|
|
931
|
+
|
|
932
|
+
if not raw and created and instance.status == ExportSource.Status.PENDING:
|
|
933
|
+
transaction.on_commit(lambda: export_data_as_task.delay(instance.id))
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
def validate_key_file(value: models.FileField):
|
|
937
|
+
ext = os.path.splitext(value.name)[1]
|
|
938
|
+
if ext.lower() != ".key":
|
|
939
|
+
raise ValidationError("The file extension needs to be a .key")
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
def validate_pem_file(value: models.FileField):
|
|
943
|
+
ext = os.path.splitext(value.name)[1]
|
|
944
|
+
if ext.lower() != ".pem":
|
|
945
|
+
raise ValidationError("The file extension needs to be a .pem")
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
class ImportCredential(models.Model):
|
|
949
|
+
class Type(models.TextChoices):
|
|
950
|
+
CREDENTIAL = "CREDENTIAL", "Credential"
|
|
951
|
+
AUTHENTICATION_TOKEN = "AUTHENTICATION_TOKEN", "Authentication Token"
|
|
952
|
+
CERTIFICATE = "CERTIFICATE", "Certificate"
|
|
953
|
+
|
|
954
|
+
key = models.CharField(max_length=255)
|
|
955
|
+
type = models.CharField(choices=Type.choices, default=Type.CREDENTIAL, max_length=64)
|
|
956
|
+
|
|
957
|
+
username = models.CharField(max_length=255, null=True, blank=True)
|
|
958
|
+
password = models.CharField(max_length=255, null=True, blank=True)
|
|
959
|
+
|
|
960
|
+
authentication_token = models.CharField(max_length=2048, null=True, blank=True)
|
|
961
|
+
|
|
962
|
+
certificate_pem = models.FileField(
|
|
963
|
+
max_length=256,
|
|
964
|
+
null=True,
|
|
965
|
+
blank=True,
|
|
966
|
+
upload_to="io/import_credential/certificates",
|
|
967
|
+
help_text="We are expecting a .cert file",
|
|
968
|
+
validators=[validate_pem_file],
|
|
969
|
+
)
|
|
970
|
+
certificate_key = models.FileField(
|
|
971
|
+
max_length=256,
|
|
972
|
+
null=True,
|
|
973
|
+
blank=True,
|
|
974
|
+
upload_to="io/import_credential/certificates",
|
|
975
|
+
help_text="We are expecting a .key file",
|
|
976
|
+
validators=[validate_key_file],
|
|
977
|
+
)
|
|
978
|
+
|
|
979
|
+
additional_resources = models.JSONField(default=dict, null=True, blank=True)
|
|
980
|
+
|
|
981
|
+
validity_start = models.DateTimeField(null=True, blank=True)
|
|
982
|
+
validity_end = models.DateTimeField(null=True, blank=True)
|
|
983
|
+
|
|
984
|
+
def save(self, *args, **kwargs):
|
|
985
|
+
if self.type == self.Type.CREDENTIAL and (not self.username or not self.password):
|
|
986
|
+
raise ValidationError("The type is credential, you need to specify a username and a password")
|
|
987
|
+
elif self.type == self.Type.AUTHENTICATION_TOKEN and not self.authentication_token:
|
|
988
|
+
raise ValidationError("The type is authentication header, you need to specify a valid header")
|
|
989
|
+
elif self.type == self.Type.CERTIFICATE and (not self.certificate_key or not self.certificate_pem):
|
|
990
|
+
raise ValidationError("The type is certificate, you need to specify valid public and private key files")
|
|
991
|
+
super().save(*args, **kwargs)
|
|
992
|
+
|
|
993
|
+
class Meta:
|
|
994
|
+
verbose_name = "Import Credential"
|
|
995
|
+
verbose_name_plural = "Import Credentials"
|
|
996
|
+
|
|
997
|
+
def __str__(self) -> str:
|
|
998
|
+
dates_repr = ""
|
|
999
|
+
if self.validity_start:
|
|
1000
|
+
dates_repr += f"{self.validity_start:%Y-%m-%d %H:%M:%S}"
|
|
1001
|
+
if self.validity_end:
|
|
1002
|
+
dates_repr += f"- {self.validity_end:%Y-%m-%d %H:%M:%S}"
|
|
1003
|
+
if dates_repr:
|
|
1004
|
+
return f"{self.key} ({dates_repr})"
|
|
1005
|
+
return self.key
|