Внёс мелкие правки и фиксы

This commit is contained in:
2025-11-21 10:31:26 +03:00
parent c2c8c8799f
commit 58838614a5
13 changed files with 423 additions and 99 deletions

View File

@@ -26,6 +26,7 @@ from .models import (
SigmaParMark,
ObjectMark,
ObjectInfo,
ObjectOwnership,
SigmaParameter,
Parameter,
Satellite,
@@ -404,6 +405,15 @@ class ObjectInfoAdmin(BaseAdmin):
ordering = ("name",)
@admin.register(ObjectOwnership)
class ObjectOwnershipAdmin(BaseAdmin):
"""Админ-панель для модели ObjectOwnership (Принадлежность объекта)."""
list_display = ("name",)
search_fields = ("name",)
ordering = ("name",)
class SigmaParameterInline(admin.StackedInline):
model = SigmaParameter
extra = 0

View File

@@ -463,15 +463,20 @@ class SourceForm(forms.ModelForm):
class Meta:
model = Source
fields = ['info']
fields = ['info', 'ownership']
widgets = {
'info': forms.Select(attrs={
'class': 'form-select',
'id': 'id_info',
}),
'ownership': forms.Select(attrs={
'class': 'form-select',
'id': 'id_ownership',
}),
}
labels = {
'info': 'Тип объекта',
'ownership': 'Принадлежность объекта',
}
def __init__(self, *args, **kwargs):
@@ -607,9 +612,8 @@ class KubsatFilterForm(forms.Form):
widget=forms.SelectMultiple(attrs={'class': 'form-select', 'size': '3'})
)
# Заглушка для принадлежности объекта
object_ownership = forms.MultipleChoiceField(
choices=[],
object_ownership = forms.ModelMultipleChoiceField(
queryset=None,
label='Принадлежность объекта',
required=False,
widget=forms.SelectMultiple(attrs={'class': 'form-select', 'size': '3'})
@@ -658,7 +662,7 @@ class KubsatFilterForm(forms.Form):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
from mainapp.models import Band, ObjectInfo, Satellite, ObjItem
from mainapp.models import Band, ObjectInfo, ObjectOwnership, Satellite, ObjItem
from django.db.models import Exists, OuterRef
# Фильтруем спутники: только те, у которых есть источники с точками
@@ -669,6 +673,7 @@ class KubsatFilterForm(forms.Form):
self.fields['satellites'].queryset = satellites_with_sources
self.fields['band'].queryset = Band.objects.all().order_by('name')
self.fields['object_type'].queryset = ObjectInfo.objects.all().order_by('name')
self.fields['object_ownership'].queryset = ObjectOwnership.objects.all().order_by('name')
class TransponderForm(forms.ModelForm):

View File

@@ -0,0 +1,36 @@
# Generated by Django 5.2.7 on 2025-11-20 11:45
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0008_objectinfo_source_info'),
]
operations = [
migrations.CreateModel(
name='ObjectOwnership',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Принадлежность объекта (страна, организация и т.д.)', max_length=255, unique=True, verbose_name='Принадлежность')),
],
options={
'verbose_name': 'Принадлежность объекта',
'verbose_name_plural': 'Принадлежности объектов',
'ordering': ['name'],
},
),
migrations.AlterField(
model_name='source',
name='info',
field=models.ForeignKey(blank=True, help_text='Тип объекта', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_info', to='mainapp.objectinfo', verbose_name='Тип объекта'),
),
migrations.AddField(
model_name='source',
name='ownership',
field=models.ForeignKey(blank=True, help_text='Принадлежность объекта (страна, организация и т.д.)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_ownership', to='mainapp.objectownership', verbose_name='Принадлежность объекта'),
),
]

View File

@@ -84,6 +84,28 @@ class ObjectInfo(models.Model):
ordering = ["name"]
class ObjectOwnership(models.Model):
"""
Модель принадлежности объекта.
Определяет к какой организации/стране/группе принадлежит объект.
"""
name = models.CharField(
max_length=255,
unique=True,
verbose_name="Принадлежность",
help_text="Принадлежность объекта (страна, организация и т.д.)",
)
def __str__(self):
return self.name
class Meta:
verbose_name = "Принадлежность объекта"
verbose_name_plural = "Принадлежности объектов"
ordering = ["name"]
class ObjectMark(models.Model):
"""
Модель отметки о наличии объекта.
@@ -457,7 +479,17 @@ class Source(models.Model):
related_name="source_info",
null=True,
blank=True,
verbose_name="Тип объекта"
verbose_name="Тип объекта",
help_text="Тип объекта",
)
ownership = models.ForeignKey(
'ObjectOwnership',
on_delete=models.SET_NULL,
related_name="source_ownership",
null=True,
blank=True,
verbose_name="Принадлежность объекта",
help_text="Принадлежность объекта (страна, организация и т.д.)",
)
coords_average = gis.PointField(

View File

@@ -23,10 +23,10 @@
<a class="nav-link" href="{% url 'mainapp:objitem_list' %}">Точки</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'mainapp:transponder_list' %}">Транспондеры</a>
<a class="nav-link" href="{% url 'mainapp:satellite_list' %}">Спутники</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'mainapp:satellite_list' %}">Спутники</a>
<a class="nav-link" href="{% url 'mainapp:transponder_list' %}">Транспондеры</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'lyngsatapp:lyngsat_list' %}">Справочные данные</a>

View File

@@ -107,10 +107,17 @@
<small class="form-text text-muted">Удерживайте Ctrl для выбора нескольких</small>
</div>
<!-- Принадлежность объекта (заглушка) -->
<!-- Принадлежность объекта -->
<div class="col-md-3 mb-3">
<label for="{{ form.object_ownership.id_for_label }}" class="form-label">{{ form.object_ownership.label }} (не работает)</label>
<label for="{{ form.object_ownership.id_for_label }}" class="form-label">{{ form.object_ownership.label }}</label>
<div class="d-flex justify-content-between mb-1">
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('object_ownership', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('object_ownership', false)">Снять</button>
</div>
{{ form.object_ownership }}
<small class="form-text text-muted">Удерживайте Ctrl для выбора нескольких</small>
</div>
</div>
@@ -231,6 +238,7 @@
<tr>
<th style="min-width: 80px;">ID объекта</th>
<th style="min-width: 120px;">Тип объекта</th>
<th style="min-width: 150px;">Принадлежность объекта</th>
<th class="text-center" style="min-width: 100px;">Кол-во точек</th>
<th style="min-width: 120px;">Имя точки</th>
<th style="min-width: 150px;">Спутник</th>
@@ -261,6 +269,24 @@
<td rowspan="{{ source_data.objitems_data|length }}" class="source-type-cell">{{ source_data.source.info.name|default:"-" }}</td>
{% endif %}
<!-- Принадлежность объекта (только для первой строки источника) -->
{% if forloop.first %}
<td rowspan="{{ source_data.objitems_data|length }}" class="source-ownership-cell">
{% if source_data.source.ownership %}
{% if source_data.source.ownership.name == "ТВ" and source_data.has_lyngsat %}
<a href="#" class="text-primary text-decoration-none"
onclick="showLyngsatModal({{ source_data.lyngsat_id }}); return false;">
<i class="bi bi-tv"></i> {{ source_data.source.ownership.name }}
</a>
{% else %}
{{ source_data.source.ownership.name }}
{% endif %}
{% else %}
-
{% endif %}
</td>
{% endif %}
<!-- Количество точек (только для первой строки источника) -->
{% if forloop.first %}
<td rowspan="{{ source_data.objitems_data|length }}" class="text-center source-count-cell" data-initial-count="{{ source_data.objitems_data|length }}">{{ source_data.objitems_data|length }}</td>
@@ -539,6 +565,55 @@ function selectAllOptions(selectName, selectAll) {
document.addEventListener('DOMContentLoaded', function() {
updateCounter();
});
// Функция для показа модального окна LyngSat
function showLyngsatModal(lyngsatId) {
const modal = new bootstrap.Modal(document.getElementById('lyngsatModal'));
modal.show();
const modalBody = document.getElementById('lyngsatModalBody');
modalBody.innerHTML = '<div class="text-center py-4"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Загрузка...</span></div></div>';
fetch('/api/lyngsat/' + lyngsatId + '/')
.then(response => {
if (!response.ok) {
throw new Error('Ошибка загрузки данных');
}
return response.json();
})
.then(data => {
let html = '<div class="container-fluid"><div class="row g-3">' +
'<div class="col-md-6"><div class="card h-100">' +
'<div class="card-header bg-light"><strong><i class="bi bi-info-circle"></i> Основная информация</strong></div>' +
'<div class="card-body"><table class="table table-sm table-borderless mb-0"><tbody>' +
'<tr><td class="text-muted" style="width: 40%;">Спутник:</td><td><strong>' + data.satellite + '</strong></td></tr>' +
'<tr><td class="text-muted">Частота:</td><td><strong>' + data.frequency + ' МГц</strong></td></tr>' +
'<tr><td class="text-muted">Поляризация:</td><td><span class="badge bg-info">' + data.polarization + '</span></td></tr>' +
'<tr><td class="text-muted">Канал:</td><td>' + data.channel_info + '</td></tr>' +
'</tbody></table></div></div></div>' +
'<div class="col-md-6"><div class="card h-100">' +
'<div class="card-header bg-light"><strong><i class="bi bi-gear"></i> Технические параметры</strong></div>' +
'<div class="card-body"><table class="table table-sm table-borderless mb-0"><tbody>' +
'<tr><td class="text-muted" style="width: 40%;">Модуляция:</td><td><span class="badge bg-secondary">' + data.modulation + '</span></td></tr>' +
'<tr><td class="text-muted">Стандарт:</td><td><span class="badge bg-secondary">' + data.standard + '</span></td></tr>' +
'<tr><td class="text-muted">Сим. скорость:</td><td><strong>' + data.sym_velocity + ' БОД</strong></td></tr>' +
'<tr><td class="text-muted">FEC:</td><td>' + data.fec + '</td></tr>' +
'</tbody></table></div></div></div>' +
'<div class="col-12"><div class="card">' +
'<div class="card-header bg-light"><strong><i class="bi bi-clock-history"></i> Дополнительная информация</strong></div>' +
'<div class="card-body"><div class="row">' +
'<div class="col-md-6"><p class="mb-2"><span class="text-muted">Последнее обновление:</span><br><strong>' + data.last_update + '</strong></p></div>' +
'<div class="col-md-6">' + (data.url ? '<p class="mb-2"><span class="text-muted">Ссылка на объект:</span><br>' +
'<a href="' + data.url + '" target="_blank" class="btn btn-sm btn-outline-primary">' +
'<i class="bi bi-link-45deg"></i> Открыть на LyngSat</a></p>' : '') +
'</div></div></div></div></div></div></div>';
modalBody.innerHTML = html;
})
.catch(error => {
modalBody.innerHTML = '<div class="alert alert-danger" role="alert">' +
'<i class="bi bi-exclamation-triangle"></i> ' + error.message + '</div>';
});
}
</script>
<style>
@@ -569,4 +644,29 @@ document.addEventListener('DOMContentLoaded', function() {
z-index: 10;
}
</style>
<!-- LyngSat Data Modal -->
<div class="modal fade" id="lyngsatModal" tabindex="-1" aria-labelledby="lyngsatModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="lyngsatModalLabel">
<i class="bi bi-tv"></i> Данные объекта LyngSat
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
aria-label="Закрыть"></button>
</div>
<div class="modal-body" id="lyngsatModalBody">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -200,10 +200,10 @@
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-save"></i> Сохранить
Сохранить
</button>
<a href="{% url 'mainapp:satellite_list' %}" class="btn btn-secondary">
<i class="bi bi-x-circle"></i> Отмена
Отмена
</a>
</div>
</form>
@@ -214,7 +214,7 @@
{% if action == 'update' and transponders %}
<!-- Frequency Plan Visualization -->
<div class="row mt-4">
<!-- <div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-body">
@@ -256,7 +256,7 @@
</div>
</div>
</div>
</div>
</div> -->
<div class="transponder-tooltip" id="transponderTooltip"></div>
{% endif %}

View File

@@ -206,6 +206,17 @@
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="id_ownership" class="form-label">{{ form.ownership.label }}:</label>
{{ form.ownership }}
{% if form.ownership.errors %}
<div class="invalid-feedback d-block">
{{ form.ownership.errors }}
</div>
{% endif %}
</div>
</div>
</div>
</div>
@@ -227,7 +238,7 @@
<!-- Координаты ГЛ -->
<div class="coord-group">
<div class="coord-group-header">Координаты ГЛ (усреднённые)</div>
<div class="coord-group-header">Координаты ГЛ</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
@@ -581,9 +592,8 @@
document.getElementById('save-btn').disabled = false;
document.getElementById('cancel-btn').disabled = false;
// Включаем drag для всех маркеров
Object.values(markers).forEach(m => {
if (m.marker.options.opacity !== 0) {
Object.entries(markers).forEach(([key, m]) => {
if (key !== 'average' && m.marker.options.opacity !== 0) {
m.marker.enableEditing();
}
});

View File

@@ -227,23 +227,6 @@
</div>
</div>
<!-- LyngSat 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_lyngsat" id="has_lyngsat_1"
value="1" {% if has_lyngsat == '1' %}checked{% endif %}>
<label class="form-check-label" for="has_lyngsat_1">Есть</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_lyngsat" id="has_lyngsat_0"
value="0" {% if has_lyngsat == '0' %}checked{% endif %}>
<label class="form-check-label" for="has_lyngsat_0">Нет</label>
</div>
</div>
</div>
<!-- ObjectInfo Filter -->
<div class="mb-2">
<label class="form-label">Тип объекта:</label>
@@ -262,6 +245,24 @@
</select>
</div>
<!-- ObjectOwnership 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('ownership_id', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('ownership_id', false)">Снять</button>
</div>
<select name="ownership_id" class="form-select form-select-sm mb-2" multiple size="4">
{% for ownership in object_ownerships %}
<option value="{{ ownership.id }}" {% if ownership.id in selected_ownership %}selected{% endif %}>
{{ ownership.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Point Count Filter -->
<div class="mb-2">
<label class="form-label">Количество точек:</label>
@@ -435,6 +436,7 @@
<th scope="col" style="min-width: 150px;">Имя</th>
<th scope="col" style="min-width: 120px;">Спутник</th>
<th scope="col" style="min-width: 120px;">Тип объекта</th>
<th scope="col" style="min-width: 150px;">Принадлежность объекта</th>
<th scope="col" style="min-width: 150px;">Координаты ГЛ</th>
<th scope="col" class="text-center" style="min-width: 100px;">
{% include 'mainapp/components/_sort_header.html' with field='objitem_count' label='Кол-во ГЛ(точек)' current_sort=sort %}
@@ -443,7 +445,6 @@
<th scope="col" style="min-width: 150px;">Координаты визуального наблюдения</th>
<th scope="col" style="min-width: 150px;">Координаты справочные</th>
<th scope="col" style="min-width: 180px;">Наличие сигнала</th>
<th scope="col" class="text-center" style="min-width: 80px;">ТВ или нет</th>
<th scope="col" style="min-width: 120px;">
{% include 'mainapp/components/_sort_header.html' with field='created_at' label='Создано' current_sort=sort %}
</th>
@@ -473,6 +474,16 @@
{% endif %}
</td>
<td>{{ source.info }}</td>
<td>
{% if source.ownership == "ТВ" and source.has_lyngsat %}
<a href="#" class="text-primary text-decoration-none"
onclick="showLyngsatModal({{ source.lyngsat_id }}); return false;">
<i class="bi bi-tv"></i> {{ source.ownership }}
</a>
{% else %}
{{ source.ownership }}
{% endif %}
</td>
<td>{{ source.coords_average }}</td>
<td class="text-center">{{ source.objitem_count }}</td>
<td>{{ source.coords_kupsat }}</td>
@@ -501,16 +512,6 @@
<span class="text-muted">-</span>
{% endif %}
</td>
<td class="text-center">
{% if source.has_lyngsat %}
<a href="#" class="text-primary text-decoration-none"
onclick="showLyngsatModal({{ source.lyngsat_id }}); return false;">
<i class="bi bi-tv"></i> ТВ
</a>
{% else %}
-
{% endif %}
</td>
<td>{{ source.created_at|date:"d.m.Y H:i" }}</td>
<td>{{ source.updated_at|date:"d.m.Y H:i" }}</td>
@@ -1091,7 +1092,6 @@ document.addEventListener('DOMContentLoaded', function() {
setupRadioLikeCheckboxes('has_coords_kupsat');
setupRadioLikeCheckboxes('has_coords_valid');
setupRadioLikeCheckboxes('has_coords_reference');
setupRadioLikeCheckboxes('has_lyngsat');
setupRadioLikeCheckboxes('has_signal_mark');
// Update filter counter on page load

View File

@@ -570,3 +570,55 @@ class SatelliteDataAPIView(LoginRequiredMixin, View):
return JsonResponse({'error': 'Спутник не найден'}, status=404)
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
class SatelliteDataAPIView(LoginRequiredMixin, View):
"""API endpoint for getting Satellite data."""
def get(self, request, satellite_id):
from ..models import Satellite
try:
satellite = Satellite.objects.prefetch_related('band').get(id=satellite_id)
# Format launch_date
launch_date_str = '-'
if satellite.launch_date:
launch_date_str = satellite.launch_date.strftime("%d.%m.%Y")
# Format created_at and updated_at
created_at_str = '-'
if satellite.created_at:
local_time = timezone.localtime(satellite.created_at)
created_at_str = local_time.strftime("%d.%m.%Y %H:%M")
updated_at_str = '-'
if satellite.updated_at:
local_time = timezone.localtime(satellite.updated_at)
updated_at_str = local_time.strftime("%d.%m.%Y %H:%M")
# Get band names
bands = list(satellite.band.values_list('name', flat=True))
bands_str = ', '.join(bands) if bands else '-'
data = {
'id': satellite.id,
'name': satellite.name,
'norad': satellite.norad if satellite.norad else None,
'bands': bands_str,
'undersat_point': satellite.undersat_point if satellite.undersat_point is not None else None,
'url': satellite.url or None,
'comment': satellite.comment or '-',
'launch_date': launch_date_str,
'created_at': created_at_str,
'updated_at': updated_at_str,
'created_by': str(satellite.created_by) if satellite.created_by else '-',
'updated_by': str(satellite.updated_by) if satellite.updated_by else '-',
}
return JsonResponse(data)
except Satellite.DoesNotExist:
return JsonResponse({'error': 'Спутник не найден'}, status=404)
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)

View File

@@ -40,10 +40,18 @@ class KubsatView(LoginRequiredMixin, FormView):
for source in sources:
source_data = {
'source': source,
'objitems_data': []
'objitems_data': [],
'has_lyngsat': False,
'lyngsat_id': None
}
for objitem in source.source_objitems.all():
# Check if objitem has LyngSat source
if hasattr(objitem, 'lyngsat_source') and objitem.lyngsat_source:
source_data['has_lyngsat'] = True
source_data['lyngsat_id'] = objitem.lyngsat_source.id
objitem_matches_date = True
objitem_matches_date = True
geo_date = None
@@ -91,11 +99,12 @@ class KubsatView(LoginRequiredMixin, FormView):
def apply_filters(self, filters):
"""Применяет фильтры к queryset Source"""
queryset = Source.objects.select_related('info').prefetch_related(
queryset = Source.objects.select_related('info', 'ownership').prefetch_related(
'source_objitems__parameter_obj__id_satellite',
'source_objitems__parameter_obj__polarization',
'source_objitems__parameter_obj__modulation',
'source_objitems__transponder__sat_id'
'source_objitems__transponder__sat_id',
'source_objitems__lyngsat_source'
).annotate(objitem_count=Count('source_objitems'))
# Фильтр по спутникам
@@ -146,6 +155,10 @@ class KubsatView(LoginRequiredMixin, FormView):
if filters.get('object_type'):
queryset = queryset.filter(info__in=filters['object_type'])
# Фильтр по принадлежности объекта
if filters.get('object_ownership'):
queryset = queryset.filter(ownership__in=filters['object_ownership'])
# Фильтр по количеству ObjItem
objitem_count = filters.get('objitem_count')
if objitem_count == '1':
@@ -191,13 +204,37 @@ class KubsatExportView(LoginRequiredMixin, FormView):
}
sources_objitems[objitem.source.id]['objitems'].append(objitem)
# Создаем Excel файл
# Создаем Excel файл с двумя листами
wb = Workbook()
ws = wb.active
ws.title = "Кубсат"
# Заголовки
headers = [
# Первый лист: "Предложения" (только основные данные)
ws_proposals = wb.active
ws_proposals.title = "Предложения"
# Заголовки для листа "Предложения"
headers_proposals = [
'Дата',
'Широта, град',
'Долгота, град',
'Высота, м',
'Местоположение',
'ИСЗ',
'Прямой канал, МГц',
'Обратный канал, МГц',
'Перенос'
]
# Стиль заголовков для листа "Предложения"
for col_num, header in enumerate(headers_proposals, 1):
cell = ws_proposals.cell(row=1, column=col_num, value=header)
cell.font = Font(bold=True)
cell.alignment = Alignment(horizontal='center', vertical='center')
# Второй лист: "Комментарий" (все данные)
ws_comments = wb.create_sheet(title="Комментарий")
# Заголовки для листа "Комментарий"
headers_comments = [
'Дата',
'Широта, град',
'Долгота, град',
@@ -215,9 +252,9 @@ class KubsatExportView(LoginRequiredMixin, FormView):
'Оператор'
]
# Стиль заголовков
for col_num, header in enumerate(headers, 1):
cell = ws.cell(row=1, column=col_num, value=header)
# Стиль заголовков для листа "Комментарий"
for col_num, header in enumerate(headers_comments, 1):
cell = ws_comments.cell(row=1, column=col_num, value=header)
cell.font = Font(bold=True)
cell.alignment = Alignment(horizontal='center', vertical='center')
@@ -225,7 +262,8 @@ class KubsatExportView(LoginRequiredMixin, FormView):
current_date = datetime.now().strftime('%d.%m.%Y')
operator_name = f"{request.user.first_name} {request.user.last_name}" if request.user.first_name else request.user.username
row_num = 2
row_num_proposals = 2
row_num_comments = 2
for source_id, data in sources_objitems.items():
source = data['source']
objitems_list = data['objitems']
@@ -315,26 +353,39 @@ class KubsatExportView(LoginRequiredMixin, FormView):
# Иначе показываем диапазон
date_range_str = f"{min_date_str}-{max_date_str}"
# Записываем строку
ws.cell(row=row_num, column=1, value=current_date)
ws.cell(row=row_num, column=2, value=latitude)
ws.cell(row=row_num, column=3, value=longitude)
ws.cell(row=row_num, column=4, value=0.0)
ws.cell(row=row_num, column=5, value=location)
ws.cell(row=row_num, column=6, value=satellite_info)
ws.cell(row=row_num, column=7, value=direct_channel)
ws.cell(row=row_num, column=8, value=reverse_channel)
ws.cell(row=row_num, column=9, value=transfer)
ws.cell(row=row_num, column=10, value=objitem_count)
ws.cell(row=row_num, column=11, value=date_range_str)
ws.cell(row=row_num, column=12, value=mirrors_str)
ws.cell(row=row_num, column=13, value='')
ws.cell(row=row_num, column=14, value='')
ws.cell(row=row_num, column=15, value=operator_name)
# Записываем строку на лист "Предложения" (только основные данные)
ws_proposals.cell(row=row_num_proposals, column=1, value=current_date)
ws_proposals.cell(row=row_num_proposals, column=2, value=latitude)
ws_proposals.cell(row=row_num_proposals, column=3, value=longitude)
ws_proposals.cell(row=row_num_proposals, column=4, value=0.0)
ws_proposals.cell(row=row_num_proposals, column=5, value=location)
ws_proposals.cell(row=row_num_proposals, column=6, value=satellite_info)
ws_proposals.cell(row=row_num_proposals, column=7, value=direct_channel)
ws_proposals.cell(row=row_num_proposals, column=8, value=reverse_channel)
ws_proposals.cell(row=row_num_proposals, column=9, value=transfer)
row_num += 1
# Записываем строку на лист "Комментарий" (все данные)
ws_comments.cell(row=row_num_comments, column=1, value=current_date)
ws_comments.cell(row=row_num_comments, column=2, value=latitude)
ws_comments.cell(row=row_num_comments, column=3, value=longitude)
ws_comments.cell(row=row_num_comments, column=4, value=0.0)
ws_comments.cell(row=row_num_comments, column=5, value=location)
ws_comments.cell(row=row_num_comments, column=6, value=satellite_info)
ws_comments.cell(row=row_num_comments, column=7, value=direct_channel)
ws_comments.cell(row=row_num_comments, column=8, value=reverse_channel)
ws_comments.cell(row=row_num_comments, column=9, value=transfer)
ws_comments.cell(row=row_num_comments, column=10, value=objitem_count)
ws_comments.cell(row=row_num_comments, column=11, value=date_range_str)
ws_comments.cell(row=row_num_comments, column=12, value=mirrors_str)
ws_comments.cell(row=row_num_comments, column=13, value='')
ws_comments.cell(row=row_num_comments, column=14, value='')
ws_comments.cell(row=row_num_comments, column=15, value=operator_name)
# Автоширина колонок
row_num_proposals += 1
row_num_comments += 1
# Автоширина колонок для обоих листов
for ws in [ws_proposals, ws_comments]:
for column in ws.columns:
max_length = 0
column_letter = column[0].column_letter

View File

@@ -71,6 +71,25 @@ class LinkLyngsatSourcesView(LoginRequiredMixin, FormMessageMixin, FormView):
matching_sources.sort(key=lambda x: abs(round(x.frequency, 1) - rounded_freq))
objitem.lyngsat_source = matching_sources[0]
objitem.save(update_fields=['lyngsat_source'])
# Update Source with ObjectInfo and ObjectOwnership for TV
if objitem.source:
from ..models import ObjectInfo, ObjectOwnership
try:
tv_type = ObjectInfo.objects.get(name="Стационарные")
tv_ownership = ObjectOwnership.objects.get(name="ТВ")
# Update source if not already set
if not objitem.source.info:
objitem.source.info = tv_type
if not objitem.source.ownership:
objitem.source.ownership = tv_ownership
objitem.source.save(update_fields=['info', 'ownership'])
except (ObjectInfo.DoesNotExist, ObjectOwnership.DoesNotExist):
# If types don't exist, skip this step
pass
linked_count += 1
messages.success(

View File

@@ -37,8 +37,8 @@ class SourceListView(LoginRequiredMixin, View):
has_coords_kupsat = request.GET.get("has_coords_kupsat")
has_coords_valid = request.GET.get("has_coords_valid")
has_coords_reference = request.GET.get("has_coords_reference")
has_lyngsat = request.GET.get("has_lyngsat")
selected_info = request.GET.getlist("info_id")
selected_ownership = request.GET.getlist("ownership_id")
objitem_count_min = request.GET.get("objitem_count_min", "").strip()
objitem_count_max = request.GET.get("objitem_count_max", "").strip()
date_from = request.GET.get("date_from", "").strip()
@@ -97,6 +97,10 @@ class SourceListView(LoginRequiredMixin, View):
# Get all ObjectInfo for filter
object_infos = ObjectInfo.objects.all().order_by("name")
# Get all ObjectOwnership for filter
from ..models import ObjectOwnership
object_ownerships = ObjectOwnership.objects.all().order_by("name")
# Get all satellites that are used as mirrors
mirrors = (
Satellite.objects.filter(geo_mirrors__isnull=False)
@@ -380,18 +384,14 @@ class SourceListView(LoginRequiredMixin, View):
elif has_coords_reference == "0":
sources = sources.filter(coords_reference__isnull=True)
# Filter by LyngSat presence
if has_lyngsat == "1":
sources = sources.filter(source_objitems__lyngsat_source__isnull=False).distinct()
elif has_lyngsat == "0":
sources = sources.filter(
~Q(source_objitems__lyngsat_source__isnull=False)
).distinct()
# Filter by ObjectInfo (info field)
if selected_info:
sources = sources.filter(info_id__in=selected_info)
# Filter by ObjectOwnership (ownership field)
if selected_ownership:
sources = sources.filter(ownership_id__in=selected_ownership)
# Filter by signal marks
if has_signal_mark or mark_date_from or mark_date_to:
mark_filter_q = Q()
@@ -648,13 +648,15 @@ class SourceListView(LoginRequiredMixin, View):
'created_by': str(mark.created_by) if mark.created_by else '-',
})
# Get info name
# Get info name and ownership
info_name = source.info.name if source.info else '-'
ownership_name = source.ownership.name if source.ownership else '-'
processed_sources.append({
'id': source.id,
'name': source_name if source_name else '-',
'info': info_name,
'ownership': ownership_name,
'coords_average': coords_average_str,
'coords_kupsat': coords_kupsat_str,
'coords_valid': coords_valid_str,
@@ -682,10 +684,12 @@ class SourceListView(LoginRequiredMixin, View):
'has_coords_kupsat': has_coords_kupsat,
'has_coords_valid': has_coords_valid,
'has_coords_reference': has_coords_reference,
'has_lyngsat': has_lyngsat,
'selected_info': [
int(x) if isinstance(x, str) else x for x in selected_info if (isinstance(x, int) or (isinstance(x, str) and x.isdigit()))
],
'selected_ownership': [
int(x) if isinstance(x, str) else x for x in selected_ownership if (isinstance(x, int) or (isinstance(x, str) and x.isdigit()))
],
'objitem_count_min': objitem_count_min,
'objitem_count_max': objitem_count_max,
'date_from': date_from,
@@ -696,6 +700,8 @@ class SourceListView(LoginRequiredMixin, View):
# ObjItem-level filters
'geo_date_from': geo_date_from,
'geo_date_to': geo_date_to,
'object_infos': object_infos,
'object_ownerships': object_ownerships,
'satellites': satellites,
'selected_satellites': [
int(x) if isinstance(x, str) else x for x in selected_satellites if (isinstance(x, int) or (isinstance(x, str) and x.isdigit()))
@@ -750,6 +756,9 @@ class SourceUpdateView(LoginRequiredMixin, AdminModeratorMixin, View):
def get(self, request, pk):
source = get_object_or_404(Source, pk=pk)
form = SourceForm(instance=source)
form.fields['average_latitude'].disabled = True
form.fields['average_longitude'].disabled = True
# Get related ObjItems ordered by creation date
objitems = source.source_objitems.select_related(