Compare commits

..

3 Commits

25 changed files with 2262 additions and 324 deletions

View File

@@ -1,217 +0,0 @@
# Celery Setup and Testing Instructions
## Prerequisites
Make sure you have Redis running (it's already configured in your docker-compose.yaml):
```bash
# Start Redis and other services
cd /home/vesemir/DataStorage
docker-compose up -d redis
```
## Installing Dependencies
```bash
pip install -r requirements.txt
```
## Database Setup
Since we're using django-celery-results and celery-beat, you need to run migrations:
```bash
python manage.py migrate
```
This will create the necessary tables for storing Celery results and managing periodic tasks.
## Running Celery
### 1. Start Celery Worker
```bash
# From the dbapp directory
cd /home/vesemir/DataStorage/dbapp
# Run with development settings
python -m celery -A dbapp worker --loglevel=info
# Or with environment variable
DJANGO_SETTINGS_MODULE=dbapp.settings.development celery -A dbapp worker --loglevel=info
```
### 2. Start Celery Beat (for periodic tasks)
```bash
# From the dbapp directory
cd /home/vesemir/DataStorage/dbapp
# Run with development settings
python -m celery -A dbapp beat --loglevel=info
# Or with environment variable
DJANGO_SETTINGS_MODULE=dbapp.settings.development celery -A dbapp beat --loglevel=info
```
### 3. Start Flower (Optional - for monitoring)
```bash
# Install flower if not already installed
pip install flower
# Run flower to monitor tasks
celery -A dbapp flower
```
## Testing Celery
### Method 1: Using Django Shell
```bash
cd /home/vesemir/DataStorage/dbapp
python manage.py shell
```
```python
# In the Django shell
from mainapp.tasks import test_celery_connection, add_numbers
from lyngsatapp.tasks import fill_lyngsat_data_task
# Test simple connection
result = test_celery_connection.delay("Test message!")
print(result.id) # Task ID
print(result.get(timeout=10)) # Wait for result and print
# Test addition
result = add_numbers.delay(5, 7)
print(result.get(timeout=10))
# Check task state
print(result.state) # Should be 'SUCCESS'
print(result.ready()) # Should be True
print(result.successful()) # Should be True
```
### Method 2: Using Django Management Command
Create a management command to test:
```bash
mkdir -p dbapp/management/commands
```
Create `/home/vesemir/DataStorage/dbapp/dbapp/management/commands/test_celery.py`:
```python
from django.core.management.base import BaseCommand
from mainapp.tasks import test_celery_connection, add_numbers
class Command(BaseCommand):
help = 'Test Celery functionality'
def handle(self, *args, **options):
self.stdout.write('Testing Celery connection...')
# Test simple task
result = test_celery_connection.delay("Hello from test command!")
self.stdout.write(f'Task ID: {result.id}')
# Wait for result
task_result = result.get(timeout=10)
self.stdout.write(self.style.SUCCESS(f'Task result: {task_result}'))
# Test math task
math_result = add_numbers.delay(10, 20)
sum_result = math_result.get(timeout=10)
self.stdout.write(self.style.SUCCESS(f'10 + 20 = {sum_result}'))
self.stdout.write(self.style.SUCCESS('All tests passed!'))
```
Then run:
```bash
python manage.py test_celery
```
## Troubleshooting
### Common Issues
1. **Connection Error with Redis**: Make sure Redis is running
```bash
docker-compose up -d redis
```
2. **Module Not Found Errors**: Ensure all dependencies are installed
```bash
pip install -r requirements.txt
```
3. **Settings Module Error**: Make sure DJANGO_SETTINGS_MODULE is set properly
```bash
export DJANGO_SETTINGS_MODULE=dbapp.settings.development
```
4. **Database Tables Missing**: Run migrations
```bash
python manage.py migrate
```
### Debugging
Check if Celery can connect to Redis:
```bash
# Test Redis connection
redis-cli ping
```
Check Celery configuration:
```python
# In Django shell
from django.conf import settings
print(settings.CELERY_BROKER_URL)
print(settings.CELERY_RESULT_BACKEND)
```
### Environment Variables
Make sure your `.env` file contains:
```
CELERY_BROKER_URL=redis://localhost:6379/0
DJANGO_SETTINGS_MODULE=dbapp.settings.development
```
## Running in Production
For production, ensure you have:
1. A production Redis instance
2. Proper security settings
3. Daemonized Celery workers
Example systemd service file for Celery worker (save as `/etc/systemd/system/celery.service`):
```
[Unit]
Description=Celery Service
After=network.target
[Service]
Type=forking
User=www-data
Group=www-data
EnvironmentFile=/path/to/your/.env
WorkingDirectory=/home/vesemir/DataStorage/dbapp
ExecStart=/path/to/your/venv/bin/celery -A dbapp worker --loglevel=info --pidfile=/var/run/celery/worker.pid --logfile=/var/log/celery/worker.log
ExecReload=/bin/kill -HUP $MAINPID
KillSignal=SIGTERM
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
```

View File

@@ -225,13 +225,38 @@ LEAFLET_CONFIG = {
} }
# ============================================================================
# CACHE CONFIGURATION
# ============================================================================
# Redis Cache Configuration
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": os.getenv("REDIS_URL", "redis://localhost:6379/1"),
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"CONNECTION_POOL_CLASS_KWARGS": {
"max_connections": 50,
"retry_on_timeout": True,
},
"SOCKET_CONNECT_TIMEOUT": 5,
"SOCKET_TIMEOUT": 5,
},
"KEY_PREFIX": "dbapp",
"TIMEOUT": 300, # Default timeout 5 minutes
}
}
# ============================================================================ # ============================================================================
# CELERY CONFIGURATION # CELERY CONFIGURATION
# ============================================================================ # ============================================================================
# Celery Configuration Options # Celery Configuration Options
CELERY_BROKER_URL = os.getenv("CELERY_BROKER_URL", "redis://localhost:6379/0") CELERY_BROKER_URL = os.getenv("CELERY_BROKER_URL", "redis://localhost:6379/0")
CELERY_RESULT_BACKEND = os.getenv("CELERY_BROKER_URL", "redis://localhost:6379/0") # Use Redis for results CELERY_RESULT_BACKEND = os.getenv(
"CELERY_BROKER_URL", "redis://localhost:6379/0"
) # Use Redis for results
CELERY_CACHE_BACKEND = "default" CELERY_CACHE_BACKEND = "default"
# Celery Task Configuration # Celery Task Configuration
@@ -257,3 +282,7 @@ CELERY_ACCEPT_CONTENT = ["json"]
CELERY_TASK_SERIALIZER = "json" CELERY_TASK_SERIALIZER = "json"
CELERY_RESULT_SERIALIZER = "json" CELERY_RESULT_SERIALIZER = "json"
CELERY_TIMEZONE = TIME_ZONE CELERY_TIMEZONE = TIME_ZONE
# Celery Exception Handling
CELERY_TASK_IGNORE_RESULT = False
CELERY_TASK_STORE_ERRORS_EVEN_IF_IGNORED = True

View File

