django-cfg 1.1.82__py3-none-any.whl → 1.2.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 +20 -448
- django_cfg/apps/accounts/README.md +3 -3
- django_cfg/apps/accounts/admin/__init__.py +0 -2
- django_cfg/apps/accounts/admin/activity.py +2 -9
- django_cfg/apps/accounts/admin/filters.py +0 -42
- django_cfg/apps/accounts/admin/inlines.py +8 -8
- django_cfg/apps/accounts/admin/otp.py +5 -5
- django_cfg/apps/accounts/admin/registration_source.py +1 -8
- django_cfg/apps/accounts/admin/user.py +12 -20
- django_cfg/apps/accounts/managers/user_manager.py +2 -129
- django_cfg/apps/accounts/migrations/0006_remove_twilioresponse_otp_secret_and_more.py +46 -0
- django_cfg/apps/accounts/models.py +3 -123
- django_cfg/apps/accounts/serializers/otp.py +40 -44
- django_cfg/apps/accounts/serializers/profile.py +0 -2
- django_cfg/apps/accounts/services/otp_service.py +98 -186
- django_cfg/apps/accounts/signals.py +25 -15
- django_cfg/apps/accounts/utils/auth_email_service.py +84 -0
- django_cfg/apps/accounts/views/otp.py +35 -36
- django_cfg/apps/agents/README.md +129 -0
- django_cfg/apps/agents/__init__.py +68 -0
- django_cfg/apps/agents/admin/__init__.py +17 -0
- django_cfg/apps/agents/admin/execution_admin.py +460 -0
- django_cfg/apps/agents/admin/registry_admin.py +360 -0
- django_cfg/apps/agents/admin/toolsets_admin.py +482 -0
- django_cfg/apps/agents/apps.py +29 -0
- django_cfg/apps/agents/core/__init__.py +20 -0
- django_cfg/apps/agents/core/agent.py +281 -0
- django_cfg/apps/agents/core/dependencies.py +154 -0
- django_cfg/apps/agents/core/exceptions.py +66 -0
- django_cfg/apps/agents/core/models.py +106 -0
- django_cfg/apps/agents/core/orchestrator.py +391 -0
- django_cfg/apps/agents/examples/__init__.py +3 -0
- django_cfg/apps/agents/examples/simple_example.py +161 -0
- django_cfg/apps/agents/integration/__init__.py +14 -0
- django_cfg/apps/agents/integration/middleware.py +80 -0
- django_cfg/apps/agents/integration/registry.py +345 -0
- django_cfg/apps/agents/integration/signals.py +50 -0
- django_cfg/apps/agents/management/__init__.py +3 -0
- django_cfg/apps/agents/management/commands/__init__.py +3 -0
- django_cfg/apps/agents/management/commands/create_agent.py +365 -0
- django_cfg/apps/agents/management/commands/orchestrator_status.py +191 -0
- django_cfg/apps/agents/managers/__init__.py +23 -0
- django_cfg/apps/agents/managers/execution.py +236 -0
- django_cfg/apps/agents/managers/registry.py +254 -0
- django_cfg/apps/agents/managers/toolsets.py +496 -0
- django_cfg/apps/agents/migrations/0001_initial.py +286 -0
- django_cfg/apps/agents/migrations/__init__.py +5 -0
- django_cfg/apps/agents/models/__init__.py +15 -0
- django_cfg/apps/agents/models/execution.py +215 -0
- django_cfg/apps/agents/models/registry.py +220 -0
- django_cfg/apps/agents/models/toolsets.py +305 -0
- django_cfg/apps/agents/patterns/__init__.py +24 -0
- django_cfg/apps/agents/patterns/content_agents.py +234 -0
- django_cfg/apps/agents/toolsets/__init__.py +15 -0
- django_cfg/apps/agents/toolsets/cache_toolset.py +285 -0
- django_cfg/apps/agents/toolsets/django_toolset.py +220 -0
- django_cfg/apps/agents/toolsets/file_toolset.py +324 -0
- django_cfg/apps/agents/toolsets/orm_toolset.py +319 -0
- django_cfg/apps/agents/urls.py +46 -0
- django_cfg/apps/knowbase/README.md +150 -0
- django_cfg/apps/knowbase/__init__.py +27 -0
- django_cfg/apps/knowbase/admin/__init__.py +23 -0
- django_cfg/apps/knowbase/admin/archive_admin.py +857 -0
- django_cfg/apps/knowbase/admin/chat_admin.py +386 -0
- django_cfg/apps/knowbase/admin/document_admin.py +650 -0
- django_cfg/apps/knowbase/admin/external_data_admin.py +685 -0
- django_cfg/apps/knowbase/apps.py +81 -0
- django_cfg/apps/knowbase/config/README.md +176 -0
- django_cfg/apps/knowbase/config/__init__.py +51 -0
- django_cfg/apps/knowbase/config/constance_fields.py +186 -0
- django_cfg/apps/knowbase/config/constance_settings.py +200 -0
- django_cfg/apps/knowbase/config/settings.py +444 -0
- django_cfg/apps/knowbase/examples/__init__.py +3 -0
- django_cfg/apps/knowbase/examples/external_data_usage.py +191 -0
- django_cfg/apps/knowbase/management/__init__.py +0 -0
- django_cfg/apps/knowbase/management/commands/__init__.py +0 -0
- django_cfg/apps/knowbase/management/commands/knowbase_stats.py +158 -0
- django_cfg/apps/knowbase/management/commands/setup_knowbase.py +59 -0
- django_cfg/apps/knowbase/managers/__init__.py +22 -0
- django_cfg/apps/knowbase/managers/archive.py +426 -0
- django_cfg/apps/knowbase/managers/base.py +32 -0
- django_cfg/apps/knowbase/managers/chat.py +141 -0
- django_cfg/apps/knowbase/managers/document.py +203 -0
- django_cfg/apps/knowbase/managers/external_data.py +471 -0
- django_cfg/apps/knowbase/migrations/0001_initial.py +427 -0
- django_cfg/apps/knowbase/migrations/0002_archiveitem_archiveitemchunk_documentarchive_and_more.py +434 -0
- django_cfg/apps/knowbase/migrations/__init__.py +5 -0
- django_cfg/apps/knowbase/mixins/__init__.py +15 -0
- django_cfg/apps/knowbase/mixins/config.py +108 -0
- django_cfg/apps/knowbase/mixins/creator.py +81 -0
- django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +199 -0
- django_cfg/apps/knowbase/mixins/external_data_mixin.py +813 -0
- django_cfg/apps/knowbase/mixins/service.py +362 -0
- django_cfg/apps/knowbase/models/__init__.py +41 -0
- django_cfg/apps/knowbase/models/archive.py +599 -0
- django_cfg/apps/knowbase/models/base.py +58 -0
- django_cfg/apps/knowbase/models/chat.py +157 -0
- django_cfg/apps/knowbase/models/document.py +267 -0
- django_cfg/apps/knowbase/models/external_data.py +376 -0
- django_cfg/apps/knowbase/serializers/__init__.py +68 -0
- django_cfg/apps/knowbase/serializers/archive_serializers.py +386 -0
- django_cfg/apps/knowbase/serializers/chat_serializers.py +137 -0
- django_cfg/apps/knowbase/serializers/document_serializers.py +94 -0
- django_cfg/apps/knowbase/serializers/external_data_serializers.py +256 -0
- django_cfg/apps/knowbase/serializers/public_serializers.py +74 -0
- django_cfg/apps/knowbase/services/__init__.py +40 -0
- django_cfg/apps/knowbase/services/archive/__init__.py +42 -0
- django_cfg/apps/knowbase/services/archive/archive_service.py +541 -0
- django_cfg/apps/knowbase/services/archive/chunking_service.py +791 -0
- django_cfg/apps/knowbase/services/archive/exceptions.py +52 -0
- django_cfg/apps/knowbase/services/archive/extraction_service.py +508 -0
- django_cfg/apps/knowbase/services/archive/vectorization_service.py +362 -0
- django_cfg/apps/knowbase/services/base.py +53 -0
- django_cfg/apps/knowbase/services/chat_service.py +239 -0
- django_cfg/apps/knowbase/services/document_service.py +144 -0
- django_cfg/apps/knowbase/services/embedding/__init__.py +43 -0
- django_cfg/apps/knowbase/services/embedding/async_processor.py +244 -0
- django_cfg/apps/knowbase/services/embedding/batch_processor.py +250 -0
- django_cfg/apps/knowbase/services/embedding/batch_result.py +61 -0
- django_cfg/apps/knowbase/services/embedding/models.py +229 -0
- django_cfg/apps/knowbase/services/embedding/processors.py +148 -0
- django_cfg/apps/knowbase/services/embedding/utils.py +176 -0
- django_cfg/apps/knowbase/services/prompt_builder.py +191 -0
- django_cfg/apps/knowbase/services/search_service.py +293 -0
- django_cfg/apps/knowbase/signals/__init__.py +21 -0
- django_cfg/apps/knowbase/signals/archive_signals.py +211 -0
- django_cfg/apps/knowbase/signals/chat_signals.py +37 -0
- django_cfg/apps/knowbase/signals/document_signals.py +143 -0
- django_cfg/apps/knowbase/signals/external_data_signals.py +157 -0
- django_cfg/apps/knowbase/tasks/__init__.py +39 -0
- django_cfg/apps/knowbase/tasks/archive_tasks.py +316 -0
- django_cfg/apps/knowbase/tasks/document_processing.py +341 -0
- django_cfg/apps/knowbase/tasks/external_data_tasks.py +341 -0
- django_cfg/apps/knowbase/tasks/maintenance.py +195 -0
- django_cfg/apps/knowbase/urls.py +43 -0
- django_cfg/apps/knowbase/utils/__init__.py +12 -0
- django_cfg/apps/knowbase/utils/chunk_settings.py +261 -0
- django_cfg/apps/knowbase/utils/text_processing.py +375 -0
- django_cfg/apps/knowbase/utils/validation.py +99 -0
- django_cfg/apps/knowbase/views/__init__.py +28 -0
- django_cfg/apps/knowbase/views/archive_views.py +469 -0
- django_cfg/apps/knowbase/views/base.py +49 -0
- django_cfg/apps/knowbase/views/chat_views.py +181 -0
- django_cfg/apps/knowbase/views/document_views.py +183 -0
- django_cfg/apps/knowbase/views/public_views.py +129 -0
- django_cfg/apps/leads/admin.py +70 -0
- django_cfg/apps/newsletter/admin.py +234 -0
- django_cfg/apps/newsletter/admin_filters.py +124 -0
- django_cfg/apps/support/admin.py +196 -0
- django_cfg/apps/support/admin_filters.py +71 -0
- django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
- django_cfg/apps/urls.py +5 -4
- django_cfg/cli/README.md +1 -1
- django_cfg/cli/commands/create_project.py +2 -2
- django_cfg/cli/commands/info.py +1 -1
- django_cfg/config.py +44 -0
- django_cfg/core/config.py +29 -82
- django_cfg/core/environment.py +1 -1
- django_cfg/core/generation.py +19 -107
- django_cfg/{integration.py → core/integration.py} +18 -16
- django_cfg/core/validation.py +1 -1
- django_cfg/management/__init__.py +1 -1
- django_cfg/management/commands/__init__.py +1 -1
- django_cfg/management/commands/auto_generate.py +482 -0
- django_cfg/management/commands/migrator.py +19 -101
- django_cfg/management/commands/test_email.py +1 -1
- django_cfg/middleware/README.md +0 -158
- django_cfg/middleware/__init__.py +0 -2
- django_cfg/middleware/user_activity.py +3 -3
- django_cfg/models/api.py +145 -0
- django_cfg/models/base.py +287 -0
- django_cfg/models/cache.py +4 -4
- django_cfg/models/constance.py +25 -88
- django_cfg/models/database.py +9 -9
- django_cfg/models/drf.py +3 -36
- django_cfg/models/email.py +163 -0
- django_cfg/models/environment.py +276 -0
- django_cfg/models/limits.py +1 -1
- django_cfg/models/logging.py +366 -0
- django_cfg/models/revolution.py +41 -2
- django_cfg/models/security.py +125 -0
- django_cfg/models/services.py +1 -1
- django_cfg/modules/__init__.py +2 -56
- django_cfg/modules/base.py +78 -52
- django_cfg/modules/django_currency/service.py +2 -2
- django_cfg/modules/django_email.py +2 -2
- django_cfg/modules/django_health.py +267 -0
- django_cfg/modules/django_llm/llm/client.py +79 -17
- django_cfg/modules/django_llm/translator/translator.py +2 -2
- django_cfg/modules/django_logger.py +2 -2
- django_cfg/modules/django_ngrok.py +2 -2
- django_cfg/modules/django_tasks.py +68 -3
- django_cfg/modules/django_telegram.py +3 -3
- django_cfg/modules/django_twilio/sendgrid_service.py +2 -2
- django_cfg/modules/django_twilio/service.py +2 -2
- django_cfg/modules/django_twilio/simple_service.py +2 -2
- django_cfg/modules/django_twilio/twilio_service.py +2 -2
- django_cfg/modules/django_unfold/__init__.py +69 -0
- django_cfg/modules/{unfold → django_unfold}/callbacks.py +23 -22
- django_cfg/modules/django_unfold/dashboard.py +278 -0
- django_cfg/modules/django_unfold/icons/README.md +145 -0
- django_cfg/modules/django_unfold/icons/__init__.py +12 -0
- django_cfg/modules/django_unfold/icons/constants.py +2851 -0
- django_cfg/modules/django_unfold/icons/generate_icons.py +486 -0
- django_cfg/modules/django_unfold/models/__init__.py +42 -0
- django_cfg/modules/django_unfold/models/config.py +601 -0
- django_cfg/modules/django_unfold/models/dashboard.py +206 -0
- django_cfg/modules/django_unfold/models/dropdown.py +40 -0
- django_cfg/modules/django_unfold/models/navigation.py +73 -0
- django_cfg/modules/django_unfold/models/tabs.py +25 -0
- django_cfg/modules/{unfold → django_unfold}/system_monitor.py +2 -2
- django_cfg/modules/django_unfold/utils.py +140 -0
- django_cfg/registry/__init__.py +23 -0
- django_cfg/registry/core.py +61 -0
- django_cfg/registry/exceptions.py +11 -0
- django_cfg/registry/modules.py +12 -0
- django_cfg/registry/services.py +26 -0
- django_cfg/registry/third_party.py +52 -0
- django_cfg/routing/__init__.py +19 -0
- django_cfg/routing/callbacks.py +198 -0
- django_cfg/routing/routers.py +48 -0
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +8 -9
- django_cfg/templatetags/__init__.py +0 -0
- django_cfg/templatetags/django_cfg.py +33 -0
- django_cfg/urls.py +33 -0
- django_cfg/utils/path_resolution.py +1 -1
- django_cfg/utils/smart_defaults.py +7 -61
- django_cfg/utils/toolkit.py +663 -0
- {django_cfg-1.1.82.dist-info → django_cfg-1.2.0.dist-info}/METADATA +83 -86
- django_cfg-1.2.0.dist-info/RECORD +441 -0
- django_cfg/archive/django_sample.zip +0 -0
- django_cfg/models/unfold.py +0 -271
- django_cfg/modules/unfold/__init__.py +0 -29
- django_cfg/modules/unfold/dashboard.py +0 -318
- django_cfg/pyproject.toml +0 -370
- django_cfg/routers.py +0 -83
- django_cfg-1.1.82.dist-info/RECORD +0 -278
- /django_cfg/{exceptions.py → core/exceptions.py} +0 -0
- /django_cfg/modules/{unfold → django_unfold}/models.py +0 -0
- /django_cfg/modules/{unfold → django_unfold}/tailwind.py +0 -0
- /django_cfg/{version_check.py → utils/version_check.py} +0 -0
- {django_cfg-1.1.82.dist-info → django_cfg-1.2.0.dist-info}/WHEEL +0 -0
- {django_cfg-1.1.82.dist-info → django_cfg-1.2.0.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.1.82.dist-info → django_cfg-1.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,813 @@
|
|
1
|
+
"""
|
2
|
+
Mixin for automatic ExternalData integration.
|
3
|
+
|
4
|
+
This mixin provides automatic integration with knowbase ExternalData system:
|
5
|
+
- Adds external_source_id field automatically
|
6
|
+
- Tracks model changes and updates vectorization
|
7
|
+
- Provides simple configuration interface
|
8
|
+
- Handles creation, updates, and deletion automatically
|
9
|
+
|
10
|
+
Usage:
|
11
|
+
class MyModel(ExternalDataMixin, models.Model):
|
12
|
+
name = models.CharField(max_length=100)
|
13
|
+
description = models.TextField()
|
14
|
+
|
15
|
+
class Meta:
|
16
|
+
# Standard Django Meta options...
|
17
|
+
|
18
|
+
class ExternalDataMeta:
|
19
|
+
# Required: fields to watch for changes
|
20
|
+
watch_fields = ['name', 'description']
|
21
|
+
|
22
|
+
# Optional: similarity threshold (default: 0.5)
|
23
|
+
similarity_threshold = 0.4
|
24
|
+
|
25
|
+
# Optional: source type (default: ExternalDataType.MODEL)
|
26
|
+
source_type = ExternalDataType.CUSTOM
|
27
|
+
|
28
|
+
# Optional: enable/disable auto-sync (default: True)
|
29
|
+
auto_sync = True
|
30
|
+
|
31
|
+
# Optional: make public (default: False)
|
32
|
+
is_public = False
|
33
|
+
|
34
|
+
# Required: content generation method
|
35
|
+
def get_external_content(self):
|
36
|
+
return f"# {self.name}\n\n{self.description}"
|
37
|
+
|
38
|
+
# Optional: custom title (default: str(instance))
|
39
|
+
def get_external_title(self):
|
40
|
+
return f"My Model: {self.name}"
|
41
|
+
|
42
|
+
# Optional: custom description (default: auto-generated)
|
43
|
+
def get_external_description(self):
|
44
|
+
return f"Information about {self.name}"
|
45
|
+
|
46
|
+
# Optional: metadata (default: basic model info)
|
47
|
+
def get_external_metadata(self):
|
48
|
+
return {
|
49
|
+
'model_type': 'my_model',
|
50
|
+
'model_id': str(self.id),
|
51
|
+
'name': self.name,
|
52
|
+
}
|
53
|
+
|
54
|
+
# Optional: tags (default: [model_name.lower()])
|
55
|
+
def get_external_tags(self):
|
56
|
+
return ['my_model', self.name.lower()]
|
57
|
+
"""
|
58
|
+
|
59
|
+
import logging
|
60
|
+
import hashlib
|
61
|
+
from typing import Optional, List, Dict, Any, Type
|
62
|
+
from django.db import models
|
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
|
+
|
67
|
+
from ..models.external_data import ExternalData, ExternalDataType, ExternalDataStatus
|
68
|
+
from .creator import ExternalDataCreator
|
69
|
+
from .config import ExternalDataConfig
|
70
|
+
|
71
|
+
logger = logging.getLogger(__name__)
|
72
|
+
|
73
|
+
|
74
|
+
class ExternalDataMixin(models.Model):
|
75
|
+
"""
|
76
|
+
Mixin that automatically integrates models with knowbase ExternalData system.
|
77
|
+
|
78
|
+
Provides:
|
79
|
+
- Automatic external_source_id field
|
80
|
+
- Change tracking and vectorization
|
81
|
+
- Simple configuration interface
|
82
|
+
- Automatic cleanup on deletion
|
83
|
+
"""
|
84
|
+
|
85
|
+
# Automatically added field for linking to ExternalData
|
86
|
+
external_source_id = models.UUIDField(
|
87
|
+
null=True,
|
88
|
+
blank=True,
|
89
|
+
db_index=True,
|
90
|
+
help_text="UUID of the linked ExternalData object in knowbase",
|
91
|
+
verbose_name="External Source ID"
|
92
|
+
)
|
93
|
+
|
94
|
+
# Track content hash for change detection
|
95
|
+
_external_content_hash = models.CharField(
|
96
|
+
max_length=64,
|
97
|
+
blank=True,
|
98
|
+
help_text="SHA256 hash of content for change detection",
|
99
|
+
verbose_name="Content Hash"
|
100
|
+
)
|
101
|
+
|
102
|
+
class Meta:
|
103
|
+
abstract = True
|
104
|
+
|
105
|
+
def __init_subclass__(cls, **kwargs):
|
106
|
+
"""Register signal handlers for each subclass."""
|
107
|
+
super().__init_subclass__(**kwargs)
|
108
|
+
|
109
|
+
# Register signals for this specific model class
|
110
|
+
post_save.connect(
|
111
|
+
cls._external_data_post_save_handler,
|
112
|
+
sender=cls,
|
113
|
+
dispatch_uid=f"external_data_mixin_{cls.__name__}"
|
114
|
+
)
|
115
|
+
|
116
|
+
post_delete.connect(
|
117
|
+
cls._external_data_post_delete_handler,
|
118
|
+
sender=cls,
|
119
|
+
dispatch_uid=f"external_data_mixin_delete_{cls.__name__}"
|
120
|
+
)
|
121
|
+
|
122
|
+
@classmethod
|
123
|
+
def _external_data_post_save_handler(cls, sender, instance, created, **kwargs):
|
124
|
+
"""Handle post_save signal for ExternalData integration."""
|
125
|
+
try:
|
126
|
+
meta_config = cls._get_external_data_meta()
|
127
|
+
if not meta_config or not meta_config.get('auto_sync', True):
|
128
|
+
return
|
129
|
+
|
130
|
+
# Check if we should process this save (only if watched fields changed)
|
131
|
+
if not created and not cls._should_update_external_data(instance, kwargs):
|
132
|
+
logger.debug(f"📊 No relevant field changes for {cls.__name__}: {instance}")
|
133
|
+
return
|
134
|
+
|
135
|
+
# Check if content changed
|
136
|
+
current_content = cls._get_content_for_instance(instance)
|
137
|
+
current_hash = cls._calculate_content_hash(current_content)
|
138
|
+
|
139
|
+
if created:
|
140
|
+
# New instance - create ExternalData
|
141
|
+
logger.info(f"🔗 Creating ExternalData for new {cls.__name__}: {instance}")
|
142
|
+
instance._external_content_hash = current_hash
|
143
|
+
instance.save(update_fields=['_external_content_hash'])
|
144
|
+
cls._create_external_data(instance)
|
145
|
+
|
146
|
+
elif instance._external_content_hash != current_hash:
|
147
|
+
# Content changed - update ExternalData
|
148
|
+
logger.info(f"🔮 Content changed for {cls.__name__}: {instance}, updating ExternalData")
|
149
|
+
instance._external_content_hash = current_hash
|
150
|
+
instance.save(update_fields=['_external_content_hash'])
|
151
|
+
|
152
|
+
if instance.external_source_id:
|
153
|
+
cls._update_external_data(instance)
|
154
|
+
else:
|
155
|
+
cls._create_external_data(instance)
|
156
|
+
else:
|
157
|
+
logger.debug(f"📊 No content changes for {cls.__name__}: {instance}")
|
158
|
+
|
159
|
+
except Exception as e:
|
160
|
+
logger.error(f"❌ Error in ExternalData post_save handler for {cls.__name__}: {e}")
|
161
|
+
|
162
|
+
@classmethod
|
163
|
+
def _external_data_post_delete_handler(cls, sender, instance, **kwargs):
|
164
|
+
"""Handle post_delete signal for ExternalData cleanup."""
|
165
|
+
try:
|
166
|
+
if instance.external_source_id:
|
167
|
+
logger.info(f"🗑️ Cleaning up ExternalData for deleted {cls.__name__}: {instance}")
|
168
|
+
ExternalData.objects.filter(id=instance.external_source_id).delete()
|
169
|
+
except Exception as e:
|
170
|
+
logger.error(f"❌ Error cleaning up ExternalData for {cls.__name__}: {e}")
|
171
|
+
|
172
|
+
@classmethod
|
173
|
+
def _get_external_data_meta(cls) -> Dict[str, Any]:
|
174
|
+
"""Get ExternalDataMeta configuration from the model or auto-generate smart defaults."""
|
175
|
+
config = {}
|
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
|
+
|
206
|
+
@classmethod
|
207
|
+
def _should_update_external_data(cls, instance, save_kwargs) -> bool:
|
208
|
+
"""Check if we should update ExternalData based on changed fields."""
|
209
|
+
meta_config = cls._get_external_data_meta()
|
210
|
+
if not meta_config:
|
211
|
+
return True # No config = update always
|
212
|
+
|
213
|
+
watch_fields = meta_config.get('watch_fields', [])
|
214
|
+
if not watch_fields:
|
215
|
+
return True # No watch fields = update always
|
216
|
+
|
217
|
+
# Check if update_fields was used in save()
|
218
|
+
update_fields = save_kwargs.get('update_fields')
|
219
|
+
if update_fields is not None:
|
220
|
+
# Only update if any watched field was updated
|
221
|
+
return any(field in update_fields for field in watch_fields)
|
222
|
+
|
223
|
+
# If no update_fields specified, assume all fields might have changed
|
224
|
+
return True
|
225
|
+
|
226
|
+
@classmethod
|
227
|
+
def _get_content_for_instance(cls, instance) -> str:
|
228
|
+
"""Get content string for the instance."""
|
229
|
+
if hasattr(instance, 'get_external_content'):
|
230
|
+
try:
|
231
|
+
return str(instance.get_external_content())
|
232
|
+
except Exception as e:
|
233
|
+
logger.warning(f"Error calling get_external_content on {cls.__name__}: {e}")
|
234
|
+
|
235
|
+
# Smart auto-generation based on model fields
|
236
|
+
return cls._auto_generate_content(instance)
|
237
|
+
|
238
|
+
@classmethod
|
239
|
+
def _get_title_for_instance(cls, instance) -> str:
|
240
|
+
"""Get title for the instance."""
|
241
|
+
if hasattr(instance, 'get_external_title'):
|
242
|
+
try:
|
243
|
+
return str(instance.get_external_title())
|
244
|
+
except Exception as e:
|
245
|
+
logger.warning(f"Error calling get_external_title on {cls.__name__}: {e}")
|
246
|
+
|
247
|
+
# Smart auto-generation based on model fields
|
248
|
+
return cls._auto_generate_title(instance)
|
249
|
+
|
250
|
+
@classmethod
|
251
|
+
def _get_description_for_instance(cls, instance) -> str:
|
252
|
+
"""Get description for the instance."""
|
253
|
+
if hasattr(instance, 'get_external_description'):
|
254
|
+
try:
|
255
|
+
return str(instance.get_external_description())
|
256
|
+
except Exception as e:
|
257
|
+
logger.warning(f"Error calling get_external_description on {cls.__name__}: {e}")
|
258
|
+
|
259
|
+
# Smart auto-generation based on model fields
|
260
|
+
return cls._auto_generate_description(instance)
|
261
|
+
|
262
|
+
@classmethod
|
263
|
+
def _get_tags_for_instance(cls, instance) -> List[str]:
|
264
|
+
"""Get tags for the instance."""
|
265
|
+
if hasattr(instance, 'get_external_tags'):
|
266
|
+
try:
|
267
|
+
tags = instance.get_external_tags()
|
268
|
+
if isinstance(tags, (list, tuple)):
|
269
|
+
return list(tags)
|
270
|
+
return [str(tags)]
|
271
|
+
except Exception as e:
|
272
|
+
logger.warning(f"Error calling get_external_tags on {cls.__name__}: {e}")
|
273
|
+
|
274
|
+
# Smart auto-generation based on model fields
|
275
|
+
return cls._auto_generate_tags(instance)
|
276
|
+
|
277
|
+
@classmethod
|
278
|
+
def _calculate_content_hash(cls, content: str) -> str:
|
279
|
+
"""Calculate SHA256 hash of content."""
|
280
|
+
return hashlib.sha256(content.encode('utf-8')).hexdigest()
|
281
|
+
|
282
|
+
@classmethod
|
283
|
+
def _create_external_data(cls, instance):
|
284
|
+
"""Create ExternalData for the instance."""
|
285
|
+
try:
|
286
|
+
meta_config = cls._get_external_data_meta()
|
287
|
+
if not meta_config:
|
288
|
+
logger.warning(f"No ExternalDataMeta found for {cls.__name__}")
|
289
|
+
return
|
290
|
+
|
291
|
+
# Get user (try to find from instance or use superuser)
|
292
|
+
user = cls._get_user_for_instance(instance)
|
293
|
+
|
294
|
+
# Build ExternalDataConfig
|
295
|
+
external_config = ExternalDataConfig(
|
296
|
+
title=cls._get_title_for_instance(instance),
|
297
|
+
description=cls._get_description_for_instance(instance),
|
298
|
+
source_type=meta_config.get('source_type', ExternalDataType.MODEL),
|
299
|
+
source_identifier=f"{cls._meta.label_lower}_{instance.pk}",
|
300
|
+
content=cls._get_content_for_instance(instance),
|
301
|
+
similarity_threshold=meta_config.get('similarity_threshold', 0.5),
|
302
|
+
is_active=True,
|
303
|
+
is_public=meta_config.get('is_public', False),
|
304
|
+
metadata=cls._build_metadata(instance, meta_config),
|
305
|
+
tags=cls._get_tags_for_instance(instance),
|
306
|
+
source_config={
|
307
|
+
'model': cls._meta.label_lower,
|
308
|
+
'pk': str(instance.pk),
|
309
|
+
'auto_sync': meta_config.get('auto_sync', True),
|
310
|
+
'watch_fields': meta_config.get('watch_fields', []),
|
311
|
+
}
|
312
|
+
)
|
313
|
+
|
314
|
+
# Create ExternalData
|
315
|
+
creator = ExternalDataCreator(user)
|
316
|
+
result = creator.create_from_config(external_config)
|
317
|
+
|
318
|
+
if result['success']:
|
319
|
+
external_data = result['external_data']
|
320
|
+
instance.external_source_id = external_data.id
|
321
|
+
instance.save(update_fields=['external_source_id'])
|
322
|
+
logger.info(f"✅ Created ExternalData {external_data.id} for {cls.__name__}: {instance}")
|
323
|
+
else:
|
324
|
+
logger.error(f"❌ Failed to create ExternalData for {cls.__name__}: {result.get('error')}")
|
325
|
+
|
326
|
+
except Exception as e:
|
327
|
+
logger.error(f"❌ Error creating ExternalData for {cls.__name__}: {e}")
|
328
|
+
|
329
|
+
@classmethod
|
330
|
+
def _update_external_data(cls, instance):
|
331
|
+
"""Update existing ExternalData for the instance."""
|
332
|
+
try:
|
333
|
+
if not instance.external_source_id:
|
334
|
+
return
|
335
|
+
|
336
|
+
external_data = ExternalData.objects.get(id=instance.external_source_id)
|
337
|
+
meta_config = cls._get_external_data_meta() or {}
|
338
|
+
|
339
|
+
# Update fields using the same methods as creation
|
340
|
+
external_data.title = cls._get_title_for_instance(instance)
|
341
|
+
external_data.description = cls._get_description_for_instance(instance)
|
342
|
+
external_data.content = cls._get_content_for_instance(instance)
|
343
|
+
external_data.metadata = cls._build_metadata(instance, meta_config)
|
344
|
+
external_data.tags = cls._get_tags_for_instance(instance)
|
345
|
+
external_data.similarity_threshold = meta_config.get('similarity_threshold', 0.5)
|
346
|
+
external_data.status = ExternalDataStatus.PENDING # Mark for reprocessing
|
347
|
+
|
348
|
+
external_data.save()
|
349
|
+
logger.info(f"✅ Updated ExternalData {external_data.id} for {cls.__name__}: {instance}")
|
350
|
+
|
351
|
+
except ExternalData.DoesNotExist:
|
352
|
+
logger.warning(f"ExternalData {instance.external_source_id} not found, creating new one")
|
353
|
+
cls._create_external_data(instance)
|
354
|
+
except Exception as e:
|
355
|
+
logger.error(f"❌ Error updating ExternalData for {cls.__name__}: {e}")
|
356
|
+
|
357
|
+
@classmethod
|
358
|
+
def _build_metadata(cls, instance, config: Dict[str, Any]) -> Dict[str, Any]:
|
359
|
+
"""Build metadata dictionary for ExternalData."""
|
360
|
+
metadata = {
|
361
|
+
'model': cls._meta.label_lower,
|
362
|
+
'model_name': cls.__name__,
|
363
|
+
'pk': str(instance.pk),
|
364
|
+
'app_label': cls._meta.app_label,
|
365
|
+
'created_at': getattr(instance, 'created_at', None),
|
366
|
+
'updated_at': getattr(instance, 'updated_at', None),
|
367
|
+
}
|
368
|
+
|
369
|
+
# Add custom metadata if method exists
|
370
|
+
if hasattr(instance, 'get_external_metadata'):
|
371
|
+
try:
|
372
|
+
custom_metadata = instance.get_external_metadata()
|
373
|
+
if isinstance(custom_metadata, dict):
|
374
|
+
metadata.update(custom_metadata)
|
375
|
+
except Exception as e:
|
376
|
+
logger.warning(f"Error calling get_external_metadata on {cls.__name__}: {e}")
|
377
|
+
|
378
|
+
# Convert datetime objects to strings
|
379
|
+
for key, value in metadata.items():
|
380
|
+
if hasattr(value, 'isoformat'):
|
381
|
+
metadata[key] = value.isoformat()
|
382
|
+
|
383
|
+
return metadata
|
384
|
+
|
385
|
+
@classmethod
|
386
|
+
def _get_user_for_instance(cls, instance):
|
387
|
+
"""Get user for ExternalData ownership."""
|
388
|
+
# Try to get user from instance
|
389
|
+
if hasattr(instance, 'user'):
|
390
|
+
return instance.user
|
391
|
+
if hasattr(instance, 'created_by'):
|
392
|
+
return instance.created_by
|
393
|
+
if hasattr(instance, 'owner'):
|
394
|
+
return instance.owner
|
395
|
+
|
396
|
+
# Fallback to superuser
|
397
|
+
from django.contrib.auth import get_user_model
|
398
|
+
User = get_user_model()
|
399
|
+
superuser = User.objects.filter(is_superuser=True).first()
|
400
|
+
if superuser:
|
401
|
+
return superuser
|
402
|
+
|
403
|
+
raise ValueError("No user found for ExternalData ownership")
|
404
|
+
|
405
|
+
def regenerate_external_data(self):
|
406
|
+
"""Manually regenerate ExternalData for this instance."""
|
407
|
+
if self.external_source_id:
|
408
|
+
self._update_external_data(self)
|
409
|
+
else:
|
410
|
+
self._create_external_data(self)
|
411
|
+
|
412
|
+
def create_external_data(self, user=None):
|
413
|
+
"""Create ExternalData for this instance if it doesn't exist."""
|
414
|
+
if self.external_source_id:
|
415
|
+
return {
|
416
|
+
'success': False,
|
417
|
+
'error': f'External data already exists: {self.external_source_id}',
|
418
|
+
'external_data': None
|
419
|
+
}
|
420
|
+
|
421
|
+
try:
|
422
|
+
self._create_external_data(self)
|
423
|
+
if self.external_source_id:
|
424
|
+
return {
|
425
|
+
'success': True,
|
426
|
+
'message': f'External data created for {self}',
|
427
|
+
'external_data': self.external_source_id
|
428
|
+
}
|
429
|
+
else:
|
430
|
+
return {
|
431
|
+
'success': False,
|
432
|
+
'error': f'Failed to create external data for {self}',
|
433
|
+
'external_data': None
|
434
|
+
}
|
435
|
+
except Exception as e:
|
436
|
+
return {
|
437
|
+
'success': False,
|
438
|
+
'error': f'Error creating external data: {str(e)}',
|
439
|
+
'external_data': None
|
440
|
+
}
|
441
|
+
|
442
|
+
def delete_external_data(self):
|
443
|
+
"""Manually delete ExternalData for this instance."""
|
444
|
+
if self.external_source_id:
|
445
|
+
try:
|
446
|
+
ExternalData.objects.filter(id=self.external_source_id).delete()
|
447
|
+
self.external_source_id = None
|
448
|
+
self.save(update_fields=['external_source_id'])
|
449
|
+
logger.info(f"🗑️ Deleted ExternalData for {self.__class__.__name__}: {self}")
|
450
|
+
except Exception as e:
|
451
|
+
logger.error(f"❌ Error deleting ExternalData: {e}")
|
452
|
+
|
453
|
+
@property
|
454
|
+
def has_external_data(self) -> bool:
|
455
|
+
"""Check if this instance has linked ExternalData."""
|
456
|
+
return bool(self.external_source_id)
|
457
|
+
|
458
|
+
@property
|
459
|
+
def external_data_status(self) -> Optional[str]:
|
460
|
+
"""Get status of linked ExternalData."""
|
461
|
+
if not self.external_source_id:
|
462
|
+
return None
|
463
|
+
|
464
|
+
try:
|
465
|
+
external_data = ExternalData.objects.get(id=self.external_source_id)
|
466
|
+
return external_data.status
|
467
|
+
except ExternalData.DoesNotExist:
|
468
|
+
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
|
+
|
745
|
+
# ==========================================
|
746
|
+
# MANAGER-LEVEL METHODS (CLASS METHODS)
|
747
|
+
# ==========================================
|
748
|
+
|
749
|
+
@classmethod
|
750
|
+
def with_external_data(cls):
|
751
|
+
"""Return queryset of instances that have external data."""
|
752
|
+
return cls.objects.filter(external_source_id__isnull=False)
|
753
|
+
|
754
|
+
@classmethod
|
755
|
+
def without_external_data(cls):
|
756
|
+
"""Return queryset of instances that don't have external data."""
|
757
|
+
return cls.objects.filter(external_source_id__isnull=True)
|
758
|
+
|
759
|
+
@classmethod
|
760
|
+
def sync_all_external_data(cls, limit=None):
|
761
|
+
"""Sync external data for all instances that have it."""
|
762
|
+
instances_with_data = cls.with_external_data()
|
763
|
+
|
764
|
+
if limit:
|
765
|
+
instances_with_data = instances_with_data[:limit]
|
766
|
+
|
767
|
+
results = {
|
768
|
+
'total_processed': 0,
|
769
|
+
'successful_updates': 0,
|
770
|
+
'failed_updates': 0,
|
771
|
+
'errors': []
|
772
|
+
}
|
773
|
+
|
774
|
+
for instance in instances_with_data:
|
775
|
+
try:
|
776
|
+
instance.regenerate_external_data()
|
777
|
+
results['successful_updates'] += 1
|
778
|
+
results['total_processed'] += 1
|
779
|
+
except Exception as e:
|
780
|
+
results['failed_updates'] += 1
|
781
|
+
results['errors'].append(f"{instance}: {str(e)}")
|
782
|
+
|
783
|
+
return results
|
784
|
+
|
785
|
+
@classmethod
|
786
|
+
def create_external_data_for_all(cls, limit=None):
|
787
|
+
"""Create external data for all instances that don't have it."""
|
788
|
+
instances_without_data = cls.without_external_data()
|
789
|
+
|
790
|
+
if limit:
|
791
|
+
instances_without_data = instances_without_data[:limit]
|
792
|
+
|
793
|
+
results = {
|
794
|
+
'total_processed': 0,
|
795
|
+
'successful_creates': 0,
|
796
|
+
'failed_creates': 0,
|
797
|
+
'errors': []
|
798
|
+
}
|
799
|
+
|
800
|
+
for instance in instances_without_data:
|
801
|
+
try:
|
802
|
+
result = instance.create_external_data()
|
803
|
+
if result['success']:
|
804
|
+
results['successful_creates'] += 1
|
805
|
+
else:
|
806
|
+
results['failed_creates'] += 1
|
807
|
+
results['errors'].append(f"{instance}: {result['error']}")
|
808
|
+
results['total_processed'] += 1
|
809
|
+
except Exception as e:
|
810
|
+
results['failed_creates'] += 1
|
811
|
+
results['errors'].append(f"{instance}: {str(e)}")
|
812
|
+
|
813
|
+
return results
|