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 +1 -1
- django_cfg/core/builders/apps_builder.py +2 -0
- django_cfg/core/constants.py +1 -0
- django_cfg/models/django/django_q2.py +13 -0
- django_cfg/modules/django_admin/apps.py +19 -0
- django_cfg/modules/django_admin/base/pydantic_admin.py +21 -10
- django_cfg/modules/django_admin/config/admin_config.py +7 -0
- django_cfg/modules/django_admin/templates/django_admin/widgets/encrypted_field.html +80 -0
- django_cfg/modules/django_admin/templates/django_admin/widgets/encrypted_password.html +62 -0
- django_cfg/modules/django_admin/widgets/__init__.py +3 -0
- django_cfg/modules/django_admin/widgets/encrypted_field_widget.py +66 -0
- django_cfg/modules/django_q2/README.md +140 -0
- django_cfg/modules/django_q2/__init__.py +8 -0
- django_cfg/modules/django_q2/apps.py +107 -0
- django_cfg/modules/django_q2/management/__init__.py +0 -0
- django_cfg/modules/django_q2/management/commands/__init__.py +0 -0
- django_cfg/modules/django_q2/management/commands/sync_django_q_schedules.py +74 -0
- django_cfg/pyproject.toml +1 -1
- {django_cfg-1.4.117.dist-info → django_cfg-1.4.119.dist-info}/METADATA +1 -1
- {django_cfg-1.4.117.dist-info → django_cfg-1.4.119.dist-info}/RECORD +23 -13
- {django_cfg-1.4.117.dist-info → django_cfg-1.4.119.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.117.dist-info → django_cfg-1.4.119.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.117.dist-info → django_cfg-1.4.119.dist-info}/licenses/LICENSE +0 -0
django_cfg/__init__.py
CHANGED
|
@@ -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:
|
django_cfg/core/constants.py
CHANGED
|
@@ -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
|
|
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
|
-
#
|
|
578
|
-
|
|
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=
|
|
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,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
|
|
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.
|
|
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.
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
1064
|
-
django_cfg-1.4.
|
|
1065
|
-
django_cfg-1.4.
|
|
1066
|
-
django_cfg-1.4.
|
|
1067
|
-
django_cfg-1.4.
|
|
1068
|
-
django_cfg-1.4.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|