From 20a13414dec550658910ee005c5a29f16dfb9195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=BE=D1=88=D0=BA=D0=B8=D0=BD=20=D0=A1=D0=B5=D1=80?= =?UTF-8?q?=D0=B3=D0=B5=D0=B9?= Date: Thu, 30 Oct 2025 12:27:25 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20=D1=81=D1=82?= =?UTF-8?q?=D1=80=D1=83=D0=BA=D1=82=D1=83=D1=80=D0=B0=20=D0=BC=D0=BE=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dbapp/dbapp/settings/base.py | 15 -- dbapp/mainapp/admin.py | 195 +++++++++++++----- ...tem_unique_objitem_combination_and_more.py | 45 ++++ ...ve_parameter_objitem_parameter_objitems.py | 22 ++ dbapp/mainapp/models.py | 6 +- dbapp/mainapp/utils.py | 49 +++-- 6 files changed, 245 insertions(+), 87 deletions(-) create mode 100644 dbapp/mainapp/migrations/0021_remove_objitem_unique_objitem_combination_and_more.py create mode 100644 dbapp/mainapp/migrations/0022_remove_parameter_objitem_parameter_objitems.py diff --git a/dbapp/dbapp/settings/base.py b/dbapp/dbapp/settings/base.py index cb21ca1..8ecf2ff 100644 --- a/dbapp/dbapp/settings/base.py +++ b/dbapp/dbapp/settings/base.py @@ -164,21 +164,6 @@ STATICFILES_DIRS = [ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -# DAISY_SETTINGS = { -# 'SITE_TITLE': 'Geo admin', # The title of the site -# 'SITE_HEADER': 'GEO', # Header text displayed in the admin panel -# 'INDEX_TITLE': 'Заголовок', # The title for the index page of dashboard -# 'SITE_LOGO': '/static/admin/img/icon-clock.svg', # Path to the logo image displayed in the sidebar -# 'EXTRA_STYLES': [], # List of extra stylesheets to be loaded in base.html (optional) -# 'EXTRA_SCRIPTS': [], # List of extra script URLs to be loaded in base.html (optional) -# 'LOAD_FULL_STYLES': False, # If True, loads full DaisyUI components in the admin (useful if you have custom template overrides) -# 'SHOW_CHANGELIST_FILTER': False, # If True, the filter sidebar will open by default on changelist views -# 'DONT_SUPPORT_ME': True, # Hide github link in sidebar footer -# 'SIDEBAR_FOOTNOTE': 'Что-то о как', # add footnote to sidebar -# 'DEFAULT_THEME': None, # Set a default theme (e.g., 'corporate', 'dark', 'light') -# 'DEFAULT_THEME_DARK': None, # Set a default dark theme when system prefers dark mode -# 'SHOW_THEME_SELECTOR': True, # If False, hides the theme selector dropdown entirely -# } # AUTH_USER_MODEL = 'mainapp.CustomUser' X_FRAME_OPTIONS = "SAMEORIGIN" diff --git a/dbapp/mainapp/admin.py b/dbapp/mainapp/admin.py index 3ffcb16..56198ac 100644 --- a/dbapp/mainapp/admin.py +++ b/dbapp/mainapp/admin.py @@ -23,6 +23,7 @@ from django.contrib.gis.db import models as gis from django.shortcuts import redirect from django.urls import reverse from django.utils import timezone +from leaflet.forms.widgets import LeafletWidget from rangefilter.filters import ( DateRangeFilterBuilder, @@ -90,6 +91,28 @@ class LocationForm(forms.ModelForm): return instance +class GeoInline(admin.StackedInline): + model = Geo + extra = 0 + verbose_name = "Гео" + verbose_name_plural = "Гео" + form = LocationForm + readonly_fields = ("distance_coords_kup", "distance_coords_valid", "distance_kup_valid") + fieldsets = ( + ("Основная информация", { + "fields": ("mirrors", "location", "distance_coords_kup", + "distance_coords_valid", "distance_kup_valid", "timestamp", "comment", "id_user_add") + }), + ("Координаты: геолокация", { + "fields": ("longitude_geo", "latitude_geo", "coords"), + }), + ("Координаты: Кубсат", { + "fields": ("longitude_kupsat", "latitude_kupsat", "coords_kupsat"), + }), + ("Координаты: Оперативный отдел", { + "fields": ("longitude_valid", "latitude_valid", "coords_valid"), + }), + ) class CustomUserInline(admin.StackedInline): @@ -130,7 +153,7 @@ class ModulationAdmin(admin.ModelAdmin): ordering = ("name",) @admin.register(SourceType) -class ModulationAdmin(admin.ModelAdmin): +class SourceTypeAdmin(admin.ModelAdmin): list_display = ("name",) search_fields = ("name",) ordering = ("name",) @@ -200,6 +223,7 @@ class ParameterAdmin(ImportExportActionModelAdmin, admin.ModelAdmin): ) ordering = ("frequency",) list_select_related = ("polarization", "modulation", "standard", "id_satellite",) + filter_horizontal = ('objitems',) # For many-to-many relationship # raw_id_fields = ("id_sigma_parameter", ) inlines = [SigmaParameterInline] # autocomplete_fields = ("id_sigma_parameter", ) @@ -378,8 +402,44 @@ def show_on_map(modeladmin, request, queryset): show_on_map.short_description = "Показать выбранные на карте" +class ObjItemForm(forms.ModelForm): + parameter = forms.ModelChoiceField( + queryset=Parameter.objects.all(), + required=False, + label="Параметр" + ) + + class Meta: + model = ObjItem + fields = '__all__' + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Set initial value if the ObjItem already has a parameter + if self.instance.pk: + first_param = self.instance.parameters_obj.first() + if first_param: + self.fields['parameter'].initial = first_param + + def save(self, commit=True): + instance = super().save(commit=commit) + + # Handle the single parameter assignment - replace all existing relationships + if self.cleaned_data.get('parameter'): + # Clear all existing parameter relationships + instance.parameters_obj.clear() + # Add the selected parameter + instance.parameters_obj.add(self.cleaned_data['parameter']) + else: + # If no parameter selected, clear all + instance.parameters_obj.clear() + + return instance + + @admin.register(ObjItem) class ObjectAdmin(admin.ModelAdmin): + form = ObjItemForm list_display = ( "name", "sat_name", @@ -399,121 +459,142 @@ class ObjectAdmin(admin.ModelAdmin): list_display_links = ("name",) list_filter = ( UniqueToggleFilter, - ("id_vch_load__id_satellite", MultiSelectRelatedDropdownFilter), - ("id_vch_load__frequency", NumericRangeFilterBuilder()), - ("id_vch_load__freq_range", NumericRangeFilterBuilder()), - ("id_vch_load__snr", NumericRangeFilterBuilder()), - ("id_vch_load__modulation", MultiSelectRelatedDropdownFilter), - ("id_vch_load__polarization", MultiSelectRelatedDropdownFilter), + ("parameters_obj__id_satellite", MultiSelectRelatedDropdownFilter), + ("parameters_obj__frequency", NumericRangeFilterBuilder()), + ("parameters_obj__freq_range", NumericRangeFilterBuilder()), + ("parameters_obj__snr", NumericRangeFilterBuilder()), + ("parameters_obj__modulation", MultiSelectRelatedDropdownFilter), + ("parameters_obj__polarization", MultiSelectRelatedDropdownFilter), GeoKupDistanceFilter, GeoValidDistanceFilter ) search_fields = ( "name", - "id_geo__coords", - # "id_satellite__name", - # "id_vch_load__frequency", + "geo_obj__coords", + "parameters_obj__frequency", ) + ordering = ("name",) - list_select_related = ( - # "id_satellite", - "id_vch_load", - "id_vch_load__polarization", - "id_vch_load__modulation", - "id_vch_load__id_satellite", - "id_geo", - "id_source_type" - ) - autocomplete_fields = ("id_geo",) - raw_id_fields = ("id_vch_load",) - # dynamic_raw_id_fields = ("id_vch_load",) + inlines = [GeoInline] actions = [show_on_map] + def get_queryset(self, request): + qs = super().get_queryset(request) + return qs.select_related('geo_obj').prefetch_related( + 'parameters_obj__id_satellite', + 'parameters_obj__polarization', + 'parameters_obj__modulation', + 'parameters_obj__standard' + ) + def sat_name(self, obj): - return obj.id_vch_load.id_satellite + param = next(iter(obj.parameters_obj.all()), None) + if param and param.id_satellite: + return param.id_satellite.name + return "-" sat_name.short_description = "Спутник" - sat_name.admin_order_field = "id_vch_load__id_satellite__name" + sat_name.admin_order_field = "parameters_obj__id_satellite__name" def freq(self, obj): - par = obj.id_vch_load - return par.frequency + # param = obj.parameters_obj.first() + param = next(iter(obj.parameters_obj.all()), None) + if param: + return param.frequency + return "-" freq.short_description = "Частота, МГц" - freq.admin_order_field = "id_vch_load__frequency" + freq.admin_order_field = "parameters_obj__frequency" def distance_geo_kup(self, obj): - par = obj.id_geo.distance_coords_kup - if par is None: + geo = obj.geo_obj + if not geo or geo.distance_coords_kup is None: return "-" - return round(par, 3) + return round(geo.distance_coords_kup, 3) distance_geo_kup.short_description = "Гео-куб, км" def distance_geo_valid(self, obj): - par = obj.id_geo.distance_coords_valid - if par is None: + geo = obj.geo_obj + if not geo or geo.distance_coords_valid is None: return "-" - return round(par, 3) + return round(geo.distance_coords_valid, 3) distance_geo_valid.short_description = "Гео-опер, км" def distance_kup_valid(self, obj): - par = obj.id_geo.distance_kup_valid - if par is None: + geo = obj.geo_obj + if not geo or geo.distance_kup_valid is None: return "-" - return round(par, 3) + return round(geo.distance_kup_valid, 3) distance_kup_valid.short_description = "Куб-опер, км" def pol(self, obj): - par = obj.id_vch_load.polarization - return par.name + # Get the first parameter associated with this objitem to display polarization + param = next(iter(obj.parameters_obj.all()), None) + if param and param.polarization: + return param.polarization.name + return "-" pol.short_description = "Поляризация" def freq_range(self, obj): - par = obj.id_vch_load - return par.freq_range + # Get the first parameter associated with this objitem to display freq_range + param = next(iter(obj.parameters_obj.all()), None) + if param: + return param.freq_range + return "-" freq_range.short_description = "Полоса, МГц" - freq_range.admin_order_field = "id_vch_load__freq_range" + freq_range.admin_order_field = "parameters_obj__freq_range" def bod_velocity(self, obj): - par = obj.id_vch_load - return par.bod_velocity + # Get the first parameter associated with this objitem to display bod_velocity + param = next(iter(obj.parameters_obj.all()), None) + if param: + return param.bod_velocity + return "-" bod_velocity.short_description = "Сим. v, БОД" def modulation(self, obj): - par = obj.id_vch_load.modulation - return par.name + # Get the first parameter associated with this objitem to display modulation + param = next(iter(obj.parameters_obj.all()), None) + if param and param.modulation: + return param.modulation.name + return "-" modulation.short_description = "Модуляция" def snr(self, obj): - par = obj.id_vch_load - return par.snr + # Get the first parameter associated with this objitem to display snr + param = next(iter(obj.parameters_obj.all()), None) + if param: + return param.snr + return "-" snr.short_description = "ОСШ" def geo_coords(self, obj): - geo = obj.id_geo + geo = obj.geo_obj + if not geo or not geo.coords: + return "-" longitude = geo.coords.coords[0] latitude = geo.coords.coords[1] lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W" lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" return f"{lat} {lon}" geo_coords.short_description = "Координаты геолокации" - geo_coords.admin_order_filed = "id_geo__coords" + geo_coords.admin_order_field = "geo_obj__coords" def kupsat_coords(self, obj): - obj = obj.id_geo - if obj.coords_kupsat is None: + geo = obj.geo_obj + if not geo or not geo.coords_kupsat: return "-" - longitude = obj.coords_kupsat.coords[0] - latitude = obj.coords_kupsat.coords[1] + longitude = geo.coords_kupsat.coords[0] + latitude = geo.coords_kupsat.coords[1] lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W" lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" return f"{lat} {lon}" kupsat_coords.short_description = "Координаты Кубсата" def valid_coords(self, obj): - obj = obj.id_geo - if obj.coords_valid is None: + geo = obj.geo_obj + if not geo or not geo.coords_valid: return "-" - longitude = obj.coords_valid.coords[0] - latitude = obj.coords_valid.coords[1] + longitude = geo.coords_valid.coords[0] + latitude = geo.coords_valid.coords[1] lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W" lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" return f"{lat} {lon}" diff --git a/dbapp/mainapp/migrations/0021_remove_objitem_unique_objitem_combination_and_more.py b/dbapp/mainapp/migrations/0021_remove_objitem_unique_objitem_combination_and_more.py new file mode 100644 index 0000000..2d01e77 --- /dev/null +++ b/dbapp/mainapp/migrations/0021_remove_objitem_unique_objitem_combination_and_more.py @@ -0,0 +1,45 @@ +# Generated by Django 5.2.7 on 2025-10-30 06:40 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mainapp', '0020_sourcetype_objitem_id_source_type'), + ] + + operations = [ + migrations.RemoveConstraint( + model_name='objitem', + name='unique_objitem_combination', + ), + migrations.RemoveField( + model_name='objitem', + name='id_geo', + ), + migrations.RemoveField( + model_name='objitem', + name='id_source_type', + ), + migrations.RemoveField( + model_name='objitem', + name='id_vch_load', + ), + migrations.AddField( + model_name='geo', + name='objitem', + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='geo_obj', to='mainapp.objitem', verbose_name='Гео'), + ), + migrations.AddField( + model_name='parameter', + name='objitem', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parameters_obj', to='mainapp.objitem', verbose_name='Источник'), + ), + migrations.AddField( + model_name='sourcetype', + name='objitem', + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_type_obj', to='mainapp.objitem', verbose_name='Гео'), + ), + ] diff --git a/dbapp/mainapp/migrations/0022_remove_parameter_objitem_parameter_objitems.py b/dbapp/mainapp/migrations/0022_remove_parameter_objitem_parameter_objitems.py new file mode 100644 index 0000000..228f335 --- /dev/null +++ b/dbapp/mainapp/migrations/0022_remove_parameter_objitem_parameter_objitems.py @@ -0,0 +1,22 @@ +# Generated by Django 5.2.7 on 2025-10-30 07:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mainapp', '0021_remove_objitem_unique_objitem_combination_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='parameter', + name='objitem', + ), + migrations.AddField( + model_name='parameter', + name='objitems', + field=models.ManyToManyField(blank=True, related_name='parameters_obj', to='mainapp.objitem', verbose_name='Источники'), + ), + ] diff --git a/dbapp/mainapp/models.py b/dbapp/mainapp/models.py index cd2fce8..2a0296a 100644 --- a/dbapp/mainapp/models.py +++ b/dbapp/mainapp/models.py @@ -132,7 +132,7 @@ class ObjItem(models.Model): class SourceType(models.Model): name = models.CharField(max_length=50, unique=True, verbose_name="Тип источника") - objitem = models.OneToOneField(ObjItem, on_delete=models.SET_NULL, verbose_name="Гео", related_name="objitems", null=True) + objitem = models.OneToOneField(ObjItem, on_delete=models.SET_NULL, verbose_name="Гео", related_name="source_type_obj", null=True) def __str__(self): return self.name @@ -158,7 +158,7 @@ class Parameter(models.Model): Standard, default=get_default_standard, on_delete=models.SET_DEFAULT, related_name="standards", null=True, blank=True, verbose_name="Стандарт" ) id_user_add = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="parameter_added", verbose_name="Пользователь", null=True, blank=True) - objitem = models.ForeignKey(ObjItem, on_delete=models.SET_NULL, related_name="objitems", verbose_name="Источник",null=True, blank=True) + objitems = models.ManyToManyField(ObjItem, related_name="parameters_obj", verbose_name="Источники", blank=True) # id_sigma_parameter = models.ManyToManyField(SigmaParameter, on_delete=models.SET_NULL, related_name="sigma_parameter", verbose_name="ВЧ с sigma", null=True, blank=True) # id_sigma_parameter = models.ManyToManyField(SigmaParameter, verbose_name="ВЧ с sigma", null=True, blank=True) @@ -272,7 +272,7 @@ class Geo(models.Model): db_persist=True, null=True, blank=True, verbose_name="Расстояние между купсатом и оперативным отделом, км" ) - objitem = models.OneToOneField(ObjItem, on_delete=models.SET_NULL, verbose_name="Гео", related_name="objitems", null=True) + objitem = models.OneToOneField(ObjItem, on_delete=models.SET_NULL, verbose_name="Гео", related_name="geo_obj", null=True) def __str__(self): longitude = self.coords.coords[0] diff --git a/dbapp/mainapp/utils.py b/dbapp/mainapp/utils.py index 1a12891..1e51c4c 100644 --- a/dbapp/mainapp/utils.py +++ b/dbapp/mainapp/utils.py @@ -21,7 +21,7 @@ import io from django.db.models import F, Count, Exists, OuterRef, Min, Max from geopy.geocoders import Nominatim import reverse_geocoder as rg -import time +from time import sleep def get_all_constants(): sats = [sat.name for sat in Satellite.objects.all()] @@ -88,6 +88,7 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite): date = stroka[1]['Дата'].date() time_ = stroka[1]['Время'] if isinstance(time_, str): + time_ = time_.strip() time_ = time(0,0,0) timestamp = datetime.combine(date, time_) current_mirrors = [] @@ -111,7 +112,7 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite): comment = stroka[1]['Комментарий'] source = stroka[1]['Объект наблюдения'] - vch_load_obj, vch_created = Parameter.objects.get_or_create( + vch_load_obj, _ = Parameter.objects.get_or_create( id_satellite=sat, polarization=polarization_obj, frequency=freq, @@ -137,17 +138,41 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite): geo.save() geo.mirrors.set(Mirror.objects.filter(name__in=current_mirrors)) - obj_item, _ = ObjItem.objects.get_or_create( - id_geo=geo, - id_vch_load=vch_load_obj, - defaults={ - 'name': source, - 'id_user_add': CustomUser.objects.get(id=1), - # 'id_satellite': sat - } + existing_obj_items = ObjItem.objects.filter( + parameters_obj=vch_load_obj, + geo_obj=geo ) + if not existing_obj_items.exists(): + # Такой объект уже существует, используем его + # obj_item = existing_obj_items.first() + obj_item = ObjItem.objects.create( + name=source, + id_user_add=CustomUser.objects.get(id=1) + ) + # Устанавливаем связь с параметром + obj_item.parameters_obj.set([vch_load_obj]) + # Устанавливаем связь с geo + geo.objitem = obj_item + geo.save() + # else: + # Создаем новый ObjItem + + + # obj_item, _ = ObjItem.objects.get_or_create( + # defaults={ + # 'name': source, + # 'id_user_add': CustomUser.objects.get(id=1), + # # 'id_satellite': sat + # } + # ) + # obj_item.save() + # obj_item.parameters_obj.set([vch_load_obj]) + # if geo: + # obj_item.geo_obj = geo + # # или в зависимости от вашей модели Geo, вы можете установить обратную связь там: + # # geo.objitem = obj_item + # geo.save() - obj_item.save() def add_satellite_list(): @@ -389,7 +414,7 @@ def kub_report(data_in: io.StringIO) -> pd.DataFrame: # loc_name = location.get('city', '') or location.get('town', '') or location.get('province', '') or location.get('country', '') # except AttributeError: # loc_name = '' - # time.sleep(1) + # sleep(1) loc_name = '' if transponder: #and not (len(transponder) > 1): transfer = transponder.transfer