@@ -0,0 +1,564 @@
"""
Асинхронный парсер данных LyngSat с поддержкой кеширования в Redis.
"""
import requests
from bs4 import BeautifulSoup
from datetime import datetime
import re
import logging
from typing import Optional
from django.core.cache import cache
logger = logging.getLogger(__name__)
def parse_satellite_names(satellite_string: str) -> list[str]:
"""Извлекает все возможные имена спутников из строки."""
slash_parts = [part.strip() for part in satellite_string.split('/')]
all_names = []
for part in slash_parts:
main_match = re.match(r'^([^(]+)', part)
if main_match:
main_name = main_match.group(1).strip()
if main_name:
all_names.append(main_name)
bracket_match = re.search(r'\(([^)]+)\)', part)
if bracket_match:
bracket_name = bracket_match.group(1).strip()
if bracket_name:
all_names.append(bracket_name)
seen = set()
result = []
for name in all_names:
if name not in seen:
seen.add(name)
result.append(name.strip().lower())
return result
class AsyncLyngSatParser:
"""
Асинхронный парсер данных для LyngSat с поддержкой кеширования.
Кеширование:
- Страницы регионов кешируются на 7 дней
- Данные спутников кешируются на 1 день
"""
# Время жизни кеша
REGION_CACHE_TTL = 60 * 60 * 24 * 7 # 7 дней
SATELLITE_CACHE_TTL = 60 * 60 * 24 # 1 день
# Префиксы для ключей кеша
REGION_CACHE_PREFIX = "lyngsat_region"
SATELLITE_CACHE_PREFIX = "lyngsat_satellite"
SATELLITE_LIST_CACHE_PREFIX = "lyngsat_sat_list"
def __init__(
self,
flaresolver_url: str = "http://localhost:8191/v1",
regions: list[str] | None = None,
target_sats: list[str] | None = None,
use_cache: bool = True,
):
"""
Инициализация парсера.
Args:
flaresolver_url: URL FlareSolverr для обхода защиты
regions: Список регионов для парсинга
target_sats: Список целевых спутников (в нижнем регистре)
use_cache: Использовать ли кеширование
"""
self.flaresolver_url = flaresolver_url
self.use_cache = use_cache
self.target_sats = (
list(map(lambda sat: sat.strip().lower(), target_sats)) if target_sats else None
)
self.regions = regions if regions else ["europe", "asia", "america", "atlantic"]
self.BASE_URL = "https://www.lyngsat.com"
def _get_cache_key(self, prefix: str, identifier: str) -> str:
"""Генерирует ключ для кеша."""
return f"{prefix}:{identifier}"
def _get_from_cache(self, key: str) -> Optional[any]:
"""Получает данные из кеша."""
if not self.use_cache:
return None
try:
data = cache.get(key)
if data:
logger.debug(f"Данные получены из кеша: {key}")
return data
except Exception as e:
logger.warning(f"Ошибка при получении из кеша {key}: {e}")
return None
def _set_to_cache(self, key: str, value: any, ttl: int) -> None:
"""Сохраняет данные в кеш."""
if not self.use_cache:
return
try:
cache.set(key, value, timeout=ttl)
logger.debug(f"Данные сохранены в кеш: {key} (TTL: {ttl}s)")
except Exception as e:
logger.warning(f"Ошибка при сохранении в кеш {key}: {e}")
@classmethod
def clear_cache(cls, cache_type: str = "all") -> dict:
"""
Очищает кеш парсера.
Args:
cache_type: Тип кеша для очистки ("regions", "satellites", "all")
Returns:
dict: Статистика очистки
"""
stats = {"cleared": 0, "errors": []}
try:
from django.core.cache import cache as django_cache
if cache_type in ("regions", "all"):
# Очищаем кеш регионов
regions = ["europe", "asia", "america", "atlantic"]
for region in regions:
key = f"{cls.REGION_CACHE_PREFIX}:{region}"
try:
result = django_cache.delete(key)
if result:
stats["cleared"] += 1
logger.info(f"Очищен кеш региона: {region}")
else:
logger.debug(f"Кеш региона {region} не найден или уже удален")
except Exception as e:
error_msg = f"Ошибка при очистке кеша региона {region}: {e}"
logger.error(error_msg)
stats["errors"].append(error_msg)
if cache_type in ("satellites", "all"):
# Для очистки кеша спутников используем keys()
if hasattr(django_cache, 'keys'):
try:
# Очищаем списки спутников
list_keys = django_cache.keys(f"{cls.SATELLITE_LIST_CACHE_PREFIX}:*")
if list_keys:
if hasattr(django_cache, 'delete_many'):
django_cache.delete_many(list_keys)
else:
for key in list_keys:
django_cache.delete(key)
stats["cleared"] += len(list_keys)
logger.info(f"Очищено {len(list_keys)} списков спутников")
# Очищаем данные спутников
sat_keys = django_cache.keys(f"{cls.SATELLITE_CACHE_PREFIX}:*")
if sat_keys:
if hasattr(django_cache, 'delete_many'):
django_cache.delete_many(sat_keys)
else:
for key in sat_keys:
django_cache.delete(key)
stats["cleared"] += len(sat_keys)
logger.info(f"Очищено {len(sat_keys)} данных спутников")
except Exception as e:
error_msg = f"Ошибка при очистке кеша спутников: {e}"
logger.error(error_msg)
stats["errors"].append(error_msg)
else:
logger.warning("Бэкенд кеша не поддерживает keys()")
logger.info("Для полной очистки используйте: redis-cli flushdb")
except Exception as e:
error_msg = f"Критическая ошибка при очистке кеша: {e}"
logger.error(error_msg)
stats["errors"].append(error_msg)
return stats
@classmethod
def clear_all_cache(cls) -> dict:
"""Полностью очищает весь кеш LyngSat."""
stats = {"cleared": 0, "errors": []}
try:
from django.core.cache import cache as django_cache
# Для django-redis используем keys() + delete_many()
if hasattr(django_cache, 'keys'):
patterns = [
f"{cls.REGION_CACHE_PREFIX}:*",
f"{cls.SATELLITE_CACHE_PREFIX}:*",
f"{cls.SATELLITE_LIST_CACHE_PREFIX}:*",
]
all_keys = []
for pattern in patterns:
try:
keys = django_cache.keys(pattern)
if keys:
all_keys.extend(keys)
logger.info(f"Найдено {len(keys)} ключей по паттерну: {pattern}")
except Exception as e:
error_msg = f"Ошибка при поиске ключей {pattern}: {e}"
logger.error(error_msg)
stats["errors"].append(error_msg)
# Удаляем все найденные ключи
if all_keys:
try:
if hasattr(django_cache, 'delete_many'):
django_cache.delete_many(all_keys)
else:
for key in all_keys:
django_cache.delete(key)
stats["cleared"] = len(all_keys)
logger.info(f"Удалено {len(all_keys)} ключей")
except Exception as e:
error_msg = f"Ошибка при удалении ключей: {e}"
logger.error(error_msg)
stats["errors"].append(error_msg)
else:
logger.info("Ключи для удаления не найдены")
elif hasattr(django_cache, 'delete_pattern'):
# Fallback на delete_pattern
patterns = [
f"{cls.REGION_CACHE_PREFIX}:*",
f"{cls.SATELLITE_CACHE_PREFIX}:*",
f"{cls.SATELLITE_LIST_CACHE_PREFIX}:*",
]
for pattern in patterns:
try:
deleted = django_cache.delete_pattern(pattern)
if deleted and isinstance(deleted, int):
stats["cleared"] += deleted
logger.info(f"Очищено {deleted} ключей по паттерну: {pattern}")
except Exception as e:
error_msg = f"Ошибка при очистке паттерна {pattern}: {e}"
logger.error(error_msg)
stats["errors"].append(error_msg)
else:
# Fallback для других бэкендов кеша
logger.warning("Бэкенд кеша не поддерживает keys() или delete_pattern()")
return cls.clear_cache("all")
except Exception as e:
error_msg = f"Критическая ошибка при полной очистке кеша: {e}"
logger.error(error_msg)
stats["errors"].append(error_msg)
return stats
def parse_metadata(self, metadata: str) -> dict:
"""Парсит метаданные транспондера."""
if not metadata or not metadata.strip():
return {
"standard": None,
"modulation": None,
"symbol_rate": None,
"fec": None,
}
normalized = re.sub(r"\s+", "", metadata.strip())
fec_match = re.search(r"([1-9]/[1-9])$", normalized)
fec = fec_match.group(1) if fec_match else None
if fec_match:
core = normalized[: fec_match.start()]
else:
core = normalized
std_match = re.match(r"(DVB-S2?|ABS-S|DVB-T2?|ATSC|ISDB)", core)
standard = std_match.group(1) if std_match else None
rest = core[len(standard) :] if standard else core
modulation = None
mod_match = re.match(r"(8PSK|QPSK|16APSK|32APSK|64QAM|256QAM|BPSK)", rest)
if mod_match:
modulation = mod_match.group(1)
rest = rest[len(modulation) :]
symbol_rate = None
sr_match = re.search(r"(\d+)$", rest)
if sr_match:
try:
symbol_rate = int(sr_match.group(1))
except ValueError:
pass
return {
"standard": standard,
"modulation": modulation,
"symbol_rate": symbol_rate,
"fec": fec,
}
def extract_date(self, s: str) -> datetime | None:
"""Извлекает дату из строки формата YYMMDD."""
s = s.strip()
match = re.search(r"(\d{6})$", s)
if not match:
return None
yymmdd = match.group(1)
try:
return datetime.strptime(yymmdd, "%y%m%d").date()
except ValueError:
return None
def convert_polarization(self, polarization: str) -> str:
"""Преобразует код поляризации в понятное название на русском."""
polarization_map = {
"V": "Вертикальная",
"H": "Горизонтальная",
"R": "Правая",
"L": "Левая",
}
return polarization_map.get(polarization.upper(), polarization)
def fetch_region_page(self, region: str, force_refresh: bool = False) -> Optional[str]:
"""
Получает HTML страницу региона с кешированием.
Args:
region: Название региона
force_refresh: Принудительно обновить кеш
Returns:
HTML содержимое страницы или None при ошибке
"""
cache_key = self._get_cache_key(self.REGION_CACHE_PREFIX, region)
# Проверяем кеш
if not force_refresh:
cached_html = self._get_from_cache(cache_key)
if cached_html:
logger.info(f"Страница региона {region} получена из кеша")
return cached_html
# Запрашиваем страницу
url = f"{self.BASE_URL}/{region}.html"
logger.info(f"Запрос страницы региона: {url}")
try:
payload = {"cmd": "request.get", "url": url, "maxTimeout": 60000}
response = requests.post(self.flaresolver_url, json=payload, timeout=70)
if response.status_code != 200:
logger.error(f"Ошибка при запросе {url}: статус {response.status_code}")
return None
html_content = response.json().get("solution", {}).get("response", "")
if html_content:
# Сохраняем в кеш
self._set_to_cache(cache_key, html_content, self.REGION_CACHE_TTL)
logger.info(f"Страница региона {region} получена и закеширована")
return html_content
except Exception as e:
logger.error(f"Ошибка при получении страницы {url}: {e}", exc_info=True)
return None
def get_satellite_list_from_region(self, region: str, force_refresh: bool = False) -> list[dict]:
"""
Получает список спутников из региона.
Args:
region: Название региона
force_refresh: Принудительно обновить кеш
Returns:
Список словарей с информацией о спутниках
"""
# Создаем уникальный ключ кеша с учетом целевых спутников
# Если target_sats не указаны, используем "all"
sats_key = "all" if not self.target_sats else "_".join(sorted(self.target_sats))
cache_key = self._get_cache_key(self.SATELLITE_LIST_CACHE_PREFIX, f"{region}_{sats_key}")
# Проверяем кеш
if not force_refresh:
cached_list = self._get_from_cache(cache_key)
if cached_list:
logger.info(f"Список спутников региона {region} (фильтр: {sats_key[:50]}) получен из кеша")
return cached_list
# Получаем HTML страницы
html_content = self.fetch_region_page(region, force_refresh)
if not html_content:
return []
# Парсим список спутников
satellites = []
try:
soup = BeautifulSoup(html_content, "html.parser")
col_table = soup.find_all("div", class_="desktab")[0]
tables = col_table.find_next_sibling("table").find_all("table")
trs = []
for table in tables:
trs.extend(table.find_all("tr"))
for tr in trs:
sat_name = tr.find("span").text.replace("ü", "u").strip().lower()
# Фильтруем по целевым спутникам
if self.target_sats is not None:
names = parse_satellite_names(sat_name)
if len(names) == 1:
sat_name = names[0]
else:
for name in names:
if name in self.target_sats:
sat_name = name
break
if sat_name not in self.target_sats:
continue
try:
sat_url = tr.find_all("a")[2]["href"]
except IndexError:
sat_url = tr.find_all("a")[0]["href"]
update_date_str = tr.find_all("td")[-1].text
try:
update_date = datetime.strptime(update_date_str, "%y%m%d").date()
except (ValueError, TypeError):
update_date = None
satellites.append({
"name": sat_name,
"url": sat_url,
"update_date": update_date,
"region": region
})
# Сохраняем в кеш
self._set_to_cache(cache_key, satellites, self.REGION_CACHE_TTL)
sats_filter = "все" if not self.target_sats else f"{len(self.target_sats)} целевых"
logger.info(f"Найдено {len(satellites)} спутников в регионе {region} (фильтр: {sats_filter})")
except Exception as e:
logger.error(f"Ошибка при парсинге списка спутников региона {region}: {e}", exc_info=True)
return satellites
def fetch_satellite_data(self, sat_name: str, sat_url: str, force_refresh: bool = False) -> Optional[dict]:
"""
Получает данные одного спутника с кешированием.
Args:
sat_name: Название спутника
sat_url: URL страницы спутника
force_refresh: Принудительно обновить кеш
Returns:
Словарь с данными спутника или None при ошибке
"""
cache_key = self._get_cache_key(self.SATELLITE_CACHE_PREFIX, sat_name)
# Проверяем кеш
if not force_refresh:
cached_data = self._get_from_cache(cache_key)
if cached_data:
logger.info(f"Данные спутника {sat_name} получены из кеша")
return cached_data
# Запрашиваем данные
full_url = f"{self.BASE_URL}/{sat_url}"
logger.info(f"Запрос данных спутника {sat_name}: {full_url}")
try:
payload = {"cmd": "request.get", "url": full_url, "maxTimeout": 60000}
response = requests.post(self.flaresolver_url, json=payload, timeout=70)
if response.status_code != 200:
logger.error(f"Ошибка при запросе {full_url}: статус {response.status_code}")
return None
html_content = response.json().get("solution", {}).get("response", "")
if not html_content:
logger.warning(f"Пустой ответ для спутника {sat_name}")
return None
# Парсим данные
sources = self.parse_satellite_content(html_content)
satellite_data = {
"name": sat_name,
"url": full_url,
"sources": sources,
"fetched_at": datetime.now().isoformat()
}
# Сохраняем в кеш
self._set_to_cache(cache_key, satellite_data, self.SATELLITE_CACHE_TTL)
logger.info(f"Данные спутника {sat_name} получены и закешированы ({len(sources)} источников)")
return satellite_data
except Exception as e:
logger.error(f"Ошибка при получении данных спутника {sat_name}: {e}", exc_info=True)
return None
def parse_satellite_content(self, html_content: str) -> list[dict]:
"""Парсит содержимое страницы спутника."""
data = []
try:
sat_soup = BeautifulSoup(html_content, "html.parser")
big_table = sat_soup.find("table", class_="bigtable")
if not big_table:
logger.warning("Таблица bigtable не найдена")
return data
all_tables = big_table.find_all("div", class_="desktab")[:-1]
for table in all_tables:
trs = table.find_next_sibling("table").find_all("tr")
for idx, tr in enumerate(trs):
tds = tr.find_all("td")
if len(tds) < 9 or idx < 2:
continue
try:
freq, polarization = tds[0].find("b").text.strip().split("\xa0")
polarization = self.convert_polarization(polarization)
meta = self.parse_metadata(tds[1].text)
provider_name = tds[3].text
last_update = self.extract_date(tds[-1].text)
data.append({
"freq": freq,
"pol": polarization,
"metadata": meta,
"provider_name": provider_name,
"last_update": last_update,
})
except Exception as e:
logger.debug(f"Ошибка при парсинге строки транспондера: {e}")
continue
except Exception as e:
logger.error(f"Ошибка при парсинге содержимого спутника: {e}", exc_info=True)
return data
def get_all_satellites_list(self, force_refresh: bool = False) -> list[dict]:
"""
Получает список всех спутников из всех регионов.
Args:
force_refresh: Принудительно обновить кеш
Returns:
Список словарей с информацией о спутниках
"""
all_satellites = []
for region in self.regions:
logger.info(f"Получение списка спутников из региона: {region}")
satellites = self.get_satellite_list_from_region(region, force_refresh)
all_satellites.extend(satellites)
logger.info(f"Всего найдено спутников: {len(all_satellites)}")
return all_satellites

View File

