diff --git a/dbapp/KUBSAT_FEATURE.md b/dbapp/KUBSAT_FEATURE.md index f213d75..6a40c56 100644 --- a/dbapp/KUBSAT_FEATURE.md +++ b/dbapp/KUBSAT_FEATURE.md @@ -54,8 +54,23 @@ - Количество точек автоматически пересчитывается при удалении строк - Таблица имеет фиксированную высоту с прокруткой и sticky заголовок -### Выделение по дате -Строки, у которых дата ГЛ попадает в выбранный диапазон дат, **выделяются зеленым цветом**. При этом все объекты остаются в таблице, независимо от фильтра по дате. +### Двухэтапная фильтрация + +Фильтрация происходит в два этапа: + +**Этап 1: Фильтрация по дате ГЛ** +- Если задан диапазон дат (от/до), отображаются только те точки, у которых дата ГЛ попадает в этот диапазон +- Точки без даты ГЛ исключаются, если фильтр по дате задан +- Если фильтр не задан, показываются все точки + +**Этап 2: Фильтрация по количеству точек** +- Применяется к уже отфильтрованным по дате точкам +- Варианты: + - "Все" - показываются источники с любым количеством точек + - "1" - только источники с ровно 1 точкой (после фильтрации по дате) + - "2 и более" - только источники с 2 и более точками (после фильтрации по дате) + +**Важно**: Фильтр по количеству точек учитывает только те точки, которые прошли фильтрацию по дате! ### Управление данными в таблице @@ -63,10 +78,7 @@ - **Кнопка с иконкой корзины** (для каждой точки) - удаляет конкретную точку (ObjItem) из таблицы - **Кнопка с иконкой заполненной корзины** (для первой точки источника) - удаляет все точки источника (Source) из таблицы -**Кнопка "Оставить только подходящие по дате":** -- Удаляет из таблицы все точки, которые НЕ подходят по заданному диапазону дат -- Оставляет только зеленые (выделенные) строки -- Запрашивает подтверждение перед удалением + **Важно**: Все удаления происходят только из таблицы, **БЕЗ удаления из базы данных**. Это позволяет пользователю исключить ненужные записи перед экспортом. @@ -83,7 +95,7 @@ 8. **Обратный канал, МГц** - частота источника 9. **Перенос** - из объекта Transponder 10. **Получено координат, раз** - количество точек (ObjItem), оставшихся в таблице для данного источника -11. **Дата** - пока заполняется как "-" +11. **Период получения координат** - диапазон дат ГЛ в формате "5.11.2025-15.11.2025" (от самой ранней до самой поздней даты среди точек источника). Если все точки имеют одну дату, показывается только одна дата. 12. **Зеркала** - все имена зеркал через перенос строки (из оставшихся точек) 13. **СКО, км** - не заполняется 14. **Примечание** - не заполняется @@ -123,14 +135,13 @@ queryset = Source.objects.select_related('info').prefetch_related( 1. Откройте страницу "Кубсат" из навигационного меню 2. Выберите нужные фильтры (спутники, поляризация, частота и т.д.) -3. Опционально укажите диапазон дат для выделения подходящих точек -4. Нажмите "Применить фильтры" -5. В таблице отобразятся все точки (ObjItem) сгруппированные по источникам (Source) -6. Точки, подходящие по дате, будут выделены зеленым цветом -7. Опционально нажмите "Оставить только подходящие по дате" для быстрого удаления неподходящих точек -8. При необходимости удалите отдельные точки или целые объекты кнопками в колонке "Действия" -9. Нажмите "Экспорт в Excel" для скачивания файла с оставшимися данными -10. Форма не сбрасывается после экспорта - можно продолжить работу +3. Опционально укажите диапазон дат для фильтрации точек по дате ГЛ (Этап 1) +4. Опционально выберите количество точек (1 или 2+) - применяется к отфильтрованным по дате точкам (Этап 2) +5. Нажмите "Применить фильтры" +6. В таблице отобразятся точки (ObjItem) сгруппированные по источникам (Source) +7. При необходимости удалите отдельные точки или целые объекты кнопками в колонке "Действия" +8. Нажмите "Экспорт в Excel" для скачивания файла с оставшимися данными +9. Форма не сбрасывается после экспорта - можно продолжить работу ## Примечания @@ -138,5 +149,6 @@ queryset = Source.objects.select_related('info').prefetch_related( - Удаление точек/объектов из таблицы не влияет на базу данных - Экспортируются только оставшиеся в таблице точки - Координаты в Excel рассчитываются как инкрементальное среднее из оставшихся точек -- Фильтр по дате не скрывает объекты, а только выделяет их цветом +- Фильтр по дате скрывает неподходящие точки (не показывает их в таблице) - Каждая строка таблицы = одна точка (ObjItem), строки группируются по источникам (Source) +- Количество точек в колонке "Кол-во точек" автоматически пересчитывается при удалении строк diff --git a/dbapp/mainapp/forms.py b/dbapp/mainapp/forms.py index 4bebaaa..de3c51e 100644 --- a/dbapp/mainapp/forms.py +++ b/dbapp/mainapp/forms.py @@ -463,7 +463,7 @@ class SourceForm(forms.ModelForm): class Meta: model = Source - fields = ['info'] # Добавляем поле info + fields = ['info'] widgets = { 'info': forms.Select(attrs={ 'class': 'form-select', @@ -555,18 +555,18 @@ class KubsatFilterForm(forms.Form): widget=forms.SelectMultiple(attrs={'class': 'form-select', 'size': '5'}) ) - band = forms.ModelChoiceField( + band = forms.ModelMultipleChoiceField( queryset=None, - label='Полоса спутника', + label='Диапазоны работы спутника', required=False, - widget=forms.Select(attrs={'class': 'form-select'}) + widget=forms.SelectMultiple(attrs={'class': 'form-select', 'size': '4'}) ) polarization = forms.ModelMultipleChoiceField( queryset=Polarization.objects.all().order_by('name'), label='Поляризация', required=False, - widget=forms.SelectMultiple(attrs={'class': 'form-select', 'size': '3'}) + widget=forms.SelectMultiple(attrs={'class': 'form-select', 'size': '4'}) ) frequency_min = forms.FloatField( @@ -597,7 +597,7 @@ class KubsatFilterForm(forms.Form): queryset=Modulation.objects.all().order_by('name'), label='Модуляция', required=False, - widget=forms.SelectMultiple(attrs={'class': 'form-select', 'size': '3'}) + widget=forms.SelectMultiple(attrs={'class': 'form-select', 'size': '4'}) ) object_type = forms.ModelMultipleChoiceField( @@ -624,22 +624,22 @@ class KubsatFilterForm(forms.Form): # Фиктивные фильтры has_plans = forms.ChoiceField( - choices=[('', 'Все'), ('yes', 'Да'), ('no', 'Нет')], - label='Планы на', + choices=[('', 'Неважно'), ('yes', 'Да'), ('no', 'Нет')], + label='Планы на Кубсат', required=False, widget=forms.RadioSelect() ) success_1 = forms.ChoiceField( - choices=[('', 'Все'), ('yes', 'Да'), ('no', 'Нет')], - label='Успех 1', + choices=[('', 'Неважно'), ('yes', 'Да'), ('no', 'Нет')], + label='ГСО успешно?', required=False, widget=forms.RadioSelect() ) success_2 = forms.ChoiceField( - choices=[('', 'Все'), ('yes', 'Да'), ('no', 'Нет')], - label='Успех 2', + choices=[('', 'Неважно'), ('yes', 'Да'), ('no', 'Нет')], + label='Кубсат успешно?', required=False, widget=forms.RadioSelect() ) diff --git a/dbapp/mainapp/templates/mainapp/components/_navbar.html b/dbapp/mainapp/templates/mainapp/components/_navbar.html index dcc5d6f..7d1675c 100644 --- a/dbapp/mainapp/templates/mainapp/components/_navbar.html +++ b/dbapp/mainapp/templates/mainapp/components/_navbar.html @@ -37,11 +37,11 @@
| ID Source | +ID объекта | Тип объекта | Кол-во точек | Имя точки | @@ -214,7 +248,6 @@ {% for objitem_data in source_data.objitems_data %}
|---|---|---|---|---|
| {% if objitem_data.objitem.parameter_obj %} - {{ objitem_data.objitem.parameter_obj.frequency|default:"-" }} + {{ objitem_data.objitem.parameter_obj.frequency|default:"-"|floatformat:3 }} {% else %} - {% endif %} @@ -260,7 +293,7 @@ | {% 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 %} - {% endif %} @@ -287,7 +320,7 @@ | {% 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 %} - {% endif %} @@ -439,70 +472,18 @@ function removeSource(button) { function updateCounter() { const rows = document.querySelectorAll('#resultsTable tbody tr'); - const counter = document.querySelector('.text-muted'); + const counter = document.getElementById('statsCounter'); if (counter) { // Подсчитываем уникальные источники const uniqueSources = new Set(); - let matchingCount = 0; rows.forEach(row => { 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() { // Собираем ID оставшихся в таблице точек (ObjItem) @@ -544,6 +525,16 @@ function exportToExcel() { 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() { updateCounter(); @@ -563,10 +554,7 @@ document.addEventListener('DOMContentLoaded', function() { margin-bottom: 0.25rem; } -/* Выделение строк, подходящих по дате */ -.table-success { - background-color: #d1e7dd !important; -} + /* Стили для кнопок действий */ .btn-sm { diff --git a/dbapp/mainapp/views/kubsat.py b/dbapp/mainapp/views/kubsat.py index 7575555..1a5fbd3 100644 --- a/dbapp/mainapp/views/kubsat.py +++ b/dbapp/mainapp/views/kubsat.py @@ -31,49 +31,58 @@ class KubsatView(LoginRequiredMixin, FormView): form = self.form_class(self.request.GET) if form.is_valid(): sources = self.apply_filters(form.cleaned_data) - - # Определяем, какие источники подходят по дате date_from = form.cleaned_data.get('date_from') 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 = [] for source in sources: source_data = { 'source': source, - 'matches_date': False, 'objitems_data': [] } - # Проверяем каждый ObjItem for objitem in source.source_objitems.all(): - objitem_matches_date = False + objitem_matches_date = True geo_date = None if hasattr(objitem, 'geo_obj') and objitem.geo_obj and objitem.geo_obj.timestamp: geo_date = objitem.geo_obj.timestamp.date() - # Проверяем попадание в диапазон дат - if date_from and date_to: - objitem_matches_date = date_from <= geo_date <= date_to - elif date_from: - objitem_matches_date = geo_date >= date_from - elif date_to: - objitem_matches_date = geo_date <= date_to - else: - objitem_matches_date = True # Нет фильтра по дате + # Проверяем попадание в диапазон дат (только если фильтр задан) + if has_date_filter: + if date_from and date_to: + objitem_matches_date = date_from <= geo_date <= date_to + elif date_from: + objitem_matches_date = geo_date >= date_from + elif date_to: + objitem_matches_date = geo_date <= date_to + elif has_date_filter: + # Если фильтр по дате задан, но у точки нет даты - не подходит + objitem_matches_date = False - source_data['objitems_data'].append({ - 'objitem': objitem, - 'matches_date': objitem_matches_date, - 'geo_date': geo_date - }) - - # Если хотя бы одна точка подходит по дате, весь источник подходит - if objitem_matches_date: - source_data['matches_date'] = True + # Добавляем только точки, подходящие по дате (или все, если фильтр не задан) + if not has_date_filter or objitem_matches_date: + source_data['objitems_data'].append({ + 'objitem': objitem, + 'matches_date': objitem_matches_date, + 'geo_date': geo_date + }) - 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['form'] = form @@ -95,9 +104,11 @@ class KubsatView(LoginRequiredMixin, FormView): source_objitems__parameter_obj__id_satellite__in=filters['satellites'] ).distinct() - # Фильтр по полосе спутника (пока не реализован полностью) + # Фильтр по полосе спутника 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'): @@ -197,7 +208,7 @@ class KubsatExportView(LoginRequiredMixin, FormView): 'Обратный канал, МГц', 'Перенос', 'Получено координат, раз', - 'Дата', + 'Период получения координат', 'Зеркала', 'СКО, км', 'Примечание', @@ -283,21 +294,42 @@ class KubsatExportView(LoginRequiredMixin, FormView): mirrors.append(mirror.name) 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=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=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='-') # Дата (пока не заполняется) + 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=13, value='') + ws.cell(row=row_num, column=14, value='') ws.cell(row=row_num, column=15, value=operator_name) row_num += 1 @@ -325,6 +357,6 @@ class KubsatExportView(LoginRequiredMixin, FormView): output.read(), 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 diff --git a/dbapp/mainapp/views/objitem.py b/dbapp/mainapp/views/objitem.py index 34196e2..94ffedc 100644 --- a/dbapp/mainapp/views/objitem.py +++ b/dbapp/mainapp/views/objitem.py @@ -484,7 +484,7 @@ class ObjItemListView(LoginRequiredMixin, View): "page_obj": page_obj, "processed_objects": processed_objects, "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_max": freq_max, "range_min": range_min, |