diff --git a/restoran_max_bot/content_bot/landing_utils.py b/restoran_max_bot/content_bot/landing_utils.py new file mode 100644 index 0000000..1a5e9cb --- /dev/null +++ b/restoran_max_bot/content_bot/landing_utils.py @@ -0,0 +1,123 @@ +import json +from typing import Callable, Optional, Tuple + +from max_bot.models import Client, Integration + + +def normalize_telegram_url(url: Optional[str]) -> Optional[str]: + """Приводит ссылку на Telegram-бота к виду https://t.me/bot_name.""" + if not url or not str(url).strip(): + return None + + value = str(url).strip() + if value.startswith('@'): + value = value[1:] + + if value.startswith('https://'): + return value + if value.startswith('http://'): + return f'https://{value[7:]}' + if value.startswith('t.me/'): + return f'https://{value}' + + return f'https://t.me/{value.lstrip("/")}' + + +def normalize_max_bot_url(url: Optional[str]) -> Optional[str]: + """Приводит ссылку на MAX-бота к виду https://max.ru/bot_id.""" + if not url or not str(url).strip(): + return None + + value = str(url).strip() + if value.startswith('https://max.ru/'): + return value + if value.startswith('http://max.ru/'): + return f'https://max.ru/{value[len("http://max.ru/"):]}' + if value.startswith('max.ru/'): + return f'https://{value}' + + return f'https://max.ru/{value.lstrip("/")}' + + +def _load_settings(integration: Integration) -> dict: + if not integration.setting: + return {} + try: + settings = json.loads(integration.setting) + except json.JSONDecodeError: + return {} + return settings if isinstance(settings, dict) else {} + + +def _save_settings(integration: Integration, settings: dict) -> None: + integration.setting = json.dumps(settings, ensure_ascii=False) + integration.save(update_fields=['setting', 'updated_at']) + + +def get_integration_url( + client: Client, + partner_slug: str, + setting_key: str = 'url', + normalizer: Optional[Callable[[Optional[str]], Optional[str]]] = None, + persist_fix: bool = True, +) -> Optional[str]: + """ + Возвращает URL из lk_integration по slug партнёра. + При необходимости нормализует и сохраняет исправленное значение в БД. + """ + integration = Integration.objects.filter( + client=client, + partner__slug=partner_slug, + status=True, + ).select_related('partner').first() + if not integration: + return None + + settings = _load_settings(integration) + raw_url = settings.get(setting_key) + if not raw_url: + return None + + normalized = normalizer(raw_url) if normalizer else raw_url + if persist_fix and normalized and normalized != raw_url: + settings[setting_key] = normalized + _save_settings(integration, settings) + + return normalized + + +def cleanup_max_bot_integration(client: Client) -> None: + """Удаляет устаревший ключ url_telegram из настроек max_bot.""" + integration = Integration.objects.filter( + client=client, + partner__slug='max_bot', + status=True, + ).first() + if not integration: + return + + settings = _load_settings(integration) + if 'url_telegram' not in settings: + return + + del settings['url_telegram'] + _save_settings(integration, settings) + + +def get_landing_bot_urls(client: Client) -> Tuple[Optional[str], Optional[str]]: + """Возвращает (max_bot_url, telegram_url) для лендинга.""" + cleanup_max_bot_integration(client) + + max_bot_url = get_integration_url( + client=client, + partner_slug='max_bot', + setting_key='url', + normalizer=normalize_max_bot_url, + ) + telegram_url = get_integration_url( + client=client, + partner_slug='telegram_bot', + setting_key='url', + normalizer=normalize_telegram_url, + ) + return max_bot_url, telegram_url diff --git a/restoran_max_bot/content_bot/tests.py b/restoran_max_bot/content_bot/tests.py index 7ce503c..cec7f02 100644 --- a/restoran_max_bot/content_bot/tests.py +++ b/restoran_max_bot/content_bot/tests.py @@ -1,3 +1,99 @@ -from django.test import TestCase +from django.test import TestCase, Client as DjangoClient +from unittest.mock import patch, MagicMock -# Create your tests here. +from content_bot.landing_utils import ( + normalize_telegram_url, + normalize_max_bot_url, + get_landing_bot_urls, +) + + +class LandingUrlNormalizationTests(TestCase): + def test_normalize_telegram_full_url(self): + self.assertEqual( + normalize_telegram_url('https://t.me/cheguevaraclub_bot'), + 'https://t.me/cheguevaraclub_bot', + ) + + def test_normalize_telegram_without_scheme(self): + self.assertEqual( + normalize_telegram_url('t.me/celentanouu_bot'), + 'https://t.me/celentanouu_bot', + ) + + def test_normalize_telegram_bot_name(self): + self.assertEqual( + normalize_telegram_url('barLeto_bot'), + 'https://t.me/barLeto_bot', + ) + + def test_normalize_max_bot_full_url(self): + self.assertEqual( + normalize_max_bot_url('https://max.ru/id0323343968_bot'), + 'https://max.ru/id0323343968_bot', + ) + + def test_normalize_max_bot_short_id(self): + self.assertEqual( + normalize_max_bot_url('d0323350531_bot'), + 'https://max.ru/d0323350531_bot', + ) + + +class LandingBotUrlsTests(TestCase): + @patch('content_bot.landing_utils.Integration') + def test_get_landing_bot_urls_uses_separate_integrations(self, mock_integration): + client = MagicMock() + max_integration = MagicMock() + max_integration.setting = '{"url":"d0323350531_bot"}' + telegram_integration = MagicMock() + telegram_integration.setting = '{"url":"https://t.me/cheguevaraclub_bot"}' + + def filter_side_effect(**kwargs): + qs = MagicMock() + slug = kwargs.get('partner__slug') + if slug == 'max_bot': + qs.first.return_value = max_integration + elif slug == 'telegram_bot': + qs.first.return_value = telegram_integration + else: + qs.first.return_value = None + return qs + + mock_integration.objects.filter.side_effect = filter_side_effect + + with patch('content_bot.landing_utils._save_settings') as mock_save: + max_url, telegram_url = get_landing_bot_urls(client) + + self.assertEqual(max_url, 'https://max.ru/d0323350531_bot') + self.assertEqual(telegram_url, 'https://t.me/cheguevaraclub_bot') + self.assertEqual(mock_save.call_count, 1) + + +class LandingPageViewTests(TestCase): + @patch('content_bot.views.get_landing_bot_urls') + @patch('content_bot.views.Qr') + @patch('content_bot.views.get_object_or_404') + def test_landing_page_passes_urls_to_template(self, mock_get_object, mock_qr, mock_get_urls): + client_obj = MagicMock() + client_obj.id = 1089 + client_obj.brand = 'Test Brand' + client_obj.logo = '' + mock_get_object.return_value = client_obj + + qr_obj = MagicMock() + qr_obj.brand = 'QR Brand' + qr_obj.logo = 'logo.png' + qr_obj.description = 'Description' + mock_qr.objects.filter.return_value.first.return_value = qr_obj + + mock_get_urls.return_value = ( + 'https://max.ru/d0323350531_bot', + 'https://t.me/cheguevaraclub_bot', + ) + + response = DjangoClient().get('/content/landing/1089/') + + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'https://max.ru/d0323350531_bot') + self.assertContains(response, 'https://t.me/cheguevaraclub_bot') diff --git a/restoran_max_bot/content_bot/views.py b/restoran_max_bot/content_bot/views.py index 9b15245..30d7987 100644 --- a/restoran_max_bot/content_bot/views.py +++ b/restoran_max_bot/content_bot/views.py @@ -2,8 +2,8 @@ from django.http import JsonResponse from django.core.handlers.wsgi import WSGIRequest from django.views.decorators.csrf import csrf_exempt from django.shortcuts import render, get_object_or_404 -from max_bot.models import Client, Qr, Integration, Partner -import json +from max_bot.models import Client, Qr +from content_bot.landing_utils import get_landing_bot_urls # Create your views here. @@ -22,75 +22,20 @@ def landing_page(request, client_id): """ Минилендинг для мобильного телефона. Отображает логотип, название, описание заведения и две кнопки: - - Max Bot - - Telegram Bot + - Max Bot (из lk_integration, partner slug=max_bot) + - Telegram Bot (из lk_integration, partner slug=telegram_bot) """ - # Получаем клиента client = get_object_or_404(Client, id=client_id, status=True) - - # Получаем данные из lk_qr qr = Qr.objects.filter(client=client).first() - - # Данные для шаблона + max_bot_url, telegram_url = get_landing_bot_urls(client) + context = { 'client_id': client.id, 'brand': qr.brand if qr and qr.brand else client.brand or 'Ресторан', 'logo': qr.logo if qr and qr.logo else client.logo or '', 'description': qr.description if qr and qr.description else '', + 'max_bot_url': max_bot_url, + 'telegram_url': telegram_url, } - - # Ищем интеграции - max_bot_url = None - telegram_url = None - - integrations = Integration.objects.filter(client=client, status=True) - - for integration in integrations: - if not integration.setting: - continue - - try: - settings = json.loads(integration.setting) - - # Max Bot - if integration.partner.slug == 'max_bot': - if 'url' in settings: - max_bot_url = settings['url'] - - # Telegram - if integration.partner.slug == 'telegram': - if 'url_telegram' in settings: - telegram_url = settings['url_telegram'] - - except json.JSONDecodeError: - continue - - # Если не нашли по партнеру, пробуем по ключам - if not max_bot_url: - for integration in integrations: - if not integration.setting: - continue - try: - settings = json.loads(integration.setting) - if 'url' in settings and 'max' in settings['url'].lower(): - max_bot_url = settings['url'] - break - except json.JSONDecodeError: - continue - - if not telegram_url: - for integration in integrations: - if not integration.setting: - continue - try: - settings = json.loads(integration.setting) - if 'url_telegram' in settings: - telegram_url = settings['url_telegram'] - break - except json.JSONDecodeError: - continue - - context['max_bot_url'] = max_bot_url - context['telegram_url'] = telegram_url - + return render(request, 'max_bot/landing.html', context)