@@ -0,0 +1,287 @@
"""
Утилиты для асинхронной обработки данных LyngSat с кешированием.
"""
import logging
from typing import Callable, Optional
from .async_parser import AsyncLyngSatParser
from .models import LyngSat
from mainapp.models import Polarization, Standard, Modulation, Satellite
logger = logging.getLogger(__name__)
def process_single_satellite(
parser: AsyncLyngSatParser,
satellite_info: dict,
force_refresh: bool = False
) -> dict:
"""
Обрабатывает один спутник и сохраняет данные в БД.
Args:
parser: Экземпляр парсера
satellite_info: Информация о спутнике (name, url, update_date)
force_refresh: Принудительно обновить кеш
Returns:
dict: Статистика обработки спутника
"""
sat_name = satellite_info["name"]
sat_url = satellite_info["url"]
stats = {
"satellite_name": sat_name,
"sources_found": 0,
"created": 0,
"updated": 0,
"errors": []
}
logger.info(f"Обработка спутника: {sat_name}")
# Получаем данные спутника (из кеша или с сайта)
satellite_data = parser.fetch_satellite_data(sat_name, sat_url, force_refresh)
if not satellite_data:
error_msg = f"Не удалось получить данные для спутника {sat_name}"
logger.error(error_msg)
stats["errors"].append(error_msg)
return stats
sources = satellite_data.get("sources", [])
stats["sources_found"] = len(sources)
logger.info(f"Найдено {len(sources)} источников для {sat_name}")
# Находим спутник в базе
try:
sat_obj = Satellite.objects.get(name__icontains=sat_name)
logger.debug(f"Спутник {sat_name} найден в базе (ID: {sat_obj.id})")
except Satellite.DoesNotExist:
error_msg = f"Спутник '{sat_name}' не найден в базе данных"
logger.warning(error_msg)
stats["errors"].append(error_msg)
return stats
except Satellite.MultipleObjectsReturned:
error_msg = f"Найдено несколько спутников с именем '{sat_name}'"
logger.warning(error_msg)
stats["errors"].append(error_msg)
return stats
# Обрабатываем каждый источник
for source_idx, source in enumerate(sources, 1):
try:
# Парсим частоту
try:
freq = float(source['freq'])
except (ValueError, TypeError):
freq = -1.0
logger.debug(f"Некорректная частота для {sat_name}: {source.get('freq')}")
last_update = source['last_update']
fec = source['metadata'].get('fec')
modulation_name = source['metadata'].get('modulation')
standard_name = source['metadata'].get('standard')
symbol_velocity = source['metadata'].get('symbol_rate')
polarization_name = source['pol']
channel_info = source['provider_name']
# Создаем или получаем связанные объекты
pol_obj, _ = Polarization.objects.get_or_create(
name=polarization_name if polarization_name else "-"
)
mod_obj, _ = Modulation.objects.get_or_create(
name=modulation_name if modulation_name else "-"
)
standard_obj, _ = Standard.objects.get_or_create(
name=standard_name if standard_name else "-"
)
# Создаем или обновляем запись Lyngsat
lyng_obj, created = LyngSat.objects.update_or_create(
id_satellite=sat_obj,
frequency=freq,
polarization=pol_obj,
defaults={
"modulation": mod_obj,
"standard": standard_obj,
"sym_velocity": symbol_velocity if symbol_velocity else 0,
"channel_info": channel_info[:20] if channel_info else "",
"last_update": last_update,
"fec": fec[:30] if fec else "",
"url": satellite_data["url"]
}
)
if created:
stats['created'] += 1
logger.debug(f"Создана запись для {sat_name} {freq} МГц")
else:
stats['updated'] += 1
logger.debug(f"Обновлена запись для {sat_name} {freq} МГц")
# Логируем прогресс каждые 10 источников
if source_idx % 10 == 0:
logger.info(f"Обработано {source_idx}/{len(sources)} источников для {sat_name}")
except Exception as e:
error_msg = f"Ошибка при обработке источника {sat_name}: {str(e)}"
logger.error(error_msg, exc_info=True)
stats['errors'].append(error_msg)
continue
logger.info(f"Завершена обработка {sat_name}: создано {stats['created']}, обновлено {stats['updated']}")
return stats
def fill_lyngsat_data_async(
target_sats: list[str],
regions: list[str] = None,
task_id: str = None,
update_progress: Optional[Callable] = None,
force_refresh: bool = False,
use_cache: bool = True
) -> dict:
"""
Асинхронно заполняет данные Lyngsat для указанных спутников.
Обрабатывает спутники по одному с кешированием.
Args:
target_sats: Список названий спутников для обработки
regions: Список регионов для парсинга (по умолчанию все)
task_id: ID задачи Celery для логирования
update_progress: Функция для обновления прогресса (current, total, status, details)
force_refresh: Принудительно обновить кеш
use_cache: Использовать ли кеширование
Returns:
dict: Статистика обработки
"""
log_prefix = f"[Task {task_id}]" if task_id else "[Lyngsat Async]"
overall_stats = {
'total_satellites': 0,
'processed_satellites': 0,
'total_sources': 0,
'created': 0,
'updated': 0,
'errors': [],
'satellites_details': []
}
if regions is None:
regions = ["europe", "asia", "america", "atlantic"]
logger.info(f"{log_prefix} Начало асинхронной обработки данных")
logger.info(f"{log_prefix} Спутники: {', '.join(target_sats)}")
logger.info(f"{log_prefix} Регионы: {', '.join(regions)}")
logger.info(f"{log_prefix} Использование кеша: {use_cache}, Принудительное обновление: {force_refresh}")
if update_progress:
update_progress(0, len(target_sats), "Инициализация парсера...", {})
try:
# Создаем парсер
parser = AsyncLyngSatParser(
flaresolver_url="http://localhost:8191/v1",
target_sats=target_sats,
regions=regions,
use_cache=use_cache
)
logger.info(f"{log_prefix} Получение списка спутников...")
if update_progress:
update_progress(0, len(target_sats), "Получение списка спутников...", {})
# Получаем список всех спутников
all_satellites = parser.get_all_satellites_list(force_refresh)
overall_stats['total_satellites'] = len(all_satellites)
logger.info(f"{log_prefix} Найдено {len(all_satellites)} спутников для обработки")
# Обрабатываем каждый спутник по отдельности
for idx, satellite_info in enumerate(all_satellites, 1):
sat_name = satellite_info["name"]
logger.info(f"{log_prefix} Обработка спутника {idx}/{len(all_satellites)}: {sat_name}")
if update_progress:
update_progress(
idx - 1,
len(all_satellites),
f"Обработка {sat_name}...",
{
"current_satellite": sat_name,
"created": overall_stats['created'],
"updated": overall_stats['updated']
}
)
# Обрабатываем спутник
sat_stats = process_single_satellite(parser, satellite_info, force_refresh)
# Обновляем общую статистику
overall_stats['processed_satellites'] += 1
overall_stats['total_sources'] += sat_stats['sources_found']
overall_stats['created'] += sat_stats['created']
overall_stats['updated'] += sat_stats['updated']
overall_stats['errors'].extend(sat_stats['errors'])
overall_stats['satellites_details'].append(sat_stats)
logger.info(
f"{log_prefix} Спутник {sat_name} обработан: "
f"источников {sat_stats['sources_found']}, "
f"создано {sat_stats['created']}, "
f"обновлено {sat_stats['updated']}"
)
logger.info(
f"{log_prefix} Обработка завершена. "
f"Спутников: {overall_stats['processed_satellites']}/{overall_stats['total_satellites']}, "
f"Источников: {overall_stats['total_sources']}, "
f"Создано: {overall_stats['created']}, "
f"Обновлено: {overall_stats['updated']}, "
f"Ошибок: {len(overall_stats['errors'])}"
)
if update_progress:
update_progress(
overall_stats['processed_satellites'],
overall_stats['total_satellites'],
"Завершено",
{
"created": overall_stats['created'],
"updated": overall_stats['updated'],
"errors_count": len(overall_stats['errors'])
}
)
except Exception as e:
error_msg = f"Критическая ошибка: {str(e)}"
logger.error(f"{log_prefix} {error_msg}", exc_info=True)
overall_stats['errors'].append(error_msg)
return overall_stats
def clear_lyngsat_cache(cache_type: str = "all") -> dict:
"""
Очищает кеш LyngSat.
Args:
cache_type: Тип кеша для очистки ("regions", "satellites", "all")
Returns:
dict: Статистика очистки
"""
logger.info(f"Очистка кеша LyngSat: {cache_type}")
if cache_type == "all":
stats = AsyncLyngSatParser.clear_all_cache()
else:
stats = AsyncLyngSatParser.clear_cache(cache_type)
logger.info(f"Кеш очищен: {stats}")
return stats

View File

View File

@@ -0,0 +1,40 @@
"""
Management команда для очистки кеша LyngSat.
"""
from django.core.management.base import BaseCommand
from lyngsatapp.async_utils import clear_lyngsat_cache
class Command(BaseCommand):
help = 'Очищает кеш данных LyngSat'
def add_arguments(self, parser):
parser.add_argument(
'--type',
type=str,
default='all',
choices=['regions', 'satellites', 'all'],
help='Тип кеша для очистки (regions, satellites, all)'
)
def handle(self, *args, **options):
cache_type = options['type']
self.stdout.write(f'Очистка кеша LyngSat: {cache_type}...')
stats = clear_lyngsat_cache(cache_type)
self.stdout.write(
self.style.SUCCESS(
f'Кеш очищен успешно! Удалено записей: {stats["cleared"]}'
)
)
if stats['errors']:
self.stdout.write(
self.style.WARNING(
f'Ошибок при очистке: {len(stats["errors"])}'
)
)
for error in stats['errors']:
self.stdout.write(self.style.ERROR(f' - {error}'))

View File

