Добавил транспондеры к ObjItem шаблону
This commit is contained in:
@@ -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',
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user