Доделал страницу с Кубсатами

This commit is contained in:
2025-11-20 10:50:27 +03:00
parent 66e1929978
commit 1d1c42a8e7
6 changed files with 171 additions and 139 deletions

View File

@@ -54,8 +54,23 @@
- Количество точек автоматически пересчитывается при удалении строк - Количество точек автоматически пересчитывается при удалении строк
- Таблица имеет фиксированную высоту с прокруткой и sticky заголовок - Таблица имеет фиксированную высоту с прокруткой и sticky заголовок
### Выделение по дате ### Двухэтапная фильтрация
Строки, у которых дата ГЛ попадает в выбранный диапазон дат, **выделяются зеленым цветом**. При этом все объекты остаются в таблице, независимо от фильтра по дате.
Фильтрация происходит в два этапа:
**Этап 1: Фильтрация по дате ГЛ**
- Если задан диапазон дат (от/до), отображаются только те точки, у которых дата ГЛ попадает в этот диапазон
- Точки без даты ГЛ исключаются, если фильтр по дате задан
- Если фильтр не задан, показываются все точки
**Этап 2: Фильтрация по количеству точек**
- Применяется к уже отфильтрованным по дате точкам
- Варианты:
- "Все" - показываются источники с любым количеством точек
- "1" - только источники с ровно 1 точкой (после фильтрации по дате)
- "2 и более" - только источники с 2 и более точками (после фильтрации по дате)
**Важно**: Фильтр по количеству точек учитывает только те точки, которые прошли фильтрацию по дате!
### Управление данными в таблице ### Управление данными в таблице
@@ -63,10 +78,7 @@
- **Кнопка с иконкой корзины** (для каждой точки) - удаляет конкретную точку (ObjItem) из таблицы - **Кнопка с иконкой корзины** (для каждой точки) - удаляет конкретную точку (ObjItem) из таблицы
- **Кнопка с иконкой заполненной корзины** (для первой точки источника) - удаляет все точки источника (Source) из таблицы - **Кнопка с иконкой заполненной корзины** (для первой точки источника) - удаляет все точки источника (Source) из таблицы
**Кнопка "Оставить только подходящие по дате":**
- Удаляет из таблицы все точки, которые НЕ подходят по заданному диапазону дат
- Оставляет только зеленые (выделенные) строки
- Запрашивает подтверждение перед удалением
**Важно**: Все удаления происходят только из таблицы, **БЕЗ удаления из базы данных**. Это позволяет пользователю исключить ненужные записи перед экспортом. **Важно**: Все удаления происходят только из таблицы, **БЕЗ удаления из базы данных**. Это позволяет пользователю исключить ненужные записи перед экспортом.
@@ -83,7 +95,7 @@
8. **Обратный канал, МГц** - частота источника 8. **Обратный канал, МГц** - частота источника
9. **Перенос** - из объекта Transponder 9. **Перенос** - из объекта Transponder
10. **Получено координат, раз** - количество точек (ObjItem), оставшихся в таблице для данного источника 10. **Получено координат, раз** - количество точек (ObjItem), оставшихся в таблице для данного источника
11. **Дата** - пока заполняется как "-" 11. **Период получения координат** - диапазон дат ГЛ в формате "5.11.2025-15.11.2025" (от самой ранней до самой поздней даты среди точек источника). Если все точки имеют одну дату, показывается только одна дата.
12. **Зеркала** - все имена зеркал через перенос строки (из оставшихся точек) 12. **Зеркала** - все имена зеркал через перенос строки (из оставшихся точек)
13. **СКО, км** - не заполняется 13. **СКО, км** - не заполняется
14. **Примечание** - не заполняется 14. **Примечание** - не заполняется
@@ -123,14 +135,13 @@ queryset = Source.objects.select_related('info').prefetch_related(
1. Откройте страницу "Кубсат" из навигационного меню 1. Откройте страницу "Кубсат" из навигационного меню
2. Выберите нужные фильтры (спутники, поляризация, частота и т.д.) 2. Выберите нужные фильтры (спутники, поляризация, частота и т.д.)
3. Опционально укажите диапазон дат для выделения подходящих точек 3. Опционально укажите диапазон дат для фильтрации точек по дате ГЛ (Этап 1)
4. Нажмите "Применить фильтры" 4. Опционально выберите количество точек (1 или 2+) - применяется к отфильтрованным по дате точкам (Этап 2)
5. В таблице отобразятся все точки (ObjItem) сгруппированные по источникам (Source) 5. Нажмите "Применить фильтры"
6. Точки, подходящие по дате, будут выделены зеленым цветом 6. В таблице отобразятся точки (ObjItem) сгруппированные по источникам (Source)
7. Опционально нажмите "Оставить только подходящие по дате" для быстрого удаления неподходящих точек 7. При необходимости удалите отдельные точки или целые объекты кнопками в колонке "Действия"
8. При необходимости удалите отдельные точки или целые объекты кнопками в колонке "Действия" 8. Нажмите "Экспорт в Excel" для скачивания файла с оставшимися данными
9. Нажмите "Экспорт в Excel" для скачивания файла с оставшимися данными 9. Форма не сбрасывается после экспорта - можно продолжить работу
10. Форма не сбрасывается после экспорта - можно продолжить работу
## Примечания ## Примечания
@@ -138,5 +149,6 @@ queryset = Source.objects.select_related('info').prefetch_related(
- Удаление точек/объектов из таблицы не влияет на базу данных - Удаление точек/объектов из таблицы не влияет на базу данных
- Экспортируются только оставшиеся в таблице точки - Экспортируются только оставшиеся в таблице точки
- Координаты в Excel рассчитываются как инкрементальное среднее из оставшихся точек - Координаты в Excel рассчитываются как инкрементальное среднее из оставшихся точек
- Фильтр по дате не скрывает объекты, а только выделяет их цветом - Фильтр по дате скрывает неподходящие точки (не показывает их в таблице)
- Каждая строка таблицы = одна точка (ObjItem), строки группируются по источникам (Source) - Каждая строка таблицы = одна точка (ObjItem), строки группируются по источникам (Source)
- Количество точек в колонке "Кол-во точек" автоматически пересчитывается при удалении строк

View File

@@ -463,7 +463,7 @@ class SourceForm(forms.ModelForm):
class Meta: class Meta:
model = Source model = Source
fields = ['info'] # Добавляем поле info fields = ['info']
widgets = { widgets = {
'info': forms.Select(attrs={ 'info': forms.Select(attrs={
'class': 'form-select', 'class': 'form-select',
@@ -555,18 +555,18 @@ class KubsatFilterForm(forms.Form):
widget=forms.SelectMultiple(attrs={'class': 'form-select', 'size': '5'}) widget=forms.SelectMultiple(attrs={'class': 'form-select', 'size': '5'})
) )
band = forms.ModelChoiceField( band = forms.ModelMultipleChoiceField(
queryset=None, queryset=None,
label='Полоса спутника', label='Диапазоны работы спутника',
required=False, required=False,
widget=forms.Select(attrs={'class': 'form-select'}) widget=forms.SelectMultiple(attrs={'class': 'form-select', 'size': '4'})
) )
polarization = forms.ModelMultipleChoiceField( polarization = forms.ModelMultipleChoiceField(
queryset=Polarization.objects.all().order_by('name'), queryset=Polarization.objects.all().order_by('name'),
label='Поляризация', label='Поляризация',
required=False, required=False,
widget=forms.SelectMultiple(attrs={'class': 'form-select', 'size': '3'}) widget=forms.SelectMultiple(attrs={'class': 'form-select', 'size': '4'})
) )
frequency_min = forms.FloatField( frequency_min = forms.FloatField(
@@ -597,7 +597,7 @@ class KubsatFilterForm(forms.Form):
queryset=Modulation.objects.all().order_by('name'), queryset=Modulation.objects.all().order_by('name'),
label='Модуляция', label='Модуляция',
required=False, required=False,
widget=forms.SelectMultiple(attrs={'class': 'form-select', 'size': '3'}) widget=forms.SelectMultiple(attrs={'class': 'form-select', 'size': '4'})
) )
object_type = forms.ModelMultipleChoiceField( object_type = forms.ModelMultipleChoiceField(
@@ -624,22 +624,22 @@ class KubsatFilterForm(forms.Form):
# Фиктивные фильтры # Фиктивные фильтры
has_plans = forms.ChoiceField( has_plans = forms.ChoiceField(
choices=[('', 'Все'), ('yes', 'Да'), ('no', 'Нет')], choices=[('', 'Неважно'), ('yes', 'Да'), ('no', 'Нет')],
label='Планы на', label='Планы на Кубсат',
required=False, required=False,
widget=forms.RadioSelect() widget=forms.RadioSelect()
) )
success_1 = forms.ChoiceField( success_1 = forms.ChoiceField(
choices=[('', 'Все'), ('yes', 'Да'), ('no', 'Нет')], choices=[('', 'Неважно'), ('yes', 'Да'), ('no', 'Нет')],
label='Успех 1', label='ГСО успешно?',
required=False, required=False,
widget=forms.RadioSelect() widget=forms.RadioSelect()
) )
success_2 = forms.ChoiceField( success_2 = forms.ChoiceField(
choices=[('', 'Все'), ('yes', 'Да'), ('no', 'Нет')], choices=[('', 'Неважно'), ('yes', 'Да'), ('no', 'Нет')],
label='Успех 2', label='Кубсат успешно?',
required=False, required=False,
widget=forms.RadioSelect() widget=forms.RadioSelect()
) )

View File

@@ -37,11 +37,11 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'mainapp:kubsat' %}">Кубсат</a> <a class="nav-link" href="{% url 'mainapp:kubsat' %}">Кубсат</a>
</li> </li>
<li class="nav-item"> <!-- <li class="nav-item">
<a class="nav-link" href="{% url 'mapsapp:3dmap' %}">3D карта</a> <a class="nav-link" href="{% url 'mapsapp:3dmap' %}">3D карта</a>
</li> </li> -->
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'mapsapp:2dmap' %}">2D карта</a> <a class="nav-link" href="{% url 'mapsapp:2dmap' %}">Карта</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'admin:index' %}">Админ панель</a> <a class="nav-link" href="{% url 'admin:index' %}">Админ панель</a>

View File

@@ -23,6 +23,12 @@
<!-- Спутники --> <!-- Спутники -->
<div class="col-md-3 mb-3"> <div class="col-md-3 mb-3">
<label for="{{ form.satellites.id_for_label }}" class="form-label">{{ form.satellites.label }}</label> <label for="{{ form.satellites.id_for_label }}" class="form-label">{{ form.satellites.label }}</label>
<div class="d-flex justify-content-between mb-1">
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('satellites', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('satellites', false)">Снять</button>
</div>
{{ form.satellites }} {{ form.satellites }}
<small class="form-text text-muted">Удерживайте Ctrl для выбора нескольких</small> <small class="form-text text-muted">Удерживайте Ctrl для выбора нескольких</small>
</div> </div>
@@ -30,19 +36,40 @@
<!-- Полоса спутника --> <!-- Полоса спутника -->
<div class="col-md-3 mb-3"> <div class="col-md-3 mb-3">
<label for="{{ form.band.id_for_label }}" class="form-label">{{ form.band.label }}</label> <label for="{{ form.band.id_for_label }}" class="form-label">{{ form.band.label }}</label>
<div class="d-flex justify-content-between mb-1">
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('band', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('band', false)">Снять</button>
</div>
{{ form.band }} {{ form.band }}
<small class="form-text text-muted">Удерживайте Ctrl для выбора нескольких</small>
</div> </div>
<!-- Поляризация --> <!-- Поляризация -->
<div class="col-md-3 mb-3"> <div class="col-md-3 mb-3">
<label for="{{ form.polarization.id_for_label }}" class="form-label">{{ form.polarization.label }}</label> <label for="{{ form.polarization.id_for_label }}" class="form-label">{{ form.polarization.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>
{{ form.polarization }} {{ form.polarization }}
<small class="form-text text-muted">Удерживайте Ctrl для выбора нескольких</small>
</div> </div>
<!-- Модуляция --> <!-- Модуляция -->
<div class="col-md-3 mb-3"> <div class="col-md-3 mb-3">
<label for="{{ form.modulation.id_for_label }}" class="form-label">{{ form.modulation.label }}</label> <label for="{{ form.modulation.id_for_label }}" class="form-label">{{ form.modulation.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>
{{ form.modulation }} {{ form.modulation }}
<small class="form-text text-muted">Удерживайте Ctrl для выбора нескольких</small>
</div> </div>
</div> </div>
@@ -70,12 +97,19 @@
<!-- Тип объекта --> <!-- Тип объекта -->
<div class="col-md-3 mb-3"> <div class="col-md-3 mb-3">
<label for="{{ form.object_type.id_for_label }}" class="form-label">{{ form.object_type.label }}</label> <label for="{{ form.object_type.id_for_label }}" class="form-label">{{ form.object_type.label }}</label>
<div class="d-flex justify-content-between mb-1">
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('object_type', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('object_type', false)">Снять</button>
</div>
{{ form.object_type }} {{ form.object_type }}
<small class="form-text text-muted">Удерживайте Ctrl для выбора нескольких</small>
</div> </div>
<!-- Принадлежность объекта (заглушка) --> <!-- Принадлежность объекта (заглушка) -->
<div class="col-md-3 mb-3"> <div class="col-md-3 mb-3">
<label for="{{ form.object_ownership.id_for_label }}" class="form-label">{{ form.object_ownership.label }} <span class="badge bg-secondary">Заглушка</span></label> <label for="{{ form.object_ownership.id_for_label }}" class="form-label">{{ form.object_ownership.label }} (не работает)</label>
{{ form.object_ownership }} {{ form.object_ownership }}
</div> </div>
</div> </div>
@@ -98,7 +132,7 @@
<!-- Планы на (фиктивный) --> <!-- Планы на (фиктивный) -->
<div class="col-md-3 mb-3"> <div class="col-md-3 mb-3">
<label class="form-label">{{ form.has_plans.label }} <span class="badge bg-secondary">Фиктивный</span></label> <label class="form-label">{{ form.has_plans.label }} (не работает)</label>
<div> <div>
{% for radio in form.has_plans %} {% for radio in form.has_plans %}
<div class="form-check"> <div class="form-check">
@@ -113,7 +147,7 @@
<!-- Успех 1 (фиктивный) --> <!-- Успех 1 (фиктивный) -->
<div class="col-md-3 mb-3"> <div class="col-md-3 mb-3">
<label class="form-label">{{ form.success_1.label }} <span class="badge bg-secondary">Фиктивный</span></label> <label class="form-label">{{ form.success_1.label }} (не рабльает)</label>
<div> <div>
{% for radio in form.success_1 %} {% for radio in form.success_1 %}
<div class="form-check"> <div class="form-check">
@@ -128,7 +162,7 @@
<!-- Успех 2 (фиктивный) --> <!-- Успех 2 (фиктивный) -->
<div class="col-md-3 mb-3"> <div class="col-md-3 mb-3">
<label class="form-label">{{ form.success_2.label }} <span class="badge bg-secondary">Фиктивный</span></label> <label class="form-label">{{ form.success_2.label }} (не работает)</label>
<div> <div>
{% for radio in form.success_2 %} {% for radio in form.success_2 %}
<div class="form-check"> <div class="form-check">
@@ -145,7 +179,7 @@
<div class="row"> <div class="row">
<!-- Диапазон дат (фиктивный) --> <!-- Диапазон дат (фиктивный) -->
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label">Диапазон дат <span class="badge bg-secondary">Фиктивный</span></label> <label class="form-label">Диапазон дат ГЛ:</label>
<div class="input-group"> <div class="input-group">
{{ form.date_from }} {{ form.date_from }}
<span class="input-group-text"></span> <span class="input-group-text"></span>
@@ -164,7 +198,7 @@
</div> </div>
</form> </form>
<!-- Кнопки экспорта и фильтрации --> <!-- Кнопка экспорта и статистика -->
{% if sources_with_date_info %} {% if sources_with_date_info %}
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12"> <div class="col-12">
@@ -174,10 +208,10 @@
<button type="button" class="btn btn-success" onclick="exportToExcel()"> <button type="button" class="btn btn-success" onclick="exportToExcel()">
<i class="bi bi-file-earmark-excel"></i> Экспорт в Excel <i class="bi bi-file-earmark-excel"></i> Экспорт в Excel
</button> </button>
<button type="button" class="btn btn-warning" onclick="filterByDateMatch()"> <span class="text-muted" id="statsCounter">
<i class="bi bi-funnel"></i> Оставить только подходящие по дате Найдено объектов: {{ sources_with_date_info|length }},
</button> точек: {% for source_data in sources_with_date_info %}{{ source_data.objitems_data|length }}{% if not forloop.last %}+{% endif %}{% endfor %}
<span class="ms-auto text-muted"></span> </span>
</div> </div>
</div> </div>
</div> </div>
@@ -195,7 +229,7 @@
<table class="table table-striped table-hover table-sm table-bordered" style="font-size: 0.85rem;" id="resultsTable"> <table class="table table-striped table-hover table-sm table-bordered" style="font-size: 0.85rem;" id="resultsTable">
<thead class="table-dark sticky-top"> <thead class="table-dark sticky-top">
<tr> <tr>
<th style="min-width: 80px;">ID Source</th> <th style="min-width: 80px;">ID объекта</th>
<th style="min-width: 120px;">Тип объекта</th> <th style="min-width: 120px;">Тип объекта</th>
<th class="text-center" style="min-width: 100px;">Кол-во точек</th> <th class="text-center" style="min-width: 100px;">Кол-во точек</th>
<th style="min-width: 120px;">Имя точки</th> <th style="min-width: 120px;">Имя точки</th>
@@ -214,7 +248,6 @@
{% for objitem_data in source_data.objitems_data %} {% for objitem_data in source_data.objitems_data %}
<tr data-source-id="{{ source_data.source.id }}" <tr data-source-id="{{ source_data.source.id }}"
data-objitem-id="{{ objitem_data.objitem.id }}" data-objitem-id="{{ objitem_data.objitem.id }}"
class="{% if objitem_data.matches_date %}table-success{% endif %}"
data-matches-date="{{ objitem_data.matches_date|yesno:'true,false' }}" data-matches-date="{{ objitem_data.matches_date|yesno:'true,false' }}"
data-is-first-in-source="{% if forloop.first %}true{% else %}false{% endif %}"> data-is-first-in-source="{% if forloop.first %}true{% else %}false{% endif %}">
@@ -251,7 +284,7 @@
<!-- Частота --> <!-- Частота -->
<td> <td>
{% if objitem_data.objitem.parameter_obj %} {% if objitem_data.objitem.parameter_obj %}
{{ objitem_data.objitem.parameter_obj.frequency|default:"-" }} {{ objitem_data.objitem.parameter_obj.frequency|default:"-"|floatformat:3 }}
{% else %} {% else %}
- -
{% endif %} {% endif %}
@@ -260,7 +293,7 @@
<!-- Полоса --> <!-- Полоса -->
<td> <td>
{% if objitem_data.objitem.parameter_obj %} {% if objitem_data.objitem.parameter_obj %}
{{ objitem_data.objitem.parameter_obj.freq_range|default:"-" }} {{ objitem_data.objitem.parameter_obj.freq_range|default:"-"|floatformat:3 }}
{% else %} {% else %}
- -
{% endif %} {% endif %}
@@ -287,7 +320,7 @@
<!-- Координаты ГЛ --> <!-- Координаты ГЛ -->
<td> <td>
{% if objitem_data.objitem.geo_obj and objitem_data.objitem.geo_obj.coords %} {% if objitem_data.objitem.geo_obj and objitem_data.objitem.geo_obj.coords %}
{{ objitem_data.objitem.geo_obj.coords.y|floatformat:6 }}, {{ objitem_data.objitem.geo_obj.coords.x|floatformat:6 }} {{ objitem_data.objitem.geo_obj.coords.y }}, {{ objitem_data.objitem.geo_obj.coords.x }}
{% else %} {% else %}
- -
{% endif %} {% endif %}
@@ -439,70 +472,18 @@ function removeSource(button) {
function updateCounter() { function updateCounter() {
const rows = document.querySelectorAll('#resultsTable tbody tr'); const rows = document.querySelectorAll('#resultsTable tbody tr');
const counter = document.querySelector('.text-muted'); const counter = document.getElementById('statsCounter');
if (counter) { if (counter) {
// Подсчитываем уникальные источники // Подсчитываем уникальные источники
const uniqueSources = new Set(); const uniqueSources = new Set();
let matchingCount = 0;
rows.forEach(row => { rows.forEach(row => {
uniqueSources.add(row.dataset.sourceId); uniqueSources.add(row.dataset.sourceId);
if (row.dataset.matchesDate === 'true') {
matchingCount++;
}
}); });
counter.textContent = `Найдено объектов: ${uniqueSources.size}, точек: ${rows.length} (подходящих по дате: ${matchingCount})`; counter.textContent = `Найдено объектов: ${uniqueSources.size}, точек: ${rows.length}`;
} }
} }
function filterByDateMatch() {
// Удаляем все строки, которые не подходят по дате
const rows = Array.from(document.querySelectorAll('#resultsTable tbody tr'));
const rowsToRemove = rows.filter(row => row.dataset.matchesDate !== 'true');
if (rowsToRemove.length === 0) {
alert('Все точки уже подходят по дате или фильтр по дате не задан');
return;
}
if (confirm(`Удалить ${rowsToRemove.length} точек, не подходящих по дате?`)) {
// Группируем строки по источникам
const sourceGroups = {};
rows.forEach(row => {
const sourceId = row.dataset.sourceId;
if (!sourceGroups[sourceId]) {
sourceGroups[sourceId] = [];
}
sourceGroups[sourceId].push(row);
});
// Обрабатываем каждый источник отдельно
Object.keys(sourceGroups).forEach(sourceId => {
const sourceRows = sourceGroups[sourceId];
const rowsToKeep = sourceRows.filter(row => row.dataset.matchesDate === 'true');
const rowsToDelete = sourceRows.filter(row => row.dataset.matchesDate !== 'true');
if (rowsToDelete.length === 0) {
return; // Все строки подходят
}
if (rowsToKeep.length === 0) {
// Удаляем все строки источника
rowsToDelete.forEach(row => row.remove());
} else {
// Есть строки для сохранения
// Удаляем строки по одной, используя функцию removeObjItem
rowsToDelete.forEach(row => {
const button = row.querySelector('button[onclick*="removeObjItem"]');
if (button) {
removeObjItem(button);
}
});
}
});
updateCounter();
}
}
function exportToExcel() { function exportToExcel() {
// Собираем ID оставшихся в таблице точек (ObjItem) // Собираем ID оставшихся в таблице точек (ObjItem)
@@ -544,6 +525,16 @@ function exportToExcel() {
document.body.removeChild(form); document.body.removeChild(form);
} }
// Функция для выбора/снятия всех опций в select
function selectAllOptions(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.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
updateCounter(); updateCounter();
@@ -563,10 +554,7 @@ document.addEventListener('DOMContentLoaded', function() {
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
} }
/* Выделение строк, подходящих по дате */
.table-success {
background-color: #d1e7dd !important;
}
/* Стили для кнопок действий */ /* Стили для кнопок действий */
.btn-sm { .btn-sm {

View File

@@ -31,49 +31,58 @@ class KubsatView(LoginRequiredMixin, FormView):
form = self.form_class(self.request.GET) form = self.form_class(self.request.GET)
if form.is_valid(): if form.is_valid():
sources = self.apply_filters(form.cleaned_data) sources = self.apply_filters(form.cleaned_data)
# Определяем, какие источники подходят по дате
date_from = form.cleaned_data.get('date_from') date_from = form.cleaned_data.get('date_from')
date_to = form.cleaned_data.get('date_to') date_to = form.cleaned_data.get('date_to')
has_date_filter = bool(date_from or date_to)
# Добавляем информацию о соответствии дате для каждого источника objitem_count = form.cleaned_data.get('objitem_count')
sources_with_date_info = [] sources_with_date_info = []
for source in sources: for source in sources:
source_data = { source_data = {
'source': source, 'source': source,
'matches_date': False,
'objitems_data': [] 'objitems_data': []
} }
# Проверяем каждый ObjItem
for objitem in source.source_objitems.all(): for objitem in source.source_objitems.all():
objitem_matches_date = False objitem_matches_date = True
geo_date = None geo_date = None
if hasattr(objitem, 'geo_obj') and objitem.geo_obj and objitem.geo_obj.timestamp: if hasattr(objitem, 'geo_obj') and objitem.geo_obj and objitem.geo_obj.timestamp:
geo_date = objitem.geo_obj.timestamp.date() geo_date = objitem.geo_obj.timestamp.date()
# Проверяем попадание в диапазон дат # Проверяем попадание в диапазон дат (только если фильтр задан)
if date_from and date_to: if has_date_filter:
objitem_matches_date = date_from <= geo_date <= date_to if date_from and date_to:
elif date_from: objitem_matches_date = date_from <= geo_date <= date_to
objitem_matches_date = geo_date >= date_from elif date_from:
elif date_to: objitem_matches_date = geo_date >= date_from
objitem_matches_date = geo_date <= date_to elif date_to:
else: objitem_matches_date = geo_date <= date_to
objitem_matches_date = True # Нет фильтра по дате elif has_date_filter:
# Если фильтр по дате задан, но у точки нет даты - не подходит
objitem_matches_date = False
source_data['objitems_data'].append({ # Добавляем только точки, подходящие по дате (или все, если фильтр не задан)
'objitem': objitem, if not has_date_filter or objitem_matches_date:
'matches_date': objitem_matches_date, source_data['objitems_data'].append({
'geo_date': geo_date 'objitem': objitem,
}) 'matches_date': objitem_matches_date,
'geo_date': geo_date
# Если хотя бы одна точка подходит по дате, весь источник подходит })
if objitem_matches_date:
source_data['matches_date'] = True
sources_with_date_info.append(source_data) # ЭТАП 2: Проверяем количество отфильтрованных точек
filtered_count = len(source_data['objitems_data'])
# Применяем фильтр по количеству точек (если задан)
include_source = True
if objitem_count:
if objitem_count == '1':
include_source = (filtered_count == 1)
elif objitem_count == '2+':
include_source = (filtered_count >= 2)
if source_data['objitems_data'] and include_source:
sources_with_date_info.append(source_data)
context['sources_with_date_info'] = sources_with_date_info context['sources_with_date_info'] = sources_with_date_info
context['form'] = form context['form'] = form
@@ -95,9 +104,11 @@ class KubsatView(LoginRequiredMixin, FormView):
source_objitems__parameter_obj__id_satellite__in=filters['satellites'] source_objitems__parameter_obj__id_satellite__in=filters['satellites']
).distinct() ).distinct()
# Фильтр по полосе спутника (пока не реализован полностью) # Фильтр по полосе спутника
if filters.get('band'): if filters.get('band'):
pass # TODO: реализовать фильтр по band queryset = queryset.filter(
source_objitems__parameter_obj__id_satellite__band__in=filters['band']
).distinct()
# Фильтр по поляризации # Фильтр по поляризации
if filters.get('polarization'): if filters.get('polarization'):
@@ -197,7 +208,7 @@ class KubsatExportView(LoginRequiredMixin, FormView):
'Обратный канал, МГц', 'Обратный канал, МГц',
'Перенос', 'Перенос',
'Получено координат, раз', 'Получено координат, раз',
'Дата', 'Период получения координат',
'Зеркала', 'Зеркала',
'СКО, км', 'СКО, км',
'Примечание', 'Примечание',
@@ -283,21 +294,42 @@ class KubsatExportView(LoginRequiredMixin, FormView):
mirrors.append(mirror.name) mirrors.append(mirror.name)
mirrors_str = '\n'.join(mirrors) mirrors_str = '\n'.join(mirrors)
# Диапазон дат ГЛ (самая ранняя - самая поздняя)
geo_dates = []
for objitem in objitems_list:
if hasattr(objitem, 'geo_obj') and objitem.geo_obj and objitem.geo_obj.timestamp:
geo_dates.append(objitem.geo_obj.timestamp.date())
date_range_str = '-'
if geo_dates:
min_date = min(geo_dates)
max_date = max(geo_dates)
# Форматируем даты в формате d.m.Y
min_date_str = min_date.strftime('%d.%m.%Y')
max_date_str = max_date.strftime('%d.%m.%Y')
if min_date == max_date:
# Если даты совпадают, показываем только одну
date_range_str = min_date_str
else:
# Иначе показываем диапазон
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=1, value=current_date)
ws.cell(row=row_num, column=2, value=latitude) ws.cell(row=row_num, column=2, value=latitude)
ws.cell(row=row_num, column=3, value=longitude) 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=4, value=0.0)
ws.cell(row=row_num, column=5, value=location) 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=6, value=satellite_info)
ws.cell(row=row_num, column=7, value=direct_channel) 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=8, value=reverse_channel)
ws.cell(row=row_num, column=9, value=transfer) 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=10, value=objitem_count)
ws.cell(row=row_num, column=11, value='-') # Дата (пока не заполняется) 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=12, value=mirrors_str)
ws.cell(row=row_num, column=13, value='') # СКО не заполняется ws.cell(row=row_num, column=13, value='')
ws.cell(row=row_num, column=14, value='') # Примечание не заполняется ws.cell(row=row_num, column=14, value='')
ws.cell(row=row_num, column=15, value=operator_name) ws.cell(row=row_num, column=15, value=operator_name)
row_num += 1 row_num += 1
@@ -325,6 +357,6 @@ class KubsatExportView(LoginRequiredMixin, FormView):
output.read(), output.read(),
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
) )
response['Content-Disposition'] = f'attachment; filename="kubsat_{datetime.now().strftime("%Y%m%d_%H%M%S")}.xlsx"' response['Content-Disposition'] = f'attachment; filename="kubsat_{datetime.now().strftime("%Y%m%d")}.xlsx"'
return response return response

View File

@@ -484,7 +484,7 @@ class ObjItemListView(LoginRequiredMixin, View):
"page_obj": page_obj, "page_obj": page_obj,
"processed_objects": processed_objects, "processed_objects": processed_objects,
"items_per_page": items_per_page, "items_per_page": items_per_page,
"available_items_per_page": [50, 100, 500, 1000], "available_items_per_page": [50, 100, 200, 500, 1000],
"freq_min": freq_min, "freq_min": freq_min,
"freq_max": freq_max, "freq_max": freq_max,
"range_min": range_min, "range_min": range_min,