Compare commits
3 Commits
4f21c9d7c8
...
902eb23bd8
| Author | SHA1 | Date | |
|---|---|---|---|
| 902eb23bd8 | |||
| 5e94086bf0 | |||
| a3c381b9c7 |
@@ -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
|
|
||||||
```
|
|
||||||
@@ -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
|
||||||
|
|||||||
564
dbapp/lyngsatapp/async_parser.py
Normal file
564
dbapp/lyngsatapp/async_parser.py
Normal 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
|
||||||
287
dbapp/lyngsatapp/async_utils.py
Normal file
287
dbapp/lyngsatapp/async_utils.py
Normal 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
|
||||||
0
dbapp/lyngsatapp/management/__init__.py
Normal file
0
dbapp/lyngsatapp/management/__init__.py
Normal file
0
dbapp/lyngsatapp/management/commands/__init__.py
Normal file
0
dbapp/lyngsatapp/management/commands/__init__.py
Normal file
40
dbapp/lyngsatapp/management/commands/clear_lyngsat_cache.py
Normal file
40
dbapp/lyngsatapp/management/commands/clear_lyngsat_cache.py
Normal 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}'))
|
||||||
@@ -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]
|
||||||
|
}
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
@@ -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):
|
||||||
"""
|
"""
|
||||||
Форма для создания и редактирования параметров ВЧ загрузки.
|
Форма для создания и редактирования параметров ВЧ загрузки.
|
||||||
|
|||||||
@@ -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',
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 %}
|
||||||
113
dbapp/mainapp/templates/mainapp/clear_lyngsat_cache.html
Normal file
113
dbapp/mainapp/templates/mainapp/clear_lyngsat_cache.html
Normal 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 %}
|
||||||
@@ -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>
|
||||||
@@ -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">
|
||||||
|
|||||||
90
dbapp/mainapp/templates/mainapp/link_lyngsat.html
Normal file
90
dbapp/mainapp/templates/mainapp/link_lyngsat.html
Normal 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 %}
|
||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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 %}
|
||||||
@@ -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'),
|
||||||
]
|
]
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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
15
dbapp/uv.lock
generated
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user