@@ -6,14 +6,101 @@ from celery import shared_task
from django.core.cache import cache from django.core.cache import cache
from .utils import fill_lyngsat_data from .utils import fill_lyngsat_data
from .async_utils import fill_lyngsat_data_async, clear_lyngsat_cache
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@shared_task(bind=True, name='lyngsatapp.fill_lyngsat_data_async') @shared_task(bind=True, name='lyngsatapp.fill_lyngsat_data_async')
def fill_lyngsat_data_task(self, target_sats, regions=None): def fill_lyngsat_data_task(self, target_sats, regions=None, force_refresh=False, use_cache=True):
""" """
Асинхронная задача для заполнения данных Lyngsat. Асинхронная задача для заполнения данных Lyngsat с кешированием.
Обрабатывает спутники по одному.
Args:
target_sats: Список названий спутников для обработки
regions: Список регионов для парсинга (по умолчанию все)
force_refresh: Принудительно обновить кеш
use_cache: Использовать ли кеширование
Returns:
dict: Статистика обработки
"""
task_id = self.request.id
logger.info(f"[Task {task_id}] Начало обработки данных Lyngsat")
logger.info(f"[Task {task_id}] Спутники: {', '.join(target_sats)}")
logger.info(f"[Task {task_id}] Регионы: {', '.join(regions) if regions else 'все'}")
logger.info(f"[Task {task_id}] Кеширование: {use_cache}, Принудительное обновление: {force_refresh}")
# Обновляем статус задачи
self.update_state(
state='PROGRESS',
meta={
'current': 0,
'total': len(target_sats),
'status': 'Инициализация...',
'details': {}
}
)
try:
# Вызываем асинхронную функцию заполнения данных
stats = fill_lyngsat_data_async(
target_sats=target_sats,
regions=regions,
task_id=task_id,
force_refresh=force_refresh,
use_cache=use_cache,
update_progress=lambda current, total, status, details: self.update_state(
state='PROGRESS',
meta={
'current': current,
'total': total,
'status': status,
'details': details
}
)
)
logger.info(f"[Task {task_id}] Обработка завершена успешно")
logger.info(f"[Task {task_id}] Статистика: {stats}")
# Сохраняем результат в кеш для отображения на странице
cache.set(f'lyngsat_task_{task_id}', stats, timeout=3600)
return stats
except Exception as e:
logger.error(f"[Task {task_id}] Ошибка при обработке: {str(e)}", exc_info=True)
error_message = f"{type(e).__name__}: {str(e)}"
self.update_state(
state='FAILURE',
meta={
'error': error_message,
'status': 'Ошибка при обработке',
'details': {},
'exc_type': type(e).__name__,
'exc_message': str(e)
}
)
# Возвращаем словарь с ошибкой вместо raise для корректной сериализации
return {
'error': error_message,
'status': 'FAILURE',
'total_satellites': 0,
'processed_satellites': 0,
'total_sources': 0,
'created': 0,
'updated': 0,
'errors': [error_message]
}
@shared_task(bind=True, name='lyngsatapp.fill_lyngsat_data_sync')
def fill_lyngsat_data_task_sync(self, target_sats, regions=None):
"""
Синхронная задача для заполнения данных Lyngsat (старая версия без кеширования).
Используется для обратной совместимости.
Args: Args:
target_sats: Список названий спутников для обработки target_sats: Список названий спутников для обработки
@@ -23,7 +110,7 @@ def fill_lyngsat_data_task(self, target_sats, regions=None):
dict: Статистика обработки dict: Статистика обработки
""" """
task_id = self.request.id task_id = self.request.id
logger.info(f"[Task {task_id}] Начало обработки данных Lyngsat") logger.info(f"[Task {task_id}] Начало синхронной обработки данных Lyngsat")
logger.info(f"[Task {task_id}] Спутники: {', '.join(target_sats)}") logger.info(f"[Task {task_id}] Спутники: {', '.join(target_sats)}")
logger.info(f"[Task {task_id}] Регионы: {', '.join(regions) if regions else 'все'}") logger.info(f"[Task {task_id}] Регионы: {', '.join(regions) if regions else 'все'}")
@@ -38,7 +125,7 @@ def fill_lyngsat_data_task(self, target_sats, regions=None):
) )
try: try:
# Вызываем функцию заполнения данных # Вызываем старую функцию заполнения данных
stats = fill_lyngsat_data( stats = fill_lyngsat_data(
target_sats=target_sats, target_sats=target_sats,
regions=regions, regions=regions,
@@ -63,11 +150,52 @@ def fill_lyngsat_data_task(self, target_sats, regions=None):
except Exception as e: except Exception as e:
logger.error(f"[Task {task_id}] Ошибка при обработке: {str(e)}", exc_info=True) logger.error(f"[Task {task_id}] Ошибка при обработке: {str(e)}", exc_info=True)
error_message = f"{type(e).__name__}: {str(e)}"
self.update_state( self.update_state(
state='FAILURE', state='FAILURE',
meta={ meta={
'error': str(e), 'error': error_message,
'status': 'Ошибка при обработке' 'status': 'Ошибка при обработке',
'exc_type': type(e).__name__,
'exc_message': str(e)
} }
) )
raise # Возвращаем словарь с ошибкой вместо raise для корректной сериализации
return {
'error': error_message,
'status': 'FAILURE',
'total_satellites': 0,
'total_sources': 0,
'created': 0,
'updated': 0,
'errors': [error_message]
}
@shared_task(bind=True, name='lyngsatapp.clear_cache')
def clear_cache_task(self, cache_type='all'):
"""
Задача для очистки кеша LyngSat.
Args:
cache_type: Тип кеша для очистки ("regions", "satellites", "all")
Returns:
dict: Статистика очистки
"""
task_id = self.request.id
logger.info(f"[Task {task_id}] Запуск задачи очистки кеша: {cache_type}")
try:
stats = clear_lyngsat_cache(cache_type)
logger.info(f"[Task {task_id}] Кеш очищен успешно: {stats}")
return stats
except Exception as e:
logger.error(f"[Task {task_id}] Ошибка при очистке кеша: {str(e)}", exc_info=True)
error_message = f"{type(e).__name__}: {str(e)}"
return {
'error': error_message,
'status': 'FAILURE',
'cleared': 0,
'errors': [error_message]
}

View File

@@ -25,7 +25,6 @@ from .models import (
Standard, Standard,
SigmaParMark, SigmaParMark,
SigmaParameter, SigmaParameter,
SourceType,
Parameter, Parameter,
Satellite, Satellite,
Mirror, Mirror,
@@ -336,14 +335,6 @@ class ModulationAdmin(BaseAdmin):
ordering = ("name",) ordering = ("name",)
@admin.register(SourceType)
class SourceTypeAdmin(BaseAdmin):
"""Админ-панель для модели SourceType."""
list_display = ("name",)
search_fields = ("name",)
ordering = ("name",)
@admin.register(Standard) @admin.register(Standard)
class StandardAdmin(BaseAdmin): class StandardAdmin(BaseAdmin):
"""Админ-панель для модели Standard.""" """Админ-панель для модели Standard."""

View File

@@ -70,17 +70,20 @@ class VchLinkForm(forms.Form):
# label='Выбор диапазона' # label='Выбор диапазона'
# ) # )
value1 = forms.FloatField( value1 = forms.FloatField(
label="Первое число", label="Разброс по частоте (не используется)",
required=False,
initial=0.0,
widget=forms.NumberInput(attrs={ widget=forms.NumberInput(attrs={
'class': 'form-control', 'class': 'form-control',
'placeholder': 'Введите первое число' 'placeholder': 'Не используется - погрешность определяется автоматически'
}) })
) )
value2 = forms.FloatField( value2 = forms.FloatField(
label="Второе число", label="Разброс по полосе (в %)",
widget=forms.NumberInput(attrs={ widget=forms.NumberInput(attrs={
'class': 'form-control', 'class': 'form-control',
'placeholder': 'Введите второе число' 'placeholder': 'Введите погрешность полосы в процентах',
'step': '0.1'
}) })
) )
@@ -110,7 +113,7 @@ class NewEventForm(forms.Form):
class FillLyngsatDataForm(forms.Form): class FillLyngsatDataForm(forms.Form):
"""Форма для заполнения данных из Lyngsat""" """Форма для заполнения данных из Lyngsat с поддержкой кеширования"""
REGION_CHOICES = [ REGION_CHOICES = [
('europe', 'Европа'), ('europe', 'Европа'),
@@ -141,6 +144,52 @@ class FillLyngsatDataForm(forms.Form):
initial=['europe', 'asia', 'america', 'atlantic'], initial=['europe', 'asia', 'america', 'atlantic'],
help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких регионов" help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких регионов"
) )
use_cache = forms.BooleanField(
label="Использовать кеширование",
required=False,
initial=True,
widget=forms.CheckboxInput(attrs={
'class': 'form-check-input'
}),
help_text="Использовать кешированные данные (ускоряет повторные запросы)"
)
force_refresh = forms.BooleanField(
label="Принудительно обновить данные",
required=False,
initial=False,
widget=forms.CheckboxInput(attrs={
'class': 'form-check-input'
}),
help_text="Игнорировать кеш и получить свежие данные с сайта"
)
class LinkLyngsatForm(forms.Form):
"""Форма для привязки источников LyngSat к объектам"""
satellites = forms.ModelMultipleChoiceField(
queryset=Satellite.objects.all().order_by('name'),
label="Выберите спутники",
widget=forms.SelectMultiple(attrs={
'class': 'form-select',
'size': '10'
}),
required=False,
help_text="Оставьте пустым для обработки всех спутников"
)
frequency_tolerance = forms.FloatField(
label="Допуск по частоте (МГц)",
initial=0.5,
min_value=0,
widget=forms.NumberInput(attrs={
'class': 'form-control',
'step': '0.1'
}),
help_text="Допустимое отклонение частоты при сравнении"
)
class ParameterForm(forms.ModelForm): class ParameterForm(forms.ModelForm):
""" """
Форма для создания и редактирования параметров ВЧ загрузки. Форма для создания и редактирования параметров ВЧ загрузки.

View File

@@ -0,0 +1,27 @@
# Generated by Django 5.2.7 on 2025-11-11 19:02
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('lyngsatapp', '0002_alter_lyngsat_last_update'),
('mainapp', '0008_remove_sourcetype_objitem_objitem_source_type_id_and_more'),
]
operations = [
migrations.RemoveField(
model_name='objitem',
name='source_type_id',
),
migrations.AddField(
model_name='objitem',
name='lyngsat_source',
field=models.ForeignKey(blank=True, help_text='Связанный источник из базы LyngSat (ТВ)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems', to='lyngsatapp.lyngsat', verbose_name='Источник LyngSat'),
),
migrations.DeleteModel(
name='SourceType',
),
]

View File

@@ -233,27 +233,7 @@ class Satellite(models.Model):
ordering = ["name"] ordering = ["name"]
class SourceType(models.Model):
"""
Модель типа источника сигнала.
Классифицирует источники по типам (наземный, морской, воздушный и т.д.).
"""
# Основные поля
name = models.CharField(
max_length=50,
unique=True,
verbose_name="Тип источника",
db_index=True,
help_text="Тип источника сигнала",
)
def __str__(self):
return self.name
class Meta:
verbose_name = "Тип источника"
verbose_name_plural = "Типы источников"
ordering = ["name"]
class ObjItemQuerySet(models.QuerySet): class ObjItemQuerySet(models.QuerySet):
"""Custom QuerySet для модели ObjItem с оптимизированными запросами""" """Custom QuerySet для модели ObjItem с оптимизированными запросами"""
@@ -264,7 +244,7 @@ class ObjItemQuerySet(models.QuerySet):
"geo_obj", "geo_obj",
"updated_by__user", "updated_by__user",
"created_by__user", "created_by__user",
"source_type_obj", "lyngsat_source",
"parameter_obj", "parameter_obj",
"parameter_obj__id_satellite", "parameter_obj__id_satellite",
"parameter_obj__polarization", "parameter_obj__polarization",
@@ -349,14 +329,14 @@ class ObjItem(models.Model):
verbose_name="Изменен пользователем", verbose_name="Изменен пользователем",
help_text="Пользователь, последним изменивший запись", help_text="Пользователь, последним изменивший запись",
) )
source_type_id = models.ForeignKey( lyngsat_source = models.ForeignKey(
SourceType, "lyngsatapp.LyngSat",
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
related_name="objitems_sourcetype", related_name="objitems",
null=True, null=True,
blank=True, blank=True,
verbose_name="Тип источника", verbose_name="Источник LyngSat",
help_text="Тип источника сигнала", help_text="Связанный источник из базы LyngSat (ТВ)",
) )
# Custom manager # Custom manager

View File

