Добавил транспондеры к ObjItem шаблону

This commit is contained in:
2025-11-14 08:00:23 +03:00
parent 5ab6770809
commit 6a26991dc0
18 changed files with 3286 additions and 1188 deletions

View File

@@ -23,6 +23,7 @@ from .api import (
SigmaParameterDataAPIView,
SourceObjItemsAPIView,
LyngsatTaskStatusAPIView,
TransponderDataAPIView,
)
from .lyngsat import (
LinkLyngsatSourcesView,
@@ -30,8 +31,14 @@ from .lyngsat import (
LyngsatTaskStatusView,
ClearLyngsatCacheView,
)
from .source import SourceListView
from .map import ShowMapView, ShowSelectedObjectsMapView, ClusterTestView
from .source import SourceListView, SourceUpdateView, SourceDeleteView
from .map import (
ShowMapView,
ShowSelectedObjectsMapView,
ShowSourcesMapView,
ShowSourceWithPointsMapView,
ClusterTestView,
)
__all__ = [
# Base
@@ -58,6 +65,7 @@ __all__ = [
'SigmaParameterDataAPIView',
'SourceObjItemsAPIView',
'LyngsatTaskStatusAPIView',
'TransponderDataAPIView',
# LyngSat
'LinkLyngsatSourcesView',
'FillLyngsatDataView',
@@ -65,8 +73,12 @@ __all__ = [
'ClearLyngsatCacheView',
# Source
'SourceListView',
'SourceUpdateView',
'SourceDeleteView',
# Map
'ShowMapView',
'ShowSelectedObjectsMapView',
'ShowSourcesMapView',
'ShowSourceWithPointsMapView',
'ClusterTestView',
]

View File

@@ -299,3 +299,34 @@ class LyngsatTaskStatusAPIView(LoginRequiredMixin, View):
response_data['status'] = task.state
return JsonResponse(response_data)
class TransponderDataAPIView(LoginRequiredMixin, View):
"""API endpoint for getting Transponder data."""
def get(self, request, transponder_id):
from mapsapp.models import Transponders
try:
transponder = Transponders.objects.select_related(
'sat_id',
'polarization'
).get(id=transponder_id)
data = {
'id': transponder.id,
'name': transponder.name or '-',
'satellite': transponder.sat_id.name if transponder.sat_id else '-',
'downlink': f"{transponder.downlink:.3f}" if transponder.downlink else '-',
'uplink': f"{transponder.uplink:.3f}" if transponder.uplink else None,
'frequency_range': f"{transponder.frequency_range:.3f}" if transponder.frequency_range else '-',
'polarization': transponder.polarization.name if transponder.polarization else '-',
'zone_name': transponder.zone_name or '-',
'transfer': f"{transponder.transfer:.3f}" if transponder.transfer else None,
}
return JsonResponse(data)
except Transponders.DoesNotExist:
return JsonResponse({'error': 'Транспондер не найден'}, status=404)
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)

View File

@@ -1,6 +1,7 @@
"""
Map related views for displaying objects on maps.
"""
from collections import defaultdict
from django.contrib.admin.views.decorators import staff_member_required
@@ -18,7 +19,7 @@ from ..models import ObjItem
@method_decorator(staff_member_required, name="dispatch")
class ShowMapView(RoleRequiredMixin, View):
"""View for displaying objects on map (admin interface)."""
required_roles = ["admin", "moderator"]
def get(self, request):
@@ -41,7 +42,7 @@ class ShowMapView(RoleRequiredMixin, View):
or not obj.geo_obj.coords
):
continue
param = getattr(obj, 'parameter_obj', None)
param = getattr(obj, "parameter_obj", None)
if not param:
continue
points.append(
@@ -53,7 +54,7 @@ class ShowMapView(RoleRequiredMixin, View):
)
else:
return redirect("admin")
grouped = defaultdict(list)
for p in points:
grouped[p["name"]].append({"point": p["point"], "frequency": p["freq"]})
@@ -71,7 +72,7 @@ class ShowMapView(RoleRequiredMixin, View):
class ShowSelectedObjectsMapView(LoginRequiredMixin, View):
"""View for displaying selected objects on map."""
def get(self, request):
ids = request.GET.get("ids", "")
points = []
@@ -92,7 +93,7 @@ class ShowSelectedObjectsMapView(LoginRequiredMixin, View):
or not obj.geo_obj.coords
):
continue
param = getattr(obj, 'parameter_obj', None)
param = getattr(obj, "parameter_obj", None)
if not param:
continue
points.append(
@@ -121,9 +122,142 @@ class ShowSelectedObjectsMapView(LoginRequiredMixin, View):
return render(request, "mainapp/objitem_map.html", context)
class ShowSourcesMapView(LoginRequiredMixin, View):
"""View for displaying selected sources on map."""
def get(self, request):
from ..models import Source
ids = request.GET.get("ids", "")
groups = []
if ids:
id_list = [int(x) for x in ids.split(",") if x.isdigit()]
sources = Source.objects.filter(id__in=id_list)
# Define coordinate types with their labels and colors
coord_types = [
("coords_average", "Усредненные координаты", "blue"),
("coords_kupsat", "Координаты Кубсата", "orange"),
("coords_valid", "Координаты оперативников", "green"),
("coords_reference", "Координаты справочные", "violet"),
]
# Group points by coordinate type
for coord_field, label, color in coord_types:
points = []
for source in sources:
coords = getattr(source, coord_field)
if coords:
# coords is a Point object with x (longitude) and y (latitude)
points.append(
{
"point": (coords.x, coords.y), # (lon, lat)
"source_id": f"Источник #{source.id}",
}
)
if points:
groups.append(
{
"name": label,
"points": points,
"color": color,
}
)
else:
return redirect("mainapp:home")
context = {
"groups": groups,
}
return render(request, "mainapp/source_map.html", context)
class ShowSourceWithPointsMapView(LoginRequiredMixin, View):
"""View for displaying a single source with all its related ObjItem points."""
def get(self, request, source_id):
from ..models import Source
try:
source = Source.objects.prefetch_related(
"source_objitems",
"source_objitems__parameter_obj",
"source_objitems__geo_obj",
).get(id=source_id)
except Source.DoesNotExist:
return redirect("mainapp:home")
groups = []
# Цвета для разных типов координат источника
source_coord_types = [
("coords_average", "Усредненные координаты", "blue"),
("coords_kupsat", "Координаты Кубсата", "orange"),
("coords_valid", "Координаты оперативников", "green"),
("coords_reference", "Координаты справочные", "violet"),
]
# Добавляем координаты источника
for coord_field, label, color in source_coord_types:
coords = getattr(source, coord_field)
if coords:
groups.append(
{
"name": label,
"points": [
{
"point": (coords.x, coords.y),
"source_id": f"Источник #{source.id}",
}
],
"color": color,
}
)
# Добавляем все точки ГЛ одной группой
gl_points = source.source_objitems.select_related(
"parameter_obj", "geo_obj"
).all()
# Собираем все точки ГЛ в одну группу
all_gl_points = []
for obj in gl_points:
if (
not hasattr(obj, "geo_obj")
or not obj.geo_obj
or not obj.geo_obj.coords
):
continue
param = getattr(obj, "parameter_obj", None)
if not param:
continue
all_gl_points.append(
{
"point": (obj.geo_obj.coords.x, obj.geo_obj.coords.y),
"name": obj.name,
"frequency": f"{param.frequency} [{param.freq_range}] МГц",
}
)
# Добавляем все точки ГЛ одним цветом (красный)
if all_gl_points:
groups.append(
{"name": "Точки ГЛ", "points": all_gl_points, "color": "red"}
)
context = {
"groups": groups,
"source_id": source_id,
}
return render(request, "mainapp/source_with_points_map.html", context)
class ClusterTestView(LoginRequiredMixin, View):
"""Test view for clustering functionality."""
def get(self, request):
objs = ObjItem.objects.filter(
name__icontains="! Astra 4A 12654,040 [1,962] МГц H"

View File

@@ -3,12 +3,15 @@ Source related views.
"""
from datetime import datetime
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.core.paginator import Paginator
from django.db.models import Count
from django.shortcuts import render
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.views import View
from ..forms import SourceForm
from ..models import Source
from ..utils import parse_pagination_params
@@ -22,8 +25,8 @@ class SourceListView(LoginRequiredMixin, View):
# Get pagination parameters
page_number, items_per_page = parse_pagination_params(request)
# Get sorting parameters
sort_param = request.GET.get("sort", "-created_at")
# Get sorting parameters (default to ID ascending)
sort_param = request.GET.get("sort", "id")
# Get filter parameters
search_query = request.GET.get("search", "").strip()
@@ -185,3 +188,117 @@ class SourceListView(LoginRequiredMixin, View):
}
return render(request, "mainapp/source_list.html", context)
class AdminModeratorMixin(UserPassesTestMixin):
"""Mixin to restrict access to admin and moderator roles only."""
def test_func(self):
return (
self.request.user.is_authenticated and
hasattr(self.request.user, 'customuser') and
self.request.user.customuser.role in ['admin', 'moderator']
)
def handle_no_permission(self):
messages.error(self.request, 'У вас нет прав для выполнения этого действия.')
return redirect('mainapp:home')
class SourceUpdateView(LoginRequiredMixin, AdminModeratorMixin, View):
"""View for editing Source with 4 coordinate fields and related ObjItems."""
def get(self, request, pk):
source = get_object_or_404(Source, pk=pk)
form = SourceForm(instance=source)
# Get related ObjItems ordered by creation date
objitems = source.source_objitems.select_related(
'parameter_obj',
'parameter_obj__id_satellite',
'parameter_obj__polarization',
'parameter_obj__modulation',
'parameter_obj__standard',
'geo_obj',
'created_by__user',
'updated_by__user'
).order_by('created_at')
context = {
'object': source,
'form': form,
'objitems': objitems,
'full_width_page': True,
}
return render(request, 'mainapp/source_form.html', context)
def post(self, request, pk):
source = get_object_or_404(Source, pk=pk)
form = SourceForm(request.POST, instance=source)
if form.is_valid():
source = form.save(commit=False)
# Set updated_by to current user
if hasattr(request.user, 'customuser'):
source.updated_by = request.user.customuser
source.save()
messages.success(request, f'Источник #{source.id} успешно обновлен.')
# Redirect back with query params if present
if request.GET.urlencode():
return redirect(f"{reverse('mainapp:source_update', args=[source.id])}?{request.GET.urlencode()}")
return redirect('mainapp:source_update', pk=source.id)
# If form is invalid, re-render with errors
objitems = source.source_objitems.select_related(
'parameter_obj',
'parameter_obj__id_satellite',
'parameter_obj__polarization',
'parameter_obj__modulation',
'parameter_obj__standard',
'geo_obj',
'created_by__user',
'updated_by__user'
).order_by('created_at')
context = {
'object': source,
'form': form,
'objitems': objitems,
'full_width_page': True,
}
return render(request, 'mainapp/source_form.html', context)
class SourceDeleteView(LoginRequiredMixin, AdminModeratorMixin, View):
"""View for deleting Source."""
def get(self, request, pk):
source = get_object_or_404(Source, pk=pk)
context = {
'object': source,
'objitems_count': source.source_objitems.count(),
}
return render(request, 'mainapp/source_confirm_delete.html', context)
def post(self, request, pk):
source = get_object_or_404(Source, pk=pk)
source_id = source.id
try:
source.delete()
messages.success(request, f'Источник #{source_id} успешно удален.')
except Exception as e:
messages.error(request, f'Ошибка при удалении источника: {str(e)}')
return redirect('mainapp:source_update', pk=pk)
# Redirect to source list
if request.GET.urlencode():
return redirect(f"{reverse('mainapp:home')}?{request.GET.urlencode()}")
return redirect('mainapp:home')