django-cfg 1.4.117__py3-none-any.whl → 1.4.119__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of django-cfg might be problematic. Click here for more details.

django_cfg/__init__.py CHANGED
@@ -32,7 +32,7 @@ Example:
32
32
  default_app_config = "django_cfg.apps.DjangoCfgConfig"
33
33
 
34
34
  # Version information
35
- __version__ = "1.4.117"
35
+ __version__ = "1.4.119"
36
36
  __license__ = "MIT"
37
37
 
38
38
  # Import registry for organized lazy loading
@@ -166,6 +166,8 @@ class InstalledAppsBuilder:
166
166
  # Add django-q2 if enabled
167
167
  if hasattr(self.config, "django_q2") and self.config.django_q2 and self.config.django_q2.enabled:
168
168
  apps.append("django_q")
169
+ # Auto-add django_q2 module for automatic schedule synchronization
170
+ apps.append("django_cfg.modules.django_q2")
169
171
 
170
172
  # Add DRF Tailwind theme module (uses Tailwind via CDN)
171
173
  if self.config.enable_drf_tailwind:
@@ -45,6 +45,7 @@ DEFAULT_APPS: List[str] = [
45
45
  # Django CFG Core
46
46
  "django_cfg",
47
47
  "django_cfg.modules.django_client",
48
+ "django_cfg.modules.django_admin",
48
49
  ]
49
50
 
50
51
  # Default middleware stack
@@ -302,6 +302,19 @@ class DjangoQ2Config(BaseModel):
302
302
  )
