Добавил альтернативное имя у спутника

This commit is contained in:
2025-12-01 12:19:24 +03:00
parent 01871c3e13
commit c72bf12d41
14 changed files with 720 additions and 220 deletions

View File

@@ -53,9 +53,13 @@ def process_single_satellite(
logger.info(f"Найдено {len(sources)} источников для {sat_name}") logger.info(f"Найдено {len(sources)} источников для {sat_name}")
# Находим спутник в базе # Находим спутник в базе по имени или альтернативному имени (lowercase)
from django.db.models import Q
sat_name_lower = sat_name.lower()
try: try:
sat_obj = Satellite.objects.get(name__icontains=sat_name) sat_obj = Satellite.objects.get(
Q(name__icontains=sat_name_lower) | Q(alternative_name__icontains=sat_name_lower)
)
logger.debug(f"Спутник {sat_name} найден в базе (ID: {sat_obj.id})") logger.debug(f"Спутник {sat_name} найден в базе (ID: {sat_obj.id})")
except Satellite.DoesNotExist: except Satellite.DoesNotExist:
error_msg = f"Спутник '{sat_name}' не найден в базе данных" error_msg = f"Спутник '{sat_name}' не найден в базе данных"

View File

@@ -76,9 +76,13 @@ def fill_lyngsat_data(
logger.info(f"{log_prefix} Найдено {len(sources)} источников для {sat_name}") logger.info(f"{log_prefix} Найдено {len(sources)} источников для {sat_name}")
# Находим спутник в базе # Находим спутник в базе по имени или альтернативному имени (lowercase)
from django.db.models import Q
sat_name_lower = sat_name.lower()
try: try:
sat_obj = Satellite.objects.get(name__icontains=sat_name) sat_obj = Satellite.objects.get(
Q(name__icontains=sat_name_lower) | Q(alternative_name__icontains=sat_name_lower)
)
logger.debug(f"{log_prefix} Спутник {sat_name} найден в базе (ID: {sat_obj.id})") logger.debug(f"{log_prefix} Спутник {sat_name} найден в базе (ID: {sat_obj.id})")
except Satellite.DoesNotExist: except Satellite.DoesNotExist:
error_msg = f"Спутник '{sat_name}' не найден в базе данных" error_msg = f"Спутник '{sat_name}' не найден в базе данных"

View File

@@ -573,6 +573,7 @@ class SatelliteAdmin(BaseAdmin):
list_display = ( list_display = (
"name", "name",
"alternative_name",
"norad", "norad",
"international_code", "international_code",
"undersat_point", "undersat_point",
@@ -580,7 +581,7 @@ class SatelliteAdmin(BaseAdmin):
"created_at", "created_at",
"updated_at", "updated_at",
) )
search_fields = ("name", "norad", "international_code") search_fields = ("name", "alternative_name", "norad", "international_code")
ordering = ("name",) ordering = ("name",)
filter_horizontal = ("band",) filter_horizontal = ("band",)
autocomplete_fields = ("band",) autocomplete_fields = ("band",)

View File

@@ -815,6 +815,7 @@ class SatelliteForm(forms.ModelForm):
model = Satellite model = Satellite
fields = [ fields = [
'name', 'name',
'alternative_name',
'norad', 'norad',
'international_code', 'international_code',
'band', 'band',
@@ -829,6 +830,10 @@ class SatelliteForm(forms.ModelForm):
'placeholder': 'Введите название спутника', 'placeholder': 'Введите название спутника',
'required': True 'required': True
}), }),
'alternative_name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Введите альтернативное название (необязательно)'
}),
'norad': forms.NumberInput(attrs={ 'norad': forms.NumberInput(attrs={
'class': 'form-control', 'class': 'form-control',
'placeholder': 'Введите NORAD ID' 'placeholder': 'Введите NORAD ID'
@@ -862,6 +867,7 @@ class SatelliteForm(forms.ModelForm):
} }
labels = { labels = {
'name': 'Название спутника', 'name': 'Название спутника',
'alternative_name': 'Альтернативное название',
'norad': 'NORAD ID', 'norad': 'NORAD ID',
'international_code': 'Международный код', 'international_code': 'Международный код',
'band': 'Диапазоны работы', 'band': 'Диапазоны работы',
@@ -872,6 +878,7 @@ class SatelliteForm(forms.ModelForm):
} }
help_texts = { help_texts = {
'name': 'Уникальное название спутника', 'name': 'Уникальное название спутника',
'alternative_name': 'Альтернативное название спутника (например, на другом языке)',
'norad': 'Идентификатор NORAD для отслеживания спутника', 'norad': 'Идентификатор NORAD для отслеживания спутника',
'international_code': 'Международный идентификатор спутника (например, 2011-074A)', 'international_code': 'Международный идентификатор спутника (например, 2011-074A)',
'band': 'Выберите диапазоны работы спутника (удерживайте Ctrl для множественного выбора)', 'band': 'Выберите диапазоны работы спутника (удерживайте Ctrl для множественного выбора)',

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.2.7 on 2025-12-01 08:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0016_alter_satellite_international_code_techanalyze'),
]
operations = [
migrations.AddField(
model_name='satellite',
name='alternative_name',
field=models.CharField(blank=True, db_index=True, help_text='Альтернативное название спутника (например, из скобок)', max_length=100, null=True, verbose_name='Альтернативное имя'),
),
migrations.AlterField(
model_name='standard',
name='name',
field=models.CharField(db_index=True, help_text='Стандарт передачи данных (DVB-S, DVB-S2, DVB-S2X и т.д.)', max_length=80, unique=True, verbose_name='Стандарт'),
),
]

View File

@@ -344,6 +344,14 @@ class Satellite(models.Model):
db_index=True, db_index=True,
help_text="Название спутника", help_text="Название спутника",
) )
alternative_name = models.CharField(
max_length=100,
blank=True,
null=True,
verbose_name="Альтернативное имя",
db_index=True,
help_text="Альтернативное название спутника (например, из скобок)",
)
norad = models.IntegerField( norad = models.IntegerField(
blank=True, blank=True,
null=True, null=True,

View File

@@ -43,8 +43,13 @@ function showSatelliteModal(satelliteId) {
'<div class="col-md-6"><div class="card h-100">' + '<div class="col-md-6"><div class="card h-100">' +
'<div class="card-header bg-light"><strong><i class="bi bi-info-circle"></i> Основная информация</strong></div>' + '<div class="card-header bg-light"><strong><i class="bi bi-info-circle"></i> Основная информация</strong></div>' +
'<div class="card-body"><table class="table table-sm table-borderless mb-0"><tbody>' + '<div class="card-body"><table class="table table-sm table-borderless mb-0"><tbody>' +
'<tr><td class="text-muted" style="width: 40%;">Название:</td><td><strong>' + data.name + '</strong></td></tr>' + '<tr><td class="text-muted" style="width: 40%;">Название:</td><td><strong>' + data.name + '</strong></td></tr>';
'<tr><td class="text-muted">NORAD ID:</td><td>' + data.norad + '</td></tr>' +
if (data.alternative_name && data.alternative_name !== '-') {
html += '<tr><td class="text-muted">Альтернативное название:</td><td><strong>' + data.alternative_name + '</strong></td></tr>';
}
html += '<tr><td class="text-muted">NORAD ID:</td><td>' + data.norad + '</td></tr>' +
'<tr><td class="text-muted">Подспутниковая точка:</td><td><strong>' + data.undersat_point + '</strong></td></tr>' + '<tr><td class="text-muted">Подспутниковая точка:</td><td><strong>' + data.undersat_point + '</strong></td></tr>' +
'<tr><td class="text-muted">Диапазоны:</td><td>' + data.bands + '</td></tr>' + '<tr><td class="text-muted">Диапазоны:</td><td>' + data.bands + '</td></tr>' +
'</tbody></table></div></div></div>' + '</tbody></table></div></div></div>' +

View File

@@ -180,6 +180,9 @@
<button id="export-xlsx" class="btn btn-success" disabled> <button id="export-xlsx" class="btn btn-success" disabled>
<i class="bi bi-file-earmark-excel"></i> Сохранить в Excel <i class="bi bi-file-earmark-excel"></i> Сохранить в Excel
</button> </button>
<button id="export-json" class="btn btn-info ms-2" disabled>
<i class="bi bi-filetype-json"></i> Сохранить в JSON
</button>
<button id="clear-table" class="btn btn-danger ms-2"> <button id="clear-table" class="btn btn-danger ms-2">
<i class="bi bi-trash"></i> Очистить таблицу <i class="bi bi-trash"></i> Очистить таблицу
</button> </button>
@@ -292,18 +295,21 @@ document.addEventListener('DOMContentLoaded', function() {
{ {
title: "Действия", title: "Действия",
field: "actions", field: "actions",
minWidth: 100, minWidth: 120,
widthGrow: 1, widthGrow: 1,
hozAlign: "center", hozAlign: "center",
formatter: function(cell, formatterParams, onRendered) { formatter: function(cell, formatterParams, onRendered) {
const data = cell.getRow().getData(); const data = cell.getRow().getData();
const btnClass = data.has_outliers ? 'btn-warning' : 'btn-info'; const btnClass = data.has_outliers ? 'btn-warning' : 'btn-info';
return `<button class="btn btn-sm ${btnClass} btn-view-details" title="Просмотр точек"><i class="bi bi-eye"></i></button>`; return `<button class="btn btn-sm ${btnClass} btn-view-details" title="Просмотр точек"><i class="bi bi-eye"></i></button>
<button class="btn btn-sm btn-danger btn-delete-row ms-1" title="Удалить строку"><i class="bi bi-trash"></i></button>`;
}, },
cellClick: function(e, cell) { cellClick: function(e, cell) {
const data = cell.getRow().getData(); const data = cell.getRow().getData();
if (e.target.closest('.btn-view-details')) { if (e.target.closest('.btn-view-details')) {
showGroupDetails(data._groupIndex); showGroupDetails(data._groupIndex);
} else if (e.target.closest('.btn-delete-row')) {
deleteGroupRow(data._groupIndex);
} }
} }
} }
@@ -321,6 +327,31 @@ document.addEventListener('DOMContentLoaded', function() {
function updateGroupCount() { function updateGroupCount() {
document.getElementById('group-count').textContent = allGroupsData.length; document.getElementById('group-count').textContent = allGroupsData.length;
document.getElementById('export-xlsx').disabled = allGroupsData.length === 0; document.getElementById('export-xlsx').disabled = allGroupsData.length === 0;
document.getElementById('export-json').disabled = allGroupsData.length === 0;
}
// Delete group row
function deleteGroupRow(groupIndex) {
if (!confirm('Удалить эту группу из таблицы?')) {
return;
}
// Удаляем группу из массива
allGroupsData.splice(groupIndex, 1);
// Пересчитываем индексы для оставшихся групп
allGroupsData.forEach((group, idx) => {
group._groupIndex = idx;
});
// Обновляем таблицу
table.setData(allGroupsData.map(g => ({
...g,
status: g.has_outliers ? 'outliers' : 'ok'
})));
updateGroupCount();
updateAllPointsTable();
} }
// Show loading overlay // Show loading overlay
@@ -837,6 +868,252 @@ document.addEventListener('DOMContentLoaded', function() {
return cookieValue; return cookieValue;
} }
// Generate UUID v4
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
// Export to JSON
document.getElementById('export-json').addEventListener('click', function() {
if (allGroupsData.length === 0) {
alert('Нет данных для экспорта');
return;
}
// Creator ID - можно изменить на нужное значение
const CREATOR_ID = '6fd12c90-7f17-43d9-a03e-ee14e880f757';
// Начальный объект path (фиксированный)
const pathObject = {
"tacticObjectType": "path",
"captionPosition": "right",
"points": [
{
"id": "b92b9cbb-dd27-49aa-bcb6-e89a147bc02c",
"latitude": 57,
"longitude": -13,
"altitude": 0,
"customActions": [],
"tags": { "creator": CREATOR_ID },
"tacticObjectType": "point"
},
{
"id": "8e3666d4-4990-4cb9-9594-63ad06333489",
"latitude": 57,
"longitude": 64,
"altitude": 0,
"customActions": [],
"tags": { "creator": CREATOR_ID },
"tacticObjectType": "point"
},
{
"id": "5f137485-d2fc-443d-8507-c936f02f3569",
"latitude": 11,
"longitude": 64,
"altitude": 0,
"customActions": [],
"tags": { "creator": CREATOR_ID },
"tacticObjectType": "point"
},
{
"id": "0fb90df7-8eb0-49fa-9d00-336389171bf5",
"latitude": 11,
"longitude": -13,
"altitude": 0,
"customActions": [],
"tags": { "creator": CREATOR_ID },
"tacticObjectType": "point"
},
{
"id": "3ef12637-585e-40a4-b0ee-8f1786c89ce6",
"latitude": 57,
"longitude": -13,
"altitude": 0,
"customActions": [],
"tags": { "creator": CREATOR_ID },
"tacticObjectType": "point"
}
],
"isCycle": false,
"id": "2f604051-4984-4c2f-8c4c-c0cb64008f5f",
"draggable": false,
"selectable": false,
"editable": false,
"caption": "Ограничение для работы с поверхностями",
"line": {
"color": "rgb(148,0,211)",
"thickness": 1,
"dash": "solid",
"border": null
},
"customActions": [],
"tags": { "creator": CREATOR_ID }
};
// Результирующий массив
const result = [pathObject];
// Палитра цветов для групп (RGB формат)
const jsonGroupColors = [
"rgb(0,128,0)", // зелёный
"rgb(0,0,255)", // синий
"rgb(255,0,0)", // красный
"rgb(255,165,0)", // оранжевый
"rgb(128,0,128)", // фиолетовый
"rgb(0,128,128)", // бирюзовый
"rgb(255,20,147)", // розовый
"rgb(139,69,19)", // коричневый
"rgb(0,100,0)", // тёмно-зелёный
"rgb(70,130,180)" // стальной синий
];
// Обрабатываем каждую группу
allGroupsData.forEach((group, groupIndex) => {
// Цвет для текущей группы
const groupColor = jsonGroupColors[groupIndex % jsonGroupColors.length];
// Формируем имя для усреднённой точки с пометкой "(усредн)"
const avgName = group.source_name;
const avgTime = group.avg_time || '-';
const avgCaption = `${avgName} (усредн) - ${avgTime}`;
// Получаем координаты усреднённой точки
const avgCoord = group.avg_coord_tuple;
const avgLat = avgCoord[1];
const avgLon = avgCoord[0];
// ID для усреднённой точки (source)
const avgSourceId = generateUUID();
// Создаём source для усреднённой точки (triangle)
const avgSource = {
"tacticObjectType": "source",
"captionPosition": "right",
"id": avgSourceId,
"icon": {
"type": "triangle",
"color": groupColor
},
"caption": avgCaption,
"name": avgCaption,
"customActions": [],
"trackBehavior": {},
"bearingStyle": {
"color": groupColor,
"thickness": 2,
"dash": "solid",
"border": null
},
"bearingBehavior": {},
"tags": { "creator": CREATOR_ID }
};
result.push(avgSource);
// Создаём position для усреднённой точки
const avgPosition = {
"tacticObjectType": "position",
"id": generateUUID(),
"parentId": avgSourceId,
"timeStamp": Date.now() / 1000,
"latitude": avgLat,
"altitude": 0,
"longitude": avgLon,
"caption": "",
"tooltip": "",
"customActions": [],
"tags": {
"layers": [],
"creator": CREATOR_ID
}
};
result.push(avgPosition);
// Обрабатываем все точки группы (не выбросы)
group.points.forEach(point => {
if (point.is_outlier) return; // Пропускаем выбросы
const pointCoord = point.coord_tuple;
const pointLat = pointCoord[1];
const pointLon = pointCoord[0];
const pointName = point.name || '-';
const pointTime = point.timestamp || '-';
const pointCaption = `${pointName} - ${pointTime}`;
// ID для source точки
const pointSourceId = generateUUID();
// Создаём source для точки (circle) с тем же цветом группы
const pointSource = {
"tacticObjectType": "source",
"captionPosition": "right",
"id": pointSourceId,
"icon": {
"type": "circle",
"color": groupColor
},
"caption": pointCaption,
"name": pointCaption,
"customActions": [],
"trackBehavior": {},
"bearingStyle": {
"color": groupColor,
"thickness": 2,
"dash": "solid",
"border": null
},
"bearingBehavior": {},
"tags": { "creator": CREATOR_ID }
};
result.push(pointSource);
// Создаём position для точки
const pointPosition = {
"tacticObjectType": "position",
"id": generateUUID(),
"parentId": pointSourceId,
"timeStamp": point.timestamp_unix || (Date.now() / 1000),
"latitude": pointLat,
"altitude": 0,
"longitude": pointLon,
"caption": "",
"tooltip": "",
"customActions": [],
"tags": {
"layers": [],
"creator": CREATOR_ID
}
};
result.push(pointPosition);
});
});
// Конвертируем в JSON строку
const jsonString = JSON.stringify(result, null, 2);
// Добавляем BOM для UTF-8
const BOM = '\uFEFF';
const blob = new Blob([BOM + jsonString], { type: 'application/json;charset=utf-8' });
// Генерируем имя файла
const now = new Date();
const dateStr = now.toISOString().slice(0, 10);
const filename = `averaging_${dateStr}.json`;
// Скачиваем файл
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
// Initialize // Initialize
updateGroupCount(); updateGroupCount();
}); });

View File

@@ -86,6 +86,25 @@
</div> </div>
</div> </div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.alternative_name.id_for_label }}" class="form-label">
{{ form.alternative_name.label }}
</label>
{{ form.alternative_name }}
{% if form.alternative_name.errors %}
<div class="invalid-feedback d-block">
{{ form.alternative_name.errors.0 }}
</div>
{% endif %}
{% if form.alternative_name.help_text %}
<div class="form-text">{{ form.alternative_name.help_text }}</div>
{% endif %}
</div>
</div>
</div>
<div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label for="{{ form.norad.id_for_label }}" class="form-label"> <label for="{{ form.norad.id_for_label }}" class="form-label">
@@ -102,9 +121,7 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div>
<div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label for="{{ form.international_code.id_for_label }}" class="form-label"> <label for="{{ form.international_code.id_for_label }}" class="form-label">
@@ -121,7 +138,9 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div>
<div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label for="{{ form.undersat_point.id_for_label }}" class="form-label"> <label for="{{ form.undersat_point.id_for_label }}" class="form-label">
@@ -138,6 +157,23 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.launch_date.id_for_label }}" class="form-label">
{{ form.launch_date.label }}
</label>
{{ form.launch_date }}
{% if form.launch_date.errors %}
<div class="invalid-feedback d-block">
{{ form.launch_date.errors.0 }}
</div>
{% endif %}
{% if form.launch_date.help_text %}
<div class="form-text">{{ form.launch_date.help_text }}</div>
{% endif %}
</div>
</div>
</div> </div>
<div class="row"> <div class="row">
@@ -160,24 +196,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-12">
<div class="mb-3">
<label for="{{ form.launch_date.id_for_label }}" class="form-label">
{{ form.launch_date.label }}
</label>
{{ form.launch_date }}
{% if form.launch_date.errors %}
<div class="invalid-feedback d-block">
{{ form.launch_date.errors.0 }}
</div>
{% endif %}
{% if form.launch_date.help_text %}
<div class="form-text">{{ form.launch_date.help_text }}</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label for="{{ form.url.id_for_label }}" class="form-label"> <label for="{{ form.url.id_for_label }}" class="form-label">
{{ form.url.label }} {{ form.url.label }}

View File

@@ -190,6 +190,16 @@
{% endif %} {% endif %}
</a> </a>
</th> </th>
<th scope="col" style="min-width: 150px;">
<a href="javascript:void(0)" onclick="updateSort('alternative_name')" class="text-white text-decoration-none">
Альт. название
{% if sort == 'alternative_name' %}
<i class="bi bi-arrow-up"></i>
{% elif sort == '-alternative_name' %}
<i class="bi bi-arrow-down"></i>
{% endif %}
</a>
</th>
<th scope="col" style="min-width: 100px;"> <th scope="col" style="min-width: 100px;">
<a href="javascript:void(0)" onclick="updateSort('norad')" class="text-white text-decoration-none"> <a href="javascript:void(0)" onclick="updateSort('norad')" class="text-white text-decoration-none">
NORAD ID NORAD ID
@@ -274,6 +284,7 @@
</td> </td>
<td class="text-center">{{ satellite.id }}</td> <td class="text-center">{{ satellite.id }}</td>
<td>{{ satellite.name }}</td> <td>{{ satellite.name }}</td>
<td>{{ satellite.alternative_name|default:"-" }}</td>
<td>{{ satellite.norad }}</td> <td>{{ satellite.norad }}</td>
<td>{{ satellite.international_code|default:"-" }}</td> <td>{{ satellite.international_code|default:"-" }}</td>
<td>{{ satellite.bands }}</td> <td>{{ satellite.bands }}</td>
@@ -307,7 +318,7 @@
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="13" class="text-center text-muted">Нет данных для отображения</td> <td colspan="14" class="text-center text-muted">Нет данных для отображения</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@@ -138,7 +138,7 @@ def find_mirror_satellites(mirror_names: list) -> list:
Алгоритм: Алгоритм:
1. Для каждого имени зеркала: 1. Для каждого имени зеркала:
- Обрезать пробелы и привести к нижнему регистру - Обрезать пробелы и привести к нижнему регистру
- Найти все спутники, в имени которых содержится это имя - Найти все спутники, в имени или альтернативном имени которых содержится это имя
2. Вернуть список найденных спутников 2. Вернуть список найденных спутников
Args: Args:
@@ -147,6 +147,8 @@ def find_mirror_satellites(mirror_names: list) -> list:
Returns: Returns:
list: список объектов Satellite list: список объектов Satellite
""" """
from django.db.models import Q
found_satellites = [] found_satellites = []
for mirror_name in mirror_names: for mirror_name in mirror_names:
@@ -159,9 +161,9 @@ def find_mirror_satellites(mirror_names: list) -> list:
if not mirror_name_clean: if not mirror_name_clean:
continue continue
# Ищем спутники, в имени которых содержится имя зеркала # Ищем спутники, в имени или альтернативном имени которых содержится имя зеркала
satellites = Satellite.objects.filter( satellites = Satellite.objects.filter(
name__icontains=mirror_name_clean Q(name__icontains=mirror_name_clean) | Q(alternative_name__icontains=mirror_name_clean)
) )
found_satellites.extend(satellites) found_satellites.extend(satellites)

View File

@@ -605,6 +605,7 @@ class SatelliteDataAPIView(LoginRequiredMixin, View):
data = { data = {
'id': satellite.id, 'id': satellite.id,
'name': satellite.name, 'name': satellite.name,
'alternative_name': satellite.alternative_name or '-',
'norad': satellite.norad if satellite.norad else None, 'norad': satellite.norad if satellite.norad else None,
'bands': bands_str, 'bands': bands_str,
'undersat_point': satellite.undersat_point if satellite.undersat_point is not None else None, 'undersat_point': satellite.undersat_point if satellite.undersat_point is not None else None,

View File

@@ -124,10 +124,11 @@ class SatelliteListView(LoginRequiredMixin, View):
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass
# Search by name # Search by name, alternative_name, or comment
if search_query: if search_query:
satellites = satellites.filter( satellites = satellites.filter(
Q(name__icontains=search_query) | Q(name__icontains=search_query) |
Q(alternative_name__icontains=search_query) |
Q(comment__icontains=search_query) Q(comment__icontains=search_query)
) )
@@ -137,6 +138,8 @@ class SatelliteListView(LoginRequiredMixin, View):
"-id": "-id", "-id": "-id",
"name": "name", "name": "name",
"-name": "-name", "-name": "-name",
"alternative_name": "alternative_name",
"-alternative_name": "-alternative_name",
"norad": "norad", "norad": "norad",
"-norad": "-norad", "-norad": "-norad",
"international_code": "international_code", "international_code": "international_code",
@@ -169,6 +172,7 @@ class SatelliteListView(LoginRequiredMixin, View):
processed_satellites.append({ processed_satellites.append({
'id': satellite.id, 'id': satellite.id,
'name': satellite.name or "-", 'name': satellite.name or "-",
'alternative_name': satellite.alternative_name or "-",
'norad': satellite.norad if satellite.norad else "-", 'norad': satellite.norad if satellite.norad else "-",
'international_code': satellite.international_code or "-", 'international_code': satellite.international_code or "-",
'bands': ", ".join(band_names) if band_names else "-", 'bands': ", ".join(band_names) if band_names else "-",

View File

@@ -5,12 +5,99 @@ from io import BytesIO
# Third-party imports # Third-party imports
import requests import requests
from django.db.models import Q
# Local imports # Local imports
from mainapp.models import Polarization, Satellite from mainapp.models import Polarization, Satellite
from .models import Transponders from .models import Transponders
def parse_satellite_name(full_name: str) -> tuple[str, str | None]:
"""
Парсит полное имя спутника и извлекает основное и альтернативное имя.
Альтернативное имя находится в скобках после основного названия.
Примеры:
"Koreasat 116 (ANASIS-II)" -> ("Koreasat 116", "ANASIS-II")
"Thaicom 6 (Africom 1)" -> ("Thaicom 6", "Africom 1")
"Express AM6" -> ("Express AM6", None)
Args:
full_name: Полное имя спутника (может содержать альтернативное имя в скобках)
Returns:
tuple: (основное_имя, альтернативное_имя или None)
"""
if not full_name:
return (full_name, None)
# Ищем текст в скобках в конце строки
match = re.match(r'^(.+?)\s*\(([^)]+)\)\s*$$', full_name.strip())
if match:
main_name = match.group(1).strip()
alt_name = match.group(2).strip()
return (main_name, alt_name)
return (full_name.strip(), None)
def find_satellite_by_name(name: str):
"""
Ищет спутник по имени или альтернативному имени.
Все сравнения выполняются в lowercase.
Алгоритм поиска:
1. Точное совпадение по name (lowercase)
2. Точное совпадение по alternative_name (lowercase)
3. Частичное совпадение по name (lowercase)
4. Частичное совпадение по alternative_name (lowercase)
Args:
name: Имя спутника для поиска
Returns:
Satellite или None: Найденный спутник или None
Raises:
Satellite.MultipleObjectsReturned: Если найдено несколько спутников
"""
if not name:
return None
name_lower = name.strip().lower()
# 1. Точное совпадение по name (lowercase)
try:
return Satellite.objects.get(name__iexact=name_lower)
except Satellite.DoesNotExist:
pass
except Satellite.MultipleObjectsReturned:
raise
# 2. Точное совпадение по alternative_name (lowercase)
try:
return Satellite.objects.get(alternative_name__iexact=name_lower)
except Satellite.DoesNotExist:
pass
except Satellite.MultipleObjectsReturned:
raise
# 3. Частичное совпадение по name или alternative_name (lowercase)
satellites = Satellite.objects.filter(
Q(name__icontains=name_lower) | Q(alternative_name__icontains=name_lower)
)
if satellites.count() == 1:
return satellites.first()
elif satellites.count() > 1:
raise Satellite.MultipleObjectsReturned(
f"Найдено несколько спутников с именем '{name_lower}'"
)
return None
def search_satellite_on_page(data: dict, satellite_name: str): def search_satellite_on_page(data: dict, satellite_name: str):
for pos, value in data.get('page', {}).get('positions').items(): for pos, value in data.get('page', {}).get('positions').items():
for name in value['satellites']: for name in value['satellites']:
@@ -82,13 +169,19 @@ def parse_transponders_from_json(filepath: str, user=None):
""" """
Парсит транспондеры из JSON файла. Парсит транспондеры из JSON файла.
Если имя спутника содержит альтернативное имя в скобках, оно извлекается
и сохраняется в поле alternative_name.
Args: Args:
filepath: путь к JSON файлу filepath: путь к JSON файлу
user: пользователь для установки created_by и updated_by (optional) user: пользователь для установки created_by и updated_by (optional)
""" """
with open(filepath, encoding="utf-8") as jf: with open(filepath, encoding="utf-8") as jf:
data = json.load(jf) data = json.load(jf)
for sat_name, trans_zone in data["satellites"].items(): for sat_name_full, trans_zone in data["satellites"].items():
# Парсим имя спутника и альтернативное имя
main_name, alt_name = parse_satellite_name(sat_name_full)
for zone, trans in trans_zone.items(): for zone, trans in trans_zone.items():
for tran in trans: for tran in trans:
f_b, f_e = tran["freq"][0].split("-") f_b, f_e = tran["freq"][0].split("-")
@@ -96,7 +189,19 @@ def parse_transponders_from_json(filepath: str, user=None):
f_range = round(abs(float(f_e) - float(f_b)), 3) f_range = round(abs(float(f_e) - float(f_b)), 3)
pol_obj = Polarization.objects.get(name=tran["pol"]) pol_obj = Polarization.objects.get(name=tran["pol"])
sat_obj = Satellite.objects.get(name__iexact=sat_name)
# Ищем спутник по имени или альтернативному имени
sat_obj = find_satellite_by_name(main_name)
if not sat_obj:
# Если не найден, создаём новый с альтернативным именем
sat_obj = Satellite.objects.create(
name=main_name,
alternative_name=alt_name
)
elif alt_name and not sat_obj.alternative_name:
# Если найден, но альтернативное имя не установлено - обновляем
sat_obj.alternative_name = alt_name
sat_obj.save()
tran_obj, created = Transponders.objects.get_or_create( tran_obj, created = Transponders.objects.get_or_create(
name=tran["name"], name=tran["name"],
@@ -124,6 +229,9 @@ def parse_transponders_from_xml(data_in: BytesIO, user=None):
""" """
Парсит транспондеры из XML файла. Парсит транспондеры из XML файла.
Если имя спутника содержит альтернативное имя в скобках, оно извлекается
и сохраняется в поле alternative_name.
Args: Args:
data_in: BytesIO объект с XML данными data_in: BytesIO объект с XML данными
user: пользователь для установки created_by и updated_by (optional) user: пользователь для установки created_by и updated_by (optional)
@@ -136,9 +244,13 @@ def parse_transponders_from_xml(data_in: BytesIO, user=None):
} }
satellites = tree.xpath('//ns:SatelliteMemo', namespaces=ns) satellites = tree.xpath('//ns:SatelliteMemo', namespaces=ns)
for sat in satellites[:]: for sat in satellites[:]:
name = sat.xpath('./ns:name/text()', namespaces=ns)[0] name_full = sat.xpath('./ns:name/text()', namespaces=ns)[0]
if name == 'X' or 'DONT USE' in name: if name_full == 'X' or 'DONT USE' in name_full:
continue continue
# Парсим имя спутника и альтернативное имя
main_name, alt_name = parse_satellite_name(name_full)
norad = sat.xpath('./ns:norad/text()', namespaces=ns) norad = sat.xpath('./ns:norad/text()', namespaces=ns)
beams = sat.xpath('.//ns:BeamMemo', namespaces=ns) beams = sat.xpath('.//ns:BeamMemo', namespaces=ns)
intl_code = sat.xpath('.//ns:internationalCode/text()', namespaces=ns) intl_code = sat.xpath('.//ns:internationalCode/text()', namespaces=ns)
@@ -158,7 +270,6 @@ def parse_transponders_from_xml(data_in: BytesIO, user=None):
uplink_start = float(transponder.xpath('./ns:uplinkFrequency/tr:start/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]) uplink_end = float(transponder.xpath('./ns:uplinkFrequency/tr:end/text()', namespaces=ns)[0])
tr_data = zones[tr_id] tr_data = zones[tr_id]
# p = tr_data['pol'][0] if tr_data['pol'] else '-'
match tr_data['pol']: match tr_data['pol']:
case 'Horizontal': case 'Horizontal':
pol = 'Горизонтальная' pol = 'Горизонтальная'
@@ -173,13 +284,36 @@ def parse_transponders_from_xml(data_in: BytesIO, user=None):
tr_name = transponder.xpath('./ns:name/text()', namespaces=ns)[0] tr_name = transponder.xpath('./ns:name/text()', namespaces=ns)[0]
pol_obj, _ = Polarization.objects.get_or_create(name=pol) pol_obj, _ = Polarization.objects.get_or_create(name=pol)
sat_obj, _ = Satellite.objects.get_or_create(
name=name, # Ищем спутник по имени или альтернативному имени
defaults={ sat_obj = find_satellite_by_name(main_name)
"norad": int(norad[0]) if norad else -1, if not sat_obj:
"international_code": intl_code[0] if intl_code else "", # Если не найден, создаём новый с альтернативным именем
"undersat_point": sub_sat_point[0 if sub_sat_point else ""] sat_obj = Satellite.objects.create(
}) name=main_name,
alternative_name=alt_name,
norad=int(norad[0]) if norad else -1,
international_code=intl_code[0] if intl_code else "",
undersat_point=float(sub_sat_point[0]) if sub_sat_point else None
)
else:
# Если найден, обновляем альтернативное имя если не установлено
updated = False
if alt_name and not sat_obj.alternative_name:
sat_obj.alternative_name = alt_name
updated = True
if norad and not sat_obj.norad:
sat_obj.norad = int(norad[0])
updated = True
if intl_code and not sat_obj.international_code:
sat_obj.international_code = intl_code[0]
updated = True
if sub_sat_point and not sat_obj.undersat_point:
sat_obj.undersat_point = float(sub_sat_point[0])
updated = True
if updated:
sat_obj.save()
trans_obj, created = Transponders.objects.get_or_create( trans_obj, created = Transponders.objects.get_or_create(
polarization=pol_obj, polarization=pol_obj,
downlink=(downlink_start+downlink_end)/2/1000000, downlink=(downlink_start+downlink_end)/2/1000000,