- Удалена лишняя модель IntegrationConfig из system_settings - Singleton-паттерн: одна запись на интеграцию с is_active тумблером - Добавлено шифрование токенов (EncryptedCharField с Fernet AES-128) - UI: тумблеры слева, форма настроек справа - API endpoints: toggle, settings, form_data - Модель Recommerce: store_url + api_token (x-auth-token) - Модель WooCommerce: store_url + consumer_key/secret Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
225 lines
8.4 KiB
Python
225 lines
8.4 KiB
Python
import json
|
||
from django.views.generic import TemplateView
|
||
from django.http import JsonResponse
|
||
from django.views.decorators.http import require_POST
|
||
|
||
from user_roles.mixins import OwnerRequiredMixin
|
||
from .models import RecommerceIntegration, WooCommerceIntegration
|
||
|
||
|
||
# Реестр доступных интеграций
|
||
# Ключ = идентификатор для URL/JS, значение = (модель, название для UI)
|
||
INTEGRATION_REGISTRY = {
|
||
'recommerce': (RecommerceIntegration, 'Recommerce', 'Маркетплейс'),
|
||
'woocommerce': (WooCommerceIntegration, 'WooCommerce', 'Маркетплейс'),
|
||
# Добавлять новые интеграции здесь:
|
||
# 'shopify': (ShopifyIntegration, 'Shopify', 'Маркетплейс'),
|
||
}
|
||
|
||
|
||
def get_integration_model(integration_id: str):
|
||
"""Получить модель интеграции по идентификатору"""
|
||
if integration_id not in INTEGRATION_REGISTRY:
|
||
return None
|
||
return INTEGRATION_REGISTRY[integration_id][0]
|
||
|
||
|
||
def get_all_integrations_status():
|
||
"""
|
||
Получить статус всех интеграций.
|
||
Возвращает dict с информацией для UI.
|
||
"""
|
||
result = {}
|
||
for key, (model, label, category) in INTEGRATION_REGISTRY.items():
|
||
instance = model.objects.first()
|
||
result[key] = {
|
||
'id': key,
|
||
'label': label,
|
||
'category': category,
|
||
'is_active': instance.is_active if instance else False,
|
||
'is_configured': instance.is_configured if instance else False,
|
||
'exists': instance is not None,
|
||
}
|
||
return result
|
||
|
||
|
||
class IntegrationsListView(OwnerRequiredMixin, TemplateView):
|
||
"""Страница настроек интеграций (доступна только владельцу)"""
|
||
template_name = "system_settings/integrations.html"
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
integrations = get_all_integrations_status()
|
||
context['integrations'] = integrations
|
||
# JSON для JavaScript
|
||
context['integrations_json'] = json.dumps(integrations, ensure_ascii=False)
|
||
return context
|
||
|
||
|
||
@require_POST
|
||
def toggle_integration(request, integration_id: str):
|
||
"""
|
||
API endpoint для включения/выключения интеграции.
|
||
POST /integrations/toggle/<integration_id>/
|
||
|
||
Создаёт запись если не существует (singleton).
|
||
Переключает is_active.
|
||
"""
|
||
# Проверка прав (только владелец)
|
||
if not hasattr(request, 'user') or not request.user.is_authenticated:
|
||
return JsonResponse({'error': 'Unauthorized'}, status=401)
|
||
|
||
# Получить модель
|
||
model = get_integration_model(integration_id)
|
||
if not model:
|
||
return JsonResponse({'error': f'Unknown integration: {integration_id}'}, status=404)
|
||
|
||
# Получить или создать singleton
|
||
instance, created = model.objects.get_or_create(
|
||
defaults={
|
||
'name': INTEGRATION_REGISTRY[integration_id][1],
|
||
'is_active': False,
|
||
}
|
||
)
|
||
|
||
# Переключить состояние
|
||
new_state = not instance.is_active
|
||
|
||
# Проверка: нельзя включить ненастроенную интеграцию
|
||
if new_state and not instance.is_configured:
|
||
return JsonResponse({
|
||
'error': 'Сначала настройте интеграцию (введите credentials)',
|
||
'is_active': instance.is_active,
|
||
'is_configured': False,
|
||
}, status=400)
|
||
|
||
instance.is_active = new_state
|
||
instance.save(update_fields=['is_active', 'updated_at'])
|
||
|
||
return JsonResponse({
|
||
'success': True,
|
||
'is_active': instance.is_active,
|
||
'is_configured': instance.is_configured,
|
||
})
|
||
|
||
|
||
@require_POST
|
||
def save_integration_settings(request, integration_id: str):
|
||
"""
|
||
API endpoint для сохранения настроек интеграции.
|
||
POST /integrations/settings/<integration_id>/
|
||
|
||
Body: JSON с полями для обновления
|
||
"""
|
||
if not hasattr(request, 'user') or not request.user.is_authenticated:
|
||
return JsonResponse({'error': 'Unauthorized'}, status=401)
|
||
|
||
model = get_integration_model(integration_id)
|
||
if not model:
|
||
return JsonResponse({'error': f'Unknown integration: {integration_id}'}, status=404)
|
||
|
||
# Получить или создать
|
||
instance, created = model.objects.get_or_create(
|
||
defaults={'name': INTEGRATION_REGISTRY[integration_id][1]}
|
||
)
|
||
|
||
# Парсинг данных
|
||
try:
|
||
data = json.loads(request.body)
|
||
except json.JSONDecodeError:
|
||
return JsonResponse({'error': 'Invalid JSON'}, status=400)
|
||
|
||
# Обновить только разрешённые поля (не is_active - для этого toggle)
|
||
allowed_fields = get_editable_fields(model)
|
||
updated_fields = []
|
||
|
||
for field in allowed_fields:
|
||
if field in data:
|
||
setattr(instance, field, data[field])
|
||
updated_fields.append(field)
|
||
|
||
if updated_fields:
|
||
updated_fields.append('updated_at')
|
||
instance.save(update_fields=updated_fields)
|
||
|
||
return JsonResponse({
|
||
'success': True,
|
||
'is_configured': instance.is_configured,
|
||
'updated_fields': updated_fields,
|
||
})
|
||
|
||
|
||
def get_editable_fields(model):
|
||
"""Получить список редактируемых полей модели (исключая служебные)"""
|
||
excluded = {'id', 'integration_type', 'is_active', 'created_at', 'updated_at', 'extra_config'}
|
||
fields = []
|
||
for field in model._meta.get_fields():
|
||
if hasattr(field, 'name') and field.name not in excluded:
|
||
if not field.is_relation: # Исключаем FK/M2M
|
||
fields.append(field.name)
|
||
return fields
|
||
|
||
|
||
def get_integration_form_data(request, integration_id: str):
|
||
"""
|
||
GET endpoint для получения текущих настроек интеграции.
|
||
Используется для заполнения формы справа.
|
||
"""
|
||
model = get_integration_model(integration_id)
|
||
if not model:
|
||
return JsonResponse({'error': f'Unknown integration: {integration_id}'}, status=404)
|
||
|
||
instance = model.objects.first()
|
||
if not instance:
|
||
# Вернуть пустую структуру полей
|
||
return JsonResponse({
|
||
'exists': False,
|
||
'fields': get_form_fields_meta(model),
|
||
})
|
||
|
||
# Собрать данные полей (без чувствительных данных полностью)
|
||
data = {}
|
||
for field_name in get_editable_fields(model):
|
||
field = model._meta.get_field(field_name)
|
||
value = getattr(instance, field_name, None)
|
||
|
||
# Маскировать секреты
|
||
if 'token' in field_name.lower() or 'secret' in field_name.lower() or 'key' in field_name.lower():
|
||
data[field_name] = '••••••••' if value else ''
|
||
else:
|
||
data[field_name] = value
|
||
|
||
return JsonResponse({
|
||
'exists': True,
|
||
'is_active': instance.is_active,
|
||
'is_configured': instance.is_configured,
|
||
'data': data,
|
||
'fields': get_form_fields_meta(model),
|
||
})
|
||
|
||
|
||
def get_form_fields_meta(model):
|
||
"""Получить метаданные полей для построения формы на фронте"""
|
||
fields = []
|
||
for field_name in get_editable_fields(model):
|
||
field = model._meta.get_field(field_name)
|
||
field_info = {
|
||
'name': field_name,
|
||
'label': getattr(field, 'verbose_name', field_name),
|
||
'help_text': getattr(field, 'help_text', ''),
|
||
'required': not getattr(field, 'blank', True),
|
||
'type': 'text', # default
|
||
}
|
||
|
||
# Определить тип поля
|
||
if isinstance(field, model._meta.get_field(field_name).__class__):
|
||
if 'BooleanField' in field.__class__.__name__:
|
||
field_info['type'] = 'checkbox'
|
||
elif 'URLField' in field.__class__.__name__:
|
||
field_info['type'] = 'url'
|
||
elif 'secret' in field_name.lower() or 'token' in field_name.lower() or 'key' in field_name.lower():
|
||
field_info['type'] = 'password'
|
||
|
||
fields.append(field_info)
|
||
return fields
|