Настроил сеелери, начал привязку lyngsat

This commit is contained in:
2025-11-11 17:23:36 +03:00
parent 65e6c9a323
commit 4f21c9d7c8
110 changed files with 34270 additions and 33631 deletions

View File

@@ -1,60 +1,60 @@
# Git
.git
.gitignore
.gitattributes
# Python
__pycache__
*.py[cod]
*$py.class
*.so
.Python
*.egg-info
dist/
build/
*.egg
# Virtual environments
venv/
env/
ENV/
.venv
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Django
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
/staticfiles/
/media/
# Environment
.env
.env.local
.env.*.local
# Testing
.pytest_cache/
.coverage
htmlcov/
.tox/
# Documentation
*.md
docs/
# OS
.DS_Store
Thumbs.db
# Docker
Dockerfile*
docker-compose*.yaml
.dockerignore
# Git
.git
.gitignore
.gitattributes
# Python
__pycache__
*.py[cod]
*$py.class
*.so
.Python
*.egg-info
dist/
build/
*.egg
# Virtual environments
venv/
env/
ENV/
.venv
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Django
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
/staticfiles/
/media/
# Environment
.env
.env.local
.env.*.local
# Testing
.pytest_cache/
.coverage
htmlcov/
.tox/
# Documentation
*.md
docs/
# OS
.DS_Store
Thumbs.db
# Docker
Dockerfile*
docker-compose*.yaml
.dockerignore

View File

@@ -1,10 +1,10 @@
# Production environment variables
DEBUG=False
ENVIRONMENT=production
SECRET_KEY=your_very_long_secret_key_here_change_this_to_something_secure
DB_NAME=geodb
DB_USER=geralt
DB_PASSWORD=123456
DB_HOST=db
DB_PORT=5432
# Production environment variables
DEBUG=False
ENVIRONMENT=production
SECRET_KEY=your_very_long_secret_key_here_change_this_to_something_secure
DB_NAME=geodb
DB_USER=geralt
DB_PASSWORD=123456
DB_HOST=db
DB_PORT=5432
ALLOWED_HOSTS=localhost,yourdomain.com

1
dbapp/.python-version Normal file
View File

@@ -0,0 +1 @@
3.13.7

217
dbapp/CELERY_SETUP.md Normal file
View File

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

View File

@@ -1,57 +1,57 @@
FROM python:3.13-slim
# Install system dependencies
RUN apt-get update && apt-get install -y \
gdal-bin \
libgdal-dev \
proj-bin \
proj-data \
libproj-dev \
libproj25 \
libgeos-dev \
libgeos-c1v5 \
build-essential \
postgresql-client \
libpq-dev \
libpq5 \
netcat-openbsd \
gcc \
g++ \
&& rm -rf /var/lib/apt/lists/*
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# Set work directory
WORKDIR /app
# Upgrade pip
RUN pip install --upgrade pip
# Copy requirements file
COPY requirements.txt ./
# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy project files
COPY . .
# Create directories
RUN mkdir -p /app/staticfiles /app/logs /app/media
# Set permissions for entrypoint
RUN chmod +x /app/entrypoint.sh
# Create non-root user
RUN useradd --create-home --shell /bin/bash app && \
chown -R app:app /app
USER app
# Expose port
EXPOSE 8000
# Run entrypoint script
ENTRYPOINT ["/app/entrypoint.sh"]
FROM python:3.13-slim
# Install system dependencies
RUN apt-get update && apt-get install -y \
gdal-bin \
libgdal-dev \
proj-bin \
proj-data \
libproj-dev \
libproj25 \
libgeos-dev \
libgeos-c1v5 \
build-essential \
postgresql-client \
libpq-dev \
libpq5 \
netcat-openbsd \
gcc \
g++ \
&& rm -rf /var/lib/apt/lists/*
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# Set work directory
WORKDIR /app
# Upgrade pip
RUN pip install --upgrade pip
# Copy requirements file
COPY requirements.txt ./
# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy project files
COPY . .
# Create directories
RUN mkdir -p /app/staticfiles /app/logs /app/media
# Set permissions for entrypoint
RUN chmod +x /app/entrypoint.sh
# Create non-root user
RUN useradd --create-home --shell /bin/bash app && \
chown -R app:app /app
USER app
# Expose port
EXPOSE 8000
# Run entrypoint script
ENTRYPOINT ["/app/entrypoint.sh"]

View File

@@ -1,8 +1,7 @@
# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
try:
from .celery import app as celery_app
__all__ = ('celery_app',)
except ImportError:
# Celery is not installed, skip initialization
pass
# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
try:
from .celery import app as celery_app
__all__ = ('celery_app',)
except ImportError:
pass

View File

@@ -1,16 +1,16 @@
"""
ASGI config for dbapp project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dbapp.settings')
application = get_asgi_application()
"""
ASGI config for dbapp project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dbapp.settings')
application = get_asgi_application()

View File

@@ -4,8 +4,8 @@ Celery configuration for dbapp project.
import os
from celery import Celery
# Set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dbapp.settings.development')
# Use the environment variable to determine the settings module
os.environ.setdefault('DJANGO_SETTINGS_MODULE', os.getenv('DJANGO_SETTINGS_MODULE', 'dbapp.settings.development'))
app = Celery('dbapp')

View File

@@ -1,25 +1,25 @@
"""
Settings module initialization.
Automatically determines the environment and loads appropriate settings.
Set DJANGO_ENVIRONMENT environment variable to 'production' or 'development'.
Defaults to 'development' if not set.
"""
import os
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
# Determine the environment from DJANGO_ENVIRONMENT variable
# Defaults to 'development' for safety
ENVIRONMENT = os.getenv('DJANGO_ENVIRONMENT', 'development').lower()
if ENVIRONMENT == 'production':
from .production import *
print("Loading production settings...")
else:
from .development import *
"""
Settings module initialization.
Automatically determines the environment and loads appropriate settings.
Set DJANGO_ENVIRONMENT environment variable to 'production' or 'development'.
Defaults to 'development' if not set.
"""
import os
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
# Determine the environment from DJANGO_ENVIRONMENT variable
# Defaults to 'development' for safety
ENVIRONMENT = os.getenv('DJANGO_ENVIRONMENT', 'development').lower()
if ENVIRONMENT == 'production':
from .production import *
print("Loading production settings...")
else:
from .development import *
print("Loading development settings...")

View File

@@ -73,22 +73,13 @@ INSTALLED_APPS = [
"django_admin_multiple_choice_list_filter",
"more_admin_filters",
"import_export",
"django_celery_results",
# Project apps
"mainapp",
"mapsapp",
"lyngsatapp",
]
# Add Celery results app if available
try:
import django_celery_results
INSTALLED_APPS.append("django_celery_results")
except ImportError:
pass
# Note: Custom user model is implemented via OneToOneField relationship
# If you need a custom user model, uncomment and configure:
# AUTH_USER_MODEL = 'mainapp.CustomUser'
# ============================================================================
@@ -240,7 +231,7 @@ LEAFLET_CONFIG = {
# Celery Configuration Options
CELERY_BROKER_URL = os.getenv("CELERY_BROKER_URL", "redis://localhost:6379/0")
CELERY_RESULT_BACKEND = "django-db"
CELERY_RESULT_BACKEND = os.getenv("CELERY_BROKER_URL", "redis://localhost:6379/0") # Use Redis for results
CELERY_CACHE_BACKEND = "default"
# Celery Task Configuration

View File

@@ -1,48 +1,48 @@
"""
Development-specific settings.
"""
from .base import *
# ============================================================================
# DEBUG CONFIGURATION
# ============================================================================
DEBUG = True
# ============================================================================
# ALLOWED HOSTS
# ============================================================================
# Allow all hosts in development
ALLOWED_HOSTS = ['*']
# ============================================================================
# INSTALLED APPS - Development additions
# ============================================================================
INSTALLED_APPS += [
'debug_toolbar',
]
# ============================================================================
# MIDDLEWARE - Development additions
# ============================================================================
# Add debug toolbar middleware at the beginning
MIDDLEWARE = ['debug_toolbar.middleware.DebugToolbarMiddleware'] + MIDDLEWARE
# ============================================================================
# DEBUG TOOLBAR CONFIGURATION
# ============================================================================
INTERNAL_IPS = [
'127.0.0.1',
]
# ============================================================================
# EMAIL CONFIGURATION
# ============================================================================
# Use console backend for development
"""
Development-specific settings.
"""
from .base import *
# ============================================================================
# DEBUG CONFIGURATION
# ============================================================================
DEBUG = True
# ============================================================================
# ALLOWED HOSTS
# ============================================================================
# Allow all hosts in development
ALLOWED_HOSTS = ['*']
# ============================================================================
# INSTALLED APPS - Development additions
# ============================================================================
INSTALLED_APPS += [
'debug_toolbar',
]
# ============================================================================
# MIDDLEWARE - Development additions
# ============================================================================
# Add debug toolbar middleware at the beginning
MIDDLEWARE = ['debug_toolbar.middleware.DebugToolbarMiddleware'] + MIDDLEWARE
# ============================================================================
# DEBUG TOOLBAR CONFIGURATION
# ============================================================================
INTERNAL_IPS = [
'127.0.0.1',
]
# ============================================================================
# EMAIL CONFIGURATION
# ============================================================================
# Use console backend for development
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

View File

@@ -1,135 +1,135 @@
"""
Production-specific settings.
"""
import os
from .base import *
# ============================================================================
# DEBUG CONFIGURATION
# ============================================================================
DEBUG = False
# ============================================================================
# ALLOWED HOSTS
# ============================================================================
# In production, specify allowed hosts explicitly from environment variable
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")
# ============================================================================
# SECURITY SETTINGS
# ============================================================================
# SSL/HTTPS settings
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# Security headers
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
# HSTS settings
SECURE_HSTS_SECONDS = 31536000 # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# Additional security settings
SECURE_REDIRECT_EXEMPT = []
X_FRAME_OPTIONS = "DENY"
# ============================================================================
# TEMPLATE CACHING
# ============================================================================
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [
BASE_DIR / "templates",
],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
"loaders": [
(
"django.template.loaders.cached.Loader",
[
"django.template.loaders.filesystem.Loader",
"django.template.loaders.app_directories.Loader",
],
),
],
},
},
]
# ============================================================================
# STATIC FILES CONFIGURATION
# ============================================================================
STATIC_ROOT = BASE_DIR.parent / "staticfiles"
STATICFILES_STORAGE = "django.contrib.staticfiles.storage.ManifestStaticFilesStorage"
# ============================================================================
# LOGGING CONFIGURATION
# ============================================================================
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
"style": "{",
},
"simple": {
"format": "{levelname} {message}",
"style": "{",
},
},
"filters": {
"require_debug_false": {
"()": "django.utils.log.RequireDebugFalse",
},
},
"handlers": {
"console": {
"level": "INFO",
"class": "logging.StreamHandler",
"formatter": "simple",
},
"file": {
"level": "ERROR",
"class": "logging.FileHandler",
"filename": BASE_DIR.parent / "logs" / "django_errors.log",
"formatter": "verbose",
},
"mail_admins": {
"level": "ERROR",
"class": "django.utils.log.AdminEmailHandler",
"filters": ["require_debug_false"],
"formatter": "verbose",
},
},
"loggers": {
"django": {
"handlers": ["console", "file"],
"level": "INFO",
"propagate": True,
},
"django.request": {
"handlers": ["mail_admins", "file"],
"level": "ERROR",
"propagate": False,
},
},
}
"""
Production-specific settings.
"""
import os
from .base import *
# ============================================================================
# DEBUG CONFIGURATION
# ============================================================================
DEBUG = False
# ============================================================================
# ALLOWED HOSTS
# ============================================================================
# In production, specify allowed hosts explicitly from environment variable
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")
# ============================================================================
# SECURITY SETTINGS
# ============================================================================
# SSL/HTTPS settings
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# Security headers
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
# HSTS settings
SECURE_HSTS_SECONDS = 31536000 # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# Additional security settings
SECURE_REDIRECT_EXEMPT = []
X_FRAME_OPTIONS = "DENY"
# ============================================================================
# TEMPLATE CACHING
# ============================================================================
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [
BASE_DIR / "templates",
],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
"loaders": [
(
"django.template.loaders.cached.Loader",
[
"django.template.loaders.filesystem.Loader",
"django.template.loaders.app_directories.Loader",
],
),
],
},
},
]
# ============================================================================
# STATIC FILES CONFIGURATION
# ============================================================================
STATIC_ROOT = BASE_DIR.parent / "staticfiles"
STATICFILES_STORAGE = "django.contrib.staticfiles.storage.ManifestStaticFilesStorage"
# ============================================================================
# LOGGING CONFIGURATION
# ============================================================================
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
"style": "{",
},
"simple": {
"format": "{levelname} {message}",
"style": "{",
},
},
"filters": {
"require_debug_false": {
"()": "django.utils.log.RequireDebugFalse",
},
},
"handlers": {
"console": {
"level": "INFO",
"class": "logging.StreamHandler",
"formatter": "simple",
},
"file": {
"level": "ERROR",
"class": "logging.FileHandler",
"filename": BASE_DIR.parent / "logs" / "django_errors.log",
"formatter": "verbose",
},
"mail_admins": {
"level": "ERROR",
"class": "django.utils.log.AdminEmailHandler",
"filters": ["require_debug_false"],
"formatter": "verbose",
},
},
"loggers": {
"django": {
"handlers": ["console", "file"],
"level": "INFO",
"propagate": True,
},
"django.request": {
"handlers": ["mail_admins", "file"],
"level": "ERROR",
"propagate": False,
},
},
}

View File

@@ -1,30 +1,30 @@
"""
URL configuration for dbapp project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from mainapp import views
from django.contrib.auth import views as auth_views
from debug_toolbar.toolbar import debug_toolbar_urls
urlpatterns = [
path('admin/', admin.site.urls, name='admin'),
path('', include('mainapp.urls', namespace='mainapp')),
path('', include('mapsapp.urls', namespace='mapsapp')),
# Authentication URLs
path('login/', auth_views.LoginView.as_view(), name='login'),
path('logout/', views.custom_logout, name='logout'),
] + debug_toolbar_urls()
"""
URL configuration for dbapp project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from mainapp import views
from django.contrib.auth import views as auth_views
from debug_toolbar.toolbar import debug_toolbar_urls
urlpatterns = [
path('admin/', admin.site.urls, name='admin'),
path('', include('mainapp.urls', namespace='mainapp')),
path('', include('mapsapp.urls', namespace='mapsapp')),
# Authentication URLs
path('login/', auth_views.LoginView.as_view(), name='login'),
path('logout/', views.custom_logout, name='logout'),
] + debug_toolbar_urls()

View File

@@ -1,16 +1,16 @@
"""
WSGI config for dbapp project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dbapp.settings')
application = get_wsgi_application()
"""
WSGI config for dbapp project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dbapp.settings')
application = get_wsgi_application()

View File

@@ -1,37 +1,37 @@
#!/bin/bash
set -e
# Определяем окружение (по умолчанию production)
ENVIRONMENT=${ENVIRONMENT:-production}
echo "Starting in $ENVIRONMENT mode..."
# Ждем PostgreSQL
echo "Waiting for PostgreSQL..."
while ! nc -z $DB_HOST $DB_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
# Выполняем миграции
echo "Running migrations..."
python manage.py migrate --noinput
# Собираем статику (только для production)
if [ "$ENVIRONMENT" = "production" ]; then
echo "Collecting static files..."
python manage.py collectstatic --noinput
fi
# Запускаем сервер в зависимости от окружения
if [ "$ENVIRONMENT" = "development" ]; then
echo "Starting Django development server..."
exec python manage.py runserver 0.0.0.0:8000
else
echo "Starting Gunicorn..."
exec gunicorn --bind 0.0.0.0:8000 \
--workers ${GUNICORN_WORKERS:-3} \
--timeout ${GUNICORN_TIMEOUT:-120} \
--reload \
dbapp.wsgi:application
fi
#!/bin/bash
set -e
# Определяем окружение (по умолчанию production)
ENVIRONMENT=${ENVIRONMENT:-production}
echo "Starting in $ENVIRONMENT mode..."
# Ждем PostgreSQL
echo "Waiting for PostgreSQL..."
while ! nc -z $DB_HOST $DB_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
# Выполняем миграции
echo "Running migrations..."
python manage.py migrate --noinput
# Собираем статику (только для production)
if [ "$ENVIRONMENT" = "production" ]; then
echo "Collecting static files..."
python manage.py collectstatic --noinput
fi
# Запускаем сервер в зависимости от окружения
if [ "$ENVIRONMENT" = "development" ]; then
echo "Starting Django development server..."
exec python manage.py runserver 0.0.0.0:8000
else
echo "Starting Gunicorn..."
exec gunicorn --bind 0.0.0.0:8000 \
--workers ${GUNICORN_WORKERS:-3} \
--timeout ${GUNICORN_TIMEOUT:-120} \
--reload \
dbapp.wsgi:application
fi

View File

@@ -1,10 +1,10 @@
from django.contrib import admin
from .models import LyngSat
@admin.register(LyngSat)
class LyngSatAdmin(admin.ModelAdmin):
list_display = ("id_satellite", "frequency", "polarization", "modulation", "last_update")
search_fields = ("id_satellite__name", "channel_info")
list_filter = ("id_satellite", "polarization", "modulation", "standard")
ordering = ("-last_update",)
from django.contrib import admin
from .models import LyngSat
@admin.register(LyngSat)
class LyngSatAdmin(admin.ModelAdmin):
list_display = ("id_satellite", "frequency", "polarization", "modulation", "last_update")
search_fields = ("id_satellite__name", "channel_info")
list_filter = ("id_satellite", "polarization", "modulation", "standard")
ordering = ("-last_update",)
readonly_fields = ("last_update",)

View File

@@ -1,6 +1,6 @@
from django.apps import AppConfig
class LyngsatappConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'lyngsatapp'
from django.apps import AppConfig
class LyngsatappConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'lyngsatapp'

View File

@@ -1,37 +1,37 @@
# Generated by Django 5.2.7 on 2025-11-10 20:03
import django.db.models.deletion
import mainapp.models
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('mainapp', '0007_remove_parameter_objitems_parameter_objitem'),
]
operations = [
migrations.CreateModel(
name='LyngSat',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('frequency', models.FloatField(blank=True, default=0, null=True, verbose_name='Частота, МГц')),
('sym_velocity', models.FloatField(blank=True, default=0, null=True, verbose_name='Символьная скорость, БОД')),
('last_update', models.DateTimeField(blank=True, null=True, verbose_name='Время')),
('channel_info', models.CharField(blank=True, max_length=20, null=True, verbose_name='Описание источника')),
('fec', models.CharField(blank=True, max_length=30, null=True, verbose_name='Коэффициент коррекции ошибок')),
('url', models.URLField(blank=True, null=True, verbose_name='Ссылка на страницу')),
('id_satellite', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='lyngsat', to='mainapp.satellite', verbose_name='Спутник')),
('modulation', models.ForeignKey(blank=True, default=mainapp.models.get_default_modulation, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='lyngsat', to='mainapp.modulation', verbose_name='Модуляция')),
('polarization', models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='lyngsat', to='mainapp.polarization', verbose_name='Поляризация')),
('standard', models.ForeignKey(blank=True, default=mainapp.models.get_default_standard, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='lyngsat', to='mainapp.standard', verbose_name='Стандарт')),
],
options={
'verbose_name': 'Источник LyngSat',
'verbose_name_plural': 'Источники LyngSat',
},
),
]
# Generated by Django 5.2.7 on 2025-11-10 20:03
import django.db.models.deletion
import mainapp.models
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('mainapp', '0007_remove_parameter_objitems_parameter_objitem'),
]
operations = [
migrations.CreateModel(
name='LyngSat',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('frequency', models.FloatField(blank=True, default=0, null=True, verbose_name='Частота, МГц')),
('sym_velocity', models.FloatField(blank=True, default=0, null=True, verbose_name='Символьная скорость, БОД')),
('last_update', models.DateTimeField(blank=True, null=True, verbose_name='Время')),
('channel_info', models.CharField(blank=True, max_length=20, null=True, verbose_name='Описание источника')),
('fec', models.CharField(blank=True, max_length=30, null=True, verbose_name='Коэффициент коррекции ошибок')),
('url', models.URLField(blank=True, null=True, verbose_name='Ссылка на страницу')),
('id_satellite', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='lyngsat', to='mainapp.satellite', verbose_name='Спутник')),
('modulation', models.ForeignKey(blank=True, default=mainapp.models.get_default_modulation, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='lyngsat', to='mainapp.modulation', verbose_name='Модуляция')),
('polarization', models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='lyngsat', to='mainapp.polarization', verbose_name='Поляризация')),
('standard', models.ForeignKey(blank=True, default=mainapp.models.get_default_standard, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='lyngsat', to='mainapp.standard', verbose_name='Стандарт')),
],
options={
'verbose_name': 'Источник LyngSat',
'verbose_name_plural': 'Источники LyngSat',
},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-11-11 13:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('lyngsatapp', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='lyngsat',
name='last_update',
field=models.DateTimeField(blank=True, null=True, verbose_name='Дата посленего обновления'),
),
]

View File

@@ -1,37 +1,37 @@
from django.db import models
from mainapp.models import (
Satellite,
Polarization,
Modulation,
Standard,
get_default_polarization,
get_default_modulation,
get_default_standard
)
class LyngSat(models.Model):
id_satellite = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="lyngsat", verbose_name="Спутник", null=True)
polarization = models.ForeignKey(
Polarization, default=get_default_polarization, on_delete=models.SET_DEFAULT, related_name="lyngsat", null=True, blank=True, verbose_name="Поляризация"
)
modulation = models.ForeignKey(
Modulation, default=get_default_modulation, on_delete=models.SET_DEFAULT, related_name="lyngsat", null=True, blank=True, verbose_name="Модуляция"
)
standard = models.ForeignKey(
Standard, default=get_default_standard, on_delete=models.SET_DEFAULT, related_name="lyngsat", null=True, blank=True, verbose_name="Стандарт"
)
frequency = models.FloatField(default=0, null=True, blank=True, verbose_name="Частота, МГц")
sym_velocity = models.FloatField(default=0, null=True, blank=True, verbose_name="Символьная скорость, БОД")
last_update = models.DateTimeField(null=True, blank=True, verbose_name="Время")
channel_info = models.CharField(max_length=20, blank=True, null=True, verbose_name="Описание источника")
fec = models.CharField(max_length=30, blank=True, null=True, verbose_name="Коэффициент коррекции ошибок")
url = models.URLField(max_length = 200, blank=True, null=True, verbose_name="Ссылка на страницу")
def __str__(self):
return f"Ист {self.frequency}, {self.polarization}"
class Meta:
verbose_name = "Источник LyngSat"
verbose_name_plural = "Источники LyngSat"
from django.db import models
from mainapp.models import (
Satellite,
Polarization,
Modulation,
Standard,
get_default_polarization,
get_default_modulation,
get_default_standard
)
class LyngSat(models.Model):
id_satellite = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="lyngsat", verbose_name="Спутник", null=True)
polarization = models.ForeignKey(
Polarization, default=get_default_polarization, on_delete=models.SET_DEFAULT, related_name="lyngsat", null=True, blank=True, verbose_name="Поляризация"
)
modulation = models.ForeignKey(
Modulation, default=get_default_modulation, on_delete=models.SET_DEFAULT, related_name="lyngsat", null=True, blank=True, verbose_name="Модуляция"
)
standard = models.ForeignKey(
Standard, default=get_default_standard, on_delete=models.SET_DEFAULT, related_name="lyngsat", null=True, blank=True, verbose_name="Стандарт"
)
frequency = models.FloatField(default=0, null=True, blank=True, verbose_name="Частота, МГц")
sym_velocity = models.FloatField(default=0, null=True, blank=True, verbose_name="Символьная скорость, БОД")
last_update = models.DateTimeField(null=True, blank=True, verbose_name="Дата посленего обновления")
channel_info = models.CharField(max_length=20, blank=True, null=True, verbose_name="Описание источника")
fec = models.CharField(max_length=30, blank=True, null=True, verbose_name="Коэффициент коррекции ошибок")
url = models.URLField(max_length = 200, blank=True, null=True, verbose_name="Ссылка на страницу")
def __str__(self):
return f"Ист {self.frequency}, {self.polarization}"
class Meta:
verbose_name = "Источник LyngSat"
verbose_name_plural = "Источники LyngSat"

View File

@@ -1,405 +1,437 @@
import requests
from bs4 import BeautifulSoup
from datetime import datetime
import re
import time
class LyngSatParser:
"""Парсер данных для LyngSat(Для работы нужен flaresolver)"""
def __init__(
self,
flaresolver_url: str = "http://localhost:8191/v1",
regions: list[str] | None = None,
target_sats: list[str] | None = None,
):
self.flaresolver_url = flaresolver_url
self.regions = regions
self.target_sats = (
list(map(lambda sat: sat.strip().lower(), target_sats)) if regions else None
)
self.regions = regions if regions else ["europe", "asia", "america", "atlantic"]
self.BASE_URL = "https://www.lyngsat.com"
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:
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 get_region_pages(self, regions: list[str] | None = None) -> list[str]:
html_regions = []
if regions is None:
regions = self.regions
for region in regions:
url = f"{self.BASE_URL}/{region}.html"
payload = {"cmd": "request.get", "url": url, "maxTimeout": 60000}
response = requests.post(self.flaresolver_url, json=payload)
if response.status_code != 200:
continue
html_content = response.json().get("solution", {}).get("response", "")
html_regions.append(html_content)
print(f"Обработал страницу по {region}")
return html_regions
def get_satellite_urls(self, html_regions: list[str]):
sat_names = []
sat_urls = []
for region_page in html_regions:
soup = BeautifulSoup(region_page, "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
if self.target_sats is not None:
if sat_name.strip().lower() 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"]
sat_names.append(sat_name)
sat_urls.append(sat_url)
return sat_names, sat_urls
def get_satellites_data(self) -> dict[dict]:
sat_data = {}
for region_page in self.get_region_pages(self.regions):
soup = BeautifulSoup(region_page, "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
if self.target_sats is not None:
if sat_name.strip().lower() 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 = tr.find_all("td")[-1].text
sat_response = requests.post(
self.flaresolver_url,
json={
"cmd": "request.get",
"url": f"{self.BASE_URL}/{sat_url}",
"maxTimeout": 60000,
},
)
html_content = (
sat_response.json().get("solution", {}).get("response", "")
)
sat_page_data = self.get_satellite_content(html_content)
sat_data[sat_name] = {
"url": f"{self.BASE_URL}/{sat_url}",
"update_date": datetime.strptime(update_date, "%y%m%d").date(),
"sources": sat_page_data,
}
return sat_data
def get_satellite_content(self, html_content: str) -> dict:
sat_soup = BeautifulSoup(html_content, "html.parser")
big_table = sat_soup.find("table", class_="bigtable")
all_tables = big_table.find_all("div", class_="desktab")[:-1]
data = []
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
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,
}
)
return data
class KingOfSatParser:
def __init__(self, base_url="https://ru.kingofsat.net", max_satellites=0):
"""
Инициализация парсера
:param base_url: Базовый URL сайта
:param max_satellites: Максимальное количество спутников для парсинга (0 - все)
"""
self.base_url = base_url
self.max_satellites = max_satellites
self.session = requests.Session()
self.session.headers.update(
{
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
}
)
def convert_polarization(self, polarization):
"""Преобразовать код поляризации в понятное название на русском"""
polarization_map = {
"V": "Вертикальная",
"H": "Горизонтальная",
"R": "Правая",
"L": "Левая",
}
return polarization_map.get(polarization.upper(), polarization)
def fetch_page(self, url):
"""Получить HTML страницу"""
try:
response = self.session.get(url, timeout=30)
response.raise_for_status()
return response.text
except Exception as e:
print(f"Ошибка при получении страницы {url}: {e}")
return None
def parse_satellite_table(self, html_content):
"""Распарсить таблицу со спутниками"""
soup = BeautifulSoup(html_content, "html.parser")
satellites = []
table = soup.find("table")
if not table:
print("Таблица не найдена")
return satellites
rows = table.find_all("tr")[1:]
for row in rows:
cols = row.find_all("td")
if len(cols) < 13:
continue
try:
position_cell = cols[0].text.strip()
position_match = re.search(r"([\d\.]+)°([EW])", position_cell)
if position_match:
position_value = position_match.group(1)
position_direction = position_match.group(2)
position = f"{position_value}{position_direction}"
else:
position = None
# Название спутника (2-я колонка)
satellite_cell = cols[1]
satellite_name = satellite_cell.get_text(strip=True)
# Удаляем возможные лишние символы или пробелы
satellite_name = re.sub(r"\s+", " ", satellite_name).strip()
# NORAD (3-я колонка)
norad = cols[2].text.strip()
if not norad or norad == "-":
norad = None
ini_link = None
ini_cell = cols[3]
ini_img = ini_cell.find("img", src=lambda x: x and "disquette.gif" in x)
if ini_img and position:
ini_link = f"https://ru.kingofsat.net/dl.php?pos={position}&fkhz=0"
update_date = cols[12].text.strip() if len(cols) > 12 else None
if satellite_name and ini_link and position:
satellites.append(
{
"position": position,
"name": satellite_name,
"norad": norad,
"ini_url": ini_link,
"update_date": update_date,
}
)
except Exception as e:
print(f"Ошибка при обработке строки таблицы: {e}")
continue
return satellites
def parse_ini_file(self, ini_content):
"""Распарсить содержимое .ini файла"""
data = {"metadata": {}, "sattype": {}, "dvb": {}}
# # Извлекаем метаданные из комментариев
# metadata_match = re.search(r'\[ downloaded from www\.kingofsat\.net \(c\) (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \]', ini_content)
# if metadata_match:
# data['metadata']['downloaded'] = metadata_match.group(1)
# Парсим секцию [SATTYPE]
sattype_match = re.search(r"\[SATTYPE\](.*?)\n\[", ini_content, re.DOTALL)
if sattype_match:
sattype_content = sattype_match.group(1).strip()
for line in sattype_content.split("\n"):
line = line.strip()
if "=" in line:
key, value = line.split("=", 1)
data["sattype"][key.strip()] = value.strip()
# Парсим секцию [DVB]
dvb_match = re.search(r"\[DVB\](.*?)(?:\n\[|$)", ini_content, re.DOTALL)
if dvb_match:
dvb_content = dvb_match.group(1).strip()
for line in dvb_content.split("\n"):
line = line.strip()
if "=" in line:
key, value = line.split("=", 1)
params = [p.strip() for p in value.split(",")]
polarization = params[1] if len(params) > 1 else ""
if polarization:
polarization = self.convert_polarization(polarization)
data["dvb"][key.strip()] = {
"frequency": params[0] if len(params) > 0 else "",
"polarization": polarization,
"symbol_rate": params[2] if len(params) > 2 else "",
"fec": params[3] if len(params) > 3 else "",
"standard": params[4] if len(params) > 4 else "",
"modulation": params[5] if len(params) > 5 else "",
}
return data
def download_ini_file(self, url):
"""Скачать содержимое .ini файла"""
try:
response = self.session.get(url, timeout=30)
response.raise_for_status()
return response.text
except Exception as e:
print(f"Ошибка при скачивании .ini файла {url}: {e}")
return None
def get_all_satellites_data(self):
"""Получить данные всех спутников с учетом ограничения max_satellites"""
html_content = self.fetch_page(self.base_url + "/satellites")
if not html_content:
return []
satellites = self.parse_satellite_table(html_content)
if self.max_satellites > 0 and len(satellites) > self.max_satellites:
satellites = satellites[: self.max_satellites]
results = []
processed_count = 0
for satellite in satellites:
print(f"Обработка спутника: {satellite['name']} ({satellite['position']})")
ini_content = self.download_ini_file(satellite["ini_url"])
if not ini_content:
print(f"Не удалось скачать .ini файл для {satellite['name']}")
continue
parsed_ini = self.parse_ini_file(ini_content)
result = {
"satellite_name": satellite["name"],
"position": satellite["position"],
"norad": satellite["norad"],
"update_date": satellite["update_date"],
"ini_url": satellite["ini_url"],
"ini_data": parsed_ini,
}
results.append(result)
processed_count += 1
if self.max_satellites > 0 and processed_count >= self.max_satellites:
break
time.sleep(1)
return results
def create_satellite_dict(self, satellites_data):
"""Создать словарь с данными спутников"""
satellite_dict = {}
for data in satellites_data:
key = f"{data['position']}_{data['satellite_name'].replace(' ', '_').replace('/', '_')}"
satellite_dict[key] = {
"name": data["satellite_name"],
"position": data["position"],
"norad": data["norad"],
"update_date": data["update_date"],
"ini_url": data["ini_url"],
"transponders_count": len(data["ini_data"]["dvb"]),
"transponders": data["ini_data"]["dvb"],
"sattype_info": data["ini_data"]["sattype"],
"metadata": data["ini_data"]["metadata"],
}
return satellite_dict
import requests
from bs4 import BeautifulSoup
from datetime import datetime
import re
import time
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 LyngSatParser:
"""Парсер данных для LyngSat(Для работы нужен flaresolver)"""
def __init__(
self,
flaresolver_url: str = "http://localhost:8191/v1",
regions: list[str] | None = None,
target_sats: list[str] | None = None,
):
self.flaresolver_url = flaresolver_url
self.regions = regions
self.target_sats = (
list(map(lambda sat: sat.strip().lower(), target_sats)) if regions else None
)
self.regions = regions if regions else ["europe", "asia", "america", "atlantic"]
self.BASE_URL = "https://www.lyngsat.com"
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:
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 get_region_pages(self, regions: list[str] | None = None) -> list[str]:
html_regions = []
if regions is None:
regions = self.regions
for region in regions:
url = f"{self.BASE_URL}/{region}.html"
payload = {"cmd": "request.get", "url": url, "maxTimeout": 60000}
response = requests.post(self.flaresolver_url, json=payload)
if response.status_code != 200:
continue
html_content = response.json().get("solution", {}).get("response", "")
html_regions.append(html_content)
print(f"Обработал страницу по {region}")
return html_regions
def get_satellite_urls(self, html_regions: list[str]):
sat_names = []
sat_urls = []
for region_page in html_regions:
soup = BeautifulSoup(region_page, "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
if self.target_sats is not None:
if sat_name.strip().lower() 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"]
sat_names.append(sat_name)
sat_urls.append(sat_url)
return sat_names, sat_urls
def get_satellites_data(self) -> dict[dict]:
sat_data = {}
for region_page in self.get_region_pages(self.regions):
soup = BeautifulSoup(region_page, "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
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 = tr.find_all("td")[-1].text
sat_response = requests.post(
self.flaresolver_url,
json={
"cmd": "request.get",
"url": f"{self.BASE_URL}/{sat_url}",
"maxTimeout": 60000,
},
)
html_content = (
sat_response.json().get("solution", {}).get("response", "")
)
sat_page_data = self.get_satellite_content(html_content)
sat_data[sat_name] = {
"url": f"{self.BASE_URL}/{sat_url}",
"update_date": datetime.strptime(update_date, "%y%m%d").date(),
"sources": sat_page_data,
}
return sat_data
def get_satellite_content(self, html_content: str) -> list[dict]:
data = []
sat_soup = BeautifulSoup(html_content, "html.parser")
try:
big_table = sat_soup.find("table", class_="bigtable")
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
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:
print(e)
return data if data else data[{}]
class KingOfSatParser:
def __init__(self, base_url="https://ru.kingofsat.net", max_satellites=0):
"""
Инициализация парсера
:param base_url: Базовый URL сайта
:param max_satellites: Максимальное количество спутников для парсинга (0 - все)
"""
self.base_url = base_url
self.max_satellites = max_satellites
self.session = requests.Session()
self.session.headers.update(
{
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
}
)
def convert_polarization(self, polarization):
"""Преобразовать код поляризации в понятное название на русском"""
polarization_map = {
"V": "Вертикальная",
"H": "Горизонтальная",
"R": "Правая",
"L": "Левая",
}
return polarization_map.get(polarization.upper(), polarization)
def fetch_page(self, url):
"""Получить HTML страницу"""
try:
response = self.session.get(url, timeout=30)
response.raise_for_status()
return response.text
except Exception as e:
print(f"Ошибка при получении страницы {url}: {e}")
return None
def parse_satellite_table(self, html_content):
"""Распарсить таблицу со спутниками"""
soup = BeautifulSoup(html_content, "html.parser")
satellites = []
table = soup.find("table")
if not table:
print("Таблица не найдена")
return satellites
rows = table.find_all("tr")[1:]
for row in rows:
cols = row.find_all("td")
if len(cols) < 13:
continue
try:
position_cell = cols[0].text.strip()
position_match = re.search(r"([\d\.]+)°([EW])", position_cell)
if position_match:
position_value = position_match.group(1)
position_direction = position_match.group(2)
position = f"{position_value}{position_direction}"
else:
position = None
# Название спутника (2-я колонка)
satellite_cell = cols[1]
satellite_name = satellite_cell.get_text(strip=True)
# Удаляем возможные лишние символы или пробелы
satellite_name = re.sub(r"\s+", " ", satellite_name).strip()
# NORAD (3-я колонка)
norad = cols[2].text.strip()
if not norad or norad == "-":
norad = None
ini_link = None
ini_cell = cols[3]
ini_img = ini_cell.find("img", src=lambda x: x and "disquette.gif" in x)
if ini_img and position:
ini_link = f"https://ru.kingofsat.net/dl.php?pos={position}&fkhz=0"
update_date = cols[12].text.strip() if len(cols) > 12 else None
if satellite_name and ini_link and position:
satellites.append(
{
"position": position,
"name": satellite_name,
"norad": norad,
"ini_url": ini_link,
"update_date": update_date,
}
)
except Exception as e:
print(f"Ошибка при обработке строки таблицы: {e}")
continue
return satellites
def parse_ini_file(self, ini_content):
"""Распарсить содержимое .ini файла"""
data = {"metadata": {}, "sattype": {}, "dvb": {}}
# # Извлекаем метаданные из комментариев
# metadata_match = re.search(r'\[ downloaded from www\.kingofsat\.net \(c\) (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \]', ini_content)
# if metadata_match:
# data['metadata']['downloaded'] = metadata_match.group(1)
# Парсим секцию [SATTYPE]
sattype_match = re.search(r"\[SATTYPE\](.*?)\n\[", ini_content, re.DOTALL)
if sattype_match:
sattype_content = sattype_match.group(1).strip()
for line in sattype_content.split("\n"):
line = line.strip()
if "=" in line:
key, value = line.split("=", 1)
data["sattype"][key.strip()] = value.strip()
# Парсим секцию [DVB]
dvb_match = re.search(r"\[DVB\](.*?)(?:\n\[|$)", ini_content, re.DOTALL)
if dvb_match:
dvb_content = dvb_match.group(1).strip()
for line in dvb_content.split("\n"):
line = line.strip()
if "=" in line:
key, value = line.split("=", 1)
params = [p.strip() for p in value.split(",")]
polarization = params[1] if len(params) > 1 else ""
if polarization:
polarization = self.convert_polarization(polarization)
data["dvb"][key.strip()] = {
"frequency": params[0] if len(params) > 0 else "",
"polarization": polarization,
"symbol_rate": params[2] if len(params) > 2 else "",
"fec": params[3] if len(params) > 3 else "",
"standard": params[4] if len(params) > 4 else "",
"modulation": params[5] if len(params) > 5 else "",
}
return data
def download_ini_file(self, url):
"""Скачать содержимое .ini файла"""
try:
response = self.session.get(url, timeout=30)
response.raise_for_status()
return response.text
except Exception as e:
print(f"Ошибка при скачивании .ini файла {url}: {e}")
return None
def get_all_satellites_data(self):
"""Получить данные всех спутников с учетом ограничения max_satellites"""
html_content = self.fetch_page(self.base_url + "/satellites")
if not html_content:
return []
satellites = self.parse_satellite_table(html_content)
if self.max_satellites > 0 and len(satellites) > self.max_satellites:
satellites = satellites[: self.max_satellites]
results = []
processed_count = 0
for satellite in satellites:
print(f"Обработка спутника: {satellite['name']} ({satellite['position']})")
ini_content = self.download_ini_file(satellite["ini_url"])
if not ini_content:
print(f"Не удалось скачать .ini файл для {satellite['name']}")
continue
parsed_ini = self.parse_ini_file(ini_content)
result = {
"satellite_name": satellite["name"],
"position": satellite["position"],
"norad": satellite["norad"],
"update_date": satellite["update_date"],
"ini_url": satellite["ini_url"],
"ini_data": parsed_ini,
}
results.append(result)
processed_count += 1
if self.max_satellites > 0 and processed_count >= self.max_satellites:
break
time.sleep(1)
return results
def create_satellite_dict(self, satellites_data):
"""Создать словарь с данными спутников"""
satellite_dict = {}
for data in satellites_data:
key = f"{data['position']}_{data['satellite_name'].replace(' ', '_').replace('/', '_')}"
satellite_dict[key] = {
"name": data["satellite_name"],
"position": data["position"],
"norad": data["norad"],
"update_date": data["update_date"],
"ini_url": data["ini_url"],
"transponders_count": len(data["ini_data"]["dvb"]),
"transponders": data["ini_data"]["dvb"],
"sattype_info": data["ini_data"]["sattype"],
"metadata": data["ini_data"]["metadata"],
}
return satellite_dict

View File

@@ -1,73 +1,73 @@
"""
Celery tasks for Lyngsat data processing.
"""
import logging
from celery import shared_task
from django.core.cache import cache
from .utils import fill_lyngsat_data
logger = logging.getLogger(__name__)
@shared_task(bind=True, name='lyngsatapp.fill_lyngsat_data_async')
def fill_lyngsat_data_task(self, target_sats, regions=None):
"""
Асинхронная задача для заполнения данных Lyngsat.
Args:
target_sats: Список названий спутников для обработки
regions: Список регионов для парсинга (по умолчанию все)
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 'все'}")
# Обновляем статус задачи
self.update_state(
state='PROGRESS',
meta={
'current': 0,
'total': len(target_sats),
'status': 'Инициализация...'
}
)
try:
# Вызываем функцию заполнения данных
stats = fill_lyngsat_data(
target_sats=target_sats,
regions=regions,
task_id=task_id,
update_progress=lambda current, total, status: self.update_state(
state='PROGRESS',
meta={
'current': current,
'total': total,
'status': status
}
)
)
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)
self.update_state(
state='FAILURE',
meta={
'error': str(e),
'status': 'Ошибка при обработке'
}
)
raise
"""
Celery tasks for Lyngsat data processing.
"""
import logging
from celery import shared_task
from django.core.cache import cache
from .utils import fill_lyngsat_data
logger = logging.getLogger(__name__)
@shared_task(bind=True, name='lyngsatapp.fill_lyngsat_data_async')
def fill_lyngsat_data_task(self, target_sats, regions=None):
"""
Асинхронная задача для заполнения данных Lyngsat.
Args:
target_sats: Список названий спутников для обработки
regions: Список регионов для парсинга (по умолчанию все)
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 'все'}")
# Обновляем статус задачи
self.update_state(
state='PROGRESS',
meta={
'current': 0,
'total': len(target_sats),
'status': 'Инициализация...'
}
)
try:
# Вызываем функцию заполнения данных
stats = fill_lyngsat_data(
target_sats=target_sats,
regions=regions,
task_id=task_id,
update_progress=lambda current, total, status: self.update_state(
state='PROGRESS',
meta={
'current': current,
'total': total,
'status': status
}
)
)
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)
self.update_state(
state='FAILURE',
meta={
'error': str(e),
'status': 'Ошибка при обработке'
}
)
raise

View File

@@ -1,3 +1,3 @@
from django.test import TestCase
# Create your tests here.
from django.test import TestCase
# Create your tests here.

View File

@@ -1,170 +1,175 @@
import logging
from .parser import LyngSatParser
from .models import LyngSat
from mainapp.models import Polarization, Standard, Modulation, Satellite
logger = logging.getLogger(__name__)
def fill_lyngsat_data(
target_sats: list[str],
regions: list[str] = None,
task_id: str = None,
update_progress=None
):
"""
Заполняет данные Lyngsat для указанных спутников и регионов.
Args:
target_sats: Список названий спутников для обработки
regions: Список регионов для парсинга (по умолчанию все)
task_id: ID задачи Celery для логирования
update_progress: Функция для обновления прогресса (current, total, status)
Returns:
dict: Статистика обработки с ключами:
- total_satellites: общее количество спутников
- total_sources: общее количество источников
- created: количество созданных записей
- updated: количество обновленных записей
- errors: список ошибок
"""
log_prefix = f"[Task {task_id}]" if task_id else "[Lyngsat]"
stats = {
'total_satellites': 0,
'total_sources': 0,
'created': 0,
'updated': 0,
'errors': []
}
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)}")
if update_progress:
update_progress(0, len(target_sats), "Инициализация парсера...")
try:
parser = LyngSatParser(
target_sats=target_sats,
regions=regions
)
logger.info(f"{log_prefix} Получение данных со спутников...")
if update_progress:
update_progress(0, len(target_sats), "Получение данных со спутников...")
lyngsat_data = parser.get_satellites_data()
stats['total_satellites'] = len(lyngsat_data)
logger.info(f"{log_prefix} Получено данных по {stats['total_satellites']} спутникам")
for idx, (sat_name, data) in enumerate(lyngsat_data.items(), 1):
logger.info(f"{log_prefix} Обработка спутника {idx}/{stats['total_satellites']}: {sat_name}")
if update_progress:
update_progress(idx, stats['total_satellites'], f"Обработка {sat_name}...")
url = data['url']
sources = data['sources']
stats['total_sources'] += len(sources)
logger.info(f"{log_prefix} Найдено {len(sources)} источников для {sat_name}")
# Находим спутник в базе
try:
sat_obj = Satellite.objects.get(name__icontains=sat_name)
logger.debug(f"{log_prefix} Спутник {sat_name} найден в базе (ID: {sat_obj.id})")
except Satellite.DoesNotExist:
error_msg = f"Спутник '{sat_name}' не найден в базе данных"
logger.warning(f"{log_prefix} {error_msg}")
stats['errors'].append(error_msg)
continue
except Satellite.MultipleObjectsReturned:
error_msg = f"Найдено несколько спутников с именем '{sat_name}'"
logger.warning(f"{log_prefix} {error_msg}")
stats['errors'].append(error_msg)
continue
for source_idx, source in enumerate(sources, 1):
try:
# Парсим частоту
try:
freq = float(source['freq'])
except (ValueError, TypeError):
freq = -1.0
error_msg = f"Некорректная частота для {sat_name}: {source.get('freq')}"
logger.debug(f"{log_prefix} {error_msg}")
stats['errors'].append(error_msg)
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": url
}
)
if created:
stats['created'] += 1
logger.debug(f"{log_prefix} Создана запись для {sat_name} {freq} МГц")
else:
stats['updated'] += 1
logger.debug(f"{log_prefix} Обновлена запись для {sat_name} {freq} МГц")
# Логируем прогресс каждые 10 источников
if source_idx % 10 == 0:
logger.info(f"{log_prefix} Обработано {source_idx}/{len(sources)} источников для {sat_name}")
except Exception as e:
error_msg = f"Ошибка при обработке источника {sat_name}: {str(e)}"
logger.error(f"{log_prefix} {error_msg}", exc_info=True)
stats['errors'].append(error_msg)
continue
logger.info(f"{log_prefix} Завершена обработка {sat_name}: создано {stats['created']}, обновлено {stats['updated']}")
except Exception as e:
error_msg = f"Критическая ошибка: {str(e)}"
logger.error(f"{log_prefix} {error_msg}", exc_info=True)
stats['errors'].append(error_msg)
logger.info(f"{log_prefix} Обработка завершена. Итого: создано {stats['created']}, обновлено {stats['updated']}, ошибок {len(stats['errors'])}")
if update_progress:
update_progress(stats['total_satellites'], stats['total_satellites'], "Завершено")
return stats
import logging
from .parser import LyngSatParser
from .models import LyngSat
from mainapp.models import Polarization, Standard, Modulation, Satellite
logger = logging.getLogger(__name__)
def fill_lyngsat_data(
target_sats: list[str],
regions: list[str] = None,
task_id: str = None,
update_progress=None
):
"""
Заполняет данные Lyngsat для указанных спутников и регионов.
Args:
target_sats: Список названий спутников для обработки
regions: Список регионов для парсинга (по умолчанию все)
task_id: ID задачи Celery для логирования
update_progress: Функция для обновления прогресса (current, total, status)
Returns:
dict: Статистика обработки с ключами:
- total_satellites: общее количество спутников
- total_sources: общее количество источников
- created: количество созданных записей
- updated: количество обновленных записей
- errors: список ошибок
"""
log_prefix = f"[Task {task_id}]" if task_id else "[Lyngsat]"
stats = {
'total_satellites': 0,
'total_sources': 0,
'created': 0,
'updated': 0,
'errors': []
}
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)}")
if update_progress:
update_progress(0, len(target_sats), "Инициализация парсера...")
try:
parser = LyngSatParser(
flaresolver_url="http://localhost:8191/v1",
target_sats=target_sats,
regions=regions
)
logger.info(f"{log_prefix} Получение данных со спутников...")
if update_progress:
update_progress(0, len(target_sats), "Получение данных со спутников...")
lyngsat_data = parser.get_satellites_data()
stats['total_satellites'] = len(lyngsat_data)
logger.info(f"{log_prefix} Получено данных по {stats['total_satellites']} спутникам")
for idx, (sat_name, data) in enumerate(lyngsat_data.items(), 1):
logger.info(f"{log_prefix} Обработка спутника {idx}/{stats['total_satellites']}: {sat_name}")
if update_progress:
update_progress(idx, stats['total_satellites'], f"Обработка {sat_name}...")
url = data['url']
sources = data['sources']
stats['total_sources'] += len(sources)
logger.info(f"{log_prefix} Найдено {len(sources)} источников для {sat_name}")
# Находим спутник в базе
try:
sat_obj = Satellite.objects.get(name__icontains=sat_name)
logger.debug(f"{log_prefix} Спутник {sat_name} найден в базе (ID: {sat_obj.id})")
except Satellite.DoesNotExist:
error_msg = f"Спутник '{sat_name}' не найден в базе данных"
logger.warning(f"{log_prefix} {error_msg}")
stats['errors'].append(error_msg)
continue
except Satellite.MultipleObjectsReturned:
error_msg = f"Найдено несколько спутников с именем '{sat_name}'"
logger.warning(f"{log_prefix} {error_msg}")
stats['errors'].append(error_msg)
continue
for source_idx, source in enumerate(sources, 1):
try:
# Парсим частоту
try:
freq = float(source['freq'])
except (ValueError, TypeError):
freq = -1.0
error_msg = f"Некорректная частота для {sat_name}: {source.get('freq')}"
logger.debug(f"{log_prefix} {error_msg}")
stats['errors'].append(error_msg)
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": url
}
)
if created:
stats['created'] += 1
logger.debug(f"{log_prefix} Создана запись для {sat_name} {freq} МГц")
else:
stats['updated'] += 1
logger.debug(f"{log_prefix} Обновлена запись для {sat_name} {freq} МГц")
# Логируем прогресс каждые 10 источников
if source_idx % 10 == 0:
logger.info(f"{log_prefix} Обработано {source_idx}/{len(sources)} источников для {sat_name}")
except Exception as e:
error_msg = f"Ошибка при обработке источника {sat_name}: {str(e)}"
logger.error(f"{log_prefix} {error_msg}", exc_info=True)
stats['errors'].append(error_msg)
continue
logger.info(f"{log_prefix} Завершена обработка {sat_name}: создано {stats['created']}, обновлено {stats['updated']}")
except Exception as e:
error_msg = f"Критическая ошибка: {str(e)}"
logger.error(f"{log_prefix} {error_msg}", exc_info=True)
stats['errors'].append(error_msg)
logger.info(f"{log_prefix} Обработка завершена. Итого: создано {stats['created']}, обновлено {stats['updated']}, ошибок {len(stats['errors'])}")
if update_progress:
update_progress(stats['total_satellites'], stats['total_satellites'], "Завершено")
return stats
def link_lyngsat_to_sources():
pass

View File

@@ -1,3 +1,3 @@
from django.shortcuts import render
# Create your views here.
from django.shortcuts import render
# Create your views here.

File diff suppressed because it is too large Load Diff

View File

@@ -1,34 +1,34 @@
# Third-party imports
import matplotlib.pyplot as plt
import numpy as np
from sklearn.cluster import DBSCAN, HDBSCAN, KMeans
# Local imports
from .models import ObjItem
def get_clusters(coords: list[tuple[float, float]]):
coords = np.radians(coords)
lat, lon = coords[:, 0], coords[:, 1]
db = DBSCAN(eps=0.06, min_samples=5, algorithm='ball_tree', metric='haversine')
# db = HDBSCAN()
cluster_labels = db.fit_predict(coords)
plt.figure(figsize=(10, 8))
unique_labels = set(cluster_labels)
colors = plt.cm.tab10(np.linspace(0, 1, len(unique_labels)))
for label, color in zip(unique_labels, colors):
if label == -1:
color = 'k'
label_name = 'Шум'
else:
label_name = f'Кластер {label}'
mask = cluster_labels == label
plt.scatter(lon[mask], lat[mask], c=[color], label=label_name, s=30)
plt.xlabel('Долгота')
plt.ylabel('Широта')
plt.title('Кластеризация геоданных с DBSCAN (метрика Хаверсина)')
plt.legend()
plt.grid(True)
plt.show()
# Third-party imports
import matplotlib.pyplot as plt
import numpy as np
from sklearn.cluster import DBSCAN, HDBSCAN, KMeans
# Local imports
from .models import ObjItem
def get_clusters(coords: list[tuple[float, float]]):
coords = np.radians(coords)
lat, lon = coords[:, 0], coords[:, 1]
db = DBSCAN(eps=0.06, min_samples=5, algorithm='ball_tree', metric='haversine')
# db = HDBSCAN()
cluster_labels = db.fit_predict(coords)
plt.figure(figsize=(10, 8))
unique_labels = set(cluster_labels)
colors = plt.cm.tab10(np.linspace(0, 1, len(unique_labels)))
for label, color in zip(unique_labels, colors):
if label == -1:
color = 'k'
label_name = 'Шум'
else:
label_name = f'Кластер {label}'
mask = cluster_labels == label
plt.scatter(lon[mask], lat[mask], c=[color], label=label_name, s=30)
plt.xlabel('Долгота')
plt.ylabel('Широта')
plt.title('Кластеризация геоданных с DBSCAN (метрика Хаверсина)')
plt.legend()
plt.grid(True)
plt.show()

View File

@@ -1,76 +1,76 @@
# Django imports
from django.contrib.admin import SimpleListFilter
# Local imports
from .models import ObjItem
class GeoKupDistanceFilter(SimpleListFilter):
title = 'Расстояние между гео и кубсатом'
parameter_name = 'distance_geo_kup'
def lookups(self, request, model_admin):
return (
('small', 'Меньше 100 км'),
('medium', '100-500 км'),
('large', 'Больше 500 км'),
)
def queryset(self, request, queryset):
if self.value() == 'small':
return queryset.filter(distance_coords_kup__lt=100)
if self.value() == 'medium':
return queryset.filter(distance_coords_kup__gte=100, distance_coords_kup__lte=500)
if self.value() == 'large':
return queryset.filter(distance_coords_kup__gt=500)
class GeoValidDistanceFilter(SimpleListFilter):
title = 'Расстояние между гео и оперативным отделом'
parameter_name = 'distance_geo_valid'
def lookups(self, request, model_admin):
return (
('small', 'Меньше 100 км'),
('medium', '100-500 км'),
('large', 'Больше 500 км'),
)
def queryset(self, request, queryset):
if self.value() == 'small':
return queryset.filter(distance_coords_valid__lt=100)
if self.value() == 'medium':
return queryset.filter(distance_coords_valid__gte=100, distance_coords_valid__lte=500)
if self.value() == 'large':
return queryset.filter(distance_coords_valid__gt=500)
class UniqueToggleFilter(SimpleListFilter):
title = 'Уникальность по имени'
parameter_name = 'name'
def lookups(self, request, model_admin):
return (
('unique', 'Только уникальные'),
('all', 'Все'),
)
def queryset(self, request, queryset):
if self.value() == 'unique':
return queryset.order_by('name').distinct('name')
return queryset
class HasSigmaParameterFilter(SimpleListFilter):
title = 'ВЧ sigma'
parameter_name = 'has_sigma'
def lookups(self, request, model_admin):
return (
('yes', 'Заполнено'),
('no', 'Пусто'),
)
def queryset(self, request, queryset):
if self.value() == 'yes':
return queryset.filter(sigma_parameter__isnull=False)
if self.value() == 'no':
return queryset.filter(sigma_parameter__isnull=True)
# Django imports
from django.contrib.admin import SimpleListFilter
# Local imports
from .models import ObjItem
class GeoKupDistanceFilter(SimpleListFilter):
title = 'Расстояние между гео и кубсатом'
parameter_name = 'distance_geo_kup'
def lookups(self, request, model_admin):
return (
('small', 'Меньше 100 км'),
('medium', '100-500 км'),
('large', 'Больше 500 км'),
)
def queryset(self, request, queryset):
if self.value() == 'small':
return queryset.filter(distance_coords_kup__lt=100)
if self.value() == 'medium':
return queryset.filter(distance_coords_kup__gte=100, distance_coords_kup__lte=500)
if self.value() == 'large':
return queryset.filter(distance_coords_kup__gt=500)
class GeoValidDistanceFilter(SimpleListFilter):
title = 'Расстояние между гео и оперативным отделом'
parameter_name = 'distance_geo_valid'
def lookups(self, request, model_admin):
return (
('small', 'Меньше 100 км'),
('medium', '100-500 км'),
('large', 'Больше 500 км'),
)
def queryset(self, request, queryset):
if self.value() == 'small':
return queryset.filter(distance_coords_valid__lt=100)
if self.value() == 'medium':
return queryset.filter(distance_coords_valid__gte=100, distance_coords_valid__lte=500)
if self.value() == 'large':
return queryset.filter(distance_coords_valid__gt=500)
class UniqueToggleFilter(SimpleListFilter):
title = 'Уникальность по имени'
parameter_name = 'name'
def lookups(self, request, model_admin):
return (
('unique', 'Только уникальные'),
('all', 'Все'),
)
def queryset(self, request, queryset):
if self.value() == 'unique':
return queryset.order_by('name').distinct('name')
return queryset
class HasSigmaParameterFilter(SimpleListFilter):
title = 'ВЧ sigma'
parameter_name = 'has_sigma'
def lookups(self, request, model_admin):
return (
('yes', 'Заполнено'),
('no', 'Пусто'),
)
def queryset(self, request, queryset):
if self.value() == 'yes':
return queryset.filter(sigma_parameter__isnull=False)
if self.value() == 'no':
return queryset.filter(sigma_parameter__isnull=True)
return queryset

View File

@@ -1,301 +1,301 @@
# Django imports
from django import forms
# Local imports
from .models import Geo, Modulation, ObjItem, Parameter, Polarization, Satellite, Standard
class UploadFileForm(forms.Form):
file = forms.FileField(
label="Выберите файл",
widget=forms.FileInput(attrs={
'class': 'form-file-input'
})
)
class LoadExcelData(forms.Form):
file = forms.FileField(
label="Выберите Excel файл",
widget=forms.FileInput(attrs={
'class': 'form-control',
'accept': '.xlsx,.xls'
})
)
sat_choice = forms.ModelChoiceField(
queryset=Satellite.objects.all(),
label="Выберите спутник",
widget=forms.Select(attrs={
'class': 'form-select'
})
)
number_input = forms.IntegerField(
label="Введите число объектов",
min_value=0,
widget=forms.NumberInput(attrs={
'class': 'form-control'
})
)
class LoadCsvData(forms.Form):
file = forms.FileField(
label="Выберите CSV файл",
widget=forms.FileInput(attrs={
'class': 'form-control',
'accept': '.csv'
})
)
class UploadVchLoad(UploadFileForm):
sat_choice = forms.ModelChoiceField(
queryset=Satellite.objects.all(),
label="Выберите спутник",
widget=forms.Select(attrs={
'class': 'form-select'
})
)
class VchLinkForm(forms.Form):
sat_choice = forms.ModelChoiceField(
queryset=Satellite.objects.all(),
label="Выберите спутник",
widget=forms.Select(attrs={
'class': 'form-select'
})
)
# ku_range = forms.ChoiceField(
# choices=[(9750.0, '9750'), (10750.0, '10750')],
# # coerce=lambda x: x == 'True',
# widget=forms.Select(attrs={'class': 'form-select'}),
# label='Выбор диапазона'
# )
value1 = forms.FloatField(
label="Первое число",
widget=forms.NumberInput(attrs={
'class': 'form-control',
'placeholder': 'Введите первое число'
})
)
value2 = forms.FloatField(
label="Второе число",
widget=forms.NumberInput(attrs={
'class': 'form-control',
'placeholder': 'Введите второе число'
})
)
class NewEventForm(forms.Form):
# sat_choice = forms.ModelChoiceField(
# queryset=Satellite.objects.all(),
# label="Выберите спутник",
# widget=forms.Select(attrs={
# 'class': 'form-select'
# })
# )
# pol_choice = forms.ModelChoiceField(
# queryset=Polarization.objects.all(),
# label="Выберите поляризацию",
# widget=forms.Select(attrs={
# 'class': 'form-select'
# })
# )
file = forms.FileField(
label="Выберите файл",
widget=forms.FileInput(attrs={
'class': 'form-control',
'accept': '.xlsx,.xls'
})
)
class FillLyngsatDataForm(forms.Form):
"""Форма для заполнения данных из Lyngsat"""
REGION_CHOICES = [
('europe', 'Европа'),
('asia', 'Азия'),
('america', 'Америка'),
('atlantic', 'Атлантика'),
]
satellites = forms.ModelMultipleChoiceField(
queryset=Satellite.objects.all().order_by('name'),
label="Выберите спутники",
widget=forms.SelectMultiple(attrs={
'class': 'form-select',
'size': '10'
}),
required=True,
help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких спутников"
)
regions = forms.MultipleChoiceField(
choices=REGION_CHOICES,
label="Выберите регионы",
widget=forms.SelectMultiple(attrs={
'class': 'form-select',
'size': '4'
}),
required=True,
initial=['europe', 'asia', 'america', 'atlantic'],
help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких регионов"
)
class ParameterForm(forms.ModelForm):
"""
Форма для создания и редактирования параметров ВЧ загрузки.
Работает с одним экземпляром Parameter, связанным с ObjItem через OneToOne связь.
"""
class Meta:
model = Parameter
fields = [
'id_satellite', 'frequency', 'freq_range', 'polarization',
'bod_velocity', 'modulation', 'snr', 'standard'
]
widgets = {
'id_satellite': forms.Select(attrs={
'class': 'form-select',
'required': True
}),
'frequency': forms.NumberInput(attrs={
'class': 'form-control',
'step': '0.000001',
'min': '0',
'max': '50000',
'placeholder': 'Введите частоту в МГц'
}),
'freq_range': forms.NumberInput(attrs={
'class': 'form-control',
'step': '0.000001',
'min': '0',
'max': '1000',
'placeholder': 'Введите полосу частот в МГц'
}),
'bod_velocity': forms.NumberInput(attrs={
'class': 'form-control',
'step': '0.001',
'min': '0',
'placeholder': 'Введите символьную скорость в БОД'
}),
'snr': forms.NumberInput(attrs={
'class': 'form-control',
'step': '0.001',
'min': '-50',
'max': '100',
'placeholder': 'Введите ОСШ в дБ'
}),
'polarization': forms.Select(attrs={'class': 'form-select'}),
'modulation': forms.Select(attrs={'class': 'form-select'}),
'standard': forms.Select(attrs={'class': 'form-select'}),
}
labels = {
'id_satellite': 'Спутник',
'frequency': 'Частота (МГц)',
'freq_range': 'Полоса частот (МГц)',
'polarization': 'Поляризация',
'bod_velocity': 'Символьная скорость (БОД)',
'modulation': 'Модуляция',
'snr': 'ОСШ (дБ)',
'standard': 'Стандарт',
}
help_texts = {
'frequency': 'Частота в диапазоне от 0 до 50000 МГц',
'freq_range': 'Полоса частот в диапазоне от 0 до 1000 МГц',
'bod_velocity': 'Символьная скорость должна быть положительной',
'snr': 'Отношение сигнал/шум в диапазоне от -50 до 100 дБ',
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Динамически загружаем choices для select полей
self.fields['id_satellite'].queryset = Satellite.objects.all().order_by('name')
self.fields['polarization'].queryset = Polarization.objects.all().order_by('name')
self.fields['modulation'].queryset = Modulation.objects.all().order_by('name')
self.fields['standard'].queryset = Standard.objects.all().order_by('name')
# Делаем спутник обязательным полем
self.fields['id_satellite'].required = True
def clean(self):
"""
Дополнительная валидация формы.
Проверяет соотношение между частотой, полосой частот и символьной скоростью.
"""
cleaned_data = super().clean()
frequency = cleaned_data.get('frequency')
freq_range = cleaned_data.get('freq_range')
bod_velocity = cleaned_data.get('bod_velocity')
# Проверка что частота больше полосы частот
if frequency and freq_range:
if freq_range > frequency:
self.add_error('freq_range', 'Полоса частот не может быть больше частоты')
# Проверка что символьная скорость соответствует полосе частот
if bod_velocity and freq_range:
if bod_velocity > freq_range * 1000000: # Конвертация МГц в Гц
self.add_error('bod_velocity', 'Символьная скорость не может превышать полосу частот')
return cleaned_data
class GeoForm(forms.ModelForm):
class Meta:
model = Geo
fields = ['location', 'comment', 'is_average']
widgets = {
'location': forms.TextInput(attrs={'class': 'form-control'}),
'comment': forms.TextInput(attrs={'class': 'form-control'}),
'is_average': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
}
class ObjItemForm(forms.ModelForm):
"""
Форма для создания и редактирования объектов (источников сигнала).
Работает с моделью ObjItem. Параметры ВЧ загрузки обрабатываются отдельно
через ParameterForm с использованием OneToOne связи.
"""
class Meta:
model = ObjItem
fields = ['name']
widgets = {
'name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Введите название объекта',
'maxlength': '100'
}),
}
labels = {
'name': 'Название объекта',
}
help_texts = {
'name': 'Уникальное название объекта/источника сигнала',
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Делаем поле name необязательным, так как оно может быть пустым
self.fields['name'].required = False
def clean_name(self):
"""
Валидация поля name.
Проверяет что название не состоит только из пробелов.
"""
name = self.cleaned_data.get('name')
if name:
# Удаляем лишние пробелы
name = name.strip()
# Проверяем что после удаления пробелов что-то осталось
if not name:
raise forms.ValidationError('Название не может состоять только из пробелов')
# Django imports
from django import forms
# Local imports
from .models import Geo, Modulation, ObjItem, Parameter, Polarization, Satellite, Standard
class UploadFileForm(forms.Form):
file = forms.FileField(
label="Выберите файл",
widget=forms.FileInput(attrs={
'class': 'form-file-input'
})
)
class LoadExcelData(forms.Form):
file = forms.FileField(
label="Выберите Excel файл",
widget=forms.FileInput(attrs={
'class': 'form-control',
'accept': '.xlsx,.xls'
})
)
sat_choice = forms.ModelChoiceField(
queryset=Satellite.objects.all(),
label="Выберите спутник",
widget=forms.Select(attrs={
'class': 'form-select'
})
)
number_input = forms.IntegerField(
label="Введите число объектов",
min_value=0,
widget=forms.NumberInput(attrs={
'class': 'form-control'
})
)
class LoadCsvData(forms.Form):
file = forms.FileField(
label="Выберите CSV файл",
widget=forms.FileInput(attrs={
'class': 'form-control',
'accept': '.csv'
})
)
class UploadVchLoad(UploadFileForm):
sat_choice = forms.ModelChoiceField(
queryset=Satellite.objects.all(),
label="Выберите спутник",
widget=forms.Select(attrs={
'class': 'form-select'
})
)
class VchLinkForm(forms.Form):
sat_choice = forms.ModelChoiceField(
queryset=Satellite.objects.all(),
label="Выберите спутник",
widget=forms.Select(attrs={
'class': 'form-select'
})
)
# ku_range = forms.ChoiceField(
# choices=[(9750.0, '9750'), (10750.0, '10750')],
# # coerce=lambda x: x == 'True',
# widget=forms.Select(attrs={'class': 'form-select'}),
# label='Выбор диапазона'
# )
value1 = forms.FloatField(
label="Первое число",
widget=forms.NumberInput(attrs={
'class': 'form-control',
'placeholder': 'Введите первое число'
})
)
value2 = forms.FloatField(
label="Второе число",
widget=forms.NumberInput(attrs={
'class': 'form-control',
'placeholder': 'Введите второе число'
})
)
class NewEventForm(forms.Form):
# sat_choice = forms.ModelChoiceField(
# queryset=Satellite.objects.all(),
# label="Выберите спутник",
# widget=forms.Select(attrs={
# 'class': 'form-select'
# })
# )
# pol_choice = forms.ModelChoiceField(
# queryset=Polarization.objects.all(),
# label="Выберите поляризацию",
# widget=forms.Select(attrs={
# 'class': 'form-select'
# })
# )
file = forms.FileField(
label="Выберите файл",
widget=forms.FileInput(attrs={
'class': 'form-control',
'accept': '.xlsx,.xls'
})
)
class FillLyngsatDataForm(forms.Form):
"""Форма для заполнения данных из Lyngsat"""
REGION_CHOICES = [
('europe', 'Европа'),
('asia', 'Азия'),
('america', 'Америка'),
('atlantic', 'Атлантика'),
]
satellites = forms.ModelMultipleChoiceField(
queryset=Satellite.objects.all().order_by('name'),
label="Выберите спутники",
widget=forms.SelectMultiple(attrs={
'class': 'form-select',
'size': '10'
}),
required=True,
help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких спутников"
)
regions = forms.MultipleChoiceField(
choices=REGION_CHOICES,
label="Выберите регионы",
widget=forms.SelectMultiple(attrs={
'class': 'form-select',
'size': '4'
}),
required=True,
initial=['europe', 'asia', 'america', 'atlantic'],
help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких регионов"
)
class ParameterForm(forms.ModelForm):
"""
Форма для создания и редактирования параметров ВЧ загрузки.
Работает с одним экземпляром Parameter, связанным с ObjItem через OneToOne связь.
"""
class Meta:
model = Parameter
fields = [
'id_satellite', 'frequency', 'freq_range', 'polarization',
'bod_velocity', 'modulation', 'snr', 'standard'
]
widgets = {
'id_satellite': forms.Select(attrs={
'class': 'form-select',
'required': True
}),
'frequency': forms.NumberInput(attrs={
'class': 'form-control',
'step': '0.000001',
'min': '0',
'max': '50000',
'placeholder': 'Введите частоту в МГц'
}),
'freq_range': forms.NumberInput(attrs={
'class': 'form-control',
'step': '0.000001',
'min': '0',
'max': '1000',
'placeholder': 'Введите полосу частот в МГц'
}),
'bod_velocity': forms.NumberInput(attrs={
'class': 'form-control',
'step': '0.001',
'min': '0',
'placeholder': 'Введите символьную скорость в БОД'
}),
'snr': forms.NumberInput(attrs={
'class': 'form-control',
'step': '0.001',
'min': '-50',
'max': '100',
'placeholder': 'Введите ОСШ в дБ'
}),
'polarization': forms.Select(attrs={'class': 'form-select'}),
'modulation': forms.Select(attrs={'class': 'form-select'}),
'standard': forms.Select(attrs={'class': 'form-select'}),
}
labels = {
'id_satellite': 'Спутник',
'frequency': 'Частота (МГц)',
'freq_range': 'Полоса частот (МГц)',
'polarization': 'Поляризация',
'bod_velocity': 'Символьная скорость (БОД)',
'modulation': 'Модуляция',
'snr': 'ОСШ (дБ)',
'standard': 'Стандарт',
}
help_texts = {
'frequency': 'Частота в диапазоне от 0 до 50000 МГц',
'freq_range': 'Полоса частот в диапазоне от 0 до 1000 МГц',
'bod_velocity': 'Символьная скорость должна быть положительной',
'snr': 'Отношение сигнал/шум в диапазоне от -50 до 100 дБ',
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Динамически загружаем choices для select полей
self.fields['id_satellite'].queryset = Satellite.objects.all().order_by('name')
self.fields['polarization'].queryset = Polarization.objects.all().order_by('name')
self.fields['modulation'].queryset = Modulation.objects.all().order_by('name')
self.fields['standard'].queryset = Standard.objects.all().order_by('name')
# Делаем спутник обязательным полем
self.fields['id_satellite'].required = True
def clean(self):
"""
Дополнительная валидация формы.
Проверяет соотношение между частотой, полосой частот и символьной скоростью.
"""
cleaned_data = super().clean()
frequency = cleaned_data.get('frequency')
freq_range = cleaned_data.get('freq_range')
bod_velocity = cleaned_data.get('bod_velocity')
# Проверка что частота больше полосы частот
if frequency and freq_range:
if freq_range > frequency:
self.add_error('freq_range', 'Полоса частот не может быть больше частоты')
# Проверка что символьная скорость соответствует полосе частот
if bod_velocity and freq_range:
if bod_velocity > freq_range * 1000000: # Конвертация МГц в Гц
self.add_error('bod_velocity', 'Символьная скорость не может превышать полосу частот')
return cleaned_data
class GeoForm(forms.ModelForm):
class Meta:
model = Geo
fields = ['location', 'comment', 'is_average']
widgets = {
'location': forms.TextInput(attrs={'class': 'form-control'}),
'comment': forms.TextInput(attrs={'class': 'form-control'}),
'is_average': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
}
class ObjItemForm(forms.ModelForm):
"""
Форма для создания и редактирования объектов (источников сигнала).
Работает с моделью ObjItem. Параметры ВЧ загрузки обрабатываются отдельно
через ParameterForm с использованием OneToOne связи.
"""
class Meta:
model = ObjItem
fields = ['name']
widgets = {
'name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Введите название объекта',
'maxlength': '100'
}),
}
labels = {
'name': 'Название объекта',
}
help_texts = {
'name': 'Уникальное название объекта/источника сигнала',
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Делаем поле name необязательным, так как оно может быть пустым
self.fields['name'].required = False
def clean_name(self):
"""
Валидация поля name.
Проверяет что название не состоит только из пробелов.
"""
name = self.cleaned_data.get('name')
if name:
# Удаляем лишние пробелы
name = name.strip()
# Проверяем что после удаления пробелов что-то осталось
if not name:
raise forms.ValidationError('Название не может состоять только из пробелов')
return name

View File

@@ -0,0 +1,24 @@
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!'))

View File

@@ -1,204 +1,204 @@
# Generated by Django 5.2.7 on 2025-10-31 13:36
import django.contrib.gis.db.models.fields
import django.contrib.gis.db.models.functions
import django.db.models.deletion
import django.db.models.expressions
import mainapp.models
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Mirror',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=30, unique=True, verbose_name='Имя зеркала')),
],
options={
'verbose_name': 'Зеркало',
'verbose_name_plural': 'Зеркала',
},
),
migrations.CreateModel(
name='Modulation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(db_index=True, max_length=20, unique=True, verbose_name='Модуляция')),
],
options={
'verbose_name': 'Модуляция',
'verbose_name_plural': 'Модуляции',
},
),
migrations.CreateModel(
name='Polarization',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=20, unique=True, verbose_name='Поляризация')),
],
options={
'verbose_name': 'Поляризация',
'verbose_name_plural': 'Поляризация',
},
),
migrations.CreateModel(
name='Satellite',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(db_index=True, max_length=100, unique=True, verbose_name='Имя спутника')),
('norad', models.IntegerField(blank=True, null=True, verbose_name='NORAD ID')),
],
options={
'verbose_name': 'Спутник',
'verbose_name_plural': 'Спутники',
},
),
migrations.CreateModel(
name='SigmaParMark',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('mark', models.BooleanField(blank=True, null=True, verbose_name='Наличие сигнала')),
('timestamp', models.DateTimeField(blank=True, null=True, verbose_name='Время')),
],
options={
'verbose_name': 'Отметка',
'verbose_name_plural': 'Отметки',
},
),
migrations.CreateModel(
name='Standard',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=20, unique=True, verbose_name='Стандарт')),
],
options={
'verbose_name': 'Стандарт',
'verbose_name_plural': 'Стандарты',
},
),
migrations.CreateModel(
name='CustomUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('role', models.CharField(choices=[('admin', 'Администратор'), ('moderator', 'Модератор'), ('user', 'Пользователь')], default='user', max_length=20, verbose_name='Роль пользователя')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Пользователь',
'verbose_name_plural': 'Пользователи',
},
),
migrations.CreateModel(
name='ObjItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(blank=True, db_index=True, max_length=100, null=True, verbose_name='Имя объекта')),
('id_user_add', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems', to='mainapp.customuser', verbose_name='Пользователь')),
],
options={
'verbose_name': 'Объект',
'verbose_name_plural': 'Объекты',
},
),
migrations.CreateModel(
name='Parameter',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('frequency', models.FloatField(blank=True, db_index=True, default=0, null=True, verbose_name='Частота, МГц')),
('freq_range', models.FloatField(blank=True, default=0, null=True, verbose_name='Полоса частот, МГц')),
('bod_velocity', models.FloatField(blank=True, default=0, null=True, verbose_name='Символьная скорость, БОД')),
('snr', models.FloatField(blank=True, default=0, null=True, verbose_name='ОСШ')),
('id_user_add', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parameter_added', to='mainapp.customuser', verbose_name='Пользователь')),
('modulation', models.ForeignKey(blank=True, default=mainapp.models.get_default_modulation, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='modulations', to='mainapp.modulation', verbose_name='Модуляция')),
('objitems', models.ManyToManyField(blank=True, related_name='parameters_obj', to='mainapp.objitem', verbose_name='Источники')),
('polarization', models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='polarizations', to='mainapp.polarization', verbose_name='Поляризация')),
('id_satellite', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='parameters', to='mainapp.satellite', verbose_name='Спутник')),
('standard', models.ForeignKey(blank=True, default=mainapp.models.get_default_standard, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='standards', to='mainapp.standard', verbose_name='Стандарт')),
],
options={
'verbose_name': 'ВЧ загрузка',
'verbose_name_plural': 'ВЧ загрузки',
},
),
migrations.CreateModel(
name='SourceType',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, unique=True, verbose_name='Тип источника')),
('objitem', models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_type_obj', to='mainapp.objitem', verbose_name='Гео')),
],
options={
'verbose_name': 'Тип источника',
'verbose_name_plural': 'Типы источников',
},
),
migrations.CreateModel(
name='SigmaParameter',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('transfer', models.FloatField(choices=[(-1.0, '-'), (9750.0, '9750 МГц'), (10750.0, '10750 МГц')], default=-1.0, verbose_name='Перенос по частоте')),
('status', models.CharField(blank=True, max_length=20, null=True, verbose_name='Статус')),
('frequency', models.FloatField(blank=True, db_index=True, default=0, null=True, verbose_name='Частота, МГц')),
('transfer_frequency', models.GeneratedField(db_persist=True, expression=models.ExpressionWrapper(django.db.models.expressions.CombinedExpression(models.F('frequency'), '+', models.F('transfer')), output_field=models.FloatField()), null=True, output_field=models.FloatField(), verbose_name='Частота в Ku, МГц')),
('freq_range', models.FloatField(blank=True, default=0, null=True, verbose_name='Полоса частот, МГц')),
('power', models.FloatField(blank=True, default=0, null=True, verbose_name='Мощность, дБм')),
('bod_velocity', models.FloatField(blank=True, default=0, null=True, verbose_name='Символьная скорость, БОД')),
('snr', models.FloatField(blank=True, default=0, null=True, verbose_name='ОСШ, Дб')),
('packets', models.BooleanField(blank=True, null=True, verbose_name='Пакетность')),
('datetime_begin', models.DateTimeField(blank=True, null=True, verbose_name='Время начала измерения')),
('datetime_end', models.DateTimeField(blank=True, null=True, verbose_name='Время окончания измерения')),
('id_satellite', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='sigmapar_sat', to='mainapp.satellite', verbose_name='Спутник')),
('modulation', models.ForeignKey(blank=True, default=mainapp.models.get_default_modulation, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='modulations_sigma', to='mainapp.modulation', verbose_name='Модуляция')),
('parameter', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sigma_parameter', to='mainapp.parameter', verbose_name='ВЧ')),
('polarization', models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='polarizations_sigma', to='mainapp.polarization', verbose_name='Поляризация')),
('mark', models.ManyToManyField(blank=True, to='mainapp.sigmaparmark', verbose_name='Отметка')),
('standard', models.ForeignKey(blank=True, default=mainapp.models.get_default_standard, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='standards_sigma', to='mainapp.standard', verbose_name='Стандарт')),
],
options={
'verbose_name': 'ВЧ sigma',
'verbose_name_plural': 'ВЧ sigma',
},
),
migrations.CreateModel(
name='Geo',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='Время')),
('coords', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326, verbose_name='Координата геолокации')),
('location', models.CharField(blank=True, max_length=255, null=True, verbose_name='Метоположение')),
('comment', models.CharField(blank=True, max_length=255, verbose_name='Комментарий')),
('coords_kupsat', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326, verbose_name='Координаты Кубсата')),
('coords_valid', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326, verbose_name='Координаты оперативников')),
('is_average', models.BooleanField(blank=True, null=True, verbose_name='Усреднённое')),
('distance_coords_kup', models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords', 'coords_kupsat'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между купсатом и гео, км')),
('distance_coords_valid', models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords', 'coords_valid'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между гео и оперативным отделом, км')),
('distance_kup_valid', models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords_valid', 'coords_kupsat'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между купсатом и оперативным отделом, км')),
('id_user_add', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='geos_added', to='mainapp.customuser', verbose_name='Пользователь')),
('mirrors', models.ManyToManyField(related_name='geo_mirrors', to='mainapp.mirror', verbose_name='Зеркала')),
('objitem', models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='geo_obj', to='mainapp.objitem', verbose_name='Гео')),
],
options={
'verbose_name': 'Гео',
'verbose_name_plural': 'Гео',
'constraints': [models.UniqueConstraint(fields=('timestamp', 'coords'), name='unique_geo_combination')],
},
),
migrations.AddIndex(
model_name='parameter',
index=models.Index(fields=['id_satellite', 'frequency'], name='mainapp_par_id_sate_cbfab2_idx'),
),
migrations.AddIndex(
model_name='parameter',
index=models.Index(fields=['frequency', 'polarization'], name='mainapp_par_frequen_75a049_idx'),
),
]
# Generated by Django 5.2.7 on 2025-10-31 13:36
import django.contrib.gis.db.models.fields
import django.contrib.gis.db.models.functions
import django.db.models.deletion
import django.db.models.expressions
import mainapp.models
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Mirror',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=30, unique=True, verbose_name='Имя зеркала')),
],
options={
'verbose_name': 'Зеркало',
'verbose_name_plural': 'Зеркала',
},
),
migrations.CreateModel(
name='Modulation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(db_index=True, max_length=20, unique=True, verbose_name='Модуляция')),
],
options={
'verbose_name': 'Модуляция',
'verbose_name_plural': 'Модуляции',
},
),
migrations.CreateModel(
name='Polarization',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=20, unique=True, verbose_name='Поляризация')),
],
options={
'verbose_name': 'Поляризация',
'verbose_name_plural': 'Поляризация',
},
),
migrations.CreateModel(
name='Satellite',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(db_index=True, max_length=100, unique=True, verbose_name='Имя спутника')),
('norad', models.IntegerField(blank=True, null=True, verbose_name='NORAD ID')),
],
options={
'verbose_name': 'Спутник',
'verbose_name_plural': 'Спутники',
},
),
migrations.CreateModel(
name='SigmaParMark',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('mark', models.BooleanField(blank=True, null=True, verbose_name='Наличие сигнала')),
('timestamp', models.DateTimeField(blank=True, null=True, verbose_name='Время')),
],
options={
'verbose_name': 'Отметка',
'verbose_name_plural': 'Отметки',
},
),
migrations.CreateModel(
name='Standard',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=20, unique=True, verbose_name='Стандарт')),
],
options={
'verbose_name': 'Стандарт',
'verbose_name_plural': 'Стандарты',
},
),
migrations.CreateModel(
name='CustomUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('role', models.CharField(choices=[('admin', 'Администратор'), ('moderator', 'Модератор'), ('user', 'Пользователь')], default='user', max_length=20, verbose_name='Роль пользователя')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Пользователь',
'verbose_name_plural': 'Пользователи',
},
),
migrations.CreateModel(
name='ObjItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(blank=True, db_index=True, max_length=100, null=True, verbose_name='Имя объекта')),
('id_user_add', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems', to='mainapp.customuser', verbose_name='Пользователь')),
],
options={
'verbose_name': 'Объект',
'verbose_name_plural': 'Объекты',
},
),
migrations.CreateModel(
name='Parameter',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('frequency', models.FloatField(blank=True, db_index=True, default=0, null=True, verbose_name='Частота, МГц')),
('freq_range', models.FloatField(blank=True, default=0, null=True, verbose_name='Полоса частот, МГц')),
('bod_velocity', models.FloatField(blank=True, default=0, null=True, verbose_name='Символьная скорость, БОД')),
('snr', models.FloatField(blank=True, default=0, null=True, verbose_name='ОСШ')),
('id_user_add', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parameter_added', to='mainapp.customuser', verbose_name='Пользователь')),
('modulation', models.ForeignKey(blank=True, default=mainapp.models.get_default_modulation, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='modulations', to='mainapp.modulation', verbose_name='Модуляция')),
('objitems', models.ManyToManyField(blank=True, related_name='parameters_obj', to='mainapp.objitem', verbose_name='Источники')),
('polarization', models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='polarizations', to='mainapp.polarization', verbose_name='Поляризация')),
('id_satellite', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='parameters', to='mainapp.satellite', verbose_name='Спутник')),
('standard', models.ForeignKey(blank=True, default=mainapp.models.get_default_standard, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='standards', to='mainapp.standard', verbose_name='Стандарт')),
],
options={
'verbose_name': 'ВЧ загрузка',
'verbose_name_plural': 'ВЧ загрузки',
},
),
migrations.CreateModel(
name='SourceType',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, unique=True, verbose_name='Тип источника')),
('objitem', models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_type_obj', to='mainapp.objitem', verbose_name='Гео')),
],
options={
'verbose_name': 'Тип источника',
'verbose_name_plural': 'Типы источников',
},
),
migrations.CreateModel(
name='SigmaParameter',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('transfer', models.FloatField(choices=[(-1.0, '-'), (9750.0, '9750 МГц'), (10750.0, '10750 МГц')], default=-1.0, verbose_name='Перенос по частоте')),
('status', models.CharField(blank=True, max_length=20, null=True, verbose_name='Статус')),
('frequency', models.FloatField(blank=True, db_index=True, default=0, null=True, verbose_name='Частота, МГц')),
('transfer_frequency', models.GeneratedField(db_persist=True, expression=models.ExpressionWrapper(django.db.models.expressions.CombinedExpression(models.F('frequency'), '+', models.F('transfer')), output_field=models.FloatField()), null=True, output_field=models.FloatField(), verbose_name='Частота в Ku, МГц')),
('freq_range', models.FloatField(blank=True, default=0, null=True, verbose_name='Полоса частот, МГц')),
('power', models.FloatField(blank=True, default=0, null=True, verbose_name='Мощность, дБм')),
('bod_velocity', models.FloatField(blank=True, default=0, null=True, verbose_name='Символьная скорость, БОД')),
('snr', models.FloatField(blank=True, default=0, null=True, verbose_name='ОСШ, Дб')),
('packets', models.BooleanField(blank=True, null=True, verbose_name='Пакетность')),
('datetime_begin', models.DateTimeField(blank=True, null=True, verbose_name='Время начала измерения')),
('datetime_end', models.DateTimeField(blank=True, null=True, verbose_name='Время окончания измерения')),
('id_satellite', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='sigmapar_sat', to='mainapp.satellite', verbose_name='Спутник')),
('modulation', models.ForeignKey(blank=True, default=mainapp.models.get_default_modulation, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='modulations_sigma', to='mainapp.modulation', verbose_name='Модуляция')),
('parameter', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sigma_parameter', to='mainapp.parameter', verbose_name='ВЧ')),
('polarization', models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='polarizations_sigma', to='mainapp.polarization', verbose_name='Поляризация')),
('mark', models.ManyToManyField(blank=True, to='mainapp.sigmaparmark', verbose_name='Отметка')),
('standard', models.ForeignKey(blank=True, default=mainapp.models.get_default_standard, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='standards_sigma', to='mainapp.standard', verbose_name='Стандарт')),
],
options={
'verbose_name': 'ВЧ sigma',
'verbose_name_plural': 'ВЧ sigma',
},
),
migrations.CreateModel(
name='Geo',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='Время')),
('coords', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326, verbose_name='Координата геолокации')),
('location', models.CharField(blank=True, max_length=255, null=True, verbose_name='Метоположение')),
('comment', models.CharField(blank=True, max_length=255, verbose_name='Комментарий')),
('coords_kupsat', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326, verbose_name='Координаты Кубсата')),
('coords_valid', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326, verbose_name='Координаты оперативников')),
('is_average', models.BooleanField(blank=True, null=True, verbose_name='Усреднённое')),
('distance_coords_kup', models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords', 'coords_kupsat'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между купсатом и гео, км')),
('distance_coords_valid', models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords', 'coords_valid'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между гео и оперативным отделом, км')),
('distance_kup_valid', models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords_valid', 'coords_kupsat'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между купсатом и оперативным отделом, км')),
('id_user_add', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='geos_added', to='mainapp.customuser', verbose_name='Пользователь')),
('mirrors', models.ManyToManyField(related_name='geo_mirrors', to='mainapp.mirror', verbose_name='Зеркала')),
('objitem', models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='geo_obj', to='mainapp.objitem', verbose_name='Гео')),
],
options={
'verbose_name': 'Гео',
'verbose_name_plural': 'Гео',
'constraints': [models.UniqueConstraint(fields=('timestamp', 'coords'), name='unique_geo_combination')],
},
),
migrations.AddIndex(
model_name='parameter',
index=models.Index(fields=['id_satellite', 'frequency'], name='mainapp_par_id_sate_cbfab2_idx'),
),
migrations.AddIndex(
model_name='parameter',
index=models.Index(fields=['frequency', 'polarization'], name='mainapp_par_frequen_75a049_idx'),
),
]

View File

@@ -1,35 +1,35 @@
# Generated by Django 5.2.7 on 2025-10-31 13:56
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='objitem',
name='created_at',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Дата создания'),
),
migrations.AddField(
model_name='objitem',
name='created_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_created', to='mainapp.customuser', verbose_name='Создан пользователем'),
),
migrations.AddField(
model_name='objitem',
name='updated_at',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Дата последнего изменения'),
),
migrations.AddField(
model_name='objitem',
name='updated_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_updated', to='mainapp.customuser', verbose_name='Изменен пользователем'),
),
]
# Generated by Django 5.2.7 on 2025-10-31 13:56
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='objitem',
name='created_at',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Дата создания'),
),
migrations.AddField(
model_name='objitem',
name='created_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_created', to='mainapp.customuser', verbose_name='Создан пользователем'),
),
migrations.AddField(
model_name='objitem',
name='updated_at',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Дата последнего изменения'),
),
migrations.AddField(
model_name='objitem',
name='updated_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_updated', to='mainapp.customuser', verbose_name='Изменен пользователем'),
),
]

View File

@@ -1,23 +1,23 @@
# Generated by Django 5.2.7 on 2025-10-31 14:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0002_objitem_created_at_objitem_created_by_and_more'),
]
operations = [
migrations.AlterField(
model_name='objitem',
name='created_at',
field=models.DateTimeField(auto_now_add=True, verbose_name='Дата создания'),
),
migrations.AlterField(
model_name='objitem',
name='updated_at',
field=models.DateTimeField(auto_now=True, verbose_name='Дата последнего изменения'),
),
]
# Generated by Django 5.2.7 on 2025-10-31 14:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0002_objitem_created_at_objitem_created_by_and_more'),
]
operations = [
migrations.AlterField(
model_name='objitem',
name='created_at',
field=models.DateTimeField(auto_now_add=True, verbose_name='Дата создания'),
),
migrations.AlterField(
model_name='objitem',
name='updated_at',
field=models.DateTimeField(auto_now=True, verbose_name='Дата последнего изменения'),
),
]

View File

@@ -1,25 +1,25 @@
# Generated by Django 5.2.7 on 2025-11-01 07:38
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0003_alter_objitem_created_at_alter_objitem_updated_at'),
]
operations = [
migrations.RemoveField(
model_name='geo',
name='id_user_add',
),
migrations.RemoveField(
model_name='objitem',
name='id_user_add',
),
migrations.RemoveField(
model_name='parameter',
name='id_user_add',
),
]
# Generated by Django 5.2.7 on 2025-11-01 07:38
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0003_alter_objitem_created_at_alter_objitem_updated_at'),
]
operations = [
migrations.RemoveField(
model_name='geo',
name='id_user_add',
),
migrations.RemoveField(
model_name='objitem',
name='id_user_add',
),
migrations.RemoveField(
model_name='parameter',
name='id_user_add',
),
]

View File

@@ -1,19 +1,19 @@
# Generated by Django 5.2.7 on 2025-11-07 19:35
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0004_remove_geo_id_user_add_remove_objitem_id_user_add_and_more'),
]
operations = [
migrations.AlterField(
model_name='geo',
name='objitem',
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='geo_obj', to='mainapp.objitem', verbose_name='Гео'),
),
]
# Generated by Django 5.2.7 on 2025-11-07 19:35
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0004_remove_geo_id_user_add_remove_objitem_id_user_add_and_more'),
]
operations = [
migrations.AlterField(
model_name='geo',
name='objitem',
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='geo_obj', to='mainapp.objitem', verbose_name='Гео'),
),
]

View File

@@ -1,290 +1,290 @@
# Generated by Django 5.2.7 on 2025-11-07 20:58
import django.contrib.gis.db.models.fields
import django.contrib.gis.db.models.functions
import django.core.validators
import django.db.models.deletion
import django.db.models.expressions
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0005_alter_geo_objitem'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterModelOptions(
name='customuser',
options={'ordering': ['user__username'], 'verbose_name': 'Пользователь', 'verbose_name_plural': 'Пользователи'},
),
migrations.AlterModelOptions(
name='geo',
options={'ordering': ['-timestamp'], 'verbose_name': 'Гео', 'verbose_name_plural': 'Гео'},
),
migrations.AlterModelOptions(
name='mirror',
options={'ordering': ['name'], 'verbose_name': 'Зеркало', 'verbose_name_plural': 'Зеркала'},
),
migrations.AlterModelOptions(
name='modulation',
options={'ordering': ['name'], 'verbose_name': 'Модуляция', 'verbose_name_plural': 'Модуляции'},
),
migrations.AlterModelOptions(
name='objitem',
options={'ordering': ['-updated_at'], 'verbose_name': 'Объект', 'verbose_name_plural': 'Объекты'},
),
migrations.AlterModelOptions(
name='polarization',
options={'ordering': ['name'], 'verbose_name': 'Поляризация', 'verbose_name_plural': 'Поляризация'},
),
migrations.AlterModelOptions(
name='satellite',
options={'ordering': ['name'], 'verbose_name': 'Спутник', 'verbose_name_plural': 'Спутники'},
),
migrations.AlterModelOptions(
name='sigmaparmark',
options={'ordering': ['-timestamp'], 'verbose_name': 'Отметка', 'verbose_name_plural': 'Отметки'},
),
migrations.AlterModelOptions(
name='sourcetype',
options={'ordering': ['name'], 'verbose_name': 'Тип источника', 'verbose_name_plural': 'Типы источников'},
),
migrations.AlterModelOptions(
name='standard',
options={'ordering': ['name'], 'verbose_name': 'Стандарт', 'verbose_name_plural': 'Стандарты'},
),
migrations.AlterField(
model_name='customuser',
name='role',
field=models.CharField(choices=[('admin', 'Администратор'), ('moderator', 'Модератор'), ('user', 'Пользователь')], db_index=True, default='user', help_text='Роль пользователя в системе', max_length=20, verbose_name='Роль пользователя'),
),
migrations.AlterField(
model_name='customuser',
name='user',
field=models.OneToOneField(help_text='Связанный пользователь Django', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Пользователь'),
),
migrations.AlterField(
model_name='geo',
name='comment',
field=models.CharField(blank=True, help_text='Дополнительные комментарии', max_length=255, verbose_name='Комментарий'),
),
migrations.AlterField(
model_name='geo',
name='coords',
field=django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Основные координаты геолокации (WGS84)', null=True, srid=4326, verbose_name='Координата геолокации'),
),
migrations.AlterField(
model_name='geo',
name='coords_kupsat',
field=django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Координаты, полученные от кубсата (WGS84)', null=True, srid=4326, verbose_name='Координаты Кубсата'),
),
migrations.AlterField(
model_name='geo',
name='coords_valid',
field=django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Координаты, предоставленные оперативным отделом (WGS84)', null=True, srid=4326, verbose_name='Координаты оперативников'),
),
migrations.AlterField(
model_name='geo',
name='distance_coords_kup',
field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords', 'coords_kupsat'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между кубсатом и гео, км'),
),
migrations.AlterField(
model_name='geo',
name='distance_kup_valid',
field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords_valid', 'coords_kupsat'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между кубсатом и оперативным отделом, км'),
),
migrations.AlterField(
model_name='geo',
name='is_average',
field=models.BooleanField(blank=True, help_text='Является ли координата усредненной', null=True, verbose_name='Усреднённое'),
),
migrations.AlterField(
model_name='geo',
name='location',
field=models.CharField(blank=True, help_text='Текстовое описание местоположения', max_length=255, null=True, verbose_name='Местоположение'),
),
migrations.AlterField(
model_name='geo',
name='mirrors',
field=models.ManyToManyField(blank=True, help_text='Зеркала антенн, использованные для приема', related_name='geo_mirrors', to='mainapp.mirror', verbose_name='Зеркала'),
),
migrations.AlterField(
model_name='geo',
name='objitem',
field=models.OneToOneField(help_text='Связанный объект', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='geo_obj', to='mainapp.objitem', verbose_name='Объект'),
),
migrations.AlterField(
model_name='geo',
name='timestamp',
field=models.DateTimeField(blank=True, db_index=True, help_text='Время фиксации геолокации', null=True, verbose_name='Время'),
),
migrations.AlterField(
model_name='mirror',
name='name',
field=models.CharField(db_index=True, help_text='Уникальное название зеркала антенны', max_length=30, unique=True, verbose_name='Имя зеркала'),
),
migrations.AlterField(
model_name='modulation',
name='name',
field=models.CharField(db_index=True, help_text='Тип модуляции сигнала (QPSK, 8PSK, 16APSK и т.д.)', max_length=20, unique=True, verbose_name='Модуляция'),
),
migrations.AlterField(
model_name='objitem',
name='created_at',
field=models.DateTimeField(auto_now_add=True, help_text='Дата и время создания записи', verbose_name='Дата создания'),
),
migrations.AlterField(
model_name='objitem',
name='created_by',
field=models.ForeignKey(blank=True, help_text='Пользователь, создавший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_created', to='mainapp.customuser', verbose_name='Создан пользователем'),
),
migrations.AlterField(
model_name='objitem',
name='name',
field=models.CharField(blank=True, db_index=True, help_text='Название объекта/источника сигнала', max_length=100, null=True, verbose_name='Имя объекта'),
),
migrations.AlterField(
model_name='objitem',
name='updated_at',
field=models.DateTimeField(auto_now=True, help_text='Дата и время последнего изменения', verbose_name='Дата последнего изменения'),
),
migrations.AlterField(
model_name='objitem',
name='updated_by',
field=models.ForeignKey(blank=True, help_text='Пользователь, последним изменивший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_updated', to='mainapp.customuser', verbose_name='Изменен пользователем'),
),
migrations.AlterField(
model_name='parameter',
name='bod_velocity',
field=models.FloatField(blank=True, default=0, help_text='Символьная скорость должна быть положительной', null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Символьная скорость, БОД'),
),
migrations.AlterField(
model_name='parameter',
name='freq_range',
field=models.FloatField(blank=True, default=0, help_text='Полоса частот в диапазоне от 0 до 1000 МГц', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000)], verbose_name='Полоса частот, МГц'),
),
migrations.AlterField(
model_name='parameter',
name='frequency',
field=models.FloatField(blank=True, db_index=True, default=0, help_text='Частота в диапазоне от 0 до 50000 МГц', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Частота, МГц'),
),
migrations.AlterField(
model_name='parameter',
name='snr',
field=models.FloatField(blank=True, default=0, help_text='Отношение сигнал/шум в диапазоне от -50 до 100 дБ', null=True, validators=[django.core.validators.MinValueValidator(-50), django.core.validators.MaxValueValidator(100)], verbose_name='ОСШ'),
),
migrations.AlterField(
model_name='polarization',
name='name',
field=models.CharField(db_index=True, help_text='Тип поляризации (H - горизонтальная, V - вертикальная, L - левая круговая, R - правая круговая)', max_length=20, unique=True, verbose_name='Поляризация'),
),
migrations.AlterField(
model_name='satellite',
name='name',
field=models.CharField(db_index=True, help_text='Название спутника', max_length=100, unique=True, verbose_name='Имя спутника'),
),
migrations.AlterField(
model_name='satellite',
name='norad',
field=models.IntegerField(blank=True, help_text='Идентификатор NORAD для отслеживания спутника', null=True, verbose_name='NORAD ID'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='bod_velocity',
field=models.FloatField(blank=True, default=0, help_text='Символьная скорость должна быть положительной', null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Символьная скорость, БОД'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='datetime_begin',
field=models.DateTimeField(blank=True, help_text='Дата и время начала измерения', null=True, verbose_name='Время начала измерения'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='datetime_end',
field=models.DateTimeField(blank=True, help_text='Дата и время окончания измерения', null=True, verbose_name='Время окончания измерения'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='freq_range',
field=models.FloatField(blank=True, default=0, help_text='Полоса частот в диапазоне от 0 до 1000 МГц', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000)], verbose_name='Полоса частот, МГц'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='frequency',
field=models.FloatField(blank=True, db_index=True, default=0, help_text='Частота в диапазоне от 0 до 50000 МГц', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Частота, МГц'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='packets',
field=models.BooleanField(blank=True, help_text='Наличие пакетной передачи', null=True, verbose_name='Пакетность'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='power',
field=models.FloatField(blank=True, default=0, help_text='Мощность сигнала в диапазоне от -100 до 100 дБм', null=True, validators=[django.core.validators.MinValueValidator(-100), django.core.validators.MaxValueValidator(100)], verbose_name='Мощность, дБм'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='snr',
field=models.FloatField(blank=True, default=0, help_text='Отношение сигнал/шум в диапазоне от -50 до 100 дБ', null=True, validators=[django.core.validators.MinValueValidator(-50), django.core.validators.MaxValueValidator(100)], verbose_name='ОСШ, Дб'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='status',
field=models.CharField(blank=True, help_text='Статус измерения', max_length=20, null=True, verbose_name='Статус'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='transfer',
field=models.FloatField(choices=[(-1.0, '-'), (9750.0, '9750 МГц'), (10750.0, '10750 МГц')], default=-1.0, help_text='Выберите перенос по частоте', verbose_name='Перенос по частоте'),
),
migrations.AlterField(
model_name='sigmaparmark',
name='mark',
field=models.BooleanField(blank=True, help_text='True - сигнал обнаружен, False - сигнал отсутствует', null=True, verbose_name='Наличие сигнала'),
),
migrations.AlterField(
model_name='sigmaparmark',
name='timestamp',
field=models.DateTimeField(blank=True, db_index=True, help_text='Время фиксации отметки', null=True, verbose_name='Время'),
),
migrations.AlterField(
model_name='sourcetype',
name='name',
field=models.CharField(db_index=True, help_text='Тип источника сигнала', max_length=50, unique=True, verbose_name='Тип источника'),
),
migrations.AlterField(
model_name='sourcetype',
name='objitem',
field=models.OneToOneField(help_text='Связанный объект', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_type_obj', to='mainapp.objitem', verbose_name='Объект'),
),
migrations.AlterField(
model_name='standard',
name='name',
field=models.CharField(db_index=True, help_text='Стандарт передачи данных (DVB-S, DVB-S2, DVB-S2X и т.д.)', max_length=20, unique=True, verbose_name='Стандарт'),
),
migrations.AddIndex(
model_name='geo',
index=models.Index(fields=['-timestamp'], name='mainapp_geo_timesta_58a605_idx'),
),
migrations.AddIndex(
model_name='geo',
index=models.Index(fields=['location'], name='mainapp_geo_locatio_b855c9_idx'),
),
migrations.AddIndex(
model_name='objitem',
index=models.Index(fields=['name'], name='mainapp_obj_name_e4f1e1_idx'),
),
migrations.AddIndex(
model_name='objitem',
index=models.Index(fields=['-updated_at'], name='mainapp_obj_updated_f46b0e_idx'),
),
migrations.AddIndex(
model_name='objitem',
index=models.Index(fields=['-created_at'], name='mainapp_obj_created_cba553_idx'),
),
]
# Generated by Django 5.2.7 on 2025-11-07 20:58
import django.contrib.gis.db.models.fields
import django.contrib.gis.db.models.functions
import django.core.validators
import django.db.models.deletion
import django.db.models.expressions
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0005_alter_geo_objitem'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterModelOptions(
name='customuser',
options={'ordering': ['user__username'], 'verbose_name': 'Пользователь', 'verbose_name_plural': 'Пользователи'},
),
migrations.AlterModelOptions(
name='geo',
options={'ordering': ['-timestamp'], 'verbose_name': 'Гео', 'verbose_name_plural': 'Гео'},
),
migrations.AlterModelOptions(
name='mirror',
options={'ordering': ['name'], 'verbose_name': 'Зеркало', 'verbose_name_plural': 'Зеркала'},
),
migrations.AlterModelOptions(
name='modulation',
options={'ordering': ['name'], 'verbose_name': 'Модуляция', 'verbose_name_plural': 'Модуляции'},
),
migrations.AlterModelOptions(
name='objitem',
options={'ordering': ['-updated_at'], 'verbose_name': 'Объект', 'verbose_name_plural': 'Объекты'},
),
migrations.AlterModelOptions(
name='polarization',
options={'ordering': ['name'], 'verbose_name': 'Поляризация', 'verbose_name_plural': 'Поляризация'},
),
migrations.AlterModelOptions(
name='satellite',
options={'ordering': ['name'], 'verbose_name': 'Спутник', 'verbose_name_plural': 'Спутники'},
),
migrations.AlterModelOptions(
name='sigmaparmark',
options={'ordering': ['-timestamp'], 'verbose_name': 'Отметка', 'verbose_name_plural': 'Отметки'},
),
migrations.AlterModelOptions(
name='sourcetype',
options={'ordering': ['name'], 'verbose_name': 'Тип источника', 'verbose_name_plural': 'Типы источников'},
),
migrations.AlterModelOptions(
name='standard',
options={'ordering': ['name'], 'verbose_name': 'Стандарт', 'verbose_name_plural': 'Стандарты'},
),
migrations.AlterField(
model_name='customuser',
name='role',
field=models.CharField(choices=[('admin', 'Администратор'), ('moderator', 'Модератор'), ('user', 'Пользователь')], db_index=True, default='user', help_text='Роль пользователя в системе', max_length=20, verbose_name='Роль пользователя'),
),
migrations.AlterField(
model_name='customuser',
name='user',
field=models.OneToOneField(help_text='Связанный пользователь Django', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Пользователь'),
),
migrations.AlterField(
model_name='geo',
name='comment',
field=models.CharField(blank=True, help_text='Дополнительные комментарии', max_length=255, verbose_name='Комментарий'),
),
migrations.AlterField(
model_name='geo',
name='coords',
field=django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Основные координаты геолокации (WGS84)', null=True, srid=4326, verbose_name='Координата геолокации'),
),
migrations.AlterField(
model_name='geo',
name='coords_kupsat',
field=django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Координаты, полученные от кубсата (WGS84)', null=True, srid=4326, verbose_name='Координаты Кубсата'),
),
migrations.AlterField(
model_name='geo',
name='coords_valid',
field=django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Координаты, предоставленные оперативным отделом (WGS84)', null=True, srid=4326, verbose_name='Координаты оперативников'),
),
migrations.AlterField(
model_name='geo',
name='distance_coords_kup',
field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords', 'coords_kupsat'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между кубсатом и гео, км'),
),
migrations.AlterField(
model_name='geo',
name='distance_kup_valid',
field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords_valid', 'coords_kupsat'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между кубсатом и оперативным отделом, км'),
),
migrations.AlterField(
model_name='geo',
name='is_average',
field=models.BooleanField(blank=True, help_text='Является ли координата усредненной', null=True, verbose_name='Усреднённое'),
),
migrations.AlterField(
model_name='geo',
name='location',
field=models.CharField(blank=True, help_text='Текстовое описание местоположения', max_length=255, null=True, verbose_name='Местоположение'),
),
migrations.AlterField(
model_name='geo',
name='mirrors',
field=models.ManyToManyField(blank=True, help_text='Зеркала антенн, использованные для приема', related_name='geo_mirrors', to='mainapp.mirror', verbose_name='Зеркала'),
),
migrations.AlterField(
model_name='geo',
name='objitem',
field=models.OneToOneField(help_text='Связанный объект', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='geo_obj', to='mainapp.objitem', verbose_name='Объект'),
),
migrations.AlterField(
model_name='geo',
name='timestamp',
field=models.DateTimeField(blank=True, db_index=True, help_text='Время фиксации геолокации', null=True, verbose_name='Время'),
),
migrations.AlterField(
model_name='mirror',
name='name',
field=models.CharField(db_index=True, help_text='Уникальное название зеркала антенны', max_length=30, unique=True, verbose_name='Имя зеркала'),
),
migrations.AlterField(
model_name='modulation',
name='name',
field=models.CharField(db_index=True, help_text='Тип модуляции сигнала (QPSK, 8PSK, 16APSK и т.д.)', max_length=20, unique=True, verbose_name='Модуляция'),
),
migrations.AlterField(
model_name='objitem',
name='created_at',
field=models.DateTimeField(auto_now_add=True, help_text='Дата и время создания записи', verbose_name='Дата создания'),
),
migrations.AlterField(
model_name='objitem',
name='created_by',
field=models.ForeignKey(blank=True, help_text='Пользователь, создавший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_created', to='mainapp.customuser', verbose_name='Создан пользователем'),
),
migrations.AlterField(
model_name='objitem',
name='name',
field=models.CharField(blank=True, db_index=True, help_text='Название объекта/источника сигнала', max_length=100, null=True, verbose_name='Имя объекта'),
),
migrations.AlterField(
model_name='objitem',
name='updated_at',
field=models.DateTimeField(auto_now=True, help_text='Дата и время последнего изменения', verbose_name='Дата последнего изменения'),
),
migrations.AlterField(
model_name='objitem',
name='updated_by',
field=models.ForeignKey(blank=True, help_text='Пользователь, последним изменивший запись', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_updated', to='mainapp.customuser', verbose_name='Изменен пользователем'),
),
migrations.AlterField(
model_name='parameter',
name='bod_velocity',
field=models.FloatField(blank=True, default=0, help_text='Символьная скорость должна быть положительной', null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Символьная скорость, БОД'),
),
migrations.AlterField(
model_name='parameter',
name='freq_range',
field=models.FloatField(blank=True, default=0, help_text='Полоса частот в диапазоне от 0 до 1000 МГц', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000)], verbose_name='Полоса частот, МГц'),
),
migrations.AlterField(
model_name='parameter',
name='frequency',
field=models.FloatField(blank=True, db_index=True, default=0, help_text='Частота в диапазоне от 0 до 50000 МГц', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Частота, МГц'),
),
migrations.AlterField(
model_name='parameter',
name='snr',
field=models.FloatField(blank=True, default=0, help_text='Отношение сигнал/шум в диапазоне от -50 до 100 дБ', null=True, validators=[django.core.validators.MinValueValidator(-50), django.core.validators.MaxValueValidator(100)], verbose_name='ОСШ'),
),
migrations.AlterField(
model_name='polarization',
name='name',
field=models.CharField(db_index=True, help_text='Тип поляризации (H - горизонтальная, V - вертикальная, L - левая круговая, R - правая круговая)', max_length=20, unique=True, verbose_name='Поляризация'),
),
migrations.AlterField(
model_name='satellite',
name='name',
field=models.CharField(db_index=True, help_text='Название спутника', max_length=100, unique=True, verbose_name='Имя спутника'),
),
migrations.AlterField(
model_name='satellite',
name='norad',
field=models.IntegerField(blank=True, help_text='Идентификатор NORAD для отслеживания спутника', null=True, verbose_name='NORAD ID'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='bod_velocity',
field=models.FloatField(blank=True, default=0, help_text='Символьная скорость должна быть положительной', null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Символьная скорость, БОД'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='datetime_begin',
field=models.DateTimeField(blank=True, help_text='Дата и время начала измерения', null=True, verbose_name='Время начала измерения'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='datetime_end',
field=models.DateTimeField(blank=True, help_text='Дата и время окончания измерения', null=True, verbose_name='Время окончания измерения'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='freq_range',
field=models.FloatField(blank=True, default=0, help_text='Полоса частот в диапазоне от 0 до 1000 МГц', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000)], verbose_name='Полоса частот, МГц'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='frequency',
field=models.FloatField(blank=True, db_index=True, default=0, help_text='Частота в диапазоне от 0 до 50000 МГц', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Частота, МГц'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='packets',
field=models.BooleanField(blank=True, help_text='Наличие пакетной передачи', null=True, verbose_name='Пакетность'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='power',
field=models.FloatField(blank=True, default=0, help_text='Мощность сигнала в диапазоне от -100 до 100 дБм', null=True, validators=[django.core.validators.MinValueValidator(-100), django.core.validators.MaxValueValidator(100)], verbose_name='Мощность, дБм'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='snr',
field=models.FloatField(blank=True, default=0, help_text='Отношение сигнал/шум в диапазоне от -50 до 100 дБ', null=True, validators=[django.core.validators.MinValueValidator(-50), django.core.validators.MaxValueValidator(100)], verbose_name='ОСШ, Дб'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='status',
field=models.CharField(blank=True, help_text='Статус измерения', max_length=20, null=True, verbose_name='Статус'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='transfer',
field=models.FloatField(choices=[(-1.0, '-'), (9750.0, '9750 МГц'), (10750.0, '10750 МГц')], default=-1.0, help_text='Выберите перенос по частоте', verbose_name='Перенос по частоте'),
),
migrations.AlterField(
model_name='sigmaparmark',
name='mark',
field=models.BooleanField(blank=True, help_text='True - сигнал обнаружен, False - сигнал отсутствует', null=True, verbose_name='Наличие сигнала'),
),
migrations.AlterField(
model_name='sigmaparmark',
name='timestamp',
field=models.DateTimeField(blank=True, db_index=True, help_text='Время фиксации отметки', null=True, verbose_name='Время'),
),
migrations.AlterField(
model_name='sourcetype',
name='name',
field=models.CharField(db_index=True, help_text='Тип источника сигнала', max_length=50, unique=True, verbose_name='Тип источника'),
),
migrations.AlterField(
model_name='sourcetype',
name='objitem',
field=models.OneToOneField(help_text='Связанный объект', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_type_obj', to='mainapp.objitem', verbose_name='Объект'),
),
migrations.AlterField(
model_name='standard',
name='name',
field=models.CharField(db_index=True, help_text='Стандарт передачи данных (DVB-S, DVB-S2, DVB-S2X и т.д.)', max_length=20, unique=True, verbose_name='Стандарт'),
),
migrations.AddIndex(
model_name='geo',
index=models.Index(fields=['-timestamp'], name='mainapp_geo_timesta_58a605_idx'),
),
migrations.AddIndex(
model_name='geo',
index=models.Index(fields=['location'], name='mainapp_geo_locatio_b855c9_idx'),
),
migrations.AddIndex(
model_name='objitem',
index=models.Index(fields=['name'], name='mainapp_obj_name_e4f1e1_idx'),
),
migrations.AddIndex(
model_name='objitem',
index=models.Index(fields=['-updated_at'], name='mainapp_obj_updated_f46b0e_idx'),
),
migrations.AddIndex(
model_name='objitem',
index=models.Index(fields=['-created_at'], name='mainapp_obj_created_cba553_idx'),
),
]

View File

@@ -1,23 +1,23 @@
# Generated by Django 5.2.7 on 2025-11-10 18:39
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0006_alter_customuser_options_alter_geo_options_and_more'),
]
operations = [
migrations.RemoveField(
model_name='parameter',
name='objitems',
),
migrations.AddField(
model_name='parameter',
name='objitem',
field=models.OneToOneField(blank=True, help_text='Связанный объект', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='parameter_obj', to='mainapp.objitem', verbose_name='Объект'),
),
]
# Generated by Django 5.2.7 on 2025-11-10 18:39
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0006_alter_customuser_options_alter_geo_options_and_more'),
]
operations = [
migrations.RemoveField(
model_name='parameter',
name='objitems',
),
migrations.AddField(
model_name='parameter',
name='objitem',
field=models.OneToOneField(blank=True, help_text='Связанный объект', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='parameter_obj', to='mainapp.objitem', verbose_name='Объект'),
),
]

View File

@@ -0,0 +1,63 @@
# Generated by Django 5.2.7 on 2025-11-11 13:59
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0007_remove_parameter_objitems_parameter_objitem'),
]
operations = [
migrations.RemoveField(
model_name='sourcetype',
name='objitem',
),
migrations.AddField(
model_name='objitem',
name='source_type_id',
field=models.ForeignKey(blank=True, help_text='Тип источника сигнала', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_sourcetype', to='mainapp.sourcetype', verbose_name='Тип источника'),
),
migrations.AlterField(
model_name='parameter',
name='bod_velocity',
field=models.FloatField(blank=True, default=0, help_text='Символьная скорость должна быть положительной', null=True, verbose_name='Символьная скорость, БОД'),
),
migrations.AlterField(
model_name='parameter',
name='freq_range',
field=models.FloatField(blank=True, default=0, help_text='Полоса частот сигнала', null=True, verbose_name='Полоса частот, МГц'),
),
migrations.AlterField(
model_name='parameter',
name='frequency',
field=models.FloatField(blank=True, db_index=True, default=0, help_text='Центральная частота сигнала', null=True, verbose_name='Частота, МГц'),
),
migrations.AlterField(
model_name='parameter',
name='snr',
field=models.FloatField(blank=True, default=0, help_text='Отношение сигнал/шум', null=True, verbose_name='ОСШ'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='bod_velocity',
field=models.FloatField(blank=True, default=0, help_text='Символьная скорость должна быть положительной', null=True, verbose_name='Символьная скорость, БОД'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='freq_range',
field=models.FloatField(blank=True, default=0, help_text='Полоса частот', null=True, verbose_name='Полоса частот, МГц'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='frequency',
field=models.FloatField(blank=True, db_index=True, default=0, help_text='Центральная частота сигнала', null=True, verbose_name='Частота, МГц'),
),
migrations.AlterField(
model_name='sigmaparameter',
name='power',
field=models.FloatField(blank=True, default=0, help_text='Мощность сигнала', null=True, verbose_name='Мощность, дБм'),
),
]

View File

@@ -1,229 +1,229 @@
"""
Переиспользуемые миксины для представлений mainapp.
Этот модуль содержит миксины для стандартизации общей логики в представлениях,
включая проверку прав доступа, обработку координат и сообщений.
"""
# Standard library imports
from datetime import datetime
from typing import Optional, Tuple
# Django imports
from django.contrib import messages
from django.contrib.auth.mixins import UserPassesTestMixin
from django.contrib.gis.geos import Point
class RoleRequiredMixin(UserPassesTestMixin):
"""
Mixin для проверки роли пользователя.
Проверяет, что пользователь имеет одну из требуемых ролей для доступа к представлению.
Attributes:
required_roles (list): Список допустимых ролей для доступа.
По умолчанию ['admin', 'moderator'].
Example:
class MyView(RoleRequiredMixin, View):
required_roles = ['admin', 'moderator']
def get(self, request):
# Только пользователи с ролью admin или moderator могут получить доступ
return render(request, 'template.html')
"""
required_roles = ["admin", "moderator"]
def test_func(self) -> bool:
"""
Проверяет, имеет ли пользователь требуемую роль.
Returns:
bool: True если пользователь имеет одну из требуемых ролей, иначе False.
"""
if not self.request.user.is_authenticated:
return False
if not hasattr(self.request.user, "customuser"):
return False
return self.request.user.customuser.role in self.required_roles
class CoordinateProcessingMixin:
"""
Mixin для обработки координат из POST данных форм.
Предоставляет методы для извлечения и обработки координат различных типов
(геолокация, кубсат, оперативники) из POST запроса и применения их к объекту Geo.
Example:
class MyFormView(CoordinateProcessingMixin, FormView):
def form_valid(self, form):
geo_instance = Geo()
self.process_coordinates(geo_instance)
geo_instance.save()
return super().form_valid(form)
"""
def process_coordinates(self, geo_instance, prefix: str = "geo") -> None:
"""
Обрабатывает координаты из POST данных и применяет их к объекту Geo.
Извлекает координаты геолокации, кубсата и оперативников из POST запроса
и устанавливает соответствующие поля объекта Geo.
Args:
geo_instance: Экземпляр модели Geo для обновления координат.
prefix (str): Префикс для полей формы (по умолчанию 'geo').
Note:
Метод ожидает следующие поля в request.POST:
- geo_longitude, geo_latitude: координаты геолокации
- kupsat_longitude, kupsat_latitude: координаты кубсата
- valid_longitude, valid_latitude: координаты оперативников
"""
# Обрабатываем координаты геолокации
geo_coords = self._extract_coordinates("geo")
if geo_coords:
geo_instance.coords = Point(geo_coords[0], geo_coords[1], srid=4326)
# Обрабатываем координаты Кубсата
kupsat_coords = self._extract_coordinates("kupsat")
if kupsat_coords:
geo_instance.coords_kupsat = Point(
kupsat_coords[0], kupsat_coords[1], srid=4326
)
# Обрабатываем координаты оперативников
valid_coords = self._extract_coordinates("valid")
if valid_coords:
geo_instance.coords_valid = Point(
valid_coords[0], valid_coords[1], srid=4326
)
def _extract_coordinates(self, coord_type: str) -> Optional[Tuple[float, float]]:
"""
Извлекает координаты указанного типа из POST данных.
Args:
coord_type (str): Тип координат ('geo', 'kupsat', 'valid').
Returns:
Optional[Tuple[float, float]]: Кортеж (longitude, latitude) или None,
если координаты не найдены или невалидны.
"""
longitude_key = f"{coord_type}_longitude"
latitude_key = f"{coord_type}_latitude"
longitude = self.request.POST.get(longitude_key)
latitude = self.request.POST.get(latitude_key)
if longitude and latitude:
try:
return (float(longitude), float(latitude))
except (ValueError, TypeError):
return None
return None
def process_timestamp(self, geo_instance) -> None:
"""
Обрабатывает дату и время из POST данных и применяет к объекту Geo.
Args:
geo_instance: Экземпляр модели Geo для обновления timestamp.
Note:
Метод ожидает следующие поля в request.POST:
- timestamp_date: дата в формате YYYY-MM-DD
- timestamp_time: время в формате HH:MM
"""
timestamp_date = self.request.POST.get("timestamp_date")
timestamp_time = self.request.POST.get("timestamp_time")
if timestamp_date and timestamp_time:
try:
naive_datetime = datetime.strptime(
f"{timestamp_date} {timestamp_time}", "%Y-%m-%d %H:%M"
)
geo_instance.timestamp = naive_datetime
except ValueError:
# Если формат даты/времени неверный, пропускаем
pass
class FormMessageMixin:
"""
Mixin для стандартизации сообщений об успехе и ошибках в формах.
Автоматически добавляет сообщения пользователю при успешной или неуспешной
обработке формы.
Attributes:
success_message (str): Сообщение при успешной обработке формы.
error_message (str): Сообщение при ошибке обработки формы.
Example:
class MyFormView(FormMessageMixin, FormView):
success_message = "Данные успешно сохранены!"
error_message = "Ошибка при сохранении данных"
def form_valid(self, form):
# Автоматически добавит success_message
return super().form_valid(form)
"""
success_message = "Операция выполнена успешно"
error_message = "Произошла ошибка при обработке формы"
def form_valid(self, form):
"""
Обрабатывает валидную форму и добавляет сообщение об успехе.
Args:
form: Валидная форма Django.
Returns:
HttpResponse: Результат обработки родительского метода form_valid.
"""
if self.success_message:
messages.success(self.request, self.success_message)
return super().form_valid(form)
def form_invalid(self, form):
"""
Обрабатывает невалидную форму и добавляет сообщение об ошибке.
Args:
form: Невалидная форма Django.
Returns:
HttpResponse: Результат обработки родительского метода form_invalid.
"""
if self.error_message:
messages.error(self.request, self.error_message)
return super().form_invalid(form)
def get_success_message(self) -> str:
"""
Возвращает сообщение об успехе.
Может быть переопределен в подклассах для динамического формирования сообщения.
Returns:
str: Сообщение об успехе.
"""
return self.success_message
def get_error_message(self) -> str:
"""
Возвращает сообщение об ошибке.
Может быть переопределен в подклассах для динамического формирования сообщения.
Returns:
str: Сообщение об ошибке.
"""
return self.error_message
"""
Переиспользуемые миксины для представлений mainapp.
Этот модуль содержит миксины для стандартизации общей логики в представлениях,
включая проверку прав доступа, обработку координат и сообщений.
"""
# Standard library imports
from datetime import datetime
from typing import Optional, Tuple
# Django imports
from django.contrib import messages
from django.contrib.auth.mixins import UserPassesTestMixin
from django.contrib.gis.geos import Point
class RoleRequiredMixin(UserPassesTestMixin):
"""
Mixin для проверки роли пользователя.
Проверяет, что пользователь имеет одну из требуемых ролей для доступа к представлению.
Attributes:
required_roles (list): Список допустимых ролей для доступа.
По умолчанию ['admin', 'moderator'].
Example:
class MyView(RoleRequiredMixin, View):
required_roles = ['admin', 'moderator']
def get(self, request):
# Только пользователи с ролью admin или moderator могут получить доступ
return render(request, 'template.html')
"""
required_roles = ["admin", "moderator"]
def test_func(self) -> bool:
"""
Проверяет, имеет ли пользователь требуемую роль.
Returns:
bool: True если пользователь имеет одну из требуемых ролей, иначе False.
"""
if not self.request.user.is_authenticated:
return False
if not hasattr(self.request.user, "customuser"):
return False
return self.request.user.customuser.role in self.required_roles
class CoordinateProcessingMixin:
"""
Mixin для обработки координат из POST данных форм.
Предоставляет методы для извлечения и обработки координат различных типов
(геолокация, кубсат, оперативники) из POST запроса и применения их к объекту Geo.
Example:
class MyFormView(CoordinateProcessingMixin, FormView):
def form_valid(self, form):
geo_instance = Geo()
self.process_coordinates(geo_instance)
geo_instance.save()
return super().form_valid(form)
"""
def process_coordinates(self, geo_instance, prefix: str = "geo") -> None:
"""
Обрабатывает координаты из POST данных и применяет их к объекту Geo.
Извлекает координаты геолокации, кубсата и оперативников из POST запроса
и устанавливает соответствующие поля объекта Geo.
Args:
geo_instance: Экземпляр модели Geo для обновления координат.
prefix (str): Префикс для полей формы (по умолчанию 'geo').
Note:
Метод ожидает следующие поля в request.POST:
- geo_longitude, geo_latitude: координаты геолокации
- kupsat_longitude, kupsat_latitude: координаты кубсата
- valid_longitude, valid_latitude: координаты оперативников
"""
# Обрабатываем координаты геолокации
geo_coords = self._extract_coordinates("geo")
if geo_coords:
geo_instance.coords = Point(geo_coords[0], geo_coords[1], srid=4326)
# Обрабатываем координаты Кубсата
kupsat_coords = self._extract_coordinates("kupsat")
if kupsat_coords:
geo_instance.coords_kupsat = Point(
kupsat_coords[0], kupsat_coords[1], srid=4326
)
# Обрабатываем координаты оперативников
valid_coords = self._extract_coordinates("valid")
if valid_coords:
geo_instance.coords_valid = Point(
valid_coords[0], valid_coords[1], srid=4326
)
def _extract_coordinates(self, coord_type: str) -> Optional[Tuple[float, float]]:
"""
Извлекает координаты указанного типа из POST данных.
Args:
coord_type (str): Тип координат ('geo', 'kupsat', 'valid').
Returns:
Optional[Tuple[float, float]]: Кортеж (longitude, latitude) или None,
если координаты не найдены или невалидны.
"""
longitude_key = f"{coord_type}_longitude"
latitude_key = f"{coord_type}_latitude"
longitude = self.request.POST.get(longitude_key)
latitude = self.request.POST.get(latitude_key)
if longitude and latitude:
try:
return (float(longitude), float(latitude))
except (ValueError, TypeError):
return None
return None
def process_timestamp(self, geo_instance) -> None:
"""
Обрабатывает дату и время из POST данных и применяет к объекту Geo.
Args:
geo_instance: Экземпляр модели Geo для обновления timestamp.
Note:
Метод ожидает следующие поля в request.POST:
- timestamp_date: дата в формате YYYY-MM-DD
- timestamp_time: время в формате HH:MM
"""
timestamp_date = self.request.POST.get("timestamp_date")
timestamp_time = self.request.POST.get("timestamp_time")
if timestamp_date and timestamp_time:
try:
naive_datetime = datetime.strptime(
f"{timestamp_date} {timestamp_time}", "%Y-%m-%d %H:%M"
)
geo_instance.timestamp = naive_datetime
except ValueError:
# Если формат даты/времени неверный, пропускаем
pass
class FormMessageMixin:
"""
Mixin для стандартизации сообщений об успехе и ошибках в формах.
Автоматически добавляет сообщения пользователю при успешной или неуспешной
обработке формы.
Attributes:
success_message (str): Сообщение при успешной обработке формы.
error_message (str): Сообщение при ошибке обработки формы.
Example:
class MyFormView(FormMessageMixin, FormView):
success_message = "Данные успешно сохранены!"
error_message = "Ошибка при сохранении данных"
def form_valid(self, form):
# Автоматически добавит success_message
return super().form_valid(form)
"""
success_message = "Операция выполнена успешно"
error_message = "Произошла ошибка при обработке формы"
def form_valid(self, form):
"""
Обрабатывает валидную форму и добавляет сообщение об успехе.
Args:
form: Валидная форма Django.
Returns:
HttpResponse: Результат обработки родительского метода form_valid.
"""
if self.success_message:
messages.success(self.request, self.success_message)
return super().form_valid(form)
def form_invalid(self, form):
"""
Обрабатывает невалидную форму и добавляет сообщение об ошибке.
Args:
form: Невалидная форма Django.
Returns:
HttpResponse: Результат обработки родительского метода form_invalid.
"""
if self.error_message:
messages.error(self.request, self.error_message)
return super().form_invalid(form)
def get_success_message(self) -> str:
"""
Возвращает сообщение об успехе.
Может быть переопределен в подклассах для динамического формирования сообщения.
Returns:
str: Сообщение об успехе.
"""
return self.success_message
def get_error_message(self) -> str:
"""
Возвращает сообщение об ошибке.
Может быть переопределен в подклассах для динамического формирования сообщения.
Returns:
str: Сообщение об ошибке.
"""
return self.error_message

File diff suppressed because it is too large Load Diff

View File

@@ -1,76 +1,76 @@
# Django imports
from django.contrib.admin.filters import ChoicesFieldListFilter
from django.forms import Media
class PopupCompatibleMultiSelectRelatedDropdownFilter(ChoicesFieldListFilter):
"""
A custom filter that maintains popup context when used in raw_id_fields modals.
"""
def __init__(self, field, request, params, model, model_admin, field_path):
super().__init__(field, request, params, model, model_admin, field_path)
# Check if we're in a popup context
self.is_popup = '_popup' in request.GET or 'pop' in request.GET or 'admin' not in request.path
# Get all choices (related objects)
self.lookup_choices = field.get_choices(include_blank=False)
def has_output(self):
return len(self.lookup_choices) > 1
def value(self):
return self.lookup_val
def expected_parameters(self):
return [self.lookup_kwarg, self.lookup_kwarg_isnull]
def choices(self, changelist):
# If in popup, preserve the popup parameters in the filter URL
popup_params = {}
if self.is_popup:
# Preserve popup parameters
if '_popup' in changelist.params:
popup_params['_popup'] = 1
if 'pop' in changelist.params:
popup_params['pop'] = changelist.params['pop']
if '_to_field' in changelist.params:
popup_params['_to_field'] = changelist.params['_to_field']
# Create the base URL with popup parameters
all_params = changelist.get_filters_params()
all_params.update(popup_params)
# Generate the URL for the filter
url = changelist.get_query_string(all_params, [self.lookup_kwarg])
yield {
'selected': self.lookup_val is None,
'query_string': url,
'display': 'All',
}
# Add choices
for lookup, title in self.lookup_choices:
params = dict(all_params)
params[self.lookup_kwarg] = lookup
# Remove the parameter if it's being set to the same value (for unselecting)
if self.lookup_val == str(lookup):
params.pop(self.lookup_kwarg, None)
# Add popup parameters to each choice URL
choice_params = params.copy()
choice_params.update(popup_params)
yield {
'selected': str(lookup) == self.lookup_val,
'query_string': changelist.get_query_string(choice_params, [self.lookup_kwarg_isnull]),
'display': title,
}
@property
def media(self):
# Include necessary CSS/JS for dropdown functionality if needed
# Django imports
from django.contrib.admin.filters import ChoicesFieldListFilter
from django.forms import Media
class PopupCompatibleMultiSelectRelatedDropdownFilter(ChoicesFieldListFilter):
"""
A custom filter that maintains popup context when used in raw_id_fields modals.
"""
def __init__(self, field, request, params, model, model_admin, field_path):
super().__init__(field, request, params, model, model_admin, field_path)
# Check if we're in a popup context
self.is_popup = '_popup' in request.GET or 'pop' in request.GET or 'admin' not in request.path
# Get all choices (related objects)
self.lookup_choices = field.get_choices(include_blank=False)
def has_output(self):
return len(self.lookup_choices) > 1
def value(self):
return self.lookup_val
def expected_parameters(self):
return [self.lookup_kwarg, self.lookup_kwarg_isnull]
def choices(self, changelist):
# If in popup, preserve the popup parameters in the filter URL
popup_params = {}
if self.is_popup:
# Preserve popup parameters
if '_popup' in changelist.params:
popup_params['_popup'] = 1
if 'pop' in changelist.params:
popup_params['pop'] = changelist.params['pop']
if '_to_field' in changelist.params:
popup_params['_to_field'] = changelist.params['_to_field']
# Create the base URL with popup parameters
all_params = changelist.get_filters_params()
all_params.update(popup_params)
# Generate the URL for the filter
url = changelist.get_query_string(all_params, [self.lookup_kwarg])
yield {
'selected': self.lookup_val is None,
'query_string': url,
'display': 'All',
}
# Add choices
for lookup, title in self.lookup_choices:
params = dict(all_params)
params[self.lookup_kwarg] = lookup
# Remove the parameter if it's being set to the same value (for unselecting)
if self.lookup_val == str(lookup):
params.pop(self.lookup_kwarg, None)
# Add popup parameters to each choice URL
choice_params = params.copy()
choice_params.update(popup_params)
yield {
'selected': str(lookup) == self.lookup_val,
'query_string': changelist.get_query_string(choice_params, [self.lookup_kwarg_isnull]),
'display': title,
}
@property
def media(self):
# Include necessary CSS/JS for dropdown functionality if needed
return Media()

View File

@@ -1,14 +1,14 @@
# Django imports
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver
# Local imports
from .models import CustomUser
@receiver(post_save, sender=User)
def create_or_update_user_profile(sender, instance, created, **kwargs):
if created:
CustomUser.objects.create(user=instance)
# Django imports
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver
# Local imports
from .models import CustomUser
@receiver(post_save, sender=User)
def create_or_update_user_profile(sender, instance, created, **kwargs):
if created:
CustomUser.objects.create(user=instance)
instance.customuser.save()

65
dbapp/mainapp/tasks.py Normal file
View File

@@ -0,0 +1,65 @@
"""
Simple test tasks for Celery functionality.
"""
import time
import logging
from celery import shared_task
logger = logging.getLogger(__name__)
@shared_task(name='mainapp.test_celery_connection')
def test_celery_connection(message="Hello from Celery!"):
"""
A simple test task to verify Celery is working.
Args:
message (str): Message to return
Returns:
str: Confirmation message with task completion time
"""
logger.info(f"Test task started with message: {message}")
time.sleep(2) # Simulate some work
result = f"Task completed! Received message: {message}"
logger.info(f"Test task completed: {result}")
return result
@shared_task(name='mainapp.add_numbers')
def add_numbers(x, y):
"""
A simple addition task to test Celery functionality.
Args:
x (int): First number
y (int): Second number
Returns:
int: Sum of x and y
"""
logger.info(f"Adding {x} + {y}")
result = x + y
logger.info(f"Addition completed: {x} + {y} = {result}")
return result
@shared_task(name='mainapp.long_running_task')
def long_running_task(duration=10):
"""
A task that runs for a specified duration to test long-running tasks.
Args:
duration (int): Duration in seconds
Returns:
str: Completion message
"""
logger.info(f"Starting long running task for {duration} seconds")
for i in range(duration):
time.sleep(1)
logger.info(f"Long task progress: {i+1}/{duration}")
result = f"Long running task completed after {duration} seconds"
logger.info(result)
return result

View File

@@ -1,60 +1,60 @@
{% extends "mapsapp/map2d_base.html" %}
{% load static %}
{% block title %}Вынос точек{% endblock title %}
{% block extra_js %}
<script>
// Цвета для стандартных маркеров (из leaflet-color-markers)
var markerColors = ['red', 'green', 'orange', 'violet', 'grey', 'black', 'blue'];
var getColorIcon = function(color) {
return L.icon({
iconUrl: '{% static "leaflet-markers/img/marker-icon-" %}' + color + '.png',
shadowUrl: '{% static "leaflet-markers/img/marker-shadow.png" %}',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
});
};
var overlays = [];
{% for group in groups %}
var groupIndex = {{ forloop.counter0 }};
var colorName = markerColors[groupIndex % markerColors.length];
var groupIcon = getColorIcon(colorName);
var groupLayer = L.layerGroup();
var subgroup = [];
{% for point_data in group.points %}
var pointName = "{{ group.name|escapejs }}";
var marker = L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}], {
icon: groupIcon
}).bindPopup(pointName);
groupLayer.addLayer(marker);
subgroup.push({
label: "{{ forloop.counter }} - {{ point_data.frequency }}",
layer: marker
});
{% endfor %}
overlays.push({
label: '{{ group.name|escapejs }}',
selectAllCheckbox: true,
children: subgroup,
layer: groupLayer
});
{% endfor %}
// Используем именно tree-контрол
L.control.layers.tree(baseLayers, overlays, {
collapsed: false,
autoZIndex: true
}).addTo(map);
</script>
{% extends "mapsapp/map2d_base.html" %}
{% load static %}
{% block title %}Вынос точек{% endblock title %}
{% block extra_js %}
<script>
// Цвета для стандартных маркеров (из leaflet-color-markers)
var markerColors = ['red', 'green', 'orange', 'violet', 'grey', 'black', 'blue'];
var getColorIcon = function(color) {
return L.icon({
iconUrl: '{% static "leaflet-markers/img/marker-icon-" %}' + color + '.png',
shadowUrl: '{% static "leaflet-markers/img/marker-shadow.png" %}',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
});
};
var overlays = [];
{% for group in groups %}
var groupIndex = {{ forloop.counter0 }};
var colorName = markerColors[groupIndex % markerColors.length];
var groupIcon = getColorIcon(colorName);
var groupLayer = L.layerGroup();
var subgroup = [];
{% for point_data in group.points %}
var pointName = "{{ group.name|escapejs }}";
var marker = L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}], {
icon: groupIcon
}).bindPopup(pointName);
groupLayer.addLayer(marker);
subgroup.push({
label: "{{ forloop.counter }} - {{ point_data.frequency }}",
layer: marker
});
{% endfor %}
overlays.push({
label: '{{ group.name|escapejs }}',
selectAllCheckbox: true,
children: subgroup,
layer: groupLayer
});
{% endfor %}
// Используем именно tree-контрол
L.control.layers.tree(baseLayers, overlays, {
collapsed: false,
autoZIndex: true
}).addTo(map);
</script>
{% endblock extra_js %}

View File

@@ -1,189 +1,189 @@
{% extends 'mainapp/base.html' %}
{% block title %}Действия{% endblock %}
{% block content %}
<div class="container">
<div class="text-center mb-5">
<h1 class="display-4 fw-bold">Действия</h1>
<p class="lead">Управление данными спутников</p>
</div>
<!-- Alert messages -->
{% include 'mainapp/components/_messages.html' %}
<!-- Main feature cards -->
<div class="row g-4">
<!-- Excel Data Upload 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-file-earmark-excel text-primary" viewBox="0 0 16 16">
<path d="M5.884 6.68a.5.5 0 1 0-.768.64L7.349 10l-2.233 2.68a.5.5 0 0 0 .768.64L8 10.781l2.116 2.54a.5.5 0 0 0 .768-.641L8.651 10l2.233-2.68a.5.5 0 0 0-.768-.64L8 9.219z"/>
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2M9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/>
</svg>
</div>
<h3 class="card-title mb-0">Загрузка данных из Excel</h3>
</div>
<p class="card-text">Загрузите данные из Excel-файла в базу данных. Поддерживается выбор спутника и ограничение количества записей.</p>
<a href="{% url 'mainapp:load_excel_data' %}" class="btn btn-primary">
Перейти к загрузке данных
</a>
</div>
</div>
</div>
<!-- CSV Data Upload 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-success 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-file-earmark-text text-success" viewBox="0 0 16 16">
<path d="M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zm0 2a.5.5 0 0 0 0 1h2a.5.5 0 0 0 0-1z"/>
<path d="M9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.5L9.5 0m0 1v2A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1z"/>
</svg>
</div>
<h3 class="card-title mb-0">Загрузка данных из CSV</h3>
</div>
<p class="card-text">Загрузите данные из CSV-файла в базу данных. Простая загрузка с возможностью указания пути к файлу.</p>
<a href="{% url 'mainapp:load_csv_data' %}" class="btn btn-success">
Перейти к загрузке данных
</a>
</div>
</div>
</div>
<!-- Satellite List 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-info 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-satellite text-info" viewBox="0 0 16 16">
<path d="M13.37 1.37c-2.75 0-5.4 1.13-7.29 3.02C4.13 6.33 3 8.98 3 11.73c0 2.75 1.13 5.4 3.02 7.29 1.94 1.94 4.54 3.02 7.29 3.02 2.75 0 5.4-1.13 7.29-3.02 1.94-1.94 3.02-4.54 3.02-7.29 0-2.75-1.13-5.4-3.02-7.29C18.77 2.5-2.75 1.37-5.5 1.37m-5.5 8.26c0-1.52.62-3.02 1.73-4.13 1.11-1.11 2.61-1.73 4.13-1.73 1.52 0 3.02.62 4.13 1.73 1.11 1.11 1.73 2.61 1.73 4.13 0 1.52-.62 3.02-1.73 4.13-1.11 1.11-2.61 1.73-4.13 1.73-1.52 0-3.02-.62-4.13-1.73-1.11-1.11-1.73-2.61-1.73-4.13"/>
<path d="M6.63 6.63c.62-.62 1.45-.98 2.27-.98.82 0 1.65.36 2.27.98.62.62.98 1.45.98 2.27 0 .82-.36 1.65-.98 2.27-.62.62-1.45.98-2.27.98-.82 0-1.65-.36-2.27-.98-.62-.62-.98-1.45-.98-2.27 0-.82.36-1.65.98-2.27m2.27 1.02c-.26 0-.52.1-.71.29-.2.2-.29.46-.29.71 0 .26.1.52.29.71.2.2.46.29.71.29.26 0 .52-.1.71-.29.2-.2.29-.46.29-.71 0-.26-.1-.52-.29-.71-.19-.19-.45-.29-.71-.29"/>
<path d="M5.13 5.13c.46-.46 1.08-.73 1.73-.73.65 0 1.27.27 1.73.73.46.46.73 1.08.73 1.73 0 .65-.27 1.27-.73 1.73-.46.46-1.08.73-1.73.73-.65 0-1.27-.27-1.73-.73-.46-.46-.73-1.08-.73-1.73 0-.65.27-1.27.73-1.73m1.73.58c-.15 0-.3.06-.42.18-.12.12-.18.27-.18.42 0 .15.06.3.18.42.12.12.27.18.42.18.15 0 .3-.06.42-.18.12-.12.18-.27.18-.42 0-.15-.06-.3-.18-.42-.12-.12-.27-.18-.42-.18"/>
<path d="M8 3.5c.28 0 .5.22.5.5v1c0 .28-.22.5-.5.5s-.5-.22-.5-.5v-1c0-.28.22-.5.5-.5"/>
<path d="M10.5 8c0-.28.22-.5.5-.5h1c.28 0 .5.22.5.5s-.22.5-.5.5h-1c-.28 0-.5-.22-.5-.5"/>
<path d="M8 12.5c-.28 0-.5.22-.5.5v1c0 .28.22.5.5.5s.5-.22.5-.5v-1c0-.28-.22-.5-.5-.5"/>
<path d="M3.5 8c0 .28-.22.5-.5.5h-1c-.28 0-.5-.22-.5-.5s.22-.5.5-.5h1c.28 0 .5.22.5.5"/>
</svg>
</div>
<h3 class="card-title mb-0">Добавление списка спутников</h3>
</div>
<p class="card-text">Добавьте новый список спутников в базу данных для последующего использования в загрузке данных.</p>
<a href="{% url 'mainapp:add_sats' %}" class="btn btn-info">
Добавить список спутников
</a>
</div>
</div>
</div>
<!-- Transponders 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-warning 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-wifi text-warning" viewBox="0 0 16 16">
<path d="M6.002 3.5a5.5 5.5 0 1 1 3.996 9.5H10A5.5 5.5 0 0 1 6.002 3.5M6.002 5.5a3.5 3.5 0 1 0 3.996 5.5H10A3.5 3.5 0 0 0 6.002 5.5"/>
<path d="M10.5 12.5a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5.5.5 0 0 0-1 0 .5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5.5.5 0 0 0-1 0 .5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5 3.5 3.5 0 0 1 7 0"/>
</svg>
</div>
<h3 class="card-title mb-0">Добавление транспондеров</h3>
</div>
<p class="card-text">Добавьте список транспондеров из JSON-файла в базу данных. Требуется наличие файла transponders.json.</p>
<a href="{% url 'mainapp:add_trans' %}" class="btn btn-warning">
Добавить транспондеры
</a>
</div>
</div>
</div>
<!-- VCH Load Data 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-danger 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-upload text-danger" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5"/>
<path d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708z"/>
</svg>
</div>
<h3 class="card-title mb-0">Добавление данных ВЧ загрузки</h3>
</div>
<p class="card-text">Загрузите данные ВЧ загрузки из HTML-файла с таблицами. Поддерживается выбор спутника для привязки данных.</p>
<a href="{% url 'mainapp:vch_load' %}" class="btn btn-danger">
Добавить данные ВЧ загрузки
</a>
</div>
</div>
</div>
<!-- Lyngsat Data Fill 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-secondary 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-cloud-download text-secondary" viewBox="0 0 16 16">
<path d="M4.406 1.342A5.53 5.53 0 0 1 8 0c2.69 0 4.923 2 5.166 4.579C14.758 4.804 16 6.137 16 7.773 16 9.569 14.502 11 12.687 11H10a.5.5 0 0 1 0-1h2.688C13.979 10 15 8.988 15 7.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 2.825 10.328 1 8 1a4.53 4.53 0 0 0-2.941 1.1c-.757.652-1.153 1.438-1.153 2.055v.448l-.445.049C2.064 4.805 1 5.952 1 7.318 1 8.785 2.23 10 3.781 10H6a.5.5 0 0 1 0 1H3.781C1.708 11 0 9.366 0 7.318c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383"/>
<path d="M7.646 15.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 14.293V5.5a.5.5 0 0 0-1 0v8.793l-2.146-2.147a.5.5 0 0 0-.708.708z"/>
</svg>
</div>
<h3 class="card-title mb-0">Заполнение данных Lyngsat</h3>
</div>
<p class="card-text">Загрузите данные о транспондерах спутников с сайта Lyngsat. Выберите спутники и регионы для парсинга данных.</p>
<a href="{% url 'mainapp:fill_lyngsat_data' %}" class="btn btn-secondary">
Заполнить данные Lyngsat
</a>
</div>
</div>
</div>
<!-- Calculation 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-info 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-calculator text-info" viewBox="0 0 16 16">
<path d="M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v4h2V2a1 1 0 0 0-1-1M5 6v1h1V6zm2 0v1h1V6zm2 0v1h1V6zm2 0v1h1V6zm1 2v1h1V8zm0 2v1h1v-1zm0 2v1h1v-1zm-8-6v8H3V8zm2 0v8h1V8zm2 0v8h1V8zm2 0v8h1V8z"/>
</svg>
</div>
<h3 class="card-title mb-0">Привязка ВЧ загрузки</h3>
</div>
<p class="card-text">Привязка ВЧ загрузки с sigma</p>
<a href="{% url 'mainapp:link_vch_sigma' %}" class="btn btn-info">
Открыть форму
</a>
</div>
</div>
</div>
<!-- New Event 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-success 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-plus-circle text-success" viewBox="0 0 16 16">
<path d="M8 0a8 8 0 1 0 0 16A8 8 0 0 0 8 0M4.5 7.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5M7.5 4.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 0 1h-1a.5.5 0 0 1-.5-.5m1 3a.5.5 0 0 1 .5.5v3a.5.5 0 0 1-1 0v-3a.5.5 0 0 1 .5-.5"/>
</svg>
</div>
<h3 class="card-title mb-0">Формирование таблицы для Кубсатов</h3>
</div>
<p class="card-text">Добавьте новое событие с помощью выбора спутника и загрузки файла данных.</p>
<a href="{% url 'mainapp:kubsat_excel' %}" class="btn btn-success">
Добавить событие
</a>
</div>
</div>
</div>
</div>
</div>
{% extends 'mainapp/base.html' %}
{% block title %}Действия{% endblock %}
{% block content %}
<div class="container">
<div class="text-center mb-5">
<h1 class="display-4 fw-bold">Действия</h1>
<p class="lead">Управление данными спутников</p>
</div>
<!-- Alert messages -->
{% include 'mainapp/components/_messages.html' %}
<!-- Main feature cards -->
<div class="row g-4">
<!-- Excel Data Upload 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-file-earmark-excel text-primary" viewBox="0 0 16 16">
<path d="M5.884 6.68a.5.5 0 1 0-.768.64L7.349 10l-2.233 2.68a.5.5 0 0 0 .768.64L8 10.781l2.116 2.54a.5.5 0 0 0 .768-.641L8.651 10l2.233-2.68a.5.5 0 0 0-.768-.64L8 9.219z"/>
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2M9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/>
</svg>
</div>
<h3 class="card-title mb-0">Загрузка данных из Excel</h3>
</div>
<p class="card-text">Загрузите данные из Excel-файла в базу данных. Поддерживается выбор спутника и ограничение количества записей.</p>
<a href="{% url 'mainapp:load_excel_data' %}" class="btn btn-primary">
Перейти к загрузке данных
</a>
</div>
</div>
</div>
<!-- CSV Data Upload 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-success 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-file-earmark-text text-success" viewBox="0 0 16 16">
<path d="M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zm0 2a.5.5 0 0 0 0 1h2a.5.5 0 0 0 0-1z"/>
<path d="M9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.5L9.5 0m0 1v2A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1z"/>
</svg>
</div>
<h3 class="card-title mb-0">Загрузка данных из CSV</h3>
</div>
<p class="card-text">Загрузите данные из CSV-файла в базу данных. Простая загрузка с возможностью указания пути к файлу.</p>
<a href="{% url 'mainapp:load_csv_data' %}" class="btn btn-success">
Перейти к загрузке данных
</a>
</div>
</div>
</div>
<!-- Satellite List 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-info 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-satellite text-info" viewBox="0 0 16 16">
<path d="M13.37 1.37c-2.75 0-5.4 1.13-7.29 3.02C4.13 6.33 3 8.98 3 11.73c0 2.75 1.13 5.4 3.02 7.29 1.94 1.94 4.54 3.02 7.29 3.02 2.75 0 5.4-1.13 7.29-3.02 1.94-1.94 3.02-4.54 3.02-7.29 0-2.75-1.13-5.4-3.02-7.29C18.77 2.5-2.75 1.37-5.5 1.37m-5.5 8.26c0-1.52.62-3.02 1.73-4.13 1.11-1.11 2.61-1.73 4.13-1.73 1.52 0 3.02.62 4.13 1.73 1.11 1.11 1.73 2.61 1.73 4.13 0 1.52-.62 3.02-1.73 4.13-1.11 1.11-2.61 1.73-4.13 1.73-1.52 0-3.02-.62-4.13-1.73-1.11-1.11-1.73-2.61-1.73-4.13"/>
<path d="M6.63 6.63c.62-.62 1.45-.98 2.27-.98.82 0 1.65.36 2.27.98.62.62.98 1.45.98 2.27 0 .82-.36 1.65-.98 2.27-.62.62-1.45.98-2.27.98-.82 0-1.65-.36-2.27-.98-.62-.62-.98-1.45-.98-2.27 0-.82.36-1.65.98-2.27m2.27 1.02c-.26 0-.52.1-.71.29-.2.2-.29.46-.29.71 0 .26.1.52.29.71.2.2.46.29.71.29.26 0 .52-.1.71-.29.2-.2.29-.46.29-.71 0-.26-.1-.52-.29-.71-.19-.19-.45-.29-.71-.29"/>
<path d="M5.13 5.13c.46-.46 1.08-.73 1.73-.73.65 0 1.27.27 1.73.73.46.46.73 1.08.73 1.73 0 .65-.27 1.27-.73 1.73-.46.46-1.08.73-1.73.73-.65 0-1.27-.27-1.73-.73-.46-.46-.73-1.08-.73-1.73 0-.65.27-1.27.73-1.73m1.73.58c-.15 0-.3.06-.42.18-.12.12-.18.27-.18.42 0 .15.06.3.18.42.12.12.27.18.42.18.15 0 .3-.06.42-.18.12-.12.18-.27.18-.42 0-.15-.06-.3-.18-.42-.12-.12-.27-.18-.42-.18"/>
<path d="M8 3.5c.28 0 .5.22.5.5v1c0 .28-.22.5-.5.5s-.5-.22-.5-.5v-1c0-.28.22-.5.5-.5"/>
<path d="M10.5 8c0-.28.22-.5.5-.5h1c.28 0 .5.22.5.5s-.22.5-.5.5h-1c-.28 0-.5-.22-.5-.5"/>
<path d="M8 12.5c-.28 0-.5.22-.5.5v1c0 .28.22.5.5.5s.5-.22.5-.5v-1c0-.28-.22-.5-.5-.5"/>
<path d="M3.5 8c0 .28-.22.5-.5.5h-1c-.28 0-.5-.22-.5-.5s.22-.5.5-.5h1c.28 0 .5.22.5.5"/>
</svg>
</div>
<h3 class="card-title mb-0">Добавление списка спутников</h3>
</div>
<p class="card-text">Добавьте новый список спутников в базу данных для последующего использования в загрузке данных.</p>
<a href="{% url 'mainapp:add_sats' %}" class="btn btn-info">
Добавить список спутников
</a>
</div>
</div>
</div>
<!-- Transponders 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-warning 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-wifi text-warning" viewBox="0 0 16 16">
<path d="M6.002 3.5a5.5 5.5 0 1 1 3.996 9.5H10A5.5 5.5 0 0 1 6.002 3.5M6.002 5.5a3.5 3.5 0 1 0 3.996 5.5H10A3.5 3.5 0 0 0 6.002 5.5"/>
<path d="M10.5 12.5a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5.5.5 0 0 0-1 0 .5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5.5.5 0 0 0-1 0 .5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5 3.5 3.5 0 0 1 7 0"/>
</svg>
</div>
<h3 class="card-title mb-0">Добавление транспондеров</h3>
</div>
<p class="card-text">Добавьте список транспондеров из JSON-файла в базу данных. Требуется наличие файла transponders.json.</p>
<a href="{% url 'mainapp:add_trans' %}" class="btn btn-warning">
Добавить транспондеры
</a>
</div>
</div>
</div>
<!-- VCH Load Data 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-danger 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-upload text-danger" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5"/>
<path d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708z"/>
</svg>
</div>
<h3 class="card-title mb-0">Добавление данных ВЧ загрузки</h3>
</div>
<p class="card-text">Загрузите данные ВЧ загрузки из HTML-файла с таблицами. Поддерживается выбор спутника для привязки данных.</p>
<a href="{% url 'mainapp:vch_load' %}" class="btn btn-danger">
Добавить данные ВЧ загрузки
</a>
</div>
</div>
</div>
<!-- Lyngsat Data Fill 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-secondary 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-cloud-download text-secondary" viewBox="0 0 16 16">
<path d="M4.406 1.342A5.53 5.53 0 0 1 8 0c2.69 0 4.923 2 5.166 4.579C14.758 4.804 16 6.137 16 7.773 16 9.569 14.502 11 12.687 11H10a.5.5 0 0 1 0-1h2.688C13.979 10 15 8.988 15 7.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 2.825 10.328 1 8 1a4.53 4.53 0 0 0-2.941 1.1c-.757.652-1.153 1.438-1.153 2.055v.448l-.445.049C2.064 4.805 1 5.952 1 7.318 1 8.785 2.23 10 3.781 10H6a.5.5 0 0 1 0 1H3.781C1.708 11 0 9.366 0 7.318c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383"/>
<path d="M7.646 15.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 14.293V5.5a.5.5 0 0 0-1 0v8.793l-2.146-2.147a.5.5 0 0 0-.708.708z"/>
</svg>
</div>
<h3 class="card-title mb-0">Заполнение данных Lyngsat</h3>
</div>
<p class="card-text">Загрузите данные о транспондерах спутников с сайта Lyngsat. Выберите спутники и регионы для парсинга данных.</p>
<a href="{% url 'mainapp:fill_lyngsat_data' %}" class="btn btn-secondary">
Заполнить данные Lyngsat
</a>
</div>
</div>
</div>
<!-- Calculation 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-info 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-calculator text-info" viewBox="0 0 16 16">
<path d="M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v4h2V2a1 1 0 0 0-1-1M5 6v1h1V6zm2 0v1h1V6zm2 0v1h1V6zm2 0v1h1V6zm1 2v1h1V8zm0 2v1h1v-1zm0 2v1h1v-1zm-8-6v8H3V8zm2 0v8h1V8zm2 0v8h1V8zm2 0v8h1V8z"/>
</svg>
</div>
<h3 class="card-title mb-0">Привязка ВЧ загрузки</h3>
</div>
<p class="card-text">Привязка ВЧ загрузки с sigma</p>
<a href="{% url 'mainapp:link_vch_sigma' %}" class="btn btn-info">
Открыть форму
</a>
</div>
</div>
</div>
<!-- New Event 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-success 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-plus-circle text-success" viewBox="0 0 16 16">
<path d="M8 0a8 8 0 1 0 0 16A8 8 0 0 0 8 0M4.5 7.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5M7.5 4.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 0 1h-1a.5.5 0 0 1-.5-.5m1 3a.5.5 0 0 1 .5.5v3a.5.5 0 0 1-1 0v-3a.5.5 0 0 1 .5-.5"/>
</svg>
</div>
<h3 class="card-title mb-0">Формирование таблицы для Кубсатов</h3>
</div>
<p class="card-text">Добавьте новое событие с помощью выбора спутника и загрузки файла данных.</p>
<a href="{% url 'mainapp:kubsat_excel' %}" class="btn btn-success">
Добавить событие
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,34 +1,34 @@
{% extends 'mainapp/base.html' %}
{% block title %}Загрузка данных из CSV{% endblock %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-success text-white">
<h2 class="mb-0">Загрузка данных из CSV</h2>
</div>
<div class="card-body">
{% include 'mainapp/components/_messages.html' %}
<p class="card-text">Загрузите CSV-файл для загрузки данных в базу.</p>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<!-- Form fields with Bootstrap styling -->
{% include 'mainapp/components/_form_field.html' with field=form.file %}
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary me-md-2">Назад</a>
<button type="submit" class="btn btn-success">Добавить в базу</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% extends 'mainapp/base.html' %}
{% block title %}Загрузка данных из CSV{% endblock %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-success text-white">
<h2 class="mb-0">Загрузка данных из CSV</h2>
</div>
<div class="card-body">
{% include 'mainapp/components/_messages.html' %}
<p class="card-text">Загрузите CSV-файл для загрузки данных в базу.</p>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<!-- Form fields with Bootstrap styling -->
{% include 'mainapp/components/_form_field.html' with field=form.file %}
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary me-md-2">Назад</a>
<button type="submit" class="btn btn-success">Добавить в базу</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,36 +1,36 @@
{% extends 'mainapp/base.html' %}
{% block title %}Загрузка данных из Excel{% endblock %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h2 class="mb-0">Загрузка данных из Excel</h2>
</div>
<div class="card-body">
{% include 'mainapp/components/_messages.html' %}
<p class="card-text">Загрузите Excel-файл и выберите спутник для загрузки данных в базу.</p>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<!-- Form fields with Bootstrap styling -->
{% include 'mainapp/components/_form_field.html' with field=form.file %}
{% include 'mainapp/components/_form_field.html' with field=form.sat_choice %}
{% include 'mainapp/components/_form_field.html' with field=form.number_input %}
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary me-md-2">Назад</a>
<button type="submit" class="btn btn-primary">Добавить в базу</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% extends 'mainapp/base.html' %}
{% block title %}Загрузка данных из Excel{% endblock %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h2 class="mb-0">Загрузка данных из Excel</h2>
</div>
<div class="card-body">
{% include 'mainapp/components/_messages.html' %}
<p class="card-text">Загрузите Excel-файл и выберите спутник для загрузки данных в базу.</p>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<!-- Form fields with Bootstrap styling -->
{% include 'mainapp/components/_form_field.html' with field=form.file %}
{% include 'mainapp/components/_form_field.html' with field=form.sat_choice %}
{% include 'mainapp/components/_form_field.html' with field=form.number_input %}
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary me-md-2">Назад</a>
<button type="submit" class="btn btn-primary">Добавить в базу</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,42 +1,42 @@
{% load static %}
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
<title>{% block title %}Геолокация{% endblock %}</title>
<!-- Bootstrap Icons -->
<link href="{% static 'bootstrap-icons/bootstrap-icons.css' %}" rel="stylesheet">
<!-- Bootstrap CSS -->
<link href="{% static 'bootstrap/bootstrap.min.css' %}" rel="stylesheet">
<!-- Дополнительные стили -->
{% block extra_css %}{% endblock %}
</head>
<body>
<!-- Навигационная панель -->
{% include 'mainapp/components/_navbar.html' %}
<!-- Сообщения -->
<div class="container mt-3">
{% include 'mainapp/components/_messages.html' %}
</div>
<!-- Основной контент -->
<main class="{% if full_width_page %}container-fluid p-0{% else %}container mt-4{% endif %}">
{% block content %}{% endblock %}
</main>
<!-- Bootstrap JS -->
<script src="{% static 'bootstrap/bootstrap.bundle.min.js' %}" defer></script>
<!-- Дополнительные скрипты -->
{% block extra_js %}{% endblock %}
</body>
{% load static %}
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
<title>{% block title %}Геолокация{% endblock %}</title>
<!-- Bootstrap Icons -->
<link href="{% static 'bootstrap-icons/bootstrap-icons.css' %}" rel="stylesheet">
<!-- Bootstrap CSS -->
<link href="{% static 'bootstrap/bootstrap.min.css' %}" rel="stylesheet">
<!-- Дополнительные стили -->
{% block extra_css %}{% endblock %}
</head>
<body>
<!-- Навигационная панель -->
{% include 'mainapp/components/_navbar.html' %}
<!-- Сообщения -->
<div class="container mt-3">
{% include 'mainapp/components/_messages.html' %}
</div>
<!-- Основной контент -->
<main class="{% if full_width_page %}container-fluid p-0{% else %}container mt-4{% endif %}">
{% block content %}{% endblock %}
</main>
<!-- Bootstrap JS -->
<script src="{% static 'bootstrap/bootstrap.bundle.min.js' %}" defer></script>
<!-- Дополнительные скрипты -->
{% block extra_js %}{% endblock %}
</body>
</html>

View File

@@ -1,33 +1,33 @@
{% comment %}
Переиспользуемый компонент для отображения полей формы
Использование:
{% include 'mainapp/components/_form_field.html' with field=form.field_name %}
{% include 'mainapp/components/_form_field.html' with field=form.field_name label_class="custom-label" %}
{% endcomment %}
<div class="mb-3">
<label for="{{ field.id_for_label }}" class="form-label {% if label_class %}{{ label_class }}{% endif %}">
{{ field.label }}
{% if field.field.required %}<span class="text-danger">*</span>{% endif %}
</label>
{% if field.field.widget.input_type == 'checkbox' %}
<div class="form-check">
{{ field }}
</div>
{% else %}
{{ field }}
{% endif %}
{% if field.errors %}
<div class="invalid-feedback d-block">
{% for error in field.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
{% if field.help_text %}
<small class="form-text text-muted">{{ field.help_text }}</small>
{% endif %}
</div>
{% comment %}
Переиспользуемый компонент для отображения полей формы
Использование:
{% include 'mainapp/components/_form_field.html' with field=form.field_name %}
{% include 'mainapp/components/_form_field.html' with field=form.field_name label_class="custom-label" %}
{% endcomment %}
<div class="mb-3">
<label for="{{ field.id_for_label }}" class="form-label {% if label_class %}{{ label_class }}{% endif %}">
{{ field.label }}
{% if field.field.required %}<span class="text-danger">*</span>{% endif %}
</label>
{% if field.field.widget.input_type == 'checkbox' %}
<div class="form-check">
{{ field }}
</div>
{% else %}
{{ field }}
{% endif %}
{% if field.errors %}
<div class="invalid-feedback d-block">
{% for error in field.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
{% if field.help_text %}
<small class="form-text text-muted">{{ field.help_text }}</small>
{% endif %}
</div>

View File

@@ -1,25 +1,25 @@
{% comment %}
Переиспользуемый компонент для отображения сообщений Django
Использование:
{% include 'mainapp/components/_messages.html' %}
{% endcomment %}
{% if messages %}
<div class="messages-container">
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{% if message.tags == 'error' %}
<i class="bi bi-exclamation-triangle-fill me-2"></i>
{% elif message.tags == 'success' %}
<i class="bi bi-check-circle-fill me-2"></i>
{% elif message.tags == 'warning' %}
<i class="bi bi-exclamation-circle-fill me-2"></i>
{% elif message.tags == 'info' %}
<i class="bi bi-info-circle-fill me-2"></i>
{% endif %}
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
</div>
{% endif %}
{% comment %}
Переиспользуемый компонент для отображения сообщений Django
Использование:
{% include 'mainapp/components/_messages.html' %}
{% endcomment %}
{% if messages %}
<div class="messages-container">
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{% if message.tags == 'error' %}
<i class="bi bi-exclamation-triangle-fill me-2"></i>
{% elif message.tags == 'success' %}
<i class="bi bi-check-circle-fill me-2"></i>
{% elif message.tags == 'warning' %}
<i class="bi bi-exclamation-circle-fill me-2"></i>
{% elif message.tags == 'info' %}
<i class="bi bi-info-circle-fill me-2"></i>
{% endif %}
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
</div>
{% endif %}

View File

@@ -1,57 +1,57 @@
{% comment %}
Переиспользуемый компонент навигационной панели
Использование:
{% include 'mainapp/components/_navbar.html' %}
{% endcomment %}
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{% url 'mainapp:home' %}">Геолокация</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
{% if user.is_authenticated %}
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="{% url 'mainapp:home' %}">Объекты</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'mainapp:actions' %}">Действия</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'mapsapp:3dmap' %}">3D карта</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'mapsapp:2dmap' %}">2D карта</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'admin:index' %}">Админ панель</a>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
{% if user.first_name and user.last_name %}
{{ user.first_name }} {{ user.last_name }}
{% elif user.get_full_name %}
{{ user.get_full_name }}
{% else %}
{{ user.username }}
{% endif %}
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="{% url 'logout' %}">Выйти</a></li>
</ul>
</li>
</ul>
{% else %}
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="{% url 'login' %}">Войти</a>
</li>
</ul>
{% endif %}
</div>
</div>
</nav>
{% comment %}
Переиспользуемый компонент навигационной панели
Использование:
{% include 'mainapp/components/_navbar.html' %}
{% endcomment %}
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{% url 'mainapp:home' %}">Геолокация</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
{% if user.is_authenticated %}
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="{% url 'mainapp:home' %}">Объекты</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'mainapp:actions' %}">Действия</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'mapsapp:3dmap' %}">3D карта</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'mapsapp:2dmap' %}">2D карта</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'admin:index' %}">Админ панель</a>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
{% if user.first_name and user.last_name %}
{{ user.first_name }} {{ user.last_name }}
{% elif user.get_full_name %}
{{ user.get_full_name }}
{% else %}
{{ user.username }}
{% endif %}
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="{% url 'logout' %}">Выйти</a></li>
</ul>
</li>
</ul>
{% else %}
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="{% url 'login' %}">Войти</a>
</li>
</ul>
{% endif %}
</div>
</div>
</nav>

View File

@@ -1,32 +1,32 @@
{% comment %}
Переиспользуемый компонент для заголовков таблиц с сортировкой
Использование:
{% include 'mainapp/components/_table_header.html' with label="Имя" field="name" sort=sort %}
{% include 'mainapp/components/_table_header.html' with label="Частота" field="frequency" sort=sort sortable=False %}
{% endcomment %}
<th scope="col">
{% if sortable != False %}
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == field %}-{{ field }}{% elif sort == '-'|add:field %}{{ field }}{% else %}{{ field }}{% endif %}"
class="text-white text-decoration-none d-inline-flex align-items-center">
{{ label }}
{% if sort == field %}
<i class="bi bi-sort-up ms-1"></i>
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}"
class="text-white ms-1" title="Сбросить сортировку">
<i class="bi bi-x-lg"></i>
</a>
{% elif sort == '-'|add:field %}
<i class="bi bi-sort-down ms-1"></i>
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}"
class="text-white ms-1" title="Сбросить сортировку">
<i class="bi bi-x-lg"></i>
</a>
{% else %}
<i class="bi bi-arrow-down-up ms-1"></i>
{% endif %}
</a>
{% else %}
{{ label }}
{% endif %}
</th>
{% comment %}
Переиспользуемый компонент для заголовков таблиц с сортировкой
Использование:
{% include 'mainapp/components/_table_header.html' with label="Имя" field="name" sort=sort %}
{% include 'mainapp/components/_table_header.html' with label="Частота" field="frequency" sort=sort sortable=False %}
{% endcomment %}
<th scope="col">
{% if sortable != False %}
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == field %}-{{ field }}{% elif sort == '-'|add:field %}{{ field }}{% else %}{{ field }}{% endif %}"
class="text-white text-decoration-none d-inline-flex align-items-center">
{{ label }}
{% if sort == field %}
<i class="bi bi-sort-up ms-1"></i>
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}"
class="text-white ms-1" title="Сбросить сортировку">
<i class="bi bi-x-lg"></i>
</a>
{% elif sort == '-'|add:field %}
<i class="bi bi-sort-down ms-1"></i>
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}"
class="text-white ms-1" title="Сбросить сортировку">
<i class="bi bi-x-lg"></i>
</a>
{% else %}
<i class="bi bi-arrow-down-up ms-1"></i>
{% endif %}
</a>
{% else %}
{{ label }}
{% endif %}
</th>

View File

@@ -1,118 +1,118 @@
{% extends 'mainapp/base.html' %}
{% block title %}Заполнение данных Lyngsat{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h3 class="mb-0">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-cloud-download me-2" viewBox="0 0 16 16">
<path d="M4.406 1.342A5.53 5.53 0 0 1 8 0c2.69 0 4.923 2 5.166 4.579C14.758 4.804 16 6.137 16 7.773 16 9.569 14.502 11 12.687 11H10a.5.5 0 0 1 0-1h2.688C13.979 10 15 8.988 15 7.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 2.825 10.328 1 8 1a4.53 4.53 0 0 0-2.941 1.1c-.757.652-1.153 1.438-1.153 2.055v.448l-.445.049C2.064 4.805 1 5.952 1 7.318 1 8.785 2.23 10 3.781 10H6a.5.5 0 0 1 0 1H3.781C1.708 11 0 9.366 0 7.318c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383"/>
<path d="M7.646 15.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 14.293V5.5a.5.5 0 0 0-1 0v8.793l-2.146-2.147a.5.5 0 0 0-.708.708z"/>
</svg>
Заполнение данных из Lyngsat
</h3>
</div>
<div class="card-body">
<!-- Alert messages -->
{% include 'mainapp/components/_messages.html' %}
<div class="alert alert-info" role="alert">
<strong>Внимание!</strong> Процесс заполнения данных может занять продолжительное время,
так как выполняются запросы к внешнему сайту Lyngsat. Пожалуйста, дождитесь завершения операции.
</div>
<form method="post" class="needs-validation" novalidate>
{% csrf_token %}
<!-- Satellites Selection -->
<div class="mb-4">
<label for="{{ form.satellites.id_for_label }}" class="form-label fw-bold">
{{ 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>
<!-- Regions Selection -->
<div class="mb-4">
<label for="{{ form.regions.id_for_label }}" class="form-label fw-bold">
{{ form.regions.label }}
</label>
{{ form.regions }}
{% if form.regions.help_text %}
<div class="form-text">{{ form.regions.help_text }}</div>
{% endif %}
{% if form.regions.errors %}
<div class="invalid-feedback d-block">
{{ form.regions.errors }}
</div>
{% endif %}
</div>
<!-- Buttons -->
<div class="d-grid gap-2 d-md-flex justify-content-md-between">
<a href="{% url 'mainapp:actions' %}" class="btn btn-secondary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-left me-1" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8"/>
</svg>
Назад
</a>
<button type="submit" class="btn btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-download me-1" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708z"/>
</svg>
Заполнить данные
</button>
</div>
</form>
</div>
</div>
<!-- Info Card -->
<div class="card mt-4 shadow-sm">
<div class="card-body">
<h5 class="card-title">Информация</h5>
<p class="card-text">
Эта форма позволяет загрузить данные о транспондерах спутников с сайта Lyngsat.
Выберите один или несколько спутников и регионы для парсинга данных.
</p>
<ul>
<li>Данные включают частоты, поляризацию, модуляцию, стандарты и другие параметры</li>
<li>Процесс может занять несколько минут в зависимости от количества выбранных спутников</li>
<li>Существующие записи будут обновлены, новые - созданы</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<script>
// Form validation
(function() {
'use strict';
var forms = document.querySelectorAll('.needs-validation');
Array.prototype.slice.call(forms).forEach(function(form) {
form.addEventListener('submit', function(event) {
if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
}
form.classList.add('was-validated');
}, false);
});
})();
</script>
{% endblock %}
{% extends 'mainapp/base.html' %}
{% block title %}Заполнение данных Lyngsat{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h3 class="mb-0">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-cloud-download me-2" viewBox="0 0 16 16">
<path d="M4.406 1.342A5.53 5.53 0 0 1 8 0c2.69 0 4.923 2 5.166 4.579C14.758 4.804 16 6.137 16 7.773 16 9.569 14.502 11 12.687 11H10a.5.5 0 0 1 0-1h2.688C13.979 10 15 8.988 15 7.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 2.825 10.328 1 8 1a4.53 4.53 0 0 0-2.941 1.1c-.757.652-1.153 1.438-1.153 2.055v.448l-.445.049C2.064 4.805 1 5.952 1 7.318 1 8.785 2.23 10 3.781 10H6a.5.5 0 0 1 0 1H3.781C1.708 11 0 9.366 0 7.318c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383"/>
<path d="M7.646 15.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 14.293V5.5a.5.5 0 0 0-1 0v8.793l-2.146-2.147a.5.5 0 0 0-.708.708z"/>
</svg>
Заполнение данных из Lyngsat
</h3>
</div>
<div class="card-body">
<!-- Alert messages -->
{% include 'mainapp/components/_messages.html' %}
<div class="alert alert-info" role="alert">
<strong>Внимание!</strong> Процесс заполнения данных может занять продолжительное время,
так как выполняются запросы к внешнему сайту Lyngsat. Пожалуйста, дождитесь завершения операции.
</div>
<form method="post" class="needs-validation" novalidate>
{% csrf_token %}
<!-- Satellites Selection -->
<div class="mb-4">
<label for="{{ form.satellites.id_for_label }}" class="form-label fw-bold">
{{ 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>
<!-- Regions Selection -->
<div class="mb-4">
<label for="{{ form.regions.id_for_label }}" class="form-label fw-bold">
{{ form.regions.label }}
</label>
{{ form.regions }}
{% if form.regions.help_text %}
<div class="form-text">{{ form.regions.help_text }}</div>
{% endif %}
{% if form.regions.errors %}
<div class="invalid-feedback d-block">
{{ form.regions.errors }}
</div>
{% endif %}
</div>
<!-- Buttons -->
<div class="d-grid gap-2 d-md-flex justify-content-md-between">
<a href="{% url 'mainapp:actions' %}" class="btn btn-secondary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-left me-1" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8"/>
</svg>
Назад
</a>
<button type="submit" class="btn btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-download me-1" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708z"/>
</svg>
Заполнить данные
</button>
</div>
</form>
</div>
</div>
<!-- Info Card -->
<div class="card mt-4 shadow-sm">
<div class="card-body">
<h5 class="card-title">Информация</h5>
<p class="card-text">
Эта форма позволяет загрузить данные о транспондерах спутников с сайта Lyngsat.
Выберите один или несколько спутников и регионы для парсинга данных.
</p>
<ul>
<li>Данные включают частоты, поляризацию, модуляцию, стандарты и другие параметры</li>
<li>Процесс может занять несколько минут в зависимости от количества выбранных спутников</li>
<li>Существующие записи будут обновлены, новые - созданы</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<script>
// Form validation
(function() {
'use strict';
var forms = document.querySelectorAll('.needs-validation');
Array.prototype.slice.call(forms).forEach(function(form) {
form.addEventListener('submit', function(event) {
if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
}
form.classList.add('was-validated');
}, false);
});
})();
</script>
{% endblock %}

View File

@@ -1,68 +1,68 @@
{% extends 'mainapp/base.html' %}
{% block title %}Привязка ВЧ{% endblock %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-6">
<div class="card shadow-sm">
<div class="card-header bg-info text-white">
<h2 class="mb-0">Привязка ВЧ загрузки</h2>
</div>
<div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert {{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
<p class="card-text">Введите допустимый разброс для частоты и полосы</p>
<form method="post">
{% csrf_token %}
<div class="mb-3">
<label for="{{ form.sat_choice.id_for_label }}" class="form-label">Выберите спутник:</label>
{{ form.sat_choice }}
{% if form.sat_choice.errors %}
<div class="text-danger mt-1">{{ form.sat_choice.errors }}</div>
{% endif %}
</div>
{% comment %} <div class="mb-3">
<label for="{{ form.ku_range.id_for_label }}" class="form-label">Выберите перенос по частоте(МГц):</label>
{{ form.ku_range }}
{% if form.ku_range.errors %}
<div class="text-danger mt-1">{{ form.ku_range.errors }}</div>
{% endif %}
</div> {% endcomment %}
<div class="mb-3">
<label for="{{ form.value1.id_for_label }}" class="form-label">Разброс по частоте(в МГц)</label>
{{ form.value1 }}
{% if form.value1.errors %}
<div class="text-danger mt-1">{{ form.value1.errors }}</div>
{% endif %}
</div>
<div class="mb-3">
<label for="{{ form.value2.id_for_label }}" class="form-label">Разброс по полосе(в %)</label>
{{ form.value2 }}
{% if form.value2.errors %}
<div class="text-danger mt-1">{{ form.value2.errors }}</div>
{% endif %}
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary me-md-2">Назад</a>
{% comment %} <a href="{% url 'mainapp:home' %}" class="btn btn-danger me-md-2">Сбросить привязку</a> {% endcomment %}
<button type="submit" class="btn btn-info">Выполнить привязку</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% extends 'mainapp/base.html' %}
{% block title %}Привязка ВЧ{% endblock %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-6">
<div class="card shadow-sm">
<div class="card-header bg-info text-white">
<h2 class="mb-0">Привязка ВЧ загрузки</h2>
</div>
<div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert {{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
<p class="card-text">Введите допустимый разброс для частоты и полосы</p>
<form method="post">
{% csrf_token %}
<div class="mb-3">
<label for="{{ form.sat_choice.id_for_label }}" class="form-label">Выберите спутник:</label>
{{ form.sat_choice }}
{% if form.sat_choice.errors %}
<div class="text-danger mt-1">{{ form.sat_choice.errors }}</div>
{% endif %}
</div>
{% comment %} <div class="mb-3">
<label for="{{ form.ku_range.id_for_label }}" class="form-label">Выберите перенос по частоте(МГц):</label>
{{ form.ku_range }}
{% if form.ku_range.errors %}
<div class="text-danger mt-1">{{ form.ku_range.errors }}</div>
{% endif %}
</div> {% endcomment %}
<div class="mb-3">
<label for="{{ form.value1.id_for_label }}" class="form-label">Разброс по частоте(в МГц)</label>
{{ form.value1 }}
{% if form.value1.errors %}
<div class="text-danger mt-1">{{ form.value1.errors }}</div>
{% endif %}
</div>
<div class="mb-3">
<label for="{{ form.value2.id_for_label }}" class="form-label">Разброс по полосе(в %)</label>
{{ form.value2 }}
{% if form.value2.errors %}
<div class="text-danger mt-1">{{ form.value2.errors }}</div>
{% endif %}
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary me-md-2">Назад</a>
{% comment %} <a href="{% url 'mainapp:home' %}" class="btn btn-danger me-md-2">Сбросить привязку</a> {% endcomment %}
<button type="submit" class="btn btn-info">Выполнить привязку</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,241 +1,241 @@
{% extends 'mainapp/base.html' %}
{% block title %}Статус задачи Lyngsat{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h3 class="mb-0">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-hourglass-split me-2" viewBox="0 0 16 16">
<path d="M2.5 15a.5.5 0 1 1 0-1h1v-1a4.5 4.5 0 0 1 2.557-4.06c.29-.139.443-.377.443-.59v-.7c0-.213-.154-.451-.443-.59A4.5 4.5 0 0 1 3.5 3V2h-1a.5.5 0 0 1 0-1h11a.5.5 0 0 1 0 1h-1v1a4.5 4.5 0 0 1-2.557 4.06c-.29.139-.443.377-.443.59v.7c0 .213.154.451.443.59A4.5 4.5 0 0 1 12.5 13v1h1a.5.5 0 0 1 0 1zm2-13v1c0 .537.12 1.045.337 1.5h6.326c.216-.455.337-.963.337-1.5V2zm3 6.35c0 .701-.478 1.236-1.011 1.492A3.5 3.5 0 0 0 4.5 13s.866-1.299 3-1.48zm1 0v3.17c2.134.181 3 1.48 3 1.48a3.5 3.5 0 0 0-1.989-3.158C8.978 9.586 8.5 9.052 8.5 8.351z"/>
</svg>
Статус задачи заполнения данных Lyngsat
</h3>
</div>
<div class="card-body">
{% if task_id %}
<div class="mb-3">
<strong>ID задачи:</strong> <code id="task-id">{{ task_id }}</code>
</div>
<!-- Progress Bar -->
<div class="mb-4">
<div class="d-flex justify-content-between mb-2">
<span id="status-text">Загрузка статуса...</span>
<span id="progress-percent">0%</span>
</div>
<div class="progress" style="height: 25px;">
<div id="progress-bar" class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar" style="width: 0%;"
aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
<span id="progress-text">0%</span>
</div>
</div>
</div>
<!-- Task State -->
<div id="task-state-container" class="alert alert-info" role="alert">
<strong>Состояние:</strong> <span id="task-state">Проверка...</span>
</div>
<!-- Results Container (hidden by default) -->
<div id="results-container" class="d-none">
<h5 class="mt-4">Результаты обработки</h5>
<div class="row">
<div class="col-md-6">
<div class="card mb-3">
<div class="card-body">
<h6 class="card-subtitle mb-2 text-muted">Обработано спутников</h6>
<h3 class="card-title" id="result-satellites">-</h3>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-3">
<div class="card-body">
<h6 class="card-subtitle mb-2 text-muted">Обработано источников</h6>
<h3 class="card-title" id="result-sources">-</h3>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-3 border-success">
<div class="card-body">
<h6 class="card-subtitle mb-2 text-success">Создано записей</h6>
<h3 class="card-title text-success" id="result-created">-</h3>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-3 border-info">
<div class="card-body">
<h6 class="card-subtitle mb-2 text-info">Обновлено записей</h6>
<h3 class="card-title text-info" id="result-updated">-</h3>
</div>
</div>
</div>
</div>
<!-- Errors -->
<div id="errors-container" class="d-none">
<h6 class="text-danger">Ошибки при обработке:</h6>
<div class="alert alert-warning">
<ul id="errors-list" class="mb-0"></ul>
</div>
</div>
</div>
<!-- Error Container (hidden by default) -->
<div id="error-container" class="alert alert-danger d-none" role="alert">
<strong>Ошибка:</strong> <span id="error-text"></span>
</div>
<!-- Action Buttons -->
<div class="d-grid gap-2 d-md-flex justify-content-md-between mt-4">
<a href="{% url 'mainapp:fill_lyngsat_data' %}" class="btn btn-secondary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-left me-1" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8"/>
</svg>
Назад к форме
</a>
<a href="{% url 'mainapp:actions' %}" class="btn btn-outline-primary" id="actions-btn">
Перейти к действиям
</a>
</div>
{% else %}
<div class="alert alert-warning" role="alert">
ID задачи не указан. Пожалуйста, запустите задачу через форму заполнения данных.
</div>
<a href="{% url 'mainapp:fill_lyngsat_data' %}" class="btn btn-primary">
Перейти к форме
</a>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% if task_id %}
<script>
let taskId = '{{ task_id }}';
let pollInterval;
let isCompleted = false;
function updateProgress(data) {
const statusText = document.getElementById('status-text');
const progressBar = document.getElementById('progress-bar');
const progressText = document.getElementById('progress-text');
const progressPercent = document.getElementById('progress-percent');
const taskState = document.getElementById('task-state');
const taskStateContainer = document.getElementById('task-state-container');
// Update state
taskState.textContent = data.state;
if (data.state === 'PENDING') {
statusText.textContent = 'Задача в очереди...';
taskStateContainer.className = 'alert alert-info';
} else if (data.state === 'PROGRESS') {
const percent = data.percent || 0;
statusText.textContent = data.status || 'Обработка...';
progressBar.style.width = percent + '%';
progressBar.setAttribute('aria-valuenow', percent);
progressText.textContent = percent + '%';
progressPercent.textContent = percent + '%';
taskStateContainer.className = 'alert alert-info';
} else if (data.state === 'SUCCESS') {
statusText.textContent = 'Задача завершена успешно!';
progressBar.style.width = '100%';
progressBar.setAttribute('aria-valuenow', 100);
progressText.textContent = '100%';
progressPercent.textContent = '100%';
progressBar.classList.remove('progress-bar-animated');
progressBar.classList.add('bg-success');
taskStateContainer.className = 'alert alert-success';
// Show results
if (data.result) {
showResults(data.result);
}
isCompleted = true;
clearInterval(pollInterval);
} else if (data.state === 'FAILURE') {
statusText.textContent = 'Ошибка при выполнении задачи';
progressBar.classList.remove('progress-bar-animated');
progressBar.classList.add('bg-danger');
taskStateContainer.className = 'alert alert-danger';
// Show error
const errorContainer = document.getElementById('error-container');
const errorText = document.getElementById('error-text');
errorText.textContent = data.error || 'Неизвестная ошибка';
errorContainer.classList.remove('d-none');
isCompleted = true;
clearInterval(pollInterval);
}
}
function showResults(result) {
const resultsContainer = document.getElementById('results-container');
resultsContainer.classList.remove('d-none');
document.getElementById('result-satellites').textContent = result.total_satellites || 0;
document.getElementById('result-sources').textContent = result.total_sources || 0;
document.getElementById('result-created').textContent = result.created || 0;
document.getElementById('result-updated').textContent = result.updated || 0;
// Show errors if any
if (result.errors && result.errors.length > 0) {
const errorsContainer = document.getElementById('errors-container');
const errorsList = document.getElementById('errors-list');
errorsContainer.classList.remove('d-none');
errorsList.innerHTML = '';
result.errors.slice(0, 10).forEach(error => {
const li = document.createElement('li');
li.textContent = error;
errorsList.appendChild(li);
});
if (result.errors.length > 10) {
const li = document.createElement('li');
li.textContent = `И еще ${result.errors.length - 10} ошибок...`;
li.className = 'text-muted';
errorsList.appendChild(li);
}
}
}
function checkTaskStatus() {
fetch(`/api/lyngsat-task-status/${taskId}/`)
.then(response => response.json())
.then(data => {
updateProgress(data);
})
.catch(error => {
console.error('Error checking task status:', error);
});
}
// Start polling
document.addEventListener('DOMContentLoaded', function() {
checkTaskStatus();
pollInterval = setInterval(checkTaskStatus, 2000); // Poll every 2 seconds
// Stop polling after 30 minutes
setTimeout(() => {
if (!isCompleted) {
clearInterval(pollInterval);
document.getElementById('status-text').textContent = 'Превышено время ожидания. Обновите страницу для проверки статуса.';
}
}, 30 * 60 * 1000);
});
</script>
{% endif %}
{% endblock %}
{% extends 'mainapp/base.html' %}
{% block title %}Статус задачи Lyngsat{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h3 class="mb-0">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-hourglass-split me-2" viewBox="0 0 16 16">
<path d="M2.5 15a.5.5 0 1 1 0-1h1v-1a4.5 4.5 0 0 1 2.557-4.06c.29-.139.443-.377.443-.59v-.7c0-.213-.154-.451-.443-.59A4.5 4.5 0 0 1 3.5 3V2h-1a.5.5 0 0 1 0-1h11a.5.5 0 0 1 0 1h-1v1a4.5 4.5 0 0 1-2.557 4.06c-.29.139-.443.377-.443.59v.7c0 .213.154.451.443.59A4.5 4.5 0 0 1 12.5 13v1h1a.5.5 0 0 1 0 1zm2-13v1c0 .537.12 1.045.337 1.5h6.326c.216-.455.337-.963.337-1.5V2zm3 6.35c0 .701-.478 1.236-1.011 1.492A3.5 3.5 0 0 0 4.5 13s.866-1.299 3-1.48zm1 0v3.17c2.134.181 3 1.48 3 1.48a3.5 3.5 0 0 0-1.989-3.158C8.978 9.586 8.5 9.052 8.5 8.351z"/>
</svg>
Статус задачи заполнения данных Lyngsat
</h3>
</div>
<div class="card-body">
{% if task_id %}
<div class="mb-3">
<strong>ID задачи:</strong> <code id="task-id">{{ task_id }}</code>
</div>
<!-- Progress Bar -->
<div class="mb-4">
<div class="d-flex justify-content-between mb-2">
<span id="status-text">Загрузка статуса...</span>
<span id="progress-percent">0%</span>
</div>
<div class="progress" style="height: 25px;">
<div id="progress-bar" class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar" style="width: 0%;"
aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
<span id="progress-text">0%</span>
</div>
</div>
</div>
<!-- Task State -->
<div id="task-state-container" class="alert alert-info" role="alert">
<strong>Состояние:</strong> <span id="task-state">Проверка...</span>
</div>
<!-- Results Container (hidden by default) -->
<div id="results-container" class="d-none">
<h5 class="mt-4">Результаты обработки</h5>
<div class="row">
<div class="col-md-6">
<div class="card mb-3">
<div class="card-body">
<h6 class="card-subtitle mb-2 text-muted">Обработано спутников</h6>
<h3 class="card-title" id="result-satellites">-</h3>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-3">
<div class="card-body">
<h6 class="card-subtitle mb-2 text-muted">Обработано источников</h6>
<h3 class="card-title" id="result-sources">-</h3>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-3 border-success">
<div class="card-body">
<h6 class="card-subtitle mb-2 text-success">Создано записей</h6>
<h3 class="card-title text-success" id="result-created">-</h3>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-3 border-info">
<div class="card-body">
<h6 class="card-subtitle mb-2 text-info">Обновлено записей</h6>
<h3 class="card-title text-info" id="result-updated">-</h3>
</div>
</div>
</div>
</div>
<!-- Errors -->
<div id="errors-container" class="d-none">
<h6 class="text-danger">Ошибки при обработке:</h6>
<div class="alert alert-warning">
<ul id="errors-list" class="mb-0"></ul>
</div>
</div>
</div>
<!-- Error Container (hidden by default) -->
<div id="error-container" class="alert alert-danger d-none" role="alert">
<strong>Ошибка:</strong> <span id="error-text"></span>
</div>
<!-- Action Buttons -->
<div class="d-grid gap-2 d-md-flex justify-content-md-between mt-4">
<a href="{% url 'mainapp:fill_lyngsat_data' %}" class="btn btn-secondary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-left me-1" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8"/>
</svg>
Назад к форме
</a>
<a href="{% url 'mainapp:actions' %}" class="btn btn-outline-primary" id="actions-btn">
Перейти к действиям
</a>
</div>
{% else %}
<div class="alert alert-warning" role="alert">
ID задачи не указан. Пожалуйста, запустите задачу через форму заполнения данных.
</div>
<a href="{% url 'mainapp:fill_lyngsat_data' %}" class="btn btn-primary">
Перейти к форме
</a>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% if task_id %}
<script>
let taskId = '{{ task_id }}';
let pollInterval;
let isCompleted = false;
function updateProgress(data) {
const statusText = document.getElementById('status-text');
const progressBar = document.getElementById('progress-bar');
const progressText = document.getElementById('progress-text');
const progressPercent = document.getElementById('progress-percent');
const taskState = document.getElementById('task-state');
const taskStateContainer = document.getElementById('task-state-container');
// Update state
taskState.textContent = data.state;
if (data.state === 'PENDING') {
statusText.textContent = 'Задача в очереди...';
taskStateContainer.className = 'alert alert-info';
} else if (data.state === 'PROGRESS') {
const percent = data.percent || 0;
statusText.textContent = data.status || 'Обработка...';
progressBar.style.width = percent + '%';
progressBar.setAttribute('aria-valuenow', percent);
progressText.textContent = percent + '%';
progressPercent.textContent = percent + '%';
taskStateContainer.className = 'alert alert-info';
} else if (data.state === 'SUCCESS') {
statusText.textContent = 'Задача завершена успешно!';
progressBar.style.width = '100%';
progressBar.setAttribute('aria-valuenow', 100);
progressText.textContent = '100%';
progressPercent.textContent = '100%';
progressBar.classList.remove('progress-bar-animated');
progressBar.classList.add('bg-success');
taskStateContainer.className = 'alert alert-success';
// Show results
if (data.result) {
showResults(data.result);
}
isCompleted = true;
clearInterval(pollInterval);
} else if (data.state === 'FAILURE') {
statusText.textContent = 'Ошибка при выполнении задачи';
progressBar.classList.remove('progress-bar-animated');
progressBar.classList.add('bg-danger');
taskStateContainer.className = 'alert alert-danger';
// Show error
const errorContainer = document.getElementById('error-container');
const errorText = document.getElementById('error-text');
errorText.textContent = data.error || 'Неизвестная ошибка';
errorContainer.classList.remove('d-none');
isCompleted = true;
clearInterval(pollInterval);
}
}
function showResults(result) {
const resultsContainer = document.getElementById('results-container');
resultsContainer.classList.remove('d-none');
document.getElementById('result-satellites').textContent = result.total_satellites || 0;
document.getElementById('result-sources').textContent = result.total_sources || 0;
document.getElementById('result-created').textContent = result.created || 0;
document.getElementById('result-updated').textContent = result.updated || 0;
// Show errors if any
if (result.errors && result.errors.length > 0) {
const errorsContainer = document.getElementById('errors-container');
const errorsList = document.getElementById('errors-list');
errorsContainer.classList.remove('d-none');
errorsList.innerHTML = '';
result.errors.slice(0, 10).forEach(error => {
const li = document.createElement('li');
li.textContent = error;
errorsList.appendChild(li);
});
if (result.errors.length > 10) {
const li = document.createElement('li');
li.textContent = `И еще ${result.errors.length - 10} ошибок...`;
li.className = 'text-muted';
errorsList.appendChild(li);
}
}
}
function checkTaskStatus() {
fetch(`/api/lyngsat-task-status/${taskId}/`)
.then(response => response.json())
.then(data => {
updateProgress(data);
})
.catch(error => {
console.error('Error checking task status:', error);
});
}
// Start polling
document.addEventListener('DOMContentLoaded', function() {
checkTaskStatus();
pollInterval = setInterval(checkTaskStatus, 2000); // Poll every 2 seconds
// Stop polling after 30 minutes
setTimeout(() => {
if (!isCompleted) {
clearInterval(pollInterval);
document.getElementById('status-text').textContent = 'Превышено время ожидания. Обновите страницу для проверки статуса.';
}
}, 30 * 60 * 1000);
});
</script>
{% endif %}
{% endblock %}

View File

@@ -1,25 +1,25 @@
{% extends 'mainapp/base.html' %}
{% block title %}Удалить объект{% endblock %}
{% block content %}
<div class="container-fluid px-3">
<div class="row mb-3">
<div class="col-12">
<h2>Удалить объект "{{ object }}"?</h2>
</div>
</div>
<div class="card">
<div class="card-body">
<form method="post">
{% csrf_token %}
<p>Вы уверены, что хотите удалить этот объект? Это действие нельзя будет отменить.</p>
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-danger">Удалить</button>
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary ms-2">Отмена</a>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% extends 'mainapp/base.html' %}
{% block title %}Удалить объект{% endblock %}
{% block content %}
<div class="container-fluid px-3">
<div class="row mb-3">
<div class="col-12">
<h2>Удалить объект "{{ object }}"?</h2>
</div>
</div>
<div class="card">
<div class="card-body">
<form method="post">
{% csrf_token %}
<p>Вы уверены, что хотите удалить этот объект? Это действие нельзя будет отменить.</p>
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-danger">Удалить</button>
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary ms-2">Отмена</a>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,473 +1,473 @@
{% extends 'mainapp/base.html' %}
{% load static %}
{% load static leaflet_tags %}
{% load l10n %}
{% block title %}Просмотр объекта: {{ object.name }}{% endblock %}
{% block extra_css %}
<style>
.form-section { margin-bottom: 2rem; border: 1px solid #dee2e6; border-radius: 0.25rem; padding: 1rem; }
.form-section-header { border-bottom: 1px solid #dee2e6; padding-bottom: 0.5rem; margin-bottom: 1rem; }
.btn-action { margin-right: 0.5rem; }
.readonly-field { background-color: #f8f9fa; padding: 0.375rem 0.75rem; border: 1px solid #ced4da; border-radius: 0.25rem; }
.coord-group { border: 1px solid #dee2e6; padding: 0.75rem; border-radius: 0.25rem; margin-bottom: 1rem; }
.coord-group-header { font-weight: bold; margin-bottom: 0.5rem; }
.form-check-input { margin-top: 0.25rem; }
.datetime-group { display: flex; gap: 1rem; }
.datetime-group > div { flex: 1; }
#map { height: 500px; width: 100%; margin-bottom: 1rem; }
.map-container { margin-bottom: 1rem; }
.coord-sync-group { border: 1px solid #dee2e6; padding: 0.75rem; border-radius: 0.25rem; }
.map-controls {
display: flex;
gap: 10px;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.map-control-btn {
padding: 0.375rem 0.75rem;
border: 1px solid #ced4da;
background-color: #f8f9fa;
border-radius: 0.25rem;
cursor: pointer;
}
.map-control-btn.active {
background-color: #e9ecef;
border-color: #dee2e6;
}
.map-control-btn.edit {
background-color: #fff3cd;
border-color: #ffeeba;
}
.map-control-btn.save {
background-color: #d1ecf1;
border-color: #bee5eb;
}
.map-control-btn.cancel {
background-color: #f8d7da;
border-color: #f5c6cb;
}
.leaflet-marker-icon {
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.3));
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid px-3">
<div class="row mb-3">
<div class="col-12 d-flex justify-content-between align-items-center">
<h2>Просмотр объекта: {{ object.name }}</h2>
<div>
<a href="{% url 'mainapp:objitem_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-secondary btn-action">Назад</a>
</div>
</div>
</div>
<!-- Основная информация -->
<div class="form-section">
<div class="form-section-header">
<h4>Основная информация</h4>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Название:</label>
<div class="readonly-field">{{ object.name|default:"-" }}</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Дата создания:</label>
<div class="readonly-field">
{% if object.created_at %}{{ object.created_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Создан пользователем:</label>
<div class="readonly-field">
{% if object.created_by %}{{ object.created_by }}{% else %}-{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Дата последнего изменения:</label>
<div class="readonly-field">
{% if object.updated_at %}{{ object.updated_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Изменен пользователем:</label>
<div class="readonly-field">
{% if object.updated_by %}{{ object.updated_by }}{% else %}-{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- ВЧ загрузка -->
<div class="form-section">
<div class="form-section-header">
<h4>ВЧ загрузка</h4>
</div>
{% if object.parameter_obj %}
<div class="row">
<div class="col-md-3">
<div class="mb-3">
<label class="form-label">Спутник:</label>
<div class="readonly-field">{{ object.parameter_obj.id_satellite.name|default:"-" }}</div>
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label class="form-label">Частота (МГц):</label>
<div class="readonly-field">{{ object.parameter_obj.frequency|default:"-" }}</div>
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label class="form-label">Полоса (МГц):</label>
<div class="readonly-field">{{ object.parameter_obj.freq_range|default:"-" }}</div>
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label class="form-label">Поляризация:</label>
<div class="readonly-field">{{ object.parameter_obj.polarization.name|default:"-" }}</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-3">
<div class="mb-3">
<label class="form-label">Символьная скорость:</label>
<div class="readonly-field">{{ object.parameter_obj.bod_velocity|default:"-" }}</div>
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label class="form-label">Модуляция:</label>
<div class="readonly-field">{{ object.parameter_obj.modulation.name|default:"-" }}</div>
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label class="form-label">ОСШ:</label>
<div class="readonly-field">{{ object.parameter_obj.snr|default:"-" }}</div>
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label class="form-label">Стандарт:</label>
<div class="readonly-field">{{ object.parameter_obj.standard.name|default:"-" }}</div>
</div>
</div>
</div>
{% else %}
<div class="mb-3">
<p>Нет данных о ВЧ загрузке</p>
</div>
{% endif %}
</div>
<!-- Блок с картой -->
<div class="form-section">
<div class="form-section-header">
<h4>Карта</h4>
</div>
<div class="map-container">
<div id="map"></div>
</div>
</div>
<!-- Геоданные -->
<div class="form-section">
<div class="form-section-header">
<h4>Геоданные</h4>
</div>
{% if object.geo_obj %}
<!-- Координаты геолокации -->
<div class="coord-sync-group">
<div class="coord-group-header">Координаты геолокации</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Широта:</label>
<div class="readonly-field">
{% if object.geo_obj.coords %}{{ object.geo_obj.coords.y|floatformat:6 }}{% else %}-{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<label class="form-label">Долгота:</label>
<div class="readonly-field">
{% if object.geo_obj.coords %}{{ object.geo_obj.coords.x|floatformat:6 }}{% else %}-{% endif %}
</div>
</div>
</div>
</div>
<!-- Координаты Кубсата -->
<div class="coord-group">
<div class="coord-group-header">Координаты Кубсата</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Долгота:</label>
<div class="readonly-field">
{% if object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.x|floatformat:6 }}{% else %}-{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Широта:</label>
<div class="readonly-field">
{% if object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.y|floatformat:6 }}{% else %}-{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Координаты оперативников -->
<div class="coord-group">
<div class="coord-group-header">Координаты оперативников</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Долгота:</label>
<div class="readonly-field">
{% if object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.x|floatformat:6 }}{% else %}-{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Широта:</label>
<div class="readonly-field">
{% if object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.y|floatformat:6 }}{% else %}-{% endif %}
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Местоположение:</label>
<div class="readonly-field">{{ object.geo_obj.location|default:"-" }}</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Комментарий:</label>
<div class="readonly-field">{{ object.geo_obj.comment|default:"-" }}</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Дата и время:</label>
<div class="readonly-field">
{% if object.geo_obj.timestamp %}{{ object.geo_obj.timestamp|date:"d.m.Y H:i" }}{% else %}-{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-check-label">Усредненное значение:</label>
<div class="readonly-field">
{% if object.geo_obj.is_average %}Да{% else %}Нет{% endif %}
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-md-4">
<div class="mb-3">
<label class="form-label">Расстояние гео-кубсат, км:</label>
<div class="readonly-field">
{% if object.geo_obj.distance_coords_kup is not None %}
{{ object.geo_obj.distance_coords_kup|floatformat:2 }}
{% else %}
-
{% endif %}
</div>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label class="form-label">Расстояние гео-опер, км:</label>
<div class="readonly-field">
{% if object.geo_obj.distance_coords_valid is not None %}
{{ object.geo_obj.distance_coords_valid|floatformat:2 }}
{% else %}
-
{% endif %}
</div>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label class="form-label">Расстояние кубсат-опер, км:</label>
<div class="readonly-field">
{% if object.geo_obj.distance_kup_valid is not None %}
{{ object.geo_obj.distance_kup_valid|floatformat:2 }}
{% else %}
-
{% endif %}
</div>
</div>
</div>
</div>
{% else %}
<p>Нет данных о геолокации</p>
{% endif %}
</div>
</div>
<div class="d-flex justify-content-end mt-4">
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
<a href="{% url 'mainapp:objitem_update' object.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-primary btn-action">Редактировать</a>
{% endif %}
<a href="{% url 'mainapp:objitem_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-secondary btn-action">Назад</a>
</div>
</div>
{% endblock %}
{% block extra_js %}
{{ block.super }}
<!-- Подключаем Leaflet и его плагины -->
{% leaflet_js %}
{% leaflet_css %}
<script src="{% static 'leaflet-markers/js/leaflet-color-markers.js' %}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Инициализация карты
const map = L.map('map').setView([55.75, 37.62], 5);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
// Определяем цвета для маркеров
const colors = {
geo: 'blue',
kupsat: 'red',
valid: 'green'
};
// Функция для создания иконки маркера
function createMarkerIcon(color) {
return L.icon({
iconUrl: '{% static "leaflet-markers/img/marker-icon-" %}' + color + '.png',
shadowUrl: `{% static 'leaflet-markers/img/marker-shadow.png' %}`,
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
});
}
// Маркеры
const markers = {};
function createMarker(position, color, name) {
const marker = L.marker(position, {
draggable: false,
icon: createMarkerIcon(color),
title: name
}).addTo(map);
marker.bindPopup(name);
return marker;
}
// Получаем координаты из данных объекта
{% if object.geo_obj and object.geo_obj.coords %}
const geoLat = {{ object.geo_obj.coords.y|unlocalize }};
const geoLng = {{ object.geo_obj.coords.x|unlocalize }};
{% else %}
const geoLat = 55.75;
const geoLng = 37.62;
{% endif %}
{% if object.geo_obj and object.geo_obj.coords_kupsat %}
const kupsatLat = {{ object.geo_obj.coords_kupsat.y|unlocalize }};
const kupsatLng = {{ object.geo_obj.coords_kupsat.x|unlocalize }};
{% else %}
const kupsatLat = 55.75;
const kupsatLng = 37.61;
{% endif %}
{% if object.geo_obj and object.geo_obj.coords_valid %}
const validLat = {{ object.geo_obj.coords_valid.y|unlocalize }};
const validLng = {{ object.geo_obj.coords_valid.x|unlocalize }};
{% else %}
const validLat = 55.75;
const validLng = 37.63;
{% endif %}
// Создаем маркеры
markers.geo = createMarker(
[geoLat, geoLng],
colors.geo,
'Геолокация'
);
markers.kupsat = createMarker(
[kupsatLat, kupsatLng],
colors.kupsat,
'Кубсат'
);
markers.valid = createMarker(
[validLat, validLng],
colors.valid,
'Оперативник'
);
// Центрируем карту на первом маркере
if (map.hasLayer(markers.geo)) {
map.setView(markers.geo.getLatLng(), 10);
}
// Легенда
const legend = L.control({ position: 'bottomright' });
legend.onAdd = function() {
const div = L.DomUtil.create('div', 'info legend');
div.style.fontSize = '14px';
div.style.backgroundColor = 'white';
div.style.padding = '10px';
div.style.borderRadius = '4px';
div.style.boxShadow = '0 0 10px rgba(0,0,0,0.2)';
div.innerHTML = `
<h5>Легенда</h5>
<div><span style="color: blue; font-weight: bold;">•</span> Геолокация</div>
<div><span style="color: red; font-weight: bold;">•</span> Кубсат</div>
<div><span style="color: green; font-weight: bold;">•</span> Оперативники</div>
`;
return div;
};
legend.addTo(map);
});
</script>
{% extends 'mainapp/base.html' %}
{% load static %}
{% load static leaflet_tags %}
{% load l10n %}
{% block title %}Просмотр объекта: {{ object.name }}{% endblock %}
{% block extra_css %}
<style>
.form-section { margin-bottom: 2rem; border: 1px solid #dee2e6; border-radius: 0.25rem; padding: 1rem; }
.form-section-header { border-bottom: 1px solid #dee2e6; padding-bottom: 0.5rem; margin-bottom: 1rem; }
.btn-action { margin-right: 0.5rem; }
.readonly-field { background-color: #f8f9fa; padding: 0.375rem 0.75rem; border: 1px solid #ced4da; border-radius: 0.25rem; }
.coord-group { border: 1px solid #dee2e6; padding: 0.75rem; border-radius: 0.25rem; margin-bottom: 1rem; }
.coord-group-header { font-weight: bold; margin-bottom: 0.5rem; }
.form-check-input { margin-top: 0.25rem; }
.datetime-group { display: flex; gap: 1rem; }
.datetime-group > div { flex: 1; }
#map { height: 500px; width: 100%; margin-bottom: 1rem; }
.map-container { margin-bottom: 1rem; }
.coord-sync-group { border: 1px solid #dee2e6; padding: 0.75rem; border-radius: 0.25rem; }
.map-controls {
display: flex;
gap: 10px;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.map-control-btn {
padding: 0.375rem 0.75rem;
border: 1px solid #ced4da;
background-color: #f8f9fa;
border-radius: 0.25rem;
cursor: pointer;
}
.map-control-btn.active {
background-color: #e9ecef;
border-color: #dee2e6;
}
.map-control-btn.edit {
background-color: #fff3cd;
border-color: #ffeeba;
}
.map-control-btn.save {
background-color: #d1ecf1;
border-color: #bee5eb;
}
.map-control-btn.cancel {
background-color: #f8d7da;
border-color: #f5c6cb;
}
.leaflet-marker-icon {
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.3));
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid px-3">
<div class="row mb-3">
<div class="col-12 d-flex justify-content-between align-items-center">
<h2>Просмотр объекта: {{ object.name }}</h2>
<div>
<a href="{% url 'mainapp:objitem_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-secondary btn-action">Назад</a>
</div>
</div>
</div>
<!-- Основная информация -->
<div class="form-section">
<div class="form-section-header">
<h4>Основная информация</h4>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Название:</label>
<div class="readonly-field">{{ object.name|default:"-" }}</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Дата создания:</label>
<div class="readonly-field">
{% if object.created_at %}{{ object.created_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Создан пользователем:</label>
<div class="readonly-field">
{% if object.created_by %}{{ object.created_by }}{% else %}-{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Дата последнего изменения:</label>
<div class="readonly-field">
{% if object.updated_at %}{{ object.updated_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Изменен пользователем:</label>
<div class="readonly-field">
{% if object.updated_by %}{{ object.updated_by }}{% else %}-{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- ВЧ загрузка -->
<div class="form-section">
<div class="form-section-header">
<h4>ВЧ загрузка</h4>
</div>
{% if object.parameter_obj %}
<div class="row">
<div class="col-md-3">
<div class="mb-3">
<label class="form-label">Спутник:</label>
<div class="readonly-field">{{ object.parameter_obj.id_satellite.name|default:"-" }}</div>
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label class="form-label">Частота (МГц):</label>
<div class="readonly-field">{{ object.parameter_obj.frequency|default:"-" }}</div>
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label class="form-label">Полоса (МГц):</label>
<div class="readonly-field">{{ object.parameter_obj.freq_range|default:"-" }}</div>
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label class="form-label">Поляризация:</label>
<div class="readonly-field">{{ object.parameter_obj.polarization.name|default:"-" }}</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-3">
<div class="mb-3">
<label class="form-label">Символьная скорость:</label>
<div class="readonly-field">{{ object.parameter_obj.bod_velocity|default:"-" }}</div>
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label class="form-label">Модуляция:</label>
<div class="readonly-field">{{ object.parameter_obj.modulation.name|default:"-" }}</div>
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label class="form-label">ОСШ:</label>
<div class="readonly-field">{{ object.parameter_obj.snr|default:"-" }}</div>
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label class="form-label">Стандарт:</label>
<div class="readonly-field">{{ object.parameter_obj.standard.name|default:"-" }}</div>
</div>
</div>
</div>
{% else %}
<div class="mb-3">
<p>Нет данных о ВЧ загрузке</p>
</div>
{% endif %}
</div>
<!-- Блок с картой -->
<div class="form-section">
<div class="form-section-header">
<h4>Карта</h4>
</div>
<div class="map-container">
<div id="map"></div>
</div>
</div>
<!-- Геоданные -->
<div class="form-section">
<div class="form-section-header">
<h4>Геоданные</h4>
</div>
{% if object.geo_obj %}
<!-- Координаты геолокации -->
<div class="coord-sync-group">
<div class="coord-group-header">Координаты геолокации</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Широта:</label>
<div class="readonly-field">
{% if object.geo_obj.coords %}{{ object.geo_obj.coords.y|floatformat:6 }}{% else %}-{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<label class="form-label">Долгота:</label>
<div class="readonly-field">
{% if object.geo_obj.coords %}{{ object.geo_obj.coords.x|floatformat:6 }}{% else %}-{% endif %}
</div>
</div>
</div>
</div>
<!-- Координаты Кубсата -->
<div class="coord-group">
<div class="coord-group-header">Координаты Кубсата</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Долгота:</label>
<div class="readonly-field">
{% if object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.x|floatformat:6 }}{% else %}-{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Широта:</label>
<div class="readonly-field">
{% if object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.y|floatformat:6 }}{% else %}-{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Координаты оперативников -->
<div class="coord-group">
<div class="coord-group-header">Координаты оперативников</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Долгота:</label>
<div class="readonly-field">
{% if object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.x|floatformat:6 }}{% else %}-{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Широта:</label>
<div class="readonly-field">
{% if object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.y|floatformat:6 }}{% else %}-{% endif %}
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Местоположение:</label>
<div class="readonly-field">{{ object.geo_obj.location|default:"-" }}</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Комментарий:</label>
<div class="readonly-field">{{ object.geo_obj.comment|default:"-" }}</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Дата и время:</label>
<div class="readonly-field">
{% if object.geo_obj.timestamp %}{{ object.geo_obj.timestamp|date:"d.m.Y H:i" }}{% else %}-{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-check-label">Усредненное значение:</label>
<div class="readonly-field">
{% if object.geo_obj.is_average %}Да{% else %}Нет{% endif %}
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-md-4">
<div class="mb-3">
<label class="form-label">Расстояние гео-кубсат, км:</label>
<div class="readonly-field">
{% if object.geo_obj.distance_coords_kup is not None %}
{{ object.geo_obj.distance_coords_kup|floatformat:2 }}
{% else %}
-
{% endif %}
</div>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label class="form-label">Расстояние гео-опер, км:</label>
<div class="readonly-field">
{% if object.geo_obj.distance_coords_valid is not None %}
{{ object.geo_obj.distance_coords_valid|floatformat:2 }}
{% else %}
-
{% endif %}
</div>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label class="form-label">Расстояние кубсат-опер, км:</label>
<div class="readonly-field">
{% if object.geo_obj.distance_kup_valid is not None %}
{{ object.geo_obj.distance_kup_valid|floatformat:2 }}
{% else %}
-
{% endif %}
</div>
</div>
</div>
</div>
{% else %}
<p>Нет данных о геолокации</p>
{% endif %}
</div>
</div>
<div class="d-flex justify-content-end mt-4">
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
<a href="{% url 'mainapp:objitem_update' object.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-primary btn-action">Редактировать</a>
{% endif %}
<a href="{% url 'mainapp:objitem_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-secondary btn-action">Назад</a>
</div>
</div>
{% endblock %}
{% block extra_js %}
{{ block.super }}
<!-- Подключаем Leaflet и его плагины -->
{% leaflet_js %}
{% leaflet_css %}
<script src="{% static 'leaflet-markers/js/leaflet-color-markers.js' %}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Инициализация карты
const map = L.map('map').setView([55.75, 37.62], 5);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
// Определяем цвета для маркеров
const colors = {
geo: 'blue',
kupsat: 'red',
valid: 'green'
};
// Функция для создания иконки маркера
function createMarkerIcon(color) {
return L.icon({
iconUrl: '{% static "leaflet-markers/img/marker-icon-" %}' + color + '.png',
shadowUrl: `{% static 'leaflet-markers/img/marker-shadow.png' %}`,
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
});
}
// Маркеры
const markers = {};
function createMarker(position, color, name) {
const marker = L.marker(position, {
draggable: false,
icon: createMarkerIcon(color),
title: name
}).addTo(map);
marker.bindPopup(name);
return marker;
}
// Получаем координаты из данных объекта
{% if object.geo_obj and object.geo_obj.coords %}
const geoLat = {{ object.geo_obj.coords.y|unlocalize }};
const geoLng = {{ object.geo_obj.coords.x|unlocalize }};
{% else %}
const geoLat = 55.75;
const geoLng = 37.62;
{% endif %}
{% if object.geo_obj and object.geo_obj.coords_kupsat %}
const kupsatLat = {{ object.geo_obj.coords_kupsat.y|unlocalize }};
const kupsatLng = {{ object.geo_obj.coords_kupsat.x|unlocalize }};
{% else %}
const kupsatLat = 55.75;
const kupsatLng = 37.61;
{% endif %}
{% if object.geo_obj and object.geo_obj.coords_valid %}
const validLat = {{ object.geo_obj.coords_valid.y|unlocalize }};
const validLng = {{ object.geo_obj.coords_valid.x|unlocalize }};
{% else %}
const validLat = 55.75;
const validLng = 37.63;
{% endif %}
// Создаем маркеры
markers.geo = createMarker(
[geoLat, geoLng],
colors.geo,
'Геолокация'
);
markers.kupsat = createMarker(
[kupsatLat, kupsatLng],
colors.kupsat,
'Кубсат'
);
markers.valid = createMarker(
[validLat, validLng],
colors.valid,
'Оперативник'
);
// Центрируем карту на первом маркере
if (map.hasLayer(markers.geo)) {
map.setView(markers.geo.getLatLng(), 10);
}
// Легенда
const legend = L.control({ position: 'bottomright' });
legend.onAdd = function() {
const div = L.DomUtil.create('div', 'info legend');
div.style.fontSize = '14px';
div.style.backgroundColor = 'white';
div.style.padding = '10px';
div.style.borderRadius = '4px';
div.style.boxShadow = '0 0 10px rgba(0,0,0,0.2)';
div.innerHTML = `
<h5>Легенда</h5>
<div><span style="color: blue; font-weight: bold;">•</span> Геолокация</div>
<div><span style="color: red; font-weight: bold;">•</span> Кубсат</div>
<div><span style="color: green; font-weight: bold;">•</span> Оперативники</div>
`;
return div;
};
legend.addTo(map);
});
</script>
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@@ -1,52 +1,52 @@
{% extends 'mainapp/base.html' %}
{% block title %}Новое событие{% endblock %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-sm border-0">
<div class="card-header bg-success text-white">
<h2 class="mb-0">Формирование таблицы Кубсат</h2>
</div>
<div class="card-body">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% comment%}
<div class="mb-4">
<label for="{{ form.sat_choice.id_for_label }}" class="form-label">{{ form.sat_choice.label }}</label>
{{ form.sat_choice }}
{% if form.sat_choice.errors %}
<div class="text-danger mt-1">{{ form.sat_choice.errors }}</div>
{% endif %}
</div>{% endcomment %}
{% comment %} <div class="mb-4">
<label for="{{ form.pol_choice.id_for_label }}" class="form-label">{{ form.pol_choice.label }}</label>
{{ form.pol_choice }}
{% if form.pol_choice.errors %}
<div class="text-danger mt-1">{{ form.pol_choice.errors }}</div>
{% endif %}
</div> {% endcomment %}
<div class="mb-4">
<label for="{{ form.file.id_for_label }}" class="form-label">{{ form.file.label }}</label>
{{ form.file }}
{% if form.file.errors %}
<div class="text-danger mt-1">{{ form.file.errors }}</div>
{% endif %}
<div class="form-text">Выберите файл для загрузки</div>
</div>
<div class="d-flex justify-content-between">
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary">Назад</a>
<button type="submit" class="btn btn-success">Выполнить</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% extends 'mainapp/base.html' %}
{% block title %}Новое событие{% endblock %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-sm border-0">
<div class="card-header bg-success text-white">
<h2 class="mb-0">Формирование таблицы Кубсат</h2>
</div>
<div class="card-body">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% comment%}
<div class="mb-4">
<label for="{{ form.sat_choice.id_for_label }}" class="form-label">{{ form.sat_choice.label }}</label>
{{ form.sat_choice }}
{% if form.sat_choice.errors %}
<div class="text-danger mt-1">{{ form.sat_choice.errors }}</div>
{% endif %}
</div>{% endcomment %}
{% comment %} <div class="mb-4">
<label for="{{ form.pol_choice.id_for_label }}" class="form-label">{{ form.pol_choice.label }}</label>
{{ form.pol_choice }}
{% if form.pol_choice.errors %}
<div class="text-danger mt-1">{{ form.pol_choice.errors }}</div>
{% endif %}
</div> {% endcomment %}
<div class="mb-4">
<label for="{{ form.file.id_for_label }}" class="form-label">{{ form.file.label }}</label>
{{ form.file }}
{% if form.file.errors %}
<div class="text-danger mt-1">{{ form.file.errors }}</div>
{% endif %}
<div class="form-text">Выберите файл для загрузки</div>
</div>
<div class="d-flex justify-content-between">
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary">Назад</a>
<button type="submit" class="btn btn-success">Выполнить</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,53 +1,53 @@
{% extends 'mainapp/base.html' %}
{% block title %}Загрузка данных транспондеров{% endblock %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-warning text-white">
<h2 class="mb-0">Загрузка данных транспондеров из CellNet</h2>
</div>
<div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert {{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
<p class="card-text">Загрузите xml-файл и выберите спутник для загрузки данных в базу.</p>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="mb-3">
<label for="{{ form.file.id_for_label }}" class="form-label">Выберите xml файл:</label>
{{ form.file }}
{% if form.file.errors %}
<div class="text-danger mt-1">{{ form.file.errors }}</div>
{% endif %}
<div class="form-text">Загрузите xml-файл (.xml) с данными для обработки</div>
</div>
{% comment %} <div class="mb-3">
<label for="{{ form.sat_choice.id_for_label }}" class="form-label">Выберите спутник:</label>
{{ form.sat_choice }}
{% if form.sat_choice.errors %}
<div class="text-danger mt-1">{{ form.sat_choice.errors }}</div>
{% endif %}
</div> {% endcomment %}
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary me-md-2">Назад</a>
<button type="submit" class="btn btn-warning">Добавить в базу</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% extends 'mainapp/base.html' %}
{% block title %}Загрузка данных транспондеров{% endblock %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-warning text-white">
<h2 class="mb-0">Загрузка данных транспондеров из CellNet</h2>
</div>
<div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert {{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
<p class="card-text">Загрузите xml-файл и выберите спутник для загрузки данных в базу.</p>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="mb-3">
<label for="{{ form.file.id_for_label }}" class="form-label">Выберите xml файл:</label>
{{ form.file }}
{% if form.file.errors %}
<div class="text-danger mt-1">{{ form.file.errors }}</div>
{% endif %}
<div class="form-text">Загрузите xml-файл (.xml) с данными для обработки</div>
</div>
{% comment %} <div class="mb-3">
<label for="{{ form.sat_choice.id_for_label }}" class="form-label">Выберите спутник:</label>
{{ form.sat_choice }}
{% if form.sat_choice.errors %}
<div class="text-danger mt-1">{{ form.sat_choice.errors }}</div>
{% endif %}
</div> {% endcomment %}
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary me-md-2">Назад</a>
<button type="submit" class="btn btn-warning">Добавить в базу</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,56 +1,56 @@
{% extends 'mainapp/base.html' %}
{% block title %}Загрузка данных ВЧ загрузки{% endblock %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-danger text-white">
<h2 class="mb-0">Загрузка данных ВЧ загрузки</h2>
</div>
<div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert {{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
<p class="card-text">Загрузите HTML-файл с таблицами данных ВЧ загрузки и выберите спутник для привязки данных.</p>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<!-- Form fields with Bootstrap styling -->
<div class="mb-3">
<label for="{{ form.file.id_for_label }}" class="form-label">Выберите HTML файл:</label>
{{ form.file }}
{% if form.file.errors %}
<div class="text-danger mt-1">{{ form.file.errors }}</div>
{% endif %}
<div class="form-text">Загрузите HTML-файл, содержащий таблицы с данными ВЧ загрузки</div>
</div>
<div class="mb-3">
<label for="{{ form.sat_choice.id_for_label }}" class="form-label">Выберите спутник:</label>
{{ form.sat_choice }}
{% if form.sat_choice.errors %}
<div class="text-danger mt-1">{{ form.sat_choice.errors }}</div>
{% endif %}
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary me-md-2">Назад</a>
<button type="submit" class="btn btn-danger">Обработать файл</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% extends 'mainapp/base.html' %}
{% block title %}Загрузка данных ВЧ загрузки{% endblock %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-danger text-white">
<h2 class="mb-0">Загрузка данных ВЧ загрузки</h2>
</div>
<div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert {{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
<p class="card-text">Загрузите HTML-файл с таблицами данных ВЧ загрузки и выберите спутник для привязки данных.</p>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<!-- Form fields with Bootstrap styling -->
<div class="mb-3">
<label for="{{ form.file.id_for_label }}" class="form-label">Выберите HTML файл:</label>
{{ form.file }}
{% if form.file.errors %}
<div class="text-danger mt-1">{{ form.file.errors }}</div>
{% endif %}
<div class="form-text">Загрузите HTML-файл, содержащий таблицы с данными ВЧ загрузки</div>
</div>
<div class="mb-3">
<label for="{{ form.sat_choice.id_for_label }}" class="form-label">Выберите спутник:</label>
{{ form.sat_choice }}
{% if form.sat_choice.errors %}
<div class="text-danger mt-1">{{ form.sat_choice.errors }}</div>
{% endif %}
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="{% url 'mainapp:home' %}" class="btn btn-secondary me-md-2">Назад</a>
<button type="submit" class="btn btn-danger">Обработать файл</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,3 +1,3 @@
"""
Template tags для mainapp.
"""
"""
Template tags для mainapp.
"""

View File

@@ -1,133 +1,133 @@
"""
Пользовательские фильтры шаблонов для форматирования координат.
Этот модуль содержит фильтры Django для форматирования географических координат
в читаемый вид в шаблонах.
"""
# Standard library imports
from typing import Optional
# Django imports
from django import template
from django.contrib.gis.geos import Point
register = template.Library()
@register.filter(name='format_coords')
def format_coords(point: Optional[Point]) -> str:
"""
Форматирует объект Point в читаемую строку координат.
Args:
point (Point): Объект Point из GeoDjango или None.
Returns:
str: Отформатированная строка координат в формате "XXN/S YYE/W"
или "-" если point равен None.
Example:
В шаблоне:
{{ geo_obj.coords|format_coords }}
Результат:
"55.75N 37.62E"
"""
if not point:
return "-"
try:
longitude = point.coords[0]
latitude = point.coords[1]
lon_direction = "E" if longitude > 0 else "W"
lat_direction = "N" if latitude > 0 else "S"
lon_value = abs(longitude)
lat_value = abs(latitude)
return f"{lat_value}{lat_direction} {lon_value}{lon_direction}"
except (AttributeError, IndexError, TypeError):
return "-"
@register.filter(name='format_coords_decimal')
def format_coords_decimal(point: Optional[Point], precision: int = 6) -> str:
"""
Форматирует объект Point в десятичные координаты с заданной точностью.
Args:
point (Point): Объект Point из GeoDjango или None.
precision (int): Количество знаков после запятой (по умолчанию 6).
Returns:
str: Отформатированная строка координат в формате "lat, lon"
или "-" если point равен None.
Example:
В шаблоне:
{{ geo_obj.coords|format_coords_decimal:4 }}
Результат:
"55.7500, 37.6200"
"""
if not point:
return "-"
try:
longitude = point.coords[0]
latitude = point.coords[1]
format_str = f"{{:.{precision}f}}, {{:.{precision}f}}"
return format_str.format(latitude, longitude)
except (AttributeError, IndexError, TypeError, ValueError):
return "-"
@register.filter(name='coords_to_lat')
def coords_to_lat(point: Optional[Point]) -> Optional[float]:
"""
Извлекает широту из объекта Point.
Args:
point (Point): Объект Point из GeoDjango или None.
Returns:
float: Значение широты или None если point равен None.
Example:
В шаблоне:
{{ geo_obj.coords|coords_to_lat }}
"""
if not point:
return None
try:
return point.coords[1]
except (AttributeError, IndexError, TypeError):
return None
@register.filter(name='coords_to_lon')
def coords_to_lon(point: Optional[Point]) -> Optional[float]:
"""
Извлекает долготу из объекта Point.
Args:
point (Point): Объект Point из GeoDjango или None.
Returns:
float: Значение долготы или None если point равен None.
Example:
В шаблоне:
{{ geo_obj.coords|coords_to_lon }}
"""
if not point:
return None
try:
return point.coords[0]
except (AttributeError, IndexError, TypeError):
return None
"""
Пользовательские фильтры шаблонов для форматирования координат.
Этот модуль содержит фильтры Django для форматирования географических координат
в читаемый вид в шаблонах.
"""
# Standard library imports
from typing import Optional
# Django imports
from django import template
from django.contrib.gis.geos import Point
register = template.Library()
@register.filter(name='format_coords')
def format_coords(point: Optional[Point]) -> str:
"""
Форматирует объект Point в читаемую строку координат.
Args:
point (Point): Объект Point из GeoDjango или None.
Returns:
str: Отформатированная строка координат в формате "XXN/S YYE/W"
или "-" если point равен None.
Example:
В шаблоне:
{{ geo_obj.coords|format_coords }}
Результат:
"55.75N 37.62E"
"""
if not point:
return "-"
try:
longitude = point.coords[0]
latitude = point.coords[1]
lon_direction = "E" if longitude > 0 else "W"
lat_direction = "N" if latitude > 0 else "S"
lon_value = abs(longitude)
lat_value = abs(latitude)
return f"{lat_value}{lat_direction} {lon_value}{lon_direction}"
except (AttributeError, IndexError, TypeError):
return "-"
@register.filter(name='format_coords_decimal')
def format_coords_decimal(point: Optional[Point], precision: int = 6) -> str:
"""
Форматирует объект Point в десятичные координаты с заданной точностью.
Args:
point (Point): Объект Point из GeoDjango или None.
precision (int): Количество знаков после запятой (по умолчанию 6).
Returns:
str: Отформатированная строка координат в формате "lat, lon"
или "-" если point равен None.
Example:
В шаблоне:
{{ geo_obj.coords|format_coords_decimal:4 }}
Результат:
"55.7500, 37.6200"
"""
if not point:
return "-"
try:
longitude = point.coords[0]
latitude = point.coords[1]
format_str = f"{{:.{precision}f}}, {{:.{precision}f}}"
return format_str.format(latitude, longitude)
except (AttributeError, IndexError, TypeError, ValueError):
return "-"
@register.filter(name='coords_to_lat')
def coords_to_lat(point: Optional[Point]) -> Optional[float]:
"""
Извлекает широту из объекта Point.
Args:
point (Point): Объект Point из GeoDjango или None.
Returns:
float: Значение широты или None если point равен None.
Example:
В шаблоне:
{{ geo_obj.coords|coords_to_lat }}
"""
if not point:
return None
try:
return point.coords[1]
except (AttributeError, IndexError, TypeError):
return None
@register.filter(name='coords_to_lon')
def coords_to_lon(point: Optional[Point]) -> Optional[float]:
"""
Извлекает долготу из объекта Point.
Args:
point (Point): Объект Point из GeoDjango или None.
Returns:
float: Значение долготы или None если point равен None.
Example:
В шаблоне:
{{ geo_obj.coords|coords_to_lon }}
"""
if not point:
return None
try:
return point.coords[0]
except (AttributeError, IndexError, TypeError):
return None

View File

@@ -1,179 +1,179 @@
from django.test import TestCase, RequestFactory
from django.contrib.auth.models import User
from django.contrib.gis.geos import Point
from .models import CustomUser, Geo, ObjItem
from .utils import format_coordinates, parse_pagination_params
from .mixins import RoleRequiredMixin, CoordinateProcessingMixin
from django.views import View
class FormatCoordinatesTestCase(TestCase):
"""Тесты для функции format_coordinates"""
def test_format_positive_coordinates(self):
"""Тест форматирования положительных координат"""
result = format_coordinates(37.62, 55.75)
self.assertEqual(result, "55.75N 37.62E")
def test_format_negative_longitude(self):
"""Тест форматирования с отрицательной долготой"""
result = format_coordinates(-122.42, 37.77)
self.assertEqual(result, "37.77N 122.42W")
def test_format_negative_latitude(self):
"""Тест форматирования с отрицательной широтой"""
result = format_coordinates(151.21, -33.87)
self.assertEqual(result, "33.87S 151.21E")
def test_format_both_negative(self):
"""Тест форматирования с обеими отрицательными координатами"""
result = format_coordinates(-58.38, -34.60)
self.assertEqual(result, "34.6S 58.38W")
class ParsePaginationParamsTestCase(TestCase):
"""Тесты для функции parse_pagination_params"""
def setUp(self):
self.factory = RequestFactory()
def test_default_values(self):
"""Тест значений по умолчанию"""
request = self.factory.get("/")
page, per_page = parse_pagination_params(request)
self.assertEqual(page, 1)
self.assertEqual(per_page, 50)
def test_custom_values(self):
"""Тест пользовательских значений"""
request = self.factory.get("/?page=3&items_per_page=100")
page, per_page = parse_pagination_params(request)
self.assertEqual(page, 3)
self.assertEqual(per_page, 100)
def test_invalid_page_number(self):
"""Тест невалидного номера страницы"""
request = self.factory.get("/?page=invalid")
page, per_page = parse_pagination_params(request)
self.assertEqual(page, 1)
def test_negative_page_number(self):
"""Тест отрицательного номера страницы"""
request = self.factory.get("/?page=-5")
page, per_page = parse_pagination_params(request)
self.assertEqual(page, 1)
def test_max_items_per_page_limit(self):
"""Тест ограничения максимального количества элементов"""
request = self.factory.get("/?items_per_page=20000")
page, per_page = parse_pagination_params(request)
self.assertEqual(per_page, 10000)
class RoleRequiredMixinTestCase(TestCase):
"""Тесты для RoleRequiredMixin"""
def setUp(self):
self.factory = RequestFactory()
def test_admin_has_access(self):
"""Тест что администратор имеет доступ"""
user = User.objects.create_user(username="testuser", password="12345")
# Get the automatically created CustomUser and set role to 'admin'
custom_user = CustomUser.objects.get(user=user)
custom_user.role = "admin"
custom_user.save()
# Refresh user to get updated customuser
user.refresh_from_db()
class TestView(RoleRequiredMixin, View):
required_roles = ["admin", "moderator"]
view = TestView()
request = self.factory.get("/")
request.user = user
view.request = request
self.assertTrue(view.test_func())
def test_user_without_role_denied(self):
"""Тест что пользователь без роли не имеет доступа"""
user_no_role = User.objects.create_user(username="norole", password="12345")
# Get the automatically created CustomUser - default role is 'user'
custom_user_no_role = CustomUser.objects.get(user=user_no_role)
self.assertEqual(custom_user_no_role.role, "user")
class TestView(RoleRequiredMixin, View):
required_roles = ["admin", "moderator"]
view = TestView()
request = self.factory.get("/")
request.user = user_no_role
view.request = request
self.assertFalse(view.test_func())
class CoordinateProcessingMixinTestCase(TestCase):
"""Тесты для CoordinateProcessingMixin"""
def setUp(self):
self.factory = RequestFactory()
def test_extract_geo_coordinates(self):
"""Тест извлечения координат геолокации"""
class TestView(CoordinateProcessingMixin, View):
pass
view = TestView()
request = self.factory.post(
"/", {"geo_longitude": "37.62", "geo_latitude": "55.75"}
)
view.request = request
coords = view._extract_coordinates("geo")
self.assertIsNotNone(coords)
self.assertEqual(coords, (37.62, 55.75))
def test_extract_invalid_coordinates(self):
"""Тест извлечения невалидных координат"""
class TestView(CoordinateProcessingMixin, View):
pass
view = TestView()
request = self.factory.post(
"/", {"geo_longitude": "invalid", "geo_latitude": "55.75"}
)
view.request = request
coords = view._extract_coordinates("geo")
self.assertIsNone(coords)
def test_process_coordinates(self):
"""Тест обработки координат и применения к объекту Geo"""
class TestView(CoordinateProcessingMixin, View):
pass
view = TestView()
request = self.factory.post(
"/",
{
"geo_longitude": "37.62",
"geo_latitude": "55.75",
"kupsat_longitude": "37.63",
"kupsat_latitude": "55.76",
},
)
view.request = request
geo_instance = Geo()
view.process_coordinates(geo_instance)
self.assertIsNotNone(geo_instance.coords)
self.assertEqual(geo_instance.coords.coords, (37.62, 55.75))
self.assertIsNotNone(geo_instance.coords_kupsat)
self.assertEqual(geo_instance.coords_kupsat.coords, (37.63, 55.76))
from django.test import TestCase, RequestFactory
from django.contrib.auth.models import User
from django.contrib.gis.geos import Point
from .models import CustomUser, Geo, ObjItem
from .utils import format_coordinates, parse_pagination_params
from .mixins import RoleRequiredMixin, CoordinateProcessingMixin
from django.views import View
class FormatCoordinatesTestCase(TestCase):
"""Тесты для функции format_coordinates"""
def test_format_positive_coordinates(self):
"""Тест форматирования положительных координат"""
result = format_coordinates(37.62, 55.75)
self.assertEqual(result, "55.75N 37.62E")
def test_format_negative_longitude(self):
"""Тест форматирования с отрицательной долготой"""
result = format_coordinates(-122.42, 37.77)
self.assertEqual(result, "37.77N 122.42W")
def test_format_negative_latitude(self):
"""Тест форматирования с отрицательной широтой"""
result = format_coordinates(151.21, -33.87)
self.assertEqual(result, "33.87S 151.21E")
def test_format_both_negative(self):
"""Тест форматирования с обеими отрицательными координатами"""
result = format_coordinates(-58.38, -34.60)
self.assertEqual(result, "34.6S 58.38W")
class ParsePaginationParamsTestCase(TestCase):
"""Тесты для функции parse_pagination_params"""
def setUp(self):
self.factory = RequestFactory()
def test_default_values(self):
"""Тест значений по умолчанию"""
request = self.factory.get("/")
page, per_page = parse_pagination_params(request)
self.assertEqual(page, 1)
self.assertEqual(per_page, 50)
def test_custom_values(self):
"""Тест пользовательских значений"""
request = self.factory.get("/?page=3&items_per_page=100")
page, per_page = parse_pagination_params(request)
self.assertEqual(page, 3)
self.assertEqual(per_page, 100)
def test_invalid_page_number(self):
"""Тест невалидного номера страницы"""
request = self.factory.get("/?page=invalid")
page, per_page = parse_pagination_params(request)
self.assertEqual(page, 1)
def test_negative_page_number(self):
"""Тест отрицательного номера страницы"""
request = self.factory.get("/?page=-5")
page, per_page = parse_pagination_params(request)
self.assertEqual(page, 1)
def test_max_items_per_page_limit(self):
"""Тест ограничения максимального количества элементов"""
request = self.factory.get("/?items_per_page=20000")
page, per_page = parse_pagination_params(request)
self.assertEqual(per_page, 10000)
class RoleRequiredMixinTestCase(TestCase):
"""Тесты для RoleRequiredMixin"""
def setUp(self):
self.factory = RequestFactory()
def test_admin_has_access(self):
"""Тест что администратор имеет доступ"""
user = User.objects.create_user(username="testuser", password="12345")
# Get the automatically created CustomUser and set role to 'admin'
custom_user = CustomUser.objects.get(user=user)
custom_user.role = "admin"
custom_user.save()
# Refresh user to get updated customuser
user.refresh_from_db()
class TestView(RoleRequiredMixin, View):
required_roles = ["admin", "moderator"]
view = TestView()
request = self.factory.get("/")
request.user = user
view.request = request
self.assertTrue(view.test_func())
def test_user_without_role_denied(self):
"""Тест что пользователь без роли не имеет доступа"""
user_no_role = User.objects.create_user(username="norole", password="12345")
# Get the automatically created CustomUser - default role is 'user'
custom_user_no_role = CustomUser.objects.get(user=user_no_role)
self.assertEqual(custom_user_no_role.role, "user")
class TestView(RoleRequiredMixin, View):
required_roles = ["admin", "moderator"]
view = TestView()
request = self.factory.get("/")
request.user = user_no_role
view.request = request
self.assertFalse(view.test_func())
class CoordinateProcessingMixinTestCase(TestCase):
"""Тесты для CoordinateProcessingMixin"""
def setUp(self):
self.factory = RequestFactory()
def test_extract_geo_coordinates(self):
"""Тест извлечения координат геолокации"""
class TestView(CoordinateProcessingMixin, View):
pass
view = TestView()
request = self.factory.post(
"/", {"geo_longitude": "37.62", "geo_latitude": "55.75"}
)
view.request = request
coords = view._extract_coordinates("geo")
self.assertIsNotNone(coords)
self.assertEqual(coords, (37.62, 55.75))
def test_extract_invalid_coordinates(self):
"""Тест извлечения невалидных координат"""
class TestView(CoordinateProcessingMixin, View):
pass
view = TestView()
request = self.factory.post(
"/", {"geo_longitude": "invalid", "geo_latitude": "55.75"}
)
view.request = request
coords = view._extract_coordinates("geo")
self.assertIsNone(coords)
def test_process_coordinates(self):
"""Тест обработки координат и применения к объекту Geo"""
class TestView(CoordinateProcessingMixin, View):
pass
view = TestView()
request = self.factory.post(
"/",
{
"geo_longitude": "37.62",
"geo_latitude": "55.75",
"kupsat_longitude": "37.63",
"kupsat_latitude": "55.76",
},
)
view.request = request
geo_instance = Geo()
view.process_coordinates(geo_instance)
self.assertIsNotNone(geo_instance.coords)
self.assertEqual(geo_instance.coords.coords, (37.62, 55.75))
self.assertIsNotNone(geo_instance.coords_kupsat)
self.assertEqual(geo_instance.coords_kupsat.coords, (37.63, 55.76))

View File

@@ -1,32 +1,32 @@
from django.conf import settings
from django.conf.urls.static import static
from django.urls import path
from . import views
app_name = 'mainapp'
urlpatterns = [
path('', views.HomePageView.as_view(), name='home'), # Home page that redirects based on auth
path('objitems/', views.ObjItemListView.as_view(), name='objitem_list'), # Objects list page
path('actions/', views.ActionsPageView.as_view(), name='actions'), # Move actions to a separate page
path('excel-data', views.LoadExcelDataView.as_view(), name='load_excel_data'),
path('satellites', views.AddSatellitesView.as_view(), name='add_sats'),
path('api/locations/<int:sat_id>/geojson/', views.GetLocationsView.as_view(), name='locations_by_id'),
path('transponders', views.AddTranspondersView.as_view(), name='add_trans'),
path('csv-data', views.LoadCsvDataView.as_view(), name='load_csv_data'),
path('map-points/', views.ShowMapView.as_view(), name='admin_show_map'),
path('show-selected-objects-map/', views.ShowSelectedObjectsMapView.as_view(), name='show_selected_objects_map'),
path('delete-selected-objects/', views.DeleteSelectedObjectsView.as_view(), name='delete_selected_objects'),
path('cluster/', views.ClusterTestView.as_view(), name='cluster'),
path('vch-upload/', views.UploadVchLoadView.as_view(), name='vch_load'),
path('vch-link/', views.LinkVchSigmaView.as_view(), name='link_vch_sigma'),
path('kubsat-excel/', views.ProcessKubsatView.as_view(), name='kubsat_excel'),
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>/', views.ObjItemDetailView.as_view(), name='objitem_detail'),
path('object/<int:pk>/delete/', views.ObjItemDeleteView.as_view(), name='objitem_delete'),
path('fill-lyngsat-data/', views.FillLyngsatDataView.as_view(), name='fill_lyngsat_data'),
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('api/lyngsat-task-status/<str:task_id>/', views.LyngsatTaskStatusAPIView.as_view(), name='lyngsat_task_status_api'),
from django.conf import settings
from django.conf.urls.static import static
from django.urls import path
from . import views
app_name = 'mainapp'
urlpatterns = [
path('', views.HomePageView.as_view(), name='home'), # Home page that redirects based on auth
path('objitems/', views.ObjItemListView.as_view(), name='objitem_list'), # Objects list page
path('actions/', views.ActionsPageView.as_view(), name='actions'), # Move actions to a separate page
path('excel-data', views.LoadExcelDataView.as_view(), name='load_excel_data'),
path('satellites', views.AddSatellitesView.as_view(), name='add_sats'),
path('api/locations/<int:sat_id>/geojson/', views.GetLocationsView.as_view(), name='locations_by_id'),
path('transponders', views.AddTranspondersView.as_view(), name='add_trans'),
path('csv-data', views.LoadCsvDataView.as_view(), name='load_csv_data'),
path('map-points/', views.ShowMapView.as_view(), name='admin_show_map'),
path('show-selected-objects-map/', views.ShowSelectedObjectsMapView.as_view(), name='show_selected_objects_map'),
path('delete-selected-objects/', views.DeleteSelectedObjectsView.as_view(), name='delete_selected_objects'),
path('cluster/', views.ClusterTestView.as_view(), name='cluster'),
path('vch-upload/', views.UploadVchLoadView.as_view(), name='vch_load'),
path('vch-link/', views.LinkVchSigmaView.as_view(), name='link_vch_sigma'),
path('kubsat-excel/', views.ProcessKubsatView.as_view(), name='kubsat_excel'),
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>/', views.ObjItemDetailView.as_view(), name='objitem_detail'),
path('object/<int:pk>/delete/', views.ObjItemDeleteView.as_view(), name='objitem_delete'),
path('fill-lyngsat-data/', views.FillLyngsatDataView.as_view(), name='fill_lyngsat_data'),
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('api/lyngsat-task-status/<str:task_id>/', views.LyngsatTaskStatusAPIView.as_view(), name='lyngsat_task_status_api'),
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dbapp.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dbapp.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

View File

@@ -1,67 +1,67 @@
# Django imports
from django.contrib import admin
# Third-party imports
from import_export.admin import ImportExportActionModelAdmin
from more_admin_filters import MultiSelectRelatedDropdownFilter
from rangefilter.filters import NumericRangeFilterBuilder
# Local imports
from .models import Transponders
# ============================================================================
# Base Admin Classes
# ============================================================================
class BaseAdmin(admin.ModelAdmin):
"""
Базовый класс для всех admin моделей mapsapp.
Предоставляет общую функциональность:
- Кнопки сохранения сверху и снизу
- Настройка количества элементов на странице
"""
save_on_top = True
list_per_page = 50
# ============================================================================
# Admin Classes
# ============================================================================
@admin.register(Transponders)
class TranspondersAdmin(ImportExportActionModelAdmin, BaseAdmin):
"""
Админ-панель для модели Transponders.
Оптимизирована для работы с транспондерами:
- Использует select_related для оптимизации запросов
- Предоставляет фильтры по спутникам, поляризации и зоне
- Поддерживает импорт/экспорт данных
"""
list_display = (
"sat_id",
"name",
"zone_name",
"downlink",
"uplink",
"frequency_range",
"transfer",
"polarization",
)
list_display_links = ("name",)
list_select_related = ("polarization", "sat_id")
list_filter = (
("polarization", MultiSelectRelatedDropdownFilter),
("sat_id", MultiSelectRelatedDropdownFilter),
("downlink", NumericRangeFilterBuilder()),
("uplink", NumericRangeFilterBuilder()),
("frequency_range", NumericRangeFilterBuilder()),
"zone_name",
)
search_fields = ("name", "sat_id__name", "zone_name")
ordering = ("name",)
autocomplete_fields = ("sat_id", "polarization")
# Django imports
from django.contrib import admin
# Third-party imports
from import_export.admin import ImportExportActionModelAdmin
from more_admin_filters import MultiSelectRelatedDropdownFilter
from rangefilter.filters import NumericRangeFilterBuilder
# Local imports
from .models import Transponders
# ============================================================================
# Base Admin Classes
# ============================================================================
class BaseAdmin(admin.ModelAdmin):
"""
Базовый класс для всех admin моделей mapsapp.
Предоставляет общую функциональность:
- Кнопки сохранения сверху и снизу
- Настройка количества элементов на странице
"""
save_on_top = True
list_per_page = 50
# ============================================================================
# Admin Classes
# ============================================================================
@admin.register(Transponders)
class TranspondersAdmin(ImportExportActionModelAdmin, BaseAdmin):
"""
Админ-панель для модели Transponders.
Оптимизирована для работы с транспондерами:
- Использует select_related для оптимизации запросов
- Предоставляет фильтры по спутникам, поляризации и зоне
- Поддерживает импорт/экспорт данных
"""
list_display = (
"sat_id",
"name",
"zone_name",
"downlink",
"uplink",
"frequency_range",
"transfer",
"polarization",
)
list_display_links = ("name",)
list_select_related = ("polarization", "sat_id")
list_filter = (
("polarization", MultiSelectRelatedDropdownFilter),
("sat_id", MultiSelectRelatedDropdownFilter),
("downlink", NumericRangeFilterBuilder()),
("uplink", NumericRangeFilterBuilder()),
("frequency_range", NumericRangeFilterBuilder()),
"zone_name",
)
search_fields = ("name", "sat_id__name", "zone_name")
ordering = ("name",)
autocomplete_fields = ("sat_id", "polarization")

View File

@@ -1,6 +1,6 @@
from django.apps import AppConfig
class MapsappConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'mapsapp'
from django.apps import AppConfig
class MapsappConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'mapsapp'

View File

@@ -1,37 +1,37 @@
# Generated by Django 5.2.7 on 2025-10-31 13:36
import django.db.models.deletion
import django.db.models.expressions
import django.db.models.functions.math
import mainapp.models
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('mainapp', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Transponders',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(blank=True, max_length=30, null=True, verbose_name='Название транспондера')),
('downlink', models.FloatField(blank=True, null=True, verbose_name='Downlink')),
('frequency_range', models.FloatField(blank=True, null=True, verbose_name='Полоса')),
('uplink', models.FloatField(blank=True, null=True, verbose_name='Uplink')),
('zone_name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Название зоны')),
('transfer', models.GeneratedField(db_persist=True, expression=models.ExpressionWrapper(django.db.models.functions.math.Abs(django.db.models.expressions.CombinedExpression(models.F('downlink'), '-', models.F('uplink'))), output_field=models.FloatField()), null=True, output_field=models.FloatField(), verbose_name='Перенос')),
('polarization', models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='tran_polarizations', to='mainapp.polarization', verbose_name='Поляризация')),
('sat_id', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='tran_satellite', to='mainapp.satellite', verbose_name='Спутник')),
],
options={
'verbose_name': 'Транспондер',
'verbose_name_plural': 'Транспондеры',
},
),
]
# Generated by Django 5.2.7 on 2025-10-31 13:36
import django.db.models.deletion
import django.db.models.expressions
import django.db.models.functions.math
import mainapp.models
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('mainapp', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Transponders',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(blank=True, max_length=30, null=True, verbose_name='Название транспондера')),
('downlink', models.FloatField(blank=True, null=True, verbose_name='Downlink')),
('frequency_range', models.FloatField(blank=True, null=True, verbose_name='Полоса')),
('uplink', models.FloatField(blank=True, null=True, verbose_name='Uplink')),
('zone_name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Название зоны')),
('transfer', models.GeneratedField(db_persist=True, expression=models.ExpressionWrapper(django.db.models.functions.math.Abs(django.db.models.expressions.CombinedExpression(models.F('downlink'), '-', models.F('uplink'))), output_field=models.FloatField()), null=True, output_field=models.FloatField(), verbose_name='Перенос')),
('polarization', models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='tran_polarizations', to='mainapp.polarization', verbose_name='Поляризация')),
('sat_id', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='tran_satellite', to='mainapp.satellite', verbose_name='Спутник')),
],
options={
'verbose_name': 'Транспондер',
'verbose_name_plural': 'Транспондеры',
},
),
]

View File

@@ -1,64 +1,64 @@
# Generated by Django 5.2.7 on 2025-11-07 20:58
import django.core.validators
import django.db.models.deletion
import mainapp.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0006_alter_customuser_options_alter_geo_options_and_more'),
('mapsapp', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='transponders',
options={'ordering': ['sat_id', 'downlink'], 'verbose_name': 'Транспондер', 'verbose_name_plural': 'Транспондеры'},
),
migrations.AlterField(
model_name='transponders',
name='downlink',
field=models.FloatField(blank=True, help_text='Частота downlink в МГц (0-50000)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Downlink'),
),
migrations.AlterField(
model_name='transponders',
name='frequency_range',
field=models.FloatField(blank=True, help_text='Полоса частот в МГц (0-1000)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000)], verbose_name='Полоса'),
),
migrations.AlterField(
model_name='transponders',
name='name',
field=models.CharField(blank=True, db_index=True, help_text='Название транспондера', max_length=30, null=True, verbose_name='Название транспондера'),
),
migrations.AlterField(
model_name='transponders',
name='polarization',
field=models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, help_text='Поляризация сигнала', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='tran_polarizations', to='mainapp.polarization', verbose_name='Поляризация'),
),
migrations.AlterField(
model_name='transponders',
name='sat_id',
field=models.ForeignKey(help_text='Спутник, которому принадлежит транспондер', on_delete=django.db.models.deletion.PROTECT, related_name='tran_satellite', to='mainapp.satellite', verbose_name='Спутник'),
),
migrations.AlterField(
model_name='transponders',
name='uplink',
field=models.FloatField(blank=True, help_text='Частота uplink в МГц (0-50000)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Uplink'),
),
migrations.AlterField(
model_name='transponders',
name='zone_name',
field=models.CharField(blank=True, db_index=True, help_text='Название зоны покрытия транспондера', max_length=255, null=True, verbose_name='Название зоны'),
),
migrations.AddIndex(
model_name='transponders',
index=models.Index(fields=['sat_id', 'downlink'], name='mapsapp_tra_sat_id__3e3fd7_idx'),
),
migrations.AddIndex(
model_name='transponders',
index=models.Index(fields=['sat_id', 'zone_name'], name='mapsapp_tra_sat_id__305ae7_idx'),
),
]
# Generated by Django 5.2.7 on 2025-11-07 20:58
import django.core.validators
import django.db.models.deletion
import mainapp.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0006_alter_customuser_options_alter_geo_options_and_more'),
('mapsapp', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='transponders',
options={'ordering': ['sat_id', 'downlink'], 'verbose_name': 'Транспондер', 'verbose_name_plural': 'Транспондеры'},
),
migrations.AlterField(
model_name='transponders',
name='downlink',
field=models.FloatField(blank=True, help_text='Частота downlink в МГц (0-50000)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Downlink'),
),
migrations.AlterField(
model_name='transponders',
name='frequency_range',
field=models.FloatField(blank=True, help_text='Полоса частот в МГц (0-1000)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000)], verbose_name='Полоса'),
),
migrations.AlterField(
model_name='transponders',
name='name',
field=models.CharField(blank=True, db_index=True, help_text='Название транспондера', max_length=30, null=True, verbose_name='Название транспондера'),
),
migrations.AlterField(
model_name='transponders',
name='polarization',
field=models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, help_text='Поляризация сигнала', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='tran_polarizations', to='mainapp.polarization', verbose_name='Поляризация'),
),
migrations.AlterField(
model_name='transponders',
name='sat_id',
field=models.ForeignKey(help_text='Спутник, которому принадлежит транспондер', on_delete=django.db.models.deletion.PROTECT, related_name='tran_satellite', to='mainapp.satellite', verbose_name='Спутник'),
),
migrations.AlterField(
model_name='transponders',
name='uplink',
field=models.FloatField(blank=True, help_text='Частота uplink в МГц (0-50000)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Uplink'),
),
migrations.AlterField(
model_name='transponders',
name='zone_name',
field=models.CharField(blank=True, db_index=True, help_text='Название зоны покрытия транспондера', max_length=255, null=True, verbose_name='Название зоны'),
),
migrations.AddIndex(
model_name='transponders',
index=models.Index(fields=['sat_id', 'downlink'], name='mapsapp_tra_sat_id__3e3fd7_idx'),
),
migrations.AddIndex(
model_name='transponders',
index=models.Index(fields=['sat_id', 'zone_name'], name='mapsapp_tra_sat_id__305ae7_idx'),
),
]

View File

@@ -1,117 +1,117 @@
# Django imports
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import ExpressionWrapper, F
from django.db.models.functions import Abs
# Local imports
from mainapp.models import Polarization, Satellite, get_default_polarization
class Transponders(models.Model):
"""
Модель транспондера спутника.
Хранит информацию о частотах uplink/downlink, зоне покрытия и поляризации.
"""
# Основные поля
name = models.CharField(
max_length=30,
null=True,
blank=True,
verbose_name="Название транспондера",
db_index=True,
help_text="Название транспондера"
)
downlink = models.FloatField(
blank=True,
null=True,
verbose_name="Downlink",
validators=[MinValueValidator(0), MaxValueValidator(50000)],
help_text="Частота downlink в МГц (0-50000)"
)
frequency_range = models.FloatField(
blank=True,
null=True,
verbose_name="Полоса",
validators=[MinValueValidator(0), MaxValueValidator(1000)],
help_text="Полоса частот в МГц (0-1000)"
)
uplink = models.FloatField(
blank=True,
null=True,
verbose_name="Uplink",
validators=[MinValueValidator(0), MaxValueValidator(50000)],
help_text="Частота uplink в МГц (0-50000)"
)
zone_name = models.CharField(
max_length=255,
blank=True,
null=True,
verbose_name="Название зоны",
db_index=True,
help_text="Название зоны покрытия транспондера"
)
# Связи
polarization = models.ForeignKey(
Polarization,
default=get_default_polarization,
on_delete=models.SET_DEFAULT,
related_name="tran_polarizations",
null=True,
blank=True,
verbose_name="Поляризация",
help_text="Поляризация сигнала"
)
sat_id = models.ForeignKey(
Satellite,
on_delete=models.PROTECT,
related_name="tran_satellite",
verbose_name="Спутник",
db_index=True,
help_text="Спутник, которому принадлежит транспондер"
)
# Вычисляемые поля
transfer = models.GeneratedField(
expression=ExpressionWrapper(
Abs(F('downlink') - F('uplink')),
output_field=models.FloatField()
),
output_field=models.FloatField(),
db_persist=True,
null=True,
blank=True,
verbose_name="Перенос"
)
def clean(self):
"""Валидация на уровне модели"""
super().clean()
# Проверка что downlink и uplink заданы
if self.downlink and self.uplink:
# Обычно uplink выше downlink для спутниковой связи
if self.uplink < self.downlink:
raise ValidationError({
'uplink': 'Частота uplink обычно выше частоты downlink'
})
def __str__(self):
if self.name:
return self.name
return f"Транспондер {self.sat_id.name if self.sat_id else 'Unknown'}"
class Meta:
verbose_name = "Транспондер"
verbose_name_plural = "Транспондеры"
ordering = ['sat_id', 'downlink']
indexes = [
models.Index(fields=['sat_id', 'downlink']),
models.Index(fields=['sat_id', 'zone_name']),
]
# Django imports
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import ExpressionWrapper, F
from django.db.models.functions import Abs
# Local imports
from mainapp.models import Polarization, Satellite, get_default_polarization
class Transponders(models.Model):
"""
Модель транспондера спутника.
Хранит информацию о частотах uplink/downlink, зоне покрытия и поляризации.
"""
# Основные поля
name = models.CharField(
max_length=30,
null=True,
blank=True,
verbose_name="Название транспондера",
db_index=True,
help_text="Название транспондера"
)
downlink = models.FloatField(
blank=True,
null=True,
verbose_name="Downlink",
validators=[MinValueValidator(0), MaxValueValidator(50000)],
help_text="Частота downlink в МГц (0-50000)"
)
frequency_range = models.FloatField(
blank=True,
null=True,
verbose_name="Полоса",
validators=[MinValueValidator(0), MaxValueValidator(1000)],
help_text="Полоса частот в МГц (0-1000)"
)
uplink = models.FloatField(
blank=True,
null=True,
verbose_name="Uplink",
validators=[MinValueValidator(0), MaxValueValidator(50000)],
help_text="Частота uplink в МГц (0-50000)"
)
zone_name = models.CharField(
max_length=255,
blank=True,
null=True,
verbose_name="Название зоны",
db_index=True,
help_text="Название зоны покрытия транспондера"
)
# Связи
polarization = models.ForeignKey(
Polarization,
default=get_default_polarization,
on_delete=models.SET_DEFAULT,
related_name="tran_polarizations",
null=True,
blank=True,
verbose_name="Поляризация",
help_text="Поляризация сигнала"
)
sat_id = models.ForeignKey(
Satellite,
on_delete=models.PROTECT,
related_name="tran_satellite",
verbose_name="Спутник",
db_index=True,
help_text="Спутник, которому принадлежит транспондер"
)
# Вычисляемые поля
transfer = models.GeneratedField(
expression=ExpressionWrapper(
Abs(F('downlink') - F('uplink')),
output_field=models.FloatField()
),
output_field=models.FloatField(),
db_persist=True,
null=True,
blank=True,
verbose_name="Перенос"
)
def clean(self):
"""Валидация на уровне модели"""
super().clean()
# Проверка что downlink и uplink заданы
if self.downlink and self.uplink:
# Обычно uplink выше downlink для спутниковой связи
if self.uplink < self.downlink:
raise ValidationError({
'uplink': 'Частота uplink обычно выше частоты downlink'
})
def __str__(self):
if self.name:
return self.name
return f"Транспондер {self.sat_id.name if self.sat_id else 'Unknown'}"
class Meta:
verbose_name = "Транспондер"
verbose_name_plural = "Транспондеры"
ordering = ['sat_id', 'downlink']
indexes = [
models.Index(fields=['sat_id', 'downlink']),
models.Index(fields=['sat_id', 'zone_name']),
]

File diff suppressed because it is too large Load Diff

View File

@@ -1,83 +1,83 @@
{% load static %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>{% block title %}Карта{% endblock %}</title>
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
<!-- Leaflet CSS -->
<link href="{% static 'leaflet/leaflet.css' %}" rel="stylesheet">
<link href="{% static 'leaflet-measure/leaflet-measure.css' %}" rel="stylesheet">
<link href="{% static 'leaflet-tree/L.Control.Layers.Tree.css' %}" rel="stylesheet">
<!-- Extra CSS -->
{% block extra_css %}{% endblock %}
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
overflow: hidden;
}
#map {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
</style>
</head>
<body>
<div id="map"></div>
{% block content %}
{% endblock %}
<!-- Leaflet JavaScript -->
<script src="{% static 'leaflet/leaflet.js' %}"></script>
<script src="{% static 'leaflet-measure/leaflet-measure.ru.js' %}"></script>
<script src="{% static 'leaflet-tree/L.Control.Layers.Tree.js' %}"></script>
<script>
let map = L.map('map').setView([0, 0], 2);
L.control.scale({
imperial: false,
metric: true}).addTo(map);
map.attributionControl.setPrefix(false);
const street = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
});
street.addTo(map);
const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles &copy; Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
});
const street_local = L.tileLayer('http://127.0.0.1:8080/styles/basic-preview/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: 'Local Tiles'
});
street_local.addTo(map);
const baseLayers = {
"Улицы": street,
"Спутник": satellite,
"Локально": street_local
};
L.control.layers(baseLayers).addTo(map);
map.setMaxZoom(18);
map.setMinZoom(0);
L.control.measure({ primaryLengthUnit: 'kilometers' }).addTo(map);
{% comment %} let imageUrl = '{% static "mapsapp/assets/world_map.jpg" %}';
let imageBounds = [[-82, -180], [82, 180]];
L.imageOverlay(imageUrl, imageBounds).addTo(map); {% endcomment %}
</script>
{% block extra_js %}{% endblock %}
</body>
{% load static %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>{% block title %}Карта{% endblock %}</title>
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
<!-- Leaflet CSS -->
<link href="{% static 'leaflet/leaflet.css' %}" rel="stylesheet">
<link href="{% static 'leaflet-measure/leaflet-measure.css' %}" rel="stylesheet">
<link href="{% static 'leaflet-tree/L.Control.Layers.Tree.css' %}" rel="stylesheet">
<!-- Extra CSS -->
{% block extra_css %}{% endblock %}
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
overflow: hidden;
}
#map {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
</style>
</head>
<body>
<div id="map"></div>
{% block content %}
{% endblock %}
<!-- Leaflet JavaScript -->
<script src="{% static 'leaflet/leaflet.js' %}"></script>
<script src="{% static 'leaflet-measure/leaflet-measure.ru.js' %}"></script>
<script src="{% static 'leaflet-tree/L.Control.Layers.Tree.js' %}"></script>
<script>
let map = L.map('map').setView([0, 0], 2);
L.control.scale({
imperial: false,
metric: true}).addTo(map);
map.attributionControl.setPrefix(false);
const street = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
});
street.addTo(map);
const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles &copy; Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
});
const street_local = L.tileLayer('http://127.0.0.1:8080/styles/basic-preview/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: 'Local Tiles'
});
street_local.addTo(map);
const baseLayers = {
"Улицы": street,
"Спутник": satellite,
"Локально": street_local
};
L.control.layers(baseLayers).addTo(map);
map.setMaxZoom(18);
map.setMinZoom(0);
L.control.measure({ primaryLengthUnit: 'kilometers' }).addTo(map);
{% comment %} let imageUrl = '{% static "mapsapp/assets/world_map.jpg" %}';
let imageBounds = [[-82, -180], [82, 180]];
L.imageOverlay(imageUrl, imageBounds).addTo(map); {% endcomment %}
</script>
{% block extra_js %}{% endblock %}
</body>
</html>

View File

@@ -1,118 +1,118 @@
{% load static %}
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
<title>Cesium Map Editor</title>
<!-- Cesium Library -->
<script src="{% static 'cesium/Cesium.js' %}" defer></script>
<link href="{% static 'cesium/Widgets/widgets.css' %}" rel="stylesheet">
<!-- Custom Styles -->
<link rel="stylesheet" href="{% static 'mapsapp/style.css' %}">
</head>
<body>
<div id="cesiumContainer"></div>
<input type="file" id="fileInput" accept=".geojson,.json,.kml" style="display: none;" />
<!-- Панель инструментов -->
<div class="toolbar">
<!-- Группа 1: Режимы рисования -->
<div class="toolbar-section">
<div class="section-title">Рисование</div>
<div class="toolbar-group">
<button id="selectMode" class="tool-btn active" title="Режим выделения (S)">
<span>🔍</span> Выделение
</button>
<button id="markerMode" class="tool-btn" title="Добавить маркер (M)">
<span>📌</span> Маркер
</button>
<button id="polygonMode" class="tool-btn" title="Рисовать полигон (P)">
<span></span> Полигон
</button>
<button id="polylineMode" class="tool-btn" title="Рисовать линию (L)">
<span>〰️</span> Линия
</button>
</div>
</div>
<!-- Группа 2: Импорт/Экспорт -->
<div class="toolbar-section">
<div class="section-title">Импорт/экспорт всех объектов</div>
<div class="toolbar-group">
<button id="importBtn" class="tool-btn" title="Импортировать GeoJSON или KML">
<span>📥</span> Импорт
</button>
<button id="exportBtn" class="tool-btn" title="Экспортировать в GeoJSON или KML">
<span>📤</span> Экспорт
</button>
</div>
</div>
<!-- Группа 3: Действия -->
<div class="toolbar-section">
<div class="section-title">Действия</div>
<div class="toolbar-group">
<button id="deleteSelected" class="tool-btn danger" title="Удалить выделенное (Del)">
<span>🗑️</span> Удалить
</button>
<button id="clearAll" class="tool-btn danger" title="Очистить всё">
<span>🧹</span> Очистить
</button>
</div>
</div>
<!-- Строка состояния -->
<div class="status-bar">
<span id="modeStatus">Режим: Выделение</span>
<span id="coordinates" style="color: #eeeeeeff; font-size: 11px;"></span>
<span id="hint">Нажмите ESC для отмены</span>
</div>
</div>
<!-- Блок выбора объектов из БД -->
<div class="db-objects-panel">
<div class="panel-title">Объекты из базы</div>
<select id="objectSelector" class="object-select">
<option value="">— Выберите объект —</option>
{% for sat in sats %}
<option value="{{sat.id}}">{{sat.name}}</option>
{% endfor %}
</select>
<button id="loadObjectBtn" class="load-btn">Загрузить на карту</button>
</div>
<div class="footprint-control">
<div class="panel-title">Области покрытия</div>
<div class="footprint-actions">
<button id="showAllFootprints">Показать все</button>
<button id="hideAllFootprints">Скрыть все</button>
</div>
<div id="footprintToggles"></div>
</div>
<!-- Модальное окно для описания -->
<div id="descriptionModal" class="modal">
<div class="modal-content">
<h3>Добавить описание</h3>
<textarea id="descriptionInput" placeholder="Введите описание объекта..."></textarea>
<div class="modal-buttons">
<button id="confirmDescription">Сохранить</button>
<button id="cancelDescription">Отмена</button>
</div>
</div>
</div>
<div id="exportModal" class="modal">
<div class="modal-content">
<h3>Экспорт данных</h3>
<p>Выберите формат для экспорта всех объектов:</p>
<div class="modal-buttons" style="justify-content: center; gap: 15px; margin-top: 20px;">
<button id="exportGeoJson">GeoJSON</button>
<button id="exportKml">KML</button>
<button id="cancelExport">Отмена</button>
</div>
</div>
</div>
<script src="{% static 'mapsapp/main.js' %}"></script>
</body>
{% load static %}
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
<title>Cesium Map Editor</title>
<!-- Cesium Library -->
<script src="{% static 'cesium/Cesium.js' %}" defer></script>
<link href="{% static 'cesium/Widgets/widgets.css' %}" rel="stylesheet">
<!-- Custom Styles -->
<link rel="stylesheet" href="{% static 'mapsapp/style.css' %}">
</head>
<body>
<div id="cesiumContainer"></div>
<input type="file" id="fileInput" accept=".geojson,.json,.kml" style="display: none;" />
<!-- Панель инструментов -->
<div class="toolbar">
<!-- Группа 1: Режимы рисования -->
<div class="toolbar-section">
<div class="section-title">Рисование</div>
<div class="toolbar-group">
<button id="selectMode" class="tool-btn active" title="Режим выделения (S)">
<span>🔍</span> Выделение
</button>
<button id="markerMode" class="tool-btn" title="Добавить маркер (M)">
<span>📌</span> Маркер
</button>
<button id="polygonMode" class="tool-btn" title="Рисовать полигон (P)">
<span></span> Полигон
</button>
<button id="polylineMode" class="tool-btn" title="Рисовать линию (L)">
<span>〰️</span> Линия
</button>
</div>
</div>
<!-- Группа 2: Импорт/Экспорт -->
<div class="toolbar-section">
<div class="section-title">Импорт/экспорт всех объектов</div>
<div class="toolbar-group">
<button id="importBtn" class="tool-btn" title="Импортировать GeoJSON или KML">
<span>📥</span> Импорт
</button>
<button id="exportBtn" class="tool-btn" title="Экспортировать в GeoJSON или KML">
<span>📤</span> Экспорт
</button>
</div>
</div>
<!-- Группа 3: Действия -->
<div class="toolbar-section">
<div class="section-title">Действия</div>
<div class="toolbar-group">
<button id="deleteSelected" class="tool-btn danger" title="Удалить выделенное (Del)">
<span>🗑️</span> Удалить
</button>
<button id="clearAll" class="tool-btn danger" title="Очистить всё">
<span>🧹</span> Очистить
</button>
</div>
</div>
<!-- Строка состояния -->
<div class="status-bar">
<span id="modeStatus">Режим: Выделение</span>
<span id="coordinates" style="color: #eeeeeeff; font-size: 11px;"></span>
<span id="hint">Нажмите ESC для отмены</span>
</div>
</div>
<!-- Блок выбора объектов из БД -->
<div class="db-objects-panel">
<div class="panel-title">Объекты из базы</div>
<select id="objectSelector" class="object-select">
<option value="">— Выберите объект —</option>
{% for sat in sats %}
<option value="{{sat.id}}">{{sat.name}}</option>
{% endfor %}
</select>
<button id="loadObjectBtn" class="load-btn">Загрузить на карту</button>
</div>
<div class="footprint-control">
<div class="panel-title">Области покрытия</div>
<div class="footprint-actions">
<button id="showAllFootprints">Показать все</button>
<button id="hideAllFootprints">Скрыть все</button>
</div>
<div id="footprintToggles"></div>
</div>
<!-- Модальное окно для описания -->
<div id="descriptionModal" class="modal">
<div class="modal-content">
<h3>Добавить описание</h3>
<textarea id="descriptionInput" placeholder="Введите описание объекта..."></textarea>
<div class="modal-buttons">
<button id="confirmDescription">Сохранить</button>
<button id="cancelDescription">Отмена</button>
</div>
</div>
</div>
<div id="exportModal" class="modal">
<div class="modal-content">
<h3>Экспорт данных</h3>
<p>Выберите формат для экспорта всех объектов:</p>
<div class="modal-buttons" style="justify-content: center; gap: 15px; margin-top: 20px;">
<button id="exportGeoJson">GeoJSON</button>
<button id="exportKml">KML</button>
<button id="cancelExport">Отмена</button>
</div>
</div>
</div>
<script src="{% static 'mapsapp/main.js' %}"></script>
</body>
</html>

View File

@@ -1,3 +1,3 @@
from django.test import TestCase
# Create your tests here.
from django.test import TestCase
# Create your tests here.

View File

@@ -1,14 +1,14 @@
from django.conf import settings
from django.conf.urls.static import static
from django.urls import path
from . import views
app_name = 'mapsapp'
urlpatterns = [
path('3dmap', views.CesiumMapView.as_view(), name='3dmap'),
path('2dmap', views.LeafletMapView.as_view(), name='2dmap'),
path('api/footprint-names/<int:sat_id>', views.GetFootprintsView.as_view(), name="footprint_names"),
path('api/transponders/<int:sat_id>', views.GetTransponderOnSatIdView.as_view(), name='transponders_data'),
path('tiles/<str:footprint_name>/<int:z>/<int:x>/<int:y>.png', views.TileProxyView.as_view(), name='tile_proxy'),
from django.conf import settings
from django.conf.urls.static import static
from django.urls import path
from . import views
app_name = 'mapsapp'
urlpatterns = [
path('3dmap', views.CesiumMapView.as_view(), name='3dmap'),
path('2dmap', views.LeafletMapView.as_view(), name='2dmap'),
path('api/footprint-names/<int:sat_id>', views.GetFootprintsView.as_view(), name="footprint_names"),
path('api/transponders/<int:sat_id>', views.GetTransponderOnSatIdView.as_view(), name='transponders_data'),
path('tiles/<str:footprint_name>/<int:z>/<int:x>/<int:y>.png', views.TileProxyView.as_view(), name='tile_proxy'),
]

View File

@@ -1,165 +1,165 @@
# Standard library imports
import json
import re
from io import BytesIO
# Third-party imports
import requests
# Local imports
from mainapp.models import Polarization, Satellite
from .models import Transponders
def search_satellite_on_page(data: dict, satellite_name: str):
for pos, value in data.get('page', {}).get('positions').items():
for name in value['satellites']:
if name['other_names'] is None:
name['other_names'] = ''
if satellite_name.lower() in name['name'].lower() or satellite_name.lower() in name['other_names'].lower():
return pos, name['id']
return '', ''
def get_footprint_data(position: str = 62) -> dict:
"""Возвращает словарь с данным по footprint для спутников на выбранной долготе"""
response = requests.get(f"https://www.satbeams.com/footprints?position={position}")
response.raise_for_status()
match = re.search(r'var data = ({.*?});', response.text, re.DOTALL)
if match:
json_str = match.group(1)
try:
data = json.loads(json_str)
return data.get("page", {}).get("footprint_data", {}).get("beams",[])
except json.JSONDecodeError as e:
print("Ошибка парсинга JSON:", e)
else:
print("Нужных данных не найдено")
return {}
def get_all_page_data(url:str = 'https://www.satbeams.com/footprints') -> dict:
"""Возвращает словарь с данными по всем спутникам на странице"""
response = requests.get(url)
response.raise_for_status()
match = re.search(r'var data = ({.*?});', response.text, re.DOTALL)
if match:
json_str = match.group(1)
try:
data = json.loads(json_str)
# Файл json на диске для достоверности
with open('data.json', 'w') as jf:
json.dump(data, jf, indent=2)
return data
except json.JSONDecodeError as e:
print("Ошибка парсинга JSON:", e)
else:
print("Нужных данных не найдено")
return {}
def get_names_footprints_for_satellite(footprint_data: dict, sat_id: str) -> list[str]:
names = []
for beam in footprint_data:
if 'ku' in beam['band'].lower() and sat_id in beam['satellite_id']:
names.append(
{
"name": beam['name'],
"fullname": beam['fullname'][8:]
}
)
return names
def get_band_names(satellite_name: str) -> list[str]:
data = get_all_page_data()
pos, sat_id = search_satellite_on_page(data, satellite_name)
footprints = get_footprint_data(pos)
names = get_names_footprints_for_satellite(footprints, sat_id)
return names
def parse_transponders_from_json(filepath: str):
with open(filepath, encoding="utf-8") as jf:
data = json.load(jf)
for sat_name, trans_zone in data["satellites"].items():
for zone, trans in trans_zone.items():
for tran in trans:
f_b, f_e = tran["freq"][0].split("-")
f = round((float(f_b) + float(f_e))/2, 3)
f_range = round(abs(float(f_e) - float(f_b)), 3)
tran_obj = Transponders.objects.create(
name=tran["name"],
frequency=f,
frequency_range=f_range,
zone_name=zone,
polarization=Polarization.objects.get(name=tran["pol"]),
sat_id=Satellite.objects.get(name__iexact=sat_name)
)
tran_obj.save()
# Third-party imports (additional)
from lxml import etree
def parse_transponders_from_xml(data_in: BytesIO):
tree = etree.parse(data_in)
ns = {
'i': 'http://www.w3.org/2001/XMLSchema-instance',
'ns': 'http://schemas.datacontract.org/2004/07/Geolocation.Domain.Utils.Repository.SatellitesSerialization.Memos',
'tr': 'http://schemas.datacontract.org/2004/07/Geolocation.Common.Extensions'
}
satellites = tree.xpath('//ns:SatelliteMemo', namespaces=ns)
for sat in satellites[:]:
name = sat.xpath('./ns:name/text()', namespaces=ns)[0]
if name == 'X' or 'DONT USE' in name:
continue
norad = sat.xpath('./ns:norad/text()', namespaces=ns)
beams = sat.xpath('.//ns:BeamMemo', namespaces=ns)
zones = {}
for zone in beams:
zone_name = zone.xpath('./ns:name/text()', namespaces=ns)[0] if zone.xpath('./ns:name/text()', namespaces=ns) else '-'
zones[zone.xpath('./ns:id/text()', namespaces=ns)[0]] = {
"name": zone_name,
"pol": zone.xpath('./ns:polarization/text()', namespaces=ns)[0],
}
transponders = sat.xpath('.//ns:TransponderMemo', namespaces=ns)
for transponder in transponders:
tr_id = transponder.xpath('./ns:downlinkBeamId/text()', namespaces=ns)[0]
downlink_start = float(transponder.xpath('./ns:downlinkFrequency/tr:start/text()', namespaces=ns)[0])
downlink_end = float(transponder.xpath('./ns:downlinkFrequency/tr:end/text()', namespaces=ns)[0])
uplink_start = float(transponder.xpath('./ns:uplinkFrequency/tr:start/text()', namespaces=ns)[0])
uplink_end = float(transponder.xpath('./ns:uplinkFrequency/tr:end/text()', namespaces=ns)[0])
tr_data = zones[tr_id]
# p = tr_data['pol'][0] if tr_data['pol'] else '-'
match tr_data['pol']:
case 'Horizontal':
pol = 'Горизонтальная'
case 'Vertical':
pol = 'Вертикальная'
case 'CircularRight':
pol = 'Правая'
case 'CircularLeft':
pol = 'Левая'
case _:
pol = '-'
tr_name = transponder.xpath('./ns:name/text()', namespaces=ns)[0]
pol_obj, _ = Polarization.objects.get_or_create(name=pol)
sat_obj, _ = Satellite.objects.get_or_create(
name=name,
defaults={
"norad": int(norad[0]) if norad else -1
})
trans_obj, _ = Transponders.objects.get_or_create(
polarization=pol_obj,
downlink=(downlink_start+downlink_end)/2/1000000,
uplink=(uplink_start+uplink_end)/2/1000000,
frequency_range=abs(downlink_end-downlink_start)/1000000,
name=tr_name,
defaults={
"zone_name": tr_data['name'],
"sat_id": sat_obj,
}
)
trans_obj.save()
# Standard library imports
import json
import re
from io import BytesIO
# Third-party imports
import requests
# Local imports
from mainapp.models import Polarization, Satellite
from .models import Transponders
def search_satellite_on_page(data: dict, satellite_name: str):
for pos, value in data.get('page', {}).get('positions').items():
for name in value['satellites']:
if name['other_names'] is None:
name['other_names'] = ''
if satellite_name.lower() in name['name'].lower() or satellite_name.lower() in name['other_names'].lower():
return pos, name['id']
return '', ''
def get_footprint_data(position: str = 62) -> dict:
"""Возвращает словарь с данным по footprint для спутников на выбранной долготе"""
response = requests.get(f"https://www.satbeams.com/footprints?position={position}")
response.raise_for_status()
match = re.search(r'var data = ({.*?});', response.text, re.DOTALL)
if match:
json_str = match.group(1)
try:
data = json.loads(json_str)
return data.get("page", {}).get("footprint_data", {}).get("beams",[])
except json.JSONDecodeError as e:
print("Ошибка парсинга JSON:", e)
else:
print("Нужных данных не найдено")
return {}
def get_all_page_data(url:str = 'https://www.satbeams.com/footprints') -> dict:
"""Возвращает словарь с данными по всем спутникам на странице"""
response = requests.get(url)
response.raise_for_status()
match = re.search(r'var data = ({.*?});', response.text, re.DOTALL)
if match:
json_str = match.group(1)
try:
data = json.loads(json_str)
# Файл json на диске для достоверности
with open('data.json', 'w') as jf:
json.dump(data, jf, indent=2)
return data
except json.JSONDecodeError as e:
print("Ошибка парсинга JSON:", e)
else:
print("Нужных данных не найдено")
return {}
def get_names_footprints_for_satellite(footprint_data: dict, sat_id: str) -> list[str]:
names = []
for beam in footprint_data:
if 'ku' in beam['band'].lower() and sat_id in beam['satellite_id']:
names.append(
{
"name": beam['name'],
"fullname": beam['fullname'][8:]
}
)
return names
def get_band_names(satellite_name: str) -> list[str]:
data = get_all_page_data()
pos, sat_id = search_satellite_on_page(data, satellite_name)
footprints = get_footprint_data(pos)
names = get_names_footprints_for_satellite(footprints, sat_id)
return names
def parse_transponders_from_json(filepath: str):
with open(filepath, encoding="utf-8") as jf:
data = json.load(jf)
for sat_name, trans_zone in data["satellites"].items():
for zone, trans in trans_zone.items():
for tran in trans:
f_b, f_e = tran["freq"][0].split("-")
f = round((float(f_b) + float(f_e))/2, 3)
f_range = round(abs(float(f_e) - float(f_b)), 3)
tran_obj = Transponders.objects.create(
name=tran["name"],
frequency=f,
frequency_range=f_range,
zone_name=zone,
polarization=Polarization.objects.get(name=tran["pol"]),
sat_id=Satellite.objects.get(name__iexact=sat_name)
)
tran_obj.save()
# Third-party imports (additional)
from lxml import etree
def parse_transponders_from_xml(data_in: BytesIO):
tree = etree.parse(data_in)
ns = {
'i': 'http://www.w3.org/2001/XMLSchema-instance',
'ns': 'http://schemas.datacontract.org/2004/07/Geolocation.Domain.Utils.Repository.SatellitesSerialization.Memos',
'tr': 'http://schemas.datacontract.org/2004/07/Geolocation.Common.Extensions'
}
satellites = tree.xpath('//ns:SatelliteMemo', namespaces=ns)
for sat in satellites[:]:
name = sat.xpath('./ns:name/text()', namespaces=ns)[0]
if name == 'X' or 'DONT USE' in name:
continue
norad = sat.xpath('./ns:norad/text()', namespaces=ns)
beams = sat.xpath('.//ns:BeamMemo', namespaces=ns)
zones = {}
for zone in beams:
zone_name = zone.xpath('./ns:name/text()', namespaces=ns)[0] if zone.xpath('./ns:name/text()', namespaces=ns) else '-'
zones[zone.xpath('./ns:id/text()', namespaces=ns)[0]] = {
"name": zone_name,
"pol": zone.xpath('./ns:polarization/text()', namespaces=ns)[0],
}
transponders = sat.xpath('.//ns:TransponderMemo', namespaces=ns)
for transponder in transponders:
tr_id = transponder.xpath('./ns:downlinkBeamId/text()', namespaces=ns)[0]
downlink_start = float(transponder.xpath('./ns:downlinkFrequency/tr:start/text()', namespaces=ns)[0])
downlink_end = float(transponder.xpath('./ns:downlinkFrequency/tr:end/text()', namespaces=ns)[0])
uplink_start = float(transponder.xpath('./ns:uplinkFrequency/tr:start/text()', namespaces=ns)[0])
uplink_end = float(transponder.xpath('./ns:uplinkFrequency/tr:end/text()', namespaces=ns)[0])
tr_data = zones[tr_id]
# p = tr_data['pol'][0] if tr_data['pol'] else '-'
match tr_data['pol']:
case 'Horizontal':
pol = 'Горизонтальная'
case 'Vertical':
pol = 'Вертикальная'
case 'CircularRight':
pol = 'Правая'
case 'CircularLeft':
pol = 'Левая'
case _:
pol = '-'
tr_name = transponder.xpath('./ns:name/text()', namespaces=ns)[0]
pol_obj, _ = Polarization.objects.get_or_create(name=pol)
sat_obj, _ = Satellite.objects.get_or_create(
name=name,
defaults={
"norad": int(norad[0]) if norad else -1
})
trans_obj, _ = Transponders.objects.get_or_create(
polarization=pol_obj,
downlink=(downlink_start+downlink_end)/2/1000000,
uplink=(uplink_start+uplink_end)/2/1000000,
frequency_range=abs(downlink_end-downlink_start)/1000000,
name=tr_name,
defaults={
"zone_name": tr_data['name'],
"sat_id": sat_obj,
}
)
trans_obj.save()

View File

@@ -1,148 +1,148 @@
# Standard library imports
from typing import Any, Dict
# Django imports
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponse, HttpResponseNotFound, JsonResponse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.cache import cache_page
from django.views.decorators.http import require_GET
from django.views.generic import TemplateView
# Third-party imports
import requests
# Local imports
from mainapp.models import Satellite
from .models import Transponders
from .utils import get_band_names
class CesiumMapView(LoginRequiredMixin, TemplateView):
"""
Представление для отображения 3D карты с использованием Cesium.
Отображает спутники и их зоны покрытия на интерактивной 3D карте.
"""
template_name = 'mapsapp/map3d.html'
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs)
# Оптимизированный запрос - загружаем только необходимые поля
context['sats'] = Satellite.objects.filter(
parameters__objitems__isnull=False
).distinct().only('id', 'name').order_by('name')
return context
class GetFootprintsView(LoginRequiredMixin, View):
"""
API для получения зон покрытия (footprints) спутника.
Возвращает список названий зон покрытия для указанного спутника.
"""
def get(self, request, sat_id):
try:
# Оптимизированный запрос - загружаем только поле name
sat_name = Satellite.objects.only('name').get(id=sat_id).name
footprint_names = get_band_names(sat_name)
return JsonResponse(footprint_names, safe=False)
except Satellite.DoesNotExist:
return JsonResponse({"error": "Спутник не найден"}, status=404)
except Exception as e:
return JsonResponse({"error": str(e)}, status=500)
class TileProxyView(View):
"""
Прокси для загрузки тайлов карты покрытия спутников.
Кэширует тайлы на 7 дней для улучшения производительности.
"""
# Константы
TILE_BASE_URL = "https://static.satbeams.com/tiles"
CACHE_DURATION = 60 * 60 * 24 * 7 # 7 дней
REQUEST_TIMEOUT = 10 # секунд
@method_decorator(require_GET)
@method_decorator(cache_page(CACHE_DURATION))
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
def get(self, request, footprint_name, z, x, y):
# Валидация имени footprint
if not footprint_name.replace('-', '').replace('_', '').isalnum():
return HttpResponse("Invalid footprint name", status=400)
url = f"{self.TILE_BASE_URL}/{footprint_name}/{z}/{x}/{y}.png"
try:
resp = requests.get(url, timeout=self.REQUEST_TIMEOUT)
if resp.status_code == 200:
response = HttpResponse(resp.content, content_type='image/png')
response["Access-Control-Allow-Origin"] = "*"
response["Cache-Control"] = f"public, max-age={self.CACHE_DURATION}"
return response
else:
return HttpResponseNotFound("Tile not found")
except requests.Timeout:
return HttpResponse("Request timeout", status=504)
except requests.RequestException as e:
return HttpResponse(f"Proxy error: {e}", status=500)
class LeafletMapView(LoginRequiredMixin, TemplateView):
"""
Представление для отображения 2D карты с использованием Leaflet.
Отображает спутники и транспондеры на интерактивной 2D карте.
"""
template_name = 'mapsapp/map2d.html'
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs)
# Оптимизированные запросы - загружаем только необходимые поля
context['sats'] = Satellite.objects.filter(
parameters__objitems__isnull=False
).distinct().only('id', 'name').order_by('name')
context['trans'] = Transponders.objects.select_related(
'sat_id', 'polarization'
).only(
'id', 'name', 'sat_id__name', 'polarization__name',
'downlink', 'frequency_range', 'zone_name'
)
return context
class GetTransponderOnSatIdView(LoginRequiredMixin, View):
"""
API для получения транспондеров спутника.
Возвращает список транспондеров для указанного спутника с оптимизированными запросами.
"""
def get(self, request, sat_id):
# Оптимизированный запрос с select_related и only
trans = Transponders.objects.filter(
sat_id=sat_id
).select_related('polarization').only(
'name', 'downlink', 'frequency_range',
'zone_name', 'polarization__name'
)
if not trans.exists():
return JsonResponse({'error': 'Объектов не найдено'}, status=404)
# Используем list comprehension для лучшей производительности
output = [
{
"name": tran.name,
"frequency": tran.downlink,
"frequency_range": tran.frequency_range,
"zone_name": tran.zone_name,
"polarization": tran.polarization.name if tran.polarization else "-"
}
for tran in trans
]
# Standard library imports
from typing import Any, Dict
# Django imports
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponse, HttpResponseNotFound, JsonResponse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.cache import cache_page
from django.views.decorators.http import require_GET
from django.views.generic import TemplateView
# Third-party imports
import requests
# Local imports
from mainapp.models import Satellite
from .models import Transponders
from .utils import get_band_names
class CesiumMapView(LoginRequiredMixin, TemplateView):
"""
Представление для отображения 3D карты с использованием Cesium.
Отображает спутники и их зоны покрытия на интерактивной 3D карте.
"""
template_name = 'mapsapp/map3d.html'
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs)
# Оптимизированный запрос - загружаем только необходимые поля
context['sats'] = Satellite.objects.filter(
parameters__objitems__isnull=False
).distinct().only('id', 'name').order_by('name')
return context
class GetFootprintsView(LoginRequiredMixin, View):
"""
API для получения зон покрытия (footprints) спутника.
Возвращает список названий зон покрытия для указанного спутника.
"""
def get(self, request, sat_id):
try:
# Оптимизированный запрос - загружаем только поле name
sat_name = Satellite.objects.only('name').get(id=sat_id).name
footprint_names = get_band_names(sat_name)
return JsonResponse(footprint_names, safe=False)
except Satellite.DoesNotExist:
return JsonResponse({"error": "Спутник не найден"}, status=404)
except Exception as e:
return JsonResponse({"error": str(e)}, status=500)
class TileProxyView(View):
"""
Прокси для загрузки тайлов карты покрытия спутников.
Кэширует тайлы на 7 дней для улучшения производительности.
"""
# Константы
TILE_BASE_URL = "https://static.satbeams.com/tiles"
CACHE_DURATION = 60 * 60 * 24 * 7 # 7 дней
REQUEST_TIMEOUT = 10 # секунд
@method_decorator(require_GET)
@method_decorator(cache_page(CACHE_DURATION))
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
def get(self, request, footprint_name, z, x, y):
# Валидация имени footprint
if not footprint_name.replace('-', '').replace('_', '').isalnum():
return HttpResponse("Invalid footprint name", status=400)
url = f"{self.TILE_BASE_URL}/{footprint_name}/{z}/{x}/{y}.png"
try:
resp = requests.get(url, timeout=self.REQUEST_TIMEOUT)
if resp.status_code == 200:
response = HttpResponse(resp.content, content_type='image/png')
response["Access-Control-Allow-Origin"] = "*"
response["Cache-Control"] = f"public, max-age={self.CACHE_DURATION}"
return response
else:
return HttpResponseNotFound("Tile not found")
except requests.Timeout:
return HttpResponse("Request timeout", status=504)
except requests.RequestException as e:
return HttpResponse(f"Proxy error: {e}", status=500)
class LeafletMapView(LoginRequiredMixin, TemplateView):
"""
Представление для отображения 2D карты с использованием Leaflet.
Отображает спутники и транспондеры на интерактивной 2D карте.
"""
template_name = 'mapsapp/map2d.html'
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs)
# Оптимизированные запросы - загружаем только необходимые поля
context['sats'] = Satellite.objects.filter(
parameters__objitems__isnull=False
).distinct().only('id', 'name').order_by('name')
context['trans'] = Transponders.objects.select_related(
'sat_id', 'polarization'
).only(
'id', 'name', 'sat_id__name', 'polarization__name',
'downlink', 'frequency_range', 'zone_name'
)
return context
class GetTransponderOnSatIdView(LoginRequiredMixin, View):
"""
API для получения транспондеров спутника.
Возвращает список транспондеров для указанного спутника с оптимизированными запросами.
"""
def get(self, request, sat_id):
# Оптимизированный запрос с select_related и only
trans = Transponders.objects.filter(
sat_id=sat_id
).select_related('polarization').only(
'name', 'downlink', 'frequency_range',
'zone_name', 'polarization__name'
)
if not trans.exists():
return JsonResponse({'error': 'Объектов не найдено'}, status=404)
# Используем list comprehension для лучшей производительности
output = [
{
"name": tran.name,
"frequency": tran.downlink,
"frequency_range": tran.frequency_range,
"zone_name": tran.zone_name,
"polarization": tran.polarization.name if tran.polarization else "-"
}
for tran in trans
]
return JsonResponse(output, safe=False)

View File

@@ -8,11 +8,14 @@ dependencies = [
"aiosqlite>=0.21.0",
"bcrypt>=5.0.0",
"beautifulsoup4>=4.14.2",
"celery>=5.5.3",
"django>=5.2.7",
"django-admin-interface>=0.30.1",
"django-admin-multiple-choice-list-filter>=0.1.1",
"django-admin-rangefilter>=0.13.3",
"django-autocomplete-light>=3.12.1",
"django-celery-beat>=2.6.0",
"django-celery-results>=2.5.1",
"django-daisy>=1.1.2",
"django-debug-toolbar>=6.0.0",
"django-dynamic-raw-id>=4.4",
@@ -21,6 +24,7 @@ dependencies = [
"django-map-widgets>=0.5.1",
"django-more-admin-filters>=1.13",
"dotenv>=0.9.9",
"flower>=2.0.1",
"geopy>=2.4.1",
"gunicorn>=23.0.0",
"lxml>=6.0.2",

View File

@@ -1,33 +0,0 @@
aiosqlite>=0.21.0
bcrypt>=5.0.0
beautifulsoup4>=4.14.2
django>=5.2.7
django-admin-interface>=0.30.1
django-admin-multiple-choice-list-filter>=0.1.1
django-admin-rangefilter>=0.13.3
django-autocomplete-light>=3.12.1
django-daisy>=1.1.2
django-debug-toolbar>=6.0.0
django-dynamic-raw-id>=4.4
django-import-export>=4.3.10
django-leaflet>=0.32.0
django-map-widgets>=0.5.1
django-more-admin-filters>=1.13
dotenv>=0.9.9
geopy>=2.4.1
gunicorn>=23.0.0
lxml>=6.0.2
matplotlib>=3.10.7
numpy>=2.3.3
openpyxl>=3.1.5
pandas>=2.3.3
psycopg>=3.2.10
psycopg2-binary>=2.9.11
redis>=6.4.0
celery>=5.4.0
django-celery-results>=2.5.1
requests>=2.32.5
reverse-geocoder>=1.5.1
scikit-learn>=1.7.2
selenium>=4.38.0
setuptools>=80.9.0

View File

@@ -1,5 +1,5 @@
#!/bin/bash
# Script to start Celery worker
echo "Starting Celery worker..."
celery -A dbapp worker --loglevel=info --logfile=logs/celery_worker.log
#!/bin/bash
# Script to start Celery worker
echo "Starting Celery worker..."
celery -A dbapp worker --loglevel=info --logfile=logs/celery_worker.log

View File

@@ -1,30 +1,30 @@
{
"measure": "Messung",
"measureDistancesAndAreas": "Messung von Abständen und Flächen",
"createNewMeasurement": "Eine neue Messung durchführen",
"startCreating": "Führen Sie die Messung durch, indem Sie der Karte Punkte hinzufügen.",
"finishMeasurement": "Messung beenden",
"lastPoint": "Letzter Punkt",
"area": "Fläche",
"perimeter": "Rand",
"pointLocation": "Lage des Punkts",
"areaMeasurement": "Gemessene Fläche",
"linearMeasurement": "Gemessener Abstand",
"pathDistance": "Abstand entlang des Pfads",
"centerOnArea": "Auf diese Fläche zentrieren",
"centerOnLine": "Auf diesen Linienzug zentrieren",
"centerOnLocation": "Auf diesen Ort zentrieren",
"cancel": "Abbrechen",
"delete": "Löschen",
"acres": "Morgen",
"feet": "Fuß",
"kilometers": "Kilometer",
"hectares": "Hektar",
"meters": "Meter",
"miles": "Meilen",
"sqfeet": "Quadratfuß",
"sqmeters": "Quadratmeter",
"sqmiles": "Quadratmeilen",
"decPoint": ",",
"thousandsSep": "."
}
{
"measure": "Messung",
"measureDistancesAndAreas": "Messung von Abständen und Flächen",
"createNewMeasurement": "Eine neue Messung durchführen",
"startCreating": "Führen Sie die Messung durch, indem Sie der Karte Punkte hinzufügen.",
"finishMeasurement": "Messung beenden",
"lastPoint": "Letzter Punkt",
"area": "Fläche",
"perimeter": "Rand",
"pointLocation": "Lage des Punkts",
"areaMeasurement": "Gemessene Fläche",
"linearMeasurement": "Gemessener Abstand",
"pathDistance": "Abstand entlang des Pfads",
"centerOnArea": "Auf diese Fläche zentrieren",
"centerOnLine": "Auf diesen Linienzug zentrieren",
"centerOnLocation": "Auf diesen Ort zentrieren",
"cancel": "Abbrechen",
"delete": "Löschen",
"acres": "Morgen",
"feet": "Fuß",
"kilometers": "Kilometer",
"hectares": "Hektar",
"meters": "Meter",
"miles": "Meilen",
"sqfeet": "Quadratfuß",
"sqmeters": "Quadratmeter",
"sqmiles": "Quadratmeilen",
"decPoint": ",",
"thousandsSep": "."
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,312 +1,312 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
overflow: hidden;
}
#cesiumContainer {
width: 100vw;
height: 100vh;
position: relative;
}
/* Панель инструментов */
.toolbar {
position: absolute;
top: 10px;
left: 10px;
background: rgba(42, 42, 42, 0.9);
border-radius: 8px;
padding: 10px;
z-index: 1000;
backdrop-filter: blur(10px);
border: 1px solid #555;
}
.toolbar-section {
margin-bottom: 10px;
padding: 8px 10px;
border-radius: 6px;
background: rgba(50, 50, 50, 0.7);
border: 1px solid #444;
}
.section-title {
font-size: 11px;
color: #aaa;
margin-bottom: 5px;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: bold;
}
/* Убираем лишние отступы внутри групп */
.toolbar-group {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 5px;
}
.toolbar-group:last-child {
margin-bottom: 0;
}
.tool-btn {
background: #4a4a4a;
border: 2px solid #666;
color: white;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 5px;
}
.tool-btn:hover {
background: #5a5a5a;
border-color: #888;
}
.tool-btn.active {
background: #2e6da4;
border-color: #4a90d9;
box-shadow: 0 0 10px rgba(74, 144, 217, 0.5);
}
.tool-btn.danger {
background: #d9534f;
border-color: #d43f3a;
}
.tool-btn.danger:hover {
background: #c9302c;
border-color: #ac2925;
}
.tool-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Строка состояния */
.status-bar {
display: flex;
flex-direction: column;
gap: 4px; /* расстояние между строками */
color: white;
font-size: 11px;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid #555;
}
#hint {
color: #aaa;
font-style: italic;
}
/* Модальное окно */
.modal {
display: none;
position: fixed;
z-index: 2000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
}
.modal-content {
background-color: #2b2b2b;
margin: 15% auto;
padding: 20px;
border-radius: 8px;
width: 400px;
max-width: 90%;
border: 1px solid #555;
color: white;
}
.modal-content h3 {
margin-bottom: 15px;
color: #fff;
}
#descriptionInput {
width: 100%;
height: 100px;
background: #3a3a3a;
border: 1px solid #555;
border-radius: 4px;
color: white;
padding: 10px;
font-family: inherit;
resize: vertical;
}
.modal-buttons {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 15px;
}
.modal-buttons button {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-family: inherit;
}
#confirmDescription {
background: #5cb85c;
color: white;
}
#cancelDescription {
background: #6c757d;
color: white;
}
/* Стили для выделенных объектов */
.selected-entity {
outline: 2px solid #ffeb3b !important;
outline-offset: 2px;
}
.edit-point {
cursor: move;
}
/* Панель объектов из БД */
.db-objects-panel {
position: absolute;
top: calc(10px + 400px); /* под toolbar'ом (~300px — примерная высота toolbar) */
left: 10px;
background: rgba(42, 42, 42, 0.9);
border-radius: 8px;
padding: 12px;
z-index: 1000;
backdrop-filter: blur(10px);
border: 1px solid #555;
display: flex;
flex-direction: column;
gap: 8px;
width: 220px;
}
.panel-title {
font-size: 11px;
color: #aaa;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: bold;
margin-bottom: 4px;
}
.object-select {
background: #3a3a3a;
color: white;
border: 1px solid #555;
border-radius: 4px;
padding: 6px 8px;
font-size: 12px;
width: 100%;
}
.load-btn {
background: #2e6da4;
color: white;
border: none;
border-radius: 4px;
padding: 6px 10px;
font-size: 12px;
cursor: pointer;
transition: background 0.2s;
}
.load-btn:hover {
background: #3a87ad;
}
.footprint-control {
position: absolute;
top: calc(10px + 400px + 120px); /* ниже блока с объектами */
left: 10px;
background: rgba(42, 42, 42, 0.9);
border-radius: 8px;
padding: 12px;
z-index: 1000;
backdrop-filter: blur(10px);
border: 1px solid #555;
width: 220px;
}
.footprint-control .panel-title {
font-size: 11px;
color: #aaa;
text-transform: uppercase;
margin-bottom: 8px;
}
.footprint-control label {
color: white;
font-size: 12px;
cursor: pointer;
}
.footprint-actions {
display: flex;
gap: 6px;
margin-bottom: 8px;
}
.footprint-actions button {
flex: 1;
padding: 4px 6px;
font-size: 11px;
background: #4a4a4a;
color: white;
border: 1px solid #666;
border-radius: 4px;
cursor: pointer;
}
.footprint-actions button:hover {
background: #5a5a5a;
}
/* Адаптивность */
@media (max-width: 768px) {
.toolbar {
left: 5px;
right: 5px;
top: 5px;
}
.toolbar-group {
flex-wrap: wrap;
}
.tool-btn {
flex: 1;
min-width: 80px;
font-size: 11px;
padding: 6px 8px;
}
.status-bar {
flex-direction: column;
gap: 5px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
overflow: hidden;
}
#cesiumContainer {
width: 100vw;
height: 100vh;
position: relative;
}
/* Панель инструментов */
.toolbar {
position: absolute;
top: 10px;
left: 10px;
background: rgba(42, 42, 42, 0.9);
border-radius: 8px;
padding: 10px;
z-index: 1000;
backdrop-filter: blur(10px);
border: 1px solid #555;
}
.toolbar-section {
margin-bottom: 10px;
padding: 8px 10px;
border-radius: 6px;
background: rgba(50, 50, 50, 0.7);
border: 1px solid #444;
}
.section-title {
font-size: 11px;
color: #aaa;
margin-bottom: 5px;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: bold;
}
/* Убираем лишние отступы внутри групп */
.toolbar-group {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 5px;
}
.toolbar-group:last-child {
margin-bottom: 0;
}
.tool-btn {
background: #4a4a4a;
border: 2px solid #666;
color: white;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 5px;
}
.tool-btn:hover {
background: #5a5a5a;
border-color: #888;
}
.tool-btn.active {
background: #2e6da4;
border-color: #4a90d9;
box-shadow: 0 0 10px rgba(74, 144, 217, 0.5);
}
.tool-btn.danger {
background: #d9534f;
border-color: #d43f3a;
}
.tool-btn.danger:hover {
background: #c9302c;
border-color: #ac2925;
}
.tool-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Строка состояния */
.status-bar {
display: flex;
flex-direction: column;
gap: 4px; /* расстояние между строками */
color: white;
font-size: 11px;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid #555;
}
#hint {
color: #aaa;
font-style: italic;
}
/* Модальное окно */
.modal {
display: none;
position: fixed;
z-index: 2000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
}
.modal-content {
background-color: #2b2b2b;
margin: 15% auto;
padding: 20px;
border-radius: 8px;
width: 400px;
max-width: 90%;
border: 1px solid #555;
color: white;
}
.modal-content h3 {
margin-bottom: 15px;
color: #fff;
}
#descriptionInput {
width: 100%;
height: 100px;
background: #3a3a3a;
border: 1px solid #555;
border-radius: 4px;
color: white;
padding: 10px;
font-family: inherit;
resize: vertical;
}
.modal-buttons {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 15px;
}
.modal-buttons button {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-family: inherit;
}
#confirmDescription {
background: #5cb85c;
color: white;
}
#cancelDescription {
background: #6c757d;
color: white;
}
/* Стили для выделенных объектов */
.selected-entity {
outline: 2px solid #ffeb3b !important;
outline-offset: 2px;
}
.edit-point {
cursor: move;
}
/* Панель объектов из БД */
.db-objects-panel {
position: absolute;
top: calc(10px + 400px); /* под toolbar'ом (~300px — примерная высота toolbar) */
left: 10px;
background: rgba(42, 42, 42, 0.9);
border-radius: 8px;
padding: 12px;
z-index: 1000;
backdrop-filter: blur(10px);
border: 1px solid #555;
display: flex;
flex-direction: column;
gap: 8px;
width: 220px;
}
.panel-title {
font-size: 11px;
color: #aaa;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: bold;
margin-bottom: 4px;
}
.object-select {
background: #3a3a3a;
color: white;
border: 1px solid #555;
border-radius: 4px;
padding: 6px 8px;
font-size: 12px;
width: 100%;
}
.load-btn {
background: #2e6da4;
color: white;
border: none;
border-radius: 4px;
padding: 6px 10px;
font-size: 12px;
cursor: pointer;
transition: background 0.2s;
}
.load-btn:hover {
background: #3a87ad;
}
.footprint-control {
position: absolute;
top: calc(10px + 400px + 120px); /* ниже блока с объектами */
left: 10px;
background: rgba(42, 42, 42, 0.9);
border-radius: 8px;
padding: 12px;
z-index: 1000;
backdrop-filter: blur(10px);
border: 1px solid #555;
width: 220px;
}
.footprint-control .panel-title {
font-size: 11px;
color: #aaa;
text-transform: uppercase;
margin-bottom: 8px;
}
.footprint-control label {
color: white;
font-size: 12px;
cursor: pointer;
}
.footprint-actions {
display: flex;
gap: 6px;
margin-bottom: 8px;
}
.footprint-actions button {
flex: 1;
padding: 4px 6px;
font-size: 11px;
background: #4a4a4a;
color: white;
border: 1px solid #666;
border-radius: 4px;
cursor: pointer;
}
.footprint-actions button:hover {
background: #5a5a5a;
}
/* Адаптивность */
@media (max-width: 768px) {
.toolbar {
left: 5px;
right: 5px;
top: 5px;
}
.toolbar-group {
flex-wrap: wrap;
}
.tool-btn {
flex: 1;
min-width: 80px;
font-size: 11px;
padding: 6px 8px;
}
.status-bar {
flex-direction: column;
gap: 5px;
}
}

File diff suppressed because it is too large Load Diff

267
dbapp/uv.lock generated
View File

@@ -14,6 +14,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0", size = 15792, upload-time = "2025-02-03T07:30:13.6Z" },
]
[[package]]
name = "amqp"
version = "5.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "vine" },
]
sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013, upload-time = "2024-11-12T19:55:44.051Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" },
]
[[package]]
name = "asgiref"
version = "3.9.2"
@@ -111,6 +123,34 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/94/fe/3aed5d0be4d404d12d36ab97e2f1791424d9ca39c2f754a6285d59a3b01d/beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515", size = 106392, upload-time = "2025-09-29T10:05:43.771Z" },
]
[[package]]
name = "billiard"
version = "4.2.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b9/6a/1405343016bce8354b29d90aad6b0bf6485b5e60404516e4b9a3a9646cf0/billiard-4.2.2.tar.gz", hash = "sha256:e815017a062b714958463e07ba15981d802dc53d41c5b69d28c5a7c238f8ecf3", size = 155592, upload-time = "2025-09-20T14:44:40.456Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a6/80/ef8dff49aae0e4430f81842f7403e14e0ca59db7bbaf7af41245b67c6b25/billiard-4.2.2-py3-none-any.whl", hash = "sha256:4bc05dcf0d1cc6addef470723aac2a6232f3c7ed7475b0b580473a9145829457", size = 86896, upload-time = "2025-09-20T14:44:39.157Z" },
]
[[package]]
name = "celery"
version = "5.5.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "billiard" },
{ name = "click" },
{ name = "click-didyoumean" },
{ name = "click-plugins" },
{ name = "click-repl" },
{ name = "kombu" },
{ name = "python-dateutil" },
{ name = "vine" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bb/7d/6c289f407d219ba36d8b384b42489ebdd0c84ce9c413875a8aae0c85f35b/celery-5.5.3.tar.gz", hash = "sha256:6c972ae7968c2b5281227f01c3a3f984037d21c5129d07bf3550cc2afc6b10a5", size = 1667144, upload-time = "2025-06-01T11:08:12.563Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c9/af/0dcccc7fdcdf170f9a1585e5e96b6fb0ba1749ef6be8c89a6202284759bd/celery-5.5.3-py3-none-any.whl", hash = "sha256:0b5761a07057acee94694464ca482416b959568904c9dfa41ce8413a7d65d525", size = 438775, upload-time = "2025-06-01T11:08:09.94Z" },
]
[[package]]
name = "certifi"
version = "2025.10.5"
@@ -171,6 +211,64 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" },
]
[[package]]
name = "click"
version = "8.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" },
]
[[package]]
name = "click-didyoumean"
version = "0.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
]
sdist = { url = "https://files.pythonhosted.org/packages/30/ce/217289b77c590ea1e7c24242d9ddd6e249e52c795ff10fac2c50062c48cb/click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", size = 3089, upload-time = "2024-03-24T08:22:07.499Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1b/5b/974430b5ffdb7a4f1941d13d83c64a0395114503cc357c6b9ae4ce5047ed/click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c", size = 3631, upload-time = "2024-03-24T08:22:06.356Z" },
]
[[package]]
name = "click-plugins"
version = "1.1.1.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c3/a4/34847b59150da33690a36da3681d6bbc2ec14ee9a846bc30a6746e5984e4/click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261", size = 8343, upload-time = "2025-06-25T00:47:37.555Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3d/9a/2abecb28ae875e39c8cad711eb1186d8d14eab564705325e77e4e6ab9ae5/click_plugins-1.1.1.2-py2.py3-none-any.whl", hash = "sha256:008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6", size = 11051, upload-time = "2025-06-25T00:47:36.731Z" },
]
[[package]]
name = "click-repl"
version = "0.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "prompt-toolkit" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cb/a2/57f4ac79838cfae6912f997b4d1a64a858fb0c86d7fcaae6f7b58d267fca/click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", size = 10449, upload-time = "2023-06-15T12:43:51.141Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", size = 10289, upload-time = "2023-06-15T12:43:48.626Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "contourpy"
version = "1.3.3"
@@ -226,6 +324,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" },
]
[[package]]
name = "cron-descriptor"
version = "2.0.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7c/31/0b21d1599656b2ffa6043e51ca01041cd1c0f6dacf5a3e2b620ed120e7d8/cron_descriptor-2.0.6.tar.gz", hash = "sha256:e39d2848e1d8913cfb6e3452e701b5eec662ee18bea8cc5aa53ee1a7bb217157", size = 49456, upload-time = "2025-09-03T16:30:22.434Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/21/cc/361326a54ad92e2e12845ad15e335a4e14b8953665007fb514d3393dfb0f/cron_descriptor-2.0.6-py3-none-any.whl", hash = "sha256:3a1c0d837c0e5a32e415f821b36cf758eb92d510e6beff8fbfe4fa16573d93d6", size = 74446, upload-time = "2025-09-03T16:30:21.397Z" },
]
[[package]]
name = "cycler"
version = "0.12.1"
@@ -243,11 +353,14 @@ dependencies = [
{ name = "aiosqlite" },
{ name = "bcrypt" },
{ name = "beautifulsoup4" },
{ name = "celery" },
{ name = "django" },
{ name = "django-admin-interface" },
{ name = "django-admin-multiple-choice-list-filter" },
{ name = "django-admin-rangefilter" },
{ name = "django-autocomplete-light" },
{ name = "django-celery-beat" },
{ name = "django-celery-results" },
{ name = "django-daisy" },
{ name = "django-debug-toolbar" },
{ name = "django-dynamic-raw-id" },
@@ -256,6 +369,7 @@ dependencies = [
{ name = "django-map-widgets" },
{ name = "django-more-admin-filters" },
{ name = "dotenv" },
{ name = "flower" },
{ name = "geopy" },
{ name = "gunicorn" },
{ name = "lxml" },
@@ -278,11 +392,14 @@ requires-dist = [
{ name = "aiosqlite", specifier = ">=0.21.0" },
{ name = "bcrypt", specifier = ">=5.0.0" },
{ name = "beautifulsoup4", specifier = ">=4.14.2" },
{ name = "celery", specifier = ">=5.5.3" },
{ name = "django", specifier = ">=5.2.7" },
{ name = "django-admin-interface", specifier = ">=0.30.1" },
{ name = "django-admin-multiple-choice-list-filter", specifier = ">=0.1.1" },
{ name = "django-admin-rangefilter", specifier = ">=0.13.3" },
{ name = "django-autocomplete-light", specifier = ">=3.12.1" },
{ name = "django-celery-beat", specifier = ">=2.6.0" },
{ name = "django-celery-results", specifier = ">=2.5.1" },
{ name = "django-daisy", specifier = ">=1.1.2" },
{ name = "django-debug-toolbar", specifier = ">=6.0.0" },
{ name = "django-dynamic-raw-id", specifier = ">=4.4" },
@@ -291,6 +408,7 @@ requires-dist = [
{ name = "django-map-widgets", specifier = ">=0.5.1" },
{ name = "django-more-admin-filters", specifier = ">=1.13" },
{ name = "dotenv", specifier = ">=0.9.9" },
{ name = "flower", specifier = ">=2.0.1" },
{ name = "geopy", specifier = ">=2.4.1" },
{ name = "gunicorn", specifier = ">=23.0.0" },
{ name = "lxml", specifier = ">=6.0.2" },
@@ -371,6 +489,36 @@ dependencies = [
]
sdist = { url = "https://files.pythonhosted.org/packages/bc/49/fd4705cd96f313d858f9a8c5fce1c33f1f61adb6b25acc92a924acf0a305/django_autocomplete_light-3.12.1.tar.gz", hash = "sha256:50f7b83681feec6491c38e6114d7a4fe80f9e99a8cc6c8458a3e7bb137ad6b1d", size = 296420, upload-time = "2025-02-26T08:21:16.332Z" }
[[package]]
name = "django-celery-beat"
version = "2.8.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "celery" },
{ name = "cron-descriptor" },
{ name = "django" },
{ name = "django-timezone-field" },
{ name = "python-crontab" },
{ name = "tzdata" },
]
sdist = { url = "https://files.pythonhosted.org/packages/aa/11/0c8b412869b4fda72828572068312b10aafe7ccef7b41af3633af31f9d4b/django_celery_beat-2.8.1.tar.gz", hash = "sha256:dfad0201c0ac50c91a34700ef8fa0a10ee098cc7f3375fe5debed79f2204f80a", size = 175802, upload-time = "2025-05-13T06:58:29.246Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/61/e5/3a0167044773dee989b498e9a851fc1663bea9ab879f1179f7b8a827ac10/django_celery_beat-2.8.1-py3-none-any.whl", hash = "sha256:da2b1c6939495c05a551717509d6e3b79444e114a027f7b77bf3727c2a39d171", size = 104833, upload-time = "2025-05-13T06:58:27.309Z" },
]
[[package]]
name = "django-celery-results"
version = "2.6.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "celery" },
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a6/b5/9966c28e31014c228305e09d48b19b35522a8f941fe5af5f81f40dc8fa80/django_celery_results-2.6.0.tar.gz", hash = "sha256:9abcd836ae6b61063779244d8887a88fe80bbfaba143df36d3cb07034671277c", size = 83985, upload-time = "2025-04-10T08:23:52.677Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/da/70f0f3c5364735344c4bc89e53413bcaae95b4fc1de4e98a7a3b9fb70c88/django_celery_results-2.6.0-py3-none-any.whl", hash = "sha256:b9ccdca2695b98c7cbbb8dea742311ba9a92773d71d7b4944a676e69a7df1c73", size = 38351, upload-time = "2025-04-10T08:23:49.965Z" },
]
[[package]]
name = "django-colorfield"
version = "0.14.0"
@@ -467,6 +615,18 @@ 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" },
]
[[package]]
name = "django-timezone-field"
version = "7.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ba/5b/0dbe271fef3c2274b83dbcb1b19fa3dacf1f7e542382819294644e78ea8b/django_timezone_field-7.1.tar.gz", hash = "sha256:b3ef409d88a2718b566fabe10ea996f2838bc72b22d3a2900c0aa905c761380c", size = 13727, upload-time = "2025-01-11T17:49:54.486Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/09/7a808392a751a24ffa62bec00e3085a9c1a151d728c323a5bab229ea0e58/django_timezone_field-7.1-py3-none-any.whl", hash = "sha256:93914713ed882f5bccda080eda388f7006349f25930b6122e9b07bf8db49c4b4", size = 13177, upload-time = "2025-01-11T17:49:52.142Z" },
]
[[package]]
name = "dotenv"
version = "0.9.9"
@@ -487,6 +647,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" },
]
[[package]]
name = "flower"
version = "2.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "celery" },
{ name = "humanize" },
{ name = "prometheus-client" },
{ name = "pytz" },
{ name = "tornado" },
]
sdist = { url = "https://files.pythonhosted.org/packages/09/a1/357f1b5d8946deafdcfdd604f51baae9de10aafa2908d0b7322597155f92/flower-2.0.1.tar.gz", hash = "sha256:5ab717b979530770c16afb48b50d2a98d23c3e9fe39851dcf6bc4d01845a02a0", size = 3220408, upload-time = "2023-08-13T14:37:46.073Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a6/ff/ee2f67c0ff146ec98b5df1df637b2bc2d17beeb05df9f427a67bd7a7d79c/flower-2.0.1-py2.py3-none-any.whl", hash = "sha256:9db2c621eeefbc844c8dd88be64aef61e84e2deb29b271e02ab2b5b9f01068e2", size = 383553, upload-time = "2023-08-13T14:37:41.552Z" },
]
[[package]]
name = "fonttools"
version = "4.60.1"
@@ -562,6 +738,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "humanize"
version = "4.14.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b6/43/50033d25ad96a7f3845f40999b4778f753c3901a11808a584fed7c00d9f5/humanize-4.14.0.tar.gz", hash = "sha256:2fa092705ea640d605c435b1ca82b2866a1b601cdf96f076d70b79a855eba90d", size = 82939, upload-time = "2025-10-15T13:04:51.214Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c3/5b/9512c5fb6c8218332b530f13500c6ff5f3ce3342f35e0dd7be9ac3856fd3/humanize-4.14.0-py3-none-any.whl", hash = "sha256:d57701248d040ad456092820e6fde56c930f17749956ac47f4f655c0c547bfff", size = 132092, upload-time = "2025-10-15T13:04:49.404Z" },
]
[[package]]
name = "idna"
version = "3.10"
@@ -639,6 +824,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" },
]
[[package]]
name = "kombu"
version = "5.5.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "amqp" },
{ name = "packaging" },
{ name = "tzdata" },
{ name = "vine" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0f/d3/5ff936d8319ac86b9c409f1501b07c426e6ad41966fedace9ef1b966e23f/kombu-5.5.4.tar.gz", hash = "sha256:886600168275ebeada93b888e831352fe578168342f0d1d5833d88ba0d847363", size = 461992, upload-time = "2025-06-01T10:19:22.281Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/70/a07dcf4f62598c8ad579df241af55ced65bed76e42e45d3c368a6d82dbc1/kombu-5.5.4-py3-none-any.whl", hash = "sha256:a12ed0557c238897d8e518f1d1fdf84bd1516c5e305af2dacd85c2015115feb8", size = 210034, upload-time = "2025-06-01T10:19:20.436Z" },
]
[[package]]
name = "lxml"
version = "6.0.2"
@@ -928,6 +1128,27 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" },
]
[[package]]
name = "prometheus-client"
version = "0.23.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/23/53/3edb5d68ecf6b38fcbcc1ad28391117d2a322d9a1a3eff04bfdb184d8c3b/prometheus_client-0.23.1.tar.gz", hash = "sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce", size = 80481, upload-time = "2025-09-18T20:47:25.043Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99", size = 61145, upload-time = "2025-09-18T20:47:23.875Z" },
]
[[package]]
name = "prompt-toolkit"
version = "3.0.52"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "wcwidth" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
]
[[package]]
name = "psycopg"
version = "3.2.10"
@@ -997,6 +1218,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725, upload-time = "2019-09-20T02:06:22.938Z" },
]
[[package]]
name = "python-crontab"
version = "3.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/99/7f/c54fb7e70b59844526aa4ae321e927a167678660ab51dda979955eafb89a/python_crontab-3.3.0.tar.gz", hash = "sha256:007c8aee68dddf3e04ec4dce0fac124b93bd68be7470fc95d2a9617a15de291b", size = 57626, upload-time = "2025-07-13T20:05:35.535Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/47/42/bb4afa5b088f64092036221843fc989b7db9d9d302494c1f8b024ee78a46/python_crontab-3.3.0-py3-none-any.whl", hash = "sha256:739a778b1a771379b75654e53fd4df58e5c63a9279a63b5dfe44c0fcc3ee7884", size = 27533, upload-time = "2025-07-13T20:05:34.266Z" },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
@@ -1251,6 +1481,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" },
]
[[package]]
name = "tornado"
version = "6.5.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/09/ce/1eb500eae19f4648281bb2186927bb062d2438c2e5093d1360391afd2f90/tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0", size = 510821, upload-time = "2025-08-08T18:27:00.78Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f6/48/6a7529df2c9cc12efd2e8f5dd219516184d703b34c06786809670df5b3bd/tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6", size = 442563, upload-time = "2025-08-08T18:26:42.945Z" },
{ url = "https://files.pythonhosted.org/packages/f2/b5/9b575a0ed3e50b00c40b08cbce82eb618229091d09f6d14bce80fc01cb0b/tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef", size = 440729, upload-time = "2025-08-08T18:26:44.473Z" },
{ url = "https://files.pythonhosted.org/packages/1b/4e/619174f52b120efcf23633c817fd3fed867c30bff785e2cd5a53a70e483c/tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e", size = 444295, upload-time = "2025-08-08T18:26:46.021Z" },
{ url = "https://files.pythonhosted.org/packages/95/fa/87b41709552bbd393c85dd18e4e3499dcd8983f66e7972926db8d96aa065/tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882", size = 443644, upload-time = "2025-08-08T18:26:47.625Z" },
{ url = "https://files.pythonhosted.org/packages/f9/41/fb15f06e33d7430ca89420283a8762a4e6b8025b800ea51796ab5e6d9559/tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108", size = 443878, upload-time = "2025-08-08T18:26:50.599Z" },
{ url = "https://files.pythonhosted.org/packages/11/92/fe6d57da897776ad2e01e279170ea8ae726755b045fe5ac73b75357a5a3f/tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c", size = 444549, upload-time = "2025-08-08T18:26:51.864Z" },
{ url = "https://files.pythonhosted.org/packages/9b/02/c8f4f6c9204526daf3d760f4aa555a7a33ad0e60843eac025ccfd6ff4a93/tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4", size = 443973, upload-time = "2025-08-08T18:26:53.625Z" },
{ url = "https://files.pythonhosted.org/packages/ae/2d/f5f5707b655ce2317190183868cd0f6822a1121b4baeae509ceb9590d0bd/tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04", size = 443954, upload-time = "2025-08-08T18:26:55.072Z" },
{ url = "https://files.pythonhosted.org/packages/e8/59/593bd0f40f7355806bf6573b47b8c22f8e1374c9b6fd03114bd6b7a3dcfd/tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0", size = 445023, upload-time = "2025-08-08T18:26:56.677Z" },
{ url = "https://files.pythonhosted.org/packages/c7/2a/f609b420c2f564a748a2d80ebfb2ee02a73ca80223af712fca591386cafb/tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f", size = 445427, upload-time = "2025-08-08T18:26:57.91Z" },
{ url = "https://files.pythonhosted.org/packages/5e/4f/e1f65e8f8c76d73658b33d33b81eed4322fb5085350e4328d5c956f0c8f9/tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", size = 444456, upload-time = "2025-08-08T18:26:59.207Z" },
]
[[package]]
name = "trio"
version = "0.32.0"
@@ -1314,6 +1563,24 @@ socks = [
{ name = "pysocks" },
]
[[package]]
name = "vine"
version = "5.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bd/e4/d07b5f29d283596b9727dd5275ccbceb63c44a1a82aa9e4bfd20426762ac/vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0", size = 48980, upload-time = "2023-11-05T08:46:53.857Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" },
]
[[package]]
name = "wcwidth"
version = "0.2.14"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" },
]
[[package]]
name = "websocket-client"
version = "1.9.0"