Compare commits
8 Commits
178854c6ba
...
331a9e41cb
| Author | SHA1 | Date | |
|---|---|---|---|
| 331a9e41cb | |||
| 439ca6407f | |||
| c8a951eac6 | |||
| e01785fa53 | |||
| 78c46a2751 | |||
| 6df48deb3c | |||
| 20a13414de | |||
| 94df5171db |
1
.gitignore
vendored
@@ -17,3 +17,4 @@ django-leaflet
|
||||
admin-interface
|
||||
|
||||
docker-*
|
||||
maplibre-gl-js-5.10.0.zip
|
||||
@@ -18,34 +18,26 @@ RUN apt-get update && apt-get install -y \
|
||||
postgresql-client \
|
||||
build-essential \
|
||||
libpq-dev \
|
||||
gcc \
|
||||
g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Python dependencies for GDAL
|
||||
RUN pip install --upgrade pip && \
|
||||
pip install --no-cache-dir GDAL==$(gdal-config --version)
|
||||
|
||||
# Set work directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy project requirements
|
||||
# Copy project files
|
||||
COPY pyproject.toml uv.lock ./
|
||||
|
||||
# Install uv package manager
|
||||
RUN pip install --upgrade pip && pip install uv
|
||||
# Install uv and dependencies
|
||||
RUN pip install --no-cache-dir uv && \
|
||||
uv sync --frozen --no-dev
|
||||
|
||||
# Install dependencies using uv
|
||||
RUN uv pip install --system --no-cache-dir -r uv.lock
|
||||
|
||||
# Copy project
|
||||
# Copy project code (после установки зависимостей для лучшего кэширования)
|
||||
COPY . .
|
||||
|
||||
# Collect static files
|
||||
RUN python manage.py collectstatic --noinput
|
||||
RUN uv run manage.py collectstatic --noinput
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
# Run gunicorn server
|
||||
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "dbapp.wsgi:application"]
|
||||
CMD [".venv/bin/gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "dbapp.wsgi:application"]
|
||||
@@ -72,6 +72,7 @@ INSTALLED_APPS = [
|
||||
|
||||
MIDDLEWARE = [
|
||||
"debug_toolbar.middleware.DebugToolbarMiddleware", #Добавил
|
||||
'django.middleware.locale.LocaleMiddleware', #Добавил
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware', #Добавил
|
||||
@@ -149,6 +150,11 @@ USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
# Authentication settings
|
||||
LOGIN_URL = 'login'
|
||||
LOGIN_REDIRECT_URL = 'home'
|
||||
LOGOUT_REDIRECT_URL = 'home'
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
||||
@@ -163,21 +169,6 @@ STATICFILES_DIRS = [
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
# DAISY_SETTINGS = {
|
||||
# 'SITE_TITLE': 'Geo admin', # The title of the site
|
||||
# 'SITE_HEADER': 'GEO', # Header text displayed in the admin panel
|
||||
# 'INDEX_TITLE': 'Заголовок', # The title for the index page of dashboard
|
||||
# 'SITE_LOGO': '/static/admin/img/icon-clock.svg', # Path to the logo image displayed in the sidebar
|
||||
# 'EXTRA_STYLES': [], # List of extra stylesheets to be loaded in base.html (optional)
|
||||
# 'EXTRA_SCRIPTS': [], # List of extra script URLs to be loaded in base.html (optional)
|
||||
# 'LOAD_FULL_STYLES': False, # If True, loads full DaisyUI components in the admin (useful if you have custom template overrides)
|
||||
# 'SHOW_CHANGELIST_FILTER': False, # If True, the filter sidebar will open by default on changelist views
|
||||
# 'DONT_SUPPORT_ME': True, # Hide github link in sidebar footer
|
||||
# 'SIDEBAR_FOOTNOTE': 'Что-то о как', # add footnote to sidebar
|
||||
# 'DEFAULT_THEME': None, # Set a default theme (e.g., 'corporate', 'dark', 'light')
|
||||
# 'DEFAULT_THEME_DARK': None, # Set a default dark theme when system prefers dark mode
|
||||
# 'SHOW_THEME_SELECTOR': True, # If False, hides the theme selector dropdown entirely
|
||||
# }
|
||||
|
||||
# AUTH_USER_MODEL = 'mainapp.CustomUser'
|
||||
X_FRAME_OPTIONS = "SAMEORIGIN"
|
||||
|
||||
@@ -17,6 +17,7 @@ Including another URLconf
|
||||
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 = [
|
||||
@@ -24,5 +25,8 @@ urlpatterns = [
|
||||
path('admin/', admin.site.urls, name='admin'),
|
||||
# path('admin/map/', views.show_map_view, name='admin_show_map'),
|
||||
path('', include('mainapp.urls')),
|
||||
path('', include('mapsapp.urls'))
|
||||
path('', include('mapsapp.urls')),
|
||||
# Authentication URLs
|
||||
path('login/', auth_views.LoginView.as_view(), name='login'),
|
||||
path('logout/', views.custom_logout, name='logout'),
|
||||
] + debug_toolbar_urls()
|
||||
|
||||
0
dbapp/lyngsatapp/__init__.py
Normal file
8
dbapp/lyngsatapp/admin.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.contrib import admin
|
||||
from .models import LyngSat
|
||||
|
||||
@admin.register(LyngSat)
|
||||
class LyngSatAdmin(admin.ModelAdmin):
|
||||
list_display = ("mark", "timestamp")
|
||||
search_fields = ("mark", )
|
||||
ordering = ("timestamp",)
|
||||
6
dbapp/lyngsatapp/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class LyngsatappConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'lyngsatapp'
|
||||
0
dbapp/lyngsatapp/migrations/__init__.py
Normal file
36
dbapp/lyngsatapp/models.py
Normal file
@@ -0,0 +1,36 @@
|
||||
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="Описание источника")
|
||||
# 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"
|
||||
|
||||
|
||||
371
dbapp/lyngsatapp/parser.py
Normal file
@@ -0,0 +1,371 @@
|
||||
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) -> list[str]:
|
||||
html_regions = []
|
||||
for region in self.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_satellites_data(self) -> dict[dict]:
|
||||
sat_data = {}
|
||||
for region_page in self.get_region_pages():
|
||||
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
|
||||
3
dbapp/lyngsatapp/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
3
dbapp/lyngsatapp/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
@@ -6,6 +6,7 @@ from .models import (
|
||||
Standard,
|
||||
SigmaParMark,
|
||||
SigmaParameter,
|
||||
SourceType,
|
||||
Parameter,
|
||||
Satellite,
|
||||
Mirror,
|
||||
@@ -22,6 +23,7 @@ from django.contrib.gis.db import models as gis
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from leaflet.forms.widgets import LeafletWidget
|
||||
|
||||
from rangefilter.filters import (
|
||||
DateRangeFilterBuilder,
|
||||
@@ -38,10 +40,17 @@ from .filters import GeoKupDistanceFilter, GeoValidDistanceFilter, UniqueToggleF
|
||||
admin.site.site_title = "Геолокация"
|
||||
admin.site.site_header = "Geolocation"
|
||||
admin.site.index_title = "Geo"
|
||||
# Unregister default User and Group since we're customizing them
|
||||
admin.site.unregister(User)
|
||||
admin.site.unregister(Group)
|
||||
|
||||
|
||||
class CustomUserInline(admin.StackedInline):
|
||||
model = CustomUser
|
||||
can_delete = False
|
||||
verbose_name_plural = 'Дополнительная информация пользователя'
|
||||
|
||||
|
||||
class LocationForm(forms.ModelForm):
|
||||
latitude_geo = forms.FloatField(required=False, label="Широта")
|
||||
longitude_geo = forms.FloatField(required=False, label="Долгота")
|
||||
@@ -89,18 +98,30 @@ class LocationForm(forms.ModelForm):
|
||||
return instance
|
||||
|
||||
|
||||
|
||||
|
||||
class CustomUserInline(admin.StackedInline):
|
||||
model = CustomUser
|
||||
can_delete = False
|
||||
verbose_name_plural = 'Дополнительная информация пользователя'
|
||||
|
||||
|
||||
@admin.register(CustomUser)
|
||||
class CustomUserAdmin(admin.ModelAdmin):
|
||||
list_display = ('user', 'role')
|
||||
list_filter = ('role',)
|
||||
class GeoInline(admin.StackedInline):
|
||||
model = Geo
|
||||
extra = 0
|
||||
verbose_name = "Гео"
|
||||
verbose_name_plural = "Гео"
|
||||
form = LocationForm
|
||||
readonly_fields = ("distance_coords_kup", "distance_coords_valid", "distance_kup_valid")
|
||||
prefetch_related = ("mirrors",)
|
||||
autocomplete_fields = ('mirrors',)
|
||||
fieldsets = (
|
||||
("Основная информация", {
|
||||
"fields": ("mirrors", "location", "distance_coords_kup",
|
||||
"distance_coords_valid", "distance_kup_valid", "timestamp", "comment",)
|
||||
}),
|
||||
("Координаты: геолокация", {
|
||||
"fields": ("longitude_geo", "latitude_geo", "coords"),
|
||||
}),
|
||||
("Координаты: Кубсат", {
|
||||
"fields": ("longitude_kupsat", "latitude_kupsat", "coords_kupsat"),
|
||||
}),
|
||||
("Координаты: Оперативный отдел", {
|
||||
"fields": ("longitude_valid", "latitude_valid", "coords_valid"),
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
class UserAdmin(BaseUserAdmin):
|
||||
@@ -108,6 +129,13 @@ class UserAdmin(BaseUserAdmin):
|
||||
|
||||
admin.site.register(User, UserAdmin)
|
||||
|
||||
|
||||
# @admin.register(CustomUser)
|
||||
# class CustomUserAdmin(admin.ModelAdmin):
|
||||
# list_display = ('user', 'role')
|
||||
# list_filter = ('role',)
|
||||
# raw_id_fields = ('user',) # For better performance with large number of users
|
||||
|
||||
@admin.register(SigmaParMark)
|
||||
class SigmaParMarkAdmin(admin.ModelAdmin):
|
||||
list_display = ("mark", "timestamp")
|
||||
@@ -128,6 +156,12 @@ class ModulationAdmin(admin.ModelAdmin):
|
||||
search_fields = ("name",)
|
||||
ordering = ("name",)
|
||||
|
||||
@admin.register(SourceType)
|
||||
class SourceTypeAdmin(admin.ModelAdmin):
|
||||
list_display = ("name",)
|
||||
search_fields = ("name",)
|
||||
ordering = ("name",)
|
||||
|
||||
|
||||
@admin.register(Standard)
|
||||
class StandardAdmin(admin.ModelAdmin):
|
||||
@@ -161,15 +195,6 @@ class ParameterAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
|
||||
"standard",
|
||||
"sigma_parameter"
|
||||
)
|
||||
# fields = ( "id_satellite",
|
||||
# "frequency",
|
||||
# "freq_range",
|
||||
# "polarization",
|
||||
# "modulation",
|
||||
# "bod_velocity",
|
||||
# "snr",
|
||||
# "standard",
|
||||
# "id_sigma_parameter")
|
||||
list_display_links = ("frequency", "id_satellite", )
|
||||
list_filter = (
|
||||
HasSigmaParameterFilter,
|
||||
@@ -193,6 +218,7 @@ class ParameterAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
|
||||
)
|
||||
ordering = ("frequency",)
|
||||
list_select_related = ("polarization", "modulation", "standard", "id_satellite",)
|
||||
autocomplete_fields = ('objitems',)
|
||||
# raw_id_fields = ("id_sigma_parameter", )
|
||||
inlines = [SigmaParameterInline]
|
||||
# autocomplete_fields = ("id_sigma_parameter", )
|
||||
@@ -209,23 +235,25 @@ class ParameterAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
|
||||
class SigmaParameterAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
|
||||
list_display = (
|
||||
"id_satellite",
|
||||
"status",
|
||||
# "status",
|
||||
"frequency",
|
||||
"transfer_frequency",
|
||||
"freq_range",
|
||||
"power",
|
||||
# "power",
|
||||
"polarization",
|
||||
"modulation",
|
||||
"bod_velocity",
|
||||
"snr",
|
||||
"standard",
|
||||
# "standard",
|
||||
"parameter",
|
||||
"packets",
|
||||
# "packets",
|
||||
"datetime_begin",
|
||||
"datetime_end",
|
||||
)
|
||||
readonly_fields = (
|
||||
"datetime_begin",
|
||||
"datetime_end",
|
||||
|
||||
"transfer_frequency"
|
||||
)
|
||||
list_display_links = ("id_satellite",)
|
||||
list_filter = (
|
||||
@@ -272,7 +300,7 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin):
|
||||
fieldsets = (
|
||||
("Основная информация", {
|
||||
"fields": ("mirrors", "location", "distance_coords_kup",
|
||||
"distance_coords_valid", "distance_kup_valid", "timestamp", "comment", "id_user_add")
|
||||
"distance_coords_valid", "distance_kup_valid", "timestamp", "comment",)
|
||||
}),
|
||||
("Координаты: геолокация", {
|
||||
"fields": ("longitude_geo", "latitude_geo", "coords"),
|
||||
@@ -300,7 +328,6 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin):
|
||||
"is_average",
|
||||
("location", MultiSelectDropdownFilter),
|
||||
("timestamp", DateRangeQuickSelectListFilterBuilder()),
|
||||
("id_user_add", MultiSelectRelatedDropdownFilter),
|
||||
)
|
||||
search_fields = (
|
||||
"mirrors__name",
|
||||
@@ -309,7 +336,6 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin):
|
||||
"coords_kupsat",
|
||||
"coords_valid"
|
||||
)
|
||||
list_select_related = ("id_user_add", )
|
||||
prefetch_related = ("mirrors", )
|
||||
|
||||
|
||||
@@ -369,6 +395,26 @@ def show_on_map(modeladmin, request, queryset):
|
||||
|
||||
show_on_map.short_description = "Показать выбранные на карте"
|
||||
|
||||
|
||||
def show_selected_on_map(modeladmin, request, queryset):
|
||||
# Получаем список ID выбранных объектов
|
||||
selected_ids = queryset.values_list('id', flat=True)
|
||||
# Формируем строку вида "1,2,3"
|
||||
ids_str = ','.join(str(pk) for pk in selected_ids)
|
||||
# Перенаправляем на view, который будет отображать карту с выбранными объектами
|
||||
return redirect(reverse('show_selected_objects_map') + f'?ids={ids_str}')
|
||||
|
||||
show_selected_on_map.short_description = "Показать выбранные объекты на карте"
|
||||
show_selected_on_map.icon = 'map'
|
||||
|
||||
class ParameterObjItemInline(admin.StackedInline):
|
||||
model = ObjItem.parameters_obj.through
|
||||
extra = 0
|
||||
max_num = 1
|
||||
verbose_name = "ВЧ загрузка"
|
||||
verbose_name_plural = "ВЧ загрузки"
|
||||
|
||||
|
||||
@admin.register(ObjItem)
|
||||
class ObjectAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
@@ -386,120 +432,159 @@ class ObjectAdmin(admin.ModelAdmin):
|
||||
"distance_geo_kup",
|
||||
"distance_geo_valid",
|
||||
"distance_kup_valid",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
list_display_links = ("name",)
|
||||
list_filter = (
|
||||
UniqueToggleFilter,
|
||||
("id_vch_load__id_satellite", MultiSelectRelatedDropdownFilter),
|
||||
("id_vch_load__frequency", NumericRangeFilterBuilder()),
|
||||
("id_vch_load__freq_range", NumericRangeFilterBuilder()),
|
||||
("id_vch_load__snr", NumericRangeFilterBuilder()),
|
||||
("id_vch_load__modulation", MultiSelectRelatedDropdownFilter),
|
||||
("id_vch_load__polarization", MultiSelectRelatedDropdownFilter),
|
||||
("parameters_obj__id_satellite", MultiSelectRelatedDropdownFilter),
|
||||
("parameters_obj__frequency", NumericRangeFilterBuilder()),
|
||||
("parameters_obj__freq_range", NumericRangeFilterBuilder()),
|
||||
("parameters_obj__snr", NumericRangeFilterBuilder()),
|
||||
("parameters_obj__modulation", MultiSelectRelatedDropdownFilter),
|
||||
("parameters_obj__polarization", MultiSelectRelatedDropdownFilter),
|
||||
GeoKupDistanceFilter,
|
||||
GeoValidDistanceFilter
|
||||
)
|
||||
search_fields = (
|
||||
"name",
|
||||
# "id_geo",
|
||||
# "id_satellite__name",
|
||||
# "id_vch_load__frequency",
|
||||
"geo_obj__coords",
|
||||
"parameters_obj__frequency",
|
||||
)
|
||||
|
||||
ordering = ("name",)
|
||||
list_select_related = (
|
||||
# "id_satellite",
|
||||
"id_vch_load",
|
||||
"id_vch_load__polarization",
|
||||
"id_vch_load__modulation",
|
||||
"id_vch_load__id_satellite",
|
||||
"id_geo",
|
||||
inlines = [ParameterObjItemInline, GeoInline]
|
||||
actions = [show_on_map, show_selected_on_map]
|
||||
readonly_fields = ('created_at', 'created_by', 'updated_at', 'updated_by')
|
||||
|
||||
def get_queryset(self, request):
|
||||
qs = super().get_queryset(request)
|
||||
return qs.select_related('geo_obj', 'created_by', 'updated_by').prefetch_related(
|
||||
'parameters_obj__id_satellite',
|
||||
'parameters_obj__polarization',
|
||||
'parameters_obj__modulation',
|
||||
'parameters_obj__standard'
|
||||
)
|
||||
autocomplete_fields = ("id_geo",)
|
||||
raw_id_fields = ("id_vch_load",)
|
||||
# dynamic_raw_id_fields = ("id_vch_load",)
|
||||
actions = [show_on_map]
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
return self.readonly_fields
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
if not change:
|
||||
if not obj.created_by_id:
|
||||
obj.created_by = request.user.customuser if hasattr(request.user, 'customuser') else None
|
||||
obj.updated_by = request.user.customuser if hasattr(request.user, 'customuser') else None
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
def sat_name(self, obj):
|
||||
return obj.id_vch_load.id_satellite
|
||||
param = next(iter(obj.parameters_obj.all()), None)
|
||||
if param and param.id_satellite:
|
||||
return param.id_satellite.name
|
||||
return "-"
|
||||
sat_name.short_description = "Спутник"
|
||||
sat_name.admin_order_field = "parameters_obj__id_satellite__name"
|
||||
|
||||
def freq(self, obj):
|
||||
par = obj.id_vch_load
|
||||
return par.frequency
|
||||
# param = obj.parameters_obj.first()
|
||||
param = next(iter(obj.parameters_obj.all()), None)
|
||||
if param:
|
||||
return param.frequency
|
||||
return "-"
|
||||
freq.short_description = "Частота, МГц"
|
||||
freq.admin_order_field = "parameters_obj__frequency"
|
||||
|
||||
def distance_geo_kup(self, obj):
|
||||
par = obj.id_geo.distance_coords_kup
|
||||
if par is None:
|
||||
geo = obj.geo_obj
|
||||
if not geo or geo.distance_coords_kup is None:
|
||||
return "-"
|
||||
return round(par, 3)
|
||||
return round(geo.distance_coords_kup, 3)
|
||||
distance_geo_kup.short_description = "Гео-куб, км"
|
||||
|
||||
def distance_geo_valid(self, obj):
|
||||
par = obj.id_geo.distance_coords_valid
|
||||
if par is None:
|
||||
geo = obj.geo_obj
|
||||
if not geo or geo.distance_coords_valid is None:
|
||||
return "-"
|
||||
return round(par, 3)
|
||||
return round(geo.distance_coords_valid, 3)
|
||||
distance_geo_valid.short_description = "Гео-опер, км"
|
||||
|
||||
def distance_kup_valid(self, obj):
|
||||
par = obj.id_geo.distance_kup_valid
|
||||
if par is None:
|
||||
geo = obj.geo_obj
|
||||
if not geo or geo.distance_kup_valid is None:
|
||||
return "-"
|
||||
return round(par, 3)
|
||||
return round(geo.distance_kup_valid, 3)
|
||||
distance_kup_valid.short_description = "Куб-опер, км"
|
||||
|
||||
def pol(self, obj):
|
||||
par = obj.id_vch_load.polarization
|
||||
return par.name
|
||||
# Get the first parameter associated with this objitem to display polarization
|
||||
param = next(iter(obj.parameters_obj.all()), None)
|
||||
if param and param.polarization:
|
||||
return param.polarization.name
|
||||
return "-"
|
||||
pol.short_description = "Поляризация"
|
||||
|
||||
def freq_range(self, obj):
|
||||
par = obj.id_vch_load
|
||||
return par.freq_range
|
||||
# Get the first parameter associated with this objitem to display freq_range
|
||||
param = next(iter(obj.parameters_obj.all()), None)
|
||||
if param:
|
||||
return param.freq_range
|
||||
return "-"
|
||||
freq_range.short_description = "Полоса, МГц"
|
||||
freq_range.admin_order_field = "parameters_obj__freq_range"
|
||||
|
||||
def bod_velocity(self, obj):
|
||||
par = obj.id_vch_load
|
||||
return par.bod_velocity
|
||||
# Get the first parameter associated with this objitem to display bod_velocity
|
||||
param = next(iter(obj.parameters_obj.all()), None)
|
||||
if param:
|
||||
return param.bod_velocity
|
||||
return "-"
|
||||
bod_velocity.short_description = "Сим. v, БОД"
|
||||
|
||||
def modulation(self, obj):
|
||||
par = obj.id_vch_load.modulation
|
||||
return par.name
|
||||
# Get the first parameter associated with this objitem to display modulation
|
||||
param = next(iter(obj.parameters_obj.all()), None)
|
||||
if param and param.modulation:
|
||||
return param.modulation.name
|
||||
return "-"
|
||||
modulation.short_description = "Модуляция"
|
||||
|
||||
def snr(self, obj):
|
||||
par = obj.id_vch_load
|
||||
return par.snr
|
||||
# Get the first parameter associated with this objitem to display snr
|
||||
param = next(iter(obj.parameters_obj.all()), None)
|
||||
if param:
|
||||
return param.snr
|
||||
return "-"
|
||||
snr.short_description = "ОСШ"
|
||||
|
||||
def geo_coords(self, obj):
|
||||
geo = obj.id_geo
|
||||
geo = obj.geo_obj
|
||||
if not geo or not geo.coords:
|
||||
return "-"
|
||||
longitude = geo.coords.coords[0]
|
||||
latitude = geo.coords.coords[1]
|
||||
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||
return f"{lat} {lon}"
|
||||
geo_coords.short_description = "Координаты геолокации"
|
||||
geo_coords.admin_order_field = "geo_obj__coords"
|
||||
|
||||
def kupsat_coords(self, obj):
|
||||
obj = obj.id_geo
|
||||
if obj.coords_kupsat is None:
|
||||
geo = obj.geo_obj
|
||||
if not geo or not geo.coords_kupsat:
|
||||
return "-"
|
||||
longitude = obj.coords_kupsat.coords[0]
|
||||
latitude = obj.coords_kupsat.coords[1]
|
||||
longitude = geo.coords_kupsat.coords[0]
|
||||
latitude = geo.coords_kupsat.coords[1]
|
||||
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||
return f"{lat} {lon}"
|
||||
kupsat_coords.short_description = "Координаты Кубсата"
|
||||
|
||||
def valid_coords(self, obj):
|
||||
obj = obj.id_geo
|
||||
if obj.coords_valid is None:
|
||||
geo = obj.geo_obj
|
||||
if not geo or not geo.coords_valid:
|
||||
return "-"
|
||||
longitude = obj.coords_valid.coords[0]
|
||||
latitude = obj.coords_valid.coords[1]
|
||||
longitude = geo.coords_valid.coords[0]
|
||||
latitude = geo.coords_valid.coords[1]
|
||||
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||
return f"{lat} {lon}"
|
||||
|
||||
@@ -4,3 +4,6 @@ from django.apps import AppConfig
|
||||
class MainappConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'mainapp'
|
||||
|
||||
def ready(self):
|
||||
import mainapp.signals # noqa
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
from django import forms
|
||||
from .models import Satellite
|
||||
from .models import Satellite, Polarization, ObjItem, Parameter, Geo, Modulation, Standard
|
||||
|
||||
class UploadFileForm(forms.Form):
|
||||
file = forms.FileField(
|
||||
label="Выберите файл",
|
||||
widget=forms.FileInput(attrs={
|
||||
'class': 'form-file-input'
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
class LoadExcelData(forms.Form):
|
||||
file = forms.FileField(
|
||||
@@ -33,7 +42,7 @@ class LoadCsvData(forms.Form):
|
||||
})
|
||||
)
|
||||
|
||||
class UploadFileForm(forms.Form):
|
||||
class UploadVchLoad(UploadFileForm):
|
||||
sat_choice = forms.ModelChoiceField(
|
||||
queryset=Satellite.objects.all(),
|
||||
label="Выберите спутник",
|
||||
@@ -41,12 +50,7 @@ class UploadFileForm(forms.Form):
|
||||
'class': 'form-select'
|
||||
})
|
||||
)
|
||||
file = forms.FileField(
|
||||
label="Выберите текстовый файл",
|
||||
widget=forms.FileInput(attrs={
|
||||
'class': 'form-file-input'
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
class VchLinkForm(forms.Form):
|
||||
sat_choice = forms.ModelChoiceField(
|
||||
@@ -56,12 +60,12 @@ class VchLinkForm(forms.Form):
|
||||
'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='Выбор диапазона'
|
||||
)
|
||||
# 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={
|
||||
@@ -76,3 +80,69 @@ class VchLinkForm(forms.Form):
|
||||
'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 ParameterForm(forms.ModelForm):
|
||||
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'}, choices=[]),
|
||||
'frequency': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}),
|
||||
'freq_range': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}),
|
||||
'bod_velocity': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}),
|
||||
'snr': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}),
|
||||
'polarization': forms.Select(attrs={'class': 'form-select'}, choices=[]),
|
||||
'modulation': forms.Select(attrs={'class': 'form-select'}, choices=[]),
|
||||
'standard': forms.Select(attrs={'class': 'form-select'}, choices=[]),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['id_satellite'].choices = [(s.id, s.name) for s in Satellite.objects.all()]
|
||||
self.fields['polarization'].choices = [(p.id, p.name) for p in Polarization.objects.all()]
|
||||
self.fields['modulation'].choices = [(m.id, m.name) for m in Modulation.objects.all()]
|
||||
self.fields['standard'].choices = [(s.id, s.name) for s in Standard.objects.all()]
|
||||
|
||||
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):
|
||||
class Meta:
|
||||
model = ObjItem
|
||||
fields = ['name']
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
}
|
||||
20
dbapp/mainapp/management/commands/create_user_profiles.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.contrib.auth.models import User
|
||||
from mainapp.models import CustomUser
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Create CustomUser profiles for existing users who do not have them'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# Find all users who don't have a CustomUser profile
|
||||
for user in User.objects.all():
|
||||
if not hasattr(user, 'customuser'):
|
||||
custom_user = CustomUser.objects.create(user=user)
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f'Created CustomUser for {user.username}')
|
||||
)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS('Successfully ensured all users have CustomUser profiles')
|
||||
)
|
||||
@@ -1,7 +1,9 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-13 12:47
|
||||
# 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
|
||||
@@ -31,7 +33,7 @@ class Migration(migrations.Migration):
|
||||
name='Modulation',
|
||||
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='Модуляция')),
|
||||
('name', models.CharField(db_index=True, max_length=20, unique=True, verbose_name='Модуляция')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Модуляция',
|
||||
@@ -53,7 +55,7 @@ class Migration(migrations.Migration):
|
||||
name='Satellite',
|
||||
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='Имя спутника')),
|
||||
('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={
|
||||
@@ -61,6 +63,18 @@ class Migration(migrations.Migration):
|
||||
'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=[
|
||||
@@ -85,35 +99,30 @@ class Migration(migrations.Migration):
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Geo',
|
||||
name='ObjItem',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('timestamp', models.DateTimeField(blank=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='Усреднённое')),
|
||||
('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='Зеркала')),
|
||||
('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': 'Гео',
|
||||
'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, default=0, null=True, verbose_name='Частота, МГц')),
|
||||
('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={
|
||||
@@ -122,26 +131,74 @@ class Migration(migrations.Migration):
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ObjItem',
|
||||
name='SourceType',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(blank=True, max_length=100, null=True, verbose_name='Имя объекта')),
|
||||
('id_geo', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='objitems', to='mainapp.geo', 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='Пользователь')),
|
||||
('id_vch_load', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='objitems', to='mainapp.parameter', verbose_name='ВЧ загрузка')),
|
||||
('id_satellite', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='objitems', to='mainapp.satellite', verbose_name='Спутник')),
|
||||
('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': 'Объекты',
|
||||
'verbose_name': 'Тип источника',
|
||||
'verbose_name_plural': 'Типы источников',
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='geo',
|
||||
constraint=models.UniqueConstraint(fields=('timestamp', 'coords'), name='unique_geo_combination'),
|
||||
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.AddConstraint(
|
||||
model_name='objitem',
|
||||
constraint=models.UniqueConstraint(fields=('id_vch_load', 'id_geo'), name='unique_objitem_combination'),
|
||||
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'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-15 09:23
|
||||
|
||||
import django.contrib.gis.db.models.functions
|
||||
import django.db.models.expressions
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
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='Расстояние между купсатом и гео'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +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='Изменен пользователем'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +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='Дата последнего изменения'),
|
||||
),
|
||||
]
|
||||
@@ -1,30 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-15 09:43
|
||||
|
||||
import django.contrib.gis.db.models.functions
|
||||
import django.db.models.expressions
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0002_geo_distance_coords_kup'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='geo',
|
||||
name='distance_coords_valid',
|
||||
field=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='Расстояние между гео и оперативным отделом, км'),
|
||||
),
|
||||
migrations.AddField(
|
||||
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='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='Расстояние между купсатом и гео, км'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +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',
|
||||
),
|
||||
]
|
||||
@@ -1,36 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-16 12:50
|
||||
|
||||
import django.db.models.deletion
|
||||
import mainapp.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0003_geo_distance_coords_valid_geo_distance_kup_valid_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SigmaParameter',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('status', models.CharField(blank=True, max_length=20, null=True, verbose_name='Статус')),
|
||||
('frequency', models.FloatField(blank=True, default=0, null=True, verbose_name='Частота, МГц')),
|
||||
('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='Время окончания измерения')),
|
||||
('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='Модуляция')),
|
||||
('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',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,20 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-20 07:57
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0004_sigmaparameter'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='sigmaparameter',
|
||||
name='id_satellite',
|
||||
field=models.ForeignKey(default=2, on_delete=django.db.models.deletion.PROTECT, related_name='sigmapar_sat', to='mainapp.satellite', verbose_name='Спутник'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-20 11:57
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0005_sigmaparameter_id_satellite'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='objitem',
|
||||
name='id_satellite',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='parameter',
|
||||
name='id_satellite',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='parameters', to='mainapp.satellite', verbose_name='Спутник'),
|
||||
),
|
||||
]
|
||||
@@ -1,19 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-20 11:58
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0006_remove_objitem_id_satellite_parameter_id_satellite'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='parameter',
|
||||
name='id_satellite',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='parameters', to='mainapp.satellite', verbose_name='Спутник'),
|
||||
),
|
||||
]
|
||||
@@ -1,38 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-22 11:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0007_alter_parameter_id_satellite'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='geo',
|
||||
name='timestamp',
|
||||
field=models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='Время'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='objitem',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=100, null=True, verbose_name='Имя объекта'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='parameter',
|
||||
name='frequency',
|
||||
field=models.FloatField(blank=True, db_index=True, default=0, null=True, verbose_name='Частота, МГц'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='satellite',
|
||||
name='name',
|
||||
field=models.CharField(db_index=True, max_length=30, unique=True, verbose_name='Имя спутника'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparameter',
|
||||
name='frequency',
|
||||
field=models.FloatField(blank=True, db_index=True, default=0, null=True, verbose_name='Частота, МГц'),
|
||||
),
|
||||
]
|
||||
@@ -1,19 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-22 12:08
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0008_alter_geo_timestamp_alter_objitem_name_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='parameter',
|
||||
name='id_sigma_parameter',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sigma_parameter', to='mainapp.sigmaparameter', verbose_name='ВЧ с sigma'),
|
||||
),
|
||||
]
|
||||
@@ -1,31 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-22 12:38
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0009_parameter_id_sigma_parameter'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
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.AddField(
|
||||
model_name='sigmaparameter',
|
||||
name='mark',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='mainapp.sigmaparmark', verbose_name='Отметка'),
|
||||
),
|
||||
]
|
||||
@@ -1,22 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-22 13:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0010_sigmaparmark_sigmaparameter_mark'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='sigmaparameter',
|
||||
name='mark',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sigmaparameter',
|
||||
name='mark',
|
||||
field=models.ManyToManyField(blank=True, null=True, to='mainapp.sigmaparmark', verbose_name='Отметка'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-22 13:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0011_remove_sigmaparameter_mark_sigmaparameter_mark'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparameter',
|
||||
name='mark',
|
||||
field=models.ManyToManyField(blank=True, to='mainapp.sigmaparmark', verbose_name='Отметка'),
|
||||
),
|
||||
]
|
||||
@@ -1,21 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-22 13:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0012_alter_sigmaparameter_mark'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
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'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-23 08:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0013_parameter_mainapp_par_id_sate_cbfab2_idx_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='modulation',
|
||||
name='name',
|
||||
field=models.CharField(db_index=True, max_length=20, unique=True, verbose_name='Модуляция'),
|
||||
),
|
||||
]
|
||||
@@ -1,19 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-23 09:40
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0014_alter_modulation_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='parameter',
|
||||
name='id_sigma_parameter',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sigma_parameter', to='mainapp.sigmaparameter', verbose_name='ВЧ с sigma'),
|
||||
),
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-23 09:58
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0015_alter_parameter_id_sigma_parameter'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='parameter',
|
||||
name='id_sigma_parameter',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sigmaparameter',
|
||||
name='parameter',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sigma_parameter', to='mainapp.parameter', verbose_name='ВЧ с sigma'),
|
||||
),
|
||||
]
|
||||
@@ -1,19 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-23 12:52
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0016_remove_parameter_id_sigma_parameter_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='sigmaparameter',
|
||||
name='parameter',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sigma_parameter', to='mainapp.parameter', verbose_name='ВЧ'),
|
||||
),
|
||||
]
|
||||
@@ -2,6 +2,8 @@ from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.gis.db import models as gis
|
||||
from django.contrib.gis.db.models import functions
|
||||
from django.db.models import F, ExpressionWrapper
|
||||
from django.utils import timezone
|
||||
|
||||
def get_default_polarization():
|
||||
obj, created = Polarization.objects.get_or_create(
|
||||
@@ -96,7 +98,7 @@ class Standard(models.Model):
|
||||
|
||||
|
||||
class Satellite(models.Model):
|
||||
name = models.CharField(max_length=30, unique=True, verbose_name="Имя спутника", db_index=True)
|
||||
name = models.CharField(max_length=100, unique=True, verbose_name="Имя спутника", db_index=True)
|
||||
norad = models.IntegerField(blank=True, null=True, verbose_name="NORAD ID")
|
||||
|
||||
def __str__(self):
|
||||
@@ -107,6 +109,47 @@ class Satellite(models.Model):
|
||||
verbose_name_plural = "Спутники"
|
||||
|
||||
|
||||
class ObjItem(models.Model):
|
||||
name = models.CharField(null=True, blank=True, max_length=100, verbose_name="Имя объекта", db_index=True)
|
||||
# id_satellite = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="objitems", verbose_name="Спутник")
|
||||
# id_vch_load = models.ForeignKey(Parameter, on_delete=models.CASCADE, related_name="objitems", verbose_name="ВЧ загрузка")
|
||||
# id_geo = models.ForeignKey(Geo, on_delete=models.CASCADE, related_name="objitems", verbose_name="Геоданные")
|
||||
# id_user_add = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="objitems", verbose_name="Пользователь", null=True, blank=True)
|
||||
# id_source_type = models.ForeignKey(SourceType, on_delete=models.SET_NULL, related_name="objitems", verbose_name='Тип источника', null=True, blank=True)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
|
||||
created_by = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="objitems_created",
|
||||
null=True, blank=True, verbose_name="Создан пользователем")
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name="Дата последнего изменения")
|
||||
updated_by = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="objitems_updated",
|
||||
null=True, blank=True, verbose_name="Изменен пользователем")
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"Объект {self.name}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Объект"
|
||||
verbose_name_plural = "Объекты"
|
||||
# constraints = [
|
||||
# models.UniqueConstraint(
|
||||
# fields=['id_vch_load', 'id_geo'],
|
||||
# name='unique_objitem_combination'
|
||||
# )
|
||||
# ]
|
||||
|
||||
class SourceType(models.Model):
|
||||
name = models.CharField(max_length=50, unique=True, verbose_name="Тип источника")
|
||||
objitem = models.OneToOneField(ObjItem, on_delete=models.SET_NULL, verbose_name="Гео", related_name="source_type_obj", null=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Тип источника"
|
||||
verbose_name_plural = 'Типы источников'
|
||||
|
||||
|
||||
class Parameter(models.Model):
|
||||
id_satellite = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="parameters", verbose_name="Спутник", null=True)
|
||||
polarization = models.ForeignKey(
|
||||
@@ -122,7 +165,8 @@ class Parameter(models.Model):
|
||||
standard = models.ForeignKey(
|
||||
Standard, default=get_default_standard, on_delete=models.SET_DEFAULT, related_name="standards", null=True, blank=True, verbose_name="Стандарт"
|
||||
)
|
||||
id_user_add = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="parameter_added", verbose_name="Пользователь", null=True, blank=True)
|
||||
# id_user_add = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="parameter_added", verbose_name="Пользователь", null=True, blank=True)
|
||||
objitems = models.ManyToManyField(ObjItem, related_name="parameters_obj", verbose_name="Источники", blank=True)
|
||||
# id_sigma_parameter = models.ManyToManyField(SigmaParameter, on_delete=models.SET_NULL, related_name="sigma_parameter", verbose_name="ВЧ с sigma", null=True, blank=True)
|
||||
# id_sigma_parameter = models.ManyToManyField(SigmaParameter, verbose_name="ВЧ с sigma", null=True, blank=True)
|
||||
|
||||
@@ -151,12 +195,35 @@ class Parameter(models.Model):
|
||||
|
||||
|
||||
class SigmaParameter(models.Model):
|
||||
TRANSFERS = [
|
||||
(-1.0, "-"),
|
||||
(9750.0, "9750 МГц"),
|
||||
(10750.0, "10750 МГц")
|
||||
]
|
||||
|
||||
id_satellite = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="sigmapar_sat", verbose_name="Спутник")
|
||||
transfer = models.FloatField(
|
||||
choices=TRANSFERS,
|
||||
default=-1.0,
|
||||
verbose_name="Перенос по частоте"
|
||||
)
|
||||
status = models.CharField(max_length=20, blank=True, null=True, verbose_name="Статус")
|
||||
frequency = models.FloatField(default=0, null=True, blank=True, verbose_name="Частота, МГц", db_index=True)
|
||||
transfer_frequency = models.GeneratedField(
|
||||
expression=ExpressionWrapper(
|
||||
F('frequency') + F('transfer'),
|
||||
output_field=models.FloatField()
|
||||
),
|
||||
output_field=models.FloatField(),
|
||||
db_persist=True,
|
||||
null=True, blank=True, verbose_name="Частота в Ku, МГц"
|
||||
)
|
||||
freq_range = models.FloatField(default=0, null=True, blank=True, verbose_name="Полоса частот, МГц")
|
||||
power = models.FloatField(default=0, null=True, blank=True, verbose_name="Мощность, дБм")
|
||||
bod_velocity = models.FloatField(default=0, null=True, blank=True, verbose_name="Символьная скорость, БОД")
|
||||
polarization = models.ForeignKey(
|
||||
Polarization, default=get_default_polarization, on_delete=models.SET_DEFAULT, related_name="polarizations_sigma", null=True, blank=True, verbose_name="Поляризация"
|
||||
)
|
||||
modulation = models.ForeignKey(
|
||||
Modulation, default=get_default_modulation, on_delete=models.SET_DEFAULT, related_name="modulations_sigma", null=True, blank=True, verbose_name="Модуляция"
|
||||
)
|
||||
@@ -194,7 +261,7 @@ class Geo(models.Model):
|
||||
coords_kupsat = gis.PointField(srid=4326, null=True, blank=True, verbose_name="Координаты Кубсата")
|
||||
coords_valid = gis.PointField(srid=4326, null=True, blank=True, verbose_name="Координаты оперативников")
|
||||
is_average = models.BooleanField(null=True, blank=True, verbose_name="Усреднённое")
|
||||
id_user_add = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="geos_added", verbose_name="Пользователь", null=True, blank=True)
|
||||
# id_user_add = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="geos_added", verbose_name="Пользователь", null=True, blank=True)
|
||||
distance_coords_kup = models.GeneratedField(
|
||||
expression=functions.Distance("coords", "coords_kupsat")/1000,
|
||||
output_field=models.FloatField(),
|
||||
@@ -213,6 +280,7 @@ class Geo(models.Model):
|
||||
db_persist=True,
|
||||
null=True, blank=True, verbose_name="Расстояние между купсатом и оперативным отделом, км"
|
||||
)
|
||||
objitem = models.OneToOneField(ObjItem, on_delete=models.CASCADE, verbose_name="Гео", related_name="geo_obj", null=True)
|
||||
|
||||
def __str__(self):
|
||||
longitude = self.coords.coords[0]
|
||||
@@ -234,23 +302,3 @@ class Geo(models.Model):
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
class ObjItem(models.Model):
|
||||
name = models.CharField(null=True, blank=True, max_length=100, verbose_name="Имя объекта", db_index=True)
|
||||
# id_satellite = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="objitems", verbose_name="Спутник")
|
||||
id_vch_load = models.ForeignKey(Parameter, on_delete=models.CASCADE, related_name="objitems", verbose_name="ВЧ загрузка")
|
||||
id_geo = models.ForeignKey(Geo, on_delete=models.CASCADE, related_name="objitems", verbose_name="Геоданные")
|
||||
id_user_add = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="objitems", verbose_name="Пользователь", null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"Объект {self.name}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Объект"
|
||||
verbose_name_plural = "Объекты"
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=['id_vch_load', 'id_geo'],
|
||||
name='unique_objitem_combination'
|
||||
)
|
||||
]
|
||||
11
dbapp/mainapp/signals.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.contrib.auth.models import User
|
||||
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()
|
||||
196
dbapp/mainapp/templates/mainapp/actions.html
Normal file
@@ -0,0 +1,196 @@
|
||||
{% 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 -->
|
||||
{% 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 %}
|
||||
|
||||
<!-- 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 '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 '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 '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 '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 'vch_load' %}" class="btn btn-danger">
|
||||
Добавить данные ВЧ загрузки
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Map Views 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-map text-secondary" viewBox="0 0 16 16">
|
||||
<path d="M15.817.113A.5.5 0 0 1 16 .5v14a.5.5 0 0 1-.402.49l-5 1a.502.502 0 0 1-.196 0L5.5 15.01l-4.902.98A.5.5 0 0 1 0 15.5v-14a.5.5 0 0 1 .402-.49l5-1a.5.5 0 0 1 .196 0L10.5.99l4.902-.98a.5.5 0 0 1 .415.103M10 1.91l-4-.8v12.98l4 .8zM1.61 2.22l4.39.88v10.88l-4.39-.88zm9.18 10.88 4-.8V2.34l-4 .8z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="card-title mb-0">Карты</h3>
|
||||
</div>
|
||||
<p class="card-text">Просматривайте данные на 2D и 3D картах для визуализации геолокации спутников.</p>
|
||||
<div class="mt-2">
|
||||
<a href="{% url '2dmap' %}" class="btn btn-secondary me-2">2D Карта</a>
|
||||
<a href="{% url '3dmap' %}" class="btn btn-outline-secondary">3D Карта</a>
|
||||
</div>
|
||||
</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 '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 'kubsat_excel' %}" class="btn btn-success">
|
||||
Добавить событие
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -6,7 +6,7 @@
|
||||
<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>
|
||||
|
||||
<link href="{% static 'bootstrap-icons/bootstrap-icons.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'bootstrap/bootstrap.min.css' %}" rel="stylesheet">
|
||||
|
||||
<!-- Дополнительные стили (если нужно) -->
|
||||
@@ -22,9 +22,13 @@
|
||||
<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 'home' %}">Главная</a>
|
||||
<a class="nav-link" href="{% url 'home' %}">Объекты</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'actions' %}">Действия</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url '3dmap' %}">3D карта</a>
|
||||
@@ -36,14 +40,36 @@
|
||||
<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>
|
||||
|
||||
<!-- Основной контент -->
|
||||
<main class="container mt-4">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
<main class="{% if full_width_page %}container-fluid p-0{% else %}container mt-4{% endif %}">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<script src="{% static 'bootstrap/bootstrap.bundle.min.js' %}"></script>
|
||||
|
||||
@@ -1,176 +1,422 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}Главная{% endblock %}
|
||||
{% 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 class="container-fluid px-3">
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<h2>Список объектов</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alert messages -->
|
||||
{% 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>
|
||||
<!-- Toolbar -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap align-items-center gap-3">
|
||||
<div style="min-width: 300px; flex-grow: 1;">
|
||||
<label for="toolbar-search" class="form-label mb-0">Поиск:</label>
|
||||
<input type="text" id="toolbar-search" class="form-control" placeholder="Поиск по имени, местоположению..." value="{{ search_query|default:'' }}">
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<button type="button" class="btn btn-outline-primary" onclick="performSearch()">Найти</button>
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="clearSearch()">Очистить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<!-- Filters Sidebar - Made narrower -->
|
||||
<div class="col-md-2">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Фильтры</h5>
|
||||
<form method="get" id="filter-form">
|
||||
<!-- Satellite Selection - Multi-select -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Спутник:</label>
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('satellite_id', true)">Выбрать</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('satellite_id', false)">Снять</button>
|
||||
</div>
|
||||
<select name="satellite_id" class="form-select form-select-sm mb-2" multiple size="4">
|
||||
{% for satellite in satellites %}
|
||||
<option value="{{ satellite.id }}"
|
||||
{% if satellite.id in selected_satellites %}selected{% endif %}>
|
||||
{{ satellite.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Frequency Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Частота, МГц:</label>
|
||||
<input type="number" step="0.001" name="freq_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ freq_min|default:'' }}">
|
||||
<input type="number" step="0.001" name="freq_max" class="form-control form-control-sm" placeholder="До" value="{{ freq_max|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- Range Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Полоса, МГц:</label>
|
||||
<input type="number" step="0.001" name="range_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ range_min|default:'' }}">
|
||||
<input type="number" step="0.001" name="range_max" class="form-control form-control-sm" placeholder="До" value="{{ range_max|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- SNR Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">ОСШ:</label>
|
||||
<input type="number" step="0.001" name="snr_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ snr_min|default:'' }}">
|
||||
<input type="number" step="0.001" name="snr_max" class="form-control form-control-sm" placeholder="До" value="{{ snr_max|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- Symbol Rate Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Сим. v, БОД:</label>
|
||||
<input type="number" step="0.001" name="bod_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ bod_min|default:'' }}">
|
||||
<input type="number" step="0.001" name="bod_max" class="form-control form-control-sm" placeholder="До" value="{{ bod_max|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- Removed old search input as it's now in the toolbar -->
|
||||
|
||||
<!-- Modulation Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Модуляция:</label>
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('modulation', true)">Выбрать</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('modulation', false)">Снять</button>
|
||||
</div>
|
||||
<select name="modulation" class="form-select form-select-sm mb-2" multiple size="4">
|
||||
{% for mod in modulations %}
|
||||
<option value="{{ mod.id }}"
|
||||
{% if mod.id in selected_modulations %}selected{% endif %}>
|
||||
{{ mod.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Polarization Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Поляризация:</label>
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('polarization', true)">Выбрать</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('polarization', false)">Снять</button>
|
||||
</div>
|
||||
<select name="polarization" class="form-select form-select-sm mb-2" multiple size="4">
|
||||
{% for pol in polarizations %}
|
||||
<option value="{{ pol.id }}"
|
||||
{% if pol.id in selected_polarizations %}selected{% endif %}>
|
||||
{{ pol.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Kubsat Coordinates Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Координаты Кубсата:</label>
|
||||
<div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_kupsat" id="has_kupsat_1" value="1"
|
||||
{% if has_kupsat == '1' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_kupsat_1">Есть</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_kupsat" id="has_kupsat_0" value="0"
|
||||
{% if has_kupsat == '0' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_kupsat_0">Нет</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Valid Coordinates Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Координаты опер. отдела:</label>
|
||||
<div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_valid" id="has_valid_1" value="1"
|
||||
{% if has_valid == '1' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_valid_1">Есть</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_valid" id="has_valid_0" value="0"
|
||||
{% if has_valid == '0' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_valid_0">Нет</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Items Per Page -->
|
||||
<div class="mb-2">
|
||||
<label for="items-per-page" class="form-label">Элементов:</label>
|
||||
<select name="items_per_page" id="items-per-page" class="form-select form-select-sm" onchange="document.getElementById('filter-form').submit();">
|
||||
{% for option in available_items_per_page %}
|
||||
<option value="{{ option }}" {% if option == items_per_page %}selected{% endif %}>
|
||||
{{ option }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Apply Filters and Reset Buttons -->
|
||||
<div class="d-grid gap-2 mt-2">
|
||||
<button type="submit" class="btn btn-primary btn-sm">Применить</button>
|
||||
<a href="?" class="btn btn-secondary btn-sm">Сбросить</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Table -->
|
||||
<div class="col-md-10">
|
||||
<div class="card h-100">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive" style="max-height: 75vh; overflow-y: auto;">
|
||||
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;">
|
||||
<thead class="table-dark sticky-top">
|
||||
<tr>
|
||||
<th scope="col" class="text-center" style="width: 3%;">
|
||||
<input type="checkbox" id="select-all" class="form-check-input">
|
||||
</th>
|
||||
<th scope="col">Имя</th>
|
||||
<th scope="col">Спутник</th>
|
||||
<th scope="col">Част, МГц</th>
|
||||
<th scope="col">Полоса, МГц</th>
|
||||
<th scope="col">Поляр</th>
|
||||
<th scope="col">Сим. v</th>
|
||||
<th scope="col">Модул</th>
|
||||
<th scope="col">ОСШ</th>
|
||||
<th scope="col">Геолокация</th>
|
||||
<th scope="col">Кубсат</th>
|
||||
<th scope="col">Опер. отд</th>
|
||||
<th scope="col">Гео-куб, км</th>
|
||||
<th scope="col">Гео-опер, км</th>
|
||||
<th scope="col">Куб-опер, км</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in processed_objects %}
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" class="form-check-input item-checkbox" value="{{ item.id }}">
|
||||
</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.satellite_name }}</td>
|
||||
<td>{{ item.frequency }}</td>
|
||||
<td>{{ item.freq_range }}</td>
|
||||
<td>{{ item.polarization }}</td>
|
||||
<td>{{ item.bod_velocity }}</td>
|
||||
<td>{{ item.modulation }}</td>
|
||||
<td>{{ item.snr }}</td>
|
||||
<td>{{ item.geo_coords }}</td>
|
||||
<td>{{ item.kupsat_coords }}</td>
|
||||
<td>{{ item.valid_coords }}</td>
|
||||
<td>{{ item.distance_geo_kup }}</td>
|
||||
<td>{{ item.distance_geo_valid }}</td>
|
||||
<td>{{ item.distance_kup_valid }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="15" class="text-center py-4">
|
||||
{% if selected_satellite_id %}
|
||||
Нет данных для выбранных фильтров
|
||||
{% else %}
|
||||
Пожалуйста, выберите спутник для отображения данных
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if page_obj.paginator.num_pages > 1 %}
|
||||
<nav aria-label="Page navigation" class="px-3 pb-3">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page=1">Первая</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.previous_page_number }}">Предыдущая</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<!-- 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 'load_excel_data' %}" class="btn btn-primary">
|
||||
Перейти к загрузке данных
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% for num in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == num %}
|
||||
<li class="page-item active">
|
||||
<span class="page-link">{{ num }}</span>
|
||||
</li>
|
||||
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ num }}">{{ num }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<!-- 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 'load_csv_data' %}" class="btn btn-success">
|
||||
Перейти к загрузке данных
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.next_page_number }}">Следующая</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.paginator.num_pages }}">Последняя</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
<!-- 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>
|
||||
<!-- Pagination Info -->
|
||||
{% if page_obj %}
|
||||
<div class="px-3 pb-3 d-flex justify-content-between align-items-center">
|
||||
<div>Показано {{ page_obj.start_index }}-{{ page_obj.end_index }} из {{ page_obj.paginator.count }} записей</div>
|
||||
</div>
|
||||
<h3 class="card-title mb-0">Добавление списка спутников</h3>
|
||||
</div>
|
||||
<p class="card-text">Добавьте новый список спутников в базу данных для последующего использования в загрузке данных.</p>
|
||||
<a href="{% url '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 '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 'vch_load' %}" class="btn btn-danger">
|
||||
Добавить данные ВЧ загрузки
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Map Views 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-map text-secondary" viewBox="0 0 16 16">
|
||||
<path d="M15.817.113A.5.5 0 0 1 16 .5v14a.5.5 0 0 1-.402.49l-5 1a.502.502 0 0 1-.196 0L5.5 15.01l-4.902.98A.5.5 0 0 1 0 15.5v-14a.5.5 0 0 1 .402-.49l5-1a.5.5 0 0 1 .196 0L10.5.99l4.902-.98a.5.5 0 0 1 .415.103M10 1.91l-4-.8v12.98l4 .8zM1.61 2.22l4.39.88v10.88l-4.39-.88zm9.18 10.88 4-.8V2.34l-4 .8z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="card-title mb-0">Карты</h3>
|
||||
</div>
|
||||
<p class="card-text">Просматривайте данные на 2D и 3D картах для визуализации геолокации спутников.</p>
|
||||
<div class="mt-2">
|
||||
<a href="{% url '2dmap' %}" class="btn btn-secondary me-2">2D Карта</a>
|
||||
<a href="{% url '3dmap' %}" class="btn btn-outline-secondary">3D Карта</a>
|
||||
</div>
|
||||
</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 'link_vch_sigma' %}" class="btn btn-info">
|
||||
Открыть форму
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript for checkbox functionality and filters -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Select/Deselect all checkboxes
|
||||
const selectAllCheckbox = document.getElementById('select-all');
|
||||
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
|
||||
|
||||
if (selectAllCheckbox && itemCheckboxes.length > 0) {
|
||||
selectAllCheckbox.addEventListener('change', function() {
|
||||
itemCheckboxes.forEach(checkbox => {
|
||||
checkbox.checked = selectAllCheckbox.checked;
|
||||
});
|
||||
});
|
||||
|
||||
// Update select all checkbox state based on individual selections
|
||||
itemCheckboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function() {
|
||||
const allChecked = Array.from(itemCheckboxes).every(cb => cb.checked);
|
||||
selectAllCheckbox.checked = allChecked;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Handle multiple selection for modulations and polarizations
|
||||
const modulationSelect = document.querySelector('select[name="modulation"]');
|
||||
const polarizationSelect = document.querySelector('select[name="polarization"]');
|
||||
|
||||
// Prevent deselecting all options when Ctrl+click is used
|
||||
if (modulationSelect) {
|
||||
modulationSelect.addEventListener('change', function(e) {
|
||||
document.getElementById('filter-form').submit();
|
||||
});
|
||||
}
|
||||
|
||||
if (polarizationSelect) {
|
||||
polarizationSelect.addEventListener('change', function(e) {
|
||||
document.getElementById('filter-form').submit();
|
||||
});
|
||||
}
|
||||
|
||||
// Handle kubsat and valid coords checkboxes (mutually exclusive)
|
||||
// Add a function to handle radio-like behavior for these checkboxes
|
||||
function setupRadioLikeCheckboxes(name) {
|
||||
const checkboxes = document.querySelectorAll(`input[name="${name}"]`);
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function() {
|
||||
// If this checkbox is checked, uncheck the other
|
||||
if (this.checked) {
|
||||
checkboxes.forEach(other => {
|
||||
if (other !== this) {
|
||||
other.checked = false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// If both are unchecked, no action needed
|
||||
}
|
||||
document.getElementById('filter-form').submit();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setupRadioLikeCheckboxes('has_kupsat');
|
||||
setupRadioLikeCheckboxes('has_valid');
|
||||
|
||||
// Function to select/deselect all options in a select element
|
||||
window.selectAllOptions = function(selectName, selectAll) {
|
||||
const selectElement = document.querySelector(`select[name="${selectName}"]`);
|
||||
if (selectElement) {
|
||||
for (let i = 0; i < selectElement.options.length; i++) {
|
||||
selectElement.options[i].selected = selectAll;
|
||||
}
|
||||
document.getElementById('filter-form').submit();
|
||||
}
|
||||
};
|
||||
|
||||
// Function to update the page when satellite selection changes
|
||||
function updateSatelliteSelection() {
|
||||
document.getElementById('filter-form').submit();
|
||||
}
|
||||
|
||||
// Get all current filter values and return as URL parameters
|
||||
function getAllFilterParams() {
|
||||
const form = document.getElementById('filter-form');
|
||||
const searchValue = document.getElementById('toolbar-search').value;
|
||||
|
||||
// Create URLSearchParams object from the form
|
||||
const params = new URLSearchParams(new FormData(form));
|
||||
|
||||
// Add search value from toolbar if present
|
||||
if (searchValue.trim() !== '') {
|
||||
params.set('search', searchValue);
|
||||
} else {
|
||||
// Remove search parameter if empty
|
||||
params.delete('search');
|
||||
}
|
||||
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
// Function to perform search
|
||||
window.performSearch = function() {
|
||||
const filterParams = getAllFilterParams();
|
||||
window.location.search = filterParams;
|
||||
};
|
||||
|
||||
// Function to clear search
|
||||
window.clearSearch = function() {
|
||||
// Clear only the search input in the toolbar
|
||||
document.getElementById('toolbar-search').value = '';
|
||||
// Submit the form to update the results
|
||||
const filterParams = getAllFilterParams();
|
||||
window.location.search = filterParams;
|
||||
};
|
||||
|
||||
// Handle Enter key in toolbar search
|
||||
const toolbarSearch = document.getElementById('toolbar-search');
|
||||
if (toolbarSearch) {
|
||||
toolbarSearch.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
performSearch();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add event listener to satellite select for immediate update
|
||||
const satelliteSelect = document.querySelector('select[name="satellite_id"]');
|
||||
if (satelliteSelect) {
|
||||
satelliteSelect.addEventListener('change', function() {
|
||||
updateSatelliteSelection();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -20,7 +20,7 @@
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<p class="card-text">Введите допустимый разброс для частоты и полосы(в кГц)</p>
|
||||
<p class="card-text">Введите допустимый разброс для частоты и полосы</p>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
@@ -31,15 +31,15 @@
|
||||
<div class="text-danger mt-1">{{ form.sat_choice.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
{% 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>
|
||||
</div> {% endcomment %}
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.value1.id_for_label }}" class="form-label">Разброс по частоте(в %)</label>
|
||||
<label for="{{ form.value1.id_for_label }}" class="form-label">Разброс по частоте(в МГц)</label>
|
||||
{{ form.value1 }}
|
||||
{% if form.value1.errors %}
|
||||
<div class="text-danger mt-1">{{ form.value1.errors }}</div>
|
||||
|
||||
19
dbapp/mainapp/templates/mainapp/login_required.html
Normal file
@@ -0,0 +1,19 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}Войдите в систему{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<h2 class="card-title">Требуется авторизация</h2>
|
||||
<p class="card-text">Для просмотра содержимого сайта необходимо войти в систему.</p>
|
||||
<a href="{% url 'login' %}" class="btn btn-primary">Войти</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
25
dbapp/mainapp/templates/mainapp/objitem_confirm_delete.html
Normal file
@@ -0,0 +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 'home' %}" class="btn btn-secondary ms-2">Отмена</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
688
dbapp/mainapp/templates/mainapp/objitem_form.html
Normal file
@@ -0,0 +1,688 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
{% load static %}
|
||||
{% load static leaflet_tags %}
|
||||
|
||||
{% block title %}{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать объект{% endif %}{% 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; }
|
||||
.dynamic-form { border: 1px dashed #ced4da; padding: 1rem; margin-top: 1rem; border-radius: 0.25rem; }
|
||||
.dynamic-form-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.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>{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать объект{% endif %}</h2>
|
||||
<div>
|
||||
<a href="{% url 'home' %}" class="btn btn-secondary btn-action">Назад</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Основная информация -->
|
||||
<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 for="{{ form.name.id_for_label }}" class="form-label">Имя объекта:</label>
|
||||
{{ form.name }}
|
||||
</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 d-flex justify-content-between align-items-center">
|
||||
<h4>ВЧ загрузка</h4>
|
||||
{% if not parameter_forms.forms.0.instance.pk %}
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="add-parameter">Добавить ВЧ загрузку</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div id="parameters-container">
|
||||
{% for param_form in parameter_forms %}
|
||||
{% comment %} <div class="dynamic-form" data-parameter-index="{{ forloop.counter0 }}"> {% endcomment %}
|
||||
<div class="dynamic-form-header">
|
||||
{% if parameter_forms.forms|length > 1 %}
|
||||
<button type="button" class="btn btn-sm btn-outline-danger remove-parameter">Удалить</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label for="{{ param_form.id_satellite.id_for_label }}" class="form-label">Спутник:</label>
|
||||
{{ param_form.id_satellite }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label for="{{ param_form.frequency.id_for_label }}" class="form-label">Частота, МГц:</label>
|
||||
{{ param_form.frequency }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label for="{{ param_form.freq_range.id_for_label }}" class="form-label">Полоса, МГц:</label>
|
||||
{{ param_form.freq_range }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label for="{{ param_form.polarization.id_for_label }}" class="form-label">Поляризация:</label>
|
||||
{{ param_form.polarization }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label for="{{ param_form.bod_velocity.id_for_label }}" class="form-label">Симв. скорость, БОД:</label>
|
||||
{{ param_form.bod_velocity }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label for="{{ param_form.modulation.id_for_label }}" class="form-label">Модуляция:</label>
|
||||
{{ param_form.modulation }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label for="{{ param_form.snr.id_for_label }}" class="form-label">ОСШ:</label>
|
||||
{{ param_form.snr }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label for="{{ param_form.standard.id_for_label }}" class="form-label">Стандарт:</label>
|
||||
{{ param_form.standard }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% comment %} </div> {% endcomment %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</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>
|
||||
|
||||
|
||||
<!-- Координаты геолокации -->
|
||||
<div class="coord-sync-group">
|
||||
<div class="coord-group-header">Координаты геолокации</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="id_geo_latitude" class="form-label">Широта:</label>
|
||||
<input type="number" step="0.000001" class="form-control"
|
||||
id="id_geo_latitude" name="geo_latitude"
|
||||
value="{% if object.geo_obj and object.geo_obj.coords %}{{ object.geo_obj.coords.y }}{% endif %}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="id_geo_longitude" class="form-label">Долгота:</label>
|
||||
<input type="number" step="0.000001" class="form-control"
|
||||
id="id_geo_longitude" name="geo_longitude"
|
||||
value="{% if object.geo_obj and object.geo_obj.coords %}{{ object.geo_obj.coords.x }}{% 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 for="id_kupsat_longitude" class="form-label">Долгота:</label>
|
||||
<input type="number" step="0.000001" class="form-control"
|
||||
id="id_kupsat_longitude" name="kupsat_longitude"
|
||||
value="{% if object.geo_obj and object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.x }}{% endif %}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="id_kupsat_latitude" class="form-label">Широта:</label>
|
||||
<input type="number" step="0.000001" class="form-control"
|
||||
id="id_kupsat_latitude" name="kupsat_latitude"
|
||||
value="{% if object.geo_obj and object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.y }}{% 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 for="id_valid_longitude" class="form-label">Долгота:</label>
|
||||
<input type="number" step="0.000001" class="form-control"
|
||||
id="id_valid_longitude" name="valid_longitude"
|
||||
value="{% if object.geo_obj and object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.x }}{% endif %}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="id_valid_latitude" class="form-label">Широта:</label>
|
||||
<input type="number" step="0.000001" class="form-control"
|
||||
id="id_valid_latitude" name="valid_latitude"
|
||||
value="{% if object.geo_obj and object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.y }}{% endif %}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="{{ geo_form.location.id_for_label }}" class="form-label">Местоположение:</label>
|
||||
{{ geo_form.location }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="{{ geo_form.comment.id_for_label }}" class="form-label">Комментарий:</label>
|
||||
{{ geo_form.comment }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Дата и время:</label>
|
||||
<div class="datetime-group">
|
||||
<div>
|
||||
<label for="id_timestamp_date" class="form-label">Дата:</label>
|
||||
<input type="date" class="form-control"
|
||||
id="id_timestamp_date" name="timestamp_date"
|
||||
value="{% if object.geo_obj and object.geo_obj.timestamp %}{{ object.geo_obj.timestamp|date:'Y-m-d' }}{% endif %}">
|
||||
</div>
|
||||
<div>
|
||||
<label for="id_timestamp_time" class="form-label">Время:</label>
|
||||
<input type="time" class="form-control"
|
||||
id="id_timestamp_time" name="timestamp_time"
|
||||
value="{% if object.geo_obj and object.geo_obj.timestamp %}{{ object.geo_obj.timestamp|time:'H:i' }}{% endif %}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="{{ geo_form.is_average.id_for_label }}" class="form-check-label">Усреднённое:</label>
|
||||
{{ geo_form.is_average }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if object.geo_obj %}
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||||
<div class="d-flex justify-content-end mt-4">
|
||||
<button type="submit" class="btn btn-primary btn-action">Сохранить</button>
|
||||
{% if object %}
|
||||
<a href="{% url 'objitem_delete' object.id %}" class="btn btn-danger btn-action">Удалить</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{{ block.super }}
|
||||
<!-- Подключаем Leaflet и его плагины -->
|
||||
{% leaflet_js %}
|
||||
{% leaflet_css %}
|
||||
<script src="{% static 'leaflet-markers/js/leaflet-color-markers.js' %}"></script>
|
||||
|
||||
|
||||
<script>
|
||||
// Динамическое добавление ВЧ загрузок
|
||||
let parameterIndex = {{ parameter_forms|length }};
|
||||
|
||||
document.getElementById('add-parameter')?.addEventListener('click', function() {
|
||||
const container = document.getElementById('parameters-container');
|
||||
const template = document.querySelector('.dynamic-form');
|
||||
|
||||
if (template) {
|
||||
const clone = template.cloneNode(true);
|
||||
clone.querySelectorAll('[id]').forEach(el => {
|
||||
el.id = el.id.replace(/-\d+-/g, `-${parameterIndex}-`);
|
||||
});
|
||||
clone.querySelectorAll('[name]').forEach(el => {
|
||||
el.name = el.name.replace(/-\d+-/g, `-${parameterIndex}-`);
|
||||
});
|
||||
clone.querySelectorAll('[for]').forEach(el => {
|
||||
el.htmlFor = el.htmlFor.replace(/-\d+-/g, `-${parameterIndex}-`);
|
||||
});
|
||||
clone.querySelector('.dynamic-form-header h5').textContent = `ВЧ загрузка #${parameterIndex + 1}`;
|
||||
clone.dataset.parameterIndex = parameterIndex;
|
||||
container.appendChild(clone);
|
||||
parameterIndex++;
|
||||
}
|
||||
});
|
||||
|
||||
// Удаление ВЧ загрузок
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('remove-parameter')) {
|
||||
if (document.querySelectorAll('.dynamic-form').length > 1) {
|
||||
e.target.closest('.dynamic-form').remove();
|
||||
} else {
|
||||
alert('Должна быть хотя бы одна ВЧ загрузка');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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: '© <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 editableLayerGroup = new L.FeatureGroup();
|
||||
map.addLayer(editableLayerGroup);
|
||||
|
||||
// Маркеры
|
||||
const markers = {};
|
||||
function createMarker(latFieldId, lngFieldId, position, color, name) {
|
||||
const marker = L.marker(position, {
|
||||
draggable: false,
|
||||
icon: createMarkerIcon(color),
|
||||
title: name
|
||||
}).addTo(editableLayerGroup);
|
||||
marker.bindPopup(name);
|
||||
|
||||
// Синхронизация при изменении формы
|
||||
function syncFromForm() {
|
||||
const lat = parseFloat(document.getElementById(latFieldId).value);
|
||||
const lng = parseFloat(document.getElementById(lngFieldId).value);
|
||||
if (!isNaN(lat) && !isNaN(lng)) {
|
||||
marker.setLatLng([lat, lng]);
|
||||
}
|
||||
}
|
||||
|
||||
// Синхронизация при перетаскивании (только если активировано)
|
||||
marker.on('dragend', function(event) {
|
||||
const latLng = event.target.getLatLng();
|
||||
document.getElementById(latFieldId).value = latLng.lat.toFixed(6);
|
||||
document.getElementById(lngFieldId).value = latLng.lng.toFixed(6);
|
||||
});
|
||||
|
||||
// Добавляем методы для управления
|
||||
marker.enableEditing = function() {
|
||||
this.dragging.enable();
|
||||
this.openPopup();
|
||||
};
|
||||
|
||||
marker.disableEditing = function() {
|
||||
this.dragging.disable();
|
||||
this.closePopup();
|
||||
};
|
||||
|
||||
marker.syncFromForm = syncFromForm;
|
||||
|
||||
return marker;
|
||||
}
|
||||
|
||||
// Создаем маркеры
|
||||
markers.geo = createMarker(
|
||||
'id_geo_latitude',
|
||||
'id_geo_longitude',
|
||||
[55.75, 37.62],
|
||||
colors.geo,
|
||||
'Геолокация'
|
||||
);
|
||||
|
||||
markers.kupsat = createMarker(
|
||||
'id_kupsat_latitude',
|
||||
'id_kupsat_longitude',
|
||||
[55.75, 37.61],
|
||||
colors.kupsat,
|
||||
'Кубсат'
|
||||
);
|
||||
|
||||
markers.valid = createMarker(
|
||||
'id_valid_latitude',
|
||||
'id_valid_longitude',
|
||||
[55.75, 37.63],
|
||||
colors.valid,
|
||||
'Оперативник'
|
||||
);
|
||||
|
||||
// Устанавливаем начальные координаты из полей формы
|
||||
function initMarkersFromForm() {
|
||||
const geoLat = parseFloat(document.getElementById('id_geo_latitude').value) || 55.75;
|
||||
const geoLng = parseFloat(document.getElementById('id_geo_longitude').value) || 37.62;
|
||||
markers.geo.setLatLng([geoLat, geoLng]);
|
||||
|
||||
const kupsatLat = parseFloat(document.getElementById('id_kupsat_latitude').value) || 55.75;
|
||||
const kupsatLng = parseFloat(document.getElementById('id_kupsat_longitude').value) || 37.61;
|
||||
markers.kupsat.setLatLng([kupsatLat, kupsatLng]);
|
||||
|
||||
const validLat = parseFloat(document.getElementById('id_valid_latitude').value) || 55.75;
|
||||
const validLng = parseFloat(document.getElementById('id_valid_longitude').value) || 37.63;
|
||||
markers.valid.setLatLng([validLat, validLng]);
|
||||
|
||||
// Центрируем карту на первом маркере
|
||||
map.setView(markers.geo.getLatLng(), 10);
|
||||
}
|
||||
|
||||
// Настройка формы для синхронизации с маркерами
|
||||
function setupFormChange(latFieldId, lngFieldId, marker) {
|
||||
const latField = document.getElementById(latFieldId);
|
||||
const lngField = document.getElementById(lngFieldId);
|
||||
|
||||
[latField, lngField].forEach(field => {
|
||||
field.addEventListener('change', function() {
|
||||
const lat = parseFloat(latField.value);
|
||||
const lng = parseFloat(lngField.value);
|
||||
|
||||
if (!isNaN(lat) && !isNaN(lng)) {
|
||||
marker.setLatLng([lat, lng]);
|
||||
map.setView(marker.getLatLng(), 10);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Инициализация
|
||||
initMarkersFromForm();
|
||||
// Настройка формы для синхронизации с маркерами
|
||||
setupFormChange('id_geo_latitude', 'id_geo_longitude', markers.geo);
|
||||
setupFormChange('id_kupsat_latitude', 'id_kupsat_longitude', markers.kupsat);
|
||||
setupFormChange('id_valid_latitude', 'id_valid_longitude', markers.valid);
|
||||
// --- УПРАВЛЕНИЕ РЕДАКТИРОВАНИЕМ ---
|
||||
// Кнопки редактирования
|
||||
const editControlsDiv = L.DomUtil.create('div', 'map-controls');
|
||||
editControlsDiv.style.position = 'absolute';
|
||||
editControlsDiv.style.top = '10px';
|
||||
editControlsDiv.style.right = '10px';
|
||||
editControlsDiv.style.zIndex = '1000';
|
||||
editControlsDiv.style.background = 'white';
|
||||
editControlsDiv.style.padding = '10px';
|
||||
editControlsDiv.style.borderRadius = '4px';
|
||||
editControlsDiv.style.boxShadow = '0 0 10px rgba(0,0,0,0.2)';
|
||||
editControlsDiv.innerHTML = `
|
||||
<div class="map-controls">
|
||||
<button type="button" id="edit-btn" class="map-control-btn edit">Редактировать</button>
|
||||
<button type="button" id="save-btn" class="map-control-btn save" disabled>Сохранить</button>
|
||||
<button type="button" id="cancel-btn" class="map-control-btn cancel" disabled>Отмена</button>
|
||||
</div>
|
||||
`;
|
||||
map.getContainer().appendChild(editControlsDiv);
|
||||
|
||||
let isEditing = false;
|
||||
|
||||
// Сохраняем начальные координаты для отмены
|
||||
const initialPositions = {
|
||||
geo: markers.geo.getLatLng(),
|
||||
kupsat: markers.kupsat.getLatLng(),
|
||||
valid: markers.valid.getLatLng()
|
||||
};
|
||||
|
||||
// Включение редактирования
|
||||
document.getElementById('edit-btn').addEventListener('click', function() {
|
||||
if (isEditing) return;
|
||||
|
||||
isEditing = true;
|
||||
document.getElementById('edit-btn').classList.add('active');
|
||||
document.getElementById('save-btn').disabled = false;
|
||||
document.getElementById('cancel-btn').disabled = false;
|
||||
|
||||
// Включаем drag для всех маркеров
|
||||
Object.values(markers).forEach(marker => {
|
||||
marker.enableEditing();
|
||||
});
|
||||
|
||||
// Показываем подсказку
|
||||
L.popup()
|
||||
.setLatLng(map.getCenter())
|
||||
.setContent('Перетаскивайте маркеры. Нажмите "Сохранить" или "Отмена".')
|
||||
.openOn(map);
|
||||
});
|
||||
|
||||
// Сохранение изменений
|
||||
document.getElementById('save-btn').addEventListener('click', function() {
|
||||
if (!isEditing) return;
|
||||
|
||||
isEditing = false;
|
||||
document.getElementById('edit-btn').classList.remove('active');
|
||||
document.getElementById('save-btn').disabled = true;
|
||||
document.getElementById('cancel-btn').disabled = true;
|
||||
|
||||
// Отключаем редактирование
|
||||
Object.values(markers).forEach(marker => {
|
||||
marker.disableEditing();
|
||||
});
|
||||
|
||||
// Обновляем начальные позиции
|
||||
initialPositions.geo = markers.geo.getLatLng();
|
||||
initialPositions.kupsat = markers.kupsat.getLatLng();
|
||||
initialPositions.valid = markers.valid.getLatLng();
|
||||
|
||||
// Убираем попап подсказки
|
||||
map.closePopup();
|
||||
});
|
||||
|
||||
// Отмена изменений
|
||||
document.getElementById('cancel-btn').addEventListener('click', function() {
|
||||
if (!isEditing) return;
|
||||
|
||||
isEditing = false;
|
||||
document.getElementById('edit-btn').classList.remove('active');
|
||||
document.getElementById('save-btn').disabled = true;
|
||||
document.getElementById('cancel-btn').disabled = true;
|
||||
|
||||
// Возвращаем маркеры на исходные позиции
|
||||
markers.geo.setLatLng(initialPositions.geo);
|
||||
markers.kupsat.setLatLng(initialPositions.kupsat);
|
||||
markers.valid.setLatLng(initialPositions.valid);
|
||||
|
||||
// Отключаем редактирование
|
||||
Object.values(markers).forEach(marker => {
|
||||
marker.disableEditing();
|
||||
});
|
||||
|
||||
// Синхронизируем форму с исходными значениями
|
||||
document.getElementById('id_geo_latitude').value = initialPositions.geo.lat.toFixed(6);
|
||||
document.getElementById('id_geo_longitude').value = initialPositions.geo.lng.toFixed(6);
|
||||
|
||||
document.getElementById('id_kupsat_latitude').value = initialPositions.kupsat.lat.toFixed(6);
|
||||
document.getElementById('id_kupsat_longitude').value = initialPositions.kupsat.lng.toFixed(6);
|
||||
|
||||
document.getElementById('id_valid_latitude').value = initialPositions.valid.lat.toFixed(6);
|
||||
document.getElementById('id_valid_longitude').value = initialPositions.valid.lng.toFixed(6);
|
||||
map.closePopup();
|
||||
});
|
||||
|
||||
// Легенда
|
||||
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 %}
|
||||
856
dbapp/mainapp/templates/mainapp/objitem_list.html
Normal file
@@ -0,0 +1,856 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}Список объектов{% endblock %}
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.table-responsive table {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.table-responsive tr.selected {
|
||||
background-color: #d4edff;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container-fluid px-3">
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<h2>Список объектов</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap align-items-center gap-3">
|
||||
<!-- Search bar made more compact -->
|
||||
<div style="min-width: 200px; flex-grow: 1; max-width: 400px;">
|
||||
<div class="input-group">
|
||||
<input type="text" id="toolbar-search" class="form-control" placeholder="Поиск..." value="{{ search_query|default:'' }}">
|
||||
<button type="button" class="btn btn-outline-primary" onclick="performSearch()">Найти</button>
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="clearSearch()">Очистить</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons bar -->
|
||||
<div class="d-flex gap-2">
|
||||
{% comment %} <button type="button" class="btn btn-success btn-sm" title="Добавить">
|
||||
<i class="bi bi-plus-circle"></i> Добавить
|
||||
</button>
|
||||
<button type="button" class="btn btn-info btn-sm" title="Изменить">
|
||||
<i class="bi bi-pencil"></i> Изменить
|
||||
</button> {% endcomment %}
|
||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||||
<button type="button" class="btn btn-danger btn-sm" title="Удалить" onclick="deleteSelectedObjects()">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" title="Показать на карте" onclick="showSelectedOnMap()">
|
||||
<i class="bi bi-map"></i> Карта
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Items per page select moved here -->
|
||||
<div>
|
||||
<label for="items-per-page" class="form-label mb-0">Показать:</label>
|
||||
<select name="items_per_page" id="items-per-page" class="form-select form-select-sm d-inline-block" style="width: auto;" onchange="updateItemsPerPage()">
|
||||
{% for option in available_items_per_page %}
|
||||
<option value="{{ option }}" {% if option == items_per_page %}selected{% endif %}>
|
||||
{{ option }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Column visibility toggle button -->
|
||||
<div class="dropdown">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle" id="columnVisibilityDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="bi bi-gear"></i> Колонки
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="columnVisibilityDropdown" style="z-index: 1050;">
|
||||
<li>
|
||||
<label class="dropdown-item">
|
||||
<input type="checkbox" id="select-all-columns" checked onchange="toggleAllColumns(this)"> Выбрать всё
|
||||
</label>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<label class="dropdown-item">
|
||||
<input type="checkbox" class="column-toggle" data-column="0" checked onchange="toggleColumn(this)"> Выбрать
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="dropdown-item">
|
||||
<input type="checkbox" class="column-toggle" data-column="1" checked onchange="toggleColumn(this)"> Имя
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="dropdown-item">
|
||||
<input type="checkbox" class="column-toggle" data-column="2" checked onchange="toggleColumn(this)"> Спутник
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="dropdown-item">
|
||||
<input type="checkbox" class="column-toggle" data-column="3" checked onchange="toggleColumn(this)"> Част, МГц
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="dropdown-item">
|
||||
<input type="checkbox" class="column-toggle" data-column="4" checked onchange="toggleColumn(this)"> Полоса, МГц
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="dropdown-item">
|
||||
<input type="checkbox" class="column-toggle" data-column="5" checked onchange="toggleColumn(this)"> Поляризация
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="dropdown-item">
|
||||
<input type="checkbox" class="column-toggle" data-column="6" checked onchange="toggleColumn(this)"> Сим. V
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="dropdown-item">
|
||||
<input type="checkbox" class="column-toggle" data-column="7" checked onchange="toggleColumn(this)"> Модул
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="dropdown-item">
|
||||
<input type="checkbox" class="column-toggle" data-column="8" checked onchange="toggleColumn(this)"> ОСШ
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="dropdown-item">
|
||||
<input type="checkbox" class="column-toggle" data-column="9" checked onchange="toggleColumn(this)"> Время ГЛ
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="dropdown-item">
|
||||
<input type="checkbox" class="column-toggle" data-column="10" checked onchange="toggleColumn(this)"> Местоположение
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="dropdown-item">
|
||||
<input type="checkbox" class="column-toggle" data-column="11" checked onchange="toggleColumn(this)"> Геолокация
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="dropdown-item">
|
||||
<input type="checkbox" class="column-toggle" data-column="12" checked onchange="toggleColumn(this)"> Кубсат
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="dropdown-item">
|
||||
<input type="checkbox" class="column-toggle" data-column="13" checked onchange="toggleColumn(this)"> Опер. отд
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="dropdown-item">
|
||||
<input type="checkbox" class="column-toggle" data-column="14" checked onchange="toggleColumn(this)"> Гео-куб, км
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="dropdown-item">
|
||||
<input type="checkbox" class="column-toggle" data-column="15" checked onchange="toggleColumn(this)"> Гео-опер, км
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="dropdown-item">
|
||||
<input type="checkbox" class="column-toggle" data-column="16" checked onchange="toggleColumn(this)"> Куб-опер, км
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="dropdown-item">
|
||||
<input type="checkbox" class="column-toggle" data-column="17" checked onchange="toggleColumn(this)"> Обновлено
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="dropdown-item">
|
||||
<input type="checkbox" class="column-toggle" data-column="18" checked onchange="toggleColumn(this)"> Кем (обновление)
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="dropdown-item">
|
||||
<input type="checkbox" class="column-toggle" data-column="19" checked onchange="toggleColumn(this)"> Создано
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="dropdown-item">
|
||||
<input type="checkbox" class="column-toggle" data-column="20" checked onchange="toggleColumn(this)"> Кем (создание)
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Pagination moved here -->
|
||||
<div class="ms-auto">
|
||||
{% if page_obj.paginator.num_pages > 1 %}
|
||||
<nav aria-label="Page navigation" class="d-flex align-items-center">
|
||||
<ul class="pagination mb-0">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page=1" title="Первая"><<</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.previous_page_number }}" title="Предыдущая"><</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for num in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == num %}
|
||||
<li class="page-item active">
|
||||
<span class="page-link">{{ num }}</span>
|
||||
</li>
|
||||
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ num }}">{{ num }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.next_page_number }}" title="Следующая">></a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.paginator.num_pages }}" title="Последняя">>></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
<!-- Pagination Info -->
|
||||
{% if page_obj %}
|
||||
<div class="ms-3 text-muted small">
|
||||
{{ page_obj.start_index }}-{{ page_obj.end_index }} из {{ page_obj.paginator.count }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<!-- Filters Sidebar - Made narrower -->
|
||||
<div class="col-md-auto">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Фильтры</h5>
|
||||
<form method="get" id="filter-form">
|
||||
<!-- Satellite Selection - Multi-select -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Спутник:</label>
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('satellite_id', true)">Выбрать</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('satellite_id', false)">Снять</button>
|
||||
</div>
|
||||
<select name="satellite_id" class="form-select form-select-sm mb-2" multiple size="6">
|
||||
{% for satellite in satellites %}
|
||||
<option value="{{ satellite.id }}"
|
||||
{% if satellite.id in selected_satellites %}selected{% endif %}>
|
||||
{{ satellite.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Frequency Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Частота, МГц:</label>
|
||||
<input type="number" step="0.001" name="freq_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ freq_min|default:'' }}">
|
||||
<input type="number" step="0.001" name="freq_max" class="form-control form-control-sm" placeholder="До" value="{{ freq_max|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- Range Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Полоса, МГц:</label>
|
||||
<input type="number" step="0.001" name="range_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ range_min|default:'' }}">
|
||||
<input type="number" step="0.001" name="range_max" class="form-control form-control-sm" placeholder="До" value="{{ range_max|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- SNR Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">ОСШ:</label>
|
||||
<input type="number" step="0.001" name="snr_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ snr_min|default:'' }}">
|
||||
<input type="number" step="0.001" name="snr_max" class="form-control form-control-sm" placeholder="До" value="{{ snr_max|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- Symbol Rate Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Сим. v, БОД:</label>
|
||||
<input type="number" step="0.001" name="bod_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ bod_min|default:'' }}">
|
||||
<input type="number" step="0.001" name="bod_max" class="form-control form-control-sm" placeholder="До" value="{{ bod_max|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- Modulation Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Модуляция:</label>
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('modulation', true)">Выбрать</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('modulation', false)">Снять</button>
|
||||
</div>
|
||||
<select name="modulation" class="form-select form-select-sm mb-2" multiple size="6">
|
||||
{% for mod in modulations %}
|
||||
<option value="{{ mod.id }}"
|
||||
{% if mod.id in selected_modulations %}selected{% endif %}>
|
||||
{{ mod.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Polarization Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Поляризация:</label>
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('polarization', true)">Выбрать</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('polarization', false)">Снять</button>
|
||||
</div>
|
||||
<select name="polarization" class="form-select form-select-sm mb-2" multiple size="4">
|
||||
{% for pol in polarizations %}
|
||||
<option value="{{ pol.id }}"
|
||||
{% if pol.id in selected_polarizations %}selected{% endif %}>
|
||||
{{ pol.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Kubsat Coordinates Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Координаты Кубсата:</label>
|
||||
<div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_kupsat" id="has_kupsat_1" value="1"
|
||||
{% if has_kupsat == '1' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_kupsat_1">Есть</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_kupsat" id="has_kupsat_0" value="0"
|
||||
{% if has_kupsat == '0' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_kupsat_0">Нет</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Valid Coordinates Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Координаты опер. отдела:</label>
|
||||
<div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_valid" id="has_valid_1" value="1"
|
||||
{% if has_valid == '1' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_valid_1">Есть</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_valid" id="has_valid_0" value="0"
|
||||
{% if has_valid == '0' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_valid_0">Нет</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Apply Filters and Reset Buttons -->
|
||||
<div class="d-grid gap-2 mt-2">
|
||||
<button type="submit" class="btn btn-primary btn-sm">Применить</button>
|
||||
<a href="?" class="btn btn-secondary btn-sm">Сбросить</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Table -->
|
||||
<div class="col-md">
|
||||
<div class="card h-100">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive" style="max-height: 75vh; overflow-y: auto;">
|
||||
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;">
|
||||
<thead class="table-dark sticky-top">
|
||||
<tr>
|
||||
<th scope="col" class="text-center" style="width: 3%;">
|
||||
<input type="checkbox" id="select-all" class="form-check-input">
|
||||
</th>
|
||||
|
||||
<!-- Столбец "Имя" -->
|
||||
<th scope="col">
|
||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'name' %}-name{% elif sort == '-name' %}name{% else %}name{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
|
||||
Имя
|
||||
{% if sort == 'name' %} <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"><i class="bi bi-x-lg"></i></a> {% elif sort == '-name' %} <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"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
|
||||
</a>
|
||||
</th>
|
||||
|
||||
<!-- Столбец "Спутник" -->
|
||||
<th scope="col">
|
||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'satellite' %}-satellite{% elif sort == '-satellite' %}satellite{% else %}satellite{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
|
||||
Спутник
|
||||
{% if sort == 'satellite' %} <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"><i class="bi bi-x-lg"></i></a> {% elif sort == '-satellite' %} <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"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
|
||||
</a>
|
||||
</th>
|
||||
|
||||
<!-- Столбец "Част, МГц" -->
|
||||
<th scope="col">
|
||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'frequency' %}-frequency{% elif sort == '-frequency' %}frequency{% else %}frequency{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
|
||||
Част, МГц
|
||||
{% if sort == 'frequency' %} <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"><i class="bi bi-x-lg"></i></a> {% elif sort == '-frequency' %} <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"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
|
||||
</a>
|
||||
</th>
|
||||
|
||||
<!-- Столбец "Полоса, МГц" -->
|
||||
<th scope="col">
|
||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'freq_range' %}-freq_range{% elif sort == '-freq_range' %}freq_range{% else %}freq_range{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
|
||||
Полоса, МГц
|
||||
{% if sort == 'freq_range' %} <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"><i class="bi bi-x-lg"></i></a> {% elif sort == '-freq_range' %} <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"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
|
||||
</a>
|
||||
</th>
|
||||
|
||||
<!-- Столбец "Поляризация" -->
|
||||
<th scope="col">
|
||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'polarization' %}-polarization{% elif sort == '-polarization' %}polarization{% else %}polarization{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
|
||||
Поляризация
|
||||
{% if sort == 'polarization' %} <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"><i class="bi bi-x-lg"></i></a> {% elif sort == '-polarization' %} <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"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
|
||||
</a>
|
||||
</th>
|
||||
|
||||
<!-- Столбец "Сим. V" -->
|
||||
<th scope="col">
|
||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'bod_velocity' %}-bod_velocity{% elif sort == '-bod_velocity' %}bod_velocity{% else %}bod_velocity{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
|
||||
Сим. V
|
||||
{% if sort == 'bod_velocity' %} <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"><i class="bi bi-x-lg"></i></a> {% elif sort == '-bod_velocity' %} <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"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
|
||||
</a>
|
||||
</th>
|
||||
|
||||
<!-- Столбец "Модул" -->
|
||||
<th scope="col">
|
||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'modulation' %}-modulation{% elif sort == '-modulation' %}modulation{% else %}modulation{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
|
||||
Модул
|
||||
{% if sort == 'modulation' %} <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"><i class="bi bi-x-lg"></i></a> {% elif sort == '-modulation' %} <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"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
|
||||
</a>
|
||||
</th>
|
||||
|
||||
<!-- Столбец "ОСШ" -->
|
||||
<th scope="col">
|
||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'snr' %}-snr{% elif sort == '-snr' %}snr{% else %}snr{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
|
||||
ОСШ
|
||||
{% if sort == 'snr' %} <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"><i class="bi bi-x-lg"></i></a> {% elif sort == '-snr' %} <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"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
|
||||
</a>
|
||||
</th>
|
||||
|
||||
<!-- Столбец "Время ГЛ" -->
|
||||
<th scope="col">
|
||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'geo_timestamp' %}-geo_timestamp{% elif sort == '-geo_timestamp' %}geo_timestamp{% else %}geo_timestamp{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
|
||||
Время ГЛ
|
||||
{% if sort == 'geo_timestamp' %} <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"><i class="bi bi-x-lg"></i></a> {% elif sort == '-geo_timestamp' %} <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"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col">Местоположение</th>
|
||||
<!-- Столбец "Геолокация" - без сортировки -->
|
||||
<th scope="col">Геолокация</th>
|
||||
|
||||
<!-- Столбец "Кубсат" - без сортировки -->
|
||||
<th scope="col">Кубсат</th>
|
||||
|
||||
<!-- Столбец "Опер. отд" - без сортировки -->
|
||||
<th scope="col">Опер. отд</th>
|
||||
|
||||
<!-- Столбец "Гео-куб, км" - без сортировки -->
|
||||
<th scope="col">Гео-куб, км</th>
|
||||
|
||||
<!-- Столбец "Гео-опер, км" - без сортировки -->
|
||||
<th scope="col">Гео-опер, км</th>
|
||||
|
||||
<!-- Столбец "Куб-опер, км" - без сортировки -->
|
||||
<th scope="col">Куб-опер, км</th>
|
||||
|
||||
<!-- Столбец "Обновлено" -->
|
||||
<th scope="col">
|
||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'updated_at' %}-updated_at{% elif sort == '-updated_at' %}updated_at{% else %}updated_at{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
|
||||
Обновлено
|
||||
{% if sort == 'updated_at' %} <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"><i class="bi bi-x-lg"></i></a> {% elif sort == '-updated_at' %} <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"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
|
||||
</a>
|
||||
</th>
|
||||
|
||||
<!-- Столбец "Кем (обновление)" - без сортировки -->
|
||||
<th scope="col">Кем(обн)</th>
|
||||
|
||||
<!-- Столбец "Создано" -->
|
||||
<th scope="col">
|
||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'created_at' %}-created_at{% elif sort == '-created_at' %}created_at{% else %}created_at{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
|
||||
Создано
|
||||
{% if sort == 'created_at' %} <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"><i class="bi bi-x-lg"></i></a> {% elif sort == '-created_at' %} <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"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
|
||||
</a>
|
||||
</th>
|
||||
|
||||
<!-- Столбец "Кем (создание)" - без сортировки -->
|
||||
<th scope="col">Кем(созд)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in processed_objects %}
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" class="form-check-input item-checkbox" value="{{ item.id }}">
|
||||
</td>
|
||||
<td><a href="{% if item.obj.id %}{% url 'objitem_update' item.obj.id %}{% endif %}">{{ item.name }}</a></td>
|
||||
<td>{{ item.satellite_name }}</td>
|
||||
<td>{{ item.frequency }}</td>
|
||||
<td>{{ item.freq_range }}</td>
|
||||
<td>{{ item.polarization }}</td>
|
||||
<td>{{ item.bod_velocity }}</td>
|
||||
<td>{{ item.modulation }}</td>
|
||||
<td>{{ item.snr }}</td>
|
||||
<td>{{ item.geo_timestamp|date:"d.m.Y H:i" }}</td>
|
||||
<td>{{ item.geo_location}}</td>
|
||||
<td>{{ item.geo_coords }}</td>
|
||||
<td>{{ item.kupsat_coords }}</td>
|
||||
<td>{{ item.valid_coords }}</td>
|
||||
<td>{{ item.distance_geo_kup }}</td>
|
||||
<td>{{ item.distance_geo_valid }}</td>
|
||||
<td>{{ item.distance_kup_valid }}</td>
|
||||
<td>{{ item.obj.updated_at|date:"d.m.Y H:i" }}</td>
|
||||
<td>{{ item.updated_by }}</td>
|
||||
<td>{{ item.obj.created_at|date:"d.m.Y H:i" }}</td>
|
||||
<td>{{ item.obj.created_by }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="19" class="text-center py-4">
|
||||
{% if selected_satellite_id %}
|
||||
Нет данных для выбранных фильтров
|
||||
{% else %}
|
||||
Пожалуйста, выберите спутник для отображения данных
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
let lastCheckedIndex = null;
|
||||
|
||||
function updateRowHighlight(checkbox) {
|
||||
const row = checkbox.closest('tr');
|
||||
if (checkbox.checked) {
|
||||
row.classList.add('selected');
|
||||
} else {
|
||||
row.classList.remove('selected');
|
||||
}
|
||||
}
|
||||
|
||||
function handleCheckboxClick(e) {
|
||||
if (e.shiftKey && lastCheckedIndex !== null) {
|
||||
const checkboxes = document.querySelectorAll('.item-checkbox');
|
||||
const currentIndex = Array.from(checkboxes).indexOf(e.target);
|
||||
const startIndex = Math.min(lastCheckedIndex, currentIndex);
|
||||
const endIndex = Math.max(lastCheckedIndex, currentIndex);
|
||||
|
||||
for (let i = startIndex; i <= endIndex; i++) {
|
||||
checkboxes[i].checked = e.target.checked;
|
||||
updateRowHighlight(checkboxes[i]);
|
||||
}
|
||||
} else {
|
||||
updateRowHighlight(e.target);
|
||||
}
|
||||
lastCheckedIndex = Array.from(document.querySelectorAll('.item-checkbox')).indexOf(e.target);
|
||||
}
|
||||
|
||||
// Function to show selected objects on map
|
||||
function showSelectedOnMap() {
|
||||
// Get all checked checkboxes
|
||||
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked');
|
||||
|
||||
if (checkedCheckboxes.length === 0) {
|
||||
alert('Пожалуйста, выберите хотя бы один объект для отображения на карте');
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract IDs from checked checkboxes
|
||||
const selectedIds = [];
|
||||
checkedCheckboxes.forEach(checkbox => {
|
||||
selectedIds.push(checkbox.value);
|
||||
});
|
||||
|
||||
// Redirect to the map view with selected IDs as query parameter
|
||||
const url = '{% url "show_selected_objects_map" %}' + '?ids=' + selectedIds.join(',');
|
||||
window.open(url, '_blank'); // Open in a new tab
|
||||
}
|
||||
|
||||
function clearSelections() {
|
||||
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
|
||||
itemCheckboxes.forEach(checkbox => {
|
||||
checkbox.checked = false;
|
||||
});
|
||||
// Also uncheck the select-all checkbox if it exists
|
||||
const selectAllCheckbox = document.getElementById('select-all');
|
||||
if (selectAllCheckbox) {
|
||||
selectAllCheckbox.checked = false;
|
||||
}
|
||||
// Remove selected class from rows
|
||||
const selectedRows = document.querySelectorAll('tr.selected');
|
||||
selectedRows.forEach(row => {
|
||||
row.classList.remove('selected');
|
||||
});
|
||||
}
|
||||
|
||||
// Function to delete selected objects
|
||||
function deleteSelectedObjects() {
|
||||
// Get all checked checkboxes
|
||||
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked');
|
||||
|
||||
if (checkedCheckboxes.length === 0) {
|
||||
alert('Пожалуйста, выберите хотя бы один объект для удаления');
|
||||
return;
|
||||
}
|
||||
|
||||
// Confirm deletion with user
|
||||
if (!confirm(`Вы уверены, что хотите удалить ${checkedCheckboxes.length} объект(ов)?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract IDs from checked checkboxes
|
||||
const selectedIds = [];
|
||||
checkedCheckboxes.forEach(checkbox => {
|
||||
selectedIds.push(checkbox.value);
|
||||
});
|
||||
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
// Does this cookie string begin with the name we want?
|
||||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
const csrftoken = getCookie('csrftoken');
|
||||
|
||||
|
||||
// Prepare request headers
|
||||
const headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
};
|
||||
|
||||
// Add CSRF token to headers only if it exists
|
||||
if (csrftoken) {
|
||||
headers['X-CSRFToken'] = csrftoken;
|
||||
}
|
||||
|
||||
// Send AJAX request to delete selected objects
|
||||
fetch('{% url "delete_selected_objects" %}', {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: 'ids=' + selectedIds.join(',')
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(data.message);
|
||||
clearSelections();
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Ошибка: ' + (data.error || 'Неизвестная ошибка'));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Произошла ошибка при удалении объектов');
|
||||
});
|
||||
}
|
||||
|
||||
// Остальной ваш JavaScript код остается без изменений
|
||||
function toggleColumn(checkbox) {
|
||||
const columnIndex = parseInt(checkbox.getAttribute('data-column'));
|
||||
const table = document.querySelector('.table');
|
||||
const cells = table.querySelectorAll(`td:nth-child(${columnIndex + 1}), th:nth-child(${columnIndex + 1})`);
|
||||
|
||||
if (checkbox.checked) {
|
||||
cells.forEach(cell => {
|
||||
cell.style.display = '';
|
||||
});
|
||||
} else {
|
||||
cells.forEach(cell => {
|
||||
cell.style.display = 'none';
|
||||
});
|
||||
}
|
||||
}
|
||||
function toggleAllColumns(selectAllCheckbox) {
|
||||
const columnCheckboxes = document.querySelectorAll('.column-toggle');
|
||||
columnCheckboxes.forEach(checkbox => {
|
||||
checkbox.checked = selectAllCheckbox.checked;
|
||||
toggleColumn(checkbox);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const selectAllCheckbox = document.getElementById('select-all');
|
||||
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
|
||||
|
||||
if (selectAllCheckbox && itemCheckboxes.length > 0) {
|
||||
selectAllCheckbox.addEventListener('change', function() {
|
||||
itemCheckboxes.forEach(checkbox => {
|
||||
checkbox.checked = selectAllCheckbox.checked;
|
||||
});
|
||||
});
|
||||
|
||||
itemCheckboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function() {
|
||||
const allChecked = Array.from(itemCheckboxes).every(cb => cb.checked);
|
||||
selectAllCheckbox.checked = allChecked;
|
||||
});
|
||||
});
|
||||
|
||||
// Добавляем обработчик для выбора диапазона
|
||||
itemCheckboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('click', handleCheckboxClick);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Handle kubsat and valid coords checkboxes (mutually exclusive)
|
||||
// Add a function to handle radio-like behavior for these checkboxes
|
||||
function setupRadioLikeCheckboxes(name) {
|
||||
const checkboxes = document.querySelectorAll(`input[name="${name}"]`);
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function() {
|
||||
// If this checkbox is checked, uncheck the other
|
||||
if (this.checked) {
|
||||
checkboxes.forEach(other => {
|
||||
if (other !== this) {
|
||||
other.checked = false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// If both are unchecked, no action needed
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setupRadioLikeCheckboxes('has_kupsat');
|
||||
setupRadioLikeCheckboxes('has_valid');
|
||||
|
||||
// Function to select/deselect all options in a select element
|
||||
window.selectAllOptions = function(selectName, selectAll) {
|
||||
const selectElement = document.querySelector(`select[name="${selectName}"]`);
|
||||
if (selectElement) {
|
||||
for (let i = 0; i < selectElement.options.length; i++) {
|
||||
selectElement.options[i].selected = selectAll;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Get all current filter values and return as URL parameters
|
||||
function getAllFilterParams() {
|
||||
const form = document.getElementById('filter-form');
|
||||
const searchValue = document.getElementById('toolbar-search').value;
|
||||
|
||||
// Create URLSearchParams object from the form
|
||||
const params = new URLSearchParams(new FormData(form));
|
||||
|
||||
// Add search value from toolbar if present
|
||||
if (searchValue.trim() !== '') {
|
||||
params.set('search', searchValue);
|
||||
} else {
|
||||
params.delete('search');
|
||||
}
|
||||
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
// Function to perform search
|
||||
window.performSearch = function() {
|
||||
const filterParams = getAllFilterParams();
|
||||
window.location.search = filterParams;
|
||||
};
|
||||
|
||||
// Function to clear search
|
||||
window.clearSearch = function() {
|
||||
document.getElementById('toolbar-search').value = '';
|
||||
const filterParams = getAllFilterParams();
|
||||
window.location.search = filterParams;
|
||||
};
|
||||
|
||||
// Handle Enter key in toolbar search
|
||||
const toolbarSearch = document.getElementById('toolbar-search');
|
||||
if (toolbarSearch) {
|
||||
toolbarSearch.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
performSearch();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//const satelliteSelect = document.querySelector('select[name="satellite_id"]');
|
||||
//if (satelliteSelect) {
|
||||
//satelliteSelect.addEventListener('change', function() {
|
||||
// updateSatelliteSelection();
|
||||
// });
|
||||
//}
|
||||
|
||||
// Function to update items per page
|
||||
window.updateItemsPerPage = function() {
|
||||
const itemsPerPageSelect = document.getElementById('items-per-page');
|
||||
const currentParams = new URLSearchParams(window.location.search);
|
||||
|
||||
// Add or update the items_per_page parameter
|
||||
currentParams.set('items_per_page', itemsPerPageSelect.value);
|
||||
|
||||
// Remove page parameter to reset to first page when changing items per page
|
||||
currentParams.delete('page');
|
||||
|
||||
// Update URL and reload
|
||||
window.location.search = currentParams.toString();
|
||||
};
|
||||
|
||||
// Initialize column visibility - hide creation columns by default
|
||||
function initColumnVisibility() {
|
||||
const creationDateCheckbox = document.querySelector('input[data-column="19"]');
|
||||
const creationUserCheckbox = document.querySelector('input[data-column="20"]');
|
||||
const creationDistanceGOpCheckbox = document.querySelector('input[data-column="15"]');
|
||||
const creationDistanceKubOpCheckbox = document.querySelector('input[data-column="16"]');
|
||||
if (creationDistanceGOpCheckbox) {
|
||||
creationDistanceGOpCheckbox.checked = false;
|
||||
toggleColumn(creationDistanceGOpCheckbox);
|
||||
}
|
||||
if (creationDistanceKubOpCheckbox) {
|
||||
creationDistanceKubOpCheckbox.checked = false;
|
||||
toggleColumn(creationDistanceKubOpCheckbox);
|
||||
}
|
||||
|
||||
if (creationDateCheckbox) {
|
||||
creationDateCheckbox.checked = false;
|
||||
toggleColumn(creationDateCheckbox);
|
||||
}
|
||||
|
||||
if (creationUserCheckbox) {
|
||||
creationUserCheckbox.checked = false;
|
||||
toggleColumn(creationUserCheckbox);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize column visibility after page loads
|
||||
setTimeout(initColumnVisibility, 100); // Slight delay to ensure DOM is fully loaded
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
106
dbapp/mainapp/templates/mainapp/objitem_map.html
Normal file
@@ -0,0 +1,106 @@
|
||||
{% 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 + '<br>' + "{{ point_data.frequency|escapejs }}");
|
||||
|
||||
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 %}
|
||||
|
||||
// Create the layer control with a custom container that includes a select all checkbox
|
||||
var layerControl = L.control.layers.tree(baseLayers, overlays, {
|
||||
collapsed: false,
|
||||
autoZIndex: true
|
||||
});
|
||||
|
||||
// Add the layer control to the map
|
||||
layerControl.addTo(map);
|
||||
|
||||
// Calculate map bounds to fit all markers
|
||||
{% if groups %}
|
||||
var groupBounds = L.featureGroup([]);
|
||||
{% for group in groups %}
|
||||
{% for point_data in group.points %}
|
||||
groupBounds.addLayer(L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}]));
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
map.fitBounds(groupBounds.getBounds().pad(0.1)); // Add some padding
|
||||
{% else %}
|
||||
map.setView([55.75, 37.62], 5); // Default view if no markers
|
||||
{% endif %}
|
||||
|
||||
// Add a "Select All" checkbox functionality for all overlays
|
||||
setTimeout(function() {
|
||||
// Create a custom "select all" checkbox
|
||||
var selectAllContainer = document.createElement('div');
|
||||
selectAllContainer.className = 'leaflet-control-layers-select-all';
|
||||
selectAllContainer.style.padding = '5px';
|
||||
selectAllContainer.style.borderBottom = '1px solid #ccc';
|
||||
selectAllContainer.style.marginBottom = '5px';
|
||||
selectAllContainer.innerHTML = '<label><input type="checkbox" id="select-all-overlays" checked> Показать все точки</label>';
|
||||
|
||||
// Insert the checkbox at the top of the layer control
|
||||
var layerControlContainer = document.querySelector('.leaflet-control-layers-list');
|
||||
if (layerControlContainer) {
|
||||
layerControlContainer.insertBefore(selectAllContainer, layerControlContainer.firstChild);
|
||||
}
|
||||
|
||||
// Add event listener to the "select all" checkbox
|
||||
document.getElementById('select-all-overlays').addEventListener('change', function() {
|
||||
var isChecked = this.checked;
|
||||
|
||||
// Iterate through all overlays and toggle visibility
|
||||
for (var i = 0; i < overlays.length; i++) {
|
||||
if (isChecked) {
|
||||
map.addLayer(overlays[i].layer);
|
||||
} else {
|
||||
map.removeLayer(overlays[i].layer);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 500); // Slight delay to ensure the tree control has been rendered
|
||||
</script>
|
||||
{% endblock extra_js %}
|
||||
52
dbapp/mainapp/templates/mainapp/process_kubsat.html
Normal file
@@ -0,0 +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 'home' %}" class="btn btn-secondary">Назад</a>
|
||||
<button type="submit" class="btn btn-success">Выполнить</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
53
dbapp/mainapp/templates/mainapp/transponders_upload.html
Normal file
@@ -0,0 +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 '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 %}
|
||||
19
dbapp/mainapp/templates/registration/logged_out.html
Normal file
@@ -0,0 +1,19 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}Выход{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<h2 class="card-title">Вы вышли из системы</h2>
|
||||
<p class="card-text">Вы успешно вышли из системы.</p>
|
||||
<a href="{% url 'login' %}" class="btn btn-primary">Войти снова</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
43
dbapp/mainapp/templates/registration/login.html
Normal file
@@ -0,0 +1,43 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
|
||||
{% block title %}Вход в систему{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-center">Вход в систему</h2>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.username.id_for_label }}" class="form-label">Имя пользователя</label>
|
||||
{{ form.username }}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.password.id_for_label }}" class="form-label">Пароль</label>
|
||||
{{ form.password }}
|
||||
</div>
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger">
|
||||
{% for field in form %}
|
||||
{% for error in field.errors %}
|
||||
<p>{{ error }}</p>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% for error in form.non_field_errors %}
|
||||
<p>{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary">Войти</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -5,16 +5,24 @@ from . import views
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.HomePageView.as_view(), name='home'),
|
||||
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>/delete/', views.ObjItemDeleteView.as_view(), name='objitem_delete'),
|
||||
# path('upload/', views.upload_file, name='upload_file'),
|
||||
|
||||
]
|
||||
@@ -10,12 +10,18 @@ from .models import (
|
||||
ObjItem,
|
||||
CustomUser
|
||||
)
|
||||
from mapsapp.models import Transponders
|
||||
from datetime import datetime, time
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from django.contrib.gis.geos import Point
|
||||
import json
|
||||
import re
|
||||
import io
|
||||
from django.db.models import F, Count, Exists, OuterRef, Min, Max
|
||||
from geopy.geocoders import Nominatim
|
||||
import reverse_geocoder as rg
|
||||
from time import sleep
|
||||
|
||||
def get_all_constants():
|
||||
sats = [sat.name for sat in Satellite.objects.all()]
|
||||
@@ -46,7 +52,7 @@ def remove_str(s: str):
|
||||
return float(s.strip().replace(",", "."))
|
||||
return s
|
||||
|
||||
def fill_data_from_df(df: pd.DataFrame, sat: Satellite):
|
||||
def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
|
||||
try:
|
||||
df.rename(columns={'Модуляция ': 'Модуляция'}, inplace=True)
|
||||
except Exception as e:
|
||||
@@ -74,11 +80,15 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite):
|
||||
freq = remove_str(stroka[1]['Частота, МГц'])
|
||||
freq_line = remove_str(stroka[1]['Полоса, МГц'])
|
||||
v = remove_str(stroka[1]['Символьная скорость, БОД'])
|
||||
try:
|
||||
mod_obj, _ = Modulation.objects.get_or_create(name=stroka[1]['Модуляция'].strip())
|
||||
except AttributeError:
|
||||
mod_obj, _ = Modulation.objects.get_or_create(name='-')
|
||||
snr = remove_str(stroka[1]['ОСШ'])
|
||||
date = stroka[1]['Дата'].date()
|
||||
time_ = stroka[1]['Время']
|
||||
if isinstance(time_, str):
|
||||
time_ = time_.strip()
|
||||
time_ = time(0,0,0)
|
||||
timestamp = datetime.combine(date, time_)
|
||||
current_mirrors = []
|
||||
@@ -101,8 +111,9 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite):
|
||||
location = stroka[1]['Местоопределение'].strip()
|
||||
comment = stroka[1]['Комментарий']
|
||||
source = stroka[1]['Объект наблюдения']
|
||||
user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
|
||||
|
||||
vch_load_obj, vch_created = Parameter.objects.get_or_create(
|
||||
vch_load_obj, _ = Parameter.objects.get_or_create(
|
||||
id_satellite=sat,
|
||||
polarization=polarization_obj,
|
||||
frequency=freq,
|
||||
@@ -110,7 +121,6 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite):
|
||||
bod_velocity=v,
|
||||
modulation=mod_obj,
|
||||
snr=snr,
|
||||
defaults={'id_user_add': CustomUser.objects.get(id=1)}
|
||||
)
|
||||
|
||||
geo, _ = Geo.objects.get_or_create(
|
||||
@@ -122,23 +132,24 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite):
|
||||
'location': location,
|
||||
'comment': comment,
|
||||
'is_average': (comment != -1.0),
|
||||
'id_user_add': CustomUser.objects.get(id=1)
|
||||
}
|
||||
)
|
||||
geo.save()
|
||||
geo.mirrors.set(Mirror.objects.filter(name__in=current_mirrors))
|
||||
|
||||
obj_item, _ = ObjItem.objects.get_or_create(
|
||||
id_geo=geo,
|
||||
id_vch_load=vch_load_obj,
|
||||
defaults={
|
||||
'name': source,
|
||||
'id_user_add': CustomUser.objects.get(id=1),
|
||||
# 'id_satellite': sat
|
||||
}
|
||||
existing_obj_items = ObjItem.objects.filter(
|
||||
parameters_obj=vch_load_obj,
|
||||
geo_obj=geo
|
||||
)
|
||||
if not existing_obj_items.exists():
|
||||
obj_item = ObjItem.objects.create(
|
||||
name=source,
|
||||
created_by=user_to_use
|
||||
)
|
||||
obj_item.parameters_obj.set([vch_load_obj])
|
||||
geo.objitem = obj_item
|
||||
geo.save()
|
||||
|
||||
obj_item.save()
|
||||
|
||||
|
||||
def add_satellite_list():
|
||||
@@ -191,18 +202,8 @@ def get_point_from_json(filepath: str):
|
||||
|
||||
|
||||
|
||||
def get_points_from_csv(file_content):
|
||||
import io
|
||||
if hasattr(file_content, 'read'):
|
||||
content = file_content.read()
|
||||
if isinstance(content, bytes):
|
||||
content = content.decode('utf-8')
|
||||
else:
|
||||
if isinstance(file_content, bytes):
|
||||
content = content.decode('utf-8')
|
||||
else:
|
||||
content = file_content
|
||||
df = pd.read_csv(io.StringIO(content), sep=";",
|
||||
def get_points_from_csv(file_content, current_user=None):
|
||||
df = pd.read_csv(io.StringIO(file_content), sep=";",
|
||||
names=['id', 'obj', 'lat', 'lon', 'h', 'time', 'sat', 'norad_id', 'freq', 'f_range', 'et', 'qaul', 'mir_1', 'mir_2', 'mir_3'])
|
||||
df[['lat', 'lon', 'freq', 'f_range']] = df[['lat', 'lon', 'freq', 'f_range']].replace(',', '.', regex=True).astype(float)
|
||||
df['time'] = pd.to_datetime(df['time'], format='%d.%m.%Y %H:%M:%S')
|
||||
@@ -238,12 +239,14 @@ def get_points_from_csv(file_content):
|
||||
|
||||
name=row['mir_3']
|
||||
)
|
||||
user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
|
||||
|
||||
vch_load_obj, _ = Parameter.objects.get_or_create(
|
||||
id_satellite=sat_obj,
|
||||
polarization=pol_obj,
|
||||
frequency=row['freq'],
|
||||
freq_range=row['f_range'],
|
||||
defaults={'id_user_add': CustomUser.objects.get(id=1)}
|
||||
# defaults={'id_user_add': user_to_use}
|
||||
)
|
||||
|
||||
geo_obj, _ = Geo.objects.get_or_create(
|
||||
@@ -251,88 +254,39 @@ def get_points_from_csv(file_content):
|
||||
coords=Point(row['lon'], row['lat'], srid=4326),
|
||||
defaults={
|
||||
'is_average': False,
|
||||
'id_user_add': CustomUser.objects.get(id=1),
|
||||
# 'id_user_add': user_to_use,
|
||||
}
|
||||
)
|
||||
geo_obj.mirrors.set(Mirror.objects.filter(name__in=mir_lst))
|
||||
|
||||
obj_item_obj, _ = ObjItem.objects.get_or_create(
|
||||
name=row['obj'],
|
||||
# id_satellite=sat_obj,
|
||||
id_vch_load=vch_load_obj,
|
||||
id_geo=geo_obj,
|
||||
defaults={
|
||||
'id_user_add': CustomUser.objects.get(id=1)
|
||||
}
|
||||
existing_obj_items = ObjItem.objects.filter(
|
||||
parameters_obj=vch_load_obj,
|
||||
geo_obj=geo_obj
|
||||
)
|
||||
obj_item_obj.save()
|
||||
# df = pd.read_csv(filepath, sep=";",
|
||||
# names=['id', 'obj', 'lat', 'lon', 'h', 'time', 'sat', 'norad_id', 'freq', 'f_range', 'et', 'qaul', 'mir_1', 'mir_2', 'mir_3'])
|
||||
# df[['lat', 'lon', 'freq', 'f_range']] = df[['lat', 'lon', 'freq', 'f_range']].replace(',', '.', regex=True).astype(float)
|
||||
# df['time'] = pd.to_datetime(df['time'], format='%d.%m.%Y %H:%M:%S')
|
||||
# for row in df.iterrows():
|
||||
# row = row[1]
|
||||
# match row['obj'].split(' ')[-1]:
|
||||
# case 'V':
|
||||
# pol = 'Вертикальная'
|
||||
# case 'H':
|
||||
# pol = 'Горизонтальная'
|
||||
# case 'R':
|
||||
# pol = 'Правая'
|
||||
# case 'L':
|
||||
# pol = 'Левая'
|
||||
# case _:
|
||||
# pol = '-'
|
||||
# pol_obj, _ = Polarization.objects.get_or_create(
|
||||
# name=pol
|
||||
# )
|
||||
# sat_obj, _ = Satellite.objects.get_or_create(
|
||||
# name=row['sat'],
|
||||
# defaults={'norad': row['norad_id']}
|
||||
# )
|
||||
# mir_1_obj, _ = Mirror.objects.get_or_create(
|
||||
# name=row['mir_1']
|
||||
# )
|
||||
# mir_2_obj, _ = Mirror.objects.get_or_create(
|
||||
# name=row['mir_2']
|
||||
# )
|
||||
# mir_lst = [row['mir_1'], row['mir_2']]
|
||||
# if not pd.isna(row['mir_3']):
|
||||
# mir_3_obj, _ = Mirror.objects.get_or_create(
|
||||
|
||||
# name=row['mir_3']
|
||||
# )
|
||||
# vch_load_obj, _ = Parameter.objects.get_or_create(
|
||||
# id_satellite=sat_obj,
|
||||
# polarization=pol_obj,
|
||||
# frequency=row['freq'],
|
||||
# freq_range=row['f_range'],
|
||||
# defaults={'id_user_add': CustomUser.objects.get(id=1)}
|
||||
# )
|
||||
|
||||
# geo_obj, _ = Geo.objects.get_or_create(
|
||||
# timestamp=row['time'],
|
||||
# coords=Point(row['lon'], row['lat'], srid=4326),
|
||||
# defaults={
|
||||
# 'is_average': False,
|
||||
# 'id_user_add': CustomUser.objects.get(id=1),
|
||||
# }
|
||||
# )
|
||||
# geo_obj.mirrors.set(Mirror.objects.filter(name__in=mir_lst))
|
||||
|
||||
# obj_item_obj, _ = ObjItem.objects.get_or_create(
|
||||
# name=row['obj'],
|
||||
# # id_satellite=sat_obj,
|
||||
# id_vch_load=vch_load_obj,
|
||||
# id_geo=geo_obj,
|
||||
# defaults={
|
||||
# 'id_user_add': CustomUser.objects.get(id=1)
|
||||
# }
|
||||
# )
|
||||
# obj_item_obj.save()
|
||||
|
||||
if not existing_obj_items.exists():
|
||||
obj_item = ObjItem.objects.create(
|
||||
name=row['obj'],
|
||||
created_by=user_to_use
|
||||
)
|
||||
obj_item.parameters_obj.set([vch_load_obj])
|
||||
geo_obj.objitem = obj_item
|
||||
geo_obj.save()
|
||||
|
||||
def get_vch_load_from_html(file, sat: Satellite) -> None:
|
||||
filename = file.name.split('_')
|
||||
transfer = filename[3]
|
||||
match filename[2]:
|
||||
case 'H':
|
||||
pol = 'Горизонтальная'
|
||||
case 'V':
|
||||
pol = 'Вертикальная'
|
||||
case 'R':
|
||||
pol = 'Правая'
|
||||
case 'L':
|
||||
pol = 'Левая'
|
||||
case _:
|
||||
pol = '-'
|
||||
|
||||
tables = pd.read_html(file, encoding='windows-1251')
|
||||
df = tables[0]
|
||||
df = df.drop(0).reset_index(drop=True)
|
||||
@@ -362,6 +316,10 @@ def get_vch_load_from_html(file, sat: Satellite) -> None:
|
||||
else:
|
||||
pack = None
|
||||
|
||||
polarization, _ = Polarization.objects.get_or_create(
|
||||
name=pol
|
||||
)
|
||||
|
||||
mod, _ = Modulation.objects.get_or_create(
|
||||
name=value['Модуляция']
|
||||
)
|
||||
@@ -372,7 +330,10 @@ def get_vch_load_from_html(file, sat: Satellite) -> None:
|
||||
id_satellite=sat,
|
||||
frequency=value['Частота, МГц'],
|
||||
freq_range=value['Полоса, МГц'],
|
||||
polarization=polarization,
|
||||
defaults={
|
||||
"transfer": float(transfer),
|
||||
# "polarization": polarization,
|
||||
"status": value['Статус'],
|
||||
"power": value['Мощность, дБм'],
|
||||
"bod_velocity": bod_velocity,
|
||||
@@ -386,30 +347,79 @@ def get_vch_load_from_html(file, sat: Satellite) -> None:
|
||||
)
|
||||
sigma_load.save()
|
||||
|
||||
def define_ku_transfer(min_freq: float, max_freq: float) -> int | None:
|
||||
fss = (10700, 11700)
|
||||
dss = (11700, 12750)
|
||||
if min_freq + 9750 >= fss[0] and max_freq + 9750 <= fss[1]:
|
||||
return 9750
|
||||
elif min_freq + 10750 >= dss[0] and max_freq + 10750 <= dss[1]:
|
||||
return 10750
|
||||
return None
|
||||
|
||||
def compare_and_link_vch_load(sat_id: Satellite, eps_freq: float, eps_frange: float, ku_range: float):
|
||||
item_obj = ObjItem.objects.filter(id_vch_load__id_satellite=sat_id)
|
||||
item_obj = ObjItem.objects.filter(parameters_obj__id_satellite=sat_id)
|
||||
vch_sigma = SigmaParameter.objects.filter(id_satellite=sat_id)
|
||||
link_count = 0
|
||||
obj_count = len(item_obj)
|
||||
for idx, obj in enumerate(item_obj):
|
||||
vch_load = obj.id_vch_load
|
||||
vch_load = obj.parameters_obj.get()
|
||||
if vch_load.frequency == -1.0:
|
||||
continue
|
||||
# if unique_points = Point.objects.order_by('frequency').distinct('frequency')
|
||||
for sigma in vch_sigma:
|
||||
if abs(sigma.frequency + ku_range - vch_load.frequency) <= vch_load.frequency*eps_freq/100 and abs(sigma.freq_range - vch_load.freq_range) <= vch_load.freq_range*eps_frange/100:
|
||||
if (
|
||||
abs(sigma.transfer_frequency - vch_load.frequency) <= eps_freq and
|
||||
abs(sigma.freq_range - vch_load.freq_range) <= vch_load.freq_range*eps_frange/100 and
|
||||
sigma.polarization == vch_load.polarization
|
||||
):
|
||||
sigma.parameter = vch_load
|
||||
sigma.save()
|
||||
link_count += 1
|
||||
return obj_count, link_count
|
||||
|
||||
def kub_report(data_in: io.StringIO) -> pd.DataFrame:
|
||||
df_in = pd.read_excel(data_in)
|
||||
df = pd.DataFrame(columns=['Дата', 'Широта', 'Долгота',
|
||||
'Высота', 'Населённый пункт', 'ИСЗ',
|
||||
'Прямой канал, МГц', 'Обратный канал, МГц', 'Перенос, МГц', 'Полоса, МГц', 'Зеркала'])
|
||||
for row in df_in.iterrows():
|
||||
value = row[1]
|
||||
date = datetime.date(datetime.now())
|
||||
isz = value['ИСЗ']
|
||||
try:
|
||||
lat = float(value['Широта, град'].strip().replace(',', '.'))
|
||||
lon = float(value['Долгота, град'].strip().replace(',', '.'))
|
||||
downlink = float(value['Обратный канал, МГц'].strip().replace(',', '.'))
|
||||
freq_range = float(value['Полоса, МГц'].strip().replace(',', '.'))
|
||||
except Exception as e:
|
||||
lat = value['Широта, град']
|
||||
lon = value['Долгота, град']
|
||||
downlink = value['Обратный канал, МГц']
|
||||
freq_range = value['Полоса, МГц']
|
||||
print(e)
|
||||
norad = int(re.findall(r'\((\d+)\)', isz)[0])
|
||||
sat_obj = Satellite.objects.get(norad=norad)
|
||||
pol_obj = Polarization.objects.get(name=value['Поляризация'].strip())
|
||||
transponder = Transponders.objects.filter(
|
||||
sat_id=sat_obj,
|
||||
polarization=pol_obj,
|
||||
downlink__gte=downlink - F('frequency_range')/2,
|
||||
downlink__lte=downlink + F('frequency_range')/2,
|
||||
).first()
|
||||
# try:
|
||||
# location = geolocator.reverse(f"{lat}, {lon}", language="ru").raw['address']
|
||||
# loc_name = location.get('city', '') or location.get('town', '') or location.get('province', '') or location.get('country', '')
|
||||
# except AttributeError:
|
||||
# loc_name = ''
|
||||
# sleep(1)
|
||||
loc_name = ''
|
||||
if transponder: #and not (len(transponder) > 1):
|
||||
transfer = transponder.transfer
|
||||
uplink = transfer + downlink
|
||||
new_row = pd.DataFrame([{'Дата': date,
|
||||
'Широта': lat,
|
||||
'Долгота': lon,
|
||||
'Высота': 0.0,
|
||||
'Населённый пункт': loc_name,
|
||||
'ИСЗ': isz,
|
||||
'Прямой канал, МГц': uplink,
|
||||
'Обратный канал, МГц': downlink,
|
||||
'Перенос, МГц': transfer,
|
||||
'Полоса, МГц': freq_range,
|
||||
'Зеркала': ''
|
||||
}])
|
||||
df = pd.concat([df, new_row], ignore_index=True)
|
||||
else:
|
||||
print("Ничего не найдено в транспондерах")
|
||||
return df
|
||||
|
||||
@@ -1,45 +1,100 @@
|
||||
from django.shortcuts import render, redirect
|
||||
from django.contrib import messages
|
||||
from django.http import JsonResponse
|
||||
from django.http import JsonResponse, HttpResponse
|
||||
from django.views.decorators.http import require_GET
|
||||
from django.contrib.admin.views.decorators import staff_member_required
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
from django.views.generic import TemplateView, FormView
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
from django.db.models import OuterRef, Subquery
|
||||
from django.views.generic import TemplateView, FormView, UpdateView, DeleteView, CreateView
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin, LoginRequiredMixin
|
||||
from django.contrib.auth import logout
|
||||
from django.forms import inlineformset_factory, modelformset_factory
|
||||
from django.db import models
|
||||
from django.urls import reverse_lazy
|
||||
from django.contrib.gis.geos import Point
|
||||
import pandas as pd
|
||||
from .utils import (
|
||||
fill_data_from_df,
|
||||
add_satellite_list,
|
||||
get_points_from_csv,
|
||||
get_vch_load_from_html,
|
||||
compare_and_link_vch_load
|
||||
compare_and_link_vch_load,
|
||||
kub_report
|
||||
)
|
||||
from mapsapp.utils import parse_transponders_from_json
|
||||
from .forms import LoadExcelData, LoadCsvData, UploadFileForm, VchLinkForm
|
||||
from .models import ObjItem
|
||||
from mapsapp.utils import parse_transponders_from_json, parse_transponders_from_xml
|
||||
from .forms import (
|
||||
LoadExcelData,
|
||||
LoadCsvData,
|
||||
UploadFileForm,
|
||||
VchLinkForm,
|
||||
UploadVchLoad,
|
||||
NewEventForm,
|
||||
ObjItemForm,
|
||||
ParameterForm,
|
||||
GeoForm
|
||||
)
|
||||
from .models import ObjItem, Modulation, Polarization
|
||||
from .clusters import get_clusters
|
||||
from dbapp.settings import BASE_DIR
|
||||
from io import BytesIO
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class AddSatellitesView(View):
|
||||
|
||||
class AddSatellitesView(LoginRequiredMixin, View):
|
||||
def get(self, request):
|
||||
add_satellite_list()
|
||||
return redirect('home')
|
||||
|
||||
class AddTranspondersView(View):
|
||||
def get(self, request):
|
||||
# class AddTranspondersView(View):
|
||||
# def get(self, request):
|
||||
# try:
|
||||
# parse_transponders_from_json(BASE_DIR / "transponders.json")
|
||||
# except FileNotFoundError:
|
||||
# print("Файл не найден")
|
||||
# return redirect('home')
|
||||
|
||||
class AddTranspondersView(LoginRequiredMixin, FormView):
|
||||
template_name = 'mainapp/transponders_upload.html'
|
||||
form_class = UploadFileForm
|
||||
|
||||
def form_valid(self, form):
|
||||
uploaded_file = self.request.FILES['file']
|
||||
try:
|
||||
parse_transponders_from_json(BASE_DIR / "transponders.json")
|
||||
except FileNotFoundError:
|
||||
print("Файл не найден")
|
||||
return redirect('home')
|
||||
content = uploaded_file.read()
|
||||
parse_transponders_from_xml(BytesIO(content))
|
||||
messages.success(self.request, "Файл успешно обработан")
|
||||
except ValueError as e:
|
||||
messages.error(self.request, f"Ошибка при чтении таблиц: {e}")
|
||||
except Exception as e:
|
||||
messages.error(self.request, f"Неизвестная ошибка: {e}")
|
||||
return redirect('add_trans')
|
||||
|
||||
class HomePageView(TemplateView):
|
||||
template_name = 'mainapp/home.html'
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, "Форма заполнена некорректно.")
|
||||
return super().form_invalid(form)
|
||||
|
||||
from django.views.generic import View
|
||||
|
||||
class ActionsPageView(View):
|
||||
def get(self, request):
|
||||
if request.user.is_authenticated:
|
||||
return render(request, 'mainapp/actions.html')
|
||||
else:
|
||||
return render(request, 'mainapp/login_required.html')
|
||||
|
||||
|
||||
class LoadExcelDataView(FormView):
|
||||
class HomePageView(View):
|
||||
def get(self, request):
|
||||
if request.user.is_authenticated:
|
||||
# Redirect to objitem list if authenticated
|
||||
return redirect('objitem_list')
|
||||
else:
|
||||
return render(request, 'mainapp/login_required.html')
|
||||
|
||||
|
||||
class LoadExcelDataView(LoginRequiredMixin, FormView):
|
||||
template_name = 'mainapp/add_data_from_excel.html'
|
||||
form_class = LoadExcelData
|
||||
|
||||
@@ -53,7 +108,7 @@ class LoadExcelDataView(FormView):
|
||||
df = pd.read_excel(io.BytesIO(uploaded_file.read()))
|
||||
if number > 0:
|
||||
df = df.head(number)
|
||||
result = fill_data_from_df(df, selected_sat)
|
||||
result = fill_data_from_df(df, selected_sat, self.request.user.customuser)
|
||||
|
||||
messages.success(self.request, f"Данные успешно загружены! Обработано строк: {result}")
|
||||
return redirect('load_excel_data')
|
||||
@@ -67,26 +122,30 @@ class LoadExcelDataView(FormView):
|
||||
|
||||
|
||||
from django.views.generic import View
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Prefetch
|
||||
from .models import Satellite, ObjItem, Parameter, Geo
|
||||
|
||||
class GetLocationsView(View):
|
||||
class GetLocationsView(LoginRequiredMixin, View):
|
||||
def get(self, request, sat_id):
|
||||
locations = ObjItem.objects.filter(id_vch_load__id_satellite=sat_id)
|
||||
locations = ObjItem.objects.filter(parameters_obj__id_satellite=sat_id)
|
||||
if not locations:
|
||||
return JsonResponse({'error': 'Объектов не найдено'}, status=400)
|
||||
|
||||
features = []
|
||||
for loc in locations:
|
||||
param = loc.parameters_obj.get()
|
||||
features.append({
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [loc.id_geo.coords[0], loc.id_geo.coords[1]]
|
||||
"coordinates": [loc.geo_obj.coords[0], loc.geo_obj.coords[1]]
|
||||
},
|
||||
"properties": {
|
||||
"pol": loc.id_vch_load.polarization.name,
|
||||
"freq": loc.id_vch_load.frequency*1000000,
|
||||
"pol": param.polarization.name,
|
||||
"freq": param.frequency*1000000,
|
||||
"name": f"{loc.name}",
|
||||
"id": loc.id_geo.id
|
||||
"id": loc.geo_obj.id
|
||||
}
|
||||
})
|
||||
|
||||
@@ -95,7 +154,7 @@ class GetLocationsView(View):
|
||||
"features": features
|
||||
})
|
||||
|
||||
class LoadCsvDataView(FormView):
|
||||
class LoadCsvDataView(LoginRequiredMixin, FormView):
|
||||
template_name = 'mainapp/add_data_from_csv.html'
|
||||
form_class = LoadCsvData
|
||||
|
||||
@@ -107,7 +166,7 @@ class LoadCsvDataView(FormView):
|
||||
if isinstance(content, bytes):
|
||||
content = content.decode('utf-8')
|
||||
|
||||
get_points_from_csv(content)
|
||||
get_points_from_csv(content, self.request.user.customuser)
|
||||
messages.success(self.request, f"Данные успешно загружены!")
|
||||
return redirect('load_csv_data')
|
||||
except Exception as e:
|
||||
@@ -118,22 +177,7 @@ class LoadCsvDataView(FormView):
|
||||
messages.error(self.request, "Форма заполнена некорректно.")
|
||||
return super().form_invalid(form)
|
||||
|
||||
# def upload_file(request):
|
||||
# if request.method == 'POST' and request.FILES:
|
||||
# form = UploadFileForm(request.POST, request.FILES)
|
||||
# if form.is_valid():
|
||||
# uploaded_file = request.FILES['file']
|
||||
# # Обработка текстового файла, например:
|
||||
# df = pd.read_csv(uploaded_file)
|
||||
# df = pd.read_csv(filepath, sep=";",
|
||||
# names=['id', 'obj', 'lat', 'lon', 'h', 'time', 'sat', 'norad_id', 'freq', 'f_range', 'et', 'qaul', 'mir_1', 'mir_2', 'mir_3'])
|
||||
# df[['lat', 'lon', 'freq', 'f_range']] = df[['lat', 'lon', 'freq', 'f_range']].replace(',', '.', regex=True).astype(float)
|
||||
# df['time'] = pd.to_datetime(df['time'], format='%d.%m.%Y %H:%M:%S')
|
||||
# get_points_from_csv(df)
|
||||
# return JsonResponse({'status': 'success'})
|
||||
# else:
|
||||
# return JsonResponse({'status': 'error', 'errors': form.errors}, status=400)
|
||||
# return render(request, 'mainapp/add_data_from_csv.html')
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
@method_decorator(staff_member_required, name='dispatch')
|
||||
@@ -146,12 +190,19 @@ class ShowMapView(UserPassesTestMixin, View):
|
||||
points = []
|
||||
if ids:
|
||||
id_list = [int(x) for x in ids.split(',') if x.isdigit()]
|
||||
locations = ObjItem.objects.filter(id__in=id_list)
|
||||
locations = ObjItem.objects.filter(id__in=id_list).prefetch_related(
|
||||
'parameters_obj__id_satellite',
|
||||
'parameters_obj__polarization',
|
||||
'parameters_obj__modulation',
|
||||
'parameters_obj__standard',
|
||||
'geo_obj'
|
||||
)
|
||||
for obj in locations:
|
||||
param = obj.parameters_obj.get()
|
||||
points.append({
|
||||
'name': f"{obj.name}",
|
||||
'freq': f"{obj.id_vch_load.frequency} [{obj.id_vch_load.freq_range}] МГц",
|
||||
'point': (obj.id_geo.coords.x, obj.id_geo.coords.y)
|
||||
'freq': f"{param.frequency} [{param.freq_range}] МГц",
|
||||
'point': (obj.geo_obj.coords.x, obj.geo_obj.coords.y)
|
||||
})
|
||||
else:
|
||||
return redirect('admin')
|
||||
@@ -162,7 +213,6 @@ class ShowMapView(UserPassesTestMixin, View):
|
||||
'frequency': p["freq"]
|
||||
})
|
||||
|
||||
# Преобразуем в список словарей для удобства в шаблоне
|
||||
groups = [
|
||||
{
|
||||
"name": name,
|
||||
@@ -178,19 +228,71 @@ class ShowMapView(UserPassesTestMixin, View):
|
||||
return render(request, 'admin/map_custom.html', context)
|
||||
|
||||
|
||||
class ClusterTestView(View):
|
||||
class ShowSelectedObjectsMapView(LoginRequiredMixin, View):
|
||||
def get(self, request):
|
||||
ids = request.GET.get('ids', '')
|
||||
points = []
|
||||
if ids:
|
||||
id_list = [int(x) for x in ids.split(',') if x.isdigit()]
|
||||
locations = ObjItem.objects.filter(id__in=id_list).prefetch_related(
|
||||
'parameters_obj__id_satellite',
|
||||
'parameters_obj__polarization',
|
||||
'parameters_obj__modulation',
|
||||
'parameters_obj__standard',
|
||||
'geo_obj'
|
||||
)
|
||||
for obj in locations:
|
||||
param = obj.parameters_obj.get()
|
||||
points.append({
|
||||
'name': f"{obj.name}",
|
||||
'freq': f"{param.frequency} [{param.freq_range}] МГц",
|
||||
'point': (obj.geo_obj.coords.x, obj.geo_obj.coords.y)
|
||||
})
|
||||
else:
|
||||
return redirect('objitem_list')
|
||||
|
||||
# Group points by object name
|
||||
from collections import defaultdict
|
||||
grouped = defaultdict(list)
|
||||
for p in points:
|
||||
grouped[p["name"]].append({
|
||||
'point': p["point"],
|
||||
'frequency': p["freq"]
|
||||
})
|
||||
|
||||
groups = [
|
||||
{
|
||||
"name": name,
|
||||
"points": coords_list
|
||||
}
|
||||
for name, coords_list in grouped.items()
|
||||
]
|
||||
|
||||
context = {
|
||||
'groups': groups,
|
||||
}
|
||||
return render(request, 'mainapp/objitem_map.html', context)
|
||||
|
||||
|
||||
class ClusterTestView(LoginRequiredMixin, View):
|
||||
def get(self, request):
|
||||
objs = ObjItem.objects.filter(name__icontains="! Astra 4A 12654,040 [1,962] МГц H")
|
||||
coords = []
|
||||
for obj in objs:
|
||||
coords.append((obj.id_geo.coords[1], obj.id_geo.coords[0]))
|
||||
if obj.geo_obj and obj.geo_obj.coords:
|
||||
coords.append((obj.geo_obj.coords.coords[1], obj.geo_obj.coords.coords[0]))
|
||||
get_clusters(coords)
|
||||
|
||||
return JsonResponse({"success": "ок"})
|
||||
|
||||
class UploadVchLoadView(FormView):
|
||||
|
||||
def custom_logout(request):
|
||||
logout(request)
|
||||
return redirect('home')
|
||||
|
||||
class UploadVchLoadView(LoginRequiredMixin, FormView):
|
||||
template_name = 'mainapp/upload_html.html'
|
||||
form_class = UploadFileForm
|
||||
form_class = UploadVchLoad
|
||||
|
||||
def form_valid(self, form):
|
||||
selected_sat = form.cleaned_data['sat_choice']
|
||||
@@ -209,19 +311,608 @@ class UploadVchLoadView(FormView):
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class LinkVchSigmaView(FormView):
|
||||
class LinkVchSigmaView(LoginRequiredMixin, FormView):
|
||||
template_name = 'mainapp/link_vch.html'
|
||||
form_class = VchLinkForm
|
||||
|
||||
def form_valid(self, form):
|
||||
freq = form.cleaned_data['value1']
|
||||
freq_range = form.cleaned_data['value2']
|
||||
ku_range = float(form.cleaned_data['ku_range'])
|
||||
# ku_range = float(form.cleaned_data['ku_range'])
|
||||
sat_id = form.cleaned_data['sat_choice']
|
||||
# print(freq, freq_range, ku_range, sat_id.pk)
|
||||
count_all, link_count = compare_and_link_vch_load(sat_id, freq, freq_range, ku_range)
|
||||
count_all, link_count = compare_and_link_vch_load(sat_id, freq, freq_range, 1)
|
||||
messages.success(self.request, f"Привязано {link_count} из {count_all} объектов")
|
||||
return redirect('link_vch_sigma')
|
||||
|
||||
def form_invalid(self, form):
|
||||
return self.render_to_response(self.get_context_data(form=form))
|
||||
|
||||
|
||||
class ProcessKubsatView(LoginRequiredMixin, FormView):
|
||||
template_name = 'mainapp/process_kubsat.html'
|
||||
form_class = NewEventForm
|
||||
|
||||
def form_valid(self, form):
|
||||
# selected_sat = form.cleaned_data['sat_choice']
|
||||
# selected_pol = form.cleaned_data['pol_choice']
|
||||
uploaded_file = self.request.FILES['file']
|
||||
try:
|
||||
content = uploaded_file.read()
|
||||
df = kub_report(BytesIO(content))
|
||||
output = BytesIO()
|
||||
with pd.ExcelWriter(output, engine='openpyxl') as writer:
|
||||
df.to_excel(writer, index=False, sheet_name='Результат')
|
||||
output.seek(0)
|
||||
|
||||
response = HttpResponse(
|
||||
output.getvalue(),
|
||||
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
)
|
||||
response['Content-Disposition'] = f'attachment; filename="kubsat_report.xlsx"'
|
||||
|
||||
messages.success(self.request, "Событие успешно обработано!")
|
||||
return response
|
||||
except Exception as e:
|
||||
messages.error(self.request, f"Ошибка при обработке файла: {str(e)}")
|
||||
return redirect('kubsat_excel')
|
||||
# return redirect('kubsat_excel')
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, "Форма заполнена некорректно.")
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class DeleteSelectedObjectsView(LoginRequiredMixin, View):
|
||||
def post(self, request):
|
||||
if request.user.customuser.role not in ['admin', 'moderator']:
|
||||
return JsonResponse({'error': 'У вас нет прав для удаления объектов'}, status=403)
|
||||
|
||||
ids = request.POST.get('ids', '')
|
||||
if not ids:
|
||||
return JsonResponse({'error': 'Нет ID для удаления'}, status=400)
|
||||
|
||||
try:
|
||||
id_list = [int(x) for x in ids.split(',') if x.isdigit()]
|
||||
deleted_count, _ = ObjItem.objects.filter(id__in=id_list).delete()
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'message': 'Объект успешно удалён',
|
||||
# 'deleted_count': deleted_count
|
||||
})
|
||||
except Exception as e:
|
||||
return JsonResponse({'error': f'Ошибка при удалении: {str(e)}'}, status=500)
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
|
||||
class ObjItemListView(LoginRequiredMixin, View):
|
||||
def get(self, request):
|
||||
satellites = Satellite.objects.filter(parameters__objitems__isnull=False).distinct().order_by('name')
|
||||
|
||||
selected_sat_id = request.GET.get('satellite_id')
|
||||
page_number = request.GET.get('page', 1)
|
||||
items_per_page = request.GET.get('items_per_page', '50')
|
||||
sort_param = request.GET.get('sort', '')
|
||||
|
||||
freq_min = request.GET.get('freq_min')
|
||||
freq_max = request.GET.get('freq_max')
|
||||
range_min = request.GET.get('range_min')
|
||||
range_max = request.GET.get('range_max')
|
||||
snr_min = request.GET.get('snr_min')
|
||||
snr_max = request.GET.get('snr_max')
|
||||
bod_min = request.GET.get('bod_min')
|
||||
bod_max = request.GET.get('bod_max')
|
||||
search_query = request.GET.get('search')
|
||||
selected_modulations = request.GET.getlist('modulation')
|
||||
selected_polarizations = request.GET.getlist('polarization')
|
||||
selected_satellites = request.GET.getlist('satellite_id')
|
||||
has_kupsat = request.GET.get('has_kupsat')
|
||||
has_valid = request.GET.get('has_valid')
|
||||
|
||||
try:
|
||||
items_per_page = int(items_per_page)
|
||||
except ValueError:
|
||||
items_per_page = 50
|
||||
|
||||
objects = ObjItem.objects.none()
|
||||
|
||||
if selected_satellites or selected_sat_id:
|
||||
if selected_sat_id and not selected_satellites:
|
||||
try:
|
||||
selected_sat_id_single = int(selected_sat_id)
|
||||
selected_satellites = [selected_sat_id_single]
|
||||
except ValueError:
|
||||
selected_satellites = []
|
||||
|
||||
if selected_satellites:
|
||||
objects = ObjItem.objects.select_related(
|
||||
'geo_obj',
|
||||
'updated_by__user',
|
||||
'created_by__user',
|
||||
).prefetch_related(
|
||||
'parameters_obj__id_satellite',
|
||||
'parameters_obj__polarization',
|
||||
'parameters_obj__modulation',
|
||||
'parameters_obj__standard'
|
||||
).filter(parameters_obj__id_satellite_id__in=selected_satellites)
|
||||
else:
|
||||
objects = ObjItem.objects.select_related(
|
||||
'geo_obj',
|
||||
'updated_by__user',
|
||||
'created_by__user',
|
||||
).prefetch_related(
|
||||
'parameters_obj__id_satellite',
|
||||
'parameters_obj__polarization',
|
||||
'parameters_obj__modulation',
|
||||
'parameters_obj__standard'
|
||||
)
|
||||
|
||||
if freq_min is not None and freq_min.strip() != '':
|
||||
try:
|
||||
freq_min_val = float(freq_min)
|
||||
objects = objects.filter(parameters_obj__frequency__gte=freq_min_val)
|
||||
except ValueError:
|
||||
pass
|
||||
if freq_max is not None and freq_max.strip() != '':
|
||||
try:
|
||||
freq_max_val = float(freq_max)
|
||||
objects = objects.filter(parameters_obj__frequency__lte=freq_max_val)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if range_min is not None and range_min.strip() != '':
|
||||
try:
|
||||
range_min_val = float(range_min)
|
||||
objects = objects.filter(parameters_obj__freq_range__gte=range_min_val)
|
||||
except ValueError:
|
||||
pass
|
||||
if range_max is not None and range_max.strip() != '':
|
||||
try:
|
||||
range_max_val = float(range_max)
|
||||
objects = objects.filter(parameters_obj__freq_range__lte=range_max_val)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if snr_min is not None and snr_min.strip() != '':
|
||||
try:
|
||||
snr_min_val = float(snr_min)
|
||||
objects = objects.filter(parameters_obj__snr__gte=snr_min_val)
|
||||
except ValueError:
|
||||
pass
|
||||
if snr_max is not None and snr_max.strip() != '':
|
||||
try:
|
||||
snr_max_val = float(snr_max)
|
||||
objects = objects.filter(parameters_obj__snr__lte=snr_max_val)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if bod_min is not None and bod_min.strip() != '':
|
||||
try:
|
||||
bod_min_val = float(bod_min)
|
||||
objects = objects.filter(parameters_obj__bod_velocity__gte=bod_min_val)
|
||||
except ValueError:
|
||||
pass
|
||||
if bod_max is not None and bod_max.strip() != '':
|
||||
try:
|
||||
bod_max_val = float(bod_max)
|
||||
objects = objects.filter(parameters_obj__bod_velocity__lte=bod_max_val)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if selected_modulations:
|
||||
objects = objects.filter(parameters_obj__modulation__id__in=selected_modulations)
|
||||
|
||||
if selected_polarizations:
|
||||
objects = objects.filter(parameters_obj__polarization__id__in=selected_polarizations)
|
||||
|
||||
if has_kupsat == '1':
|
||||
objects = objects.filter(geo_obj__coords_kupsat__isnull=False)
|
||||
elif has_kupsat == '0':
|
||||
objects = objects.filter(geo_obj__coords_kupsat__isnull=True)
|
||||
|
||||
if has_valid == '1':
|
||||
objects = objects.filter(geo_obj__coords_valid__isnull=False)
|
||||
elif has_valid == '0':
|
||||
objects = objects.filter(geo_obj__coords_valid__isnull=True)
|
||||
|
||||
if search_query:
|
||||
search_query = search_query.strip()
|
||||
if search_query:
|
||||
objects = objects.filter(
|
||||
models.Q(name__icontains=search_query) |
|
||||
models.Q(geo_obj__location__icontains=search_query)
|
||||
)
|
||||
else:
|
||||
selected_sat_id = None
|
||||
|
||||
first_param_freq_subq = self.get_first_param_subquery('frequency')
|
||||
first_param_range_subq = self.get_first_param_subquery('freq_range')
|
||||
first_param_snr_subq = self.get_first_param_subquery('snr')
|
||||
first_param_bod_subq = self.get_first_param_subquery('bod_velocity')
|
||||
first_param_sat_name_subq = self.get_first_param_subquery('id_satellite__name')
|
||||
first_param_pol_name_subq = self.get_first_param_subquery('polarization__name')
|
||||
first_param_mod_name_subq = self.get_first_param_subquery('modulation__name')
|
||||
|
||||
objects = objects.annotate(
|
||||
first_param_freq=Subquery(first_param_freq_subq),
|
||||
first_param_range=Subquery(first_param_range_subq),
|
||||
first_param_snr=Subquery(first_param_snr_subq),
|
||||
first_param_bod=Subquery(first_param_bod_subq),
|
||||
first_param_sat_name=Subquery(first_param_sat_name_subq),
|
||||
first_param_pol_name=Subquery(first_param_pol_name_subq),
|
||||
first_param_mod_name=Subquery(first_param_mod_name_subq),
|
||||
)
|
||||
|
||||
valid_sort_fields = {
|
||||
'name': 'name',
|
||||
'-name': '-name',
|
||||
'updated_at': 'updated_at',
|
||||
'-updated_at': '-updated_at',
|
||||
'created_at': 'created_at',
|
||||
'-created_at': '-created_at',
|
||||
'updated_by': 'updated_by__user__username',
|
||||
'-updated_by': '-updated_by__user__username',
|
||||
'created_by': 'created_by__user__username',
|
||||
'-created_by': '-created_by__user__username',
|
||||
'geo_timestamp': 'geo_obj__timestamp',
|
||||
'-geo_timestamp': '-geo_obj__timestamp',
|
||||
'frequency': 'first_param_freq',
|
||||
'-frequency': '-first_param_freq',
|
||||
'freq_range': 'first_param_range',
|
||||
'-freq_range': '-first_param_range',
|
||||
'snr': 'first_param_snr',
|
||||
'-snr': '-first_param_snr',
|
||||
'bod_velocity': 'first_param_bod',
|
||||
'-bod_velocity': '-first_param_bod',
|
||||
'satellite': 'first_param_sat_name',
|
||||
'-satellite': '-first_param_sat_name',
|
||||
'polarization': 'first_param_pol_name',
|
||||
'-polarization': '-first_param_pol_name',
|
||||
'modulation': 'first_param_mod_name',
|
||||
'-modulation': '-first_param_mod_name',
|
||||
}
|
||||
|
||||
if sort_param in valid_sort_fields:
|
||||
objects = objects.order_by(valid_sort_fields[sort_param])
|
||||
|
||||
paginator = Paginator(objects, items_per_page)
|
||||
page_obj = paginator.get_page(page_number)
|
||||
|
||||
processed_objects = []
|
||||
for obj in page_obj:
|
||||
param = None
|
||||
if hasattr(obj, 'parameters_obj') and obj.parameters_obj.all():
|
||||
param_list = list(obj.parameters_obj.all())
|
||||
if param_list:
|
||||
param = param_list[0]
|
||||
|
||||
geo_coords = "-"
|
||||
geo_timestamp = "-"
|
||||
geo_location = "-"
|
||||
kupsat_coords = "-"
|
||||
valid_coords = "-"
|
||||
distance_geo_kup = "-"
|
||||
distance_geo_valid = "-"
|
||||
distance_kup_valid = "-"
|
||||
|
||||
if obj.geo_obj:
|
||||
geo_timestamp = obj.geo_obj.timestamp
|
||||
geo_location = obj.geo_obj.location
|
||||
|
||||
if obj.geo_obj.coords:
|
||||
longitude = obj.geo_obj.coords.coords[0]
|
||||
latitude = obj.geo_obj.coords.coords[1]
|
||||
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||
geo_coords = f"{lat} {lon}"
|
||||
|
||||
if obj.geo_obj.coords_kupsat:
|
||||
longitude = obj.geo_obj.coords_kupsat.coords[0]
|
||||
latitude = obj.geo_obj.coords_kupsat.coords[1]
|
||||
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||
kupsat_coords = f"{lat} {lon}"
|
||||
elif obj.geo_obj.coords_kupsat is not None:
|
||||
kupsat_coords = "-"
|
||||
|
||||
if obj.geo_obj.coords_valid:
|
||||
longitude = obj.geo_obj.coords_valid.coords[0]
|
||||
latitude = obj.geo_obj.coords_valid.coords[1]
|
||||
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||
valid_coords = f"{lat} {lon}"
|
||||
elif obj.geo_obj.coords_valid is not None:
|
||||
valid_coords = "-"
|
||||
|
||||
if obj.geo_obj.distance_coords_kup is not None:
|
||||
distance_geo_kup = f"{obj.geo_obj.distance_coords_kup:.3f}"
|
||||
|
||||
if obj.geo_obj.distance_coords_valid is not None:
|
||||
distance_geo_valid = f"{obj.geo_obj.distance_coords_valid:.3f}"
|
||||
|
||||
if obj.geo_obj.distance_kup_valid is not None:
|
||||
distance_kup_valid = f"{obj.geo_obj.distance_kup_valid:.3f}"
|
||||
|
||||
satellite_name = "-"
|
||||
frequency = "-"
|
||||
freq_range = "-"
|
||||
polarization_name = "-"
|
||||
bod_velocity = "-"
|
||||
modulation_name = "-"
|
||||
snr = "-"
|
||||
|
||||
if param:
|
||||
if hasattr(param, 'id_satellite') and param.id_satellite:
|
||||
satellite_name = param.id_satellite.name if hasattr(param.id_satellite, 'name') else "-"
|
||||
|
||||
frequency = f"{param.frequency:.3f}" if param.frequency is not None else "-"
|
||||
freq_range = f"{param.freq_range:.3f}" if param.freq_range is not None else "-"
|
||||
bod_velocity = f"{param.bod_velocity:.0f}" if param.bod_velocity is not None else "-"
|
||||
snr = f"{param.snr:.0f}" if param.snr is not None else "-"
|
||||
|
||||
if hasattr(param, 'polarization') and param.polarization:
|
||||
polarization_name = param.polarization.name if hasattr(param.polarization, 'name') else "-"
|
||||
|
||||
if hasattr(param, 'modulation') and param.modulation:
|
||||
modulation_name = param.modulation.name if hasattr(param.modulation, 'name') else "-"
|
||||
|
||||
processed_objects.append({
|
||||
'id': obj.id,
|
||||
'name': obj.name or "-",
|
||||
'satellite_name': satellite_name,
|
||||
'frequency': frequency,
|
||||
'freq_range': freq_range,
|
||||
'polarization': polarization_name,
|
||||
'bod_velocity': bod_velocity,
|
||||
'modulation': modulation_name,
|
||||
'snr': snr,
|
||||
'geo_timestamp': geo_timestamp,
|
||||
'geo_location': geo_location,
|
||||
'geo_coords': geo_coords,
|
||||
'kupsat_coords': kupsat_coords,
|
||||
'valid_coords': valid_coords,
|
||||
'distance_geo_kup': distance_geo_kup,
|
||||
'distance_geo_valid': distance_geo_valid,
|
||||
'distance_kup_valid': distance_kup_valid,
|
||||
'updated_by': obj.updated_by if obj.updated_by else '-',
|
||||
'obj': obj
|
||||
})
|
||||
|
||||
modulations = Modulation.objects.all()
|
||||
polarizations = Polarization.objects.all()
|
||||
|
||||
context = {
|
||||
'satellites': satellites,
|
||||
'selected_satellite_id': selected_sat_id,
|
||||
'page_obj': page_obj,
|
||||
'processed_objects': processed_objects,
|
||||
'items_per_page': items_per_page,
|
||||
'available_items_per_page': [50, 100, 500, 1000],
|
||||
'freq_min': freq_min,
|
||||
'freq_max': freq_max,
|
||||
'range_min': range_min,
|
||||
'range_max': range_max,
|
||||
'snr_min': snr_min,
|
||||
'snr_max': snr_max,
|
||||
'bod_min': bod_min,
|
||||
'bod_max': bod_max,
|
||||
'search_query': search_query,
|
||||
'selected_modulations': [int(x) for x in selected_modulations if x.isdigit()],
|
||||
'selected_polarizations': [int(x) for x in selected_polarizations if x.isdigit()],
|
||||
'selected_satellites': [int(x) for x in selected_satellites if x.isdigit()],
|
||||
'has_kupsat': has_kupsat,
|
||||
'has_valid': has_valid,
|
||||
'modulations': modulations,
|
||||
'polarizations': polarizations,
|
||||
'full_width_page': True,
|
||||
'sort': sort_param,
|
||||
}
|
||||
|
||||
return render(request, 'mainapp/objitem_list.html', context)
|
||||
|
||||
def get_first_param_subquery(self, field_name):
|
||||
return Parameter.objects.filter(
|
||||
objitems=OuterRef('pk')
|
||||
).order_by('id').values(field_name)[:1]
|
||||
|
||||
class ObjItemUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
|
||||
model = ObjItem
|
||||
form_class = ObjItemForm
|
||||
template_name = 'mainapp/objitem_form.html'
|
||||
success_url = reverse_lazy('home')
|
||||
|
||||
def test_func(self):
|
||||
return self.request.user.customuser.role in ['admin', 'moderator']
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['LEAFLET_CONFIG'] = {
|
||||
'DEFAULT_CENTER': (55.75, 37.62),
|
||||
'DEFAULT_ZOOM': 5,
|
||||
}
|
||||
ParameterFormSet = modelformset_factory(
|
||||
Parameter,
|
||||
form=ParameterForm,
|
||||
extra=0,
|
||||
can_delete=True
|
||||
)
|
||||
|
||||
if self.object:
|
||||
parameter_queryset = self.object.parameters_obj.all()
|
||||
context['parameter_forms'] = ParameterFormSet(
|
||||
queryset=parameter_queryset,
|
||||
prefix='parameters'
|
||||
)
|
||||
|
||||
if hasattr(self.object, 'geo_obj'):
|
||||
context['geo_form'] = GeoForm(instance=self.object.geo_obj, prefix='geo')
|
||||
else:
|
||||
context['geo_form'] = GeoForm(prefix='geo')
|
||||
else:
|
||||
context['parameter_forms'] = ParameterFormSet(
|
||||
queryset=Parameter.objects.none(),
|
||||
prefix='parameters'
|
||||
)
|
||||
context['geo_form'] = GeoForm(prefix='geo')
|
||||
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
context = self.get_context_data()
|
||||
parameter_forms = context['parameter_forms']
|
||||
geo_form = context['geo_form']
|
||||
|
||||
# Сохраняем основной объект
|
||||
self.object = form.save(commit=False)
|
||||
self.object.updated_by = self.request.user.customuser
|
||||
self.object.save()
|
||||
|
||||
# Сохраняем связанные параметры
|
||||
if parameter_forms.is_valid():
|
||||
instances = parameter_forms.save(commit=False)
|
||||
for instance in instances:
|
||||
instance.save()
|
||||
instance.objitems.set([self.object])
|
||||
|
||||
# Сохраняем геоданные
|
||||
geo_instance = None
|
||||
if hasattr(self.object, 'geo_obj'):
|
||||
geo_instance = self.object.geo_obj
|
||||
|
||||
# Создаем или обновляем гео-объект
|
||||
if geo_instance is None:
|
||||
geo_instance = Geo(objitem=self.object)
|
||||
|
||||
# Обновляем поля из geo_form
|
||||
if geo_form.is_valid():
|
||||
geo_instance.location = geo_form.cleaned_data['location']
|
||||
geo_instance.comment = geo_form.cleaned_data['comment']
|
||||
geo_instance.is_average = geo_form.cleaned_data['is_average']
|
||||
|
||||
# Обрабатываем координаты геолокации
|
||||
geo_longitude = self.request.POST.get('geo_longitude')
|
||||
geo_latitude = self.request.POST.get('geo_latitude')
|
||||
if geo_longitude and geo_latitude:
|
||||
geo_instance.coords = Point(float(geo_longitude), float(geo_latitude), srid=4326)
|
||||
|
||||
# Обрабатываем координаты Кубсата
|
||||
kupsat_longitude = self.request.POST.get('kupsat_longitude')
|
||||
kupsat_latitude = self.request.POST.get('kupsat_latitude')
|
||||
if kupsat_longitude and kupsat_latitude:
|
||||
geo_instance.coords_kupsat = Point(float(kupsat_longitude), float(kupsat_latitude), srid=4326)
|
||||
|
||||
# Обрабатываем координаты оперативников
|
||||
valid_longitude = self.request.POST.get('valid_longitude')
|
||||
valid_latitude = self.request.POST.get('valid_latitude')
|
||||
if valid_longitude and valid_latitude:
|
||||
geo_instance.coords_valid = Point(float(valid_longitude), float(valid_latitude), srid=4326)
|
||||
|
||||
# Обрабатываем дату/время
|
||||
timestamp_date = self.request.POST.get('timestamp_date')
|
||||
timestamp_time = self.request.POST.get('timestamp_time')
|
||||
if timestamp_date and timestamp_time:
|
||||
naive_datetime = datetime.strptime(f"{timestamp_date} {timestamp_time}", "%Y-%m-%d %H:%M")
|
||||
geo_instance.timestamp = naive_datetime
|
||||
|
||||
geo_instance.save()
|
||||
|
||||
messages.success(self.request, 'Объект успешно сохранён!')
|
||||
return super().form_valid(form)
|
||||
|
||||
class ObjItemCreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView):
|
||||
model = ObjItem
|
||||
form_class = ObjItemForm
|
||||
template_name = 'mainapp/objitem_form.html'
|
||||
success_url = reverse_lazy('home')
|
||||
|
||||
def test_func(self):
|
||||
return self.request.user.customuser.role in ['admin', 'moderator']
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
ParameterFormSet = modelformset_factory(
|
||||
Parameter,
|
||||
form=ParameterForm,
|
||||
extra=1,
|
||||
can_delete=True
|
||||
)
|
||||
|
||||
context['parameter_forms'] = ParameterFormSet(
|
||||
queryset=Parameter.objects.none(),
|
||||
prefix='parameters'
|
||||
)
|
||||
|
||||
context['geo_form'] = GeoForm(prefix='geo')
|
||||
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
context = self.get_context_data()
|
||||
parameter_forms = context['parameter_forms']
|
||||
geo_form = context['geo_form']
|
||||
|
||||
# Сохраняем основной объект
|
||||
self.object = form.save(commit=False)
|
||||
self.object.created_by = self.request.user.customuser
|
||||
self.object.updated_by = self.request.user.customuser
|
||||
self.object.save()
|
||||
|
||||
# Сохраняем связанные параметры
|
||||
if parameter_forms.is_valid():
|
||||
instances = parameter_forms.save(commit=False)
|
||||
for instance in instances:
|
||||
instance.save()
|
||||
instance.objitems.add(self.object)
|
||||
|
||||
# Создаем гео-объект
|
||||
geo_instance = Geo(objitem=self.object)
|
||||
|
||||
# Обновляем поля из geo_form
|
||||
if geo_form.is_valid():
|
||||
geo_instance.location = geo_form.cleaned_data['location']
|
||||
geo_instance.comment = geo_form.cleaned_data['comment']
|
||||
geo_instance.is_average = geo_form.cleaned_data['is_average']
|
||||
|
||||
# Обрабатываем координаты геолокации
|
||||
geo_longitude = self.request.POST.get('geo_longitude')
|
||||
geo_latitude = self.request.POST.get('geo_latitude')
|
||||
if geo_longitude and geo_latitude:
|
||||
geo_instance.coords = Point(float(geo_longitude), float(geo_latitude), srid=4326)
|
||||
|
||||
# Обрабатываем координаты Кубсата
|
||||
kupsat_longitude = self.request.POST.get('kupsat_longitude')
|
||||
kupsat_latitude = self.request.POST.get('kupsat_latitude')
|
||||
if kupsat_longitude and kupsat_latitude:
|
||||
geo_instance.coords_kupsat = Point(float(kupsat_longitude), float(kupsat_latitude), srid=4326)
|
||||
|
||||
# Обрабатываем координаты оперативников
|
||||
valid_longitude = self.request.POST.get('valid_longitude')
|
||||
valid_latitude = self.request.POST.get('valid_latitude')
|
||||
if valid_longitude and valid_latitude:
|
||||
geo_instance.coords_valid = Point(float(valid_longitude), float(valid_latitude), srid=4326)
|
||||
|
||||
# Обрабатываем дату/время
|
||||
timestamp_date = self.request.POST.get('timestamp_date')
|
||||
timestamp_time = self.request.POST.get('timestamp_time')
|
||||
if timestamp_date and timestamp_time:
|
||||
naive_datetime = datetime.strptime(f"{timestamp_date} {timestamp_time}", "%Y-%m-%d %H:%M")
|
||||
geo_instance.timestamp = naive_datetime
|
||||
|
||||
geo_instance.save()
|
||||
|
||||
messages.success(self.request, 'Объект успешно создан!')
|
||||
return super().form_valid(form)
|
||||
|
||||
class ObjItemDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
|
||||
model = ObjItem
|
||||
template_name = 'mainapp/objitem_confirm_delete.html'
|
||||
success_url = reverse_lazy('home')
|
||||
|
||||
def test_func(self):
|
||||
return self.request.user.customuser.role in ['admin', 'moderator']
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
messages.success(self.request, 'Объект успешно удалён!')
|
||||
return super().delete(request, *args, **kwargs)
|
||||
@@ -5,20 +5,24 @@ from more_admin_filters import MultiSelectDropdownFilter, MultiSelectFilter, Mul
|
||||
from import_export.admin import ImportExportActionModelAdmin
|
||||
|
||||
@admin.register(Transponders)
|
||||
class PolarizationAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
|
||||
class TranspondersAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
|
||||
list_display = (
|
||||
"sat_id",
|
||||
"name",
|
||||
"zone_name",
|
||||
"frequency",
|
||||
"downlink",
|
||||
"uplink",
|
||||
"frequency_range",
|
||||
"transfer",
|
||||
"polarization",
|
||||
)
|
||||
list_filter = (
|
||||
("polarization", MultiSelectRelatedDropdownFilter),
|
||||
("sat_id", MultiSelectRelatedDropdownFilter),
|
||||
("frequency", NumericRangeFilterBuilder()),
|
||||
# ("frequency", NumericRangeFilterBuilder()),
|
||||
"zone_name"
|
||||
)
|
||||
search_fields = ("name",)
|
||||
search_fields = ("name", "sat_id__name")
|
||||
ordering = ("name",)
|
||||
# def sat_name(self, obj):
|
||||
# return
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-13 12:47
|
||||
# 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
|
||||
|
||||
@@ -19,9 +21,11 @@ class Migration(migrations.Migration):
|
||||
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='Название транспондера')),
|
||||
('frequency', models.FloatField(blank=True, null=True, verbose_name='Центральная частота')),
|
||||
('frequency_range', models.FloatField(blank=True, null=True, verbose_name='Полоса частот')),
|
||||
('zone_name', models.CharField(blank=True, max_length=60, 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='Спутник')),
|
||||
],
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
from django.db import models
|
||||
from mainapp.models import Satellite, Polarization, get_default_polarization
|
||||
from django.db.models import F, ExpressionWrapper
|
||||
from django.db.models.functions import Abs
|
||||
|
||||
class Transponders(models.Model):
|
||||
name = models.CharField(max_length=30, null=True, blank=True, verbose_name="Название транспондера")
|
||||
frequency = models.FloatField(blank=True, null=True, verbose_name="Центральная частота")
|
||||
frequency_range = models.FloatField(blank=True, null=True, verbose_name="Полоса частот")
|
||||
zone_name = models.CharField(max_length=60, blank=True, 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(max_length=255, blank=True, null=True, verbose_name="Название зоны")
|
||||
polarization = models.ForeignKey(
|
||||
Polarization, default=get_default_polarization, on_delete=models.SET_DEFAULT, related_name="tran_polarizations", null=True, blank=True, verbose_name="Поляризация"
|
||||
)
|
||||
sat_id = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="tran_satellite", verbose_name="Спутник")
|
||||
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 __str__(self):
|
||||
return self.name
|
||||
|
||||
@@ -53,9 +53,15 @@
|
||||
const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
||||
attribution: 'Tiles © 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
|
||||
"Спутник": satellite,
|
||||
"Локально": street_local
|
||||
};
|
||||
L.control.layers(baseLayers).addTo(map);
|
||||
map.setMaxZoom(18);
|
||||
|
||||
@@ -3,6 +3,7 @@ import re
|
||||
import json
|
||||
from .models import Transponders
|
||||
from mainapp.models import Polarization, Satellite
|
||||
from io import BytesIO
|
||||
|
||||
def search_satellite_on_page(data: dict, satellite_name: str):
|
||||
for pos, value in data.get('page', {}).get('positions').items():
|
||||
@@ -90,3 +91,68 @@ def parse_transponders_from_json(filepath: str):
|
||||
)
|
||||
tran_obj.save()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
@@ -72,7 +72,7 @@ class GetTransponderOnSatIdView(View):
|
||||
output.append(
|
||||
{
|
||||
"name": tran.name,
|
||||
"frequency": tran.frequency,
|
||||
"frequency": tran.downlink,
|
||||
"frequency_range": tran.frequency_range,
|
||||
"zone_name": tran.zone_name,
|
||||
"polarization": tran.polarization.name
|
||||
|
||||
@@ -7,6 +7,7 @@ requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"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",
|
||||
@@ -19,7 +20,9 @@ dependencies = [
|
||||
"django-leaflet>=0.32.0",
|
||||
"django-map-widgets>=0.5.1",
|
||||
"django-more-admin-filters>=1.13",
|
||||
"gdal",
|
||||
"dotenv>=0.9.9",
|
||||
"geopy>=2.4.1",
|
||||
"gunicorn>=23.0.0",
|
||||
"lxml>=6.0.2",
|
||||
"matplotlib>=3.10.7",
|
||||
"numpy>=2.3.3",
|
||||
@@ -28,9 +31,12 @@ dependencies = [
|
||||
"psycopg>=3.2.10",
|
||||
"redis>=6.4.0",
|
||||
"requests>=2.32.5",
|
||||
"reverse-geocoder>=1.5.1",
|
||||
"scikit-learn>=1.7.2",
|
||||
"selenium>=4.38.0",
|
||||
"setuptools>=80.9.0",
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
gdal = { path = "gdal-3.10.2-cp313-cp313-win_amd64.whl" }
|
||||
|
||||
[dependency-groups]
|
||||
dev = []
|
||||
|
||||
4
dbapp/static/bootstrap-icons/0-circle-fill.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-0-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895"/>
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-8.012 4.158c1.858 0 2.96-1.582 2.96-3.99V7.84c0-2.426-1.079-3.996-2.936-3.996-1.864 0-2.965 1.588-2.965 3.996v.328c0 2.42 1.09 3.99 2.941 3.99"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 476 B |
4
dbapp/static/bootstrap-icons/0-circle.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-0-circle" viewBox="0 0 16 16">
|
||||
<path d="M7.988 12.158c-1.851 0-2.941-1.57-2.941-3.99V7.84c0-2.408 1.101-3.996 2.965-3.996 1.857 0 2.935 1.57 2.935 3.996v.328c0 2.408-1.101 3.99-2.959 3.99M8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895"/>
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 507 B |
4
dbapp/static/bootstrap-icons/0-square-fill.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-0-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895"/>
|
||||
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm5.988 12.158c-1.851 0-2.941-1.57-2.941-3.99V7.84c0-2.408 1.101-3.996 2.965-3.996 1.857 0 2.935 1.57 2.935 3.996v.328c0 2.408-1.101 3.99-2.959 3.99"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 514 B |
4
dbapp/static/bootstrap-icons/0-square.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-0-square" viewBox="0 0 16 16">
|
||||
<path d="M7.988 12.158c-1.851 0-2.941-1.57-2.941-3.99V7.84c0-2.408 1.101-3.996 2.965-3.996 1.857 0 2.935 1.57 2.935 3.996v.328c0 2.408-1.101 3.99-2.959 3.99M8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895"/>
|
||||
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 579 B |
3
dbapp/static/bootstrap-icons/1-circle-fill.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-1-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M9.283 4.002H7.971L6.072 5.385v1.271l1.834-1.318h.065V12h1.312z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 250 B |
3
dbapp/static/bootstrap-icons/1-circle.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-1-circle" viewBox="0 0 16 16">
|
||||
<path d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0M9.283 4.002V12H7.971V5.338h-.065L6.072 6.656V5.385l1.899-1.383z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 279 B |
3
dbapp/static/bootstrap-icons/1-square-fill.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-1-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm7.283 4.002V12H7.971V5.338h-.065L6.072 6.656V5.385l1.899-1.383z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 286 B |
4
dbapp/static/bootstrap-icons/1-square.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-1-square" viewBox="0 0 16 16">
|
||||
<path d="M9.283 4.002V12H7.971V5.338h-.065L6.072 6.656V5.385l1.899-1.383z"/>
|
||||
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 366 B |
3
dbapp/static/bootstrap-icons/123.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-123" viewBox="0 0 16 16">
|
||||
<path d="M2.873 11.297V4.142H1.699L0 5.379v1.137l1.64-1.18h.06v5.961zm3.213-5.09v-.063c0-.618.44-1.169 1.196-1.169.676 0 1.174.44 1.174 1.106 0 .624-.42 1.101-.807 1.526L4.99 10.553v.744h4.78v-.99H6.643v-.069L8.41 8.252c.65-.724 1.237-1.332 1.237-2.27C9.646 4.849 8.723 4 7.308 4c-1.573 0-2.36 1.064-2.36 2.15v.057zm6.559 1.883h.786c.823 0 1.374.481 1.379 1.179.01.707-.55 1.216-1.421 1.21-.77-.005-1.326-.419-1.379-.953h-1.095c.042 1.053.938 1.918 2.464 1.918 1.478 0 2.642-.839 2.62-2.144-.02-1.143-.922-1.651-1.551-1.714v-.063c.535-.09 1.347-.66 1.326-1.678-.026-1.053-.933-1.855-2.359-1.845-1.5.005-2.317.88-2.348 1.898h1.116c.032-.498.498-.944 1.206-.944.703 0 1.206.435 1.206 1.07.005.64-.504 1.106-1.2 1.106h-.75z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 854 B |
3
dbapp/static/bootstrap-icons/2-circle-fill.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-2-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M6.646 6.24c0-.691.493-1.306 1.336-1.306.756 0 1.313.492 1.313 1.236 0 .697-.469 1.23-.902 1.705l-2.971 3.293V12h5.344v-1.107H7.268v-.077l1.974-2.22.096-.107c.688-.763 1.287-1.428 1.287-2.43 0-1.266-1.031-2.215-2.613-2.215-1.758 0-2.637 1.19-2.637 2.402v.065h1.271v-.07Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 457 B |
3
dbapp/static/bootstrap-icons/2-circle.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-2-circle" viewBox="0 0 16 16">
|
||||
<path d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0M6.646 6.24v.07H5.375v-.064c0-1.213.879-2.402 2.637-2.402 1.582 0 2.613.949 2.613 2.215 0 1.002-.6 1.667-1.287 2.43l-.096.107-1.974 2.22v.077h3.498V12H5.422v-.832l2.97-3.293c.434-.475.903-1.008.903-1.705 0-.744-.557-1.236-1.313-1.236-.843 0-1.336.615-1.336 1.306"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 477 B |
3
dbapp/static/bootstrap-icons/2-square-fill.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-2-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm4.646 6.24v.07H5.375v-.064c0-1.213.879-2.402 2.637-2.402 1.582 0 2.613.949 2.613 2.215 0 1.002-.6 1.667-1.287 2.43l-.096.107-1.974 2.22v.077h3.498V12H5.422v-.832l2.97-3.293c.434-.475.903-1.008.903-1.705 0-.744-.557-1.236-1.313-1.236-.843 0-1.336.615-1.336 1.306"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 484 B |
4
dbapp/static/bootstrap-icons/2-square.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-2-square" viewBox="0 0 16 16">
|
||||
<path d="M6.646 6.24v.07H5.375v-.064c0-1.213.879-2.402 2.637-2.402 1.582 0 2.613.949 2.613 2.215 0 1.002-.6 1.667-1.287 2.43l-.096.107-1.974 2.22v.077h3.498V12H5.422v-.832l2.97-3.293c.434-.475.903-1.008.903-1.705 0-.744-.557-1.236-1.313-1.236-.843 0-1.336.615-1.336 1.306"/>
|
||||
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 564 B |
3
dbapp/static/bootstrap-icons/3-circle-fill.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-3-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-8.082.414c.92 0 1.535.54 1.541 1.318.012.791-.615 1.36-1.588 1.354-.861-.006-1.482-.469-1.54-1.066H5.104c.047 1.177 1.05 2.144 2.754 2.144 1.653 0 2.954-.937 2.93-2.396-.023-1.278-1.031-1.846-1.734-1.916v-.07c.597-.1 1.505-.739 1.482-1.876-.03-1.177-1.043-2.074-2.637-2.062-1.675.006-2.59.984-2.625 2.12h1.248c.036-.556.557-1.054 1.348-1.054.785 0 1.348.486 1.348 1.195.006.715-.563 1.237-1.342 1.237h-.838v1.072h.879Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 607 B |
4
dbapp/static/bootstrap-icons/3-circle.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-3-circle" viewBox="0 0 16 16">
|
||||
<path d="M7.918 8.414h-.879V7.342h.838c.78 0 1.348-.522 1.342-1.237 0-.709-.563-1.195-1.348-1.195-.79 0-1.312.498-1.348 1.055H5.275c.036-1.137.95-2.115 2.625-2.121 1.594-.012 2.608.885 2.637 2.062.023 1.137-.885 1.776-1.482 1.875v.07c.703.07 1.71.64 1.734 1.917.024 1.459-1.277 2.396-2.93 2.396-1.705 0-2.707-.967-2.754-2.144H6.33c.059.597.68 1.06 1.541 1.066.973.006 1.6-.563 1.588-1.354-.006-.779-.621-1.318-1.541-1.318"/>
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 642 B |
3
dbapp/static/bootstrap-icons/3-square-fill.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-3-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm5.918 8.414h-.879V7.342h.838c.78 0 1.348-.522 1.342-1.237 0-.709-.563-1.195-1.348-1.195-.79 0-1.312.498-1.348 1.055H5.275c.036-1.137.95-2.115 2.625-2.121 1.594-.012 2.608.885 2.637 2.062.023 1.137-.885 1.776-1.482 1.875v.07c.703.07 1.71.64 1.734 1.917.024 1.459-1.277 2.396-2.93 2.396-1.705 0-2.707-.967-2.754-2.144H6.33c.059.597.68 1.06 1.541 1.066.973.006 1.6-.563 1.588-1.354-.006-.779-.621-1.318-1.541-1.318"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 634 B |
4
dbapp/static/bootstrap-icons/3-square.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-3-square" viewBox="0 0 16 16">
|
||||
<path d="M7.918 8.414h-.879V7.342h.838c.78 0 1.348-.522 1.342-1.237 0-.709-.563-1.195-1.348-1.195-.79 0-1.312.498-1.348 1.055H5.275c.036-1.137.95-2.115 2.625-2.121 1.594-.012 2.608.885 2.637 2.062.023 1.137-.885 1.776-1.482 1.875v.07c.703.07 1.71.64 1.734 1.917.024 1.459-1.277 2.396-2.93 2.396-1.705 0-2.707-.967-2.754-2.144H6.33c.059.597.68 1.06 1.541 1.066.973.006 1.6-.563 1.588-1.354-.006-.779-.621-1.318-1.541-1.318"/>
|
||||
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 714 B |
3
dbapp/static/bootstrap-icons/4-circle-fill.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-4-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M7.519 5.057c-.886 1.418-1.772 2.838-2.542 4.265v1.12H8.85V12h1.26v-1.559h1.007V9.334H10.11V4.002H8.176zM6.225 9.281v.053H8.85V5.063h-.065c-.867 1.33-1.787 2.806-2.56 4.218"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 359 B |
4
dbapp/static/bootstrap-icons/4-circle.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-4-circle" viewBox="0 0 16 16">
|
||||
<path d="M7.519 5.057q.33-.527.657-1.055h1.933v5.332h1.008v1.107H10.11V12H8.85v-1.559H4.978V9.322c.77-1.427 1.656-2.847 2.542-4.265ZM6.225 9.281v.053H8.85V5.063h-.065c-.867 1.33-1.787 2.806-2.56 4.218"/>
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 421 B |
4
dbapp/static/bootstrap-icons/4-square-fill.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-4-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M6.225 9.281v.053H8.85V5.063h-.065c-.867 1.33-1.787 2.806-2.56 4.218"/>
|
||||
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm5.519 5.057q.33-.527.657-1.055h1.933v5.332h1.008v1.107H10.11V12H8.85v-1.559H4.978V9.322c.77-1.427 1.656-2.847 2.542-4.265Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 428 B |
4
dbapp/static/bootstrap-icons/4-square.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-4-square" viewBox="0 0 16 16">
|
||||
<path d="M7.519 5.057q.33-.527.657-1.055h1.933v5.332h1.008v1.107H10.11V12H8.85v-1.559H4.978V9.322c.77-1.427 1.656-2.847 2.542-4.265ZM6.225 9.281v.053H8.85V5.063h-.065c-.867 1.33-1.787 2.806-2.56 4.218"/>
|
||||
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 493 B |
3
dbapp/static/bootstrap-icons/5-circle-fill.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-5-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-8.006 4.158c1.74 0 2.924-1.119 2.924-2.806 0-1.641-1.178-2.584-2.56-2.584-.897 0-1.442.421-1.612.68h-.064l.193-2.344h3.621V4.002H5.791L5.445 8.63h1.149c.193-.358.668-.809 1.435-.809.85 0 1.582.604 1.582 1.57 0 1.085-.779 1.682-1.57 1.682-.697 0-1.389-.31-1.53-1.031H5.276c.065 1.213 1.149 2.115 2.72 2.115Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 495 B |
3
dbapp/static/bootstrap-icons/5-circle.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-5-circle" viewBox="0 0 16 16">
|
||||
<path d="M1 8a7 7 0 1 1 14 0A7 7 0 0 1 1 8m15 0A8 8 0 1 0 0 8a8 8 0 0 0 16 0m-8.006 4.158c-1.57 0-2.654-.902-2.719-2.115h1.237c.14.72.832 1.031 1.529 1.031.791 0 1.57-.597 1.57-1.681 0-.967-.732-1.57-1.582-1.57-.767 0-1.242.45-1.435.808H5.445L5.791 4h4.705v1.103H6.875l-.193 2.343h.064c.17-.258.715-.68 1.611-.68 1.383 0 2.561.944 2.561 2.585 0 1.687-1.184 2.806-2.924 2.806Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 514 B |
3
dbapp/static/bootstrap-icons/5-square-fill.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-5-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm5.994 12.158c-1.57 0-2.654-.902-2.719-2.115h1.237c.14.72.832 1.031 1.529 1.031.791 0 1.57-.597 1.57-1.681 0-.967-.732-1.57-1.582-1.57-.767 0-1.242.45-1.435.808H5.445L5.791 4h4.705v1.103H6.875l-.193 2.343h.064c.17-.258.715-.68 1.611-.68 1.383 0 2.561.944 2.561 2.585 0 1.687-1.184 2.806-2.924 2.806Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 521 B |
4
dbapp/static/bootstrap-icons/5-square.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-5-square" viewBox="0 0 16 16">
|
||||
<path d="M7.994 12.158c-1.57 0-2.654-.902-2.719-2.115h1.237c.14.72.832 1.031 1.529 1.031.791 0 1.57-.597 1.57-1.681 0-.967-.732-1.57-1.582-1.57-.767 0-1.242.45-1.435.808H5.445L5.791 4h4.705v1.103H6.875l-.193 2.343h.064c.17-.258.715-.68 1.611-.68 1.383 0 2.561.944 2.561 2.585 0 1.687-1.184 2.806-2.924 2.806Z"/>
|
||||
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 601 B |
3
dbapp/static/bootstrap-icons/6-circle-fill.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-6-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M8.21 3.855c-1.868 0-3.116 1.395-3.116 4.407 0 1.183.228 2.039.597 2.642.569.926 1.477 1.254 2.409 1.254 1.629 0 2.847-1.013 2.847-2.783 0-1.676-1.254-2.555-2.508-2.555-1.125 0-1.752.61-1.98 1.155h-.082c-.012-1.946.727-3.036 1.805-3.036.802 0 1.213.457 1.312.815h1.29c-.06-.908-.962-1.899-2.573-1.899Zm-.099 4.008c-.92 0-1.564.65-1.564 1.576 0 1.032.703 1.635 1.558 1.635.868 0 1.553-.533 1.553-1.629 0-1.06-.744-1.582-1.547-1.582"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 617 B |
3
dbapp/static/bootstrap-icons/6-circle.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-6-circle" viewBox="0 0 16 16">
|
||||
<path d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0M8.21 3.855c1.612 0 2.515.99 2.573 1.899H9.494c-.1-.358-.51-.815-1.312-.815-1.078 0-1.817 1.09-1.805 3.036h.082c.229-.545.855-1.155 1.98-1.155 1.254 0 2.508.88 2.508 2.555 0 1.77-1.218 2.783-2.847 2.783-.932 0-1.84-.328-2.409-1.254-.369-.603-.597-1.459-.597-2.642 0-3.012 1.248-4.407 3.117-4.407Zm-.099 4.008c-.92 0-1.564.65-1.564 1.576 0 1.032.703 1.635 1.558 1.635.868 0 1.553-.533 1.553-1.629 0-1.06-.744-1.582-1.547-1.582"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 640 B |
4
dbapp/static/bootstrap-icons/6-square-fill.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-6-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M8.111 7.863c-.92 0-1.564.65-1.564 1.576 0 1.032.703 1.635 1.558 1.635.868 0 1.553-.533 1.553-1.629 0-1.06-.744-1.582-1.547-1.582"/>
|
||||
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm6.21 3.855c1.612 0 2.515.99 2.573 1.899H9.494c-.1-.358-.51-.815-1.312-.815-1.078 0-1.817 1.09-1.805 3.036h.082c.229-.545.855-1.155 1.98-1.155 1.254 0 2.508.88 2.508 2.555 0 1.77-1.218 2.783-2.847 2.783-.932 0-1.84-.328-2.409-1.254-.369-.603-.597-1.459-.597-2.642 0-3.012 1.248-4.407 3.117-4.407Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 662 B |
4
dbapp/static/bootstrap-icons/6-square.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-6-square" viewBox="0 0 16 16">
|
||||
<path d="M8.21 3.855c1.612 0 2.515.99 2.573 1.899H9.494c-.1-.358-.51-.815-1.312-.815-1.078 0-1.817 1.09-1.805 3.036h.082c.229-.545.855-1.155 1.98-1.155 1.254 0 2.508.88 2.508 2.555 0 1.77-1.218 2.783-2.847 2.783-.932 0-1.84-.328-2.409-1.254-.369-.603-.597-1.459-.597-2.642 0-3.012 1.248-4.407 3.117-4.407Zm-.099 4.008c-.92 0-1.564.65-1.564 1.576 0 1.032.703 1.635 1.558 1.635.868 0 1.553-.533 1.553-1.629 0-1.06-.744-1.582-1.547-1.582"/>
|
||||
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 727 B |
3
dbapp/static/bootstrap-icons/7-circle-fill.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-7-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M5.37 5.11h3.972v.07L6.025 12H7.42l3.258-6.85V4.002H5.369v1.107Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 251 B |
3
dbapp/static/bootstrap-icons/7-circle.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-7-circle" viewBox="0 0 16 16">
|
||||
<path d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0M5.37 5.11V4.001h5.308V5.15L7.42 12H6.025l3.317-6.82v-.07H5.369Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 279 B |
3
dbapp/static/bootstrap-icons/7-square-fill.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-7-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm3.37 5.11V4.001h5.308V5.15L7.42 12H6.025l3.317-6.82v-.07H5.369Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 286 B |
4
dbapp/static/bootstrap-icons/7-square.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-7-square" viewBox="0 0 16 16">
|
||||
<path d="M5.37 5.11V4.001h5.308V5.15L7.42 12H6.025l3.317-6.82v-.07H5.369Z"/>
|
||||
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 366 B |
3
dbapp/static/bootstrap-icons/8-circle-fill.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-8-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-5.03 1.803c0-1.248-.943-1.84-1.646-1.992v-.065c.598-.187 1.336-.72 1.336-1.781 0-1.225-1.084-2.121-2.654-2.121s-2.66.896-2.66 2.12c0 1.044.709 1.589 1.33 1.782v.065c-.697.152-1.647.732-1.647 2.003 0 1.39 1.19 2.344 2.953 2.344 1.77 0 2.989-.96 2.989-2.355Zm-4.347-3.71c0 .739.586 1.255 1.383 1.255s1.377-.516 1.377-1.254c0-.733-.58-1.23-1.377-1.23s-1.383.497-1.383 1.23Zm-.281 3.645c0 .838.72 1.412 1.664 1.412.943 0 1.658-.574 1.658-1.412 0-.843-.715-1.424-1.658-1.424-.944 0-1.664.58-1.664 1.424"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 686 B |
3
dbapp/static/bootstrap-icons/8-circle.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-8-circle" viewBox="0 0 16 16">
|
||||
<path d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-5.03 1.803c0 1.394-1.218 2.355-2.988 2.355-1.763 0-2.953-.955-2.953-2.344 0-1.271.95-1.851 1.647-2.003v-.065c-.621-.193-1.33-.738-1.33-1.781 0-1.225 1.09-2.121 2.66-2.121s2.654.896 2.654 2.12c0 1.061-.738 1.595-1.336 1.782v.065c.703.152 1.647.744 1.647 1.992Zm-4.347-3.71c0 .739.586 1.255 1.383 1.255s1.377-.516 1.377-1.254c0-.733-.58-1.23-1.377-1.23s-1.383.497-1.383 1.23Zm-.281 3.645c0 .838.72 1.412 1.664 1.412.943 0 1.658-.574 1.658-1.412 0-.843-.715-1.424-1.658-1.424-.944 0-1.664.58-1.664 1.424"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 717 B |
4
dbapp/static/bootstrap-icons/8-square-fill.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-8-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M6.623 6.094c0 .738.586 1.254 1.383 1.254s1.377-.516 1.377-1.254c0-.733-.58-1.23-1.377-1.23s-1.383.497-1.383 1.23m-.281 3.644c0 .838.72 1.412 1.664 1.412.943 0 1.658-.574 1.658-1.412 0-.843-.715-1.424-1.658-1.424-.944 0-1.664.58-1.664 1.424"/>
|
||||
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm8.97 9.803c0 1.394-1.218 2.355-2.988 2.355-1.763 0-2.953-.955-2.953-2.344 0-1.271.95-1.851 1.647-2.003v-.065c-.621-.193-1.33-.738-1.33-1.781 0-1.225 1.09-2.121 2.66-2.121s2.654.896 2.654 2.12c0 1.061-.738 1.595-1.336 1.782v.065c.703.152 1.647.744 1.647 1.992Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 737 B |
4
dbapp/static/bootstrap-icons/8-square.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-8-square" viewBox="0 0 16 16">
|
||||
<path d="M10.97 9.803c0 1.394-1.218 2.355-2.988 2.355-1.763 0-2.953-.955-2.953-2.344 0-1.271.95-1.851 1.647-2.003v-.065c-.621-.193-1.33-.738-1.33-1.781 0-1.225 1.09-2.121 2.66-2.121s2.654.896 2.654 2.12c0 1.061-.738 1.595-1.336 1.782v.065c.703.152 1.647.744 1.647 1.992Zm-4.347-3.71c0 .739.586 1.255 1.383 1.255s1.377-.516 1.377-1.254c0-.733-.58-1.23-1.377-1.23s-1.383.497-1.383 1.23Zm-.281 3.645c0 .838.72 1.412 1.664 1.412.943 0 1.658-.574 1.658-1.412 0-.843-.715-1.424-1.658-1.424-.944 0-1.664.58-1.664 1.424"/>
|
||||
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 804 B |
3
dbapp/static/bootstrap-icons/9-circle-fill.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-9-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-8.223 4.146c2.104 0 3.123-1.464 3.123-4.3 0-3.147-1.459-4.014-2.97-4.014-1.63 0-2.871 1.02-2.871 2.73 0 1.706 1.171 2.667 2.566 2.667 1.06 0 1.7-.557 1.934-1.184h.076c.047 1.67-.475 3.023-1.834 3.023-.71 0-1.149-.363-1.248-.72H5.258c.094.908.926 1.798 2.52 1.798Zm.118-3.972c.808 0 1.535-.528 1.535-1.594s-.668-1.676-1.56-1.676c-.838 0-1.517.616-1.517 1.659 0 1.072.708 1.61 1.54 1.61Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 574 B |