Добавил форму для загрузки данных с LyngSat

This commit is contained in:
2025-11-10 23:28:06 +03:00
parent 1b345a3fd9
commit 65e6c9a323
24 changed files with 2730 additions and 308 deletions

View File

@@ -107,6 +107,40 @@ class NewEventForm(forms.Form):
'accept': '.xlsx,.xls'
})
)
class FillLyngsatDataForm(forms.Form):
"""Форма для заполнения данных из Lyngsat"""
REGION_CHOICES = [
('europe', 'Европа'),
('asia', 'Азия'),
('america', 'Америка'),
('atlantic', 'Атлантика'),
]
satellites = forms.ModelMultipleChoiceField(
queryset=Satellite.objects.all().order_by('name'),
label="Выберите спутники",
widget=forms.SelectMultiple(attrs={
'class': 'form-select',
'size': '10'
}),
required=True,
help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких спутников"
)
regions = forms.MultipleChoiceField(
choices=REGION_CHOICES,
label="Выберите регионы",
widget=forms.SelectMultiple(attrs={
'class': 'form-select',
'size': '4'
}),
required=True,
initial=['europe', 'asia', 'america', 'atlantic'],
help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких регионов"
)
class ParameterForm(forms.ModelForm):
"""
Форма для создания и редактирования параметров ВЧ загрузки.

View File

@@ -124,23 +124,23 @@
</div>
</div>
<!-- Map Views Card -->
<!-- Lyngsat Data Fill 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-secondary 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-map text-secondary" viewBox="0 0 16 16">
<path d="M15.817.113A.5.5 0 0 1 16 .5v14a.5.5 0 0 1-.402.49l-5 1a.502.502 0 0 1-.196 0L5.5 15.01l-4.902.98A.5.5 0 0 1 0 15.5v-14a.5.5 0 0 1 .402-.49l5-1a.5.5 0 0 1 .196 0L10.5.99l4.902-.98a.5.5 0 0 1 .415.103M10 1.91l-4-.8v12.98l4 .8zM1.61 2.22l4.39.88v10.88l-4.39-.88zm9.18 10.88 4-.8V2.34l-4 .8z"/>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-cloud-download text-secondary" viewBox="0 0 16 16">
<path d="M4.406 1.342A5.53 5.53 0 0 1 8 0c2.69 0 4.923 2 5.166 4.579C14.758 4.804 16 6.137 16 7.773 16 9.569 14.502 11 12.687 11H10a.5.5 0 0 1 0-1h2.688C13.979 10 15 8.988 15 7.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 2.825 10.328 1 8 1a4.53 4.53 0 0 0-2.941 1.1c-.757.652-1.153 1.438-1.153 2.055v.448l-.445.049C2.064 4.805 1 5.952 1 7.318 1 8.785 2.23 10 3.781 10H6a.5.5 0 0 1 0 1H3.781C1.708 11 0 9.366 0 7.318c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383"/>
<path d="M7.646 15.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 14.293V5.5a.5.5 0 0 0-1 0v8.793l-2.146-2.147a.5.5 0 0 0-.708.708z"/>
</svg>
</div>
<h3 class="card-title mb-0">Карты</h3>
</div>
<p class="card-text">Просматривайте данные на 2D и 3D картах для визуализации геолокации спутников.</p>
<div class="mt-2">
<a href="{% url 'mapsapp:2dmap' %}" class="btn btn-secondary me-2">2D Карта</a>
<a href="{% url 'mapsapp:3dmap' %}" class="btn btn-outline-secondary">3D Карта</a>
<h3 class="card-title mb-0">Заполнение данных Lyngsat</h3>
</div>
<p class="card-text">Загрузите данные о транспондерах спутников с сайта Lyngsat. Выберите спутники и регионы для парсинга данных.</p>
<a href="{% url 'mainapp:fill_lyngsat_data' %}" class="btn btn-secondary">
Заполнить данные Lyngsat
</a>
</div>
</div>
</div>

View File

@@ -0,0 +1,118 @@
{% extends 'mainapp/base.html' %}
{% block title %}Заполнение данных Lyngsat{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h3 class="mb-0">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-cloud-download me-2" viewBox="0 0 16 16">
<path d="M4.406 1.342A5.53 5.53 0 0 1 8 0c2.69 0 4.923 2 5.166 4.579C14.758 4.804 16 6.137 16 7.773 16 9.569 14.502 11 12.687 11H10a.5.5 0 0 1 0-1h2.688C13.979 10 15 8.988 15 7.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 2.825 10.328 1 8 1a4.53 4.53 0 0 0-2.941 1.1c-.757.652-1.153 1.438-1.153 2.055v.448l-.445.049C2.064 4.805 1 5.952 1 7.318 1 8.785 2.23 10 3.781 10H6a.5.5 0 0 1 0 1H3.781C1.708 11 0 9.366 0 7.318c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383"/>
<path d="M7.646 15.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 14.293V5.5a.5.5 0 0 0-1 0v8.793l-2.146-2.147a.5.5 0 0 0-.708.708z"/>
</svg>
Заполнение данных из Lyngsat
</h3>
</div>
<div class="card-body">
<!-- Alert messages -->
{% include 'mainapp/components/_messages.html' %}
<div class="alert alert-info" role="alert">
<strong>Внимание!</strong> Процесс заполнения данных может занять продолжительное время,
так как выполняются запросы к внешнему сайту Lyngsat. Пожалуйста, дождитесь завершения операции.
</div>
<form method="post" class="needs-validation" novalidate>
{% csrf_token %}
<!-- Satellites Selection -->
<div class="mb-4">
<label for="{{ form.satellites.id_for_label }}" class="form-label fw-bold">
{{ form.satellites.label }}
</label>
{{ form.satellites }}
{% if form.satellites.help_text %}
<div class="form-text">{{ form.satellites.help_text }}</div>
{% endif %}
{% if form.satellites.errors %}
<div class="invalid-feedback d-block">
{{ form.satellites.errors }}
</div>
{% endif %}
</div>
<!-- Regions Selection -->
<div class="mb-4">
<label for="{{ form.regions.id_for_label }}" class="form-label fw-bold">
{{ form.regions.label }}
</label>
{{ form.regions }}
{% if form.regions.help_text %}
<div class="form-text">{{ form.regions.help_text }}</div>
{% endif %}
{% if form.regions.errors %}
<div class="invalid-feedback d-block">
{{ form.regions.errors }}
</div>
{% endif %}
</div>
<!-- Buttons -->
<div class="d-grid gap-2 d-md-flex justify-content-md-between">
<a href="{% url 'mainapp:actions' %}" class="btn btn-secondary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-left me-1" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8"/>
</svg>
Назад
</a>
<button type="submit" class="btn btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-download me-1" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708z"/>
</svg>
Заполнить данные
</button>
</div>
</form>
</div>
</div>
<!-- Info Card -->
<div class="card mt-4 shadow-sm">
<div class="card-body">
<h5 class="card-title">Информация</h5>
<p class="card-text">
Эта форма позволяет загрузить данные о транспондерах спутников с сайта Lyngsat.
Выберите один или несколько спутников и регионы для парсинга данных.
</p>
<ul>
<li>Данные включают частоты, поляризацию, модуляцию, стандарты и другие параметры</li>
<li>Процесс может занять несколько минут в зависимости от количества выбранных спутников</li>
<li>Существующие записи будут обновлены, новые - созданы</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<script>
// Form validation
(function() {
'use strict';
var forms = document.querySelectorAll('.needs-validation');
Array.prototype.slice.call(forms).forEach(function(form) {
form.addEventListener('submit', function(event) {
if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
}
form.classList.add('was-validated');
}, false);
});
})();
</script>
{% endblock %}

View File

@@ -0,0 +1,241 @@
{% extends 'mainapp/base.html' %}
{% block title %}Статус задачи Lyngsat{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h3 class="mb-0">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-hourglass-split me-2" viewBox="0 0 16 16">
<path d="M2.5 15a.5.5 0 1 1 0-1h1v-1a4.5 4.5 0 0 1 2.557-4.06c.29-.139.443-.377.443-.59v-.7c0-.213-.154-.451-.443-.59A4.5 4.5 0 0 1 3.5 3V2h-1a.5.5 0 0 1 0-1h11a.5.5 0 0 1 0 1h-1v1a4.5 4.5 0 0 1-2.557 4.06c-.29.139-.443.377-.443.59v.7c0 .213.154.451.443.59A4.5 4.5 0 0 1 12.5 13v1h1a.5.5 0 0 1 0 1zm2-13v1c0 .537.12 1.045.337 1.5h6.326c.216-.455.337-.963.337-1.5V2zm3 6.35c0 .701-.478 1.236-1.011 1.492A3.5 3.5 0 0 0 4.5 13s.866-1.299 3-1.48zm1 0v3.17c2.134.181 3 1.48 3 1.48a3.5 3.5 0 0 0-1.989-3.158C8.978 9.586 8.5 9.052 8.5 8.351z"/>
</svg>
Статус задачи заполнения данных Lyngsat
</h3>
</div>
<div class="card-body">
{% if task_id %}
<div class="mb-3">
<strong>ID задачи:</strong> <code id="task-id">{{ task_id }}</code>
</div>
<!-- Progress Bar -->
<div class="mb-4">
<div class="d-flex justify-content-between mb-2">
<span id="status-text">Загрузка статуса...</span>
<span id="progress-percent">0%</span>
</div>
<div class="progress" style="height: 25px;">
<div id="progress-bar" class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar" style="width: 0%;"
aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
<span id="progress-text">0%</span>
</div>
</div>
</div>
<!-- Task State -->
<div id="task-state-container" class="alert alert-info" role="alert">
<strong>Состояние:</strong> <span id="task-state">Проверка...</span>
</div>
<!-- Results Container (hidden by default) -->
<div id="results-container" class="d-none">
<h5 class="mt-4">Результаты обработки</h5>
<div class="row">
<div class="col-md-6">
<div class="card mb-3">
<div class="card-body">
<h6 class="card-subtitle mb-2 text-muted">Обработано спутников</h6>
<h3 class="card-title" id="result-satellites">-</h3>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-3">
<div class="card-body">
<h6 class="card-subtitle mb-2 text-muted">Обработано источников</h6>
<h3 class="card-title" id="result-sources">-</h3>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-3 border-success">
<div class="card-body">
<h6 class="card-subtitle mb-2 text-success">Создано записей</h6>
<h3 class="card-title text-success" id="result-created">-</h3>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-3 border-info">
<div class="card-body">
<h6 class="card-subtitle mb-2 text-info">Обновлено записей</h6>
<h3 class="card-title text-info" id="result-updated">-</h3>
</div>
</div>
</div>
</div>
<!-- Errors -->
<div id="errors-container" class="d-none">
<h6 class="text-danger">Ошибки при обработке:</h6>
<div class="alert alert-warning">
<ul id="errors-list" class="mb-0"></ul>
</div>
</div>
</div>
<!-- Error Container (hidden by default) -->
<div id="error-container" class="alert alert-danger d-none" role="alert">
<strong>Ошибка:</strong> <span id="error-text"></span>
</div>
<!-- Action Buttons -->
<div class="d-grid gap-2 d-md-flex justify-content-md-between mt-4">
<a href="{% url 'mainapp:fill_lyngsat_data' %}" class="btn btn-secondary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-left me-1" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8"/>
</svg>
Назад к форме
</a>
<a href="{% url 'mainapp:actions' %}" class="btn btn-outline-primary" id="actions-btn">
Перейти к действиям
</a>
</div>
{% else %}
<div class="alert alert-warning" role="alert">
ID задачи не указан. Пожалуйста, запустите задачу через форму заполнения данных.
</div>
<a href="{% url 'mainapp:fill_lyngsat_data' %}" class="btn btn-primary">
Перейти к форме
</a>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% if task_id %}
<script>
let taskId = '{{ task_id }}';
let pollInterval;
let isCompleted = false;
function updateProgress(data) {
const statusText = document.getElementById('status-text');
const progressBar = document.getElementById('progress-bar');
const progressText = document.getElementById('progress-text');
const progressPercent = document.getElementById('progress-percent');
const taskState = document.getElementById('task-state');
const taskStateContainer = document.getElementById('task-state-container');
// Update state
taskState.textContent = data.state;
if (data.state === 'PENDING') {
statusText.textContent = 'Задача в очереди...';
taskStateContainer.className = 'alert alert-info';
} else if (data.state === 'PROGRESS') {
const percent = data.percent || 0;
statusText.textContent = data.status || 'Обработка...';
progressBar.style.width = percent + '%';
progressBar.setAttribute('aria-valuenow', percent);
progressText.textContent = percent + '%';
progressPercent.textContent = percent + '%';
taskStateContainer.className = 'alert alert-info';
} else if (data.state === 'SUCCESS') {
statusText.textContent = 'Задача завершена успешно!';
progressBar.style.width = '100%';
progressBar.setAttribute('aria-valuenow', 100);
progressText.textContent = '100%';
progressPercent.textContent = '100%';
progressBar.classList.remove('progress-bar-animated');
progressBar.classList.add('bg-success');
taskStateContainer.className = 'alert alert-success';
// Show results
if (data.result) {
showResults(data.result);
}
isCompleted = true;
clearInterval(pollInterval);
} else if (data.state === 'FAILURE') {
statusText.textContent = 'Ошибка при выполнении задачи';
progressBar.classList.remove('progress-bar-animated');
progressBar.classList.add('bg-danger');
taskStateContainer.className = 'alert alert-danger';
// Show error
const errorContainer = document.getElementById('error-container');
const errorText = document.getElementById('error-text');
errorText.textContent = data.error || 'Неизвестная ошибка';
errorContainer.classList.remove('d-none');
isCompleted = true;
clearInterval(pollInterval);
}
}
function showResults(result) {
const resultsContainer = document.getElementById('results-container');
resultsContainer.classList.remove('d-none');
document.getElementById('result-satellites').textContent = result.total_satellites || 0;
document.getElementById('result-sources').textContent = result.total_sources || 0;
document.getElementById('result-created').textContent = result.created || 0;
document.getElementById('result-updated').textContent = result.updated || 0;
// Show errors if any
if (result.errors && result.errors.length > 0) {
const errorsContainer = document.getElementById('errors-container');
const errorsList = document.getElementById('errors-list');
errorsContainer.classList.remove('d-none');
errorsList.innerHTML = '';
result.errors.slice(0, 10).forEach(error => {
const li = document.createElement('li');
li.textContent = error;
errorsList.appendChild(li);
});
if (result.errors.length > 10) {
const li = document.createElement('li');
li.textContent = `И еще ${result.errors.length - 10} ошибок...`;
li.className = 'text-muted';
errorsList.appendChild(li);
}
}
}
function checkTaskStatus() {
fetch(`/api/lyngsat-task-status/${taskId}/`)
.then(response => response.json())
.then(data => {
updateProgress(data);
})
.catch(error => {
console.error('Error checking task status:', error);
});
}
// Start polling
document.addEventListener('DOMContentLoaded', function() {
checkTaskStatus();
pollInterval = setInterval(checkTaskStatus, 2000); // Poll every 2 seconds
// Stop polling after 30 minutes
setTimeout(() => {
if (!isCompleted) {
clearInterval(pollInterval);
document.getElementById('status-text').textContent = 'Превышено время ожидания. Обновите страницу для проверки статуса.';
}
}, 30 * 60 * 1000);
});
</script>
{% endif %}
{% endblock %}

View File

@@ -25,4 +25,8 @@ urlpatterns = [
path('object/<int:pk>/edit/', views.ObjItemUpdateView.as_view(), name='objitem_update'),
path('object/<int:pk>/', views.ObjItemDetailView.as_view(), name='objitem_detail'),
path('object/<int:pk>/delete/', views.ObjItemDeleteView.as_view(), name='objitem_delete'),
path('fill-lyngsat-data/', views.FillLyngsatDataView.as_view(), name='fill_lyngsat_data'),
path('lyngsat-task-status/', views.LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'),
path('lyngsat-task-status/<str:task_id>/', views.LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'),
path('api/lyngsat-task-status/<str:task_id>/', views.LyngsatTaskStatusAPIView.as_view(), name='lyngsat_task_status_api'),
]

View File

@@ -37,6 +37,7 @@ from .forms import (
UploadFileForm,
UploadVchLoad,
VchLinkForm,
FillLyngsatDataForm,
)
from .mixins import CoordinateProcessingMixin, FormMessageMixin, RoleRequiredMixin
from .models import Geo, Modulation, ObjItem, Polarization, Satellite
@@ -1029,3 +1030,97 @@ class ObjItemDetailView(LoginRequiredMixin, View):
}
return render(request, "mainapp/objitem_detail.html", context)
class FillLyngsatDataView(LoginRequiredMixin, FormMessageMixin, FormView):
"""
Представление для заполнения данных из Lyngsat.
Позволяет выбрать спутники и регионы для парсинга данных с сайта Lyngsat.
Запускает асинхронную задачу Celery для обработки.
"""
template_name = "mainapp/fill_lyngsat_data.html"
form_class = FillLyngsatDataForm
success_url = reverse_lazy("mainapp:lyngsat_task_status")
error_message = "Форма заполнена некорректно"
def form_valid(self, form):
satellites = form.cleaned_data["satellites"]
regions = form.cleaned_data["regions"]
# Получаем названия спутников
target_sats = [sat.name for sat in satellites]
try:
from lyngsatapp.tasks import fill_lyngsat_data_task
# Запускаем асинхронную задачу
task = fill_lyngsat_data_task.delay(target_sats, regions)
messages.success(
self.request,
f"Задача запущена! ID задачи: {task.id}. "
"Вы будете перенаправлены на страницу отслеживания прогресса."
)
# Перенаправляем на страницу статуса задачи
return redirect('mainapp:lyngsat_task_status', task_id=task.id)
except Exception as e:
messages.error(self.request, f"Ошибка при запуске задачи: {str(e)}")
return redirect("mainapp:fill_lyngsat_data")
class LyngsatTaskStatusView(LoginRequiredMixin, View):
"""
Представление для отслеживания статуса задачи заполнения данных Lyngsat.
"""
template_name = "mainapp/lyngsat_task_status.html"
def get(self, request, task_id=None):
context = {
'task_id': task_id
}
return render(request, self.template_name, context)
class LyngsatTaskStatusAPIView(LoginRequiredMixin, View):
"""
API для получения статуса задачи Celery.
"""
def get(self, request, task_id):
from celery.result import AsyncResult
from django.core.cache import cache
task = AsyncResult(task_id)
response_data = {
'task_id': task_id,
'state': task.state,
'result': None,
'error': None
}
if task.state == 'PENDING':
response_data['status'] = 'Задача в очереди...'
elif task.state == 'PROGRESS':
response_data['status'] = task.info.get('status', '')
response_data['current'] = task.info.get('current', 0)
response_data['total'] = task.info.get('total', 1)
response_data['percent'] = int((task.info.get('current', 0) / task.info.get('total', 1)) * 100)
elif task.state == 'SUCCESS':
# Получаем результат из кеша
result = cache.get(f'lyngsat_task_{task_id}')
if result:
response_data['result'] = result
response_data['status'] = 'Задача завершена успешно'
else:
response_data['result'] = task.result
response_data['status'] = 'Задача завершена'
elif task.state == 'FAILURE':
response_data['status'] = 'Ошибка при выполнении задачи'
response_data['error'] = str(task.info)
else:
response_data['status'] = task.state
return JsonResponse(response_data)