Добавил формы создания и пофиксил баг с пользователями

This commit is contained in:
2025-11-24 23:47:00 +03:00
parent 1c18ae96f7
commit 7879c3d9b5
19 changed files with 448 additions and 183 deletions

View File

@@ -40,6 +40,13 @@ class LoadExcelData(forms.Form):
min_value=0, min_value=0,
widget=forms.NumberInput(attrs={"class": "form-control"}), 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): class LoadCsvData(forms.Form):
@@ -47,6 +54,13 @@ class LoadCsvData(forms.Form):
label="Выберите CSV файл", label="Выберите CSV файл",
widget=forms.FileInput(attrs={"class": "form-control", "accept": ".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): class UploadVchLoad(UploadFileForm):

View 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',
),
]

View File

@@ -728,6 +728,12 @@ class ObjItem(models.Model):
verbose_name="Транспондер", verbose_name="Транспондер",
help_text="Транспондер, с помощью которого была получена точка", help_text="Транспондер, с помощью которого была получена точка",
) )
is_automatic = models.BooleanField(
default=False,
verbose_name="Автоматическая",
db_index=True,
help_text="Если True, точка не добавляется к объектам (Source), а хранится отдельно",
)
# Метаданные # Метаданные
created_at = models.DateTimeField( created_at = models.DateTimeField(

View File

@@ -1,14 +1,17 @@
# Django imports # Django imports
from django.contrib.auth.models import User # from django.contrib.auth.models import User
from django.db.models.signals import post_save # from django.db.models.signals import post_save
from django.dispatch import receiver # from django.dispatch import receiver
# Local imports # # Local imports
from .models import CustomUser # from .models import CustomUser
@receiver(post_save, sender=User) # @receiver(post_save, sender=User)
def create_or_update_user_profile(sender, instance, created, **kwargs): # def create_or_update_user_profile(sender, instance, created, **kwargs):
if created: # if created:
CustomUser.objects.create(user=instance) # CustomUser.objects.get_or_create(user=instance)
instance.customuser.save() # else:
# # Only save if customuser exists (avoid error if it doesn't)
# if hasattr(instance, 'customuser'):
# instance.customuser.save()

View File

@@ -79,26 +79,7 @@
</div> </div>
</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 --> <!-- VCH Load Data Card -->
<div class="col-lg-6"> <div class="col-lg-6">

View File

@@ -19,6 +19,19 @@
<!-- Form fields with Bootstrap styling --> <!-- Form fields with Bootstrap styling -->
{% include 'mainapp/components/_form_field.html' with field=form.file %} {% 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"> <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> <a href="{% url 'mainapp:source_list' %}" class="btn btn-secondary me-md-2">Назад</a>
<button type="submit" class="btn btn-success">Добавить в базу</button> <button type="submit" class="btn btn-success">Добавить в базу</button>

View File

@@ -21,6 +21,19 @@
{% include 'mainapp/components/_form_field.html' with field=form.sat_choice %} {% include 'mainapp/components/_form_field.html' with field=form.sat_choice %}
{% include 'mainapp/components/_form_field.html' with field=form.number_input %} {% 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"> <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> <a href="{% url 'mainapp:source_list' %}" class="btn btn-secondary me-md-2">Назад</a>
<button type="submit" class="btn btn-primary">Добавить в базу</button> <button type="submit" class="btn btn-primary">Добавить в базу</button>

View File

@@ -3,7 +3,7 @@
{% load static leaflet_tags %} {% load static leaflet_tags %}
{% load l10n %} {% load l10n %}
{% block title %}{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать объект{% endif %}{% endblock %} {% block title %}{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать новый объект{% endif %}{% endblock %}
{% block extra_css %} {% block extra_css %}
<link rel="stylesheet" href="{% static 'css/checkbox-select-multiple.css' %}"> <link rel="stylesheet" href="{% static 'css/checkbox-select-multiple.css' %}">
@@ -144,7 +144,7 @@
<div class="container-fluid px-3"> <div class="container-fluid px-3">
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12 d-flex justify-content-between align-items-center"> <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> <div>
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %} {% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
<button type="submit" form="objitem-form" class="btn btn-primary btn-action">Сохранить</button> <button type="submit" form="objitem-form" class="btn btn-primary btn-action">Сохранить</button>
@@ -248,6 +248,7 @@
</div> </div>
</div> </div>
{% if object %}
<!-- Транспондер --> <!-- Транспондер -->
<div class="form-section"> <div class="form-section">
<div class="form-section-header"> <div class="form-section-header">
@@ -339,6 +340,7 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% endif %}
<!-- Блок с картой --> <!-- Блок с картой -->
<div class="form-section"> <div class="form-section">

View File

@@ -40,13 +40,10 @@
<!-- Action buttons bar --> <!-- Action buttons bar -->
<div class="d-flex gap-2"> <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' %} {% 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="Удалить" <button type="button" class="btn btn-danger btn-sm" title="Удалить"
onclick="deleteSelectedObjects()"> onclick="deleteSelectedObjects()">
<i class="bi bi-trash"></i> Удалить <i class="bi bi-trash"></i> Удалить
@@ -243,6 +240,23 @@
</div> </div>
</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 --> <!-- Date Filter -->
<div class="mb-2"> <div class="mb-2">
<label class="form-label">Дата ГЛ:</label> <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="Тип точки" field="" sortable=False %}
{% include 'mainapp/components/_table_header.html' with label="Sigma" 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="" sortable=False %}
{% include 'mainapp/components/_table_header.html' with label="Автоматическая?" field="is_automatic" sort=sort %}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -378,10 +393,11 @@
{% endif %} {% endif %}
</td> </td>
<td>{{ item.mirrors }}</td> <td>{{ item.mirrors }}</td>
<td>{{ item.is_automatic }}</td>
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="22" class="text-center py-4"> <td colspan="23" class="text-center py-4">
{% if selected_satellite_id %} {% if selected_satellite_id %}
Нет данных для выбранных фильтров Нет данных для выбранных фильтров
{% else %} {% else %}
@@ -612,6 +628,7 @@
setupRadioLikeCheckboxes('has_valid'); setupRadioLikeCheckboxes('has_valid');
setupRadioLikeCheckboxes('has_source_type'); setupRadioLikeCheckboxes('has_source_type');
setupRadioLikeCheckboxes('has_sigma'); setupRadioLikeCheckboxes('has_sigma');
setupRadioLikeCheckboxes('is_automatic');
// Date range quick selection functions // Date range quick selection functions
window.setDateRange = function (period) { window.setDateRange = function (period) {

View File

@@ -212,7 +212,7 @@
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<h4>Частотный план</h4> <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="frequency-plan">
<div class="chart-controls"> <div class="chart-controls">
@@ -272,19 +272,6 @@
// Transponder data from Django // Transponder data from Django
const transpondersData = {{ transponders|safe }}; 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 // Chart state
let canvas, ctx, container; let canvas, ctx, container;
let zoomLevel = 1; let zoomLevel = 1;
@@ -505,19 +492,21 @@ function renderChart() {
if (barWidth < 1) return; if (barWidth < 1) return;
const isHovered = hoveredTransponder && hoveredTransponder.transponder.name === t.name;
// Draw downlink bar // Draw downlink bar
ctx.fillStyle = downlinkColor; ctx.fillStyle = downlinkColor;
ctx.fillRect(x1, downlinkBarY, barWidth, downlinkBarHeight); ctx.fillRect(x1, downlinkBarY, barWidth, downlinkBarHeight);
// Draw border // Draw border (thicker if hovered)
ctx.strokeStyle = '#fff'; ctx.strokeStyle = isHovered ? '#000' : '#fff';
ctx.lineWidth = 1; ctx.lineWidth = isHovered ? 3 : 1;
ctx.strokeRect(x1, downlinkBarY, barWidth, downlinkBarHeight); ctx.strokeRect(x1, downlinkBarY, barWidth, downlinkBarHeight);
// Draw name if there's space // Draw name if there's space
if (barWidth > 40) { if (barWidth > 40) {
ctx.fillStyle = (pol === 'R') ? '#000' : '#fff'; ctx.fillStyle = '#fff';
ctx.font = '9px sans-serif'; ctx.font = isHovered ? 'bold 10px sans-serif' : '9px sans-serif';
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.fillText(t.name, x1 + barWidth / 2, downlinkBarY + downlinkBarHeight / 2 + 3); ctx.fillText(t.name, x1 + barWidth / 2, downlinkBarY + downlinkBarHeight / 2 + 3);
} }
@@ -529,7 +518,8 @@ function renderChart() {
width: barWidth, width: barWidth,
height: downlinkBarHeight, height: downlinkBarHeight,
transponder: t, transponder: t,
type: 'downlink' type: 'downlink',
centerX: x1 + barWidth / 2
}); });
}); });
@@ -553,19 +543,21 @@ function renderChart() {
// Skip if too small // Skip if too small
if (barWidth < 1) return; if (barWidth < 1) return;
const isHovered = hoveredTransponder && hoveredTransponder.transponder.name === t.name;
// Draw uplink bar // Draw uplink bar
ctx.fillStyle = uplinkColor; ctx.fillStyle = uplinkColor;
ctx.fillRect(x1, uplinkBarY, barWidth, uplinkBarHeight); ctx.fillRect(x1, uplinkBarY, barWidth, uplinkBarHeight);
// Draw border // Draw border (thicker if hovered)
ctx.strokeStyle = '#fff'; ctx.strokeStyle = isHovered ? '#000' : '#fff';
ctx.lineWidth = 1; ctx.lineWidth = isHovered ? 3 : 1;
ctx.strokeRect(x1, uplinkBarY, barWidth, uplinkBarHeight); ctx.strokeRect(x1, uplinkBarY, barWidth, uplinkBarHeight);
// Draw name if there's space // Draw name if there's space
if (barWidth > 40) { if (barWidth > 40) {
ctx.fillStyle = '#fff'; ctx.fillStyle = '#fff';
ctx.font = '9px sans-serif'; ctx.font = isHovered ? 'bold 10px sans-serif' : '9px sans-serif';
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.fillText(t.name, x1 + barWidth / 2, uplinkBarY + uplinkBarHeight / 2 + 3); ctx.fillText(t.name, x1 + barWidth / 2, uplinkBarY + uplinkBarHeight / 2 + 3);
} }
@@ -577,7 +569,8 @@ function renderChart() {
width: barWidth, width: barWidth,
height: uplinkBarHeight, height: uplinkBarHeight,
transponder: t, 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) { if (hoveredTransponder) {
drawConnectionLine(hoveredTransponder);
drawTooltip(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) { function drawTooltip(rectInfo) {
const t = rectInfo.transponder; const t = rectInfo.transponder;
const isUplink = rectInfo.type === 'uplink'; const isUplink = rectInfo.type === 'uplink';
@@ -639,14 +663,16 @@ function drawTooltip(rectInfo) {
const mouseX = rectInfo._mouseX || canvas.width / 2; const mouseX = rectInfo._mouseX || canvas.width / 2;
const mouseY = rectInfo._mouseY || canvas.height / 2; const mouseY = rectInfo._mouseY || canvas.height / 2;
let tooltipX = mouseX + 15; 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) { if (tooltipX + tooltipWidth > canvas.width) {
tooltipX = mouseX - tooltipWidth - 15; 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 // Draw tooltip background

View File

@@ -129,13 +129,15 @@
<div class="container-fluid px-3"> <div class="container-fluid px-3">
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12 d-flex justify-content-between align-items-center"> <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> <div>
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %} {% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
<button type="submit" form="source-form" class="btn btn-primary btn-action">Сохранить</button> <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 %}" <a href="{% url 'mainapp:source_delete' object.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}"
class="btn btn-danger btn-action">Удалить</a> class="btn btn-danger btn-action">Удалить</a>
{% endif %} {% endif %}
{% endif %}
<a href="{% url 'mainapp:source_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" <a href="{% url 'mainapp:source_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}"
class="btn btn-secondary btn-action">Назад</a> class="btn btn-secondary btn-action">Назад</a>
</div> </div>
@@ -331,6 +333,7 @@
</div> </div>
</div> </div>
{% if object %}
<!-- Привязанные объекты --> <!-- Привязанные объекты -->
<div class="form-section"> <div class="form-section">
<div class="form-section-header"> <div class="form-section-header">
@@ -416,6 +419,7 @@
<p class="text-muted">Нет привязанных объектов</p> <p class="text-muted">Нет привязанных объектов</p>
{% endif %} {% endif %}
</div> </div>
{% endif %}
</form> </form>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -74,6 +74,11 @@
<!-- Action buttons --> <!-- Action buttons -->
<div class="d-flex gap-2"> <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"> <a href="{% url 'mainapp:load_excel_data' %}" class="btn btn-primary btn-sm" title="Загрузка данных из Excel">
<i class="bi bi-file-earmark-excel"></i> Excel <i class="bi bi-file-earmark-excel"></i> Excel
</a> </a>

View File

@@ -61,6 +61,9 @@
<a href="{% url 'mainapp:transponder_create' %}" class="btn btn-success btn-sm" title="Создать"> <a href="{% url 'mainapp:transponder_create' %}" class="btn btn-success btn-sm" title="Создать">
<i class="bi bi-plus-circle"></i> Создать <i class="bi bi-plus-circle"></i> Создать
</a> </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="Удалить" <button type="button" class="btn btn-danger btn-sm" title="Удалить"
onclick="deleteSelectedTransponders()"> onclick="deleteSelectedTransponders()">
<i class="bi bi-trash"></i> Удалить <i class="bi bi-trash"></i> Удалить

View File

@@ -41,6 +41,7 @@ from .views import (
ShowSourceWithPointsMapView, ShowSourceWithPointsMapView,
ShowSourceAveragingStepsMapView, ShowSourceAveragingStepsMapView,
SourceListView, SourceListView,
SourceCreateView,
SourceUpdateView, SourceUpdateView,
SourceDeleteView, SourceDeleteView,
SourceObjItemsAPIView, SourceObjItemsAPIView,
@@ -64,6 +65,7 @@ urlpatterns = [
path('home/', RedirectView.as_view(pattern_name='mainapp:source_list', permanent=True), name='home_redirect'), path('home/', RedirectView.as_view(pattern_name='mainapp:source_list', permanent=True), name='home_redirect'),
# Keep /sources/ as an alias (Requirement 1.2) # Keep /sources/ as an alias (Requirement 1.2)
path('sources/', SourceListView.as_view(), name='source_list'), 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>/edit/', SourceUpdateView.as_view(), name='source_update'),
path('source/<int:pk>/delete/', SourceDeleteView.as_view(), name='source_delete'), path('source/<int:pk>/delete/', SourceDeleteView.as_view(), name='source_delete'),
path('delete-selected-sources/', DeleteSelectedSourcesView.as_view(), name='delete_selected_sources'), path('delete-selected-sources/', DeleteSelectedSourcesView.as_view(), name='delete_selected_sources'),

View File

@@ -271,34 +271,40 @@ def _find_or_create_source_by_name_and_distance(
return source 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 с группировкой по имени источника и расстоянию. Импортирует данные из DataFrame с группировкой по имени источника и расстоянию.
Алгоритм: Алгоритм:
1. Для каждой строки DataFrame: 1. Для каждой строки DataFrame:
a. Извлечь имя источника (из колонки "Объект наблюдения") a. Извлечь имя источника (из колонки "Объект наблюдения")
b. Найти подходящий Source: b. Если is_automatic=False:
- Ищет все Source с таким же именем и спутником - Найти подходящий Source:
- Проверяет расстояние до каждого Source * Ищет все Source с таким же именем и спутником
- Если найден Source в радиусе ≤56 км - использует его * Проверяет расстояние до каждого Source
- Иначе создает новый Source * Если найден Source в радиусе ≤56 км - использует его
c. Обновить coords_average инкрементально * Иначе создает новый Source
d. Создать ObjItem и связать с Source - Обновить coords_average инкрементально
- Создать ObjItem и связать с Source
c. Если is_automatic=True:
- Создать ObjItem без связи с Source (source=None)
- Точка просто хранится в базе
Важные правила: Важные правила:
- Источники разных спутников НЕ объединяются - Источники разных спутников НЕ объединяются
- Может быть несколько Source с одинаковым именем, но разделенных географически - Может быть несколько Source с одинаковым именем, но разделенных географически
- Точка добавляется к Source только если расстояние ≤56 км - Точка добавляется к Source только если расстояние ≤56 км
- Координаты усредняются инкрементально для каждого источника - Координаты усредняются инкрементально для каждого источника
- Если точка уже существует (по координатам и времени ГЛ), она не добавляется повторно
Args: Args:
df: DataFrame с данными df: DataFrame с данными
sat: объект Satellite sat: объект Satellite
current_user: текущий пользователь (optional) current_user: текущий пользователь (optional)
is_automatic: если True, точки не добавляются к Source (optional, default=False)
Returns: Returns:
int: количество созданных Source int: количество созданных Source (или 0 если is_automatic=True)
""" """
try: try:
df.rename(columns={"Модуляция ": "Модуляция"}, inplace=True) 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) user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
new_sources_count = 0 new_sources_count = 0
added_count = 0 added_count = 0
skipped_count = 0
# Словарь для кэширования Source в рамках текущего импорта # Словарь для кэширования Source в рамках текущего импорта
# Ключ: (имя источника, id Source), Значение: объект Source # Ключ: (имя источника, id Source), Значение: объект Source
@@ -325,42 +332,59 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
# Извлекаем имя источника # Извлекаем имя источника
source_name = row["Объект наблюдения"] source_name = row["Объект наблюдения"]
found_in_cache = False # Извлекаем время для проверки дубликатов
for cache_key, cached_source in sources_cache.items(): date = row["Дата"].date()
cached_name, cached_id = cache_key time_ = row["Время"]
if isinstance(time_, str):
time_ = time_.strip()
time_ = time(0, 0, 0)
timestamp = datetime.combine(date, time_)
# Проверяем имя # Проверяем дубликаты по координатам и времени
if cached_name != source_name: if _is_duplicate_by_coords_and_time(coord_tuple, timestamp):
continue skipped_count += 1
continue
# Проверяем расстояние source = None
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 distance <= RANGE_DISTANCE: # Если is_automatic=False, работаем с Source
# Нашли подходящий Source в кэше if not is_automatic:
cached_source.update_coords_average(coord_tuple) found_in_cache = False
cached_source.save() for cache_key, cached_source in sources_cache.items():
source = cached_source cached_name, cached_id = cache_key
found_in_cache = True
break
if not found_in_cache: # Проверяем имя
# Ищем в БД или создаем новый Source if cached_name != source_name:
source = _find_or_create_source_by_name_and_distance( continue
source_name, sat, coord_tuple, user_to_use
)
# Проверяем, был ли создан новый Source # Проверяем расстояние
if source.created_at.timestamp() > (datetime.now().timestamp() - 1): if cached_source.coords_average:
new_sources_count += 1 source_coord = (cached_source.coords_average.x, cached_source.coords_average.y)
_, distance = calculate_mean_coords(source_coord, coord_tuple)
# Добавляем в кэш if distance <= RANGE_DISTANCE:
sources_cache[(source_name, source.id)] = source # Нашли подходящий Source в кэше
cached_source.update_coords_average(coord_tuple)
cached_source.save()
source = cached_source
found_in_cache = True
break
# Создаем ObjItem и связываем с Source if not found_in_cache:
_create_objitem_from_row(row, sat, source, user_to_use, consts) # Ищем в БД или создаем новый 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 added_count += 1
except Exception as e: except Exception as e:
@@ -368,21 +392,22 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
continue continue
print(f"Импорт завершен: создано {new_sources_count} новых источников, " print(f"Импорт завершен: создано {new_sources_count} новых источников, "
f"добавлено {added_count} точек") f"добавлено {added_count} точек, пропущено {skipped_count} дубликатов")
return new_sources_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. Вспомогательная функция для создания ObjItem из строки DataFrame.
Args: Args:
row: строка DataFrame row: строка DataFrame
sat: объект Satellite sat: объект Satellite
source: объект Source для связи source: объект Source для связи (может быть None если is_automatic=True)
user_to_use: пользователь для created_by user_to_use: пользователь для created_by
consts: константы из get_all_constants() consts: константы из get_all_constants()
is_automatic: если True, точка не связывается с Source
""" """
# Извлекаем координату # Извлекаем координату
geo_point = Point(coords_transform(row["Координаты"]), srid=4326) 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 (точность 0.1 МГц)
lyngsat_source = find_matching_lyngsat(sat, freq, polarization_obj, tolerance_mhz=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( obj_item = ObjItem.objects.create(
name=source_name, name=source_name,
source=source, source=source if not is_automatic else None,
transponder=transponder, transponder=transponder,
lyngsat_source=lyngsat_source, lyngsat_source=lyngsat_source,
is_automatic=is_automatic,
created_by=user_to_use 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.objitem = obj_item
geo.save() geo.save()
# Обновляем дату подтверждения источника # Обновляем дату подтверждения источника (только если не автоматическая)
source.update_confirm_at() if source and not is_automatic:
source.save() source.update_confirm_at()
source.save()
def add_satellite_list(): 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 с группировкой по имени источника и расстоянию. Импортирует данные из CSV с группировкой по имени источника и расстоянию.
Алгоритм: Алгоритм:
1. Для каждой строки CSV: 1. Для каждой строки CSV:
a. Извлечь имя источника (из колонки "obj") и спутник a. Извлечь имя источника (из колонки "obj") и спутник
b. Проверить дубликаты (координаты + частота) b. Проверить дубликаты (координаты + время ГЛ)
c. Найти подходящий Source: c. Если is_automatic=False:
- Ищет все Source с таким же именем и спутником - Найти подходящий Source:
- Проверяет расстояние до каждого Source * Ищет все Source с таким же именем и спутником
- Если найден Source в радиусе ≤56 км - использует его * Проверяет расстояние до каждого Source
- Иначе создает новый Source * Если найден Source в радиусе ≤56 км - использует его
d. Обновить coords_average инкрементально * Иначе создает новый Source
e. Создать ObjItem и связать с Source - Обновить coords_average инкрементально
- Создать ObjItem и связать с Source
d. Если is_automatic=True:
- Создать ObjItem без связи с Source (source=None)
- Точка просто хранится в базе
Важные правила: Важные правила:
- Источники разных спутников НЕ объединяются - Источники разных спутников НЕ объединяются
- Может быть несколько Source с одинаковым именем, но разделенных географически - Может быть несколько Source с одинаковым именем, но разделенных географически
- Точка добавляется к Source только если расстояние ≤56 км - Точка добавляется к Source только если расстояние ≤56 км
- Координаты усредняются инкрементально для каждого источника - Координаты усредняются инкрементально для каждого источника
- Если точка уже существует (по координатам и времени ГЛ), она не добавляется повторно
Args: Args:
file_content: содержимое CSV файла file_content: содержимое CSV файла
current_user: текущий пользователь (optional) current_user: текущий пользователь (optional)
is_automatic: если True, точки не добавляются к Source (optional, default=False)
Returns: Returns:
int: количество созданных Source int: количество созданных Source (или 0 если is_automatic=True)
""" """
df = pd.read_csv( df = pd.read_csv(
io.StringIO(file_content), io.StringIO(file_content),
@@ -647,8 +680,11 @@ def get_points_from_csv(file_content, current_user=None):
source_name = row["obj"] source_name = row["obj"]
sat_name = row["sat"] 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 skipped_count += 1
continue continue
@@ -657,43 +693,47 @@ def get_points_from_csv(file_content, current_user=None):
name=sat_name, defaults={"norad": row["norad_id"]} name=sat_name, defaults={"norad": row["norad_id"]}
) )
# Проверяем кэш: ищем подходящий Source среди закэшированных source = None
found_in_cache = False
for cache_key, cached_source in sources_cache.items():
cached_name, cached_sat, cached_id = cache_key
# Проверяем имя и спутник # Если is_automatic=False, работаем с Source
if cached_name != source_name or cached_sat != sat_name: if not is_automatic:
continue # Проверяем кэш: ищем подходящий 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: if cached_name != source_name or cached_sat != sat_name:
source_coord = (cached_source.coords_average.x, cached_source.coords_average.y) continue
_, distance = calculate_mean_coords(source_coord, coord_tuple)
if distance <= RANGE_DISTANCE: # Проверяем расстояние
# Нашли подходящий Source в кэше if cached_source.coords_average:
cached_source.update_coords_average(coord_tuple) source_coord = (cached_source.coords_average.x, cached_source.coords_average.y)
cached_source.save() _, distance = calculate_mean_coords(source_coord, coord_tuple)
source = cached_source
found_in_cache = True
break
if not found_in_cache: if distance <= RANGE_DISTANCE:
# Ищем в БД или создаем новый Source # Нашли подходящий Source в кэше
source = _find_or_create_source_by_name_and_distance( cached_source.update_coords_average(coord_tuple)
source_name, sat_obj, coord_tuple, user_to_use cached_source.save()
) source = cached_source
found_in_cache = True
break
# Проверяем, был ли создан новый Source if not found_in_cache:
if source.created_at.timestamp() > (datetime.now().timestamp() - 1): # Ищем в БД или создаем новый Source
new_sources_count += 1 source = _find_or_create_source_by_name_and_distance(
source_name, sat_obj, coord_tuple, user_to_use
)
# Добавляем в кэш # Проверяем, был ли создан новый Source
sources_cache[(source_name, sat_name, source.id)] = 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 added_count += 1
except Exception as e: except Exception as e:
@@ -706,6 +746,39 @@ def get_points_from_csv(file_content, current_user=None):
return new_sources_count 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): def _is_duplicate_objitem(coord_tuple, frequency, freq_range, tolerance=0.1):
""" """
Проверяет, существует ли уже ObjItem с такими же координатами и частотой. Проверяет, существует ли уже ObjItem с такими же координатами и частотой.
@@ -744,14 +817,15 @@ def _is_duplicate_objitem(coord_tuple, frequency, freq_range, tolerance=0.1):
return False 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. Вспомогательная функция для создания ObjItem из строки CSV DataFrame.
Args: Args:
row: строка DataFrame row: строка DataFrame
source: объект Source для связи source: объект Source для связи (может быть None если is_automatic=True)
user_to_use: пользователь для created_by user_to_use: пользователь для created_by
is_automatic: если True, точка не связывается с Source
""" """
# Определяем поляризацию # Определяем поляризацию
match row["obj"].split(" ")[-1]: match row["obj"].split(" ")[-1]:
@@ -817,12 +891,13 @@ def _create_objitem_from_csv_row(row, source, user_to_use):
# Находим подходящий источник LyngSat (точность 0.1 МГц) # Находим подходящий источник LyngSat (точность 0.1 МГц)
lyngsat_source = find_matching_lyngsat(sat_obj, row["freq"], pol_obj, tolerance_mhz=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( obj_item = ObjItem.objects.create(
name=row["obj"], name=row["obj"],
source=source, source=source if not is_automatic else None,
transponder=transponder, transponder=transponder,
lyngsat_source=lyngsat_source, lyngsat_source=lyngsat_source,
is_automatic=is_automatic,
created_by=user_to_use 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.objitem = obj_item
geo_obj.save() geo_obj.save()
# Обновляем дату подтверждения источника # Обновляем дату подтверждения источника (только если не автоматическая)
source.update_confirm_at() if source and not is_automatic:
source.save() source.update_confirm_at()
source.save()
def get_vch_load_from_html(file, sat: Satellite) -> None: def get_vch_load_from_html(file, sat: Satellite) -> None:

View File

@@ -34,7 +34,7 @@ from .lyngsat import (
ClearLyngsatCacheView, ClearLyngsatCacheView,
UnlinkAllLyngsatSourcesView, UnlinkAllLyngsatSourcesView,
) )
from .source import SourceListView, SourceUpdateView, SourceDeleteView, DeleteSelectedSourcesView from .source import SourceListView, SourceCreateView, SourceUpdateView, SourceDeleteView, DeleteSelectedSourcesView
from .transponder import ( from .transponder import (
TransponderListView, TransponderListView,
TransponderCreateView, TransponderCreateView,
@@ -97,6 +97,7 @@ __all__ = [
'UnlinkAllLyngsatSourcesView', 'UnlinkAllLyngsatSourcesView',
# Source # Source
'SourceListView', 'SourceListView',
'SourceCreateView',
'SourceUpdateView', 'SourceUpdateView',
'SourceDeleteView', 'SourceDeleteView',
'DeleteSelectedSourcesView', 'DeleteSelectedSourcesView',

View File

@@ -78,6 +78,7 @@ class LoadExcelDataView(LoginRequiredMixin, FormMessageMixin, FormView):
uploaded_file = self.request.FILES["file"] uploaded_file = self.request.FILES["file"]
selected_sat = form.cleaned_data["sat_choice"] selected_sat = form.cleaned_data["sat_choice"]
number = form.cleaned_data["number_input"] number = form.cleaned_data["number_input"]
is_automatic = form.cleaned_data.get("is_automatic", False)
try: try:
import io import io
@@ -85,11 +86,16 @@ class LoadExcelDataView(LoginRequiredMixin, FormMessageMixin, FormView):
df = pd.read_excel(io.BytesIO(uploaded_file.read())) df = pd.read_excel(io.BytesIO(uploaded_file.read()))
if number > 0: if number > 0:
df = df.head(number) 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( if is_automatic:
self.request, f"Данные успешно загружены! Обработано строк: {result}" messages.success(
) self.request, f"Данные успешно загружены как автоматические! Добавлено точек: {len(df)}"
)
else:
messages.success(
self.request, f"Данные успешно загружены! Создано источников: {result}"
)
except Exception as e: except Exception as e:
messages.error(self.request, f"Ошибка при обработке файла: {str(e)}") messages.error(self.request, f"Ошибка при обработке файла: {str(e)}")
@@ -109,12 +115,19 @@ class LoadCsvDataView(LoginRequiredMixin, FormMessageMixin, FormView):
def form_valid(self, form): def form_valid(self, form):
uploaded_file = self.request.FILES["file"] uploaded_file = self.request.FILES["file"]
is_automatic = form.cleaned_data.get("is_automatic", False)
try: try:
content = uploaded_file.read() content = uploaded_file.read()
if isinstance(content, bytes): if isinstance(content, bytes):
content = content.decode("utf-8") 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: except Exception as e:
messages.error(self.request, f"Ошибка при обработке файла: {str(e)}") messages.error(self.request, f"Ошибка при обработке файла: {str(e)}")
return redirect("mainapp:load_csv_data") return redirect("mainapp:load_csv_data")

View File

@@ -286,6 +286,13 @@ class ObjItemListView(LoginRequiredMixin, View):
elif has_sigma == "0": elif has_sigma == "0":
objects = objects.filter(parameter_obj__sigma_parameter__isnull=True) 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: if search_query:
search_query = search_query.strip() search_query = search_query.strip()
if search_query: if search_query:
@@ -336,6 +343,8 @@ class ObjItemListView(LoginRequiredMixin, View):
"-polarization": "-first_param_pol_name", "-polarization": "-first_param_pol_name",
"modulation": "first_param_mod_name", "modulation": "first_param_mod_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 # Apply sorting if valid, otherwise use default
@@ -467,6 +476,7 @@ class ObjItemListView(LoginRequiredMixin, View):
"has_sigma": has_sigma, "has_sigma": has_sigma,
"sigma_info": sigma_info, "sigma_info": sigma_info,
"mirrors": ", ".join(mirrors_list) if mirrors_list else "-", "mirrors": ", ".join(mirrors_list) if mirrors_list else "-",
"is_automatic": "Да" if obj.is_automatic else "Нет",
"obj": obj, "obj": obj,
} }
) )
@@ -477,6 +487,7 @@ class ObjItemListView(LoginRequiredMixin, View):
# Get the new filter values # Get the new filter values
has_source_type = request.GET.get("has_source_type") has_source_type = request.GET.get("has_source_type")
has_sigma = request.GET.get("has_sigma") has_sigma = request.GET.get("has_sigma")
is_automatic_filter = request.GET.get("is_automatic")
context = { context = {
"satellites": satellites, "satellites": satellites,
@@ -509,6 +520,7 @@ class ObjItemListView(LoginRequiredMixin, View):
"date_to": date_to, "date_to": date_to,
"has_source_type": has_source_type, "has_source_type": has_source_type,
"has_sigma": has_sigma, "has_sigma": has_sigma,
"is_automatic": is_automatic_filter,
"modulations": modulations, "modulations": modulations,
"polarizations": polarizations, "polarizations": polarizations,
"full_width_page": True, "full_width_page": True,
@@ -679,6 +691,10 @@ class ObjItemCreateView(ObjItemFormView, CreateView):
success_message = "Объект успешно создан!" success_message = "Объект успешно создан!"
def get_object(self, queryset=None):
"""Return None for create view."""
return None
def set_user_fields(self): def set_user_fields(self):
self.object.created_by = self.request.user.customuser self.object.created_by = self.request.user.customuser
self.object.updated_by = self.request.user.customuser self.object.updated_by = self.request.user.customuser

View File

@@ -752,6 +752,48 @@ class AdminModeratorMixin(UserPassesTestMixin):
return redirect('mainapp:source_list') 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): class SourceUpdateView(LoginRequiredMixin, AdminModeratorMixin, View):
"""View for editing Source with 4 coordinate fields and related ObjItems.""" """View for editing Source with 4 coordinate fields and related ObjItems."""