303
303
  ```
304
304
 
305
+ Schedule Synchronization (AUTOMATIC):
306
+ When Django-Q2 is enabled, schedules are automatically synced after migrations.
307
+
308
+ The module 'django_cfg.modules.django_q2' is automatically added to INSTALLED_APPS
309
+ when django_q2.enabled=True, so you don't need to add it manually.
310
+
311
+ It uses Django's post_migrate signal to sync schedules from config to database.
312
+
313
+ Manual sync (optional):
314
+ ```bash
315
+ python manage.py sync_django_q_schedules
316
+ ```
317
+
305
318
  Admin interface:
306
319
  - Visit /admin/django_q/ to view tasks and schedules
307
320
  - Monitor task execution, failures, and performance
@@ -0,0 +1,19 @@
1
+ """
2
+ Django app configuration for django_admin module.
3
+
4
+ This makes django_admin a proper Django app so templates are automatically discovered.
5
+ """
6
+ from django.apps import AppConfig
7
+
8
+
9
+ class DjangoAdminConfig(AppConfig):
10
+ """Configuration for django_admin module."""
11
+
12
+ default_auto_field = "django.db.models.BigAutoField"
13
+ name = "django_cfg.modules.django_admin"
14
+ label = "django_cfg_admin"
15
+ verbose_name = "Django Admin (django-cfg)"
16
+
17
+ def ready(self):
18
+ """Called when Django is ready."""
19
+ pass
@@ -558,12 +558,14 @@ class PydanticAdminMixin:
558
558
  Override form field for specific database field types.
559
559
 
560
560
  Automatically detects and customizes encrypted fields from django-crypto-fields.
561
+ Respects the show_encrypted_fields_as_plain_text setting from AdminConfig.
562
+ Uses custom widgets with copy-to-clipboard functionality.
561
563
  """
562
564
  # Check if this is an EncryptedTextField or EncryptedCharField
563
565
  field_class_name = db_field.__class__.__name__
564
566
  if 'Encrypted' in field_class_name and ('TextField' in field_class_name or 'CharField' in field_class_name):
565
567
  from django import forms
566
- from django.forms.widgets import PasswordInput
568
+ from ..widgets import EncryptedFieldWidget, EncryptedPasswordWidget
567
569
 
568
570
  # Determine placeholder based on field name
569
571
  placeholder = "Enter value"
@@ -574,16 +576,25 @@ class PydanticAdminMixin:
574
576
  elif 'passphrase' in db_field.name.lower():
575
577
  placeholder = "Enter Passphrase (if required)"
576
578
 
577
- # Return CharField with PasswordInput widget for security
578
- # render_value=True shows masked value (••••••) after save
579
+ # Widget attributes
580
+ widget_attrs = {
581
+ 'placeholder': placeholder,
582
+ }
583
+
584
+ # Decide widget based on config
585
+ show_plain_text = getattr(self.config, 'show_encrypted_fields_as_plain_text', False)
586
+
587
+ if show_plain_text:
588
+ # Show as plain text with copy button
589
+ widget = EncryptedFieldWidget(attrs=widget_attrs, show_copy_button=True)
590
+ else:
591
+ # Show as password (masked) with copy button
592
+ # render_value=True shows masked value (••••••) after save
593
+ widget = EncryptedPasswordWidget(attrs=widget_attrs, render_value=True, show_copy_button=True)
594
+
595
+ # Return CharField with appropriate widget
579
596
  return forms.CharField(
580
- widget=PasswordInput(
581
- attrs={
582
- 'placeholder': placeholder,
583
- 'class': 'appearance-none block w-full bg-gray-200 text-gray-700 border border-gray-200 rounded py-3 px-4 leading-tight focus:outline-none focus:bg-white focus:border-gray-500'
584
- },
585
- render_value=True # Show masked value after save
586
- ),
597
+ widget=widget,
587
598
  required=not db_field.blank and not db_field.null,
588
599
  help_text=db_field.help_text or "This field is encrypted at rest",
589
600
  label=db_field.verbose_name if hasattr(db_field, 'verbose_name') else db_field.name.replace('_', ' ').title()
@@ -148,6 +148,13 @@ class AdminConfig(BaseModel):
148
148
  description="Markdown documentation configuration"
149
149
  )
150
150
 
151
+ # Encrypted fields options
152
+ show_encrypted_fields_as_plain_text: bool = Field(
153
+ False,
154
+ description="Show encrypted fields (django-crypto-fields) as plain text instead of password masked. "
155
+ "WARNING: This exposes sensitive data in the admin interface. Use only in trusted environments."
156
+ )
157
+
151
158
  def get_display_field_config(self, field_name: str) -> Optional[FieldConfig]:
152
159
  """Get FieldConfig for a specific field."""
153
160
  for field_config in self.display_fields:
@@ -0,0 +1,80 @@
1
+ {% comment %}
2
+ Template for encrypted text field with copy button.
3
+ Based on unfold/widgets/text.html with added copy functionality.
4
+ {% endcomment %}
5
+
6
+ <div class="max-w-2xl relative w-full">
7
+ {% if widget.prefix %}
8
+ <span class="absolute left-3 top-0 bottom-0 flex items-center justify-center">
9
+ {{ widget.prefix }}
10
+ </span>
11
+ {% endif %}
12
+
13
+ {% if widget.prefix_icon %}
14
+ <span class="material-symbols-outlined absolute left-3 top-0 bottom-0 flex items-center justify-center text-base-400 dark:text-base-500">
15
+ {{ widget.prefix_icon }}
16
+ </span>
17
+ {% endif %}
18
+
19
+ {% include "django/forms/widgets/input.html" %}
20
+
21
+ {% if widget.suffix %}
22
+ <span class="absolute right-3 top-0 bottom-0 flex items-center justify-center">
23
+ {{ widget.suffix }}
24
+ </span>
25
+ {% endif %}
26
+
27
+ {% if widget.suffix_icon %}
28
+ <span class="material-symbols-outlined absolute right-3 top-0 bottom-0 flex items-center justify-center text-base-400 dark:text-base-500">
29
+ {{ widget.suffix_icon }}
30
+ </span>
31
+ {% endif %}
32
+
33
+ {% if widget.show_copy_button %}
34
+ <button
35
+ type="button"
36
+ onclick="copyEncryptedField(this)"
37
+ class="material-symbols-outlined absolute right-3 top-0 bottom-0 flex items-center justify-center text-base-400 hover:text-primary-600 dark:text-base-500 dark:hover:text-primary-500 cursor-pointer transition-colors"
38
+ title="Copy to clipboard"
39
+ >
40
+ content_copy
41
+ </button>
42
+ {% endif %}
43
+ </div>
44
+
45
+ <script>
46
+ function copyEncryptedField(button) {
47
+ // Get the input field (previous sibling of button's parent)
48
+ const container = button.parentElement;
49
+ const input = container.querySelector('input');
50
+
51
+ if (!input || !input.value) {
52
+ return;
53
+ }
54
+
55
+ // Copy to clipboard
56
+ navigator.clipboard.writeText(input.value).then(() => {
57
+ // Visual feedback - change icon temporarily
58
+ const originalIcon = button.textContent;
59
+ button.textContent = 'check';
60
+ button.classList.add('text-green-600', 'dark:text-green-500');
61
+
62
+ // Reset after 2 seconds
63
+ setTimeout(() => {
64
+ button.textContent = originalIcon;
65
+ button.classList.remove('text-green-600', 'dark:text-green-500');
66
+ }, 2000);
67
+ }).catch(err => {
68
+ console.error('Failed to copy:', err);
69
+ // Show error feedback
70
+ const originalIcon = button.textContent;
71
+ button.textContent = 'error';
72
+ button.classList.add('text-red-600', 'dark:text-red-500');
73
+
74
+ setTimeout(() => {
75
+ button.textContent = originalIcon;
76
+ button.classList.remove('text-red-600', 'dark:text-red-500');
77
+ }, 2000);
78
+ });
79
+ }
80
+ </script>
@@ -0,0 +1,62 @@
1
+ {% comment %}
2
+ Template for encrypted password field with copy button.
3
+ Based on Django's default password widget with Unfold styling and copy button.
4
+ {% endcomment %}
5
+
6
+ <div class="max-w-2xl relative w-full">
7
+ {% include "django/forms/widgets/input.html" %}
8
+
9
+ {% if widget.show_copy_button %}
10
+ <button
11
+ type="button"
12
+ onclick="copyEncryptedPassword(this)"
13
+ class="material-symbols-outlined absolute right-3 top-0 bottom-0 flex items-center justify-center text-base-400 hover:text-primary-600 dark:text-base-500 dark:hover:text-primary-500 cursor-pointer transition-colors"
14
+ title="Copy to clipboard"
15
+ >
16
+ content_copy
17
+ </button>
18
+ {% endif %}
19
+ </div>
20
+
21
+ <script>
22
+ function copyEncryptedPassword(button) {
23
+ // Get the input field (previous sibling of button's parent)
24
+ const container = button.parentElement;
25
+ const input = container.querySelector('input');
26
+
27
+ if (!input || !input.value) {
28
+ return;
29
+ }
30
+
31
+ // For password fields, we need to temporarily change type to text to get the real value
32
+ const originalType = input.type;
33
+ input.type = 'text';
34
+ const value = input.value;
35
+ input.type = originalType;
36
+
37
+ // Copy to clipboard
38
+ navigator.clipboard.writeText(value).then(() => {
39
+ // Visual feedback - change icon temporarily
40
+ const originalIcon = button.textContent;
41
+ button.textContent = 'check';
42
+ button.classList.add('text-green-600', 'dark:text-green-500');
43
+
44
+ // Reset after 2 seconds
45
+ setTimeout(() => {
46
+ button.textContent = originalIcon;
47
+ button.classList.remove('text-green-600', 'dark:text-green-500');
48
+ }, 2000);
49
+ }).catch(err => {
50
+ console.error('Failed to copy:', err);
51
+ // Show error feedback
52
+ const originalIcon = button.textContent;
53
+ button.textContent = 'error';
54
+ button.classList.add('text-red-600', 'dark:text-red-500');
55
+
56
+ setTimeout(() => {
57
+ button.textContent = originalIcon;
58
+ button.classList.remove('text-red-600', 'dark:text-red-500');
59
+ }, 2000);
60
+ });
61
+ }
62
+ </script>
@@ -2,8 +2,11 @@
2
2
  Widget system for Django Admin.
3
3
  """
4
4
 
5
+ from .encrypted_field_widget import EncryptedFieldWidget, EncryptedPasswordWidget
5
6
  from .registry import WidgetRegistry
6
7
 
7
8
  __all__ = [
8
9
  "WidgetRegistry",
10
+ "EncryptedFieldWidget",
11
+ "EncryptedPasswordWidget",
9
12
  ]
@@ -0,0 +1,66 @@
1
+ """
2
+ Custom widget for encrypted fields with copy button.
3
+ """
4
+ from typing import Any, Optional
5
+
6
+ from unfold.widgets import UnfoldAdminPasswordInput, UnfoldAdminTextInputWidget
7
+
8
+
9
+ class EncryptedFieldWidget(UnfoldAdminTextInputWidget):
10
+ """
11
+ Text input widget for encrypted fields with copy-to-clipboard button.
12
+
13
+ Extends UnfoldAdminTextInputWidget to add a copy button on the right side.
14
+ """
15
+
16
+ template_name = "django_admin/widgets/encrypted_field.html"
17
+
18
+ def __init__(self, attrs: Optional[dict[str, Any]] = None, show_copy_button: bool = True) -> None:
19
+ """
20
+ Initialize the widget.
21
+
22
+ Args:
23
+ attrs: Widget attributes
24
+ show_copy_button: Whether to show the copy button (default: True)
25
+ """
26
+ self.show_copy_button = show_copy_button
27
+ super().__init__(attrs=attrs)
28
+
29
+ def get_context(self, name, value, attrs):
30
+ """Add copy button context."""
31
+ context = super().get_context(name, value, attrs)
32
+ context['widget']['show_copy_button'] = self.show_copy_button
33
+ return context
34
+
35
+
36
+ class EncryptedPasswordWidget(UnfoldAdminPasswordInput):
37
+ """
38
+ Password input widget for encrypted fields with copy button.
39
+
40
+ Extends UnfoldAdminPasswordInput to add a copy button on the right side.
41
+ """
42
+
43
+ template_name = "django_admin/widgets/encrypted_password.html"
44
+
45
+ def __init__(
46
+ self,
47
+ attrs: Optional[dict[str, Any]] = None,
48
+ render_value: bool = False,
49
+ show_copy_button: bool = True
50
+ ) -> None:
51
+ """
52
+ Initialize the widget.
53
+
54
+ Args:
55
+ attrs: Widget attributes
56
+ render_value: Whether to render the value (default: False)
57
+ show_copy_button: Whether to show the copy button (default: True)
58
+ """
59
+ self.show_copy_button = show_copy_button
60
+ super().__init__(attrs=attrs, render_value=render_value)
61
+
62
+ def get_context(self, name, value, attrs):
63
+ """Add copy button context."""
64
+ context = super().get_context(name, value, attrs)
65
+ context['widget']['show_copy_button'] = self.show_copy_button
66
+ return context
@@ -0,0 +1,140 @@
1
+ # Django-Q2 Module
2
+
3
+ Автоматическая синхронизация расписаний Django-Q2 из конфига в базу данных.
4
+
5
+ ## Зачем это нужно?
6
+
7
+ Django-Q2 хранит расписания в базе данных, но **не создаёт их автоматически** из конфига.
8
+ Этот модуль решает эту проблему - синхронизирует расписания после каждой миграции.
9
+
10
+ ## Использование
11
+
12
+ ### 1. Включи Django-Q2 в конфиге
13
+
14
+ ```python
15
+ # config.py
16
+ from django_cfg.models.django import DjangoQ2Config
17
+
18
+ django_q2 = DjangoQ2Config(
19
+ enabled=True, # ← Автоматически добавит django_q и django_cfg.modules.django_q2 в INSTALLED_APPS
20
+ schedules=[...]
21
+ )
22
+ ```
23
+
24
+ **Модуль подключается автоматически!** Не нужно вручную добавлять в INSTALLED_APPS.
25
+
26
+ ### 2. Определи расписания в конфиге
27
+
28
+ ```python
29
+ from django_cfg.models.django import DjangoQ2Config, DjangoQ2ScheduleConfig
30
+
31
+ django_q2 = DjangoQ2Config(
32
+ enabled=True,
33
+ schedules=[
34
+ DjangoQ2ScheduleConfig(
35
+ name="Sync balances hourly",
36
+ schedule_type="hourly",
37
+ command="sync_account_balances",
38
+ command_args=["--verbose"],
39
+ ),
40
+ DjangoQ2ScheduleConfig(
41
+ name="Cleanup daily",
42
+ schedule_type="cron",
43
+ cron="0 2 * * *", # 2 AM каждый день
44
+ command="cleanup_old_data",
45
+ command_kwargs={"days": 30},
46
+ ),
47
+ ],
48
+ )
49
+ ```
50
+
51
+ ### 3. Запусти миграции
52
+
53
+ ```bash
54
+ python manage.py migrate
55
+ ```
56
+
57
+ **Вывод:**
58
+ ```
59
+ Running migrations:
60
+ ...
61
+ Syncing 2 Django-Q2 schedule(s)...
62
+ ✓ Created schedule: Sync balances hourly
63
+ ✓ Created schedule: Cleanup daily
64
+ ✅ Django-Q2 schedules synced: 2 created, 0 updated
65
+ ```
66
+
67
+ ### 4. Запусти qcluster
68
+
69
+ ```bash
70
+ python manage.py qcluster
71
+ ```
72
+
73
+ Готово! Расписания автоматически синхронизированы и работают.
74
+
75
+ ## Как это работает?
76
+
77
+ 1. **Модуль подключается** к сигналу `post_migrate`
78
+ 2. **После миграций** автоматически:
79
+ - Читает расписания из конфига
80
+ - Создаёт/обновляет их в базе данных (Schedule model)
81
+ 3. **Django-Q2 читает** расписания из базы и выполняет задачи
82
+
83
+ ## Ручная синхронизация (опционально)
84
+
85
+ Если нужно синхронизировать без миграций:
86
+
87
+ ```bash
88
+ python manage.py sync_django_q_schedules
89
+
90
+ # Или с --dry-run для проверки:
91
+ python manage.py sync_django_q_schedules --dry-run
92
+ ```
93
+
94
+ ## Безопасность
95
+
96
+ - ✅ **Идемпотентность**: можно запускать много раз, не создаст дубликаты
97
+ - ✅ **Без race conditions**: синхронизация происходит один раз за цикл миграций
98
+ - ✅ **Graceful degradation**: если Django-Q2 не установлен, модуль просто молча пропустит синхронизацию
99
+ - ✅ **Logging**: все операции логируются для отладки
100
+
101
+ ## Преимущества перед ручной синхронизацией
102
+
103
+ | Аспект | Ручная синхронизация | Модуль |
104
+ |--------|---------------------|--------|
105
+ | Автоматизация | Нужно помнить запускать | Автоматически |
106
+ | Деплой | Легко забыть | Всегда синхронизировано |
107
+ | CI/CD | Нужно добавлять в скрипты | Работает из коробки |
108
+ | Ошибки | Легко пропустить | Логи миграций |
109
+
110
+ ## Troubleshooting
111
+
112
+ ### Расписания не создаются
113
+
114
+ Проверь:
115
+ 1. Модуль добавлен в INSTALLED_APPS
116
+ 2. `django_q2.enabled = True` в конфиге
117
+ 3. В конфиге есть расписания
118
+ 4. Миграции запущены: `python manage.py migrate`
119
+
120
+ ### Расписания не обновляются
121
+
122
+ Запусти миграции повторно или используй ручную синхронизацию:
123
+ ```bash
124
+ python manage.py migrate --run-syncdb
125
+ # или
126
+ python manage.py sync_django_q_schedules
127
+ ```
128
+
129
+ ### Логи синхронизации
130
+
131
+ Включи DEBUG логи:
132
+ ```python
133
+ LOGGING = {
134
+ 'loggers': {
135
+ 'django_cfg.modules.django_q2': {
136
+ 'level': 'DEBUG',
137
+ },
138
+ },
139
+ }
140
+ ```
@@ -0,0 +1,8 @@
1
+ """
2
+ Django-Q2 module for django-cfg.
3
+
4
+ Provides automatic schedule synchronization from config to database.
5
+ """
6
+ from .apps import DjangoQ2ModuleConfig
7
+
8
+ __all__ = ['DjangoQ2ModuleConfig']
@@ -0,0 +1,107 @@
1
+ """
2
+ AppConfig for Django-Q2 module with automatic schedule synchronization.
3
+ """
4
+ import logging
5
+ from django.apps import AppConfig
6
+ from django.db.models.signals import post_migrate
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ def sync_schedules_after_migrate(sender, **kwargs):
12
+ """
13
+ Automatically sync Django-Q2 schedules after migrations.
14
+
15
+ This ensures schedules are always up-to-date after deployment.
16
+ Runs only once per migration cycle, safe from race conditions.
17
+ """
18
+ # Only run for the django_cfg_django_q2 app itself
19
+ if sender.name != 'django_cfg.modules.django_q2':
20
+ return
21
+
22
+ # Import here to avoid circular imports and ensure Django is ready
23
+ try:
24
+ from django_q.models import Schedule
25
+ from django_cfg.core.config import get_current_config
26
+ except ImportError as e:
27
+ logger.warning(f"Could not import Django-Q2 dependencies: {e}")
28
+ return
29
+
30
+ config = get_current_config()
31
+
32
+ if not config or not hasattr(config, 'django_q2') or not config.django_q2 or not config.django_q2.enabled:
33
+ logger.debug("Django-Q2 not enabled, skipping schedule sync")
34
+ return
35
+
36
+ enabled_schedules = config.django_q2.get_enabled_schedules()
37
+
38
+ if not enabled_schedules:
39
+ logger.debug("No Django-Q2 schedules found in config")
40
+ return
41
+
42
+ logger.info(f"Syncing {len(enabled_schedules)} Django-Q2 schedule(s)...")
43
+
44
+ created = 0
45
+ updated = 0
46
+
47
+ for schedule_config in enabled_schedules:
48
+ schedule_dict = schedule_config.to_django_q_format()
49
+ name = schedule_dict['name']
50
+
51
+ try:
52
+ schedule, created_flag = Schedule.objects.update_or_create(
53
+ name=name,
54
+ defaults=schedule_dict
55
+ )
56
+
57
+ if created_flag:
58
+ created += 1
59
+ logger.info(f" ✓ Created schedule: {name}")
60
+ else:
61
+ updated += 1
62
+ logger.debug(f" ✓ Updated schedule: {name}")
63
+
64
+ except Exception as e:
65
+ logger.error(f" ✗ Failed to sync schedule '{name}': {e}")
66
+
67
+ logger.info(f"✅ Django-Q2 schedules synced: {created} created, {updated} updated")
68
+
69
+
70
+ class DjangoQ2ModuleConfig(AppConfig):
71
+ """
72
+ AppConfig for Django-Q2 module.
73
+
74
+ Automatically syncs schedules from config to database after migrations.
75
+ This eliminates the need for manual `sync_django_q_schedules` command.
76
+
77
+ Features:
78
+ - Automatic schedule sync after migrations
79
+ - Safe from race conditions (runs only once)
80
+ - Logs all sync operations
81
+ - Gracefully handles missing dependencies
82
+
83
+ Usage:
84
+ Add to INSTALLED_APPS:
85
+ INSTALLED_APPS = [
86
+ ...
87
+ 'django_cfg.modules.django_q2', # Auto-syncs schedules
88
+ ]
89
+ """
90
+
91
+ default_auto_field = 'django.db.models.BigAutoField'
92
+ name = 'django_cfg.modules.django_q2'
93
+ verbose_name = 'Django-CFG Django-Q2 Module'
94
+
95
+ def ready(self):
96
+ """
97
+ Connect post_migrate signal to automatically sync schedules.
98
+
99
+ This runs after all migrations are complete, ensuring:
100
+ 1. Database tables exist
101
+ 2. Config is loaded
102
+ 3. Schedules are synced only once per migration cycle
103
+ """
104
+ # Connect the signal
105
+ post_migrate.connect(sync_schedules_after_migrate, sender=self)
106
+
107
+ logger.debug(f"{self.verbose_name} initialized - auto-sync enabled")
File without changes
@@ -0,0 +1,74 @@
1
+ """
2
+ Management command to sync Django-Q2 schedules from config.
3
+
4
+ Usage:
5
+ python manage.py sync_django_q_schedules # Create/update schedules
6
+ python manage.py sync_django_q_schedules --dry-run # Show what would be created
7
+ """
8
+ from django.core.management.base import BaseCommand
9
+ from django_q.models import Schedule
10
+ from django_cfg.core.config import get_current_config
11
+
12
+
13
+ class Command(BaseCommand):
14
+ help = 'Sync Django-Q2 schedules from config to database'
15
+
16
+ def add_arguments(self, parser):
17
+ parser.add_argument(
18
+ '--dry-run',
19
+ action='store_true',
20
+ help='Show what would be created without actually creating',
21
+ )
22
+
23
+ def handle(self, *args, **options):
24
+ config = get_current_config()
25
+
26
+ if not config:
27
+ self.stdout.write(self.style.ERROR('❌ No config found'))
28
+ return
29
+
30
+ # Check if Django-Q2 is enabled
31
+ if not hasattr(config, 'django_q2') or not config.django_q2 or not config.django_q2.enabled:
32
+ self.stdout.write(self.style.WARNING('⚠️ Django-Q2 is not enabled in config'))
33
+ return
34
+
35
+ enabled_schedules = config.django_q2.get_enabled_schedules()
36
+
37
+ if not enabled_schedules:
38
+ self.stdout.write(self.style.WARNING('⚠️ No schedules found in config'))
39
+ return
40
+
41
+ self.stdout.write(f'📋 Found {len(enabled_schedules)} schedule(s) in config\n')
42
+
43
+ created = 0
44
+ updated = 0
45
+
46
+ for schedule_config in enabled_schedules:
47
+ schedule_dict = schedule_config.to_django_q_format()
48
+ name = schedule_dict['name']
49
+
50
+ if options['dry_run']:
51
+ self.stdout.write(f' [DRY RUN] Would create/update: {name}')
52
+ continue
53
+
54
+ # Update or create schedule
55
+ schedule, created_flag = Schedule.objects.update_or_create(
56
+ name=name,
57
+ defaults=schedule_dict
58
+ )
59
+
60
+ if created_flag:
61
+ created += 1
62
+ self.stdout.write(self.style.SUCCESS(f' ✓ Created: {name}'))
63
+ else:
64
+ updated += 1
65
+ self.stdout.write(self.style.SUCCESS(f' ✓ Updated: {name}'))
66
+
67
+ if not options['dry_run']:
68
+ self.stdout.write(self.style.SUCCESS(
69
+ f'\n✅ Summary: {created} created, {updated} updated'
70
+ ))
71
+ else:
72
+ self.stdout.write(self.style.WARNING(
73
+ f'\n[DRY RUN] Would create {len(enabled_schedules)} schedule(s)'
74
+ ))
django_cfg/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "django-cfg"
7
- version = "1.4.117"
7
+ version = "1.4.119"
8
8
  description = "Modern Django framework with type-safe Pydantic v2 configuration, Next.js admin integration, real-time WebSockets, and 8 enterprise apps. Replace settings.py with validated models, 90% less code. Production-ready with AI agents, auto-generated TypeScript clients, and zero-config features."
9
9
  readme = "README.md"
10
10
  keywords = [ "django", "configuration", "pydantic", "settings", "type-safety", "pydantic-settings", "django-environ", "startup-validation", "ide-autocomplete", "nextjs-admin", "react-admin", "websocket", "centrifugo", "real-time", "typescript-generation", "ai-agents", "enterprise-django", "django-settings", "type-safe-config", "modern-django",]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-cfg
3
- Version: 1.4.117
3
+ Version: 1.4.119
4
4
  Summary: Modern Django framework with type-safe Pydantic v2 configuration, Next.js admin integration, real-time WebSockets, and 8 enterprise apps. Replace settings.py with validated models, 90% less code. Production-ready with AI agents, auto-generated TypeScript clients, and zero-config features.
5
5
  Project-URL: Homepage, https://djangocfg.com
6
6
  Project-URL: Documentation, https://djangocfg.com
@@ -1,5 +1,5 @@
1
1
  django_cfg/README.md,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- django_cfg/__init__.py,sha256=a-zgNY8EXo7-I6ZParGfaJYes0Qu_GNrYhqILSkLhAU,1621
2
+ django_cfg/__init__.py,sha256=H3Lu0mvpULyWtMRq04fjixa209x5dcO9YdaHFessd4Q,1621
3
3
  django_cfg/apps.py,sha256=72m3uuvyqGiLx6gOfE-BD3P61jddCCERuBOYpxTX518,1605
4
4
  django_cfg/config.py,sha256=xdwFE8bocOEnMjqDOwr1_M02oUgdaMG_2M6F_IuB9GQ,1351
5
5
  django_cfg/apps/__init__.py,sha256=JtDmEYt1OcleWM2ZaeX0LKDnRQzPOavfaXBWG4ECB5Q,26
@@ -536,7 +536,7 @@ django_cfg/cli/commands/create_project.py,sha256=cENwRIG7zX_95lY7Kt_xKGULZWqfwni
536
536
  django_cfg/cli/commands/info.py,sha256=JNLt9gUTZopR-D7FUDIeEyOQR9-2LTpHntHi_h-7Mho,4794
537
537
  django_cfg/core/__init__.py,sha256=bZ3hgN9SX6l3DXEN-cMyOGwwXMu4UFkWyR0ds_yEnVs,1784
538
538
  django_cfg/core/config.py,sha256=0xwXf0cPiRGM8paUPXjECHdYqwuRLy4WkunavWl9Igw,679
539
- django_cfg/core/constants.py,sha256=ool-h1MXDtgRKj8-e8aYcM6XY_Uzy1dgx-xLy0gsk10,2686
539
+ django_cfg/core/constants.py,sha256=D3-9XIMpwoYDgye7P_ixurS1IzV3R4fUvmSclXo8uf4,2725
540
540
  django_cfg/core/exceptions.py,sha256=-jHOxN_X_k9BGI50zBLloVawCMe-uIruiyLHto12B-k,2215
541
541
  django_cfg/core/validation.py,sha256=QmwGlNDa0MFG2OUmgykIUAaEotT55YNh-wAWSVxOAos,6165
542
542
  django_cfg/core/backends/__init__.py,sha256=5Qfza7oKQ8_UIHOs4ibhYvwtgAjngUqLrlwjKl_q124,38
@@ -544,7 +544,7 @@ django_cfg/core/backends/smtp.py,sha256=kWkNMG7UwLsHcFYSKRgrk1HbP9mU1fxzWYnalHXq
544
544
  django_cfg/core/base/__init__.py,sha256=Z3bZvxejxk4vvWqmqTBLUi9XJpo6A_5Bq4R0J8q81Y4,116
545
545
  django_cfg/core/base/config_model.py,sha256=fnmvjOs2fHTb8ifjFv0EsbVPFScG87ExikIqVAqpDZc,21810
546
546
  django_cfg/core/builders/__init__.py,sha256=jkInI7_jbxcjitapohw6QmbJPpacnnID6V1JovqtOFM,282
547
- django_cfg/core/builders/apps_builder.py,sha256=96o35vB7-eIzAv32bZ505H6lGmcxjqZmhUDsxbOQ4Ug,6675
547
+ django_cfg/core/builders/apps_builder.py,sha256=1C7M3Uq4aScuz4syuNGHyl3USus1h_FgULPfzKAerak,6810
548
548
  django_cfg/core/builders/middleware_builder.py,sha256=OwqQRoJKYWlXsQNPFBfUvVMYdppUHCPw-UDczV_esYg,3101
549
549
  django_cfg/core/builders/security_builder.py,sha256=W8Lk9eTMi3PuNK5qH-BIHegeE0cbvmuHKTumLcwOAh8,23961
550
550
  django_cfg/core/environment/__init__.py,sha256=sMOIe9z1i51j8B1VGjpLHJMaeDsBfsgn1TmL03FIeNo,141
@@ -644,7 +644,7 @@ django_cfg/models/django/__init__.py,sha256=J4VRl77vgXzfKcdUcX-sPxpBSOJ4qA-8RstP
644
644
  django_cfg/models/django/axes.py,sha256=-4nk2gSfpj7lNY5vnm_2jHVLz8VAKoEd9yF2TuCR8O8,5624
645
645
  django_cfg/models/django/constance.py,sha256=6x57bi40mDX0fKcKeQKgV2BO3WIVYPQllAchWsj4KvM,8847
646
646
  django_cfg/models/django/crypto_fields.py,sha256=OguITidM4Mp564p_gbsokeNCZxjL9hrK1Vw0McuA3yo,4700
647
- django_cfg/models/django/django_q2.py,sha256=le-Zjg2JjiFyfOJoHeODrjUylUBBm7eoneaGmVOmq1I,15067
647
+ django_cfg/models/django/django_q2.py,sha256=G8m2hg0-IE-GLsGabaHetGVdbb_cNas8RCbZBBr-xz4,15564
648
648
  django_cfg/models/django/environment.py,sha256=lBCHBs1lphv9tlu1BCTfLZeH_kUame0p66A_BIjBY7M,9440
649
649
  django_cfg/models/django/openapi.py,sha256=avE3iapaCj8eyOqVUum_v2EExR3V-hwHrexqtXMHtTQ,3739
650
650
  django_cfg/models/django/revolution_legacy.py,sha256=Z4SPUS7QSv62EuPAeFFoXGEgqLmdXnVEr7Ofk1IDtVc,8918
@@ -675,12 +675,13 @@ django_cfg/models/tasks/utils.py,sha256=9TEbdxgd0N_O2T_7GGwkyPeDg4H7tSKLHoHG_n8e
675
675
  django_cfg/modules/__init__.py,sha256=Ip9WMpzImEwIAywpFwU056_v0O9oIGG7nCT1YSArxkw,316
676
676
  django_cfg/modules/base.py,sha256=Grmgxc5dvnAEM1sudWEWO4kv8L0Ks-y32nxTk2vwdjQ,6272
677
677
  django_cfg/modules/django_admin/__init__.py,sha256=ncTMbxR7ccVd5xS-sZE_yJODjVMFq8HDFaDnP6lyvIg,3328
678
+ django_cfg/modules/django_admin/apps.py,sha256=xJjgIRgS_I1ehyq7ZbFpZY_L4umRa5ZPQP3mf0alUaU,526
678
679
  django_cfg/modules/django_admin/base/__init__.py,sha256=tzre09bnD_SlS-pA30WzYZRxyvch7eLq3q0wLEcZOmc,118
679
- django_cfg/modules/django_admin/base/pydantic_admin.py,sha256=6tB_8hZLT3ZzwUu6rlvzhkQ_BkDq8niaTE3TIa0IcgM,24977
680
+ django_cfg/modules/django_admin/base/pydantic_admin.py,sha256=xewt_ld40NntdjtZ-k-VwinhfDkIRk71nwu5DVDQzeQ,25364
680
681
  django_cfg/modules/django_admin/base/unfold_admin.py,sha256=iqpRWSkzW5HktXDuuG7G3J6RoIfW48dWPMJTa7Yk08g,729
681
682
  django_cfg/modules/django_admin/config/__init__.py,sha256=HDuJxhAS27EiL1pXLNvMePREB2gfoS2i_0Ur1ITCjAM,926
682
683
  django_cfg/modules/django_admin/config/action_config.py,sha256=JjS01JxLT-FzUVq7RlKaB7L38wmVL8uibXO_iXZcljo,1668
683
- django_cfg/modules/django_admin/config/admin_config.py,sha256=TnPI-kLZj6uoIH7opCD70HQ6I_yJ7jPKnphS0h9P42s,5242
684
+ django_cfg/modules/django_admin/config/admin_config.py,sha256=L48YHLt-JyNyL-XOltqIZVh1ALoG4wXQsvi_WIbhuFw,5577
684
685
  django_cfg/modules/django_admin/config/background_task_config.py,sha256=7-8B1rhpeafanVxtjFQUx0mVjcA5xmxZIxqKzaBwMX0,1760
685
686
  django_cfg/modules/django_admin/config/documentation_config.py,sha256=lI_gMSWCtyKmdyttLNdgbg_zbGgrwXA-QoLxVOXJj9A,14189
686
687
  django_cfg/modules/django_admin/config/field_config.py,sha256=LeHoumm-lmiYIu0dBmoyCdTkddQdcv20_KrD3WRhkQ0,11743
@@ -699,6 +700,8 @@ django_cfg/modules/django_admin/templates/django_admin/change_form_docs.html,sha
699
700
  django_cfg/modules/django_admin/templates/django_admin/change_list_docs.html,sha256=MfH6mf9v6E3y97TjvjvZ2A7RMhOkKd6CtxEw3tPV5WE,802
700
701
  django_cfg/modules/django_admin/templates/django_admin/documentation_block.html,sha256=fDUZYDWuB68NOm4fjTr0OLfqG2m-UpE-MZL1upkUNHc,14174
701
702
  django_cfg/modules/django_admin/templates/django_admin/markdown_docs_block.html,sha256=nO9oEyDYozYF-euaLzE0Cn_Yo_LGUlta-BsRgShDG10,1992
703
+ django_cfg/modules/django_admin/templates/django_admin/widgets/encrypted_field.html,sha256=-Kd4twY0sav2M5x_0oDj8ElA7CBccToVHFiwGa2WF-w,2774
704
+ django_cfg/modules/django_admin/templates/django_admin/widgets/encrypted_password.html,sha256=BdiM0CqRyBH1aHGf6iLSfSKOoADY5qaqetbC9AktzaI,2154
702
705
  django_cfg/modules/django_admin/utils/CODE_BLOCK_DOCS.md,sha256=rp5qMG-Ci30fIs6EyZctjEbhQwxfNq9e36B4CTZOnR0,9456
703
706
  django_cfg/modules/django_admin/utils/__init__.py,sha256=5V1Og6fpIpRqDIoqF7jYnBmPl9TjV5kByghzT6iOpPs,745
704
707
  django_cfg/modules/django_admin/utils/badges.py,sha256=eZ1UThdwvv2cHAIDc4vTrD5xAet7fmeb9h9yj4ZXJ-c,6328
@@ -707,7 +710,8 @@ django_cfg/modules/django_admin/utils/displays.py,sha256=f-FT1mD-X56X6xLDJ9FuCi4
707
710
  django_cfg/modules/django_admin/utils/html_builder.py,sha256=E0ysbbImpZqSr7YFw3FYryG0jjJfffIOqFzefk-HMl8,14500
708
711
  django_cfg/modules/django_admin/utils/markdown_renderer.py,sha256=eMYde8cDo9y5CO5qNjy7juYe_OPpSpFB1-d84ylpbk4,14324
709
712
  django_cfg/modules/django_admin/utils/mermaid_plugin.py,sha256=37x_0FQE5IyR6TiaDZDrQQHeQmAsPtlZBnq5N8eAKqA,8889
710
- django_cfg/modules/django_admin/widgets/__init__.py,sha256=mmPw5FMYR21GDGFMr-MOCcdM4G2_ZR60ClInHjdnTBE,115
713
+ django_cfg/modules/django_admin/widgets/__init__.py,sha256=SVVytNURG_TlWePwhws9FYeDe_xp4Ob_CYVA88Y08es,256
714
+ django_cfg/modules/django_admin/widgets/encrypted_field_widget.py,sha256=h4bSAI2sLjKuBFDufWhtQQQ-eMIAqUzsUezi1p0ERuM,2146
711
715
  django_cfg/modules/django_admin/widgets/registry.py,sha256=q0Yyaze5ZTYLJslPyX9e4Few_FGLnGBQwtNln9Okyt4,5610
712
716
  django_cfg/modules/django_client/__init__.py,sha256=iHaGKbsyR2wMmVCWNsETC7cwB60fZudvnFMiK1bchW8,529
713
717
  django_cfg/modules/django_client/apps.py,sha256=xfkw2aXy08xXlkFhbCiTFveMmRwlDk3SQOAWdqXraFM,1952
@@ -947,6 +951,12 @@ django_cfg/modules/django_ngrok/service.py,sha256=Xkh9Gl6Rth32UcT0UYjD0ckROHFw6F
947
951
  django_cfg/modules/django_ngrok/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
948
952
  django_cfg/modules/django_ngrok/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
949
953
  django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py,sha256=urwO1nPjcBMPn7k8WXBzfWQpjzCsIKaiET2Gf5afYwk,6259
954
+ django_cfg/modules/django_q2/README.md,sha256=pyY0EqefXTJ47k_liMv22d0zx-9dHVPGFUSZS3aLQjE,4763
955
+ django_cfg/modules/django_q2/__init__.py,sha256=6Eu26O-4ddwE8qO27pTT6x13-iPc9eA2Pzq8_cRHZMc,186
956
+ django_cfg/modules/django_q2/apps.py,sha256=UoSbow-Wlkwriem5mEud1vrUD7_TnqANMzoajGLBVQs,3396
957
+ django_cfg/modules/django_q2/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
958
+ django_cfg/modules/django_q2/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
959
+ django_cfg/modules/django_q2/management/commands/sync_django_q_schedules.py,sha256=ik_4kmKp9RUTOaxewWK6KgwK_1_1wJcVZQ2LAdSG0Lo,2671
950
960
  django_cfg/modules/django_tailwind/README.md,sha256=tPQd4ir79MLFSVUKBGfPAF22LZWE0BcmzcNdQhJ4k7I,10804
951
961
  django_cfg/modules/django_tailwind/__init__.py,sha256=K0GbFxjemNixRUgexurSTDfN4kB--TmuFCk9n5DbLYc,186
952
962
  django_cfg/modules/django_tailwind/apps.py,sha256=CKNd0xDoLaQpe7SEfay0ZtWkjLUVRgofkB8FobKLXV8,313
@@ -1060,9 +1070,9 @@ django_cfg/utils/version_check.py,sha256=WO51J2m2e-wVqWCRwbultEwu3q1lQasV67Mw2aa
1060
1070
  django_cfg/CHANGELOG.md,sha256=jtT3EprqEJkqSUh7IraP73vQ8PmKUMdRtznQsEnqDZk,2052
1061
1071
  django_cfg/CONTRIBUTING.md,sha256=DU2kyQ6PU0Z24ob7O_OqKWEYHcZmJDgzw-lQCmu6uBg,3041
1062
1072
  django_cfg/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
1063
- django_cfg/pyproject.toml,sha256=MwMh3DgYM1djCIx9K6E9rCngbKw5nVTDLWGmYMH6Ueo,8665
1064
- django_cfg-1.4.117.dist-info/METADATA,sha256=Ll9z9f22ytdtCrn-anLFdvYpvVLBer0V7pqxnuefcFI,23876
1065
- django_cfg-1.4.117.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
1066
- django_cfg-1.4.117.dist-info/entry_points.txt,sha256=Ucmde4Z2wEzgb4AggxxZ0zaYDb9HpyE5blM3uJ0_VNg,56
1067
- django_cfg-1.4.117.dist-info/licenses/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
1068
- django_cfg-1.4.117.dist-info/RECORD,,
1073
+ django_cfg/pyproject.toml,sha256=bhPs3HhPDoIc56W0TLwHYE5Ekh1zHRrPX_5o8IoncAU,8665
1074
+ django_cfg-1.4.119.dist-info/METADATA,sha256=qCVV9288Nf1L4WQUHvVb74viHrUEZ-Q-pmRLDuTl9kM,23876
1075
+ django_cfg-1.4.119.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
1076
+ django_cfg-1.4.119.dist-info/entry_points.txt,sha256=Ucmde4Z2wEzgb4AggxxZ0zaYDb9HpyE5blM3uJ0_VNg,56
1077
+ django_cfg-1.4.119.dist-info/licenses/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
1078
+ django_cfg-1.4.119.dist-info/RECORD,,