Виджет для формы выбора зеркал
This commit is contained in:
@@ -2,57 +2,54 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
# Local imports
|
# Local imports
|
||||||
from .models import Geo, Modulation, ObjItem, Parameter, Polarization, Satellite, Standard
|
from .models import (
|
||||||
from .widgets import TagSelectWidget
|
Geo,
|
||||||
|
Modulation,
|
||||||
|
ObjItem,
|
||||||
|
Parameter,
|
||||||
|
Polarization,
|
||||||
|
Satellite,
|
||||||
|
Standard,
|
||||||
|
)
|
||||||
|
from .widgets import CheckboxSelectMultipleWidget
|
||||||
|
|
||||||
|
|
||||||
class UploadFileForm(forms.Form):
|
class UploadFileForm(forms.Form):
|
||||||
file = forms.FileField(
|
file = forms.FileField(
|
||||||
label="Выберите файл",
|
label="Выберите файл",
|
||||||
widget=forms.FileInput(attrs={
|
widget=forms.FileInput(attrs={"class": "form-file-input"}),
|
||||||
'class': 'form-file-input'
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class LoadExcelData(forms.Form):
|
class LoadExcelData(forms.Form):
|
||||||
file = forms.FileField(
|
file = forms.FileField(
|
||||||
label="Выберите Excel файл",
|
label="Выберите Excel файл",
|
||||||
widget=forms.FileInput(attrs={
|
widget=forms.FileInput(attrs={"class": "form-control", "accept": ".xlsx,.xls"}),
|
||||||
'class': 'form-control',
|
|
||||||
'accept': '.xlsx,.xls'
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
sat_choice = forms.ModelChoiceField(
|
sat_choice = forms.ModelChoiceField(
|
||||||
queryset=Satellite.objects.all(),
|
queryset=Satellite.objects.all(),
|
||||||
label="Выберите спутник",
|
label="Выберите спутник",
|
||||||
widget=forms.Select(attrs={
|
widget=forms.Select(attrs={"class": "form-select"}),
|
||||||
'class': 'form-select'
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
number_input = forms.IntegerField(
|
number_input = forms.IntegerField(
|
||||||
label="Введите число объектов",
|
label="Введите число объектов",
|
||||||
min_value=0,
|
min_value=0,
|
||||||
widget=forms.NumberInput(attrs={
|
widget=forms.NumberInput(attrs={"class": "form-control"}),
|
||||||
'class': 'form-control'
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class LoadCsvData(forms.Form):
|
class LoadCsvData(forms.Form):
|
||||||
file = forms.FileField(
|
file = forms.FileField(
|
||||||
label="Выберите CSV файл",
|
label="Выберите CSV файл",
|
||||||
widget=forms.FileInput(attrs={
|
widget=forms.FileInput(attrs={"class": "form-control", "accept": ".csv"}),
|
||||||
'class': 'form-control',
|
|
||||||
'accept': '.csv'
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class UploadVchLoad(UploadFileForm):
|
class UploadVchLoad(UploadFileForm):
|
||||||
sat_choice = forms.ModelChoiceField(
|
sat_choice = forms.ModelChoiceField(
|
||||||
queryset=Satellite.objects.all(),
|
queryset=Satellite.objects.all(),
|
||||||
label="Выберите спутник",
|
label="Выберите спутник",
|
||||||
widget=forms.Select(attrs={
|
widget=forms.Select(attrs={"class": "form-select"}),
|
||||||
'class': 'form-select'
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -60,9 +57,7 @@ class VchLinkForm(forms.Form):
|
|||||||
sat_choice = forms.ModelChoiceField(
|
sat_choice = forms.ModelChoiceField(
|
||||||
queryset=Satellite.objects.all(),
|
queryset=Satellite.objects.all(),
|
||||||
label="Выберите спутник",
|
label="Выберите спутник",
|
||||||
widget=forms.Select(attrs={
|
widget=forms.Select(attrs={"class": "form-select"}),
|
||||||
'class': 'form-select'
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
# ku_range = forms.ChoiceField(
|
# ku_range = forms.ChoiceField(
|
||||||
# choices=[(9750.0, '9750'), (10750.0, '10750')],
|
# choices=[(9750.0, '9750'), (10750.0, '10750')],
|
||||||
@@ -74,18 +69,22 @@ class VchLinkForm(forms.Form):
|
|||||||
label="Разброс по частоте (не используется)",
|
label="Разброс по частоте (не используется)",
|
||||||
required=False,
|
required=False,
|
||||||
initial=0.0,
|
initial=0.0,
|
||||||
widget=forms.NumberInput(attrs={
|
widget=forms.NumberInput(
|
||||||
'class': 'form-control',
|
attrs={
|
||||||
'placeholder': 'Не используется - погрешность определяется автоматически'
|
"class": "form-control",
|
||||||
})
|
"placeholder": "Не используется - погрешность определяется автоматически",
|
||||||
|
}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
value2 = forms.FloatField(
|
value2 = forms.FloatField(
|
||||||
label="Разброс по полосе (в %)",
|
label="Разброс по полосе (в %)",
|
||||||
widget=forms.NumberInput(attrs={
|
widget=forms.NumberInput(
|
||||||
'class': 'form-control',
|
attrs={
|
||||||
'placeholder': 'Введите погрешность полосы в процентах',
|
"class": "form-control",
|
||||||
'step': '0.1'
|
"placeholder": "Введите погрешность полосы в процентах",
|
||||||
})
|
"step": "0.1",
|
||||||
|
}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -106,10 +105,7 @@ class NewEventForm(forms.Form):
|
|||||||
# )
|
# )
|
||||||
file = forms.FileField(
|
file = forms.FileField(
|
||||||
label="Выберите файл",
|
label="Выберите файл",
|
||||||
widget=forms.FileInput(attrs={
|
widget=forms.FileInput(attrs={"class": "form-control", "accept": ".xlsx,.xls"}),
|
||||||
'class': 'form-control',
|
|
||||||
'accept': '.xlsx,.xls'
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -117,53 +113,43 @@ class FillLyngsatDataForm(forms.Form):
|
|||||||
"""Форма для заполнения данных из Lyngsat с поддержкой кеширования"""
|
"""Форма для заполнения данных из Lyngsat с поддержкой кеширования"""
|
||||||
|
|
||||||
REGION_CHOICES = [
|
REGION_CHOICES = [
|
||||||
('europe', 'Европа'),
|
("europe", "Европа"),
|
||||||
('asia', 'Азия'),
|
("asia", "Азия"),
|
||||||
('america', 'Америка'),
|
("america", "Америка"),
|
||||||
('atlantic', 'Атлантика'),
|
("atlantic", "Атлантика"),
|
||||||
]
|
]
|
||||||
|
|
||||||
satellites = forms.ModelMultipleChoiceField(
|
satellites = forms.ModelMultipleChoiceField(
|
||||||
queryset=Satellite.objects.all().order_by('name'),
|
queryset=Satellite.objects.all().order_by("name"),
|
||||||
label="Выберите спутники",
|
label="Выберите спутники",
|
||||||
widget=forms.SelectMultiple(attrs={
|
widget=forms.SelectMultiple(attrs={"class": "form-select", "size": "10"}),
|
||||||
'class': 'form-select',
|
|
||||||
'size': '10'
|
|
||||||
}),
|
|
||||||
required=True,
|
required=True,
|
||||||
help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких спутников"
|
help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких спутников",
|
||||||
)
|
)
|
||||||
|
|
||||||
regions = forms.MultipleChoiceField(
|
regions = forms.MultipleChoiceField(
|
||||||
choices=REGION_CHOICES,
|
choices=REGION_CHOICES,
|
||||||
label="Выберите регионы",
|
label="Выберите регионы",
|
||||||
widget=forms.SelectMultiple(attrs={
|
widget=forms.SelectMultiple(attrs={"class": "form-select", "size": "4"}),
|
||||||
'class': 'form-select',
|
|
||||||
'size': '4'
|
|
||||||
}),
|
|
||||||
required=True,
|
required=True,
|
||||||
initial=['europe', 'asia', 'america', 'atlantic'],
|
initial=["europe", "asia", "america", "atlantic"],
|
||||||
help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких регионов"
|
help_text="Удерживайте Ctrl (Cmd на Mac) для выбора нескольких регионов",
|
||||||
)
|
)
|
||||||
|
|
||||||
use_cache = forms.BooleanField(
|
use_cache = forms.BooleanField(
|
||||||
label="Использовать кеширование",
|
label="Использовать кеширование",
|
||||||
required=False,
|
required=False,
|
||||||
initial=True,
|
initial=True,
|
||||||
widget=forms.CheckboxInput(attrs={
|
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||||
'class': 'form-check-input'
|
help_text="Использовать кешированные данные (ускоряет повторные запросы)",
|
||||||
}),
|
|
||||||
help_text="Использовать кешированные данные (ускоряет повторные запросы)"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
force_refresh = forms.BooleanField(
|
force_refresh = forms.BooleanField(
|
||||||
label="Принудительно обновить данные",
|
label="Принудительно обновить данные",
|
||||||
required=False,
|
required=False,
|
||||||
initial=False,
|
initial=False,
|
||||||
widget=forms.CheckboxInput(attrs={
|
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||||
'class': 'form-check-input'
|
help_text="Игнорировать кеш и получить свежие данные с сайта",
|
||||||
}),
|
|
||||||
help_text="Игнорировать кеш и получить свежие данные с сайта"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -171,26 +157,22 @@ class LinkLyngsatForm(forms.Form):
|
|||||||
"""Форма для привязки источников LyngSat к объектам"""
|
"""Форма для привязки источников LyngSat к объектам"""
|
||||||
|
|
||||||
satellites = forms.ModelMultipleChoiceField(
|
satellites = forms.ModelMultipleChoiceField(
|
||||||
queryset=Satellite.objects.all().order_by('name'),
|
queryset=Satellite.objects.all().order_by("name"),
|
||||||
label="Выберите спутники",
|
label="Выберите спутники",
|
||||||
widget=forms.SelectMultiple(attrs={
|
widget=forms.SelectMultiple(attrs={"class": "form-select", "size": "10"}),
|
||||||
'class': 'form-select',
|
|
||||||
'size': '10'
|
|
||||||
}),
|
|
||||||
required=False,
|
required=False,
|
||||||
help_text="Оставьте пустым для обработки всех спутников"
|
help_text="Оставьте пустым для обработки всех спутников",
|
||||||
)
|
)
|
||||||
|
|
||||||
frequency_tolerance = forms.FloatField(
|
frequency_tolerance = forms.FloatField(
|
||||||
label="Допуск по частоте (МГц)",
|
label="Допуск по частоте (МГц)",
|
||||||
initial=0.5,
|
initial=0.5,
|
||||||
min_value=0,
|
min_value=0,
|
||||||
widget=forms.NumberInput(attrs={
|
widget=forms.NumberInput(attrs={"class": "form-control", "step": "0.1"}),
|
||||||
'class': 'form-control',
|
help_text="Допустимое отклонение частоты при сравнении",
|
||||||
'step': '0.1'
|
|
||||||
}),
|
|
||||||
help_text="Допустимое отклонение частоты при сравнении"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ParameterForm(forms.ModelForm):
|
class ParameterForm(forms.ModelForm):
|
||||||
"""
|
"""
|
||||||
Форма для создания и редактирования параметров ВЧ загрузки.
|
Форма для создания и редактирования параметров ВЧ загрузки.
|
||||||
@@ -201,73 +183,88 @@ class ParameterForm(forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Parameter
|
model = Parameter
|
||||||
fields = [
|
fields = [
|
||||||
'id_satellite', 'frequency', 'freq_range', 'polarization',
|
"id_satellite",
|
||||||
'bod_velocity', 'modulation', 'snr', 'standard'
|
"frequency",
|
||||||
|
"freq_range",
|
||||||
|
"polarization",
|
||||||
|
"bod_velocity",
|
||||||
|
"modulation",
|
||||||
|
"snr",
|
||||||
|
"standard",
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'id_satellite': forms.Select(attrs={
|
"id_satellite": forms.Select(
|
||||||
'class': 'form-select',
|
attrs={"class": "form-select", "required": True}
|
||||||
'required': True
|
),
|
||||||
}),
|
"frequency": forms.NumberInput(
|
||||||
'frequency': forms.NumberInput(attrs={
|
attrs={
|
||||||
'class': 'form-control',
|
"class": "form-control",
|
||||||
'step': '0.000001',
|
"step": "0.000001",
|
||||||
'min': '0',
|
"min": "0",
|
||||||
'max': '50000',
|
"max": "50000",
|
||||||
'placeholder': 'Введите частоту в МГц'
|
"placeholder": "Введите частоту в МГц",
|
||||||
}),
|
}
|
||||||
'freq_range': forms.NumberInput(attrs={
|
),
|
||||||
'class': 'form-control',
|
"freq_range": forms.NumberInput(
|
||||||
'step': '0.000001',
|
attrs={
|
||||||
'min': '0',
|
"class": "form-control",
|
||||||
'max': '1000',
|
"step": "0.000001",
|
||||||
'placeholder': 'Введите полосу частот в МГц'
|
"min": "0",
|
||||||
}),
|
"max": "1000",
|
||||||
'bod_velocity': forms.NumberInput(attrs={
|
"placeholder": "Введите полосу частот в МГц",
|
||||||
'class': 'form-control',
|
}
|
||||||
'step': '0.001',
|
),
|
||||||
'min': '0',
|
"bod_velocity": forms.NumberInput(
|
||||||
'placeholder': 'Введите символьную скорость в БОД'
|
attrs={
|
||||||
}),
|
"class": "form-control",
|
||||||
'snr': forms.NumberInput(attrs={
|
"step": "0.001",
|
||||||
'class': 'form-control',
|
"min": "0",
|
||||||
'step': '0.001',
|
"placeholder": "Введите символьную скорость в БОД",
|
||||||
'min': '-50',
|
}
|
||||||
'max': '100',
|
),
|
||||||
'placeholder': 'Введите ОСШ в дБ'
|
"snr": forms.NumberInput(
|
||||||
}),
|
attrs={
|
||||||
'polarization': forms.Select(attrs={'class': 'form-select'}),
|
"class": "form-control",
|
||||||
'modulation': forms.Select(attrs={'class': 'form-select'}),
|
"step": "0.001",
|
||||||
'standard': forms.Select(attrs={'class': 'form-select'}),
|
"min": "-50",
|
||||||
|
"max": "100",
|
||||||
|
"placeholder": "Введите ОСШ в дБ",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
"polarization": forms.Select(attrs={"class": "form-select"}),
|
||||||
|
"modulation": forms.Select(attrs={"class": "form-select"}),
|
||||||
|
"standard": forms.Select(attrs={"class": "form-select"}),
|
||||||
}
|
}
|
||||||
labels = {
|
labels = {
|
||||||
'id_satellite': 'Спутник',
|
"id_satellite": "Спутник",
|
||||||
'frequency': 'Частота (МГц)',
|
"frequency": "Частота (МГц)",
|
||||||
'freq_range': 'Полоса частот (МГц)',
|
"freq_range": "Полоса частот (МГц)",
|
||||||
'polarization': 'Поляризация',
|
"polarization": "Поляризация",
|
||||||
'bod_velocity': 'Символьная скорость (БОД)',
|
"bod_velocity": "Символьная скорость (БОД)",
|
||||||
'modulation': 'Модуляция',
|
"modulation": "Модуляция",
|
||||||
'snr': 'ОСШ (дБ)',
|
"snr": "ОСШ (дБ)",
|
||||||
'standard': 'Стандарт',
|
"standard": "Стандарт",
|
||||||
}
|
}
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'frequency': 'Частота в диапазоне от 0 до 50000 МГц',
|
"frequency": "Частота в диапазоне от 0 до 50000 МГц",
|
||||||
'freq_range': 'Полоса частот в диапазоне от 0 до 1000 МГц',
|
"freq_range": "Полоса частот в диапазоне от 0 до 1000 МГц",
|
||||||
'bod_velocity': 'Символьная скорость должна быть положительной',
|
"bod_velocity": "Символьная скорость должна быть положительной",
|
||||||
'snr': 'Отношение сигнал/шум в диапазоне от -50 до 100 дБ',
|
"snr": "Отношение сигнал/шум в диапазоне от -50 до 100 дБ",
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Динамически загружаем choices для select полей
|
# Динамически загружаем choices для select полей
|
||||||
self.fields['id_satellite'].queryset = Satellite.objects.all().order_by('name')
|
self.fields["id_satellite"].queryset = Satellite.objects.all().order_by("name")
|
||||||
self.fields['polarization'].queryset = Polarization.objects.all().order_by('name')
|
self.fields["polarization"].queryset = Polarization.objects.all().order_by(
|
||||||
self.fields['modulation'].queryset = Modulation.objects.all().order_by('name')
|
"name"
|
||||||
self.fields['standard'].queryset = Standard.objects.all().order_by('name')
|
)
|
||||||
|
self.fields["modulation"].queryset = Modulation.objects.all().order_by("name")
|
||||||
|
self.fields["standard"].queryset = Standard.objects.all().order_by("name")
|
||||||
|
|
||||||
# Делаем спутник обязательным полем
|
# Делаем спутник обязательным полем
|
||||||
self.fields['id_satellite'].required = True
|
self.fields["id_satellite"].required = True
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""
|
"""
|
||||||
@@ -276,42 +273,54 @@ class ParameterForm(forms.ModelForm):
|
|||||||
Проверяет соотношение между частотой, полосой частот и символьной скоростью.
|
Проверяет соотношение между частотой, полосой частот и символьной скоростью.
|
||||||
"""
|
"""
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
frequency = cleaned_data.get('frequency')
|
frequency = cleaned_data.get("frequency")
|
||||||
freq_range = cleaned_data.get('freq_range')
|
freq_range = cleaned_data.get("freq_range")
|
||||||
bod_velocity = cleaned_data.get('bod_velocity')
|
bod_velocity = cleaned_data.get("bod_velocity")
|
||||||
|
|
||||||
# Проверка что частота больше полосы частот
|
# Проверка что частота больше полосы частот
|
||||||
if frequency and freq_range:
|
if frequency and freq_range:
|
||||||
if freq_range > frequency:
|
if freq_range > frequency:
|
||||||
self.add_error('freq_range', 'Полоса частот не может быть больше частоты')
|
self.add_error(
|
||||||
|
"freq_range", "Полоса частот не может быть больше частоты"
|
||||||
|
)
|
||||||
|
|
||||||
# Проверка что символьная скорость соответствует полосе частот
|
# Проверка что символьная скорость соответствует полосе частот
|
||||||
if bod_velocity and freq_range:
|
if bod_velocity and freq_range:
|
||||||
if bod_velocity > freq_range * 1000000: # Конвертация МГц в Гц
|
if bod_velocity > freq_range * 1000000: # Конвертация МГц в Гц
|
||||||
self.add_error('bod_velocity', 'Символьная скорость не может превышать полосу частот')
|
self.add_error(
|
||||||
|
"bod_velocity",
|
||||||
|
"Символьная скорость не может превышать полосу частот",
|
||||||
|
)
|
||||||
|
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
class GeoForm(forms.ModelForm):
|
class GeoForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Geo
|
model = Geo
|
||||||
fields = ['location', 'comment', 'is_average', 'mirrors']
|
fields = ["location", "comment", "is_average", "mirrors"]
|
||||||
widgets = {
|
widgets = {
|
||||||
'location': forms.TextInput(attrs={'class': 'form-control'}),
|
"location": forms.TextInput(attrs={"class": "form-control"}),
|
||||||
'comment': forms.TextInput(attrs={'class': 'form-control'}),
|
"comment": forms.TextInput(attrs={"class": "form-control"}),
|
||||||
'is_average': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
"is_average": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||||
'mirrors': TagSelectWidget(attrs={'id': 'id_geo-mirrors'}),
|
"mirrors": CheckboxSelectMultipleWidget(
|
||||||
|
attrs={
|
||||||
|
'id': 'id_geo-mirrors',
|
||||||
|
'placeholder': 'Выберите спутники...',
|
||||||
|
}
|
||||||
|
),
|
||||||
}
|
}
|
||||||
labels = {
|
labels = {
|
||||||
'location': 'Местоположение',
|
"location": "Местоположение",
|
||||||
'comment': 'Комментарий',
|
"comment": "Комментарий",
|
||||||
'is_average': 'Усреднённое',
|
"is_average": "Усреднённое",
|
||||||
'mirrors': 'Спутники-зеркала, использованные для приёма',
|
"mirrors": "Спутники-зеркала, использованные для приёма",
|
||||||
}
|
}
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'mirrors': 'Начните вводить название спутника для поиска',
|
"mirrors": "Выберите спутники из списка",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ObjItemForm(forms.ModelForm):
|
class ObjItemForm(forms.ModelForm):
|
||||||
"""
|
"""
|
||||||
Форма для создания и редактирования объектов (источников сигнала).
|
Форма для создания и редактирования объектов (источников сигнала).
|
||||||
@@ -322,25 +331,27 @@ class ObjItemForm(forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ObjItem
|
model = ObjItem
|
||||||
fields = ['name']
|
fields = ["name"]
|
||||||
widgets = {
|
widgets = {
|
||||||
'name': forms.TextInput(attrs={
|
"name": forms.TextInput(
|
||||||
'class': 'form-control',
|
attrs={
|
||||||
'placeholder': 'Введите название объекта',
|
"class": "form-control",
|
||||||
'maxlength': '100'
|
"placeholder": "Введите название объекта",
|
||||||
}),
|
"maxlength": "100",
|
||||||
|
}
|
||||||
|
),
|
||||||
}
|
}
|
||||||
labels = {
|
labels = {
|
||||||
'name': 'Название объекта',
|
"name": "Название объекта",
|
||||||
}
|
}
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'name': 'Уникальное название объекта/источника сигнала',
|
"name": "Уникальное название объекта/источника сигнала",
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
# Делаем поле name необязательным, так как оно может быть пустым
|
# Делаем поле name необязательным, так как оно может быть пустым
|
||||||
self.fields['name'].required = False
|
self.fields["name"].required = False
|
||||||
|
|
||||||
def clean_name(self):
|
def clean_name(self):
|
||||||
"""
|
"""
|
||||||
@@ -348,7 +359,7 @@ class ObjItemForm(forms.ModelForm):
|
|||||||
|
|
||||||
Проверяет что название не состоит только из пробелов.
|
Проверяет что название не состоит только из пробелов.
|
||||||
"""
|
"""
|
||||||
name = self.cleaned_data.get('name')
|
name = self.cleaned_data.get("name")
|
||||||
|
|
||||||
if name:
|
if name:
|
||||||
# Удаляем лишние пробелы
|
# Удаляем лишние пробелы
|
||||||
@@ -356,6 +367,8 @@ class ObjItemForm(forms.ModelForm):
|
|||||||
|
|
||||||
# Проверяем что после удаления пробелов что-то осталось
|
# Проверяем что после удаления пробелов что-то осталось
|
||||||
if not name:
|
if not name:
|
||||||
raise forms.ValidationError('Название не может состоять только из пробелов')
|
raise forms.ValidationError(
|
||||||
|
"Название не может состоять только из пробелов"
|
||||||
|
)
|
||||||
|
|
||||||
return name
|
return name
|
||||||
160
dbapp/mainapp/static/css/checkbox-select-multiple.css
Normal file
160
dbapp/mainapp/static/css/checkbox-select-multiple.css
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
.checkbox-multiselect-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-input-container {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 38px;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
background-color: #fff;
|
||||||
|
cursor: text;
|
||||||
|
padding: 4px 30px 4px 4px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-input-container:focus-within {
|
||||||
|
border-color: #86b7fe;
|
||||||
|
outline: 0;
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #e9ecef;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-tag-remove {
|
||||||
|
margin-left: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #6c757d;
|
||||||
|
font-weight: bold;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-tag-remove:hover {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-search {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 120px;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
padding: 4px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-search:focus {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-clear {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 1;
|
||||||
|
color: #6c757d;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-clear:hover {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-input-container.has-selections .multiselect-clear {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 1000;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Открытие вверх (по умолчанию) */
|
||||||
|
.multiselect-dropdown {
|
||||||
|
bottom: 100%;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Открытие вниз (если места сверху недостаточно) */
|
||||||
|
.multiselect-dropdown.dropdown-below {
|
||||||
|
bottom: auto;
|
||||||
|
top: 100%;
|
||||||
|
margin-top: 2px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-dropdown.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-options {
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0;
|
||||||
|
transition: background-color 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-option:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-option input[type="checkbox"] {
|
||||||
|
margin-right: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-option .option-label {
|
||||||
|
flex: 1;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-option.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
120
dbapp/mainapp/static/js/checkbox-select-multiple.js
Normal file
120
dbapp/mainapp/static/js/checkbox-select-multiple.js
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
/**
|
||||||
|
* Checkbox Select Multiple Widget
|
||||||
|
* Provides a multi-select dropdown with checkboxes and tag display
|
||||||
|
*/
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Initialize all checkbox multiselect widgets
|
||||||
|
document.querySelectorAll('.checkbox-multiselect-wrapper').forEach(function(wrapper) {
|
||||||
|
initCheckboxMultiselect(wrapper);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function initCheckboxMultiselect(wrapper) {
|
||||||
|
const widgetId = wrapper.dataset.widgetId;
|
||||||
|
const inputContainer = wrapper.querySelector('.multiselect-input-container');
|
||||||
|
const searchInput = wrapper.querySelector('.multiselect-search');
|
||||||
|
const dropdown = wrapper.querySelector('.multiselect-dropdown');
|
||||||
|
const tagsContainer = wrapper.querySelector('.multiselect-tags');
|
||||||
|
const clearButton = wrapper.querySelector('.multiselect-clear');
|
||||||
|
const checkboxes = wrapper.querySelectorAll('input[type="checkbox"]');
|
||||||
|
|
||||||
|
// Show dropdown when clicking on input container
|
||||||
|
inputContainer.addEventListener('click', function(e) {
|
||||||
|
if (e.target !== clearButton) {
|
||||||
|
positionDropdown();
|
||||||
|
dropdown.classList.add('show');
|
||||||
|
searchInput.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Position dropdown (up or down based on available space)
|
||||||
|
function positionDropdown() {
|
||||||
|
const rect = inputContainer.getBoundingClientRect();
|
||||||
|
const spaceAbove = rect.top;
|
||||||
|
const spaceBelow = window.innerHeight - rect.bottom;
|
||||||
|
const dropdownHeight = 300; // max-height from CSS
|
||||||
|
|
||||||
|
// If more space below and enough space, open downward
|
||||||
|
if (spaceBelow > spaceAbove && spaceBelow >= dropdownHeight) {
|
||||||
|
dropdown.classList.add('dropdown-below');
|
||||||
|
} else {
|
||||||
|
dropdown.classList.remove('dropdown-below');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide dropdown when clicking outside
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
if (!wrapper.contains(e.target)) {
|
||||||
|
dropdown.classList.remove('show');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Search functionality
|
||||||
|
searchInput.addEventListener('input', function() {
|
||||||
|
const searchTerm = this.value.toLowerCase();
|
||||||
|
const options = wrapper.querySelectorAll('.multiselect-option');
|
||||||
|
|
||||||
|
options.forEach(function(option) {
|
||||||
|
const label = option.querySelector('.option-label').textContent.toLowerCase();
|
||||||
|
if (label.includes(searchTerm)) {
|
||||||
|
option.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
option.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle checkbox changes
|
||||||
|
checkboxes.forEach(function(checkbox) {
|
||||||
|
checkbox.addEventListener('change', function() {
|
||||||
|
updateTags();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear all button
|
||||||
|
clearButton.addEventListener('click', function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
checkboxes.forEach(function(checkbox) {
|
||||||
|
checkbox.checked = false;
|
||||||
|
});
|
||||||
|
updateTags();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update tags display
|
||||||
|
function updateTags() {
|
||||||
|
tagsContainer.innerHTML = '';
|
||||||
|
let hasSelections = false;
|
||||||
|
|
||||||
|
checkboxes.forEach(function(checkbox) {
|
||||||
|
if (checkbox.checked) {
|
||||||
|
hasSelections = true;
|
||||||
|
const tag = document.createElement('div');
|
||||||
|
tag.className = 'multiselect-tag';
|
||||||
|
tag.innerHTML = `
|
||||||
|
<span>${checkbox.dataset.label}</span>
|
||||||
|
<button type="button" class="multiselect-tag-remove" data-value="${checkbox.value}">×</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Remove tag on click
|
||||||
|
tag.querySelector('.multiselect-tag-remove').addEventListener('click', function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
checkbox.checked = false;
|
||||||
|
updateTags();
|
||||||
|
});
|
||||||
|
|
||||||
|
tagsContainer.appendChild(tag);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show/hide clear button
|
||||||
|
if (hasSelections) {
|
||||||
|
inputContainer.classList.add('has-selections');
|
||||||
|
} else {
|
||||||
|
inputContainer.classList.remove('has-selections');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize tags on load
|
||||||
|
updateTags();
|
||||||
|
}
|
||||||
@@ -6,27 +6,93 @@
|
|||||||
{% 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' %}">
|
||||||
<style>
|
<style>
|
||||||
.form-section { margin-bottom: 2rem; border: 1px solid #dee2e6; border-radius: 0.25rem; padding: 1rem; }
|
.form-section {
|
||||||
.form-section-header { border-bottom: 1px solid #dee2e6; padding-bottom: 0.5rem; margin-bottom: 1rem; }
|
margin-bottom: 2rem;
|
||||||
.btn-action { margin-right: 0.5rem; }
|
border: 1px solid #dee2e6;
|
||||||
.dynamic-form { border: 1px dashed #ced4da; padding: 1rem; margin-top: 1rem; border-radius: 0.25rem; }
|
border-radius: 0.25rem;
|
||||||
.dynamic-form-header { display: flex; justify-content: space-between; align-items: center; }
|
padding: 1rem;
|
||||||
.readonly-field { background-color: #f8f9fa; padding: 0.375rem 0.75rem; border: 1px solid #ced4da; border-radius: 0.25rem; }
|
}
|
||||||
.coord-group { border: 1px solid #dee2e6; padding: 0.75rem; border-radius: 0.25rem; margin-bottom: 1rem; }
|
|
||||||
.coord-group-header { font-weight: bold; margin-bottom: 0.5rem; }
|
.form-section-header {
|
||||||
.form-check-input { margin-top: 0.25rem; }
|
border-bottom: 1px solid #dee2e6;
|
||||||
.datetime-group { display: flex; gap: 1rem; }
|
padding-bottom: 0.5rem;
|
||||||
.datetime-group > div { flex: 1; }
|
margin-bottom: 1rem;
|
||||||
#map { height: 500px; width: 100%; margin-bottom: 1rem; }
|
}
|
||||||
.map-container { margin-bottom: 1rem; }
|
|
||||||
.coord-sync-group { border: 1px solid #dee2e6; padding: 0.75rem; border-radius: 0.25rem; }
|
.btn-action {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-form {
|
||||||
|
border: 1px dashed #ced4da;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-form-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.readonly-field {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coord-group {
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coord-group-header {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check-input {
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datetime-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datetime-group>div {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#map {
|
||||||
|
height: 500px;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-container {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coord-sync-group {
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.map-controls {
|
.map-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-control-btn {
|
.map-control-btn {
|
||||||
padding: 0.375rem 0.75rem;
|
padding: 0.375rem 0.75rem;
|
||||||
border: 1px solid #ced4da;
|
border: 1px solid #ced4da;
|
||||||
@@ -34,25 +100,43 @@
|
|||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-control-btn.active {
|
.map-control-btn.active {
|
||||||
background-color: #e9ecef;
|
background-color: #e9ecef;
|
||||||
border-color: #dee2e6;
|
border-color: #dee2e6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-control-btn.edit {
|
.map-control-btn.edit {
|
||||||
background-color: #fff3cd;
|
background-color: #fff3cd;
|
||||||
border-color: #ffeeba;
|
border-color: #ffeeba;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-control-btn.save {
|
.map-control-btn.save {
|
||||||
background-color: #d1ecf1;
|
background-color: #d1ecf1;
|
||||||
border-color: #bee5eb;
|
border-color: #bee5eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-control-btn.cancel {
|
.map-control-btn.cancel {
|
||||||
background-color: #f8d7da;
|
background-color: #f8d7da;
|
||||||
border-color: #f5c6cb;
|
border-color: #f5c6cb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaflet-marker-icon {
|
.leaflet-marker-icon {
|
||||||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.3));
|
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.3));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Select2 custom styling */
|
||||||
|
.select2-container--default .select2-selection--multiple {
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
min-height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--default.select2-container--focus .select2-selection--multiple {
|
||||||
|
border-color: #86b7fe;
|
||||||
|
outline: 0;
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -65,10 +149,12 @@
|
|||||||
{% 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>
|
||||||
{% if object %}
|
{% if object %}
|
||||||
<a href="{% url 'mainapp:objitem_delete' object.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-action">Удалить</a>
|
<a href="{% url 'mainapp:objitem_delete' object.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}"
|
||||||
|
class="btn btn-danger btn-action">Удалить</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url 'mainapp:objitem_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-secondary btn-action">Назад</a>
|
<a href="{% url 'mainapp:objitem_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}"
|
||||||
|
class="btn btn-secondary btn-action">Назад</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -186,16 +272,16 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="id_geo_latitude" class="form-label">Широта:</label>
|
<label for="id_geo_latitude" class="form-label">Широта:</label>
|
||||||
<input type="number" step="0.000001" class="form-control"
|
<input type="number" step="0.000001" class="form-control" id="id_geo_latitude"
|
||||||
id="id_geo_latitude" name="geo_latitude"
|
name="geo_latitude"
|
||||||
value="{% if object.geo_obj and object.geo_obj.coords %}{{ object.geo_obj.coords.y|unlocalize }}{% endif %}">
|
value="{% if object.geo_obj and object.geo_obj.coords %}{{ object.geo_obj.coords.y|unlocalize }}{% endif %}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="id_geo_longitude" class="form-label">Долгота:</label>
|
<label for="id_geo_longitude" class="form-label">Долгота:</label>
|
||||||
<input type="number" step="0.000001" class="form-control"
|
<input type="number" step="0.000001" class="form-control" id="id_geo_longitude"
|
||||||
id="id_geo_longitude" name="geo_longitude"
|
name="geo_longitude"
|
||||||
value="{% if object.geo_obj and object.geo_obj.coords %}{{ object.geo_obj.coords.x|unlocalize }}{% endif %}">
|
value="{% if object.geo_obj and object.geo_obj.coords %}{{ object.geo_obj.coords.x|unlocalize }}{% endif %}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -218,15 +304,13 @@
|
|||||||
<div class="datetime-group">
|
<div class="datetime-group">
|
||||||
<div>
|
<div>
|
||||||
<label for="id_timestamp_date" class="form-label">Дата:</label>
|
<label for="id_timestamp_date" class="form-label">Дата:</label>
|
||||||
<input type="date" class="form-control"
|
<input type="date" class="form-control" id="id_timestamp_date" name="timestamp_date"
|
||||||
id="id_timestamp_date" name="timestamp_date"
|
value="{% if object.geo_obj and object.geo_obj.timestamp %}{{ object.geo_obj.timestamp|date:'Y-m-d' }}{% endif %}">
|
||||||
value="{% if object.geo_obj and object.geo_obj.timestamp %}{{ object.geo_obj.timestamp|date:'Y-m-d' }}{% endif %}">
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="id_timestamp_time" class="form-label">Время:</label>
|
<label for="id_timestamp_time" class="form-label">Время:</label>
|
||||||
<input type="time" class="form-control"
|
<input type="time" class="form-control" id="id_timestamp_time" name="timestamp_time"
|
||||||
id="id_timestamp_time" name="timestamp_time"
|
value="{% if object.geo_obj and object.geo_obj.timestamp %}{{ object.geo_obj.timestamp|time:'H:i' }}{% endif %}">
|
||||||
value="{% if object.geo_obj and object.geo_obj.timestamp %}{{ object.geo_obj.timestamp|time:'H:i' }}{% endif %}">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -253,201 +337,204 @@
|
|||||||
{% leaflet_css %}
|
{% leaflet_css %}
|
||||||
<script src="{% static 'leaflet-markers/js/leaflet-color-markers.js' %}"></script>
|
<script src="{% static 'leaflet-markers/js/leaflet-color-markers.js' %}"></script>
|
||||||
|
|
||||||
|
<!-- Подключаем кастомный виджет для мультивыбора -->
|
||||||
|
<script src="{% static 'js/checkbox-select-multiple.js' %}"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
// Инициализация карты
|
// Инициализация карты
|
||||||
const map = L.map('map').setView([55.75, 37.62], 5);
|
const map = L.map('map').setView([55.75, 37.62], 5);
|
||||||
|
|
||||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
|
|
||||||
// Функция для создания иконки маркера
|
// Функция для создания иконки маркера
|
||||||
function createMarkerIcon() {
|
function createMarkerIcon() {
|
||||||
return L.icon({
|
return L.icon({
|
||||||
iconUrl: '{% static "leaflet-markers/img/marker-icon-blue.png" %}',
|
iconUrl: '{% static "leaflet-markers/img/marker-icon-blue.png" %}',
|
||||||
shadowUrl: `{% static 'leaflet-markers/img/marker-shadow.png' %}`,
|
shadowUrl: `{% static 'leaflet-markers/img/marker-shadow.png' %}`,
|
||||||
iconSize: [25, 41],
|
iconSize: [25, 41],
|
||||||
iconAnchor: [12, 41],
|
iconAnchor: [12, 41],
|
||||||
popupAnchor: [1, -34],
|
popupAnchor: [1, -34],
|
||||||
shadowSize: [41, 41]
|
shadowSize: [41, 41]
|
||||||
});
|
|
||||||
}
|
|
||||||
const editableLayerGroup = new L.FeatureGroup();
|
|
||||||
map.addLayer(editableLayerGroup);
|
|
||||||
|
|
||||||
// Маркер геолокации
|
|
||||||
const marker = L.marker([55.75, 37.62], {
|
|
||||||
draggable: false,
|
|
||||||
icon: createMarkerIcon(),
|
|
||||||
title: 'Геолокация'
|
|
||||||
}).addTo(editableLayerGroup);
|
|
||||||
marker.bindPopup('Геолокация');
|
|
||||||
|
|
||||||
// Синхронизация при изменении формы
|
|
||||||
function syncFromForm() {
|
|
||||||
const lat = parseFloat(document.getElementById('id_geo_latitude').value);
|
|
||||||
const lng = parseFloat(document.getElementById('id_geo_longitude').value);
|
|
||||||
if (!isNaN(lat) && !isNaN(lng)) {
|
|
||||||
marker.setLatLng([lat, lng]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Синхронизация при перетаскивании (только если активировано)
|
|
||||||
marker.on('dragend', function(event) {
|
|
||||||
const latLng = event.target.getLatLng();
|
|
||||||
document.getElementById('id_geo_latitude').value = latLng.lat.toFixed(6);
|
|
||||||
document.getElementById('id_geo_longitude').value = latLng.lng.toFixed(6);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Добавляем методы для управления
|
|
||||||
marker.enableEditing = function() {
|
|
||||||
this.dragging.enable();
|
|
||||||
this.openPopup();
|
|
||||||
};
|
|
||||||
|
|
||||||
marker.disableEditing = function() {
|
|
||||||
this.dragging.disable();
|
|
||||||
this.closePopup();
|
|
||||||
};
|
|
||||||
|
|
||||||
marker.syncFromForm = syncFromForm;
|
|
||||||
|
|
||||||
// Устанавливаем начальные координаты из полей формы
|
|
||||||
function initMarkersFromForm() {
|
|
||||||
const geoLat = parseFloat(document.getElementById('id_geo_latitude').value) || 55.75;
|
|
||||||
const geoLng = parseFloat(document.getElementById('id_geo_longitude').value) || 37.62;
|
|
||||||
marker.setLatLng([geoLat, geoLng]);
|
|
||||||
|
|
||||||
// Центрируем карту на маркере
|
|
||||||
map.setView(marker.getLatLng(), 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Настройка формы для синхронизации с маркером
|
|
||||||
function setupFormChange(latFieldId, lngFieldId, marker) {
|
|
||||||
const latField = document.getElementById(latFieldId);
|
|
||||||
const lngField = document.getElementById(lngFieldId);
|
|
||||||
|
|
||||||
[latField, lngField].forEach(field => {
|
|
||||||
field.addEventListener('change', function() {
|
|
||||||
const lat = parseFloat(latField.value);
|
|
||||||
const lng = parseFloat(lngField.value);
|
|
||||||
|
|
||||||
if (!isNaN(lat) && !isNaN(lng)) {
|
|
||||||
marker.setLatLng([lat, lng]);
|
|
||||||
map.setView(marker.getLatLng(), 10);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
const editableLayerGroup = new L.FeatureGroup();
|
||||||
|
map.addLayer(editableLayerGroup);
|
||||||
|
|
||||||
// Инициализация
|
// Маркер геолокации
|
||||||
initMarkersFromForm();
|
const marker = L.marker([55.75, 37.62], {
|
||||||
// Настройка формы для синхронизации с маркером
|
draggable: false,
|
||||||
setupFormChange('id_geo_latitude', 'id_geo_longitude', marker);
|
icon: createMarkerIcon(),
|
||||||
// --- УПРАВЛЕНИЕ РЕДАКТИРОВАНИЕМ ---
|
title: 'Геолокация'
|
||||||
// Кнопки редактирования
|
}).addTo(editableLayerGroup);
|
||||||
const editControlsDiv = L.DomUtil.create('div', 'map-controls');
|
marker.bindPopup('Геолокация');
|
||||||
editControlsDiv.style.position = 'absolute';
|
|
||||||
editControlsDiv.style.top = '10px';
|
// Синхронизация при изменении формы
|
||||||
editControlsDiv.style.right = '10px';
|
function syncFromForm() {
|
||||||
editControlsDiv.style.zIndex = '1000';
|
const lat = parseFloat(document.getElementById('id_geo_latitude').value);
|
||||||
editControlsDiv.style.background = 'white';
|
const lng = parseFloat(document.getElementById('id_geo_longitude').value);
|
||||||
editControlsDiv.style.padding = '10px';
|
if (!isNaN(lat) && !isNaN(lng)) {
|
||||||
editControlsDiv.style.borderRadius = '4px';
|
marker.setLatLng([lat, lng]);
|
||||||
editControlsDiv.style.boxShadow = '0 0 10px rgba(0,0,0,0.2)';
|
}
|
||||||
editControlsDiv.innerHTML = `
|
}
|
||||||
|
|
||||||
|
// Синхронизация при перетаскивании (только если активировано)
|
||||||
|
marker.on('dragend', function (event) {
|
||||||
|
const latLng = event.target.getLatLng();
|
||||||
|
document.getElementById('id_geo_latitude').value = latLng.lat.toFixed(6);
|
||||||
|
document.getElementById('id_geo_longitude').value = latLng.lng.toFixed(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Добавляем методы для управления
|
||||||
|
marker.enableEditing = function () {
|
||||||
|
this.dragging.enable();
|
||||||
|
this.openPopup();
|
||||||
|
};
|
||||||
|
|
||||||
|
marker.disableEditing = function () {
|
||||||
|
this.dragging.disable();
|
||||||
|
this.closePopup();
|
||||||
|
};
|
||||||
|
|
||||||
|
marker.syncFromForm = syncFromForm;
|
||||||
|
|
||||||
|
// Устанавливаем начальные координаты из полей формы
|
||||||
|
function initMarkersFromForm() {
|
||||||
|
const geoLat = parseFloat(document.getElementById('id_geo_latitude').value) || 55.75;
|
||||||
|
const geoLng = parseFloat(document.getElementById('id_geo_longitude').value) || 37.62;
|
||||||
|
marker.setLatLng([geoLat, geoLng]);
|
||||||
|
|
||||||
|
// Центрируем карту на маркере
|
||||||
|
map.setView(marker.getLatLng(), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Настройка формы для синхронизации с маркером
|
||||||
|
function setupFormChange(latFieldId, lngFieldId, marker) {
|
||||||
|
const latField = document.getElementById(latFieldId);
|
||||||
|
const lngField = document.getElementById(lngFieldId);
|
||||||
|
|
||||||
|
[latField, lngField].forEach(field => {
|
||||||
|
field.addEventListener('change', function () {
|
||||||
|
const lat = parseFloat(latField.value);
|
||||||
|
const lng = parseFloat(lngField.value);
|
||||||
|
|
||||||
|
if (!isNaN(lat) && !isNaN(lng)) {
|
||||||
|
marker.setLatLng([lat, lng]);
|
||||||
|
map.setView(marker.getLatLng(), 10);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализация
|
||||||
|
initMarkersFromForm();
|
||||||
|
// Настройка формы для синхронизации с маркером
|
||||||
|
setupFormChange('id_geo_latitude', 'id_geo_longitude', marker);
|
||||||
|
// --- УПРАВЛЕНИЕ РЕДАКТИРОВАНИЕМ ---
|
||||||
|
// Кнопки редактирования
|
||||||
|
const editControlsDiv = L.DomUtil.create('div', 'map-controls');
|
||||||
|
editControlsDiv.style.position = 'absolute';
|
||||||
|
editControlsDiv.style.top = '10px';
|
||||||
|
editControlsDiv.style.right = '10px';
|
||||||
|
editControlsDiv.style.zIndex = '1000';
|
||||||
|
editControlsDiv.style.background = 'white';
|
||||||
|
editControlsDiv.style.padding = '10px';
|
||||||
|
editControlsDiv.style.borderRadius = '4px';
|
||||||
|
editControlsDiv.style.boxShadow = '0 0 10px rgba(0,0,0,0.2)';
|
||||||
|
editControlsDiv.innerHTML = `
|
||||||
<div class="map-controls">
|
<div class="map-controls">
|
||||||
<button type="button" id="edit-btn" class="map-control-btn edit">Редактировать</button>
|
<button type="button" id="edit-btn" class="map-control-btn edit">Редактировать</button>
|
||||||
<button type="button" id="save-btn" class="map-control-btn save" disabled>Сохранить</button>
|
<button type="button" id="save-btn" class="map-control-btn save" disabled>Сохранить</button>
|
||||||
<button type="button" id="cancel-btn" class="map-control-btn cancel" disabled>Отмена</button>
|
<button type="button" id="cancel-btn" class="map-control-btn cancel" disabled>Отмена</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
map.getContainer().appendChild(editControlsDiv);
|
map.getContainer().appendChild(editControlsDiv);
|
||||||
|
|
||||||
let isEditing = false;
|
let isEditing = false;
|
||||||
|
|
||||||
// Сохраняем начальные координаты для отмены
|
// Сохраняем начальные координаты для отмены
|
||||||
const initialPosition = marker.getLatLng();
|
const initialPosition = marker.getLatLng();
|
||||||
|
|
||||||
// Включение редактирования
|
// Включение редактирования
|
||||||
document.getElementById('edit-btn').addEventListener('click', function() {
|
document.getElementById('edit-btn').addEventListener('click', function () {
|
||||||
if (isEditing) return;
|
if (isEditing) return;
|
||||||
|
|
||||||
isEditing = true;
|
isEditing = true;
|
||||||
document.getElementById('edit-btn').classList.add('active');
|
document.getElementById('edit-btn').classList.add('active');
|
||||||
document.getElementById('save-btn').disabled = false;
|
document.getElementById('save-btn').disabled = false;
|
||||||
document.getElementById('cancel-btn').disabled = false;
|
document.getElementById('cancel-btn').disabled = false;
|
||||||
|
|
||||||
// Включаем drag для маркера
|
// Включаем drag для маркера
|
||||||
marker.enableEditing();
|
marker.enableEditing();
|
||||||
|
|
||||||
// Показываем подсказку
|
// Показываем подсказку
|
||||||
L.popup()
|
L.popup()
|
||||||
.setLatLng(map.getCenter())
|
.setLatLng(map.getCenter())
|
||||||
.setContent('Перетаскивайте маркер. Нажмите "Сохранить" или "Отмена".')
|
.setContent('Перетаскивайте маркер. Нажмите "Сохранить" или "Отмена".')
|
||||||
.openOn(map);
|
.openOn(map);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Сохранение изменений
|
// Сохранение изменений
|
||||||
document.getElementById('save-btn').addEventListener('click', function() {
|
document.getElementById('save-btn').addEventListener('click', function () {
|
||||||
if (!isEditing) return;
|
if (!isEditing) return;
|
||||||
|
|
||||||
isEditing = false;
|
isEditing = false;
|
||||||
document.getElementById('edit-btn').classList.remove('active');
|
document.getElementById('edit-btn').classList.remove('active');
|
||||||
document.getElementById('save-btn').disabled = true;
|
document.getElementById('save-btn').disabled = true;
|
||||||
document.getElementById('cancel-btn').disabled = true;
|
document.getElementById('cancel-btn').disabled = true;
|
||||||
|
|
||||||
// Отключаем редактирование
|
// Отключаем редактирование
|
||||||
marker.disableEditing();
|
marker.disableEditing();
|
||||||
|
|
||||||
// Обновляем начальную позицию
|
// Обновляем начальную позицию
|
||||||
initialPosition.lat = marker.getLatLng().lat;
|
initialPosition.lat = marker.getLatLng().lat;
|
||||||
initialPosition.lng = marker.getLatLng().lng;
|
initialPosition.lng = marker.getLatLng().lng;
|
||||||
|
|
||||||
// Убираем попап подсказки
|
// Убираем попап подсказки
|
||||||
map.closePopup();
|
map.closePopup();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Отмена изменений
|
// Отмена изменений
|
||||||
document.getElementById('cancel-btn').addEventListener('click', function() {
|
document.getElementById('cancel-btn').addEventListener('click', function () {
|
||||||
if (!isEditing) return;
|
if (!isEditing) return;
|
||||||
|
|
||||||
isEditing = false;
|
isEditing = false;
|
||||||
document.getElementById('edit-btn').classList.remove('active');
|
document.getElementById('edit-btn').classList.remove('active');
|
||||||
document.getElementById('save-btn').disabled = true;
|
document.getElementById('save-btn').disabled = true;
|
||||||
document.getElementById('cancel-btn').disabled = true;
|
document.getElementById('cancel-btn').disabled = true;
|
||||||
|
|
||||||
// Возвращаем маркер на исходную позицию
|
// Возвращаем маркер на исходную позицию
|
||||||
marker.setLatLng(initialPosition);
|
marker.setLatLng(initialPosition);
|
||||||
|
|
||||||
// Отключаем редактирование
|
// Отключаем редактирование
|
||||||
marker.disableEditing();
|
marker.disableEditing();
|
||||||
|
|
||||||
// Синхронизируем форму с исходным значением
|
// Синхронизируем форму с исходным значением
|
||||||
document.getElementById('id_geo_latitude').value = initialPosition.lat.toFixed(6);
|
document.getElementById('id_geo_latitude').value = initialPosition.lat.toFixed(6);
|
||||||
document.getElementById('id_geo_longitude').value = initialPosition.lng.toFixed(6);
|
document.getElementById('id_geo_longitude').value = initialPosition.lng.toFixed(6);
|
||||||
map.closePopup();
|
map.closePopup();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Легенда
|
// Легенда
|
||||||
const legend = L.control({ position: 'bottomright' });
|
const legend = L.control({ position: 'bottomright' });
|
||||||
|
|
||||||
legend.onAdd = function() {
|
legend.onAdd = function () {
|
||||||
const div = L.DomUtil.create('div', 'info legend');
|
const div = L.DomUtil.create('div', 'info legend');
|
||||||
div.style.fontSize = '14px';
|
div.style.fontSize = '14px';
|
||||||
div.style.backgroundColor = 'white';
|
div.style.backgroundColor = 'white';
|
||||||
div.style.padding = '10px';
|
div.style.padding = '10px';
|
||||||
div.style.borderRadius = '4px';
|
div.style.borderRadius = '4px';
|
||||||
div.style.boxShadow = '0 0 10px rgba(0,0,0,0.2)';
|
div.style.boxShadow = '0 0 10px rgba(0,0,0,0.2)';
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<h5>Легенда</h5>
|
<h5>Легенда</h5>
|
||||||
<div><span style="color: blue; font-weight: bold;">•</span> Геолокация</div>
|
<div><span style="color: blue; font-weight: bold;">•</span> Геолокация</div>
|
||||||
`;
|
`;
|
||||||
return div;
|
return div;
|
||||||
};
|
};
|
||||||
|
|
||||||
legend.addTo(map);
|
legend.addTo(map);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{% load static %}
|
||||||
|
<div class="checkbox-multiselect-wrapper" data-widget-id="{{ widget.attrs.id }}">
|
||||||
|
<div class="multiselect-input-container">
|
||||||
|
<div class="multiselect-tags" id="{{ widget.attrs.id }}_tags"></div>
|
||||||
|
<input type="text"
|
||||||
|
class="multiselect-search form-control"
|
||||||
|
placeholder="{{ widget.attrs.placeholder|default:'Выберите элементы...' }}"
|
||||||
|
id="{{ widget.attrs.id }}_search"
|
||||||
|
autocomplete="off">
|
||||||
|
<button type="button" class="multiselect-clear" id="{{ widget.attrs.id }}_clear" title="Очистить все">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" id="{{ widget.attrs.id }}_dropdown">
|
||||||
|
<div class="multiselect-options">
|
||||||
|
{% for group_name, group_choices, group_index in widget.optgroups %}
|
||||||
|
{% for option in group_choices %}
|
||||||
|
<label class="multiselect-option">
|
||||||
|
<input type="checkbox"
|
||||||
|
name="{{ widget.name }}"
|
||||||
|
value="{{ option.value }}"
|
||||||
|
{% if option.selected %}checked{% endif %}
|
||||||
|
data-label="{{ option.label }}">
|
||||||
|
<span class="option-label">{{ option.label }}</span>
|
||||||
|
</label>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -37,10 +37,9 @@ class SourceListView(LoginRequiredMixin, View):
|
|||||||
date_to = request.GET.get("date_to", "").strip()
|
date_to = request.GET.get("date_to", "").strip()
|
||||||
|
|
||||||
# Get all Source objects with query optimization
|
# Get all Source objects with query optimization
|
||||||
sources = Source.objects.select_related(
|
# Using annotate to count ObjItems efficiently (single query with GROUP BY)
|
||||||
'created_by__user',
|
# Using prefetch_related for reverse ForeignKey relationships to avoid N+1 queries
|
||||||
'updated_by__user'
|
sources = Source.objects.prefetch_related(
|
||||||
).prefetch_related(
|
|
||||||
'source_objitems',
|
'source_objitems',
|
||||||
'source_objitems__parameter_obj',
|
'source_objitems__parameter_obj',
|
||||||
'source_objitems__geo_obj'
|
'source_objitems__geo_obj'
|
||||||
@@ -164,8 +163,6 @@ class SourceListView(LoginRequiredMixin, View):
|
|||||||
'objitem_count': objitem_count,
|
'objitem_count': objitem_count,
|
||||||
'created_at': source.created_at,
|
'created_at': source.created_at,
|
||||||
'updated_at': source.updated_at,
|
'updated_at': source.updated_at,
|
||||||
'created_by': source.created_by,
|
|
||||||
'updated_by': source.updated_by,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
# Prepare context for template
|
# Prepare context for template
|
||||||
|
|||||||
20
dbapp/mainapp/widgets.py
Normal file
20
dbapp/mainapp/widgets.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
"""
|
||||||
|
Custom widgets for forms.
|
||||||
|
"""
|
||||||
|
from django import forms
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
|
||||||
|
class CheckboxSelectMultipleWidget(forms.CheckboxSelectMultiple):
|
||||||
|
"""
|
||||||
|
Custom widget that displays selected items as tags in an input field
|
||||||
|
with a dropdown containing checkboxes for selection.
|
||||||
|
"""
|
||||||
|
|
||||||
|
template_name = 'mainapp/widgets/checkbox_select_multiple.html'
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
css = {
|
||||||
|
'all': ('css/checkbox-select-multiple.css',)
|
||||||
|
}
|
||||||
|
js = ('js/checkbox-select-multiple.js',)
|
||||||
Reference in New Issue
Block a user