@@ -75,7 +75,7 @@
<h3 class="card-title mb-0">Добавление списка спутников</h3> <h3 class="card-title mb-0">Добавление списка спутников</h3>
</div> </div>
<p class="card-text">Добавьте новый список спутников в базу данных для последующего использования в загрузке данных.</p> <p class="card-text">Добавьте новый список спутников в базу данных для последующего использования в загрузке данных.</p>
<a href="{% url 'mainapp:add_sats' %}" class="btn btn-info"> <a href="{% url 'mainapp:add_sats' %}" class="btn btn-info disabled">
Добавить список спутников Добавить список спутников
</a> </a>
</div> </div>
@@ -96,7 +96,7 @@
<h3 class="card-title mb-0">Добавление транспондеров</h3> <h3 class="card-title mb-0">Добавление транспондеров</h3>
</div> </div>
<p class="card-text">Добавьте список транспондеров из JSON-файла в базу данных. Требуется наличие файла transponders.json.</p> <p class="card-text">Добавьте список транспондеров из JSON-файла в базу данных. Требуется наличие файла transponders.json.</p>
<a href="{% url 'mainapp:add_trans' %}" class="btn btn-warning"> <a href="{% url 'mainapp:add_trans' %}" class="btn btn-warning disabled">
Добавить транспондеры Добавить транспондеры
</a> </a>
</div> </div>
@@ -184,6 +184,27 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Link LyngSat Sources Card -->
<div class="col-lg-6">
<div class="card h-100 shadow-sm border-0">
<div class="card-body">
<div class="d-flex align-items-center mb-3">
<div class="bg-primary bg-opacity-10 rounded-circle p-2 me-3">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-link-45deg text-primary" viewBox="0 0 16 16">
<path d="M4.715 6.542 3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1.002 1.002 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4.018 4.018 0 0 1-.128-1.287z"/>
<path d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 1 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 1 0-4.243-4.243z"/>
</svg>
</div>
<h3 class="card-title mb-0">Привязка источников LyngSat</h3>
</div>
<p class="card-text">Автоматическая привязка источников из базы LyngSat к объектам по частоте и поляризации. Объекты с привязанными источниками отображаются как "ТВ".</p>
<a href="{% url 'mainapp:link_lyngsat' %}" class="btn btn-primary">
Привязать источники
</a>
</div>
</div>
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,113 @@
{% extends 'mainapp/base.html' %}
{% load static %}
{% block title %}Управление кешем LyngSat{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">
<i class="bi bi-database"></i> Управление кешем LyngSat
</h4>
</div>
<div class="card-body">
<div class="alert alert-info">
<h5><i class="bi bi-info-circle"></i> Информация о кешировании</h5>
<ul class="mb-0">
<li><strong>Страницы регионов:</strong> кешируются на 7 дней</li>
<li><strong>Данные спутников:</strong> кешируются на 1 день</li>
<li><strong>Списки спутников:</strong> кешируются на 7 дней</li>
</ul>
</div>
<h5 class="mt-4">Очистка кеша</h5>
<p class="text-muted">
Выберите тип кеша для очистки. Это полезно, если нужно принудительно обновить данные.
</p>
<form method="post" class="mt-3">
{% csrf_token %}
<div class="mb-3">
<label class="form-label">Тип кеша для очистки:</label>
<div class="list-group">
<button type="submit" name="cache_type" value="all"
class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">
<i class="bi bi-trash"></i> Очистить весь кеш
</h6>
</div>
<p class="mb-1 text-muted small">
Удалить все кешированные данные LyngSat (регионы, спутники, списки)
</p>
</button>
<button type="submit" name="cache_type" value="regions"
class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">
<i class="bi bi-globe"></i> Очистить кеш регионов
</h6>
</div>
<p class="mb-1 text-muted small">
Удалить кешированные страницы регионов (Europe, Asia, America, Atlantic)
</p>
</button>
<button type="submit" name="cache_type" value="satellites"
class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">
<i class="bi bi-satellite"></i> Очистить кеш спутников
</h6>
</div>
<p class="mb-1 text-muted small">
Удалить кешированные данные отдельных спутников
</p>
</button>
</div>
</div>
</form>
<hr>
<div class="alert alert-warning">
<h6><i class="bi bi-exclamation-triangle"></i> Внимание</h6>
<p class="mb-0 small">
После очистки кеша следующий запрос данных будет выполняться дольше,
так как данные будут загружаться заново с сайта LyngSat.
</p>
</div>
<div class="mt-3">
<a href="{% url 'mainapp:fill_lyngsat_data' %}" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Назад к заполнению данных
</a>
</div>
</div>
</div>
<div class="card mt-3">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-terminal"></i> Альтернативные способы очистки
</h5>
</div>
<div class="card-body">
<h6>Через Django Management команду:</h6>
<pre class="bg-dark text-light p-3 rounded"><code>python manage.py clear_lyngsat_cache --type all</code></pre>
<h6 class="mt-3">Через Redis CLI:</h6>
<pre class="bg-dark text-light p-3 rounded"><code>redis-cli keys "dbapp:lyngsat*"
redis-cli del "dbapp:lyngsat_region:europe"</code></pre>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,236 @@
<!-- SigmaParameter Data Modal -->
<div class="modal fade" id="sigmaParameterModal" tabindex="-1" aria-labelledby="sigmaParameterModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header bg-info text-white">
<h5 class="modal-title" id="sigmaParameterModalLabel">
<i class="bi bi-graph-up"></i> Данные SigmaParameter
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body" id="sigmaParameterModalBody">
<div class="text-center py-4">
<div class="spinner-border text-info" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
</div>
</div>
</div>
</div>
<script>
function showSigmaParameterModal(parameterId) {
// Показываем модальное окно
const modal = new bootstrap.Modal(document.getElementById('sigmaParameterModal'));
modal.show();
// Показываем индикатор загрузки
const modalBody = document.getElementById('sigmaParameterModalBody');
modalBody.innerHTML = `
<div class="text-center py-4">
<div class="spinner-border text-info" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
</div>
`;
// Загружаем данные
fetch(`/api/sigma-parameter/${parameterId}/`)
.then(response => {
if (!response.ok) {
throw new Error('Ошибка загрузки данных');
}
return response.json();
})
.then(data => {
if (data.sigma_parameters.length === 0) {
modalBody.innerHTML = `
<div class="alert alert-info" role="alert">
<i class="bi bi-info-circle"></i> Нет связанных SigmaParameter
</div>
`;
return;
}
// Формируем HTML с данными
let html = '<div class="container-fluid">';
data.sigma_parameters.forEach((sigma, index) => {
html += `
<div class="card mb-3">
<div class="card-header bg-light">
<strong><i class="bi bi-broadcast"></i> SigmaParameter #${index + 1}</strong>
</div>
<div class="card-body">
<div class="row g-3">
<!-- Основная информация -->
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-light">
<strong><i class="bi bi-info-circle"></i> Основная информация</strong>
</div>
<div class="card-body">
<table class="table table-sm table-borderless mb-0">
<tbody>
<tr>
<td class="text-muted" style="width: 40%;">Спутник:</td>
<td><strong>${sigma.satellite}</strong></td>
</tr>
<tr>
<td class="text-muted">Частота:</td>
<td><strong>${sigma.frequency} МГц</strong></td>
</tr>
<tr>
<td class="text-muted">Частота в Ku:</td>
<td><strong>${sigma.transfer_frequency} МГц</strong></td>
</tr>
<tr>
<td class="text-muted">Полоса частот:</td>
<td><strong>${sigma.freq_range} МГц</strong></td>
</tr>
<tr>
<td class="text-muted">Поляризация:</td>
<td><span class="badge bg-info">${sigma.polarization}</span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Технические параметры -->
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-light">
<strong><i class="bi bi-gear"></i> Технические параметры</strong>
</div>
<div class="card-body">
<table class="table table-sm table-borderless mb-0">
<tbody>
<tr>
<td class="text-muted" style="width: 40%;">Модуляция:</td>
<td><span class="badge bg-secondary">${sigma.modulation}</span></td>
</tr>
<tr>
<td class="text-muted">Стандарт:</td>
<td><span class="badge bg-secondary">${sigma.standard}</span></td>
</tr>
<tr>
<td class="text-muted">Сим. скорость:</td>
<td><strong>${sigma.bod_velocity} БОД</strong></td>
</tr>
<tr>
<td class="text-muted">ОСШ:</td>
<td><strong>${sigma.snr} дБ</strong></td>
</tr>
<tr>
<td class="text-muted">Мощность:</td>
<td><strong>${sigma.power} дБм</strong></td>
</tr>
<tr>
<td class="text-muted">Статус:</td>
<td>${sigma.status}</td>
</tr>
<tr>
<td class="text-muted">Пакетность:</td>
<td>${sigma.packets}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Временные параметры -->
<div class="col-md-6">
<div class="card">
<div class="card-header bg-light">
<strong><i class="bi bi-clock-history"></i> Временные параметры</strong>
</div>
<div class="card-body">
<table class="table table-sm table-borderless mb-0">
<tbody>
<tr>
<td class="text-muted" style="width: 40%;">Начало измерения:</td>
<td><strong>${sigma.datetime_begin}</strong></td>
</tr>
<tr>
<td class="text-muted">Окончание измерения:</td>
<td><strong>${sigma.datetime_end}</strong></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Отметки -->
<div class="col-md-6">
<div class="card">
<div class="card-header bg-light">
<strong><i class="bi bi-check2-square"></i> Отметки (${sigma.marks.length})</strong>
</div>
<div class="card-body">
`;
if (sigma.marks.length > 0) {
html += `
<div class="table-responsive" style="max-height: 200px; overflow-y: auto;">
<table class="table table-sm table-striped mb-0">
<thead class="table-light sticky-top">
<tr>
<th style="width: 20%;">Отметка</th>
<th>Дата</th>
</tr>
</thead>
<tbody>
`;
sigma.marks.forEach(mark => {
const markClass = mark.mark === '+' ? 'text-success' : 'text-danger';
html += `
<tr>
<td class="${markClass}"><strong>${mark.mark}</strong></td>
<td>${mark.date}</td>
</tr>
`;
});
html += `
</tbody>
</table>
</div>
`;
} else {
html += `
<p class="text-muted mb-0">Нет отметок</p>
`;
}
html += `
</div>
</div>
</div>
</div>
</div>
</div>
`;
});
html += '</div>';
modalBody.innerHTML = html;
})
.catch(error => {
modalBody.innerHTML = `
<div class="alert alert-danger" role="alert">
<i class="bi bi-exclamation-triangle"></i> ${error.message}
</div>
`;
});
}
</script>

View File

@@ -60,6 +60,42 @@
{% endif %} {% endif %}
</div> </div>
<!-- Cache Options -->
<div class="card mb-4 border-info">
<div class="card-header bg-info bg-opacity-10">
<h6 class="mb-0">
<i class="bi bi-database"></i> Настройки кеширования
</h6>
</div>
<div class="card-body">
<div class="form-check mb-2">
{{ form.use_cache }}
<label class="form-check-label" for="{{ form.use_cache.id_for_label }}">
{{ form.use_cache.label }}
</label>
{% if form.use_cache.help_text %}
<div class="form-text">{{ form.use_cache.help_text }}</div>
{% endif %}
</div>
<div class="form-check">
{{ form.force_refresh }}
<label class="form-check-label" for="{{ form.force_refresh.id_for_label }}">
{{ form.force_refresh.label }}
</label>
{% if form.force_refresh.help_text %}
<div class="form-text">{{ form.force_refresh.help_text }}</div>
{% endif %}
</div>
<div class="mt-3">
<a href="{% url 'mainapp:clear_lyngsat_cache' %}" class="btn btn-sm btn-outline-warning">
<i class="bi bi-trash"></i> Управление кешем
</a>
</div>
</div>
</div>
<!-- Buttons --> <!-- Buttons -->
<div class="d-grid gap-2 d-md-flex justify-content-md-between"> <div class="d-grid gap-2 d-md-flex justify-content-md-between">
<a href="{% url 'mainapp:actions' %}" class="btn btn-secondary"> <a href="{% url 'mainapp:actions' %}" class="btn btn-secondary">

View File

@@ -0,0 +1,90 @@
{% extends 'mainapp/base.html' %}
{% block title %}Привязка источников LyngSat{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h3 class="mb-0">
<i class="bi bi-link-45deg"></i> Привязка источников LyngSat к объектам
</h3>
</div>
<div class="card-body">
<!-- Alert messages -->
{% include 'mainapp/components/_messages.html' %}
<div class="alert alert-info" role="alert">
<i class="bi bi-info-circle"></i>
<strong>Информация:</strong> Эта функция автоматически привязывает источники из базы LyngSat к объектам
на основе совпадения частоты (с округлением) и поляризации. Объекты с привязанными источниками LyngSat
будут отмечены как "ТВ" в списке объектов.
</div>
<form method="post" class="needs-validation" novalidate>
{% csrf_token %}
<div class="mb-4">
<label for="{{ form.satellites.id_for_label }}" class="form-label">
{{ form.satellites.label }}
</label>
{{ form.satellites }}
{% if form.satellites.help_text %}
<div class="form-text">{{ form.satellites.help_text }}</div>
{% endif %}
{% if form.satellites.errors %}
<div class="invalid-feedback d-block">
{{ form.satellites.errors }}
</div>
{% endif %}
</div>
<div class="mb-4">
<label for="{{ form.frequency_tolerance.id_for_label }}" class="form-label">
{{ form.frequency_tolerance.label }}
</label>
{{ form.frequency_tolerance }}
{% if form.frequency_tolerance.help_text %}
<div class="form-text">{{ form.frequency_tolerance.help_text }}</div>
{% endif %}
{% if form.frequency_tolerance.errors %}
<div class="invalid-feedback d-block">
{{ form.frequency_tolerance.errors }}
</div>
{% endif %}
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-link-45deg"></i> Привязать источники
</button>
<a href="{% url 'mainapp:actions' %}" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Назад к действиям
</a>
</div>
</form>
</div>
</div>
<!-- Help section -->
<div class="card mt-4 shadow-sm">
<div class="card-header bg-light">
<h5 class="mb-0">
<i class="bi bi-question-circle"></i> Как это работает?
</h5>
</div>
<div class="card-body">
<ol class="mb-0">
<li>Система округляет частоту каждого объекта до целого числа</li>
<li>Ищет источники LyngSat с той же поляризацией и близкой частотой (в пределах допуска)</li>
<li>При нахождении совпадения создается связь между объектом и источником LyngSat</li>
<li>Объекты с привязанными источниками отображаются как "ТВ" в списке</li>
</ol>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -20,7 +20,17 @@
{% endfor %} {% endfor %}
{% endif %} {% endif %}
<p class="card-text">Введите допустимый разброс для частоты и полосы</p> <div class="alert alert-info" role="alert">
<strong>Информация о привязке:</strong>
<p class="mb-2">Погрешность центральной частоты определяется автоматически в зависимости от полосы:</p>
<ul class="mb-0 small">
<li>0 - 500 кГц: <strong>0.1%</strong></li>
<li>500 кГц - 1.5 МГц: <strong>0.5%</strong></li>
<li>1.5 - 5 МГц: <strong>1%</strong></li>
<li>5 - 10 МГц: <strong>2%</strong></li>
<li>Более 10 МГц: <strong>5%</strong></li>
</ul>
</div>
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
@@ -38,17 +48,19 @@
<div class="text-danger mt-1">{{ form.ku_range.errors }}</div> <div class="text-danger mt-1">{{ form.ku_range.errors }}</div>
{% endif %} {% endif %}
</div> {% endcomment %} </div> {% endcomment %}
<div class="mb-3"> {% comment %} <div class="mb-3">
<label for="{{ form.value1.id_for_label }}" class="form-label">Разброс по частоте(в МГц)</label> <label for="{{ form.value1.id_for_label }}" class="form-label">Разброс по частоте(в МГц)</label>
{{ form.value1 }} {{ form.value1 }}
<small class="form-text text-muted">Не используется - погрешность определяется автоматически</small>
{% if form.value1.errors %} {% if form.value1.errors %}
<div class="text-danger mt-1">{{ form.value1.errors }}</div> <div class="text-danger mt-1">{{ form.value1.errors }}</div>
{% endif %} {% endif %}
</div> </div> {% endcomment %}
<div class="mb-3"> <div class="mb-3">
<label for="{{ form.value2.id_for_label }}" class="form-label">Разброс по полосе(в %)</label> <label for="{{ form.value2.id_for_label }}" class="form-label">Разброс по полосе (в %)</label>
{{ form.value2 }} {{ form.value2 }}
<small class="form-text text-muted">Допустимое отклонение полосы частот в процентах</small>
{% if form.value2.errors %} {% if form.value2.errors %}
<div class="text-danger mt-1">{{ form.value2.errors }}</div> <div class="text-danger mt-1">{{ form.value2.errors }}</div>
{% endif %} {% endif %}

View File

@@ -251,6 +251,18 @@
onchange="toggleColumn(this)"> Стандарт onchange="toggleColumn(this)"> Стандарт
</label> </label>
</li> </li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="24" checked
onchange="toggleColumn(this)"> Тип источника
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="25" checked
onchange="toggleColumn(this)"> Sigma
</label>
</li>
</ul> </ul>
</div> </div>
@@ -463,6 +475,8 @@
{% include 'mainapp/components/_table_header.html' with label="Комментарий" field="" sortable=False %} {% include 'mainapp/components/_table_header.html' with label="Комментарий" field="" sortable=False %}
{% include 'mainapp/components/_table_header.html' with label="Усреднённое" field="" sortable=False %} {% include 'mainapp/components/_table_header.html' with label="Усреднённое" field="" sortable=False %}
{% include 'mainapp/components/_table_header.html' with label="Стандарт" field="standard" sort=sort %} {% include 'mainapp/components/_table_header.html' with label="Стандарт" field="standard" sort=sort %}
{% include 'mainapp/components/_table_header.html' with label="Тип источника" field="" sortable=False %}
{% include 'mainapp/components/_table_header.html' with label="Sigma" field="" sortable=False %}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -495,10 +509,28 @@
<td>{{ item.comment }}</td> <td>{{ item.comment }}</td>
<td>{{ item.is_average }}</td> <td>{{ item.is_average }}</td>
<td>{{ item.standard }}</td> <td>{{ item.standard }}</td>
<td>
{% if item.obj.lyngsat_source %}
<a href="#" class="text-primary text-decoration-none" onclick="showLyngsatModal({{ item.obj.lyngsat_source.id }}); return false;">
<i class="bi bi-tv"></i> ТВ
</a>
{% else %}
-
{% endif %}
</td>
<td>
{% if item.has_sigma %}
<a href="#" class="text-info text-decoration-none" onclick="showSigmaParameterModal({{ item.obj.parameter_obj.id }}); return false;" title="{{ item.sigma_info }}">
<i class="bi bi-graph-up"></i> {{ item.sigma_info }}
</a>
{% else %}
-
{% endif %}
</td>
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="22" class="text-center py-4"> <td colspan="26" class="text-center py-4">
{% if selected_satellite_id %} {% if selected_satellite_id %}
Нет данных для выбранных фильтров Нет данных для выбранных фильтров
{% else %} {% else %}
@@ -1165,4 +1197,163 @@
<!-- Include the selected items offcanvas component --> <!-- Include the selected items offcanvas component -->
{% include 'mainapp/components/_selected_items_offcanvas.html' %} {% include 'mainapp/components/_selected_items_offcanvas.html' %}
<!-- Include the sigma parameter modal component -->
{% include 'mainapp/components/_sigma_parameter_modal.html' %}
<!-- LyngSat Data Modal -->
<div class="modal fade" id="lyngsatModal" tabindex="-1" aria-labelledby="lyngsatModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="lyngsatModalLabel">
<i class="bi bi-tv"></i> Данные источника LyngSat
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body" id="lyngsatModalBody">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
</div>
</div>
</div>
</div>
<script>
function showLyngsatModal(lyngsatId) {
// Показываем модальное окно
const modal = new bootstrap.Modal(document.getElementById('lyngsatModal'));
modal.show();
// Показываем индикатор загрузки
const modalBody = document.getElementById('lyngsatModalBody');
modalBody.innerHTML = `
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
</div>
`;
// Загружаем данные
fetch(`/api/lyngsat/${lyngsatId}/`)
.then(response => {
if (!response.ok) {
throw new Error('Ошибка загрузки данных');
}
return response.json();
})
.then(data => {
// Формируем HTML с данными
let html = `
<div class="container-fluid">
<div class="row g-3">
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-light">
<strong><i class="bi bi-info-circle"></i> Основная информация</strong>
</div>
<div class="card-body">
<table class="table table-sm table-borderless mb-0">
<tbody>
<tr>
<td class="text-muted" style="width: 40%;">Спутник:</td>
<td><strong>${data.satellite}</strong></td>
</tr>
<tr>
<td class="text-muted">Частота:</td>
<td><strong>${data.frequency} МГц</strong></td>
</tr>
<tr>
<td class="text-muted">Поляризация:</td>
<td><span class="badge bg-info">${data.polarization}</span></td>
</tr>
<tr>
<td class="text-muted">Канал:</td>
<td>${data.channel_info}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-light">
<strong><i class="bi bi-gear"></i> Технические параметры</strong>
</div>
<div class="card-body">
<table class="table table-sm table-borderless mb-0">
<tbody>
<tr>
<td class="text-muted" style="width: 40%;">Модуляция:</td>
<td><span class="badge bg-secondary">${data.modulation}</span></td>
</tr>
<tr>
<td class="text-muted">Стандарт:</td>
<td><span class="badge bg-secondary">${data.standard}</span></td>
</tr>
<tr>
<td class="text-muted">Сим. скорость:</td>
<td><strong>${data.sym_velocity} БОД</strong></td>
</tr>
<tr>
<td class="text-muted">FEC:</td>
<td>${data.fec}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-12">
<div class="card">
<div class="card-header bg-light">
<strong><i class="bi bi-clock-history"></i> Дополнительная информация</strong>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<p class="mb-2">
<span class="text-muted">Последнее обновление:</span><br>
<strong>${data.last_update}</strong>
</p>
</div>
<div class="col-md-6">
${data.url ? `
<p class="mb-2">
<span class="text-muted">Ссылка на источник:</span><br>
<a href="${data.url}" target="_blank" class="btn btn-sm btn-outline-primary">
<i class="bi bi-link-45deg"></i> Открыть на LyngSat
</a>
</p>
` : ''}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
modalBody.innerHTML = html;
})
.catch(error => {
modalBody.innerHTML = `
<div class="alert alert-danger" role="alert">
<i class="bi bi-exclamation-triangle"></i> ${error.message}
</div>
`;
});
}
</script>
{% endblock %} {% endblock %}

View File

@@ -20,6 +20,9 @@ urlpatterns = [
path('cluster/', views.ClusterTestView.as_view(), name='cluster'), path('cluster/', views.ClusterTestView.as_view(), name='cluster'),
path('vch-upload/', views.UploadVchLoadView.as_view(), name='vch_load'), path('vch-upload/', views.UploadVchLoadView.as_view(), name='vch_load'),
path('vch-link/', views.LinkVchSigmaView.as_view(), name='link_vch_sigma'), path('vch-link/', views.LinkVchSigmaView.as_view(), name='link_vch_sigma'),
path('link-lyngsat/', views.LinkLyngsatSourcesView.as_view(), name='link_lyngsat'),
path('api/lyngsat/<int:lyngsat_id>/', views.LyngsatDataAPIView.as_view(), name='lyngsat_data_api'),
path('api/sigma-parameter/<int:parameter_id>/', views.SigmaParameterDataAPIView.as_view(), name='sigma_parameter_data_api'),
path('kubsat-excel/', views.ProcessKubsatView.as_view(), name='kubsat_excel'), path('kubsat-excel/', views.ProcessKubsatView.as_view(), name='kubsat_excel'),
path('object/create/', views.ObjItemCreateView.as_view(), name='objitem_create'), path('object/create/', views.ObjItemCreateView.as_view(), name='objitem_create'),
path('object/<int:pk>/edit/', views.ObjItemUpdateView.as_view(), name='objitem_update'), path('object/<int:pk>/edit/', views.ObjItemUpdateView.as_view(), name='objitem_update'),
@@ -29,4 +32,5 @@ urlpatterns = [
path('lyngsat-task-status/', views.LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'), path('lyngsat-task-status/', views.LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'),
path('lyngsat-task-status/<str:task_id>/', views.LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'), path('lyngsat-task-status/<str:task_id>/', views.LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'),
path('api/lyngsat-task-status/<str:task_id>/', views.LyngsatTaskStatusAPIView.as_view(), name='lyngsat_task_status_api'), path('api/lyngsat-task-status/<str:task_id>/', views.LyngsatTaskStatusAPIView.as_view(), name='lyngsat_task_status_api'),
path('clear-lyngsat-cache/', views.ClearLyngsatCacheView.as_view(), name='clear_lyngsat_cache'),
] ]

View File

@@ -447,27 +447,96 @@ def get_vch_load_from_html(file, sat: Satellite) -> None:
sigma_load.save() sigma_load.save()
def get_frequency_tolerance_percent(freq_range_mhz: float) -> float:
"""
Определяет процент погрешности центральной частоты в зависимости от полосы частот.
Args:
freq_range_mhz (float): Полоса частот в МГц
Returns:
float: Процент погрешности для центральной частоты
Диапазоны:
- 0 - 0.5 МГц (0 - 500 кГц): 0.1%
- 0.5 - 1.5 МГц (500 кГц - 1.5 МГц): 0.5%
- 1.5 - 5 МГц: 1%
- 5 - 10 МГц: 2%
- > 10 МГц: 5%
"""
if freq_range_mhz < 0.5:
return 0.005
elif freq_range_mhz < 1.5:
return 0.01
elif freq_range_mhz < 5.0:
return 0.02
elif freq_range_mhz < 10.0:
return 0.05
else:
return 0.1
def compare_and_link_vch_load( def compare_and_link_vch_load(
sat_id: Satellite, eps_freq: float, eps_frange: float, ku_range: float sat_id: Satellite, eps_freq: float, eps_frange: float, ku_range: float
): ):
item_obj = ObjItem.objects.filter(parameters_obj__id_satellite=sat_id) """
vch_sigma = SigmaParameter.objects.filter(id_satellite=sat_id) Привязывает SigmaParameter к Parameter на основе совпадения параметров.
Погрешность центральной частоты определяется автоматически в зависимости от полосы частот:
- 0-500 кГц: 0.1%
- 500 кГц-1.5 МГц: 0.5%
- 1.5-5 МГц: 1%
- 5-10 МГц: 2%
- >10 МГц: 5%
Args:
sat_id (Satellite): Спутник для фильтрации
eps_freq (float): Не используется (оставлен для обратной совместимости)
eps_frange (float): Погрешность полосы частот в процентах
ku_range (float): Не используется (оставлен для обратной совместимости)
Returns:
tuple: (количество объектов, количество привязок)
"""
# Получаем все ObjItem с Parameter для данного спутника
item_obj = ObjItem.objects.filter(
parameter_obj__id_satellite=sat_id
).select_related('parameter_obj', 'parameter_obj__polarization')
vch_sigma = SigmaParameter.objects.filter(
id_satellite=sat_id
).select_related('polarization')
link_count = 0 link_count = 0
obj_count = len(item_obj) obj_count = item_obj.count()
for idx, obj in enumerate(item_obj):
vch_load = obj.parameters_obj.get() for obj in item_obj:
if vch_load.frequency == -1.0: vch_load = obj.parameter_obj
# Пропускаем объекты с некорректной частотой
if not vch_load or vch_load.frequency == -1.0:
continue continue
# Определяем погрешность частоты на основе полосы
freq_tolerance_percent = get_frequency_tolerance_percent(vch_load.freq_range)
# Вычисляем допустимое отклонение частоты в МГц
freq_tolerance_mhz = vch_load.frequency * freq_tolerance_percent / 100
# Вычисляем допустимое отклонение полосы в МГц
frange_tolerance_mhz = vch_load.freq_range * eps_frange / 100
for sigma in vch_sigma: for sigma in vch_sigma:
if ( # Проверяем совпадение по всем параметрам
abs(sigma.transfer_frequency - vch_load.frequency) <= eps_freq freq_match = abs(sigma.transfer_frequency - vch_load.frequency) <= freq_tolerance_mhz
and abs(sigma.freq_range - vch_load.freq_range) frange_match = abs(sigma.freq_range - vch_load.freq_range) <= frange_tolerance_mhz
<= vch_load.freq_range * eps_frange / 100 pol_match = sigma.polarization == vch_load.polarization
and sigma.polarization == vch_load.polarization
): if freq_match and frange_match and pol_match:
sigma.parameter = vch_load sigma.parameter = vch_load
sigma.save() sigma.save()
link_count += 1 link_count += 1
return obj_count, link_count return obj_count, link_count

View File

@@ -3,6 +3,7 @@ from collections import defaultdict
from io import BytesIO from io import BytesIO
# Django imports # Django imports
from django.utils import timezone
from django.contrib import messages from django.contrib import messages
from django.contrib.admin.views.decorators import staff_member_required from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth import logout from django.contrib.auth import logout
@@ -38,6 +39,7 @@ from .forms import (
UploadVchLoad, UploadVchLoad,
VchLinkForm, VchLinkForm,
FillLyngsatDataForm, FillLyngsatDataForm,
LinkLyngsatForm,
) )
from .mixins import CoordinateProcessingMixin, FormMessageMixin, RoleRequiredMixin from .mixins import CoordinateProcessingMixin, FormMessageMixin, RoleRequiredMixin
from .models import Geo, Modulation, ObjItem, Polarization, Satellite from .models import Geo, Modulation, ObjItem, Polarization, Satellite
@@ -360,12 +362,13 @@ class LinkVchSigmaView(LoginRequiredMixin, FormView):
form_class = VchLinkForm form_class = VchLinkForm
def form_valid(self, form): def form_valid(self, form):
freq = form.cleaned_data["value1"] # value1 больше не используется - погрешность частоты определяется автоматически
freq_range = form.cleaned_data["value2"] freq_range = form.cleaned_data["value2"]
# ku_range = float(form.cleaned_data['ku_range'])
sat_id = form.cleaned_data["sat_choice"] sat_id = form.cleaned_data["sat_choice"]
# print(freq, freq_range, ku_range, sat_id.pk)
count_all, link_count = compare_and_link_vch_load(sat_id, freq, freq_range, 1) # Передаём 0 для eps_freq и ku_range, так как они не используются
count_all, link_count = compare_and_link_vch_load(sat_id, 0, freq_range, 0)
messages.success( messages.success(
self.request, f"Привязано {link_count} из {count_all} объектов" self.request, f"Привязано {link_count} из {count_all} объектов"
) )
@@ -375,6 +378,186 @@ class LinkVchSigmaView(LoginRequiredMixin, FormView):
return self.render_to_response(self.get_context_data(form=form)) return self.render_to_response(self.get_context_data(form=form))
class LinkLyngsatSourcesView(LoginRequiredMixin, FormMessageMixin, FormView):
"""Представление для привязки источников LyngSat к объектам"""
template_name = "mainapp/link_lyngsat.html"
form_class = LinkLyngsatForm
success_message = "Привязка источников LyngSat завершена"
error_message = "Ошибка при привязке источников"
def form_valid(self, form):
from lyngsatapp.models import LyngSat
satellites = form.cleaned_data.get("satellites")
frequency_tolerance = form.cleaned_data.get("frequency_tolerance", 0.5)
# Если спутники не выбраны, обрабатываем все
if satellites:
objitems = ObjItem.objects.filter(
parameter_obj__id_satellite__in=satellites
).select_related('parameter_obj', 'parameter_obj__polarization')
else:
objitems = ObjItem.objects.filter(
parameter_obj__isnull=False
).select_related('parameter_obj', 'parameter_obj__polarization')
linked_count = 0
total_count = objitems.count()
for objitem in objitems:
if not hasattr(objitem, 'parameter_obj') or not objitem.parameter_obj:
continue
param = objitem.parameter_obj
# Округляем частоту объекта
if param.frequency:
rounded_freq = round(param.frequency, 0) # Округление до целого
# Ищем подходящий источник LyngSat
# Сравниваем по округленной частоте и поляризации
lyngsat_sources = LyngSat.objects.filter(
id_satellite=param.id_satellite,
polarization=param.polarization,
frequency__gte=rounded_freq - frequency_tolerance,
frequency__lte=rounded_freq + frequency_tolerance
).order_by('frequency')
if lyngsat_sources.exists():
# Берем первый подходящий источник
objitem.lyngsat_source = lyngsat_sources.first()
objitem.save(update_fields=['lyngsat_source'])
linked_count += 1
messages.success(
self.request,
f"Привязано {linked_count} из {total_count} объектов к источникам LyngSat"
)
return redirect("mainapp:link_lyngsat")
def form_invalid(self, form):
return self.render_to_response(self.get_context_data(form=form))
class LyngsatDataAPIView(LoginRequiredMixin, View):
"""API для получения данных LyngSat источника"""
def get(self, request, lyngsat_id):
from lyngsatapp.models import LyngSat
try:
lyngsat = LyngSat.objects.select_related(
'id_satellite',
'polarization',
'modulation',
'standard'
).get(id=lyngsat_id)
# Форматируем дату с учетом локального времени
last_update_str = '-'
if lyngsat.last_update:
local_time = timezone.localtime(lyngsat.last_update)
last_update_str = local_time.strftime("%d.%m.%Y")
data = {
'id': lyngsat.id,
'satellite': lyngsat.id_satellite.name if lyngsat.id_satellite else '-',
'frequency': f"{lyngsat.frequency:.3f}" if lyngsat.frequency else '-',
'polarization': lyngsat.polarization.name if lyngsat.polarization else '-',
'modulation': lyngsat.modulation.name if lyngsat.modulation else '-',
'standard': lyngsat.standard.name if lyngsat.standard else '-',
'sym_velocity': f"{lyngsat.sym_velocity:.0f}" if lyngsat.sym_velocity else '-',
'fec': lyngsat.fec or '-',
'channel_info': lyngsat.channel_info or '-',
'last_update': last_update_str,
'url': lyngsat.url or None,
}
return JsonResponse(data)
except LyngSat.DoesNotExist:
return JsonResponse({'error': 'Источник LyngSat не найден'}, status=404)
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
class SigmaParameterDataAPIView(LoginRequiredMixin, View):
"""API для получения данных SigmaParameter"""
def get(self, request, parameter_id):
from .models import Parameter
try:
parameter = Parameter.objects.select_related(
'id_satellite',
'polarization',
'modulation',
'standard'
).prefetch_related(
'sigma_parameter__mark',
'sigma_parameter__id_satellite',
'sigma_parameter__polarization',
'sigma_parameter__modulation',
'sigma_parameter__standard'
).get(id=parameter_id)
# Получаем все связанные SigmaParameter
sigma_params = parameter.sigma_parameter.all()
sigma_data = []
for sigma in sigma_params:
# Получаем отметки
marks = []
for mark in sigma.mark.all().order_by('-timestamp'):
mark_str = '+' if mark.mark else '-'
date_str = '-'
if mark.timestamp:
local_time = timezone.localtime(mark.timestamp)
date_str = local_time.strftime("%d.%m.%Y %H:%M")
marks.append({
'mark': mark_str,
'date': date_str
})
# Форматируем даты начала и окончания
datetime_begin_str = '-'
if sigma.datetime_begin:
local_time = timezone.localtime(sigma.datetime_begin)
datetime_begin_str = local_time.strftime("%d.%m.%Y %H:%M")
datetime_end_str = '-'
if sigma.datetime_end:
local_time = timezone.localtime(sigma.datetime_end)
datetime_end_str = local_time.strftime("%d.%m.%Y %H:%M")
sigma_data.append({
'id': sigma.id,
'satellite': sigma.id_satellite.name if sigma.id_satellite else '-',
'frequency': f"{sigma.frequency:.3f}" if sigma.frequency else '-',
'transfer_frequency': f"{sigma.transfer_frequency:.3f}" if sigma.transfer_frequency else '-',
'freq_range': f"{sigma.freq_range:.3f}" if sigma.freq_range else '-',
'polarization': sigma.polarization.name if sigma.polarization else '-',
'modulation': sigma.modulation.name if sigma.modulation else '-',
'standard': sigma.standard.name if sigma.standard else '-',
'bod_velocity': f"{sigma.bod_velocity:.0f}" if sigma.bod_velocity else '-',
'snr': f"{sigma.snr:.1f}" if sigma.snr is not None else '-',
'power': f"{sigma.power:.1f}" if sigma.power is not None else '-',
'status': sigma.status or '-',
'packets': 'Да' if sigma.packets else 'Нет' if sigma.packets is not None else '-',
'datetime_begin': datetime_begin_str,
'datetime_end': datetime_end_str,
'marks': marks
})
return JsonResponse({
'parameter_id': parameter.id,
'sigma_parameters': sigma_data
})
except Parameter.DoesNotExist:
return JsonResponse({'error': 'Parameter не найден'}, status=404)
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
class ProcessKubsatView(LoginRequiredMixin, FormMessageMixin, FormView): class ProcessKubsatView(LoginRequiredMixin, FormMessageMixin, FormView):
template_name = "mainapp/process_kubsat.html" template_name = "mainapp/process_kubsat.html"
form_class = NewEventForm form_class = NewEventForm
@@ -474,12 +657,17 @@ class ObjItemListView(LoginRequiredMixin, View):
"geo_obj", "geo_obj",
"updated_by__user", "updated_by__user",
"created_by__user", "created_by__user",
"lyngsat_source",
"parameter_obj", "parameter_obj",
"parameter_obj__id_satellite", "parameter_obj__id_satellite",
"parameter_obj__polarization", "parameter_obj__polarization",
"parameter_obj__modulation", "parameter_obj__modulation",
"parameter_obj__standard", "parameter_obj__standard",
) )
.prefetch_related(
"parameter_obj__sigma_parameter",
"parameter_obj__sigma_parameter__polarization",
)
.filter(parameter_obj__id_satellite_id__in=selected_satellites) .filter(parameter_obj__id_satellite_id__in=selected_satellites)
) )
else: else:
@@ -487,11 +675,15 @@ class ObjItemListView(LoginRequiredMixin, View):
"geo_obj", "geo_obj",
"updated_by__user", "updated_by__user",
"created_by__user", "created_by__user",
"lyngsat_source",
"parameter_obj", "parameter_obj",
"parameter_obj__id_satellite", "parameter_obj__id_satellite",
"parameter_obj__polarization", "parameter_obj__polarization",
"parameter_obj__modulation", "parameter_obj__modulation",
"parameter_obj__standard", "parameter_obj__standard",
).prefetch_related(
"parameter_obj__sigma_parameter",
"parameter_obj__sigma_parameter__polarization",
) )
if freq_min is not None and freq_min.strip() != "": if freq_min is not None and freq_min.strip() != "":
@@ -763,6 +955,26 @@ class ObjItemListView(LoginRequiredMixin, View):
comment = obj.geo_obj.comment or "-" comment = obj.geo_obj.comment or "-"
is_average = "Да" if obj.geo_obj.is_average else "Нет" if obj.geo_obj.is_average is not None else "-" is_average = "Да" if obj.geo_obj.is_average else "Нет" if obj.geo_obj.is_average is not None else "-"
# Check if LyngSat source is linked
source_type = "ТВ" if obj.lyngsat_source else "-"
# Check if SigmaParameter is linked
has_sigma = False
sigma_info = "-"
if param:
sigma_count = param.sigma_parameter.count()
if sigma_count > 0:
has_sigma = True
# Get first sigma parameter for preview
first_sigma = param.sigma_parameter.first()
if first_sigma:
sigma_freq = f"{first_sigma.frequency:.3f}" if first_sigma.frequency else "-"
sigma_range = f"{first_sigma.freq_range:.3f}" if first_sigma.freq_range else "-"
sigma_pol = first_sigma.polarization.name if first_sigma.polarization else "-"
# Сокращаем поляризацию
sigma_pol_short = sigma_pol[0] if sigma_pol and sigma_pol != "-" else "-"
sigma_info = f"{sigma_freq}/{sigma_range}/{sigma_pol_short}"
processed_objects.append( processed_objects.append(
{ {
"id": obj.id, "id": obj.id,
@@ -785,7 +997,10 @@ class ObjItemListView(LoginRequiredMixin, View):
"updated_by": obj.updated_by if obj.updated_by else "-", "updated_by": obj.updated_by if obj.updated_by else "-",
"comment": comment, "comment": comment,
"is_average": is_average, "is_average": is_average,
"source_type": source_type,
"standard": standard_name, "standard": standard_name,
"has_sigma": has_sigma,
"sigma_info": sigma_info,
"obj": obj, "obj": obj,
} }
) )
@@ -1047,6 +1262,8 @@ class FillLyngsatDataView(LoginRequiredMixin, FormMessageMixin, FormView):
def form_valid(self, form): def form_valid(self, form):
satellites = form.cleaned_data["satellites"] satellites = form.cleaned_data["satellites"]
regions = form.cleaned_data["regions"] regions = form.cleaned_data["regions"]
use_cache = form.cleaned_data.get("use_cache", True)
force_refresh = form.cleaned_data.get("force_refresh", False)
# Получаем названия спутников # Получаем названия спутников
target_sats = [sat.name for sat in satellites] target_sats = [sat.name for sat in satellites]
@@ -1054,12 +1271,19 @@ class FillLyngsatDataView(LoginRequiredMixin, FormMessageMixin, FormView):
try: try:
from lyngsatapp.tasks import fill_lyngsat_data_task from lyngsatapp.tasks import fill_lyngsat_data_task
# Запускаем асинхронную задачу # Запускаем асинхронную задачу с параметрами кеширования
task = fill_lyngsat_data_task.delay(target_sats, regions) task = fill_lyngsat_data_task.delay(
target_sats,
regions,
force_refresh=force_refresh,
use_cache=use_cache
)
cache_status = "без кеша" if not use_cache else ("с обновлением кеша" if force_refresh else "с кешированием")
messages.success( messages.success(
self.request, self.request,
f"Задача запущена! ID задачи: {task.id}. " f"Задача запущена ({cache_status})! ID задачи: {task.id}. "
"Вы будете перенаправлены на страницу отслеживания прогресса." "Вы будете перенаправлены на страницу отслеживания прогресса."
) )
@@ -1124,3 +1348,30 @@ class LyngsatTaskStatusAPIView(LoginRequiredMixin, View):
response_data['status'] = task.state response_data['status'] = task.state
return JsonResponse(response_data) return JsonResponse(response_data)
class ClearLyngsatCacheView(LoginRequiredMixin, View):
"""
Представление для очистки кеша LyngSat.
"""
def post(self, request):
from lyngsatapp.tasks import clear_cache_task
cache_type = request.POST.get('cache_type', 'all')
try:
# Запускаем задачу очистки кеша
task = clear_cache_task.delay(cache_type)
messages.success(
request,
f"Задача очистки кеша ({cache_type}) запущена! ID задачи: {task.id}"
)
except Exception as e:
messages.error(request, f"Ошибка при запуске задачи очистки кеша: {str(e)}")
return redirect(request.META.get('HTTP_REFERER', 'mainapp:home'))
def get(self, request):
"""Страница управления кешем"""
return render(request, 'mainapp/clear_lyngsat_cache.html')

View File

@@ -25,26 +25,33 @@ class CesiumMapView(LoginRequiredMixin, TemplateView):
Отображает спутники и их зоны покрытия на интерактивной 3D карте. Отображает спутники и их зоны покрытия на интерактивной 3D карте.
""" """
template_name = 'mapsapp/map3d.html'
template_name = "mapsapp/map3d.html"
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
# Оптимизированный запрос - загружаем только необходимые поля # Оптимизированный запрос - загружаем только необходимые поля
context['sats'] = Satellite.objects.filter( # Фильтруем спутники, у которых есть параметры с привязанными объектами
parameters__objitems__isnull=False context["sats"] = (
).distinct().only('id', 'name').order_by('name') Satellite.objects.filter(parameters__objitem__isnull=False)
.distinct()
.only("id", "name")
.order_by("name")
)
return context return context
class GetFootprintsView(LoginRequiredMixin, View): class GetFootprintsView(LoginRequiredMixin, View):
""" """
API для получения зон покрытия (footprints) спутника. API для получения зон покрытия (footprints) спутника.
Возвращает список названий зон покрытия для указанного спутника. Возвращает список названий зон покрытия для указанного спутника.
""" """
def get(self, request, sat_id): def get(self, request, sat_id):
try: try:
# Оптимизированный запрос - загружаем только поле name # Оптимизированный запрос - загружаем только поле name
sat_name = Satellite.objects.only('name').get(id=sat_id).name sat_name = Satellite.objects.only("name").get(id=sat_id).name
footprint_names = get_band_names(sat_name) footprint_names = get_band_names(sat_name)
return JsonResponse(footprint_names, safe=False) return JsonResponse(footprint_names, safe=False)
@@ -60,6 +67,7 @@ class TileProxyView(View):
Кэширует тайлы на 7 дней для улучшения производительности. Кэширует тайлы на 7 дней для улучшения производительности.
""" """
# Константы # Константы
TILE_BASE_URL = "https://static.satbeams.com/tiles" TILE_BASE_URL = "https://static.satbeams.com/tiles"
CACHE_DURATION = 60 * 60 * 24 * 7 # 7 дней CACHE_DURATION = 60 * 60 * 24 * 7 # 7 дней
@@ -72,7 +80,7 @@ class TileProxyView(View):
def get(self, request, footprint_name, z, x, y): def get(self, request, footprint_name, z, x, y):
# Валидация имени footprint # Валидация имени footprint
if not footprint_name.replace('-', '').replace('_', '').isalnum(): if not footprint_name.replace("-", "").replace("_", "").isalnum():
return HttpResponse("Invalid footprint name", status=400) return HttpResponse("Invalid footprint name", status=400)
url = f"{self.TILE_BASE_URL}/{footprint_name}/{z}/{x}/{y}.png" url = f"{self.TILE_BASE_URL}/{footprint_name}/{z}/{x}/{y}.png"
@@ -80,7 +88,7 @@ class TileProxyView(View):
try: try:
resp = requests.get(url, timeout=self.REQUEST_TIMEOUT) resp = requests.get(url, timeout=self.REQUEST_TIMEOUT)
if resp.status_code == 200: if resp.status_code == 200:
response = HttpResponse(resp.content, content_type='image/png') response = HttpResponse(resp.content, content_type="image/png")
response["Access-Control-Allow-Origin"] = "*" response["Access-Control-Allow-Origin"] = "*"
response["Cache-Control"] = f"public, max-age={self.CACHE_DURATION}" response["Cache-Control"] = f"public, max-age={self.CACHE_DURATION}"
return response return response
@@ -91,26 +99,37 @@ class TileProxyView(View):
except requests.RequestException as e: except requests.RequestException as e:
return HttpResponse(f"Proxy error: {e}", status=500) return HttpResponse(f"Proxy error: {e}", status=500)
class LeafletMapView(LoginRequiredMixin, TemplateView): class LeafletMapView(LoginRequiredMixin, TemplateView):
""" """
Представление для отображения 2D карты с использованием Leaflet. Представление для отображения 2D карты с использованием Leaflet.
Отображает спутники и транспондеры на интерактивной 2D карте. Отображает спутники и транспондеры на интерактивной 2D карте.
""" """
template_name = 'mapsapp/map2d.html'
template_name = "mapsapp/map2d.html"
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
# Оптимизированные запросы - загружаем только необходимые поля # Оптимизированные запросы - загружаем только необходимые поля
context['sats'] = Satellite.objects.filter( # Фильтруем спутники, у которых есть параметры с привязанными объектами
parameters__objitems__isnull=False context["sats"] = (
).distinct().only('id', 'name').order_by('name') Satellite.objects.filter(parameters__objitem__isnull=False)
.distinct()
.only("id", "name")
.order_by("name")
)
context['trans'] = Transponders.objects.select_related( context["trans"] = Transponders.objects.select_related(
'sat_id', 'polarization' "sat_id", "polarization"
).only( ).only(
'id', 'name', 'sat_id__name', 'polarization__name', "id",
'downlink', 'frequency_range', 'zone_name' "name",
"sat_id__name",
"polarization__name",
"downlink",
"frequency_range",
"zone_name",
) )
return context return context
@@ -121,17 +140,19 @@ class GetTransponderOnSatIdView(LoginRequiredMixin, View):
Возвращает список транспондеров для указанного спутника с оптимизированными запросами. Возвращает список транспондеров для указанного спутника с оптимизированными запросами.
""" """
def get(self, request, sat_id): def get(self, request, sat_id):
# Оптимизированный запрос с select_related и only # Оптимизированный запрос с select_related и only
trans = Transponders.objects.filter( trans = (
sat_id=sat_id Transponders.objects.filter(sat_id=sat_id)
).select_related('polarization').only( .select_related("polarization")
'name', 'downlink', 'frequency_range', .only(
'zone_name', 'polarization__name' "name", "downlink", "frequency_range", "zone_name", "polarization__name"
)
) )
if not trans.exists(): if not trans.exists():
return JsonResponse({'error': 'Объектов не найдено'}, status=404) return JsonResponse({"error": "Объектов не найдено"}, status=404)
# Используем list comprehension для лучшей производительности # Используем list comprehension для лучшей производительности
output = [ output = [
@@ -140,7 +161,7 @@ class GetTransponderOnSatIdView(LoginRequiredMixin, View):
"frequency": tran.downlink, "frequency": tran.downlink,
"frequency_range": tran.frequency_range, "frequency_range": tran.frequency_range,
"zone_name": tran.zone_name, "zone_name": tran.zone_name,
"polarization": tran.polarization.name if tran.polarization else "-" "polarization": tran.polarization.name if tran.polarization else "-",
} }
for tran in trans for tran in trans
] ]

View File

@@ -35,6 +35,7 @@ dependencies = [
"psycopg>=3.2.10", "psycopg>=3.2.10",
"psycopg2-binary>=2.9.11", "psycopg2-binary>=2.9.11",
"redis>=6.4.0", "redis>=6.4.0",
"django-redis>=5.4.0",
"requests>=2.32.5", "requests>=2.32.5",
"reverse-geocoder>=1.5.1", "reverse-geocoder>=1.5.1",
"scikit-learn>=1.7.2", "scikit-learn>=1.7.2",

15
dbapp/uv.lock generated
View File

@@ -368,6 +368,7 @@ dependencies = [
{ name = "django-leaflet" }, { name = "django-leaflet" },
{ name = "django-map-widgets" }, { name = "django-map-widgets" },
{ name = "django-more-admin-filters" }, { name = "django-more-admin-filters" },
{ name = "django-redis" },
{ name = "dotenv" }, { name = "dotenv" },
{ name = "flower" }, { name = "flower" },
{ name = "geopy" }, { name = "geopy" },
@@ -407,6 +408,7 @@ requires-dist = [
{ name = "django-leaflet", specifier = ">=0.32.0" }, { name = "django-leaflet", specifier = ">=0.32.0" },
{ name = "django-map-widgets", specifier = ">=0.5.1" }, { name = "django-map-widgets", specifier = ">=0.5.1" },
{ name = "django-more-admin-filters", specifier = ">=1.13" }, { name = "django-more-admin-filters", specifier = ">=1.13" },
{ name = "django-redis", specifier = ">=5.4.0" },
{ name = "dotenv", specifier = ">=0.9.9" }, { name = "dotenv", specifier = ">=0.9.9" },
{ name = "flower", specifier = ">=2.0.1" }, { name = "flower", specifier = ">=2.0.1" },
{ name = "geopy", specifier = ">=2.4.1" }, { name = "geopy", specifier = ">=2.4.1" },
@@ -615,6 +617,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/87/7c/4b261b96b357d94ef267f39856ef0bb72a33f078a38bd22ee96d168fe272/django_more_admin_filters-1.13-py3-none-any.whl", hash = "sha256:df4d46e4b589566b85f149ea5b7558c6cc4ae22b0d264973f8d4a2d478ef5120", size = 147360, upload-time = "2025-06-06T11:26:42.964Z" }, { url = "https://files.pythonhosted.org/packages/87/7c/4b261b96b357d94ef267f39856ef0bb72a33f078a38bd22ee96d168fe272/django_more_admin_filters-1.13-py3-none-any.whl", hash = "sha256:df4d46e4b589566b85f149ea5b7558c6cc4ae22b0d264973f8d4a2d478ef5120", size = 147360, upload-time = "2025-06-06T11:26:42.964Z" },
] ]
[[package]]
name = "django-redis"
version = "6.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
{ name = "redis" },
]
sdist = { url = "https://files.pythonhosted.org/packages/08/53/dbcfa1e528e0d6c39947092625b2c89274b5d88f14d357cee53c4d6dbbd4/django_redis-6.0.0.tar.gz", hash = "sha256:2d9cb12a20424a4c4dde082c6122f486628bae2d9c2bee4c0126a4de7fda00dd", size = 56904, upload-time = "2025-06-17T18:15:46.376Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/79/055dfcc508cfe9f439d9f453741188d633efa9eab90fc78a67b0ab50b137/django_redis-6.0.0-py3-none-any.whl", hash = "sha256:20bf0063a8abee567eb5f77f375143c32810c8700c0674ced34737f8de4e36c0", size = 33687, upload-time = "2025-06-17T18:15:34.165Z" },
]
[[package]] [[package]]
name = "django-timezone-field" name = "django-timezone-field"
version = "7.1" version = "7.1"