Добавил формы создания и пофиксил баг с пользователями
This commit is contained in:
@@ -40,6 +40,13 @@ class LoadExcelData(forms.Form):
|
||||
min_value=0,
|
||||
widget=forms.NumberInput(attrs={"class": "form-control"}),
|
||||
)
|
||||
is_automatic = forms.BooleanField(
|
||||
label="Автоматическая загрузка",
|
||||
required=False,
|
||||
initial=False,
|
||||
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||
help_text="Если отмечено, точки не будут добавляться к объектам (Source)",
|
||||
)
|
||||
|
||||
|
||||
class LoadCsvData(forms.Form):
|
||||
@@ -47,6 +54,13 @@ class LoadCsvData(forms.Form):
|
||||
label="Выберите CSV файл",
|
||||
widget=forms.FileInput(attrs={"class": "form-control", "accept": ".csv"}),
|
||||
)
|
||||
is_automatic = forms.BooleanField(
|
||||
label="Автоматическая загрузка",
|
||||
required=False,
|
||||
initial=False,
|
||||
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||
help_text="Если отмечено, точки не будут добавляться к объектам (Source)",
|
||||
)
|
||||
|
||||
|
||||
class UploadVchLoad(UploadFileForm):
|
||||
|
||||
28
dbapp/mainapp/migrations/0013_add_is_automatic_to_objitem.py
Normal file
28
dbapp/mainapp/migrations/0013_add_is_automatic_to_objitem.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-24 19:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0012_source_confirm_at_source_last_signal_at'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='Mirror',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='sigmaparameter',
|
||||
name='mark',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='objitem',
|
||||
name='is_automatic',
|
||||
field=models.BooleanField(db_index=True, default=False, help_text='Если True, точка не добавляется к объектам (Source), а хранится отдельно', verbose_name='Автоматическая'),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='SigmaParMark',
|
||||
),
|
||||
]
|
||||
@@ -728,6 +728,12 @@ class ObjItem(models.Model):
|
||||
verbose_name="Транспондер",
|
||||
help_text="Транспондер, с помощью которого была получена точка",
|
||||
)
|
||||
is_automatic = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name="Автоматическая",
|
||||
db_index=True,
|
||||
help_text="Если True, точка не добавляется к объектам (Source), а хранится отдельно",
|
||||
)
|
||||
|
||||
# Метаданные
|
||||
created_at = models.DateTimeField(
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
# Django imports
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
# from django.contrib.auth.models import User
|
||||
# from django.db.models.signals import post_save
|
||||
# from django.dispatch import receiver
|
||||
|
||||
# Local imports
|
||||
from .models import CustomUser
|
||||
# # Local imports
|
||||
# from .models import CustomUser
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def create_or_update_user_profile(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
CustomUser.objects.create(user=instance)
|
||||
instance.customuser.save()
|
||||
# @receiver(post_save, sender=User)
|
||||
# def create_or_update_user_profile(sender, instance, created, **kwargs):
|
||||
# if created:
|
||||
# CustomUser.objects.get_or_create(user=instance)
|
||||
# else:
|
||||
# # Only save if customuser exists (avoid error if it doesn't)
|
||||
# if hasattr(instance, 'customuser'):
|
||||
# instance.customuser.save()
|
||||
@@ -79,26 +79,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transponders Card -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="bg-warning bg-opacity-10 rounded-circle p-2 me-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-wifi text-warning" viewBox="0 0 16 16">
|
||||
<path d="M6.002 3.5a5.5 5.5 0 1 1 3.996 9.5H10A5.5 5.5 0 0 1 6.002 3.5M6.002 5.5a3.5 3.5 0 1 0 3.996 5.5H10A3.5 3.5 0 0 0 6.002 5.5"/>
|
||||
<path d="M10.5 12.5a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5.5.5 0 0 0-1 0 .5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5.5.5 0 0 0-1 0 .5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5 3.5 3.5 0 0 1 7 0"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="card-title mb-0">Добавление транспондеров</h3>
|
||||
</div>
|
||||
<p class="card-text">Добавьте список транспондеров в базу данных.</p>
|
||||
<a href="{% url 'mainapp:add_trans' %}" class="btn btn-warning">
|
||||
Добавить транспондеры
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- VCH Load Data Card -->
|
||||
<div class="col-lg-6">
|
||||
|
||||
@@ -19,6 +19,19 @@
|
||||
<!-- Form fields with Bootstrap styling -->
|
||||
{% include 'mainapp/components/_form_field.html' with field=form.file %}
|
||||
|
||||
<!-- Automatic checkbox -->
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
{{ form.is_automatic }}
|
||||
<label class="form-check-label" for="{{ form.is_automatic.id_for_label }}">
|
||||
{{ form.is_automatic.label }}
|
||||
</label>
|
||||
{% if form.is_automatic.help_text %}
|
||||
<div class="form-text">{{ form.is_automatic.help_text }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<a href="{% url 'mainapp:source_list' %}" class="btn btn-secondary me-md-2">Назад</a>
|
||||
<button type="submit" class="btn btn-success">Добавить в базу</button>
|
||||
|
||||
@@ -21,6 +21,19 @@
|
||||
{% include 'mainapp/components/_form_field.html' with field=form.sat_choice %}
|
||||
{% include 'mainapp/components/_form_field.html' with field=form.number_input %}
|
||||
|
||||
<!-- Automatic checkbox -->
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
{{ form.is_automatic }}
|
||||
<label class="form-check-label" for="{{ form.is_automatic.id_for_label }}">
|
||||
{{ form.is_automatic.label }}
|
||||
</label>
|
||||
{% if form.is_automatic.help_text %}
|
||||
<div class="form-text">{{ form.is_automatic.help_text }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<a href="{% url 'mainapp:source_list' %}" class="btn btn-secondary me-md-2">Назад</a>
|
||||
<button type="submit" class="btn btn-primary">Добавить в базу</button>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% load static leaflet_tags %}
|
||||
{% load l10n %}
|
||||
|
||||
{% block title %}{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать объект{% endif %}{% endblock %}
|
||||
{% block title %}{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать новый объект{% endif %}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="{% static 'css/checkbox-select-multiple.css' %}">
|
||||
@@ -144,7 +144,7 @@
|
||||
<div class="container-fluid px-3">
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 d-flex justify-content-between align-items-center">
|
||||
<h2>{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать объект{% endif %}</h2>
|
||||
<h2>{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать новый объект{% endif %}</h2>
|
||||
<div>
|
||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||||
<button type="submit" form="objitem-form" class="btn btn-primary btn-action">Сохранить</button>
|
||||
@@ -248,6 +248,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if object %}
|
||||
<!-- Транспондер -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-header">
|
||||
@@ -339,6 +340,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Блок с картой -->
|
||||
<div class="form-section">
|
||||
|
||||
@@ -40,13 +40,10 @@
|
||||
|
||||
<!-- Action buttons bar -->
|
||||
<div class="d-flex gap-2">
|
||||
{% comment %} <button type="button" class="btn btn-success btn-sm" title="Добавить">
|
||||
<i class="bi bi-plus-circle"></i> Добавить
|
||||
</button>
|
||||
<button type="button" class="btn btn-info btn-sm" title="Изменить">
|
||||
<i class="bi bi-pencil"></i> Изменить
|
||||
</button> {% endcomment %}
|
||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||||
<a href="{% url 'mainapp:objitem_create' %}" class="btn btn-success btn-sm" title="Создать новый объект">
|
||||
<i class="bi bi-plus-circle"></i> Создать
|
||||
</a>
|
||||
<button type="button" class="btn btn-danger btn-sm" title="Удалить"
|
||||
onclick="deleteSelectedObjects()">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
@@ -243,6 +240,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Automatic 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="is_automatic" id="is_automatic_1" value="1"
|
||||
{% if is_automatic == '1' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="is_automatic_1">Да</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="is_automatic" id="is_automatic_0" value="0"
|
||||
{% if is_automatic == '0' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="is_automatic_0">Нет</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Дата ГЛ:</label>
|
||||
@@ -309,6 +323,7 @@
|
||||
{% include 'mainapp/components/_table_header.html' with label="Тип точки" field="" sortable=False %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Sigma" field="" sortable=False %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Зеркала" field="" sortable=False %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Автоматическая?" field="is_automatic" sort=sort %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -378,10 +393,11 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item.mirrors }}</td>
|
||||
<td>{{ item.is_automatic }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="22" class="text-center py-4">
|
||||
<td colspan="23" class="text-center py-4">
|
||||
{% if selected_satellite_id %}
|
||||
Нет данных для выбранных фильтров
|
||||
{% else %}
|
||||
@@ -612,6 +628,7 @@
|
||||
setupRadioLikeCheckboxes('has_valid');
|
||||
setupRadioLikeCheckboxes('has_source_type');
|
||||
setupRadioLikeCheckboxes('has_sigma');
|
||||
setupRadioLikeCheckboxes('is_automatic');
|
||||
|
||||
// Date range quick selection functions
|
||||
window.setDateRange = function (period) {
|
||||
|
||||
@@ -212,7 +212,7 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h4>Частотный план</h4>
|
||||
<p class="text-muted">Визуализация транспондеров спутника по частотам. <span style="color: #0d6efd;">■</span> Downlink (синий), <span style="color: #fd7e14;">■</span> Uplink (оранжевый). Используйте колесико мыши для масштабирования, наведите курсор на полосу для подробной информации.</p>
|
||||
<p class="text-muted">Визуализация транспондеров спутника по частотам. <span style="color: #0d6efd;">■</span> Downlink (синий), <span style="color: #fd7e14;">■</span> Uplink (оранжевый). Используйте колесико мыши для масштабирования, наведите курсор на полосу для подробной информации и связи с парным каналом.</p>
|
||||
|
||||
<div class="frequency-plan">
|
||||
<div class="chart-controls">
|
||||
@@ -272,19 +272,6 @@
|
||||
// Transponder data from Django
|
||||
const transpondersData = {{ transponders|safe }};
|
||||
|
||||
// Color mapping for polarizations
|
||||
const polarizationColors = {
|
||||
'H': '#0d6efd',
|
||||
'V': '#198754',
|
||||
'L': '#dc3545',
|
||||
'R': '#ffc107',
|
||||
'default': '#6c757d'
|
||||
};
|
||||
|
||||
function getColor(polarization) {
|
||||
return polarizationColors[polarization] || polarizationColors['default'];
|
||||
}
|
||||
|
||||
// Chart state
|
||||
let canvas, ctx, container;
|
||||
let zoomLevel = 1;
|
||||
@@ -505,19 +492,21 @@ function renderChart() {
|
||||
|
||||
if (barWidth < 1) return;
|
||||
|
||||
const isHovered = hoveredTransponder && hoveredTransponder.transponder.name === t.name;
|
||||
|
||||
// Draw downlink bar
|
||||
ctx.fillStyle = downlinkColor;
|
||||
ctx.fillRect(x1, downlinkBarY, barWidth, downlinkBarHeight);
|
||||
|
||||
// Draw border
|
||||
ctx.strokeStyle = '#fff';
|
||||
ctx.lineWidth = 1;
|
||||
// Draw border (thicker if hovered)
|
||||
ctx.strokeStyle = isHovered ? '#000' : '#fff';
|
||||
ctx.lineWidth = isHovered ? 3 : 1;
|
||||
ctx.strokeRect(x1, downlinkBarY, barWidth, downlinkBarHeight);
|
||||
|
||||
// Draw name if there's space
|
||||
if (barWidth > 40) {
|
||||
ctx.fillStyle = (pol === 'R') ? '#000' : '#fff';
|
||||
ctx.font = '9px sans-serif';
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = isHovered ? 'bold 10px sans-serif' : '9px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(t.name, x1 + barWidth / 2, downlinkBarY + downlinkBarHeight / 2 + 3);
|
||||
}
|
||||
@@ -529,7 +518,8 @@ function renderChart() {
|
||||
width: barWidth,
|
||||
height: downlinkBarHeight,
|
||||
transponder: t,
|
||||
type: 'downlink'
|
||||
type: 'downlink',
|
||||
centerX: x1 + barWidth / 2
|
||||
});
|
||||
});
|
||||
|
||||
@@ -553,19 +543,21 @@ function renderChart() {
|
||||
// Skip if too small
|
||||
if (barWidth < 1) return;
|
||||
|
||||
const isHovered = hoveredTransponder && hoveredTransponder.transponder.name === t.name;
|
||||
|
||||
// Draw uplink bar
|
||||
ctx.fillStyle = uplinkColor;
|
||||
ctx.fillRect(x1, uplinkBarY, barWidth, uplinkBarHeight);
|
||||
|
||||
// Draw border
|
||||
ctx.strokeStyle = '#fff';
|
||||
ctx.lineWidth = 1;
|
||||
// Draw border (thicker if hovered)
|
||||
ctx.strokeStyle = isHovered ? '#000' : '#fff';
|
||||
ctx.lineWidth = isHovered ? 3 : 1;
|
||||
ctx.strokeRect(x1, uplinkBarY, barWidth, uplinkBarHeight);
|
||||
|
||||
// Draw name if there's space
|
||||
if (barWidth > 40) {
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = '9px sans-serif';
|
||||
ctx.font = isHovered ? 'bold 10px sans-serif' : '9px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(t.name, x1 + barWidth / 2, uplinkBarY + uplinkBarHeight / 2 + 3);
|
||||
}
|
||||
@@ -577,7 +569,8 @@ function renderChart() {
|
||||
width: barWidth,
|
||||
height: uplinkBarHeight,
|
||||
transponder: t,
|
||||
type: 'uplink'
|
||||
type: 'uplink',
|
||||
centerX: x1 + barWidth / 2
|
||||
});
|
||||
});
|
||||
|
||||
@@ -593,12 +586,43 @@ function renderChart() {
|
||||
}
|
||||
});
|
||||
|
||||
// Draw hover tooltip
|
||||
// Draw connection line between downlink and uplink when hovering
|
||||
if (hoveredTransponder) {
|
||||
drawConnectionLine(hoveredTransponder);
|
||||
drawTooltip(hoveredTransponder);
|
||||
}
|
||||
}
|
||||
|
||||
function drawConnectionLine(rectInfo) {
|
||||
const t = rectInfo.transponder;
|
||||
if (!t.uplink) return; // No uplink to connect
|
||||
|
||||
// Find both downlink and uplink rects for this transponder
|
||||
const downlinkRect = transponderRects.find(r => r.transponder.name === t.name && r.type === 'downlink');
|
||||
const uplinkRect = transponderRects.find(r => r.transponder.name === t.name && r.type === 'uplink');
|
||||
|
||||
if (!downlinkRect || !uplinkRect) return;
|
||||
|
||||
// Draw connecting line
|
||||
const x1 = downlinkRect.centerX;
|
||||
const y1 = downlinkRect.y + downlinkRect.height;
|
||||
const x2 = uplinkRect.centerX;
|
||||
const y2 = uplinkRect.y;
|
||||
|
||||
ctx.save();
|
||||
ctx.strokeStyle = '#ffc107';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.setLineDash([5, 3]);
|
||||
ctx.globalAlpha = 0.8;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, y1);
|
||||
ctx.lineTo(x2, y2);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawTooltip(rectInfo) {
|
||||
const t = rectInfo.transponder;
|
||||
const isUplink = rectInfo.type === 'uplink';
|
||||
@@ -639,14 +663,16 @@ function drawTooltip(rectInfo) {
|
||||
const mouseX = rectInfo._mouseX || canvas.width / 2;
|
||||
const mouseY = rectInfo._mouseY || canvas.height / 2;
|
||||
let tooltipX = mouseX + 15;
|
||||
let tooltipY = mouseY + 15;
|
||||
let tooltipY = mouseY - tooltipHeight - 15; // Always show above cursor
|
||||
|
||||
// Keep tooltip in bounds
|
||||
// Keep tooltip in bounds horizontally
|
||||
if (tooltipX + tooltipWidth > canvas.width) {
|
||||
tooltipX = mouseX - tooltipWidth - 15;
|
||||
}
|
||||
if (tooltipY + tooltipHeight > canvas.height) {
|
||||
tooltipY = mouseY - tooltipHeight - 15;
|
||||
|
||||
// If tooltip goes above canvas, show below cursor instead
|
||||
if (tooltipY < 0) {
|
||||
tooltipY = mouseY + 15;
|
||||
}
|
||||
|
||||
// Draw tooltip background
|
||||
|
||||
@@ -129,13 +129,15 @@
|
||||
<div class="container-fluid px-3">
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 d-flex justify-content-between align-items-center">
|
||||
<h2>Редактировать объект #{{ object.id }}</h2>
|
||||
<h2>{% if object %}Редактировать объект #{{ object.id }}{% else %}Создать новый источник{% endif %}</h2>
|
||||
<div>
|
||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||||
<button type="submit" form="source-form" class="btn btn-primary btn-action">Сохранить</button>
|
||||
{% if object %}
|
||||
<a href="{% url 'mainapp:source_delete' object.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}"
|
||||
class="btn btn-danger btn-action">Удалить</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<a href="{% url 'mainapp:source_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}"
|
||||
class="btn btn-secondary btn-action">Назад</a>
|
||||
</div>
|
||||
@@ -331,6 +333,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if object %}
|
||||
<!-- Привязанные объекты -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-header">
|
||||
@@ -416,6 +419,7 @@
|
||||
<p class="text-muted">Нет привязанных объектов</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="d-flex gap-2">
|
||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||||
<a href="{% url 'mainapp:source_create' %}" class="btn btn-success btn-sm" title="Создать новый источник">
|
||||
<i class="bi bi-plus-circle"></i> Создать
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'mainapp:load_excel_data' %}" class="btn btn-primary btn-sm" title="Загрузка данных из Excel">
|
||||
<i class="bi bi-file-earmark-excel"></i> Excel
|
||||
</a>
|
||||
|
||||
@@ -61,6 +61,9 @@
|
||||
<a href="{% url 'mainapp:transponder_create' %}" class="btn btn-success btn-sm" title="Создать">
|
||||
<i class="bi bi-plus-circle"></i> Создать
|
||||
</a>
|
||||
<a href="{% url 'mainapp:add_trans' %}" class="btn btn-warning btn-sm" title="Загрузить из XML">
|
||||
<i class="bi bi-upload"></i> Загрузить XML
|
||||
</a>
|
||||
<button type="button" class="btn btn-danger btn-sm" title="Удалить"
|
||||
onclick="deleteSelectedTransponders()">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
|
||||
@@ -41,6 +41,7 @@ from .views import (
|
||||
ShowSourceWithPointsMapView,
|
||||
ShowSourceAveragingStepsMapView,
|
||||
SourceListView,
|
||||
SourceCreateView,
|
||||
SourceUpdateView,
|
||||
SourceDeleteView,
|
||||
SourceObjItemsAPIView,
|
||||
@@ -64,6 +65,7 @@ urlpatterns = [
|
||||
path('home/', RedirectView.as_view(pattern_name='mainapp:source_list', permanent=True), name='home_redirect'),
|
||||
# Keep /sources/ as an alias (Requirement 1.2)
|
||||
path('sources/', SourceListView.as_view(), name='source_list'),
|
||||
path('source/create/', SourceCreateView.as_view(), name='source_create'),
|
||||
path('source/<int:pk>/edit/', SourceUpdateView.as_view(), name='source_update'),
|
||||
path('source/<int:pk>/delete/', SourceDeleteView.as_view(), name='source_delete'),
|
||||
path('delete-selected-sources/', DeleteSelectedSourcesView.as_view(), name='delete_selected_sources'),
|
||||
|
||||
@@ -271,34 +271,40 @@ def _find_or_create_source_by_name_and_distance(
|
||||
return source
|
||||
|
||||
|
||||
def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
|
||||
def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None, is_automatic=False):
|
||||
"""
|
||||
Импортирует данные из DataFrame с группировкой по имени источника и расстоянию.
|
||||
|
||||
Алгоритм:
|
||||
1. Для каждой строки DataFrame:
|
||||
a. Извлечь имя источника (из колонки "Объект наблюдения")
|
||||
b. Найти подходящий Source:
|
||||
- Ищет все Source с таким же именем и спутником
|
||||
- Проверяет расстояние до каждого Source
|
||||
- Если найден Source в радиусе ≤56 км - использует его
|
||||
- Иначе создает новый Source
|
||||
c. Обновить coords_average инкрементально
|
||||
d. Создать ObjItem и связать с Source
|
||||
b. Если is_automatic=False:
|
||||
- Найти подходящий Source:
|
||||
* Ищет все Source с таким же именем и спутником
|
||||
* Проверяет расстояние до каждого Source
|
||||
* Если найден Source в радиусе ≤56 км - использует его
|
||||
* Иначе создает новый Source
|
||||
- Обновить coords_average инкрементально
|
||||
- Создать ObjItem и связать с Source
|
||||
c. Если is_automatic=True:
|
||||
- Создать ObjItem без связи с Source (source=None)
|
||||
- Точка просто хранится в базе
|
||||
|
||||
Важные правила:
|
||||
- Источники разных спутников НЕ объединяются
|
||||
- Может быть несколько Source с одинаковым именем, но разделенных географически
|
||||
- Точка добавляется к Source только если расстояние ≤56 км
|
||||
- Координаты усредняются инкрементально для каждого источника
|
||||
- Если точка уже существует (по координатам и времени ГЛ), она не добавляется повторно
|
||||
|
||||
Args:
|
||||
df: DataFrame с данными
|
||||
sat: объект Satellite
|
||||
current_user: текущий пользователь (optional)
|
||||
is_automatic: если True, точки не добавляются к Source (optional, default=False)
|
||||
|
||||
Returns:
|
||||
int: количество созданных Source
|
||||
int: количество созданных Source (или 0 если is_automatic=True)
|
||||
"""
|
||||
try:
|
||||
df.rename(columns={"Модуляция ": "Модуляция"}, inplace=True)
|
||||
@@ -311,6 +317,7 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
|
||||
user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
|
||||
new_sources_count = 0
|
||||
added_count = 0
|
||||
skipped_count = 0
|
||||
|
||||
# Словарь для кэширования Source в рамках текущего импорта
|
||||
# Ключ: (имя источника, id Source), Значение: объект Source
|
||||
@@ -325,42 +332,59 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
|
||||
# Извлекаем имя источника
|
||||
source_name = row["Объект наблюдения"]
|
||||
|
||||
found_in_cache = False
|
||||
for cache_key, cached_source in sources_cache.items():
|
||||
cached_name, cached_id = cache_key
|
||||
# Извлекаем время для проверки дубликатов
|
||||
date = row["Дата"].date()
|
||||
time_ = row["Время"]
|
||||
if isinstance(time_, str):
|
||||
time_ = time_.strip()
|
||||
time_ = time(0, 0, 0)
|
||||
timestamp = datetime.combine(date, time_)
|
||||
|
||||
# Проверяем имя
|
||||
if cached_name != source_name:
|
||||
continue
|
||||
# Проверяем дубликаты по координатам и времени
|
||||
if _is_duplicate_by_coords_and_time(coord_tuple, timestamp):
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
# Проверяем расстояние
|
||||
if cached_source.coords_average:
|
||||
source_coord = (cached_source.coords_average.x, cached_source.coords_average.y)
|
||||
_, distance = calculate_mean_coords(source_coord, coord_tuple)
|
||||
source = None
|
||||
|
||||
if distance <= RANGE_DISTANCE:
|
||||
# Нашли подходящий Source в кэше
|
||||
cached_source.update_coords_average(coord_tuple)
|
||||
cached_source.save()
|
||||
source = cached_source
|
||||
found_in_cache = True
|
||||
break
|
||||
# Если is_automatic=False, работаем с Source
|
||||
if not is_automatic:
|
||||
found_in_cache = False
|
||||
for cache_key, cached_source in sources_cache.items():
|
||||
cached_name, cached_id = cache_key
|
||||
|
||||
if not found_in_cache:
|
||||
# Ищем в БД или создаем новый Source
|
||||
source = _find_or_create_source_by_name_and_distance(
|
||||
source_name, sat, coord_tuple, user_to_use
|
||||
)
|
||||
# Проверяем имя
|
||||
if cached_name != source_name:
|
||||
continue
|
||||
|
||||
# Проверяем, был ли создан новый Source
|
||||
if source.created_at.timestamp() > (datetime.now().timestamp() - 1):
|
||||
new_sources_count += 1
|
||||
# Проверяем расстояние
|
||||
if cached_source.coords_average:
|
||||
source_coord = (cached_source.coords_average.x, cached_source.coords_average.y)
|
||||
_, distance = calculate_mean_coords(source_coord, coord_tuple)
|
||||
|
||||
# Добавляем в кэш
|
||||
sources_cache[(source_name, source.id)] = source
|
||||
if distance <= RANGE_DISTANCE:
|
||||
# Нашли подходящий Source в кэше
|
||||
cached_source.update_coords_average(coord_tuple)
|
||||
cached_source.save()
|
||||
source = cached_source
|
||||
found_in_cache = True
|
||||
break
|
||||
|
||||
# Создаем ObjItem и связываем с Source
|
||||
_create_objitem_from_row(row, sat, source, user_to_use, consts)
|
||||
if not found_in_cache:
|
||||
# Ищем в БД или создаем новый Source
|
||||
source = _find_or_create_source_by_name_and_distance(
|
||||
source_name, sat, coord_tuple, user_to_use
|
||||
)
|
||||
|
||||
# Проверяем, был ли создан новый Source
|
||||
if source.created_at.timestamp() > (datetime.now().timestamp() - 1):
|
||||
new_sources_count += 1
|
||||
|
||||
# Добавляем в кэш
|
||||
sources_cache[(source_name, source.id)] = source
|
||||
|
||||
# Создаем ObjItem (с Source или без, в зависимости от is_automatic)
|
||||
_create_objitem_from_row(row, sat, source, user_to_use, consts, is_automatic)
|
||||
added_count += 1
|
||||
|
||||
except Exception as e:
|
||||
@@ -368,21 +392,22 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
|
||||
continue
|
||||
|
||||
print(f"Импорт завершен: создано {new_sources_count} новых источников, "
|
||||
f"добавлено {added_count} точек")
|
||||
f"добавлено {added_count} точек, пропущено {skipped_count} дубликатов")
|
||||
|
||||
return new_sources_count
|
||||
|
||||
|
||||
def _create_objitem_from_row(row, sat, source, user_to_use, consts):
|
||||
def _create_objitem_from_row(row, sat, source, user_to_use, consts, is_automatic=False):
|
||||
"""
|
||||
Вспомогательная функция для создания ObjItem из строки DataFrame.
|
||||
|
||||
Args:
|
||||
row: строка DataFrame
|
||||
sat: объект Satellite
|
||||
source: объект Source для связи
|
||||
source: объект Source для связи (может быть None если is_automatic=True)
|
||||
user_to_use: пользователь для created_by
|
||||
consts: константы из get_all_constants()
|
||||
is_automatic: если True, точка не связывается с Source
|
||||
"""
|
||||
# Извлекаем координату
|
||||
geo_point = Point(coords_transform(row["Координаты"]), srid=4326)
|
||||
@@ -475,12 +500,13 @@ def _create_objitem_from_row(row, sat, source, user_to_use, consts):
|
||||
# Находим подходящий источник LyngSat (точность 0.1 МГц)
|
||||
lyngsat_source = find_matching_lyngsat(sat, freq, polarization_obj, tolerance_mhz=0.1)
|
||||
|
||||
# Создаем новый ObjItem и связываем с Source, Transponder и LyngSat
|
||||
# Создаем новый ObjItem и связываем с Source (если не автоматическая), Transponder и LyngSat
|
||||
obj_item = ObjItem.objects.create(
|
||||
name=source_name,
|
||||
source=source,
|
||||
source=source if not is_automatic else None,
|
||||
transponder=transponder,
|
||||
lyngsat_source=lyngsat_source,
|
||||
is_automatic=is_automatic,
|
||||
created_by=user_to_use
|
||||
)
|
||||
|
||||
@@ -500,9 +526,10 @@ def _create_objitem_from_row(row, sat, source, user_to_use, consts):
|
||||
geo.objitem = obj_item
|
||||
geo.save()
|
||||
|
||||
# Обновляем дату подтверждения источника
|
||||
source.update_confirm_at()
|
||||
source.save()
|
||||
# Обновляем дату подтверждения источника (только если не автоматическая)
|
||||
if source and not is_automatic:
|
||||
source.update_confirm_at()
|
||||
source.save()
|
||||
|
||||
|
||||
def add_satellite_list():
|
||||
@@ -572,34 +599,40 @@ def get_point_from_json(filepath: str):
|
||||
)
|
||||
|
||||
|
||||
def get_points_from_csv(file_content, current_user=None):
|
||||
def get_points_from_csv(file_content, current_user=None, is_automatic=False):
|
||||
"""
|
||||
Импортирует данные из CSV с группировкой по имени источника и расстоянию.
|
||||
|
||||
Алгоритм:
|
||||
1. Для каждой строки CSV:
|
||||
a. Извлечь имя источника (из колонки "obj") и спутник
|
||||
b. Проверить дубликаты (координаты + частота)
|
||||
c. Найти подходящий Source:
|
||||
- Ищет все Source с таким же именем и спутником
|
||||
- Проверяет расстояние до каждого Source
|
||||
- Если найден Source в радиусе ≤56 км - использует его
|
||||
- Иначе создает новый Source
|
||||
d. Обновить coords_average инкрементально
|
||||
e. Создать ObjItem и связать с Source
|
||||
b. Проверить дубликаты (координаты + время ГЛ)
|
||||
c. Если is_automatic=False:
|
||||
- Найти подходящий Source:
|
||||
* Ищет все Source с таким же именем и спутником
|
||||
* Проверяет расстояние до каждого Source
|
||||
* Если найден Source в радиусе ≤56 км - использует его
|
||||
* Иначе создает новый Source
|
||||
- Обновить coords_average инкрементально
|
||||
- Создать ObjItem и связать с Source
|
||||
d. Если is_automatic=True:
|
||||
- Создать ObjItem без связи с Source (source=None)
|
||||
- Точка просто хранится в базе
|
||||
|
||||
Важные правила:
|
||||
- Источники разных спутников НЕ объединяются
|
||||
- Может быть несколько Source с одинаковым именем, но разделенных географически
|
||||
- Точка добавляется к Source только если расстояние ≤56 км
|
||||
- Координаты усредняются инкрементально для каждого источника
|
||||
- Если точка уже существует (по координатам и времени ГЛ), она не добавляется повторно
|
||||
|
||||
Args:
|
||||
file_content: содержимое CSV файла
|
||||
current_user: текущий пользователь (optional)
|
||||
is_automatic: если True, точки не добавляются к Source (optional, default=False)
|
||||
|
||||
Returns:
|
||||
int: количество созданных Source
|
||||
int: количество созданных Source (или 0 если is_automatic=True)
|
||||
"""
|
||||
df = pd.read_csv(
|
||||
io.StringIO(file_content),
|
||||
@@ -647,8 +680,11 @@ def get_points_from_csv(file_content, current_user=None):
|
||||
source_name = row["obj"]
|
||||
sat_name = row["sat"]
|
||||
|
||||
# Проверяем дубликаты
|
||||
if _is_duplicate_objitem(coord_tuple, row["freq"], row["f_range"]):
|
||||
# Извлекаем время для проверки дубликатов
|
||||
timestamp = row["time"]
|
||||
|
||||
# Проверяем дубликаты по координатам и времени
|
||||
if _is_duplicate_by_coords_and_time(coord_tuple, timestamp):
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
@@ -657,43 +693,47 @@ def get_points_from_csv(file_content, current_user=None):
|
||||
name=sat_name, defaults={"norad": row["norad_id"]}
|
||||
)
|
||||
|
||||
# Проверяем кэш: ищем подходящий Source среди закэшированных
|
||||
found_in_cache = False
|
||||
for cache_key, cached_source in sources_cache.items():
|
||||
cached_name, cached_sat, cached_id = cache_key
|
||||
source = None
|
||||
|
||||
# Проверяем имя и спутник
|
||||
if cached_name != source_name or cached_sat != sat_name:
|
||||
continue
|
||||
# Если is_automatic=False, работаем с Source
|
||||
if not is_automatic:
|
||||
# Проверяем кэш: ищем подходящий Source среди закэшированных
|
||||
found_in_cache = False
|
||||
for cache_key, cached_source in sources_cache.items():
|
||||
cached_name, cached_sat, cached_id = cache_key
|
||||
|
||||
# Проверяем расстояние
|
||||
if cached_source.coords_average:
|
||||
source_coord = (cached_source.coords_average.x, cached_source.coords_average.y)
|
||||
_, distance = calculate_mean_coords(source_coord, coord_tuple)
|
||||
# Проверяем имя и спутник
|
||||
if cached_name != source_name or cached_sat != sat_name:
|
||||
continue
|
||||
|
||||
if distance <= RANGE_DISTANCE:
|
||||
# Нашли подходящий Source в кэше
|
||||
cached_source.update_coords_average(coord_tuple)
|
||||
cached_source.save()
|
||||
source = cached_source
|
||||
found_in_cache = True
|
||||
break
|
||||
# Проверяем расстояние
|
||||
if cached_source.coords_average:
|
||||
source_coord = (cached_source.coords_average.x, cached_source.coords_average.y)
|
||||
_, distance = calculate_mean_coords(source_coord, coord_tuple)
|
||||
|
||||
if not found_in_cache:
|
||||
# Ищем в БД или создаем новый Source
|
||||
source = _find_or_create_source_by_name_and_distance(
|
||||
source_name, sat_obj, coord_tuple, user_to_use
|
||||
)
|
||||
if distance <= RANGE_DISTANCE:
|
||||
# Нашли подходящий Source в кэше
|
||||
cached_source.update_coords_average(coord_tuple)
|
||||
cached_source.save()
|
||||
source = cached_source
|
||||
found_in_cache = True
|
||||
break
|
||||
|
||||
# Проверяем, был ли создан новый Source
|
||||
if source.created_at.timestamp() > (datetime.now().timestamp() - 1):
|
||||
new_sources_count += 1
|
||||
if not found_in_cache:
|
||||
# Ищем в БД или создаем новый Source
|
||||
source = _find_or_create_source_by_name_and_distance(
|
||||
source_name, sat_obj, coord_tuple, user_to_use
|
||||
)
|
||||
|
||||
# Добавляем в кэш
|
||||
sources_cache[(source_name, sat_name, source.id)] = source
|
||||
# Проверяем, был ли создан новый Source
|
||||
if source.created_at.timestamp() > (datetime.now().timestamp() - 1):
|
||||
new_sources_count += 1
|
||||
|
||||
# Создаем ObjItem и связываем с Source
|
||||
_create_objitem_from_csv_row(row, source, user_to_use)
|
||||
# Добавляем в кэш
|
||||
sources_cache[(source_name, sat_name, source.id)] = source
|
||||
|
||||
# Создаем ObjItem (с Source или без, в зависимости от is_automatic)
|
||||
_create_objitem_from_csv_row(row, source, user_to_use, is_automatic)
|
||||
added_count += 1
|
||||
|
||||
except Exception as e:
|
||||
@@ -706,6 +746,39 @@ def get_points_from_csv(file_content, current_user=None):
|
||||
return new_sources_count
|
||||
|
||||
|
||||
def _is_duplicate_by_coords_and_time(coord_tuple, timestamp, tolerance_km=0.1):
|
||||
"""
|
||||
Проверяет, существует ли уже ObjItem с такими же координатами и временем ГЛ.
|
||||
|
||||
Args:
|
||||
coord_tuple: кортеж (lon, lat) координат
|
||||
timestamp: время ГЛ (datetime)
|
||||
tolerance_km: допуск для сравнения координат в километрах (default=0.1)
|
||||
|
||||
Returns:
|
||||
bool: True если дубликат найден, False иначе
|
||||
"""
|
||||
# Ищем Geo с таким же timestamp и близкими координатами
|
||||
existing_geo = Geo.objects.filter(
|
||||
timestamp=timestamp,
|
||||
coords__isnull=False
|
||||
)
|
||||
|
||||
for geo in existing_geo:
|
||||
if not geo.coords:
|
||||
continue
|
||||
|
||||
# Проверяем расстояние между координатами
|
||||
geo_coord = (geo.coords.x, geo.coords.y)
|
||||
_, distance = calculate_mean_coords(coord_tuple, geo_coord)
|
||||
|
||||
if distance <= tolerance_km:
|
||||
# Найден дубликат
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _is_duplicate_objitem(coord_tuple, frequency, freq_range, tolerance=0.1):
|
||||
"""
|
||||
Проверяет, существует ли уже ObjItem с такими же координатами и частотой.
|
||||
@@ -744,14 +817,15 @@ def _is_duplicate_objitem(coord_tuple, frequency, freq_range, tolerance=0.1):
|
||||
return False
|
||||
|
||||
|
||||
def _create_objitem_from_csv_row(row, source, user_to_use):
|
||||
def _create_objitem_from_csv_row(row, source, user_to_use, is_automatic=False):
|
||||
"""
|
||||
Вспомогательная функция для создания ObjItem из строки CSV DataFrame.
|
||||
|
||||
Args:
|
||||
row: строка DataFrame
|
||||
source: объект Source для связи
|
||||
source: объект Source для связи (может быть None если is_automatic=True)
|
||||
user_to_use: пользователь для created_by
|
||||
is_automatic: если True, точка не связывается с Source
|
||||
"""
|
||||
# Определяем поляризацию
|
||||
match row["obj"].split(" ")[-1]:
|
||||
@@ -817,12 +891,13 @@ def _create_objitem_from_csv_row(row, source, user_to_use):
|
||||
# Находим подходящий источник LyngSat (точность 0.1 МГц)
|
||||
lyngsat_source = find_matching_lyngsat(sat_obj, row["freq"], pol_obj, tolerance_mhz=0.1)
|
||||
|
||||
# Создаем новый ObjItem и связываем с Source, Transponder и LyngSat
|
||||
# Создаем новый ObjItem и связываем с Source (если не автоматическая), Transponder и LyngSat
|
||||
obj_item = ObjItem.objects.create(
|
||||
name=row["obj"],
|
||||
source=source,
|
||||
source=source if not is_automatic else None,
|
||||
transponder=transponder,
|
||||
lyngsat_source=lyngsat_source,
|
||||
is_automatic=is_automatic,
|
||||
created_by=user_to_use
|
||||
)
|
||||
|
||||
@@ -839,9 +914,10 @@ def _create_objitem_from_csv_row(row, source, user_to_use):
|
||||
geo_obj.objitem = obj_item
|
||||
geo_obj.save()
|
||||
|
||||
# Обновляем дату подтверждения источника
|
||||
source.update_confirm_at()
|
||||
source.save()
|
||||
# Обновляем дату подтверждения источника (только если не автоматическая)
|
||||
if source and not is_automatic:
|
||||
source.update_confirm_at()
|
||||
source.save()
|
||||
|
||||
|
||||
def get_vch_load_from_html(file, sat: Satellite) -> None:
|
||||
|
||||
@@ -34,7 +34,7 @@ from .lyngsat import (
|
||||
ClearLyngsatCacheView,
|
||||
UnlinkAllLyngsatSourcesView,
|
||||
)
|
||||
from .source import SourceListView, SourceUpdateView, SourceDeleteView, DeleteSelectedSourcesView
|
||||
from .source import SourceListView, SourceCreateView, SourceUpdateView, SourceDeleteView, DeleteSelectedSourcesView
|
||||
from .transponder import (
|
||||
TransponderListView,
|
||||
TransponderCreateView,
|
||||
@@ -97,6 +97,7 @@ __all__ = [
|
||||
'UnlinkAllLyngsatSourcesView',
|
||||
# Source
|
||||
'SourceListView',
|
||||
'SourceCreateView',
|
||||
'SourceUpdateView',
|
||||
'SourceDeleteView',
|
||||
'DeleteSelectedSourcesView',
|
||||
|
||||
@@ -78,6 +78,7 @@ class LoadExcelDataView(LoginRequiredMixin, FormMessageMixin, FormView):
|
||||
uploaded_file = self.request.FILES["file"]
|
||||
selected_sat = form.cleaned_data["sat_choice"]
|
||||
number = form.cleaned_data["number_input"]
|
||||
is_automatic = form.cleaned_data.get("is_automatic", False)
|
||||
|
||||
try:
|
||||
import io
|
||||
@@ -85,11 +86,16 @@ class LoadExcelDataView(LoginRequiredMixin, FormMessageMixin, FormView):
|
||||
df = pd.read_excel(io.BytesIO(uploaded_file.read()))
|
||||
if number > 0:
|
||||
df = df.head(number)
|
||||
result = fill_data_from_df(df, selected_sat, self.request.user.customuser)
|
||||
result = fill_data_from_df(df, selected_sat, self.request.user.customuser, is_automatic)
|
||||
|
||||
messages.success(
|
||||
self.request, f"Данные успешно загружены! Обработано строк: {result}"
|
||||
)
|
||||
if is_automatic:
|
||||
messages.success(
|
||||
self.request, f"Данные успешно загружены как автоматические! Добавлено точек: {len(df)}"
|
||||
)
|
||||
else:
|
||||
messages.success(
|
||||
self.request, f"Данные успешно загружены! Создано источников: {result}"
|
||||
)
|
||||
except Exception as e:
|
||||
messages.error(self.request, f"Ошибка при обработке файла: {str(e)}")
|
||||
|
||||
@@ -109,12 +115,19 @@ class LoadCsvDataView(LoginRequiredMixin, FormMessageMixin, FormView):
|
||||
|
||||
def form_valid(self, form):
|
||||
uploaded_file = self.request.FILES["file"]
|
||||
is_automatic = form.cleaned_data.get("is_automatic", False)
|
||||
|
||||
try:
|
||||
content = uploaded_file.read()
|
||||
if isinstance(content, bytes):
|
||||
content = content.decode("utf-8")
|
||||
|
||||
get_points_from_csv(content, self.request.user.customuser)
|
||||
result = get_points_from_csv(content, self.request.user.customuser, is_automatic)
|
||||
|
||||
if is_automatic:
|
||||
messages.success(self.request, "Данные успешно загружены как автоматические!")
|
||||
else:
|
||||
messages.success(self.request, f"Данные успешно загружены! Создано источников: {result}")
|
||||
except Exception as e:
|
||||
messages.error(self.request, f"Ошибка при обработке файла: {str(e)}")
|
||||
return redirect("mainapp:load_csv_data")
|
||||
|
||||
@@ -286,6 +286,13 @@ class ObjItemListView(LoginRequiredMixin, View):
|
||||
elif has_sigma == "0":
|
||||
objects = objects.filter(parameter_obj__sigma_parameter__isnull=True)
|
||||
|
||||
# Filter by is_automatic
|
||||
is_automatic_filter = request.GET.get("is_automatic")
|
||||
if is_automatic_filter == "1":
|
||||
objects = objects.filter(is_automatic=True)
|
||||
elif is_automatic_filter == "0":
|
||||
objects = objects.filter(is_automatic=False)
|
||||
|
||||
if search_query:
|
||||
search_query = search_query.strip()
|
||||
if search_query:
|
||||
@@ -336,6 +343,8 @@ class ObjItemListView(LoginRequiredMixin, View):
|
||||
"-polarization": "-first_param_pol_name",
|
||||
"modulation": "first_param_mod_name",
|
||||
"-modulation": "-first_param_mod_name",
|
||||
"is_automatic": "is_automatic",
|
||||
"-is_automatic": "-is_automatic",
|
||||
}
|
||||
|
||||
# Apply sorting if valid, otherwise use default
|
||||
@@ -467,6 +476,7 @@ class ObjItemListView(LoginRequiredMixin, View):
|
||||
"has_sigma": has_sigma,
|
||||
"sigma_info": sigma_info,
|
||||
"mirrors": ", ".join(mirrors_list) if mirrors_list else "-",
|
||||
"is_automatic": "Да" if obj.is_automatic else "Нет",
|
||||
"obj": obj,
|
||||
}
|
||||
)
|
||||
@@ -477,6 +487,7 @@ class ObjItemListView(LoginRequiredMixin, View):
|
||||
# Get the new filter values
|
||||
has_source_type = request.GET.get("has_source_type")
|
||||
has_sigma = request.GET.get("has_sigma")
|
||||
is_automatic_filter = request.GET.get("is_automatic")
|
||||
|
||||
context = {
|
||||
"satellites": satellites,
|
||||
@@ -509,6 +520,7 @@ class ObjItemListView(LoginRequiredMixin, View):
|
||||
"date_to": date_to,
|
||||
"has_source_type": has_source_type,
|
||||
"has_sigma": has_sigma,
|
||||
"is_automatic": is_automatic_filter,
|
||||
"modulations": modulations,
|
||||
"polarizations": polarizations,
|
||||
"full_width_page": True,
|
||||
@@ -679,6 +691,10 @@ class ObjItemCreateView(ObjItemFormView, CreateView):
|
||||
|
||||
success_message = "Объект успешно создан!"
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
"""Return None for create view."""
|
||||
return None
|
||||
|
||||
def set_user_fields(self):
|
||||
self.object.created_by = self.request.user.customuser
|
||||
self.object.updated_by = self.request.user.customuser
|
||||
|
||||
@@ -752,6 +752,48 @@ class AdminModeratorMixin(UserPassesTestMixin):
|
||||
return redirect('mainapp:source_list')
|
||||
|
||||
|
||||
class SourceCreateView(LoginRequiredMixin, AdminModeratorMixin, View):
|
||||
"""View for creating new Source."""
|
||||
|
||||
def get(self, request):
|
||||
form = SourceForm()
|
||||
|
||||
context = {
|
||||
'object': None,
|
||||
'form': form,
|
||||
'objitems': [],
|
||||
'full_width_page': True,
|
||||
}
|
||||
|
||||
return render(request, 'mainapp/source_form.html', context)
|
||||
|
||||
def post(self, request):
|
||||
form = SourceForm(request.POST)
|
||||
|
||||
if form.is_valid():
|
||||
source = form.save(commit=False)
|
||||
# Set created_by and updated_by to current user
|
||||
if hasattr(request.user, 'customuser'):
|
||||
source.created_by = request.user.customuser
|
||||
source.updated_by = request.user.customuser
|
||||
source.save()
|
||||
|
||||
messages.success(request, f'Источник #{source.id} успешно создан.')
|
||||
|
||||
# Redirect to edit page
|
||||
return redirect('mainapp:source_update', pk=source.id)
|
||||
|
||||
# If form is invalid, re-render with errors
|
||||
context = {
|
||||
'object': None,
|
||||
'form': form,
|
||||
'objitems': [],
|
||||
'full_width_page': True,
|
||||
}
|
||||
|
||||
return render(request, 'mainapp/source_form.html', context)
|
||||
|
||||
|
||||
class SourceUpdateView(LoginRequiredMixin, AdminModeratorMixin, View):
|
||||
"""View for editing Source with 4 coordinate fields and related ObjItems."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user