import json import logging from django.views.generic import TemplateView from django.http import JsonResponse from django.views.decorators.http import require_POST, require_GET logger = logging.getLogger(__name__) from user_roles.mixins import OwnerRequiredMixin from .models import RecommerceIntegration, WooCommerceIntegration, GLMIntegration, OpenRouterIntegration from integrations.recommerce.tasks import sync_products_batch_task # Реестр доступных интеграций # Ключ = идентификатор для URL/JS, значение = (модель, название для UI) INTEGRATION_REGISTRY = { 'recommerce': (RecommerceIntegration, 'Recommerce', 'Маркетплейс'), 'woocommerce': (WooCommerceIntegration, 'WooCommerce', 'Маркетплейс'), 'glm': (GLMIntegration, 'GLM от Z.AI', 'Сервис ИИ'), 'openrouter': (OpenRouterIntegration, 'OpenRouter.ai', 'Сервис ИИ'), # Добавлять новые интеграции здесь: # '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// Создаёт запись если не существует (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 test_integration_connection(request, integration_id: str): """ Тестировать соединение с интеграцией. POST /integrations/test// """ import logging logger = logging.getLogger(__name__) logger.info(f"=== Начало тестирования интеграции: {integration_id} ===") if not hasattr(request, 'user') or not request.user.is_authenticated: logger.error("Пользователь не авторизован") return JsonResponse({'error': 'Unauthorized'}, status=401) model = get_integration_model(integration_id) if not model: logger.error(f"Неизвестная интеграция: {integration_id}") return JsonResponse({'error': f'Unknown integration: {integration_id}'}, status=404) instance = model.objects.first() if not instance: logger.error(f"Интеграция {integration_id} не настроена (нет записи в БД)") return JsonResponse({'error': 'Интеграция не настроена'}, status=400) logger.info(f"Найдена интеграция: {instance.name}, is_active={instance.is_active}, is_configured={instance.is_configured}") if not instance.is_configured: logger.error(f"Интеграция {integration_id} не сконфигурирована") return JsonResponse({'error': 'Заполните настройки интеграции'}, status=400) # Получить сервис для интеграции try: logger.info(f"Попытка получить сервис для {integration_id}...") service = get_integration_service(integration_id, instance) if not service: logger.error(f"Сервис для {integration_id} не реализован") return JsonResponse({'error': 'Сервис не реализован'}, status=501) logger.info(f"Сервис успешно создан: {service.__class__.__name__}") except Exception as e: logger.error(f"Ошибка при создании сервиса для {integration_id}: {str(e)}", exc_info=True) return JsonResponse({'error': f'Ошибка при создании сервиса: {str(e)}'}, status=500) # Выполнить тест try: logger.info(f"Выполнение теста соединения...") success, message = service.test_connection() logger.info(f"Результат теста: success={success}, message={message}") except Exception as e: logger.error(f"Ошибка при выполнении теста соединения: {str(e)}", exc_info=True) return JsonResponse({'error': f'Ошибка при тестировании: {str(e)}'}, status=500) return JsonResponse({ 'success': success, 'message': message, }) def get_integration_service(integration_id: str, instance): """Получить сервис для интеграции""" if integration_id == 'recommerce': from .services.marketplaces.recommerce import RecommerceService return RecommerceService(instance) elif integration_id == 'woocommerce': from .services.marketplaces.woocommerce import WooCommerceService return WooCommerceService(instance) elif integration_id == 'glm': from .services.ai_services.glm_service import GLMIntegrationService return GLMIntegrationService(instance) elif integration_id == 'openrouter': from .services.ai_services.openrouter_service import OpenRouterIntegrationService return OpenRouterIntegrationService(instance) return None @require_GET def get_openrouter_models(request): """ GET /settings/integrations/openrouter/models/ Возвращает список моделей OpenRouter (бесплатные сверху) """ import requests try: response = requests.get('https://openrouter.ai/api/v1/models', timeout=10) response.raise_for_status() data = response.json() models = data.get('data', []) # Разделить на бесплатные и платные free_models = [] paid_models = [] for model in models: model_id = model.get('id', '') model_name = model.get('name', model_id) if ':free' in model_id: free_models.append({'id': model_id, 'name': f"{model_name} (Бесплатная)"}) else: paid_models.append({'id': model_id, 'name': model_name}) # Бесплатные сверху all_models = free_models + paid_models return JsonResponse({'models': all_models}) except Exception as e: logger.error(f"Error fetching OpenRouter models: {e}") return JsonResponse({'error': str(e)}, status=500) class RecommerceBatchSyncView(TemplateView): """ API View для запуска массовой синхронизации с Recommerce. POST /integrations/recommerce/sync/ """ def dispatch(self, request, *args, **kwargs): # Временное логирование для отладки from user_roles.services import RoleService import logging logger = logging.getLogger(__name__) logger.info(f"User: {request.user}, Authenticated: {request.user.is_authenticated}") user_role = RoleService.get_user_role(request.user) logger.info(f"User role: {user_role}") return super().dispatch(request, *args, **kwargs) def post(self, request, *args, **kwargs): try: data = json.loads(request.body) product_ids = data.get('product_ids', []) options = data.get('options', {}) if not product_ids: return JsonResponse({'error': 'No products selected'}, status=400) # Запуск Celery задачи с передачей schema_name from django_tenants.utils import get_tenant_model Tenant = get_tenant_model() schema_name = request.tenant.schema_name task = sync_products_batch_task.delay(product_ids, options, schema_name) return JsonResponse({ 'success': True, 'task_id': task.id, 'message': f'Запущена синхронизация {len(product_ids)} товаров' }) except Exception as e: return JsonResponse({'error': str(e)}, status=500) @require_POST def save_integration_settings(request, integration_id: str): """ API endpoint для сохранения настроек интеграции. POST /integrations/settings// 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 = [] editable_fields = get_editable_fields(model) # Для GLM отображаем только имя и API ключ if model.__name__ == 'GLMIntegration': for field_name in editable_fields: if field_name in ['name', 'api_key']: 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 '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) elif field_name == 'model_name': 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': 'select', # dropdown 'choices': getattr(field, 'choices', []) } fields.append(field_info) # Для OpenRouter отображаем только имя, API ключ, модель и температуру elif model.__name__ == 'OpenRouterIntegration': for field_name in editable_fields: if field_name in ['name', 'api_key']: 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': 'password' if field_name == 'api_key' else 'text', } fields.append(field_info) elif field_name == 'temperature': 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': 'select', 'choices': getattr(field, 'choices', []) } fields.append(field_info) elif field_name == 'model_name': 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': 'select', 'dynamic_choices': True, 'choices_url': '/settings/integrations/openrouter/models/' } fields.append(field_info) # Для WooCommerce показываем только базовые поля для подключения elif model.__name__ == 'WooCommerceIntegration': basic_fields = ['store_url', 'consumer_key', 'consumer_secret'] for field_name in editable_fields: if field_name in basic_fields: 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', } # Определить тип поля 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 'key' in field_name.lower(): field_info['type'] = 'password' fields.append(field_info) else: # Для других интеграций - все редактируемые поля for field_name in editable_fields: 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 '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