django-cfg 1.3.11__py3-none-any.whl → 1.4.0__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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/accounts/admin/inlines.py +11 -5
- django_cfg/apps/accounts/admin/user_admin.py +39 -16
- django_cfg/apps/accounts/serializers/profile.py +1 -1
- django_cfg/apps/accounts/services/otp_service.py +18 -11
- django_cfg/apps/accounts/signals.py +15 -24
- django_cfg/apps/accounts/utils/notifications.py +217 -358
- django_cfg/apps/accounts/views/otp.py +2 -2
- django_cfg/apps/accounts/views/webhook.py +1 -1
- django_cfg/apps/agents/core/django_agent.py +1 -1
- django_cfg/apps/api/commands/views.py +66 -83
- django_cfg/apps/api/health/drf_views.py +269 -0
- django_cfg/apps/api/health/serializers.py +45 -0
- django_cfg/apps/api/health/urls.py +6 -1
- django_cfg/apps/knowbase/admin/actions/__init__.py +13 -0
- django_cfg/apps/knowbase/admin/actions/visibility_actions.py +56 -0
- django_cfg/apps/knowbase/admin/document_admin.py +136 -270
- django_cfg/apps/knowbase/admin/helpers/__init__.py +17 -0
- django_cfg/apps/knowbase/admin/helpers/configs.py +72 -0
- django_cfg/apps/knowbase/admin/helpers/display_helpers.py +156 -0
- django_cfg/apps/knowbase/admin/helpers/statistics.py +108 -0
- django_cfg/apps/knowbase/config/constance_fields.py +1 -1
- django_cfg/apps/knowbase/config/settings.py +2 -2
- django_cfg/apps/knowbase/mixins/__init__.py +19 -2
- django_cfg/apps/knowbase/mixins/config/__init__.py +14 -0
- django_cfg/apps/knowbase/mixins/config/defaults.py +75 -0
- django_cfg/apps/knowbase/mixins/config/meta_config.py +120 -0
- django_cfg/apps/knowbase/mixins/creator.py +10 -10
- django_cfg/apps/knowbase/mixins/external_data_mixin.py +105 -403
- django_cfg/apps/knowbase/mixins/generators/__init__.py +16 -0
- django_cfg/apps/knowbase/mixins/generators/content_generator.py +218 -0
- django_cfg/apps/knowbase/mixins/generators/field_analyzer.py +76 -0
- django_cfg/apps/knowbase/mixins/generators/metadata_generator.py +124 -0
- django_cfg/apps/knowbase/mixins/service.py +2 -2
- django_cfg/apps/knowbase/services/archive/__init__.py +1 -0
- django_cfg/apps/knowbase/services/archive/analyzers/__init__.py +17 -0
- django_cfg/apps/knowbase/services/archive/analyzers/complexity_analyzer.py +33 -0
- django_cfg/apps/knowbase/services/archive/analyzers/purpose_detector.py +36 -0
- django_cfg/apps/knowbase/services/archive/analyzers/quality_analyzer.py +39 -0
- django_cfg/apps/knowbase/services/archive/analyzers/tag_generator.py +103 -0
- django_cfg/apps/knowbase/services/archive/chunking/__init__.py +19 -0
- django_cfg/apps/knowbase/services/archive/chunking/base.py +81 -0
- django_cfg/apps/knowbase/services/archive/chunking/json_chunker.py +62 -0
- django_cfg/apps/knowbase/services/archive/chunking/markdown_chunker.py +107 -0
- django_cfg/apps/knowbase/services/archive/chunking/python_chunker.py +248 -0
- django_cfg/apps/knowbase/services/archive/chunking/text_chunker.py +70 -0
- django_cfg/apps/knowbase/services/archive/chunking_service.py +110 -729
- django_cfg/apps/knowbase/services/archive/context/__init__.py +14 -0
- django_cfg/apps/knowbase/services/archive/context/builders.py +220 -0
- django_cfg/apps/knowbase/services/archive/context/models.py +38 -0
- django_cfg/apps/knowbase/services/embedding/models.py +18 -14
- django_cfg/apps/knowbase/services/embedding/processors.py +6 -3
- django_cfg/apps/knowbase/tasks/document_processing.py +11 -3
- django_cfg/apps/leads/tests.py +1 -1
- django_cfg/apps/payments/admin/api_keys_admin.py +1 -1
- django_cfg/apps/payments/admin/balance_admin.py +1 -1
- django_cfg/apps/payments/admin/currencies_admin.py +1 -1
- django_cfg/apps/payments/admin/payments_admin.py +1 -1
- django_cfg/apps/payments/admin/subscriptions_admin.py +1 -1
- django_cfg/apps/payments/admin_interface/templates/payments/base.html +59 -126
- django_cfg/apps/payments/admin_interface/views/api/payments.py +1 -1
- django_cfg/apps/payments/admin_interface/views/api/stats.py +1 -1
- django_cfg/apps/payments/admin_interface/views/api/users.py +1 -1
- django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +1 -1
- django_cfg/apps/payments/admin_interface/views/api/webhook_public.py +1 -1
- django_cfg/apps/payments/admin_interface/views/base.py +29 -2
- django_cfg/apps/payments/apps.py +1 -1
- django_cfg/apps/payments/config/django_cfg_integration.py +2 -2
- django_cfg/apps/payments/config/helpers.py +3 -2
- django_cfg/apps/payments/management/commands/cleanup_expired_data.py +1 -1
- django_cfg/apps/payments/management/commands/currency_stats.py +1 -1
- django_cfg/apps/payments/management/commands/manage_currencies.py +1 -1
- django_cfg/apps/payments/management/commands/manage_providers.py +1 -1
- django_cfg/apps/payments/management/commands/process_pending_payments.py +1 -1
- django_cfg/apps/payments/management/commands/test_providers.py +1 -1
- django_cfg/apps/payments/middleware/api_access.py +1 -1
- django_cfg/apps/payments/middleware/rate_limiting.py +1 -1
- django_cfg/apps/payments/middleware/usage_tracking.py +1 -1
- django_cfg/apps/payments/models/balance.py +2 -2
- django_cfg/apps/payments/models/managers/api_key_managers.py +1 -1
- django_cfg/apps/payments/models/managers/balance_managers.py +1 -1
- django_cfg/apps/payments/models/managers/currency_managers.py +1 -1
- django_cfg/apps/payments/models/managers/payment_managers.py +1 -1
- django_cfg/apps/payments/models/managers/subscription_managers.py +1 -1
- django_cfg/apps/payments/models/payments.py +2 -2
- django_cfg/apps/payments/services/cache_service/__init__.py +1 -1
- django_cfg/apps/payments/services/cache_service/simple_cache.py +10 -5
- django_cfg/apps/payments/services/core/base.py +1 -1
- django_cfg/apps/payments/services/core/currency/__init__.py +13 -0
- django_cfg/apps/payments/services/core/currency/currency_converter.py +57 -0
- django_cfg/apps/payments/services/core/currency/currency_validator.py +61 -0
- django_cfg/apps/payments/services/core/operations/__init__.py +15 -0
- django_cfg/apps/payments/services/core/operations/payment_canceller.py +100 -0
- django_cfg/apps/payments/services/core/operations/payment_creator.py +196 -0
- django_cfg/apps/payments/services/core/operations/status_checker.py +100 -0
- django_cfg/apps/payments/services/core/payment_service.py +124 -612
- django_cfg/apps/payments/services/core/providers/__init__.py +13 -0
- django_cfg/apps/payments/services/core/providers/provider_client.py +132 -0
- django_cfg/apps/payments/services/core/providers/status_mapper.py +89 -0
- django_cfg/apps/payments/services/core/utils/__init__.py +13 -0
- django_cfg/apps/payments/services/core/utils/data_converter.py +48 -0
- django_cfg/apps/payments/services/core/utils/statistics_calculator.py +69 -0
- django_cfg/apps/payments/services/providers/base.py +1 -1
- django_cfg/apps/payments/services/providers/nowpayments/__init__.py +3 -3
- django_cfg/apps/payments/services/providers/nowpayments/parsers/__init__.py +9 -0
- django_cfg/apps/payments/services/providers/nowpayments/parsers/data/__init__.py +23 -0
- django_cfg/apps/payments/services/providers/nowpayments/parsers/data/constants.py +23 -0
- django_cfg/apps/payments/services/providers/nowpayments/parsers/data/currency_names.py +244 -0
- django_cfg/apps/payments/services/providers/nowpayments/parsers/data/patterns.py +511 -0
- django_cfg/apps/payments/services/providers/nowpayments/parsers/parser.py +168 -0
- django_cfg/apps/payments/services/providers/nowpayments/provider.py +1 -1
- django_cfg/apps/payments/services/providers/nowpayments/sync.py +1 -1
- django_cfg/apps/payments/services/providers/registry.py +1 -1
- django_cfg/apps/payments/services/providers/sync_service.py +1 -1
- django_cfg/apps/payments/signals/__init__.py +1 -1
- django_cfg/apps/payments/signals/api_key_signals.py +1 -1
- django_cfg/apps/payments/signals/balance_signals.py +1 -1
- django_cfg/apps/payments/signals/payment_signals.py +1 -1
- django_cfg/apps/payments/signals/subscription_signals.py +1 -1
- django_cfg/apps/payments/views/api/api_keys.py +1 -1
- django_cfg/apps/payments/views/api/balances.py +1 -1
- django_cfg/apps/payments/views/api/base.py +1 -1
- django_cfg/apps/payments/views/api/currencies.py +1 -1
- django_cfg/apps/payments/views/api/payments.py +1 -1
- django_cfg/apps/payments/views/api/subscriptions.py +1 -1
- django_cfg/apps/payments/views/api/webhooks.py +1 -1
- django_cfg/apps/payments/views/serializers/api_keys.py +1 -1
- django_cfg/apps/payments/views/serializers/balances.py +1 -1
- django_cfg/apps/payments/views/serializers/currencies.py +1 -1
- django_cfg/apps/payments/views/serializers/payments.py +1 -1
- django_cfg/apps/payments/views/serializers/subscriptions.py +1 -1
- django_cfg/apps/payments/views/serializers/webhooks.py +1 -1
- django_cfg/apps/support/admin/support_admin.py +21 -13
- django_cfg/apps/support/templates/support/chat/access_denied.html +21 -27
- django_cfg/apps/support/templates/support/chat/ticket_chat.html +183 -254
- django_cfg/apps/support/utils/support_email_service.py +1 -1
- django_cfg/apps/tasks/templates/tasks/layout/base.html +20 -115
- django_cfg/apps/tasks/utils/simulator.py +1 -1
- django_cfg/apps/tasks/views/dashboard.py +33 -3
- django_cfg/apps/urls.py +5 -1
- django_cfg/cli/README.md +57 -471
- django_cfg/cli/commands/create_project.py +140 -529
- django_cfg/cli/main.py +13 -10
- django_cfg/core/__init__.py +63 -6
- django_cfg/core/base/__init__.py +5 -0
- django_cfg/core/base/config_model.py +652 -0
- django_cfg/core/builders/__init__.py +11 -0
- django_cfg/core/builders/apps_builder.py +258 -0
- django_cfg/core/builders/middleware_builder.py +115 -0
- django_cfg/core/builders/security_builder.py +96 -0
- django_cfg/core/config.py +20 -892
- django_cfg/core/constants.py +69 -0
- django_cfg/core/environment/__init__.py +9 -0
- django_cfg/core/exceptions.py +45 -298
- django_cfg/core/generation/__init__.py +51 -0
- django_cfg/core/generation/core_generators/__init__.py +0 -0
- django_cfg/core/generation/core_generators/settings.py +90 -0
- django_cfg/core/generation/core_generators/static.py +82 -0
- django_cfg/core/generation/core_generators/templates.py +141 -0
- django_cfg/core/generation/data_generators/__init__.py +15 -0
- django_cfg/core/generation/data_generators/cache.py +132 -0
- django_cfg/core/generation/data_generators/database.py +117 -0
- django_cfg/core/generation/generation.py +92 -0
- django_cfg/core/generation/integration_generators/__init__.py +21 -0
- django_cfg/core/generation/integration_generators/api.py +237 -0
- django_cfg/core/generation/integration_generators/sessions.py +65 -0
- django_cfg/core/generation/integration_generators/tailwind.py +54 -0
- django_cfg/core/generation/integration_generators/tasks.py +92 -0
- django_cfg/core/generation/integration_generators/third_party.py +144 -0
- django_cfg/core/generation/orchestrator.py +285 -0
- django_cfg/core/generation/protocols.py +30 -0
- django_cfg/core/generation/security_generators/__init__.py +0 -0
- django_cfg/core/generation/utility_generators/__init__.py +24 -0
- django_cfg/core/generation/utility_generators/email.py +58 -0
- django_cfg/core/generation/utility_generators/i18n.py +66 -0
- django_cfg/core/generation/utility_generators/limits.py +58 -0
- django_cfg/core/generation/utility_generators/logging.py +66 -0
- django_cfg/core/generation/utility_generators/security.py +101 -0
- django_cfg/core/generation/utils/__init__.py +0 -0
- django_cfg/core/generation/utils/helpers.py +32 -0
- django_cfg/core/integration/__init__.py +18 -25
- django_cfg/core/integration/display/startup.py +146 -133
- django_cfg/core/integration/url_integration.py +13 -2
- django_cfg/core/services/__init__.py +5 -0
- django_cfg/core/services/config_service.py +121 -0
- django_cfg/core/state/__init__.py +9 -0
- django_cfg/core/state/registry.py +84 -0
- django_cfg/core/types/__init__.py +15 -0
- django_cfg/core/types/aliases.py +15 -0
- django_cfg/core/types/enums.py +49 -0
- django_cfg/dashboard/DEBUG_README.md +105 -0
- django_cfg/dashboard/REFACTORING_SUMMARY.md +237 -0
- django_cfg/dashboard/__init__.py +24 -0
- django_cfg/dashboard/components.py +308 -0
- django_cfg/dashboard/debug.py +176 -0
- django_cfg/dashboard/management/__init__.py +0 -0
- django_cfg/dashboard/management/commands/__init__.py +0 -0
- django_cfg/dashboard/management/commands/debug_dashboard.py +109 -0
- django_cfg/dashboard/sections/__init__.py +1 -0
- django_cfg/dashboard/sections/base.py +128 -0
- django_cfg/dashboard/sections/commands.py +32 -0
- django_cfg/dashboard/sections/overview.py +394 -0
- django_cfg/dashboard/sections/stats.py +48 -0
- django_cfg/dashboard/sections/system.py +73 -0
- django_cfg/management/commands/check_settings.py +6 -2
- django_cfg/management/commands/clear_constance.py +6 -1
- django_cfg/management/commands/create_token.py +5 -4
- django_cfg/management/commands/generate.py +5 -0
- django_cfg/management/commands/list_urls.py +7 -2
- django_cfg/management/commands/migrate_all.py +6 -2
- django_cfg/management/commands/migrator.py +6 -1
- django_cfg/management/commands/rundramatiq.py +6 -1
- django_cfg/management/commands/rundramatiq_simulator.py +11 -4
- django_cfg/management/commands/runserver_ngrok.py +9 -7
- django_cfg/management/commands/script.py +25 -21
- django_cfg/management/commands/show_config.py +6 -1
- django_cfg/management/commands/show_urls.py +8 -3
- django_cfg/management/commands/superuser.py +5 -4
- django_cfg/management/commands/task_clear.py +8 -3
- django_cfg/management/commands/task_status.py +8 -3
- django_cfg/management/commands/test_email.py +6 -1
- django_cfg/management/commands/test_telegram.py +6 -1
- django_cfg/management/commands/test_twilio.py +6 -1
- django_cfg/management/commands/tree.py +7 -4
- django_cfg/models/__init__.py +88 -3
- django_cfg/models/api/__init__.py +27 -0
- django_cfg/models/{api.py → api/config.py} +1 -1
- django_cfg/models/api/drf/__init__.py +21 -0
- django_cfg/models/api/drf/config.py +101 -0
- django_cfg/models/api/drf/redoc.py +31 -0
- django_cfg/models/api/drf/spectacular.py +129 -0
- django_cfg/models/api/drf/swagger.py +59 -0
- django_cfg/models/{api_keys.py → api/keys.py} +16 -6
- django_cfg/models/{limits.py → api/limits.py} +0 -1
- django_cfg/models/base/__init__.py +14 -0
- django_cfg/models/django/__init__.py +16 -0
- django_cfg/models/{constance.py → django/constance.py} +1 -1
- django_cfg/models/{environment.py → django/environment.py} +1 -1
- django_cfg/models/infrastructure/__init__.py +17 -0
- django_cfg/models/{cache.py → infrastructure/cache.py} +3 -2
- django_cfg/models/infrastructure/database/__init__.py +22 -0
- django_cfg/models/infrastructure/database/config.py +265 -0
- django_cfg/models/infrastructure/database/converters.py +91 -0
- django_cfg/models/infrastructure/database/parsers.py +96 -0
- django_cfg/models/infrastructure/database/routing.py +85 -0
- django_cfg/models/infrastructure/database/validators.py +170 -0
- django_cfg/models/{logging.py → infrastructure/logging.py} +1 -1
- django_cfg/models/{security.py → infrastructure/security.py} +2 -2
- django_cfg/models/ngrok/__init__.py +11 -0
- django_cfg/models/ngrok/auth.py +37 -0
- django_cfg/models/ngrok/config.py +77 -0
- django_cfg/models/ngrok/tunnel.py +35 -0
- django_cfg/models/payments/__init__.py +20 -0
- django_cfg/models/payments/api_keys.py +57 -0
- django_cfg/models/{payments.py → payments/config.py} +56 -154
- django_cfg/models/payments/providers/__init__.py +15 -0
- django_cfg/models/payments/providers/base.py +25 -0
- django_cfg/models/payments/providers/nowpayments.py +48 -0
- django_cfg/models/services/__init__.py +18 -0
- django_cfg/models/services/base.py +65 -0
- django_cfg/models/{email.py → services/email.py} +1 -1
- django_cfg/models/services/telegram.py +172 -0
- django_cfg/models/tasks/__init__.py +51 -0
- django_cfg/models/tasks/backends.py +250 -0
- django_cfg/models/tasks/config.py +314 -0
- django_cfg/models/tasks/utils.py +174 -0
- django_cfg/modules/base.py +18 -3
- django_cfg/modules/django_admin/decorators/actions.py +1 -1
- django_cfg/modules/django_admin/decorators/display.py +1 -1
- django_cfg/modules/django_admin/mixins/standalone_actions_mixin.py +1 -1
- django_cfg/modules/django_cfg_rpc_client/README.md +346 -0
- django_cfg/modules/django_cfg_rpc_client/__init__.py +51 -0
- django_cfg/modules/django_cfg_rpc_client/client.py +540 -0
- django_cfg/modules/django_cfg_rpc_client/config.py +207 -0
- django_cfg/modules/django_cfg_rpc_client/dashboard/README.md +517 -0
- django_cfg/modules/django_cfg_rpc_client/dashboard/UNFOLD_INTEGRATION.md +439 -0
- django_cfg/modules/django_cfg_rpc_client/dashboard/__init__.py +11 -0
- django_cfg/modules/django_cfg_rpc_client/dashboard/apps.py +22 -0
- django_cfg/modules/django_cfg_rpc_client/dashboard/monitor.py +435 -0
- django_cfg/modules/django_cfg_rpc_client/dashboard/static/django_cfg_rpc_dashboard/js/dashboard.js +373 -0
- django_cfg/modules/django_cfg_rpc_client/dashboard/templates/django_cfg_rpc_dashboard/base.html +76 -0
- django_cfg/modules/django_cfg_rpc_client/dashboard/templates/django_cfg_rpc_dashboard/dashboard.html +200 -0
- django_cfg/modules/django_cfg_rpc_client/dashboard/urls.py +22 -0
- django_cfg/modules/django_cfg_rpc_client/dashboard/urls_admin.py +9 -0
- django_cfg/modules/django_cfg_rpc_client/dashboard/views.py +251 -0
- django_cfg/modules/django_cfg_rpc_client/exceptions.py +201 -0
- django_cfg/modules/django_drf_theme/CHANGELOG.md +210 -0
- django_cfg/modules/django_drf_theme/EXAMPLE.md +465 -0
- django_cfg/modules/django_drf_theme/IMPLEMENTATION.md +232 -0
- django_cfg/modules/django_drf_theme/README.md +207 -0
- django_cfg/modules/django_drf_theme/TAILWIND_CDN_GUIDE.md +274 -0
- django_cfg/modules/django_drf_theme/__init__.py +23 -0
- django_cfg/modules/django_drf_theme/apps.py +15 -0
- django_cfg/modules/django_drf_theme/renderers.py +58 -0
- django_cfg/modules/django_drf_theme/templates/rest_framework/tailwind/api.html +375 -0
- django_cfg/modules/django_drf_theme/templates/rest_framework/tailwind/base.html +938 -0
- django_cfg/modules/django_drf_theme/templates/rest_framework/tailwind/forms/filter_form.html +132 -0
- django_cfg/modules/django_drf_theme/templates/rest_framework/tailwind/forms/raw_data_form.html +123 -0
- django_cfg/modules/django_drf_theme/templatetags/__init__.py +1 -0
- django_cfg/modules/django_drf_theme/templatetags/tailwind_tags.py +57 -0
- django_cfg/modules/django_email/__init__.py +14 -0
- django_cfg/modules/{django_email.py → django_email/service.py} +78 -113
- django_cfg/modules/django_email/utils.py +40 -0
- django_cfg/modules/django_health/__init__.py +9 -0
- django_cfg/modules/{django_health.py → django_health/service.py} +23 -21
- django_cfg/modules/django_llm/llm/client.py +155 -550
- django_cfg/modules/django_llm/llm/embeddings/__init__.py +13 -0
- django_cfg/modules/django_llm/llm/embeddings/mock_embedder.py +106 -0
- django_cfg/modules/django_llm/llm/embeddings/openai_embedder.py +79 -0
- django_cfg/modules/django_llm/llm/models_api/__init__.py +9 -0
- django_cfg/modules/django_llm/llm/models_api/models_query.py +163 -0
- django_cfg/modules/django_llm/llm/providers/__init__.py +15 -0
- django_cfg/modules/django_llm/llm/providers/config_builder.py +103 -0
- django_cfg/modules/django_llm/llm/providers/provider_manager.py +148 -0
- django_cfg/modules/django_llm/llm/providers/provider_selector.py +60 -0
- django_cfg/modules/django_llm/llm/requests/__init__.py +15 -0
- django_cfg/modules/django_llm/llm/requests/cache_manager.py +170 -0
- django_cfg/modules/django_llm/llm/requests/chat_handler.py +199 -0
- django_cfg/modules/django_llm/llm/requests/embedding_handler.py +113 -0
- django_cfg/modules/django_llm/llm/responses/__init__.py +9 -0
- django_cfg/modules/django_llm/llm/responses/response_builder.py +131 -0
- django_cfg/modules/django_llm/llm/stats/__init__.py +9 -0
- django_cfg/modules/django_llm/llm/stats/stats_manager.py +107 -0
- django_cfg/modules/django_llm/translator/detectors/__init__.py +13 -0
- django_cfg/modules/django_llm/translator/detectors/language_detector.py +90 -0
- django_cfg/modules/django_llm/translator/detectors/script_detector.py +153 -0
- django_cfg/modules/django_llm/translator/stats/__init__.py +11 -0
- django_cfg/modules/django_llm/translator/stats/stats_tracker.py +85 -0
- django_cfg/modules/django_llm/translator/translator.py +150 -603
- django_cfg/modules/django_llm/translator/translators/__init__.py +15 -0
- django_cfg/modules/django_llm/translator/translators/json_translator.py +316 -0
- django_cfg/modules/django_llm/translator/translators/text_translator.py +139 -0
- django_cfg/modules/django_llm/translator/utils/__init__.py +13 -0
- django_cfg/modules/django_llm/translator/utils/prompt_builder.py +110 -0
- django_cfg/modules/django_llm/translator/utils/text_utils.py +114 -0
- django_cfg/modules/django_logging/FIXES_SUMMARY.md +276 -0
- django_cfg/modules/django_logging/LOGGING_GUIDE.md +504 -0
- django_cfg/modules/django_logging/__init__.py +14 -0
- django_cfg/modules/{django_logger.py → django_logging/django_logger.py} +13 -13
- django_cfg/modules/{logger.py → django_logging/logger.py} +14 -4
- django_cfg/modules/django_ngrok/__init__.py +39 -0
- django_cfg/modules/{django_ngrok.py → django_ngrok/service.py} +14 -42
- django_cfg/modules/django_rpc_old/POETRY.md +344 -0
- django_cfg/modules/django_rpc_old/README.md +397 -0
- django_cfg/modules/django_rpc_old/TESTING.md +358 -0
- django_cfg/modules/django_rpc_old/__init__.py +39 -0
- django_cfg/modules/django_rpc_old/client.py +531 -0
- django_cfg/modules/django_rpc_old/config.py +279 -0
- django_cfg/modules/django_rpc_old/exceptions.py +172 -0
- django_cfg/modules/django_tailwind/README.md +478 -0
- django_cfg/modules/django_tailwind/__init__.py +7 -0
- django_cfg/modules/django_tailwind/apps.py +10 -0
- django_cfg/modules/django_tailwind/templates/django_tailwind/app.html +5 -0
- django_cfg/modules/django_tailwind/templates/django_tailwind/base.html +117 -0
- django_cfg/modules/django_tailwind/templates/django_tailwind/components/navbar.html +124 -0
- django_cfg/modules/django_tailwind/templates/django_tailwind/components/theme_toggle.html +54 -0
- django_cfg/modules/django_tailwind/templates/django_tailwind/components/user_menu.html +116 -0
- django_cfg/modules/django_tailwind/templates/django_tailwind/simple.html +46 -0
- django_cfg/modules/django_tailwind/templatetags/__init__.py +1 -0
- django_cfg/modules/django_tailwind/templatetags/tailwind_info.py +185 -0
- django_cfg/modules/django_tasks/__init__.py +29 -0
- django_cfg/modules/django_tasks/factory.py +127 -0
- django_cfg/modules/{django_tasks.py → django_tasks/service.py} +45 -274
- django_cfg/modules/django_tasks/settings.py +107 -0
- django_cfg/modules/django_telegram/__init__.py +29 -0
- django_cfg/modules/{django_telegram.py → django_telegram/service.py} +45 -113
- django_cfg/modules/django_telegram/utils.py +62 -0
- django_cfg/modules/django_twilio/__init__.py +54 -107
- django_cfg/modules/django_twilio/_imports.py +30 -0
- django_cfg/modules/django_twilio/base.py +192 -0
- django_cfg/modules/django_twilio/email_otp.py +227 -0
- django_cfg/modules/django_twilio/sendgrid_service.py +1 -1
- django_cfg/modules/django_twilio/simple_service.py +1 -2
- django_cfg/modules/django_twilio/sms.py +94 -0
- django_cfg/modules/django_twilio/twilio_service.py +2 -3
- django_cfg/modules/django_twilio/unified.py +310 -0
- django_cfg/modules/django_twilio/utils.py +190 -0
- django_cfg/modules/django_twilio/whatsapp.py +137 -0
- django_cfg/modules/django_unfold/callbacks/base.py +198 -7
- django_cfg/modules/django_unfold/callbacks/main.py +102 -10
- django_cfg/modules/django_unfold/dashboard.py +65 -43
- django_cfg/modules/django_unfold/models/config.py +13 -12
- django_cfg/modules/django_unfold/models/navigation.py +8 -3
- django_cfg/modules/django_unfold/models/tabs.py +2 -2
- django_cfg/modules/django_unfold/templates/unfold/helpers/app_list.html +102 -0
- django_cfg/registry/core.py +24 -26
- django_cfg/registry/modules.py +5 -2
- django_cfg/registry/services.py +20 -3
- django_cfg/registry/third_party.py +8 -8
- django_cfg/static/admin/css/dashboard.css +260 -0
- django_cfg/static/admin/js/commands.js +171 -0
- django_cfg/static/admin/js/dashboard.js +126 -0
- django_cfg/templates/admin/components/management_commands.js +375 -0
- django_cfg/templates/admin/components/progress_bar.html +18 -23
- django_cfg/templates/admin/index.html +48 -20
- django_cfg/templates/admin/index_new.html +106 -0
- django_cfg/templates/admin/layouts/base_dashboard.html +60 -0
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +1 -20
- django_cfg/templates/admin/sections/commands_section.html +626 -0
- django_cfg/templates/admin/sections/overview_section.html +112 -0
- django_cfg/templates/admin/sections/stats_section.html +35 -0
- django_cfg/templates/admin/sections/system_section.html +99 -0
- django_cfg/templates/admin/snippets/components/CHARTS_GUIDE.md +322 -0
- django_cfg/templates/admin/snippets/components/activity_tracker.html +85 -47
- django_cfg/templates/admin/snippets/components/charts_section.html +154 -64
- django_cfg/templates/admin/snippets/components/django_commands.html +3 -3
- django_cfg/templates/admin/snippets/components/recent_activity_improved.html +25 -0
- django_cfg/templates/admin/snippets/components/recent_users_table.html +1 -1
- django_cfg/templates/admin/snippets/components/system_metrics.html +179 -93
- django_cfg/templates/admin/snippets/zones/zones_table.html +2 -2
- django_cfg/templatetags/django_cfg.py +7 -1
- django_cfg/utils/smart_defaults.py +4 -4
- django_cfg-1.4.0.dist-info/METADATA +920 -0
- {django_cfg-1.3.11.dist-info → django_cfg-1.4.0.dist-info}/RECORD +425 -196
- django_cfg/apps/accounts/utils/auth_email_service.py +0 -84
- django_cfg/apps/payments/services/providers/nowpayments/parsers.py +0 -879
- django_cfg/core/generation.py +0 -621
- django_cfg/management/commands/validate_config.py +0 -189
- django_cfg/models/database.py +0 -480
- django_cfg/models/drf.py +0 -272
- django_cfg/models/ngrok.py +0 -122
- django_cfg/models/services.py +0 -440
- django_cfg/models/tasks.py +0 -550
- django_cfg/modules/django_twilio/service.py +0 -942
- django_cfg/template_archive/django_sample.zip +0 -0
- django_cfg/templates/rest_framework/api.html +0 -12
- django_cfg/utils/toolkit.py +0 -703
- django_cfg-1.3.11.dist-info/METADATA +0 -1029
- /django_cfg/apps/accounts/management/commands/{test_otp.py → otp_test.py} +0 -0
- /django_cfg/core/{environment.py → environment/detector.py} +0 -0
- /django_cfg/models/{cors.py → api/cors.py} +0 -0
- /django_cfg/models/{jwt.py → api/jwt.py} +0 -0
- /django_cfg/models/{base.py → base/config.py} +0 -0
- /django_cfg/models/{cfg.py → base/module.py} +0 -0
- /django_cfg/models/{revolution.py → django/revolution.py} +0 -0
- /django_cfg/modules/{dramatiq_setup.py → django_tasks/dramatiq_setup.py} +0 -0
- {django_cfg-1.3.11.dist-info → django_cfg-1.4.0.dist-info}/WHEEL +0 -0
- {django_cfg-1.3.11.dist-info → django_cfg-1.4.0.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.3.11.dist-info → django_cfg-1.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -11,38 +11,38 @@ Usage:
|
|
11
11
|
class MyModel(ExternalDataMixin, models.Model):
|
12
12
|
name = models.CharField(max_length=100)
|
13
13
|
description = models.TextField()
|
14
|
-
|
14
|
+
|
15
15
|
class Meta:
|
16
16
|
# Standard Django Meta options...
|
17
|
-
|
17
|
+
|
18
18
|
class ExternalDataMeta:
|
19
19
|
# Required: fields to watch for changes
|
20
20
|
watch_fields = ['name', 'description']
|
21
|
-
|
21
|
+
|
22
22
|
# Optional: similarity threshold (default: 0.5)
|
23
23
|
similarity_threshold = 0.4
|
24
|
-
|
24
|
+
|
25
25
|
# Optional: source type (default: ExternalDataType.MODEL)
|
26
26
|
source_type = ExternalDataType.CUSTOM
|
27
|
-
|
27
|
+
|
28
28
|
# Optional: enable/disable auto-sync (default: True)
|
29
29
|
auto_sync = True
|
30
|
-
|
30
|
+
|
31
31
|
# Optional: make public (default: False)
|
32
32
|
is_public = False
|
33
|
-
|
33
|
+
|
34
34
|
# Required: content generation method
|
35
35
|
def get_external_content(self):
|
36
36
|
return f"# {self.name}\n\n{self.description}"
|
37
|
-
|
37
|
+
|
38
38
|
# Optional: custom title (default: str(instance))
|
39
39
|
def get_external_title(self):
|
40
40
|
return f"My Model: {self.name}"
|
41
|
-
|
41
|
+
|
42
42
|
# Optional: custom description (default: auto-generated)
|
43
43
|
def get_external_description(self):
|
44
44
|
return f"Information about {self.name}"
|
45
|
-
|
45
|
+
|
46
46
|
# Optional: metadata (default: basic model info)
|
47
47
|
def get_external_metadata(self):
|
48
48
|
return {
|
@@ -50,7 +50,7 @@ Usage:
|
|
50
50
|
'model_id': str(self.id),
|
51
51
|
'name': self.name,
|
52
52
|
}
|
53
|
-
|
53
|
+
|
54
54
|
# Optional: tags (default: [model_name.lower()])
|
55
55
|
def get_external_tags(self):
|
56
56
|
return ['my_model', self.name.lower()]
|
@@ -58,15 +58,17 @@ Usage:
|
|
58
58
|
|
59
59
|
import logging
|
60
60
|
import hashlib
|
61
|
-
from typing import Optional, List, Dict, Any
|
61
|
+
from typing import Optional, List, Dict, Any
|
62
62
|
from django.db import models
|
63
63
|
from django.db.models.signals import post_save, post_delete
|
64
|
-
from django.dispatch import receiver
|
65
|
-
from django.contrib.contenttypes.models import ContentType
|
66
64
|
|
67
65
|
from ..models.external_data import ExternalData, ExternalDataType, ExternalDataStatus
|
68
66
|
from .creator import ExternalDataCreator
|
69
|
-
from .config import
|
67
|
+
from .config import ExternalDataMetaConfig, ExternalDataMetaParser
|
68
|
+
from .generators import (
|
69
|
+
ExternalDataContentGenerator,
|
70
|
+
ExternalDataMetadataGenerator,
|
71
|
+
)
|
70
72
|
|
71
73
|
logger = logging.getLogger(__name__)
|
72
74
|
|
@@ -74,23 +76,23 @@ logger = logging.getLogger(__name__)
|
|
74
76
|
class ExternalDataMixin(models.Model):
|
75
77
|
"""
|
76
78
|
Mixin that automatically integrates models with knowbase ExternalData system.
|
77
|
-
|
79
|
+
|
78
80
|
Provides:
|
79
81
|
- Automatic external_source_id field
|
80
82
|
- Change tracking and vectorization
|
81
83
|
- Simple configuration interface
|
82
84
|
- Automatic cleanup on deletion
|
83
85
|
"""
|
84
|
-
|
86
|
+
|
85
87
|
# Automatically added field for linking to ExternalData
|
86
88
|
external_source_id = models.UUIDField(
|
87
|
-
null=True,
|
88
|
-
blank=True,
|
89
|
+
null=True,
|
90
|
+
blank=True,
|
89
91
|
db_index=True,
|
90
92
|
help_text="UUID of the linked ExternalData object in knowbase",
|
91
93
|
verbose_name="External Source ID"
|
92
94
|
)
|
93
|
-
|
95
|
+
|
94
96
|
# Track content hash for change detection
|
95
97
|
_external_content_hash = models.CharField(
|
96
98
|
max_length=64,
|
@@ -98,27 +100,27 @@ class ExternalDataMixin(models.Model):
|
|
98
100
|
help_text="SHA256 hash of content for change detection",
|
99
101
|
verbose_name="Content Hash"
|
100
102
|
)
|
101
|
-
|
103
|
+
|
102
104
|
class Meta:
|
103
105
|
abstract = True
|
104
|
-
|
106
|
+
|
105
107
|
def __init_subclass__(cls, **kwargs):
|
106
108
|
"""Register signal handlers for each subclass."""
|
107
109
|
super().__init_subclass__(**kwargs)
|
108
|
-
|
110
|
+
|
109
111
|
# Register signals for this specific model class
|
110
112
|
post_save.connect(
|
111
113
|
cls._external_data_post_save_handler,
|
112
114
|
sender=cls,
|
113
115
|
dispatch_uid=f"external_data_mixin_{cls.__name__}"
|
114
116
|
)
|
115
|
-
|
117
|
+
|
116
118
|
post_delete.connect(
|
117
119
|
cls._external_data_post_delete_handler,
|
118
120
|
sender=cls,
|
119
121
|
dispatch_uid=f"external_data_mixin_delete_{cls.__name__}"
|
120
122
|
)
|
121
|
-
|
123
|
+
|
122
124
|
@classmethod
|
123
125
|
def _external_data_post_save_handler(cls, sender, instance, created, **kwargs):
|
124
126
|
"""Handle post_save signal for ExternalData integration."""
|
@@ -126,39 +128,39 @@ class ExternalDataMixin(models.Model):
|
|
126
128
|
meta_config = cls._get_external_data_meta()
|
127
129
|
if not meta_config or not meta_config.get('auto_sync', True):
|
128
130
|
return
|
129
|
-
|
131
|
+
|
130
132
|
# Check if we should process this save (only if watched fields changed)
|
131
133
|
if not created and not cls._should_update_external_data(instance, kwargs):
|
132
134
|
logger.debug(f"📊 No relevant field changes for {cls.__name__}: {instance}")
|
133
135
|
return
|
134
|
-
|
136
|
+
|
135
137
|
# Check if content changed
|
136
138
|
current_content = cls._get_content_for_instance(instance)
|
137
139
|
current_hash = cls._calculate_content_hash(current_content)
|
138
|
-
|
140
|
+
|
139
141
|
if created:
|
140
142
|
# New instance - create ExternalData
|
141
143
|
logger.info(f"🔗 Creating ExternalData for new {cls.__name__}: {instance}")
|
142
144
|
instance._external_content_hash = current_hash
|
143
145
|
instance.save(update_fields=['_external_content_hash'])
|
144
146
|
cls._create_external_data(instance)
|
145
|
-
|
147
|
+
|
146
148
|
elif instance._external_content_hash != current_hash:
|
147
149
|
# Content changed - update ExternalData
|
148
150
|
logger.info(f"🔮 Content changed for {cls.__name__}: {instance}, updating ExternalData")
|
149
151
|
instance._external_content_hash = current_hash
|
150
152
|
instance.save(update_fields=['_external_content_hash'])
|
151
|
-
|
153
|
+
|
152
154
|
if instance.external_source_id:
|
153
155
|
cls._update_external_data(instance)
|
154
156
|
else:
|
155
157
|
cls._create_external_data(instance)
|
156
158
|
else:
|
157
159
|
logger.debug(f"📊 No content changes for {cls.__name__}: {instance}")
|
158
|
-
|
160
|
+
|
159
161
|
except Exception as e:
|
160
162
|
logger.error(f"❌ Error in ExternalData post_save handler for {cls.__name__}: {e}")
|
161
|
-
|
163
|
+
|
162
164
|
@classmethod
|
163
165
|
def _external_data_post_delete_handler(cls, sender, instance, **kwargs):
|
164
166
|
"""Handle post_delete signal for ExternalData cleanup."""
|
@@ -168,61 +170,32 @@ class ExternalDataMixin(models.Model):
|
|
168
170
|
ExternalData.objects.filter(id=instance.external_source_id).delete()
|
169
171
|
except Exception as e:
|
170
172
|
logger.error(f"❌ Error cleaning up ExternalData for {cls.__name__}: {e}")
|
171
|
-
|
173
|
+
|
172
174
|
@classmethod
|
173
175
|
def _get_external_data_meta(cls) -> Dict[str, Any]:
|
174
176
|
"""Get ExternalDataMeta configuration from the model or auto-generate smart defaults."""
|
175
|
-
|
176
|
-
|
177
|
-
# If ExternalDataMeta exists, use it
|
178
|
-
if hasattr(cls, 'ExternalDataMeta'):
|
179
|
-
meta_class = cls.ExternalDataMeta
|
180
|
-
# Extract configuration from ExternalDataMeta
|
181
|
-
for attr in dir(meta_class):
|
182
|
-
if not attr.startswith('_'):
|
183
|
-
value = getattr(meta_class, attr)
|
184
|
-
if not callable(value): # Only properties, not methods
|
185
|
-
config[attr] = value
|
186
|
-
|
187
|
-
# Smart defaults based on model analysis
|
188
|
-
if 'watch_fields' not in config:
|
189
|
-
config['watch_fields'] = cls._auto_detect_watch_fields()
|
190
|
-
|
191
|
-
if 'similarity_threshold' not in config:
|
192
|
-
config['similarity_threshold'] = 0.5 # Balanced default
|
193
|
-
|
194
|
-
if 'source_type' not in config:
|
195
|
-
from ..models.external_data import ExternalDataType
|
196
|
-
config['source_type'] = ExternalDataType.MODEL # Smart default
|
197
|
-
|
198
|
-
if 'auto_sync' not in config:
|
199
|
-
config['auto_sync'] = True # Enable by default
|
200
|
-
|
201
|
-
if 'is_public' not in config:
|
202
|
-
config['is_public'] = False # Private by default for security
|
203
|
-
|
204
|
-
return config
|
205
|
-
|
177
|
+
return ExternalDataMetaParser.parse(cls)
|
178
|
+
|
206
179
|
@classmethod
|
207
180
|
def _should_update_external_data(cls, instance, save_kwargs) -> bool:
|
208
181
|
"""Check if we should update ExternalData based on changed fields."""
|
209
182
|
meta_config = cls._get_external_data_meta()
|
210
183
|
if not meta_config:
|
211
184
|
return True # No config = update always
|
212
|
-
|
185
|
+
|
213
186
|
watch_fields = meta_config.get('watch_fields', [])
|
214
187
|
if not watch_fields:
|
215
188
|
return True # No watch fields = update always
|
216
|
-
|
189
|
+
|
217
190
|
# Check if update_fields was used in save()
|
218
191
|
update_fields = save_kwargs.get('update_fields')
|
219
192
|
if update_fields is not None:
|
220
193
|
# Only update if any watched field was updated
|
221
194
|
return any(field in update_fields for field in watch_fields)
|
222
|
-
|
195
|
+
|
223
196
|
# If no update_fields specified, assume all fields might have changed
|
224
197
|
return True
|
225
|
-
|
198
|
+
|
226
199
|
@classmethod
|
227
200
|
def _get_content_for_instance(cls, instance) -> str:
|
228
201
|
"""Get content string for the instance."""
|
@@ -231,10 +204,11 @@ class ExternalDataMixin(models.Model):
|
|
231
204
|
return str(instance.get_external_content())
|
232
205
|
except Exception as e:
|
233
206
|
logger.warning(f"Error calling get_external_content on {cls.__name__}: {e}")
|
234
|
-
|
235
|
-
#
|
236
|
-
|
237
|
-
|
207
|
+
|
208
|
+
# Use generator for auto-generation
|
209
|
+
generator = ExternalDataContentGenerator(instance)
|
210
|
+
return generator.generate_content()
|
211
|
+
|
238
212
|
@classmethod
|
239
213
|
def _get_title_for_instance(cls, instance) -> str:
|
240
214
|
"""Get title for the instance."""
|
@@ -243,10 +217,11 @@ class ExternalDataMixin(models.Model):
|
|
243
217
|
return str(instance.get_external_title())
|
244
218
|
except Exception as e:
|
245
219
|
logger.warning(f"Error calling get_external_title on {cls.__name__}: {e}")
|
246
|
-
|
247
|
-
#
|
248
|
-
|
249
|
-
|
220
|
+
|
221
|
+
# Use generator for auto-generation
|
222
|
+
generator = ExternalDataContentGenerator(instance)
|
223
|
+
return generator.generate_title()
|
224
|
+
|
250
225
|
@classmethod
|
251
226
|
def _get_description_for_instance(cls, instance) -> str:
|
252
227
|
"""Get description for the instance."""
|
@@ -255,10 +230,11 @@ class ExternalDataMixin(models.Model):
|
|
255
230
|
return str(instance.get_external_description())
|
256
231
|
except Exception as e:
|
257
232
|
logger.warning(f"Error calling get_external_description on {cls.__name__}: {e}")
|
258
|
-
|
259
|
-
#
|
260
|
-
|
261
|
-
|
233
|
+
|
234
|
+
# Use generator for auto-generation
|
235
|
+
generator = ExternalDataMetadataGenerator(instance)
|
236
|
+
return generator.generate_description()
|
237
|
+
|
262
238
|
@classmethod
|
263
239
|
def _get_tags_for_instance(cls, instance) -> List[str]:
|
264
240
|
"""Get tags for the instance."""
|
@@ -270,15 +246,16 @@ class ExternalDataMixin(models.Model):
|
|
270
246
|
return [str(tags)]
|
271
247
|
except Exception as e:
|
272
248
|
logger.warning(f"Error calling get_external_tags on {cls.__name__}: {e}")
|
273
|
-
|
274
|
-
#
|
275
|
-
|
276
|
-
|
249
|
+
|
250
|
+
# Use generator for auto-generation
|
251
|
+
generator = ExternalDataMetadataGenerator(instance)
|
252
|
+
return generator.generate_tags()
|
253
|
+
|
277
254
|
@classmethod
|
278
255
|
def _calculate_content_hash(cls, content: str) -> str:
|
279
256
|
"""Calculate SHA256 hash of content."""
|
280
257
|
return hashlib.sha256(content.encode('utf-8')).hexdigest()
|
281
|
-
|
258
|
+
|
282
259
|
@classmethod
|
283
260
|
def _create_external_data(cls, instance):
|
284
261
|
"""Create ExternalData for the instance."""
|
@@ -287,12 +264,12 @@ class ExternalDataMixin(models.Model):
|
|
287
264
|
if not meta_config:
|
288
265
|
logger.warning(f"No ExternalDataMeta found for {cls.__name__}")
|
289
266
|
return
|
290
|
-
|
267
|
+
|
291
268
|
# Get user (try to find from instance or use superuser)
|
292
269
|
user = cls._get_user_for_instance(instance)
|
293
|
-
|
294
|
-
# Build
|
295
|
-
external_config =
|
270
|
+
|
271
|
+
# Build ExternalDataMetaConfig
|
272
|
+
external_config = ExternalDataMetaConfig(
|
296
273
|
title=cls._get_title_for_instance(instance),
|
297
274
|
description=cls._get_description_for_instance(instance),
|
298
275
|
source_type=meta_config.get('source_type', ExternalDataType.MODEL),
|
@@ -310,11 +287,11 @@ class ExternalDataMixin(models.Model):
|
|
310
287
|
'watch_fields': meta_config.get('watch_fields', []),
|
311
288
|
}
|
312
289
|
)
|
313
|
-
|
290
|
+
|
314
291
|
# Create ExternalData
|
315
292
|
creator = ExternalDataCreator(user)
|
316
293
|
result = creator.create_from_config(external_config)
|
317
|
-
|
294
|
+
|
318
295
|
if result['success']:
|
319
296
|
external_data = result['external_data']
|
320
297
|
instance.external_source_id = external_data.id
|
@@ -322,20 +299,20 @@ class ExternalDataMixin(models.Model):
|
|
322
299
|
logger.info(f"✅ Created ExternalData {external_data.id} for {cls.__name__}: {instance}")
|
323
300
|
else:
|
324
301
|
logger.error(f"❌ Failed to create ExternalData for {cls.__name__}: {result.get('error')}")
|
325
|
-
|
302
|
+
|
326
303
|
except Exception as e:
|
327
304
|
logger.error(f"❌ Error creating ExternalData for {cls.__name__}: {e}")
|
328
|
-
|
305
|
+
|
329
306
|
@classmethod
|
330
307
|
def _update_external_data(cls, instance):
|
331
308
|
"""Update existing ExternalData for the instance."""
|
332
309
|
try:
|
333
310
|
if not instance.external_source_id:
|
334
311
|
return
|
335
|
-
|
312
|
+
|
336
313
|
external_data = ExternalData.objects.get(id=instance.external_source_id)
|
337
314
|
meta_config = cls._get_external_data_meta() or {}
|
338
|
-
|
315
|
+
|
339
316
|
# Update fields using the same methods as creation
|
340
317
|
external_data.title = cls._get_title_for_instance(instance)
|
341
318
|
external_data.description = cls._get_description_for_instance(instance)
|
@@ -344,16 +321,16 @@ class ExternalDataMixin(models.Model):
|
|
344
321
|
external_data.tags = cls._get_tags_for_instance(instance)
|
345
322
|
external_data.similarity_threshold = meta_config.get('similarity_threshold', 0.5)
|
346
323
|
external_data.status = ExternalDataStatus.PENDING # Mark for reprocessing
|
347
|
-
|
324
|
+
|
348
325
|
external_data.save()
|
349
326
|
logger.info(f"✅ Updated ExternalData {external_data.id} for {cls.__name__}: {instance}")
|
350
|
-
|
327
|
+
|
351
328
|
except ExternalData.DoesNotExist:
|
352
329
|
logger.warning(f"ExternalData {instance.external_source_id} not found, creating new one")
|
353
330
|
cls._create_external_data(instance)
|
354
331
|
except Exception as e:
|
355
332
|
logger.error(f"❌ Error updating ExternalData for {cls.__name__}: {e}")
|
356
|
-
|
333
|
+
|
357
334
|
@classmethod
|
358
335
|
def _build_metadata(cls, instance, config: Dict[str, Any]) -> Dict[str, Any]:
|
359
336
|
"""Build metadata dictionary for ExternalData."""
|
@@ -365,7 +342,7 @@ class ExternalDataMixin(models.Model):
|
|
365
342
|
'created_at': getattr(instance, 'created_at', None),
|
366
343
|
'updated_at': getattr(instance, 'updated_at', None),
|
367
344
|
}
|
368
|
-
|
345
|
+
|
369
346
|
# Add custom metadata if method exists
|
370
347
|
if hasattr(instance, 'get_external_metadata'):
|
371
348
|
try:
|
@@ -374,14 +351,14 @@ class ExternalDataMixin(models.Model):
|
|
374
351
|
metadata.update(custom_metadata)
|
375
352
|
except Exception as e:
|
376
353
|
logger.warning(f"Error calling get_external_metadata on {cls.__name__}: {e}")
|
377
|
-
|
354
|
+
|
378
355
|
# Convert datetime objects to strings
|
379
356
|
for key, value in metadata.items():
|
380
357
|
if hasattr(value, 'isoformat'):
|
381
358
|
metadata[key] = value.isoformat()
|
382
|
-
|
359
|
+
|
383
360
|
return metadata
|
384
|
-
|
361
|
+
|
385
362
|
@classmethod
|
386
363
|
def _get_user_for_instance(cls, instance):
|
387
364
|
"""Get user for ExternalData ownership."""
|
@@ -392,23 +369,23 @@ class ExternalDataMixin(models.Model):
|
|
392
369
|
return instance.created_by
|
393
370
|
if hasattr(instance, 'owner'):
|
394
371
|
return instance.owner
|
395
|
-
|
396
|
-
# Fallback to
|
372
|
+
|
373
|
+
# Fallback to staff user
|
397
374
|
from django.contrib.auth import get_user_model
|
398
375
|
User = get_user_model()
|
399
|
-
|
400
|
-
if
|
401
|
-
return
|
402
|
-
|
376
|
+
staff_user = User.objects.filter(is_staff=True).first()
|
377
|
+
if staff_user:
|
378
|
+
return staff_user
|
379
|
+
|
403
380
|
raise ValueError("No user found for ExternalData ownership")
|
404
|
-
|
381
|
+
|
405
382
|
def regenerate_external_data(self):
|
406
383
|
"""Manually regenerate ExternalData for this instance."""
|
407
384
|
if self.external_source_id:
|
408
385
|
self._update_external_data(self)
|
409
386
|
else:
|
410
387
|
self._create_external_data(self)
|
411
|
-
|
388
|
+
|
412
389
|
def create_external_data(self, user=None):
|
413
390
|
"""Create ExternalData for this instance if it doesn't exist."""
|
414
391
|
if self.external_source_id:
|
@@ -417,7 +394,7 @@ class ExternalDataMixin(models.Model):
|
|
417
394
|
'error': f'External data already exists: {self.external_source_id}',
|
418
395
|
'external_data': None
|
419
396
|
}
|
420
|
-
|
397
|
+
|
421
398
|
try:
|
422
399
|
self._create_external_data(self)
|
423
400
|
if self.external_source_id:
|
@@ -438,7 +415,7 @@ class ExternalDataMixin(models.Model):
|
|
438
415
|
'error': f'Error creating external data: {str(e)}',
|
439
416
|
'external_data': None
|
440
417
|
}
|
441
|
-
|
418
|
+
|
442
419
|
def delete_external_data(self):
|
443
420
|
"""Manually delete ExternalData for this instance."""
|
444
421
|
if self.external_source_id:
|
@@ -449,328 +426,53 @@ class ExternalDataMixin(models.Model):
|
|
449
426
|
logger.info(f"🗑️ Deleted ExternalData for {self.__class__.__name__}: {self}")
|
450
427
|
except Exception as e:
|
451
428
|
logger.error(f"❌ Error deleting ExternalData: {e}")
|
452
|
-
|
429
|
+
|
453
430
|
@property
|
454
431
|
def has_external_data(self) -> bool:
|
455
432
|
"""Check if this instance has linked ExternalData."""
|
456
433
|
return bool(self.external_source_id)
|
457
|
-
|
434
|
+
|
458
435
|
@property
|
459
436
|
def external_data_status(self) -> Optional[str]:
|
460
437
|
"""Get status of linked ExternalData."""
|
461
438
|
if not self.external_source_id:
|
462
439
|
return None
|
463
|
-
|
440
|
+
|
464
441
|
try:
|
465
442
|
external_data = ExternalData.objects.get(id=self.external_source_id)
|
466
443
|
return external_data.status
|
467
444
|
except ExternalData.DoesNotExist:
|
468
445
|
return None
|
469
|
-
|
470
|
-
# ==========================================
|
471
|
-
# SMART AUTO-GENERATION METHODS
|
472
|
-
# ==========================================
|
473
|
-
|
474
|
-
@classmethod
|
475
|
-
def _auto_generate_title(cls, instance) -> str:
|
476
|
-
"""Auto-generate title based on model fields."""
|
477
|
-
# Try common title fields first
|
478
|
-
title_fields = ['title', 'name', 'full_name', 'display_name', 'label']
|
479
|
-
|
480
|
-
for field_name in title_fields:
|
481
|
-
if hasattr(instance, field_name):
|
482
|
-
value = getattr(instance, field_name, None)
|
483
|
-
if value and str(value).strip():
|
484
|
-
# Add model context for clarity
|
485
|
-
model_name = cls._meta.verbose_name or cls.__name__
|
486
|
-
return f"{model_name}: {value}"
|
487
|
-
|
488
|
-
# Fallback: use string representation with model name
|
489
|
-
model_name = cls._meta.verbose_name or cls.__name__
|
490
|
-
return f"{model_name}: {instance}"
|
491
|
-
|
492
|
-
@classmethod
|
493
|
-
def _auto_generate_description(cls, instance) -> str:
|
494
|
-
"""Auto-generate description based on model fields."""
|
495
|
-
model_name = cls._meta.verbose_name or cls.__name__
|
496
|
-
|
497
|
-
# Try common description fields
|
498
|
-
desc_fields = ['description', 'summary', 'about', 'details', 'info']
|
499
|
-
for field_name in desc_fields:
|
500
|
-
if hasattr(instance, field_name):
|
501
|
-
value = getattr(instance, field_name, None)
|
502
|
-
if value and str(value).strip():
|
503
|
-
return f"{model_name} information: {value}"
|
504
|
-
|
505
|
-
# Build description from key fields
|
506
|
-
key_info = []
|
507
|
-
|
508
|
-
# Add primary identifier
|
509
|
-
if hasattr(instance, 'name') and instance.name:
|
510
|
-
key_info.append(f"Name: {instance.name}")
|
511
|
-
elif hasattr(instance, 'title') and instance.title:
|
512
|
-
key_info.append(f"Title: {instance.title}")
|
513
|
-
|
514
|
-
# Add status if available
|
515
|
-
if hasattr(instance, 'is_active'):
|
516
|
-
status = "Active" if instance.is_active else "Inactive"
|
517
|
-
key_info.append(f"Status: {status}")
|
518
|
-
|
519
|
-
# Add creation date if available
|
520
|
-
if hasattr(instance, 'created_at') and instance.created_at:
|
521
|
-
key_info.append(f"Created: {instance.created_at.strftime('%Y-%m-%d')}")
|
522
|
-
|
523
|
-
if key_info:
|
524
|
-
return f"Comprehensive information about this {model_name.lower()}. {', '.join(key_info)}."
|
525
|
-
|
526
|
-
return f"Auto-generated information from {model_name} model."
|
527
|
-
|
528
|
-
@classmethod
|
529
|
-
def _auto_generate_tags(cls, instance) -> List[str]:
|
530
|
-
"""Auto-generate tags based on model fields and metadata."""
|
531
|
-
tags = []
|
532
|
-
|
533
|
-
# Add model-based tags
|
534
|
-
tags.append(cls.__name__.lower())
|
535
|
-
if cls._meta.verbose_name:
|
536
|
-
tags.append(cls._meta.verbose_name.lower().replace(' ', '_'))
|
537
|
-
|
538
|
-
# Add app label
|
539
|
-
tags.append(cls._meta.app_label)
|
540
|
-
|
541
|
-
# Add field-based tags
|
542
|
-
tag_fields = ['category', 'type', 'kind', 'status', 'brand', 'model']
|
543
|
-
for field_name in tag_fields:
|
544
|
-
if hasattr(instance, field_name):
|
545
|
-
value = getattr(instance, field_name, None)
|
546
|
-
if value:
|
547
|
-
# Handle foreign key relationships
|
548
|
-
if hasattr(value, 'name'):
|
549
|
-
tags.append(str(value.name).lower())
|
550
|
-
elif hasattr(value, 'code'):
|
551
|
-
tags.append(str(value.code).lower())
|
552
|
-
else:
|
553
|
-
tags.append(str(value).lower())
|
554
|
-
|
555
|
-
# Add boolean field tags
|
556
|
-
bool_fields = ['is_active', 'is_public', 'is_featured', 'is_published']
|
557
|
-
for field_name in bool_fields:
|
558
|
-
if hasattr(instance, field_name):
|
559
|
-
value = getattr(instance, field_name, None)
|
560
|
-
if value is True:
|
561
|
-
tags.append(field_name.replace('is_', ''))
|
562
|
-
|
563
|
-
# Clean and deduplicate tags
|
564
|
-
clean_tags = []
|
565
|
-
for tag in tags:
|
566
|
-
clean_tag = str(tag).lower().strip().replace(' ', '_')
|
567
|
-
if clean_tag and clean_tag not in clean_tags:
|
568
|
-
clean_tags.append(clean_tag)
|
569
|
-
|
570
|
-
return clean_tags[:10] # Limit to 10 tags
|
571
|
-
|
572
|
-
@classmethod
|
573
|
-
def _auto_generate_content(cls, instance) -> str:
|
574
|
-
"""Auto-generate comprehensive content based on model fields."""
|
575
|
-
content_parts = []
|
576
|
-
|
577
|
-
# Header with title
|
578
|
-
title = cls._auto_generate_title(instance)
|
579
|
-
content_parts.append(f"# {title}")
|
580
|
-
content_parts.append("")
|
581
|
-
|
582
|
-
# Basic Information section
|
583
|
-
content_parts.append("## Basic Information")
|
584
|
-
|
585
|
-
# Add key fields
|
586
|
-
key_fields = cls._get_content_fields(instance)
|
587
|
-
for field_name, field_value, field_label in key_fields:
|
588
|
-
if field_value is not None and str(field_value).strip():
|
589
|
-
content_parts.append(f"- **{field_label}**: {field_value}")
|
590
|
-
|
591
|
-
content_parts.append("")
|
592
|
-
|
593
|
-
# Add relationships section if any
|
594
|
-
relationships = cls._get_relationship_info(instance)
|
595
|
-
if relationships:
|
596
|
-
content_parts.append("## Related Information")
|
597
|
-
for rel_name, rel_info in relationships.items():
|
598
|
-
content_parts.append(f"- **{rel_name}**: {rel_info}")
|
599
|
-
content_parts.append("")
|
600
|
-
|
601
|
-
# Add statistics if available
|
602
|
-
stats = cls._get_statistics_info(instance)
|
603
|
-
if stats:
|
604
|
-
content_parts.append("## Statistics")
|
605
|
-
for stat_name, stat_value in stats.items():
|
606
|
-
content_parts.append(f"- **{stat_name}**: {stat_value}")
|
607
|
-
content_parts.append("")
|
608
|
-
|
609
|
-
# Add metadata section
|
610
|
-
content_parts.append("## Technical Information")
|
611
|
-
content_parts.append(f"This data is automatically synchronized from the {cls.__name__} model using ExternalDataMixin.")
|
612
|
-
content_parts.append(f"")
|
613
|
-
content_parts.append(f"**Model**: {cls._meta.label}")
|
614
|
-
content_parts.append(f"**ID**: {instance.pk}")
|
615
|
-
if hasattr(instance, 'created_at') and instance.created_at:
|
616
|
-
content_parts.append(f"**Created**: {instance.created_at.strftime('%Y-%m-%d %H:%M:%S')}")
|
617
|
-
if hasattr(instance, 'updated_at') and instance.updated_at:
|
618
|
-
content_parts.append(f"**Updated**: {instance.updated_at.strftime('%Y-%m-%d %H:%M:%S')}")
|
619
|
-
|
620
|
-
return "\n".join(content_parts)
|
621
|
-
|
622
|
-
@classmethod
|
623
|
-
def _get_content_fields(cls, instance) -> List[tuple]:
|
624
|
-
"""Get fields to include in content generation."""
|
625
|
-
fields_info = []
|
626
|
-
|
627
|
-
# Define field priority and labels
|
628
|
-
priority_fields = {
|
629
|
-
'name': 'Name',
|
630
|
-
'title': 'Title',
|
631
|
-
'code': 'Code',
|
632
|
-
'description': 'Description',
|
633
|
-
'summary': 'Summary',
|
634
|
-
'body_type': 'Body Type',
|
635
|
-
'segment': 'Segment',
|
636
|
-
'category': 'Category',
|
637
|
-
'type': 'Type',
|
638
|
-
'status': 'Status',
|
639
|
-
'is_active': 'Active',
|
640
|
-
'is_public': 'Public',
|
641
|
-
'price': 'Price',
|
642
|
-
'year': 'Year',
|
643
|
-
'fuel_type': 'Fuel Type',
|
644
|
-
}
|
645
|
-
|
646
|
-
# Add priority fields first
|
647
|
-
for field_name, field_label in priority_fields.items():
|
648
|
-
if hasattr(instance, field_name):
|
649
|
-
value = getattr(instance, field_name, None)
|
650
|
-
if value is not None:
|
651
|
-
# Format boolean fields
|
652
|
-
if isinstance(value, bool):
|
653
|
-
value = "Yes" if value else "No"
|
654
|
-
# Format choice fields
|
655
|
-
elif hasattr(instance, f'get_{field_name}_display'):
|
656
|
-
display_value = getattr(instance, f'get_{field_name}_display')()
|
657
|
-
if display_value:
|
658
|
-
value = display_value
|
659
|
-
# Format foreign key relationships
|
660
|
-
elif hasattr(value, '__str__'):
|
661
|
-
value = str(value)
|
662
|
-
|
663
|
-
fields_info.append((field_name, value, field_label))
|
664
|
-
|
665
|
-
return fields_info
|
666
|
-
|
667
|
-
@classmethod
|
668
|
-
def _get_relationship_info(cls, instance) -> Dict[str, str]:
|
669
|
-
"""Get relationship information for content."""
|
670
|
-
relationships = {}
|
671
|
-
|
672
|
-
# Common relationship field names
|
673
|
-
rel_fields = ['brand', 'category', 'parent', 'owner', 'user', 'created_by']
|
674
|
-
|
675
|
-
for field_name in rel_fields:
|
676
|
-
if hasattr(instance, field_name):
|
677
|
-
value = getattr(instance, field_name, None)
|
678
|
-
if value:
|
679
|
-
relationships[field_name.replace('_', ' ').title()] = str(value)
|
680
|
-
|
681
|
-
return relationships
|
682
|
-
|
683
|
-
@classmethod
|
684
|
-
def _get_statistics_info(cls, instance) -> Dict[str, Any]:
|
685
|
-
"""Get statistics information for content."""
|
686
|
-
stats = {}
|
687
|
-
|
688
|
-
# Common statistics field names
|
689
|
-
stat_fields = ['total_vehicles', 'total_models', 'total_items', 'count', 'views', 'likes']
|
690
|
-
|
691
|
-
for field_name in stat_fields:
|
692
|
-
if hasattr(instance, field_name):
|
693
|
-
value = getattr(instance, field_name, None)
|
694
|
-
if value is not None and (isinstance(value, (int, float)) and value > 0):
|
695
|
-
label = field_name.replace('_', ' ').title()
|
696
|
-
if isinstance(value, float):
|
697
|
-
stats[label] = f"{value:,.2f}"
|
698
|
-
else:
|
699
|
-
stats[label] = f"{value:,}"
|
700
|
-
|
701
|
-
return stats
|
702
|
-
|
703
|
-
@classmethod
|
704
|
-
def _auto_detect_watch_fields(cls) -> List[str]:
|
705
|
-
"""Auto-detect important fields to watch for changes."""
|
706
|
-
watch_fields = []
|
707
|
-
|
708
|
-
# Get all model fields
|
709
|
-
for field in cls._meta.get_fields():
|
710
|
-
if hasattr(field, 'name') and not field.name.startswith('_'):
|
711
|
-
field_name = field.name
|
712
|
-
|
713
|
-
# Skip auto-generated and system fields
|
714
|
-
skip_fields = {
|
715
|
-
'id', 'pk', 'created_at', 'updated_at', 'external_source_id',
|
716
|
-
'_external_content_hash', 'slug'
|
717
|
-
}
|
718
|
-
if field_name in skip_fields:
|
719
|
-
continue
|
720
|
-
|
721
|
-
# Skip reverse foreign keys and many-to-many
|
722
|
-
if hasattr(field, 'related_model') and field.many_to_many:
|
723
|
-
continue
|
724
|
-
if hasattr(field, 'remote_field') and field.remote_field and hasattr(field.remote_field, 'related_name'):
|
725
|
-
continue
|
726
|
-
|
727
|
-
# Include important field types
|
728
|
-
if hasattr(field, '__class__'):
|
729
|
-
field_type = field.__class__.__name__
|
730
|
-
important_types = {
|
731
|
-
'CharField', 'TextField', 'BooleanField', 'IntegerField',
|
732
|
-
'PositiveIntegerField', 'ForeignKey', 'DecimalField', 'FloatField'
|
733
|
-
}
|
734
|
-
if field_type in important_types:
|
735
|
-
watch_fields.append(field_name)
|
736
|
-
|
737
|
-
# If no fields detected, watch all non-system fields
|
738
|
-
if not watch_fields:
|
739
|
-
for field in cls._meta.get_fields():
|
740
|
-
if hasattr(field, 'name') and not field.name.startswith('_') and field.name not in {'id', 'pk'}:
|
741
|
-
watch_fields.append(field.name)
|
742
|
-
|
743
|
-
return watch_fields[:10] # Limit to prevent too many triggers
|
744
|
-
|
446
|
+
|
745
447
|
# ==========================================
|
746
448
|
# MANAGER-LEVEL METHODS (CLASS METHODS)
|
747
449
|
# ==========================================
|
748
|
-
|
450
|
+
|
749
451
|
@classmethod
|
750
452
|
def with_external_data(cls):
|
751
453
|
"""Return queryset of instances that have external data."""
|
752
454
|
return cls.objects.filter(external_source_id__isnull=False)
|
753
|
-
|
455
|
+
|
754
456
|
@classmethod
|
755
457
|
def without_external_data(cls):
|
756
458
|
"""Return queryset of instances that don't have external data."""
|
757
459
|
return cls.objects.filter(external_source_id__isnull=True)
|
758
|
-
|
460
|
+
|
759
461
|
@classmethod
|
760
462
|
def sync_all_external_data(cls, limit=None):
|
761
463
|
"""Sync external data for all instances that have it."""
|
762
464
|
instances_with_data = cls.with_external_data()
|
763
|
-
|
465
|
+
|
764
466
|
if limit:
|
765
467
|
instances_with_data = instances_with_data[:limit]
|
766
|
-
|
468
|
+
|
767
469
|
results = {
|
768
470
|
'total_processed': 0,
|
769
471
|
'successful_updates': 0,
|
770
472
|
'failed_updates': 0,
|
771
473
|
'errors': []
|
772
474
|
}
|
773
|
-
|
475
|
+
|
774
476
|
for instance in instances_with_data:
|
775
477
|
try:
|
776
478
|
instance.regenerate_external_data()
|
@@ -779,24 +481,24 @@ class ExternalDataMixin(models.Model):
|
|
779
481
|
except Exception as e:
|
780
482
|
results['failed_updates'] += 1
|
781
483
|
results['errors'].append(f"{instance}: {str(e)}")
|
782
|
-
|
484
|
+
|
783
485
|
return results
|
784
|
-
|
486
|
+
|
785
487
|
@classmethod
|
786
488
|
def create_external_data_for_all(cls, limit=None):
|
787
489
|
"""Create external data for all instances that don't have it."""
|
788
490
|
instances_without_data = cls.without_external_data()
|
789
|
-
|
491
|
+
|
790
492
|
if limit:
|
791
493
|
instances_without_data = instances_without_data[:limit]
|
792
|
-
|
494
|
+
|
793
495
|
results = {
|
794
496
|
'total_processed': 0,
|
795
497
|
'successful_creates': 0,
|
796
498
|
'failed_creates': 0,
|
797
499
|
'errors': []
|
798
500
|
}
|
799
|
-
|
501
|
+
|
800
502
|
for instance in instances_without_data:
|
801
503
|
try:
|
802
504
|
result = instance.create_external_data()
|
@@ -809,5 +511,5 @@ class ExternalDataMixin(models.Model):
|
|
809
511
|
except Exception as e:
|
810
512
|
results['failed_creates'] += 1
|
811
513
|
results['errors'].append(f"{instance}: {str(e)}")
|
812
|
-
|
514
|
+
|
813
515
|
return results
|