Реализовал систему разрешений
This commit is contained in:
@@ -116,6 +116,7 @@ TEMPLATES = [
|
|||||||
"django.template.context_processors.request",
|
"django.template.context_processors.request",
|
||||||
"django.contrib.auth.context_processors.auth",
|
"django.contrib.auth.context_processors.auth",
|
||||||
"django.contrib.messages.context_processors.messages",
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
"mainapp.context_processors.user_permissions",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -147,15 +148,15 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||||||
{
|
{
|
||||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||||
},
|
},
|
||||||
{
|
# {
|
||||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
# "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||||
},
|
# },
|
||||||
{
|
# {
|
||||||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
# "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||||
},
|
# },
|
||||||
{
|
# {
|
||||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
# "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||||
},
|
# },
|
||||||
]
|
]
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
@@ -32,12 +32,14 @@ from .models import (
|
|||||||
Geo,
|
Geo,
|
||||||
ObjItem,
|
ObjItem,
|
||||||
CustomUser,
|
CustomUser,
|
||||||
|
UserPermission,
|
||||||
Band,
|
Band,
|
||||||
Source,
|
Source,
|
||||||
TechAnalyze,
|
TechAnalyze,
|
||||||
SourceRequest,
|
SourceRequest,
|
||||||
SourceRequestStatusHistory,
|
SourceRequestStatusHistory,
|
||||||
)
|
)
|
||||||
|
from .permissions import PERMISSIONS, DEFAULT_ROLE_PERMISSIONS
|
||||||
from .filters import (
|
from .filters import (
|
||||||
GeoKupDistanceFilter,
|
GeoKupDistanceFilter,
|
||||||
GeoValidDistanceFilter,
|
GeoValidDistanceFilter,
|
||||||
@@ -99,6 +101,19 @@ class CustomUserInline(admin.StackedInline):
|
|||||||
model = CustomUser
|
model = CustomUser
|
||||||
can_delete = False
|
can_delete = False
|
||||||
verbose_name_plural = "Дополнительная информация пользователя"
|
verbose_name_plural = "Дополнительная информация пользователя"
|
||||||
|
filter_horizontal = ('user_permissions',)
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'fields': ('role',)
|
||||||
|
}),
|
||||||
|
('Индивидуальные разрешения', {
|
||||||
|
'fields': ('use_custom_permissions', 'user_permissions'),
|
||||||
|
'classes': ('collapse',),
|
||||||
|
'description': 'Если включено "Использовать индивидуальные разрешения", '
|
||||||
|
'будут использоваться выбранные разрешения вместо прав роли по умолчанию.'
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class LocationForm(forms.ModelForm):
|
class LocationForm(forms.ModelForm):
|
||||||
@@ -195,6 +210,88 @@ class UserAdmin(BaseUserAdmin):
|
|||||||
admin.site.register(User, UserAdmin)
|
admin.site.register(User, UserAdmin)
|
||||||
|
|
||||||
|
|
||||||
|
class UserPermissionForm(forms.ModelForm):
|
||||||
|
"""Форма для UserPermission с выбором из списка разрешений."""
|
||||||
|
|
||||||
|
code = forms.ChoiceField(
|
||||||
|
choices=[],
|
||||||
|
label="Код разрешения",
|
||||||
|
help_text="Выберите разрешение из списка"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['code'].choices = PERMISSIONS[:28] # Используем PERMISSION_CHOICES
|
||||||
|
# Преобразуем в формат (code, name)
|
||||||
|
self.fields['code'].choices = [(code, name) for code, name, _ in PERMISSIONS]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = UserPermission
|
||||||
|
fields = ['code']
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(UserPermission)
|
||||||
|
class UserPermissionAdmin(BaseAdmin):
|
||||||
|
"""Админ-панель для модели UserPermission."""
|
||||||
|
|
||||||
|
form = UserPermissionForm
|
||||||
|
list_display = ('code', 'get_name', 'get_description')
|
||||||
|
search_fields = ('code',)
|
||||||
|
ordering = ('code',)
|
||||||
|
|
||||||
|
def get_name(self, obj):
|
||||||
|
"""Возвращает название разрешения."""
|
||||||
|
from .permissions import PERMISSION_CHOICES
|
||||||
|
choices_dict = dict(PERMISSION_CHOICES)
|
||||||
|
return choices_dict.get(obj.code, '-')
|
||||||
|
get_name.short_description = 'Название'
|
||||||
|
|
||||||
|
def get_description(self, obj):
|
||||||
|
"""Возвращает описание разрешения."""
|
||||||
|
from .permissions import PERMISSION_DESCRIPTIONS
|
||||||
|
return PERMISSION_DESCRIPTIONS.get(obj.code, '-')
|
||||||
|
get_description.short_description = 'Описание'
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(CustomUser)
|
||||||
|
class CustomUserAdmin(BaseAdmin):
|
||||||
|
"""Админ-панель для модели CustomUser с управлением разрешениями."""
|
||||||
|
|
||||||
|
list_display = ('user', 'role', 'use_custom_permissions', 'permissions_count')
|
||||||
|
list_filter = ('role', 'use_custom_permissions')
|
||||||
|
search_fields = ('user__username', 'user__first_name', 'user__last_name')
|
||||||
|
filter_horizontal = ('user_permissions',)
|
||||||
|
ordering = ('user__username',)
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
('Основная информация', {
|
||||||
|
'fields': ('user', 'role')
|
||||||
|
}),
|
||||||
|
('Индивидуальные разрешения', {
|
||||||
|
'fields': ('use_custom_permissions', 'user_permissions'),
|
||||||
|
'description': 'Если включено "Использовать индивидуальные разрешения", '
|
||||||
|
'будут использоваться выбранные разрешения вместо прав роли по умолчанию.'
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
def permissions_count(self, obj):
|
||||||
|
"""Показывает количество индивидуальных разрешений."""
|
||||||
|
if obj.use_custom_permissions:
|
||||||
|
count = obj.user_permissions.count()
|
||||||
|
return f'{count} (индивид.)'
|
||||||
|
return f'По роли ({obj.role})'
|
||||||
|
permissions_count.short_description = 'Разрешения'
|
||||||
|
|
||||||
|
def get_readonly_fields(self, request, obj=None):
|
||||||
|
"""User поле только для чтения при редактировании."""
|
||||||
|
if obj:
|
||||||
|
return ('user',)
|
||||||
|
return ()
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
js = ('admin/js/permissions_admin.js',)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Custom Admin Actions
|
# Custom Admin Actions
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
27
dbapp/mainapp/context_processors.py
Normal file
27
dbapp/mainapp/context_processors.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
"""
|
||||||
|
Context processors для mainapp.
|
||||||
|
|
||||||
|
Добавляет глобальные переменные во все шаблоны.
|
||||||
|
"""
|
||||||
|
from .permissions import get_user_permissions, PERMISSIONS
|
||||||
|
|
||||||
|
|
||||||
|
def user_permissions(request):
|
||||||
|
"""
|
||||||
|
Добавляет права пользователя в контекст шаблона.
|
||||||
|
|
||||||
|
Использование в шаблонах:
|
||||||
|
{% if 'source_create' in user_perms %}
|
||||||
|
...
|
||||||
|
{% endif %}
|
||||||
|
"""
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
perms = get_user_permissions(request.user)
|
||||||
|
return {
|
||||||
|
'user_perms': perms,
|
||||||
|
'all_permissions': PERMISSIONS,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
'user_perms': [],
|
||||||
|
'all_permissions': PERMISSIONS,
|
||||||
|
}
|
||||||
38
dbapp/mainapp/management/commands/init_permissions.py
Normal file
38
dbapp/mainapp/management/commands/init_permissions.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
"""
|
||||||
|
Management command для инициализации разрешений в базе данных.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python manage.py init_permissions
|
||||||
|
"""
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from mainapp.models import UserPermission
|
||||||
|
from mainapp.permissions import PERMISSIONS
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Инициализирует все разрешения в базе данных'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
created_count = 0
|
||||||
|
existing_count = 0
|
||||||
|
|
||||||
|
for code, name, description in PERMISSIONS:
|
||||||
|
permission, created = UserPermission.objects.get_or_create(code=code)
|
||||||
|
if created:
|
||||||
|
created_count += 1
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(f'Создано разрешение: {code} - {name}')
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
existing_count += 1
|
||||||
|
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(
|
||||||
|
f'\nГотово! Создано: {created_count}, уже существовало: {existing_count}'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Показываем все разрешения
|
||||||
|
self.stdout.write('\nВсе разрешения в системе:')
|
||||||
|
for code, name, description in PERMISSIONS:
|
||||||
|
self.stdout.write(f' - {code}: {name}')
|
||||||
35
dbapp/mainapp/migrations/0025_add_user_permissions.py
Normal file
35
dbapp/mainapp/migrations/0025_add_user_permissions.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-12-12 14:02
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mainapp', '0024_change_objectmark_timestamp_editable'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UserPermission',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('code', models.CharField(choices=[('source_create', 'Создание источника'), ('source_edit', 'Редактирование источника'), ('source_delete', 'Удаление источников'), ('source_import_excel', 'Импорт из Excel'), ('source_import_csv', 'Импорт из CSV'), ('source_averaging', 'Усреднение точек'), ('source_tech_analyze', 'Технический анализ'), ('source_merge', 'Объединение источников'), ('request_create', 'Создание заявки'), ('request_edit', 'Редактирование заявки'), ('request_delete', 'Удаление заявки'), ('request_import', 'Импорт заявок'), ('objitem_create', 'Создание точки'), ('objitem_edit', 'Редактирование точки'), ('objitem_delete', 'Удаление точки'), ('satellite_create', 'Создание спутника'), ('satellite_edit', 'Редактирование спутника'), ('satellite_delete', 'Удаление спутника'), ('tech_analyze_create', 'Создание тех. анализа'), ('tech_analyze_edit', 'Редактирование тех. анализа'), ('tech_analyze_delete', 'Удаление тех. анализа'), ('mark_create', 'Создание отметки'), ('mark_edit', 'Редактирование отметки'), ('statistics_view', 'Просмотр статистики'), ('kubsat_view', 'Просмотр Кубсат'), ('kubsat_edit', 'Редактирование Кубсат'), ('lyngsat_parse', 'Парсинг LyngSat'), ('admin_access', 'Доступ к админ-панели')], db_index=True, help_text='Уникальный код разрешения', max_length=50, verbose_name='Код разрешения')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Разрешение',
|
||||||
|
'verbose_name_plural': 'Разрешения',
|
||||||
|
'ordering': ['code'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customuser',
|
||||||
|
name='use_custom_permissions',
|
||||||
|
field=models.BooleanField(default=False, help_text='Если включено - используются индивидуальные разрешения вместо прав роли', verbose_name='Использовать индивидуальные разрешения'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customuser',
|
||||||
|
name='user_permissions',
|
||||||
|
field=models.ManyToManyField(blank=True, help_text='Если указаны - используются вместо прав роли по умолчанию', related_name='users', to='mainapp.userpermission', verbose_name='Индивидуальные разрешения'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -24,6 +24,37 @@ def get_default_standard():
|
|||||||
return obj.id
|
return obj.id
|
||||||
|
|
||||||
|
|
||||||
|
def get_permission_choices():
|
||||||
|
"""Ленивая загрузка choices для избежания циклического импорта."""
|
||||||
|
from .permissions import PERMISSION_CHOICES
|
||||||
|
return PERMISSION_CHOICES
|
||||||
|
|
||||||
|
|
||||||
|
class UserPermission(models.Model):
|
||||||
|
"""
|
||||||
|
Модель разрешения пользователя.
|
||||||
|
|
||||||
|
Хранит гранулярные разрешения для конкретных действий в системе.
|
||||||
|
"""
|
||||||
|
|
||||||
|
code = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
verbose_name="Код разрешения",
|
||||||
|
db_index=True,
|
||||||
|
help_text="Уникальный код разрешения",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
from .permissions import PERMISSION_CHOICES
|
||||||
|
choices_dict = dict(PERMISSION_CHOICES)
|
||||||
|
return choices_dict.get(self.code, self.code)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Разрешение"
|
||||||
|
verbose_name_plural = "Разрешения"
|
||||||
|
ordering = ["code"]
|
||||||
|
|
||||||
|
|
||||||
class CustomUser(models.Model):
|
class CustomUser(models.Model):
|
||||||
"""
|
"""
|
||||||
Расширенная модель пользователя с ролями.
|
Расширенная модель пользователя с ролями.
|
||||||
@@ -54,6 +85,22 @@ class CustomUser(models.Model):
|
|||||||
db_index=True,
|
db_index=True,
|
||||||
help_text="Роль пользователя в системе",
|
help_text="Роль пользователя в системе",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Индивидуальные разрешения (если пусто - используются права роли по умолчанию)
|
||||||
|
user_permissions = models.ManyToManyField(
|
||||||
|
UserPermission,
|
||||||
|
related_name="users",
|
||||||
|
verbose_name="Индивидуальные разрешения",
|
||||||
|
blank=True,
|
||||||
|
help_text="Если указаны - используются вместо прав роли по умолчанию",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Флаг использования индивидуальных разрешений
|
||||||
|
use_custom_permissions = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name="Использовать индивидуальные разрешения",
|
||||||
|
help_text="Если включено - используются индивидуальные разрешения вместо прав роли",
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return (
|
return (
|
||||||
@@ -61,6 +108,19 @@ class CustomUser(models.Model):
|
|||||||
if self.user.first_name and self.user.last_name
|
if self.user.first_name and self.user.last_name
|
||||||
else self.user.username
|
else self.user.username
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def has_perm(self, permission_code):
|
||||||
|
"""
|
||||||
|
Проверяет наличие разрешения у пользователя.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
permission_code: Код разрешения
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True если пользователь имеет разрешение
|
||||||
|
"""
|
||||||
|
from .permissions import has_permission
|
||||||
|
return has_permission(self.user, permission_code)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Пользователь"
|
verbose_name = "Пользователь"
|
||||||
|
|||||||
251
dbapp/mainapp/permissions.py
Normal file
251
dbapp/mainapp/permissions.py
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
"""
|
||||||
|
Система гранулярных прав доступа.
|
||||||
|
|
||||||
|
Определяет все доступные разрешения и функции для их проверки.
|
||||||
|
"""
|
||||||
|
from functools import wraps
|
||||||
|
from django.http import JsonResponse, HttpResponseForbidden
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.contrib import messages
|
||||||
|
|
||||||
|
|
||||||
|
# Определение всех разрешений в системе
|
||||||
|
# Формат: (код, название, описание)
|
||||||
|
PERMISSIONS = [
|
||||||
|
# Source List Page - Toolbar buttons
|
||||||
|
('source_create', 'Создание источника', 'Кнопка "Создать" на странице списка источников'),
|
||||||
|
('source_edit', 'Редактирование источника', 'Кнопка редактирования источника'),
|
||||||
|
('source_delete', 'Удаление источников', 'Кнопка "Удалить" на странице списка источников'),
|
||||||
|
('source_import_excel', 'Импорт из Excel', 'Кнопка "Excel" для загрузки данных'),
|
||||||
|
('source_import_csv', 'Импорт из CSV', 'Кнопка "CSV" для загрузки данных'),
|
||||||
|
('source_averaging', 'Усреднение точек', 'Кнопка "Усреднение" на странице списка источников'),
|
||||||
|
('source_tech_analyze', 'Технический анализ', 'Кнопка "Тех. анализ" на странице списка источников'),
|
||||||
|
('source_merge', 'Объединение источников', 'Кнопка "Объединить" в offcanvas списка'),
|
||||||
|
|
||||||
|
# Source Requests
|
||||||
|
('request_create', 'Создание заявки', 'Создание новой заявки на источник'),
|
||||||
|
('request_edit', 'Редактирование заявки', 'Редактирование существующей заявки'),
|
||||||
|
('request_delete', 'Удаление заявки', 'Удаление заявки на источник'),
|
||||||
|
('request_import', 'Импорт заявок', 'Импорт заявок из файла'),
|
||||||
|
|
||||||
|
# ObjItem (Points)
|
||||||
|
('objitem_create', 'Создание точки', 'Создание новой точки ГЛ'),
|
||||||
|
('objitem_edit', 'Редактирование точки', 'Редактирование точки ГЛ'),
|
||||||
|
('objitem_delete', 'Удаление точки', 'Удаление точки ГЛ'),
|
||||||
|
|
||||||
|
# Satellites
|
||||||
|
('satellite_create', 'Создание спутника', 'Создание нового спутника'),
|
||||||
|
('satellite_edit', 'Редактирование спутника', 'Редактирование спутника'),
|
||||||
|
('satellite_delete', 'Удаление спутника', 'Удаление спутника'),
|
||||||
|
|
||||||
|
# Tech Analyze
|
||||||
|
('tech_analyze_create', 'Создание тех. анализа', 'Создание записи технического анализа'),
|
||||||
|
('tech_analyze_edit', 'Редактирование тех. анализа', 'Редактирование записи технического анализа'),
|
||||||
|
('tech_analyze_delete', 'Удаление тех. анализа', 'Удаление записи технического анализа'),
|
||||||
|
|
||||||
|
# Signal Marks
|
||||||
|
('mark_create', 'Создание отметки', 'Создание отметки о сигнале'),
|
||||||
|
('mark_edit', 'Редактирование отметки', 'Редактирование отметки о сигнале'),
|
||||||
|
|
||||||
|
# Statistics
|
||||||
|
('statistics_view', 'Просмотр статистики', 'Доступ к странице статистики'),
|
||||||
|
|
||||||
|
# Kubsat
|
||||||
|
('kubsat_view', 'Просмотр Кубсат', 'Доступ к странице Кубсат'),
|
||||||
|
('kubsat_edit', 'Редактирование Кубсат', 'Редактирование данных Кубсат'),
|
||||||
|
|
||||||
|
# LyngSat
|
||||||
|
('lyngsat_parse', 'Парсинг LyngSat', 'Запуск парсинга LyngSat'),
|
||||||
|
|
||||||
|
# Transponders
|
||||||
|
('transponder_create', 'Создание транспондера', 'Создание нового транспондера'),
|
||||||
|
('transponder_edit', 'Редактирование транспондера', 'Редактирование транспондера'),
|
||||||
|
('transponder_delete', 'Удаление транспондера', 'Удаление транспондера'),
|
||||||
|
('transponder_import_xml', 'Импорт транспондеров из XML', 'Загрузка транспондеров из XML файла'),
|
||||||
|
|
||||||
|
# Admin access
|
||||||
|
# ('admin_access', 'Доступ к админ-панели', 'Доступ к административной панели Django'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Словарь для быстрого доступа к разрешениям
|
||||||
|
PERMISSION_CHOICES = [(code, name) for code, name, _ in PERMISSIONS]
|
||||||
|
PERMISSION_DESCRIPTIONS = {code: desc for code, _, desc in PERMISSIONS}
|
||||||
|
|
||||||
|
# Права по умолчанию для ролей
|
||||||
|
DEFAULT_ROLE_PERMISSIONS = {
|
||||||
|
'admin': [code for code, _, _ in PERMISSIONS], # Все права
|
||||||
|
'moderator': [
|
||||||
|
'source_create', 'source_edit', 'source_import_excel', 'source_import_csv',
|
||||||
|
'source_averaging', 'source_tech_analyze', 'source_merge',
|
||||||
|
'request_create', 'request_edit', 'request_import',
|
||||||
|
'objitem_create', 'objitem_edit',
|
||||||
|
'satellite_create', 'satellite_edit',
|
||||||
|
'transponder_create', 'transponder_edit', 'transponder_import_xml',
|
||||||
|
'tech_analyze_create', 'tech_analyze_edit',
|
||||||
|
'mark_create', 'mark_edit',
|
||||||
|
'statistics_view',
|
||||||
|
'kubsat_view', 'kubsat_edit',
|
||||||
|
],
|
||||||
|
'user': [
|
||||||
|
'statistics_view',
|
||||||
|
'kubsat_view',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def has_permission(user, permission_code):
|
||||||
|
"""
|
||||||
|
Проверяет, имеет ли пользователь указанное разрешение.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user: Объект User Django
|
||||||
|
permission_code: Код разрешения (строка)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True если пользователь имеет разрешение
|
||||||
|
"""
|
||||||
|
if not user or not user.is_authenticated:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Суперпользователь имеет все права
|
||||||
|
if user.is_superuser:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Получаем CustomUser
|
||||||
|
custom_user = getattr(user, 'customuser', None)
|
||||||
|
if not custom_user:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Проверяем, используются ли индивидуальные разрешения
|
||||||
|
if custom_user.use_custom_permissions:
|
||||||
|
# Используем индивидуальные разрешения
|
||||||
|
return custom_user.user_permissions.filter(code=permission_code).exists()
|
||||||
|
|
||||||
|
# Иначе используем права по умолчанию для роли
|
||||||
|
role = custom_user.role
|
||||||
|
default_perms = DEFAULT_ROLE_PERMISSIONS.get(role, [])
|
||||||
|
return permission_code in default_perms
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_permissions(user):
|
||||||
|
"""
|
||||||
|
Возвращает список кодов разрешений пользователя.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user: Объект User Django
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: Список кодов разрешений
|
||||||
|
"""
|
||||||
|
if not user or not user.is_authenticated:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if user.is_superuser:
|
||||||
|
return [code for code, _, _ in PERMISSIONS]
|
||||||
|
|
||||||
|
custom_user = getattr(user, 'customuser', None)
|
||||||
|
if not custom_user:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Проверяем, используются ли индивидуальные разрешения
|
||||||
|
if custom_user.use_custom_permissions:
|
||||||
|
return list(custom_user.user_permissions.values_list('code', flat=True))
|
||||||
|
|
||||||
|
# Права по умолчанию для роли
|
||||||
|
return DEFAULT_ROLE_PERMISSIONS.get(custom_user.role, [])
|
||||||
|
|
||||||
|
|
||||||
|
def permission_required(permission_code, redirect_url=None, raise_exception=False):
|
||||||
|
"""
|
||||||
|
Декоратор для проверки разрешения на уровне view.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
permission_code: Код разрешения
|
||||||
|
redirect_url: URL для редиректа при отсутствии прав (по умолчанию на предыдущую страницу)
|
||||||
|
raise_exception: Если True, возвращает 403 вместо редиректа
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
@permission_required('source_create')
|
||||||
|
def my_view(request):
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
def decorator(view_func):
|
||||||
|
@wraps(view_func)
|
||||||
|
def wrapper(request, *args, **kwargs):
|
||||||
|
if has_permission(request.user, permission_code):
|
||||||
|
return view_func(request, *args, **kwargs)
|
||||||
|
|
||||||
|
# Для AJAX запросов возвращаем JSON
|
||||||
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': 'У вас нет прав для выполнения этого действия'
|
||||||
|
}, status=403)
|
||||||
|
|
||||||
|
if raise_exception:
|
||||||
|
return HttpResponseForbidden('У вас нет прав для выполнения этого действия')
|
||||||
|
|
||||||
|
messages.error(request, 'У вас нет прав для выполнения этого действия')
|
||||||
|
|
||||||
|
if redirect_url:
|
||||||
|
return redirect(redirect_url)
|
||||||
|
|
||||||
|
# Редирект на предыдущую страницу или на главную
|
||||||
|
referer = request.META.get('HTTP_REFERER')
|
||||||
|
if referer:
|
||||||
|
return redirect(referer)
|
||||||
|
return redirect('mainapp:source_list')
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionRequiredMixin:
|
||||||
|
"""
|
||||||
|
Миксин для class-based views для проверки разрешений.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
class MyView(PermissionRequiredMixin, View):
|
||||||
|
permission_required = 'source_create'
|
||||||
|
# или для нескольких разрешений (любое из них):
|
||||||
|
permission_required = ['source_create', 'source_edit']
|
||||||
|
"""
|
||||||
|
permission_required = None
|
||||||
|
permission_denied_message = 'У вас нет прав для выполнения этого действия'
|
||||||
|
|
||||||
|
def has_permission(self):
|
||||||
|
"""Проверяет наличие разрешения."""
|
||||||
|
perms = self.get_permission_required()
|
||||||
|
if isinstance(perms, str):
|
||||||
|
return has_permission(self.request.user, perms)
|
||||||
|
# Для списка - проверяем наличие хотя бы одного разрешения
|
||||||
|
return any(has_permission(self.request.user, perm) for perm in perms)
|
||||||
|
|
||||||
|
def get_permission_required(self):
|
||||||
|
"""Возвращает требуемое разрешение."""
|
||||||
|
if self.permission_required is None:
|
||||||
|
raise ValueError(
|
||||||
|
f'{self.__class__.__name__} is missing the permission_required attribute.'
|
||||||
|
)
|
||||||
|
return self.permission_required
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
if not self.has_permission():
|
||||||
|
return self.handle_no_permission()
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def handle_no_permission(self):
|
||||||
|
"""Обработка отсутствия разрешения."""
|
||||||
|
# Для AJAX запросов
|
||||||
|
if self.request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': self.permission_denied_message
|
||||||
|
}, status=403)
|
||||||
|
|
||||||
|
messages.error(self.request, self.permission_denied_message)
|
||||||
|
|
||||||
|
referer = self.request.META.get('HTTP_REFERER')
|
||||||
|
if referer:
|
||||||
|
return redirect(referer)
|
||||||
|
return redirect('mainapp:source_list')
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
Использование:
|
Использование:
|
||||||
{% include 'mainapp/components/_navbar.html' %}
|
{% include 'mainapp/components/_navbar.html' %}
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
{% load permission_tags %}
|
||||||
|
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -37,18 +38,27 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{% url 'mainapp:signal_marks' %}">Отметки сигналов</a>
|
<a class="nav-link" href="{% url 'mainapp:signal_marks' %}">Отметки сигналов</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% if user|has_perm:'kubsat_view' %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{% url 'mainapp:kubsat' %}">Кубсат</a>
|
<a class="nav-link" href="{% url 'mainapp:kubsat' %}">Кубсат</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
<!-- <li class="nav-item">
|
<!-- <li class="nav-item">
|
||||||
<a class="nav-link" href="{% url 'mapsapp:3dmap' %}">3D карта</a>
|
<a class="nav-link" href="{% url 'mapsapp:3dmap' %}">3D карта</a>
|
||||||
</li> -->
|
</li> -->
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{% url 'mapsapp:2dmap' %}">Карта</a>
|
<a class="nav-link" href="{% url 'mapsapp:2dmap' %}">Карта</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% if user.customuser.role == 'admin' %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'mainapp:user_permissions_list' %}">Разрешения</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if user.customuser.role == 'admin' %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{% url 'admin:index' %}">Админ панель</a>
|
<a class="nav-link" href="{% url 'admin:index' %}">Админ панель</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{% comment %}
|
||||||
|
Компонент кнопки с проверкой разрешений.
|
||||||
|
Используется через template tag {% permission_button %}
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
{% if has_permission %}
|
||||||
|
{% if url %}
|
||||||
|
<a href="{% url url %}" class="{{ btn_class }}" {% if title %}title="{{ title }}"{% endif %}>
|
||||||
|
{% if icon %}<i class="{{ icon }}"></i>{% endif %}
|
||||||
|
{% if text %} {{ text }}{% endif %}
|
||||||
|
</a>
|
||||||
|
{% elif onclick %}
|
||||||
|
<button type="button" class="{{ btn_class }}" onclick="{{ onclick }}" {% if title %}title="{{ title }}"{% endif %}>
|
||||||
|
{% if icon %}<i class="{{ icon }}"></i>{% endif %}
|
||||||
|
{% if text %} {{ text }}{% endif %}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load permission_tags %}
|
||||||
<!-- Вкладка заявок на источники -->
|
<!-- Вкладка заявок на источники -->
|
||||||
<link href="{% static 'tabulator/css/tabulator_bootstrap5.min.css' %}" rel="stylesheet">
|
<link href="{% static 'tabulator/css/tabulator_bootstrap5.min.css' %}" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
@@ -22,15 +23,19 @@
|
|||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<h5 class="mb-0"><i class="bi bi-list-task"></i> Заявки на источники</h5>
|
<h5 class="mb-0"><i class="bi bi-list-task"></i> Заявки на источники</h5>
|
||||||
<div>
|
<div>
|
||||||
|
{% if user|has_perm:'request_delete' %}
|
||||||
<button type="button" class="btn btn-outline-danger btn-sm me-2" id="bulkDeleteBtn" onclick="bulkDeleteRequests()">
|
<button type="button" class="btn btn-outline-danger btn-sm me-2" id="bulkDeleteBtn" onclick="bulkDeleteRequests()">
|
||||||
<i class="bi bi-trash"></i> Удалить
|
<i class="bi bi-trash"></i> Удалить
|
||||||
</button>
|
</button>
|
||||||
|
{% endif %}
|
||||||
<button type="button" class="btn btn-outline-success btn-sm me-2" onclick="exportRequests()">
|
<button type="button" class="btn btn-outline-success btn-sm me-2" onclick="exportRequests()">
|
||||||
<i class="bi bi-file-earmark-excel"></i> Экспорт
|
<i class="bi bi-file-earmark-excel"></i> Экспорт
|
||||||
</button>
|
</button>
|
||||||
|
{% if user|has_perm:'request_create' %}
|
||||||
<button type="button" class="btn btn-success btn-sm" onclick="openCreateRequestModal()">
|
<button type="button" class="btn btn-success btn-sm" onclick="openCreateRequestModal()">
|
||||||
<i class="bi bi-plus-circle"></i> Создать
|
<i class="bi bi-plus-circle"></i> Создать
|
||||||
</button>
|
</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -193,22 +198,37 @@ function commentFormatter(cell) {
|
|||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Права пользователя (передаются из Django)
|
||||||
|
const userPermissions = {
|
||||||
|
canEditRequest: {% if user|has_perm:'request_edit' %}true{% else %}false{% endif %},
|
||||||
|
canDeleteRequest: {% if user|has_perm:'request_delete' %}true{% else %}false{% endif %}
|
||||||
|
};
|
||||||
|
|
||||||
// Форматтер для действий
|
// Форматтер для действий
|
||||||
function actionsFormatter(cell) {
|
function actionsFormatter(cell) {
|
||||||
const id = cell.getData().id;
|
const id = cell.getData().id;
|
||||||
return `
|
let buttons = `
|
||||||
<div class="btn-group btn-group-sm" role="group">
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
<button type="button" class="btn btn-outline-info btn-sm" onclick="showHistory(${id})" title="История">
|
<button type="button" class="btn btn-outline-info btn-sm" onclick="showHistory(${id})" title="История">
|
||||||
<i class="bi bi-clock-history"></i>
|
<i class="bi bi-clock-history"></i>
|
||||||
</button>
|
</button>`;
|
||||||
|
|
||||||
|
if (userPermissions.canEditRequest) {
|
||||||
|
buttons += `
|
||||||
<button type="button" class="btn btn-outline-warning btn-sm" onclick="openEditRequestModal(${id})" title="Редактировать">
|
<button type="button" class="btn btn-outline-warning btn-sm" onclick="openEditRequestModal(${id})" title="Редактировать">
|
||||||
<i class="bi bi-pencil"></i>
|
<i class="bi bi-pencil"></i>
|
||||||
</button>
|
</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userPermissions.canDeleteRequest) {
|
||||||
|
buttons += `
|
||||||
<button type="button" class="btn btn-outline-danger btn-sm" onclick="deleteRequest(${id})" title="Удалить">
|
<button type="button" class="btn btn-outline-danger btn-sm" onclick="deleteRequest(${id})" title="Удалить">
|
||||||
<i class="bi bi-trash"></i>
|
<i class="bi bi-trash"></i>
|
||||||
</button>
|
</button>`;
|
||||||
</div>
|
}
|
||||||
`;
|
|
||||||
|
buttons += `</div>`;
|
||||||
|
return buttons;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Инициализация Tabulator
|
// Инициализация Tabulator
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load static leaflet_tags %}
|
{% load static leaflet_tags %}
|
||||||
{% load l10n %}
|
{% load l10n %}
|
||||||
|
{% load permission_tags %}
|
||||||
|
|
||||||
{% block title %}{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать новый объект{% endif %}{% endblock %}
|
{% block title %}{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать новый объект{% endif %}{% endblock %}
|
||||||
|
|
||||||
@@ -146,12 +147,18 @@
|
|||||||
<div class="col-12 d-flex justify-content-between align-items-center">
|
<div class="col-12 d-flex justify-content-between align-items-center">
|
||||||
<h2>{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать новый объект{% endif %}</h2>
|
<h2>{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать новый объект{% endif %}</h2>
|
||||||
<div>
|
<div>
|
||||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
|
||||||
<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 %}"
|
{% if user|has_perm:'objitem_edit' %}
|
||||||
class="btn btn-danger btn-action">Удалить</a>
|
<button type="submit" form="objitem-form" class="btn btn-primary btn-action">Сохранить</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if user|has_perm:'objitem_delete' %}
|
||||||
|
<a href="{% url 'mainapp:objitem_delete' object.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}"
|
||||||
|
class="btn btn-danger btn-action">Удалить</a>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{% if user|has_perm:'objitem_create' %}
|
||||||
|
<button type="submit" form="objitem-form" class="btn btn-primary btn-action">Сохранить</button>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url 'mainapp:objitem_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}"
|
<a href="{% url 'mainapp:objitem_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}"
|
||||||
class="btn btn-secondary btn-action">Назад</a>
|
class="btn btn-secondary btn-action">Назад</a>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{% extends 'mainapp/base.html' %}
|
{% extends 'mainapp/base.html' %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load permission_tags %}
|
||||||
|
|
||||||
{% block title %}Список объектов{% endblock %}
|
{% block title %}Список объектов{% endblock %}
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
@@ -47,10 +48,12 @@
|
|||||||
|
|
||||||
<!-- Action buttons bar -->
|
<!-- Action buttons bar -->
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
{% if user|has_perm:'objitem_create' %}
|
||||||
<a href="{% url 'mainapp:objitem_create' %}" class="btn btn-success btn-sm" title="Создать новый объект">
|
<a href="{% url 'mainapp:objitem_create' %}" class="btn btn-success btn-sm" title="Создать новый объект">
|
||||||
<i class="bi bi-plus-circle"></i> Создать
|
<i class="bi bi-plus-circle"></i> Создать
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if user|has_perm:'objitem_delete' %}
|
||||||
<button type="button" class="btn btn-danger btn-sm" title="Удалить"
|
<button type="button" class="btn btn-danger btn-sm" title="Удалить"
|
||||||
onclick="deleteSelectedObjects()">
|
onclick="deleteSelectedObjects()">
|
||||||
<i class="bi bi-trash"></i> Удалить
|
<i class="bi bi-trash"></i> Удалить
|
||||||
@@ -390,7 +393,7 @@
|
|||||||
<input type="checkbox" class="form-check-input item-checkbox" value="{{ item.id }}">
|
<input type="checkbox" class="form-check-input item-checkbox" value="{{ item.id }}">
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% if item.obj.id %}{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}{% url 'mainapp:objitem_update' item.obj.id %}?{{ request.GET.urlencode }}{% else %}{% url 'mainapp:objitem_detail' item.obj.id %}?{{ request.GET.urlencode }}{% endif %}{% endif %}">{{ item.name }}</a></td>
|
<a href="{% if item.obj.id %}{% if user|has_perm:'objitem_edit' %}{% url 'mainapp:objitem_update' item.obj.id %}?{{ request.GET.urlencode }}{% else %}{% url 'mainapp:objitem_detail' item.obj.id %}?{{ request.GET.urlencode }}{% endif %}{% endif %}">{{ item.name }}</a></td>
|
||||||
<td>
|
<td>
|
||||||
{% if item.satellite_id %}
|
{% if item.satellite_id %}
|
||||||
<a href="#" class="text-decoration-underline"
|
<a href="#" class="text-decoration-underline"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends 'mainapp/base.html' %}
|
{% extends 'mainapp/base.html' %}
|
||||||
|
{% load permission_tags %}
|
||||||
|
|
||||||
{% block title %}Список спутников{% endblock %}
|
{% block title %}Список спутников{% endblock %}
|
||||||
|
|
||||||
@@ -60,10 +61,12 @@
|
|||||||
|
|
||||||
<!-- Action buttons -->
|
<!-- Action buttons -->
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
{% if user|has_perm:'satellite_create' %}
|
||||||
<a href="{% url 'mainapp:satellite_create' %}" class="btn btn-success btn-sm" title="Создать">
|
<a href="{% url 'mainapp:satellite_create' %}" class="btn btn-success btn-sm" title="Создать">
|
||||||
<i class="bi bi-plus-circle"></i> Создать
|
<i class="bi bi-plus-circle"></i> Создать
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if user|has_perm:'satellite_delete' %}
|
||||||
<button type="button" class="btn btn-danger btn-sm" title="Удалить"
|
<button type="button" class="btn btn-danger btn-sm" title="Удалить"
|
||||||
onclick="deleteSelectedSatellites()">
|
onclick="deleteSelectedSatellites()">
|
||||||
<i class="bi bi-trash"></i> Удалить
|
<i class="bi bi-trash"></i> Удалить
|
||||||
@@ -355,7 +358,7 @@
|
|||||||
<td>{{ satellite.updated_at|date:"d.m.Y H:i" }}</td>
|
<td>{{ satellite.updated_at|date:"d.m.Y H:i" }}</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<div class="d-flex gap-1 justify-content-center">
|
<div class="d-flex gap-1 justify-content-center">
|
||||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
{% if user|has_perm:'satellite_edit' %}
|
||||||
<a href="{% url 'mainapp:satellite_update' satellite.id %}"
|
<a href="{% url 'mainapp:satellite_update' satellite.id %}"
|
||||||
class="btn btn-sm btn-outline-warning"
|
class="btn btn-sm btn-outline-warning"
|
||||||
title="Редактировать спутник">
|
title="Редактировать спутник">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{% extends "mainapp/base.html" %}
|
{% extends "mainapp/base.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load permission_tags %}
|
||||||
|
|
||||||
{% block title %}Отметки сигналов{% endblock %}
|
{% block title %}Отметки сигналов{% endblock %}
|
||||||
|
|
||||||
@@ -192,13 +193,17 @@
|
|||||||
placeholder="Поиск по имени..." style="width: 200px;">
|
placeholder="Поиск по имени..." style="width: 200px;">
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
|
{% if user|has_perm:'tech_analyze_create' %}
|
||||||
<button class="btn btn-outline-primary btn-sm" onclick="openCreateModal()">
|
<button class="btn btn-outline-primary btn-sm" onclick="openCreateModal()">
|
||||||
<i class="bi bi-plus-lg"></i> Создать теханализ
|
<i class="bi bi-plus-lg"></i> Создать теханализ
|
||||||
</button>
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% if user|has_perm:'mark_create' %}
|
||||||
<button class="btn btn-success" id="save-marks-btn" onclick="saveMarks()" disabled>
|
<button class="btn btn-success" id="save-marks-btn" onclick="saveMarks()" disabled>
|
||||||
<i class="bi bi-check-lg"></i> Сохранить
|
<i class="bi bi-check-lg"></i> Сохранить
|
||||||
<span class="badge bg-light text-dark" id="marks-count">0</span>
|
<span class="badge bg-light text-dark" id="marks-count">0</span>
|
||||||
</button>
|
</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
@@ -262,6 +267,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if user|has_perm:'tech_analyze_create' %}
|
||||||
<!-- Modal for creating TechAnalyze -->
|
<!-- Modal for creating TechAnalyze -->
|
||||||
<div class="modal fade" id="createTechAnalyzeModal" tabindex="-1">
|
<div class="modal fade" id="createTechAnalyzeModal" tabindex="-1">
|
||||||
<div class="modal-dialog modal-lg">
|
<div class="modal-dialog modal-lg">
|
||||||
@@ -321,12 +327,14 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if user|has_perm:'mark_create' %}
|
||||||
<div class="form-check mb-3">
|
<div class="form-check mb-3">
|
||||||
<input class="form-check-input" type="checkbox" id="ta-add-mark" checked>
|
<input class="form-check-input" type="checkbox" id="ta-add-mark" checked>
|
||||||
<label class="form-check-label" for="ta-add-mark">
|
<label class="form-check-label" for="ta-add-mark">
|
||||||
Сразу добавить отметку "Есть сигнал"
|
Сразу добавить отметку "Есть сигнал"
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
@@ -336,6 +344,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
@@ -343,6 +352,8 @@
|
|||||||
<script>
|
<script>
|
||||||
const SATELLITE_ID = {% if selected_satellite_id %}{{ selected_satellite_id }}{% else %}null{% endif %};
|
const SATELLITE_ID = {% if selected_satellite_id %}{{ selected_satellite_id }}{% else %}null{% endif %};
|
||||||
const CSRF_TOKEN = '{{ csrf_token }}';
|
const CSRF_TOKEN = '{{ csrf_token }}';
|
||||||
|
const CAN_CREATE_MARK = {% if user|has_perm:'mark_create' %}true{% else %}false{% endif %};
|
||||||
|
const CAN_CREATE_TECH_ANALYZE = {% if user|has_perm:'tech_analyze_create' %}true{% else %}false{% endif %};
|
||||||
|
|
||||||
let entryTable = null;
|
let entryTable = null;
|
||||||
let pendingMarks = {};
|
let pendingMarks = {};
|
||||||
@@ -360,6 +371,48 @@ function selectSatellite() {
|
|||||||
function initEntryTable() {
|
function initEntryTable() {
|
||||||
if (!SATELLITE_ID) return;
|
if (!SATELLITE_ID) return;
|
||||||
|
|
||||||
|
// Базовые колонки
|
||||||
|
const columns = [
|
||||||
|
{title: "ID", field: "id", width: 60},
|
||||||
|
{title: "Имя", field: "name", width: 500},
|
||||||
|
{title: "Частота", field: "frequency", width: 120, hozAlign: "right",
|
||||||
|
formatter: c => c.getValue() ? c.getValue().toFixed(3) : '-'},
|
||||||
|
{title: "Полоса", field: "freq_range", width: 120, hozAlign: "right",
|
||||||
|
formatter: c => c.getValue() ? c.getValue().toFixed(3) : '-'},
|
||||||
|
{title: "Сим.v", field: "bod_velocity", width: 120, hozAlign: "right",
|
||||||
|
formatter: c => c.getValue() ? Math.round(c.getValue()) : '-'},
|
||||||
|
{title: "Пол.", field: "polarization", width: 105, hozAlign: "center"},
|
||||||
|
{title: "Мод.", field: "modulation", width: 95, hozAlign: "center"},
|
||||||
|
{title: "Станд.", field: "standard", width: 125},
|
||||||
|
{title: "Посл. отметка", field: "last_mark", width: 190,
|
||||||
|
formatter: function(c) {
|
||||||
|
const d = c.getValue();
|
||||||
|
if (!d) return '<span class="text-muted">—</span>';
|
||||||
|
const icon = d.mark ? '✓' : '✗';
|
||||||
|
const cls = d.mark ? 'text-success' : 'text-danger';
|
||||||
|
return `<span class="${cls}">${icon}</span> ${d.timestamp}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Добавляем колонку отметок только если есть право
|
||||||
|
if (CAN_CREATE_MARK) {
|
||||||
|
columns.push({
|
||||||
|
title: "Отметка", field: "id", width: 100, hozAlign: "center", headerSort: false,
|
||||||
|
formatter: function(c) {
|
||||||
|
const row = c.getRow().getData();
|
||||||
|
const id = row.id;
|
||||||
|
if (!row.can_add_mark) return '<span class="text-muted small">5 мин</span>';
|
||||||
|
const yesS = pendingMarks[id] === true ? 'selected' : '';
|
||||||
|
const noS = pendingMarks[id] === false ? 'selected' : '';
|
||||||
|
return `<div class="mark-btn-group">
|
||||||
|
<button type="button" class="mark-btn mark-btn-yes ${yesS}" data-id="${id}" data-val="true">✓</button>
|
||||||
|
<button type="button" class="mark-btn mark-btn-no ${noS}" data-id="${id}" data-val="false">✗</button>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
entryTable = new Tabulator("#entry-table", {
|
entryTable = new Tabulator("#entry-table", {
|
||||||
ajaxURL: "{% url 'mainapp:signal_marks_entry_api' %}",
|
ajaxURL: "{% url 'mainapp:signal_marks_entry_api' %}",
|
||||||
ajaxParams: { satellite_id: SATELLITE_ID },
|
ajaxParams: { satellite_id: SATELLITE_ID },
|
||||||
@@ -370,41 +423,7 @@ function initEntryTable() {
|
|||||||
layout: "fitColumns",
|
layout: "fitColumns",
|
||||||
height: "65vh",
|
height: "65vh",
|
||||||
placeholder: "Нет данных",
|
placeholder: "Нет данных",
|
||||||
columns: [
|
columns: columns,
|
||||||
{title: "ID", field: "id", width: 60},
|
|
||||||
{title: "Имя", field: "name", width: 500},
|
|
||||||
{title: "Частота", field: "frequency", width: 120, hozAlign: "right",
|
|
||||||
formatter: c => c.getValue() ? c.getValue().toFixed(3) : '-'},
|
|
||||||
{title: "Полоса", field: "freq_range", width: 120, hozAlign: "right",
|
|
||||||
formatter: c => c.getValue() ? c.getValue().toFixed(3) : '-'},
|
|
||||||
{title: "Сим.v", field: "bod_velocity", width: 120, hozAlign: "right",
|
|
||||||
formatter: c => c.getValue() ? Math.round(c.getValue()) : '-'},
|
|
||||||
{title: "Пол.", field: "polarization", width: 105, hozAlign: "center"},
|
|
||||||
{title: "Мод.", field: "modulation", width: 95, hozAlign: "center"},
|
|
||||||
{title: "Станд.", field: "standard", width: 125},
|
|
||||||
{title: "Посл. отметка", field: "last_mark", width: 190,
|
|
||||||
formatter: function(c) {
|
|
||||||
const d = c.getValue();
|
|
||||||
if (!d) return '<span class="text-muted">—</span>';
|
|
||||||
const icon = d.mark ? '✓' : '✗';
|
|
||||||
const cls = d.mark ? 'text-success' : 'text-danger';
|
|
||||||
return `<span class="${cls}">${icon}</span> ${d.timestamp}`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{title: "Отметка", field: "id", width: 100, hozAlign: "center", headerSort: false,
|
|
||||||
formatter: function(c) {
|
|
||||||
const row = c.getRow().getData();
|
|
||||||
const id = row.id;
|
|
||||||
if (!row.can_add_mark) return '<span class="text-muted small">5 мин</span>';
|
|
||||||
const yesS = pendingMarks[id] === true ? 'selected' : '';
|
|
||||||
const noS = pendingMarks[id] === false ? 'selected' : '';
|
|
||||||
return `<div class="mark-btn-group">
|
|
||||||
<button type="button" class="mark-btn mark-btn-yes ${yesS}" data-id="${id}" data-val="true">✓</button>
|
|
||||||
<button type="button" class="mark-btn mark-btn-no ${noS}" data-id="${id}" data-val="false">✗</button>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Делегирование событий для кнопок отметок - без перерисовки таблицы
|
// Делегирование событий для кнопок отметок - без перерисовки таблицы
|
||||||
@@ -600,12 +619,21 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
// Modal
|
// Modal
|
||||||
function openCreateModal() {
|
function openCreateModal() {
|
||||||
|
if (!CAN_CREATE_TECH_ANALYZE) {
|
||||||
|
alert('У вас нет прав для создания теханализа');
|
||||||
|
return;
|
||||||
|
}
|
||||||
document.getElementById('create-tech-analyze-form').reset();
|
document.getElementById('create-tech-analyze-form').reset();
|
||||||
document.getElementById('ta-add-mark').checked = true;
|
const addMarkCheckbox = document.getElementById('ta-add-mark');
|
||||||
|
if (addMarkCheckbox) addMarkCheckbox.checked = true;
|
||||||
new bootstrap.Modal(document.getElementById('createTechAnalyzeModal')).show();
|
new bootstrap.Modal(document.getElementById('createTechAnalyzeModal')).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTechAnalyze() {
|
function createTechAnalyze() {
|
||||||
|
if (!CAN_CREATE_TECH_ANALYZE) {
|
||||||
|
alert('У вас нет прав для создания теханализа');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const name = document.getElementById('ta-name').value.trim();
|
const name = document.getElementById('ta-name').value.trim();
|
||||||
if (!name) { alert('Укажите имя'); return; }
|
if (!name) { alert('Укажите имя'); return; }
|
||||||
|
|
||||||
@@ -627,7 +655,8 @@ function createTechAnalyze() {
|
|||||||
.then(result => {
|
.then(result => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
bootstrap.Modal.getInstance(document.getElementById('createTechAnalyzeModal')).hide();
|
bootstrap.Modal.getInstance(document.getElementById('createTechAnalyzeModal')).hide();
|
||||||
if (document.getElementById('ta-add-mark').checked) {
|
const addMarkCheckbox = document.getElementById('ta-add-mark');
|
||||||
|
if (CAN_CREATE_MARK && addMarkCheckbox && addMarkCheckbox.checked) {
|
||||||
pendingMarks[result.tech_analyze.id] = true;
|
pendingMarks[result.tech_analyze.id] = true;
|
||||||
updateMarksCount();
|
updateMarksCount();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{% extends 'mainapp/base.html' %}
|
{% extends 'mainapp/base.html' %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load static leaflet_tags %}
|
{% load static leaflet_tags %}
|
||||||
|
{% load permission_tags %}
|
||||||
|
|
||||||
{% block title %}Список объектов{% endblock %}
|
{% block title %}Список объектов{% endblock %}
|
||||||
|
|
||||||
@@ -74,21 +75,22 @@
|
|||||||
|
|
||||||
<!-- Action buttons -->
|
<!-- Action buttons -->
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
{% if user|has_perm:'source_create' %}
|
||||||
<a href="{% url 'mainapp:source_create' %}" class="btn btn-success btn-sm" title="Создать новый источник">
|
<a href="{% url 'mainapp:source_create' %}" class="btn btn-success btn-sm" title="Создать новый источник">
|
||||||
<i class="bi bi-plus-circle"></i> Создать
|
<i class="bi bi-plus-circle"></i> Создать
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- <a href="{% url 'mainapp:data_entry' %}" class="btn btn-info btn-sm" title="Ввод данных точек спутников">
|
{% if user|has_perm:'source_import_excel' %}
|
||||||
Передача точек
|
|
||||||
</a> -->
|
|
||||||
<a href="{% url 'mainapp:load_excel_data' %}" class="btn btn-primary btn-sm" title="Загрузка данных из Excel">
|
<a href="{% url 'mainapp:load_excel_data' %}" class="btn btn-primary btn-sm" title="Загрузка данных из Excel">
|
||||||
<i class="bi bi-file-earmark-excel"></i> Excel
|
<i class="bi bi-file-earmark-excel"></i> Excel
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if user|has_perm:'source_import_csv' %}
|
||||||
<a href="{% url 'mainapp:load_csv_data' %}" class="btn btn-success btn-sm" title="Загрузка данных из CSV">
|
<a href="{% url 'mainapp:load_csv_data' %}" class="btn btn-success btn-sm" title="Загрузка данных из CSV">
|
||||||
<i class="bi bi-file-earmark-text"></i> CSV
|
<i class="bi bi-file-earmark-text"></i> CSV
|
||||||
</a>
|
</a>
|
||||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
{% endif %}
|
||||||
|
{% if user|has_perm:'source_delete' %}
|
||||||
<button type="button" class="btn btn-danger btn-sm" title="Удалить"
|
<button type="button" class="btn btn-danger btn-sm" title="Удалить"
|
||||||
onclick="deleteSelectedSources()">
|
onclick="deleteSelectedSources()">
|
||||||
<i class="bi bi-trash"></i> Удалить
|
<i class="bi bi-trash"></i> Удалить
|
||||||
@@ -98,15 +100,21 @@
|
|||||||
onclick="showSelectedOnMap()">
|
onclick="showSelectedOnMap()">
|
||||||
<i class="bi bi-map"></i> Карта
|
<i class="bi bi-map"></i> Карта
|
||||||
</button>
|
</button>
|
||||||
|
{% if user|has_perm:'source_averaging' %}
|
||||||
<a href="{% url 'mainapp:points_averaging' %}" class="btn btn-warning btn-sm" title="Усреднение точек">
|
<a href="{% url 'mainapp:points_averaging' %}" class="btn btn-warning btn-sm" title="Усреднение точек">
|
||||||
<i class="bi bi-calculator"></i> Усреднение
|
<i class="bi bi-calculator"></i> Усреднение
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if user|has_perm:'source_tech_analyze' %}
|
||||||
<a href="{% url 'mainapp:tech_analyze_list' %}" class="btn btn-info btn-sm" title="Технический анализ">
|
<a href="{% url 'mainapp:tech_analyze_list' %}" class="btn btn-info btn-sm" title="Технический анализ">
|
||||||
<i class="bi bi-gear-wide-connected"></i> Тех. анализ
|
<i class="bi bi-gear-wide-connected"></i> Тех. анализ
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if user|has_perm:'statistics_view' %}
|
||||||
<a href="{% url 'mainapp:statistics' %}" class="btn btn-secondary btn-sm" title="Статистика">
|
<a href="{% url 'mainapp:statistics' %}" class="btn btn-secondary btn-sm" title="Статистика">
|
||||||
<i class="bi bi-bar-chart-line"></i> Статистика
|
<i class="bi bi-bar-chart-line"></i> Статистика
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add to List Button -->
|
<!-- Add to List Button -->
|
||||||
@@ -729,7 +737,7 @@
|
|||||||
<i class="bi bi-list-task"></i>
|
<i class="bi bi-list-task"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
{% if user|has_perm:'source_edit' %}
|
||||||
<a href="{% url 'mainapp:source_update' source.id %}"
|
<a href="{% url 'mainapp:source_update' source.id %}"
|
||||||
class="btn btn-sm btn-outline-warning"
|
class="btn btn-sm btn-outline-warning"
|
||||||
title="Редактировать объект">
|
title="Редактировать объект">
|
||||||
@@ -2224,7 +2232,7 @@ function showTransponderModal(transponderId) {
|
|||||||
<button type="button" class="btn btn-outline-info btn-sm" onclick="showPlaybackAnimation()" title="Анимация движения объектов">
|
<button type="button" class="btn btn-outline-info btn-sm" onclick="showPlaybackAnimation()" title="Анимация движения объектов">
|
||||||
<i class="bi bi-play-circle"></i> Анимация
|
<i class="bi bi-play-circle"></i> Анимация
|
||||||
</button>
|
</button>
|
||||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
{% if user|has_perm:'source_merge' %}
|
||||||
<button type="button" class="btn btn-outline-success btn-sm" onclick="mergeSelectedSources()">
|
<button type="button" class="btn btn-outline-success btn-sm" onclick="mergeSelectedSources()">
|
||||||
<i class="bi bi-union"></i> Объединить
|
<i class="bi bi-union"></i> Объединить
|
||||||
</button>
|
</button>
|
||||||
@@ -2384,11 +2392,13 @@ function showTransponderModal(transponderId) {
|
|||||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
{% if user|has_perm:'request_create' %}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<button type="button" class="btn btn-success btn-sm" onclick="openCreateRequestModalForSource()">
|
<button type="button" class="btn btn-success btn-sm" onclick="openCreateRequestModalForSource()">
|
||||||
<i class="bi bi-plus-circle"></i> Создать заявку
|
<i class="bi bi-plus-circle"></i> Создать заявку
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div id="requestsLoadingSpinner" class="text-center py-4">
|
<div id="requestsLoadingSpinner" class="text-center py-4">
|
||||||
<div class="spinner-border text-primary" role="status">
|
<div class="spinner-border text-primary" role="status">
|
||||||
<span class="visually-hidden">Загрузка...</span>
|
<span class="visually-hidden">Загрузка...</span>
|
||||||
@@ -2648,12 +2658,16 @@ function showSourceRequests(sourceId) {
|
|||||||
<button type="button" class="btn btn-outline-info" onclick="showRequestHistory(${req.id})" title="История">
|
<button type="button" class="btn btn-outline-info" onclick="showRequestHistory(${req.id})" title="История">
|
||||||
<i class="bi bi-clock-history"></i>
|
<i class="bi bi-clock-history"></i>
|
||||||
</button>
|
</button>
|
||||||
|
{% if user|has_perm:'request_edit' %}
|
||||||
<button type="button" class="btn btn-outline-warning" onclick="editSourceRequest(${req.id})" title="Редактировать">
|
<button type="button" class="btn btn-outline-warning" onclick="editSourceRequest(${req.id})" title="Редактировать">
|
||||||
<i class="bi bi-pencil"></i>
|
<i class="bi bi-pencil"></i>
|
||||||
</button>
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% if user|has_perm:'request_delete' %}
|
||||||
<button type="button" class="btn btn-outline-danger" onclick="deleteSourceRequest(${req.id})" title="Удалить">
|
<button type="button" class="btn btn-outline-danger" onclick="deleteSourceRequest(${req.id})" title="Удалить">
|
||||||
<i class="bi bi-trash"></i>
|
<i class="bi bi-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{% extends 'mainapp/base.html' %}
|
{% extends 'mainapp/base.html' %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load permission_tags %}
|
||||||
|
|
||||||
{% block title %}Тех. анализ - Список{% endblock %}
|
{% block title %}Тех. анализ - Список{% endblock %}
|
||||||
|
|
||||||
@@ -53,17 +54,21 @@
|
|||||||
|
|
||||||
<!-- Action buttons -->
|
<!-- Action buttons -->
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
|
{% if user|has_perm:'tech_analyze_create' %}
|
||||||
<a href="{% url 'mainapp:tech_analyze_entry' %}" class="btn btn-success btn-sm" title="Ввод данных">
|
<a href="{% url 'mainapp:tech_analyze_entry' %}" class="btn btn-success btn-sm" title="Ввод данных">
|
||||||
<i class="bi bi-plus-circle"></i> Ввод данных
|
<i class="bi bi-plus-circle"></i> Ввод данных
|
||||||
</a>
|
</a>
|
||||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
{% endif %}
|
||||||
|
{% if user|has_perm:'tech_analyze_delete' %}
|
||||||
<button type="button" class="btn btn-danger btn-sm" title="Удалить выбранные" onclick="deleteSelected()">
|
<button type="button" class="btn btn-danger btn-sm" title="Удалить выбранные" onclick="deleteSelected()">
|
||||||
<i class="bi bi-trash"></i> Удалить
|
<i class="bi bi-trash"></i> Удалить
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if user|has_perm:'tech_analyze_edit' %}
|
||||||
<button type="button" class="btn btn-info btn-sm" title="Привязать к существующим точкам" onclick="showLinkModal()">
|
<button type="button" class="btn btn-info btn-sm" title="Привязать к существующим точкам" onclick="showLinkModal()">
|
||||||
<i class="bi bi-link-45deg"></i> Привязать к точкам
|
<i class="bi bi-link-45deg"></i> Привязать к точкам
|
||||||
</button>
|
</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filter Toggle Button -->
|
<!-- Filter Toggle Button -->
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends 'mainapp/base.html' %}
|
{% extends 'mainapp/base.html' %}
|
||||||
|
{% load permission_tags %}
|
||||||
|
|
||||||
{% block title %}Список транспондеров{% endblock %}
|
{% block title %}Список транспондеров{% endblock %}
|
||||||
|
|
||||||
@@ -57,13 +58,17 @@
|
|||||||
|
|
||||||
<!-- Action buttons -->
|
<!-- Action buttons -->
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
{% if user|has_perm:'transponder_create' %}
|
||||||
<a href="{% url 'mainapp:transponder_create' %}" class="btn btn-success btn-sm" title="Создать">
|
<a href="{% url 'mainapp:transponder_create' %}" class="btn btn-success btn-sm" title="Создать">
|
||||||
<i class="bi bi-plus-circle"></i> Создать
|
<i class="bi bi-plus-circle"></i> Создать
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if user|has_perm:'transponder_import_xml' %}
|
||||||
<a href="{% url 'mainapp:add_trans' %}" class="btn btn-warning btn-sm" title="Загрузить из XML">
|
<a href="{% url 'mainapp:add_trans' %}" class="btn btn-warning btn-sm" title="Загрузить из XML">
|
||||||
<i class="bi bi-upload"></i> Загрузить XML
|
<i class="bi bi-upload"></i> Загрузить XML
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if user|has_perm:'transponder_delete' %}
|
||||||
<button type="button" class="btn btn-danger btn-sm" title="Удалить"
|
<button type="button" class="btn btn-danger btn-sm" title="Удалить"
|
||||||
onclick="deleteSelectedTransponders()">
|
onclick="deleteSelectedTransponders()">
|
||||||
<i class="bi bi-trash"></i> Удалить
|
<i class="bi bi-trash"></i> Удалить
|
||||||
@@ -354,7 +359,7 @@
|
|||||||
<td>{{ transponder.created_at|date:"d.m.Y H:i" }}</td>
|
<td>{{ transponder.created_at|date:"d.m.Y H:i" }}</td>
|
||||||
<td>{{ transponder.updated_at|date:"d.m.Y H:i" }}</td>
|
<td>{{ transponder.updated_at|date:"d.m.Y H:i" }}</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
{% if user|has_perm:'transponder_edit' %}
|
||||||
<a href="{% url 'mainapp:transponder_update' transponder.id %}"
|
<a href="{% url 'mainapp:transponder_update' transponder.id %}"
|
||||||
class="btn btn-sm btn-outline-warning"
|
class="btn btn-sm btn-outline-warning"
|
||||||
title="Редактировать транспондер">
|
title="Редактировать транспондер">
|
||||||
|
|||||||
153
dbapp/mainapp/templates/mainapp/user_permissions_edit.html
Normal file
153
dbapp/mainapp/templates/mainapp/user_permissions_edit.html
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
{% extends 'mainapp/base.html' %}
|
||||||
|
{% load permission_tags %}
|
||||||
|
|
||||||
|
{% block title %}Права пользователя {{ custom_user.user.username }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid px-3">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'mainapp:user_permissions_list' %}">Управление правами</a></li>
|
||||||
|
<li class="breadcrumb-item active">{{ custom_user.user.username }}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<h2>
|
||||||
|
<i class="bi bi-person-gear"></i>
|
||||||
|
Права пользователя: {{ custom_user.user.username }}
|
||||||
|
{% if custom_user.user.first_name or custom_user.user.last_name %}
|
||||||
|
<small class="text-muted">({{ custom_user.user.first_name }} {{ custom_user.user.last_name }})</small>
|
||||||
|
{% endif %}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<span>Настройки прав</span>
|
||||||
|
<span class="badge {% if custom_user.role == 'admin' %}bg-danger{% elif custom_user.role == 'moderator' %}bg-warning text-dark{% else %}bg-secondary{% endif %}">
|
||||||
|
{{ custom_user.get_role_display }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-check form-switch mb-3">
|
||||||
|
<input class="form-check-input" type="checkbox" id="use_custom_permissions"
|
||||||
|
name="use_custom_permissions" {% if custom_user.use_custom_permissions %}checked{% endif %}
|
||||||
|
onchange="toggleCustomPermissions()">
|
||||||
|
<label class="form-check-label" for="use_custom_permissions">
|
||||||
|
<strong>Использовать индивидуальные разрешения</strong>
|
||||||
|
</label>
|
||||||
|
<div class="form-text">
|
||||||
|
Если выключено, будут использоваться права по умолчанию для роли "{{ custom_user.get_role_display }}"
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="permissions-container" {% if not custom_user.use_custom_permissions %}style="opacity: 0.5; pointer-events: none;"{% endif %}>
|
||||||
|
{% for group_name, perms in permission_groups.items %}
|
||||||
|
{% if perms %}
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<span><i class="bi bi-folder"></i> {{ group_name }}</span>
|
||||||
|
<div>
|
||||||
|
<button type="button" class="btn btn-outline-success btn-sm"
|
||||||
|
onclick="selectAllInGroup('{{ group_name }}')">
|
||||||
|
Выбрать все
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||||
|
onclick="deselectAllInGroup('{{ group_name }}')">
|
||||||
|
Снять все
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
{% for perm in perms %}
|
||||||
|
<div class="col-md-6 col-lg-4 mb-2">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input perm-checkbox" type="checkbox"
|
||||||
|
name="permissions" value="{{ perm.code }}"
|
||||||
|
id="perm_{{ perm.code }}"
|
||||||
|
data-group="{{ group_name }}"
|
||||||
|
{% if perm.has_permission %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="perm_{{ perm.code }}">
|
||||||
|
<strong>{{ perm.name }}</strong>
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">{{ perm.description }}</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2 mb-4">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-check-lg"></i> Сохранить
|
||||||
|
</button>
|
||||||
|
<a href="{% url 'mainapp:user_permissions_list' %}" class="btn btn-secondary">
|
||||||
|
<i class="bi bi-x-lg"></i> Отмена
|
||||||
|
</a>
|
||||||
|
<button type="button" class="btn btn-outline-warning" onclick="resetToDefaults()">
|
||||||
|
<i class="bi bi-arrow-counterclockwise"></i> Сбросить к правам роли
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleCustomPermissions() {
|
||||||
|
const checkbox = document.getElementById('use_custom_permissions');
|
||||||
|
const container = document.getElementById('permissions-container');
|
||||||
|
|
||||||
|
if (checkbox.checked) {
|
||||||
|
container.style.opacity = '1';
|
||||||
|
container.style.pointerEvents = 'auto';
|
||||||
|
} else {
|
||||||
|
container.style.opacity = '0.5';
|
||||||
|
container.style.pointerEvents = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAllInGroup(groupName) {
|
||||||
|
document.querySelectorAll(`.perm-checkbox[data-group="${groupName}"]`).forEach(cb => {
|
||||||
|
cb.checked = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deselectAllInGroup(groupName) {
|
||||||
|
document.querySelectorAll(`.perm-checkbox[data-group="${groupName}"]`).forEach(cb => {
|
||||||
|
cb.checked = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetToDefaults() {
|
||||||
|
// Снимаем все галочки
|
||||||
|
document.querySelectorAll('.perm-checkbox').forEach(cb => {
|
||||||
|
cb.checked = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Устанавливаем галочки для прав по умолчанию
|
||||||
|
const defaultPerms = [{% for perm in default_perms %}'{{ perm }}'{% if not forloop.last %}, {% endif %}{% endfor %}];
|
||||||
|
defaultPerms.forEach(perm => {
|
||||||
|
const checkbox = document.getElementById('perm_' + perm);
|
||||||
|
if (checkbox) {
|
||||||
|
checkbox.checked = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
117
dbapp/mainapp/templates/mainapp/user_permissions_list.html
Normal file
117
dbapp/mainapp/templates/mainapp/user_permissions_list.html
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
{% extends 'mainapp/base.html' %}
|
||||||
|
{% load permission_tags %}
|
||||||
|
|
||||||
|
{% block title %}Управление правами пользователей{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid px-3">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<h2><i class="bi bi-shield-lock"></i> Управление правами пользователей</h2>
|
||||||
|
<p class="text-muted">Настройка индивидуальных разрешений для пользователей системы</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<span>Пользователи</span>
|
||||||
|
<a href="{% url 'mainapp:init_permissions' %}" class="btn btn-outline-primary btn-sm">
|
||||||
|
<i class="bi bi-arrow-repeat"></i> Инициализировать разрешения
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover mb-0">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th>Пользователь</th>
|
||||||
|
<th>Роль</th>
|
||||||
|
<th>Тип прав</th>
|
||||||
|
<th>Кол-во разрешений</th>
|
||||||
|
<th class="text-center">Действия</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for custom_user in users %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>{{ custom_user.user.username }}</strong>
|
||||||
|
{% if custom_user.user.first_name or custom_user.user.last_name %}
|
||||||
|
<br><small class="text-muted">{{ custom_user.user.first_name }} {{ custom_user.user.last_name }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if custom_user.role == 'admin' %}
|
||||||
|
<span class="badge bg-danger">Администратор</span>
|
||||||
|
{% elif custom_user.role == 'moderator' %}
|
||||||
|
<span class="badge bg-warning text-dark">Модератор</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">Пользователь</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if custom_user.use_custom_permissions %}
|
||||||
|
<span class="badge bg-info">Индивидуальные</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">По роли</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if custom_user.use_custom_permissions %}
|
||||||
|
{{ custom_user.user_permissions.count }}
|
||||||
|
{% else %}
|
||||||
|
{{ default_permissions|get_item:custom_user.role|length|default:0 }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<a href="{% url 'mainapp:user_permissions_edit' custom_user.pk %}"
|
||||||
|
class="btn btn-outline-primary btn-sm">
|
||||||
|
<i class="bi bi-pencil"></i> Настроить
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="text-center text-muted">Нет пользователей</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <div class="card mt-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<i class="bi bi-info-circle"></i> Права по умолчанию для ролей
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
{% for role, perms in default_permissions.items %}
|
||||||
|
<div class="col-md-4">
|
||||||
|
<h6>
|
||||||
|
{% if role == 'admin' %}
|
||||||
|
<span class="badge bg-danger">Администратор</span>
|
||||||
|
{% elif role == 'moderator' %}
|
||||||
|
<span class="badge bg-warning text-dark">Модератор</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">Пользователь</span>
|
||||||
|
{% endif %}
|
||||||
|
</h6>
|
||||||
|
<ul class="list-unstyled small">
|
||||||
|
{% for perm in perms|slice:":10" %}
|
||||||
|
<li><i class="bi bi-check text-success"></i> {{ perm }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% if perms|length > 10 %}
|
||||||
|
<li class="text-muted">... и ещё {{ perms|length|add:"-10" }}</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
98
dbapp/mainapp/templatetags/permission_tags.py
Normal file
98
dbapp/mainapp/templatetags/permission_tags.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
"""
|
||||||
|
Template tags для проверки разрешений пользователя.
|
||||||
|
|
||||||
|
Usage в шаблонах:
|
||||||
|
{% load permission_tags %}
|
||||||
|
|
||||||
|
{% if user|has_perm:'source_create' %}
|
||||||
|
<button>Создать</button>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% has_perm user 'source_create' as can_create %}
|
||||||
|
{% if can_create %}
|
||||||
|
...
|
||||||
|
{% endif %}
|
||||||
|
"""
|
||||||
|
from django import template
|
||||||
|
from ..permissions import has_permission, get_user_permissions
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name='has_perm')
|
||||||
|
def has_perm_filter(user, permission_code):
|
||||||
|
"""
|
||||||
|
Фильтр для проверки разрешения.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
{% if user|has_perm:'source_create' %}
|
||||||
|
...
|
||||||
|
{% endif %}
|
||||||
|
"""
|
||||||
|
return has_permission(user, permission_code)
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def has_perm(user, permission_code):
|
||||||
|
"""
|
||||||
|
Тег для проверки разрешения с сохранением в переменную.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
{% has_perm user 'source_create' as can_create %}
|
||||||
|
{% if can_create %}
|
||||||
|
...
|
||||||
|
{% endif %}
|
||||||
|
"""
|
||||||
|
return has_permission(user, permission_code)
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def user_permissions(user):
|
||||||
|
"""
|
||||||
|
Возвращает список разрешений пользователя.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
{% user_permissions user as perms %}
|
||||||
|
{% if 'source_create' in perms %}
|
||||||
|
...
|
||||||
|
{% endif %}
|
||||||
|
"""
|
||||||
|
return get_user_permissions(user)
|
||||||
|
|
||||||
|
|
||||||
|
@register.inclusion_tag('mainapp/components/_permission_button.html')
|
||||||
|
def permission_button(user, permission_code, **kwargs):
|
||||||
|
"""
|
||||||
|
Рендерит кнопку только если у пользователя есть разрешение.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
{% permission_button user 'source_create'
|
||||||
|
url='mainapp:source_create'
|
||||||
|
class='btn btn-success btn-sm'
|
||||||
|
icon='bi-plus-circle'
|
||||||
|
text='Создать' %}
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'has_permission': has_permission(user, permission_code),
|
||||||
|
'url': kwargs.get('url', ''),
|
||||||
|
'btn_class': kwargs.get('class', 'btn btn-primary btn-sm'),
|
||||||
|
'icon': kwargs.get('icon', ''),
|
||||||
|
'text': kwargs.get('text', ''),
|
||||||
|
'title': kwargs.get('title', ''),
|
||||||
|
'onclick': kwargs.get('onclick', ''),
|
||||||
|
'data_attrs': kwargs.get('data_attrs', {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name='get_item')
|
||||||
|
def get_item(dictionary, key):
|
||||||
|
"""
|
||||||
|
Получает элемент из словаря по ключу.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
{{ my_dict|get_item:key }}
|
||||||
|
"""
|
||||||
|
if dictionary is None:
|
||||||
|
return None
|
||||||
|
return dictionary.get(key, [])
|
||||||
@@ -95,6 +95,12 @@ from .views.tech_analyze import (
|
|||||||
from .views.points_averaging import PointsAveragingView, PointsAveragingAPIView, RecalculateGroupAPIView
|
from .views.points_averaging import PointsAveragingView, PointsAveragingAPIView, RecalculateGroupAPIView
|
||||||
from .views.statistics import StatisticsView, StatisticsAPIView, ExtendedStatisticsAPIView
|
from .views.statistics import StatisticsView, StatisticsAPIView, ExtendedStatisticsAPIView
|
||||||
from .views.secret_stats import SecretStatsView
|
from .views.secret_stats import SecretStatsView
|
||||||
|
from .views.user_permissions import (
|
||||||
|
UserPermissionsListView,
|
||||||
|
UserPermissionsEditView,
|
||||||
|
UserPermissionsApiView,
|
||||||
|
InitPermissionsView,
|
||||||
|
)
|
||||||
|
|
||||||
app_name = 'mainapp'
|
app_name = 'mainapp'
|
||||||
|
|
||||||
@@ -196,4 +202,10 @@ urlpatterns = [
|
|||||||
path('api/statistics/extended/', ExtendedStatisticsAPIView.as_view(), name='extended_statistics_api'),
|
path('api/statistics/extended/', ExtendedStatisticsAPIView.as_view(), name='extended_statistics_api'),
|
||||||
path('secret-stat/', SecretStatsView.as_view(), name='secret_stats'),
|
path('secret-stat/', SecretStatsView.as_view(), name='secret_stats'),
|
||||||
path('logout/', custom_logout, name='logout'),
|
path('logout/', custom_logout, name='logout'),
|
||||||
|
|
||||||
|
# User permissions management
|
||||||
|
path('user-permissions/', UserPermissionsListView.as_view(), name='user_permissions_list'),
|
||||||
|
path('user-permissions/<int:pk>/edit/', UserPermissionsEditView.as_view(), name='user_permissions_edit'),
|
||||||
|
path('user-permissions/init/', InitPermissionsView.as_view(), name='init_permissions'),
|
||||||
|
path('api/user-permissions/<int:pk>/', UserPermissionsApiView.as_view(), name='user_permissions_api'),
|
||||||
]
|
]
|
||||||
@@ -22,6 +22,7 @@ from ..forms import (
|
|||||||
VchLinkForm,
|
VchLinkForm,
|
||||||
)
|
)
|
||||||
from ..mixins import FormMessageMixin
|
from ..mixins import FormMessageMixin
|
||||||
|
from ..permissions import PermissionRequiredMixin
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
add_satellite_list,
|
add_satellite_list,
|
||||||
compare_and_link_vch_load,
|
compare_and_link_vch_load,
|
||||||
@@ -41,9 +42,10 @@ class AddSatellitesView(LoginRequiredMixin, View):
|
|||||||
return redirect("mainapp:source_list")
|
return redirect("mainapp:source_list")
|
||||||
|
|
||||||
|
|
||||||
class AddTranspondersView(LoginRequiredMixin, FormMessageMixin, FormView):
|
class AddTranspondersView(LoginRequiredMixin, PermissionRequiredMixin, FormMessageMixin, FormView):
|
||||||
"""View for uploading and parsing transponder data from XML."""
|
"""View for uploading and parsing transponder data from XML."""
|
||||||
|
|
||||||
|
permission_required = 'transponder_import_xml'
|
||||||
template_name = "mainapp/transponders_upload.html"
|
template_name = "mainapp/transponders_upload.html"
|
||||||
form_class = UploadFileForm
|
form_class = UploadFileForm
|
||||||
success_message = "Файл успешно обработан"
|
success_message = "Файл успешно обработан"
|
||||||
@@ -85,8 +87,9 @@ class AddTranspondersView(LoginRequiredMixin, FormMessageMixin, FormView):
|
|||||||
return reverse_lazy("mainapp:add_trans")
|
return reverse_lazy("mainapp:add_trans")
|
||||||
|
|
||||||
|
|
||||||
class LoadExcelDataView(LoginRequiredMixin, FormMessageMixin, FormView):
|
class LoadExcelDataView(LoginRequiredMixin, PermissionRequiredMixin, FormMessageMixin, FormView):
|
||||||
"""View for loading data from Excel files."""
|
"""View for loading data from Excel files."""
|
||||||
|
permission_required = 'source_import_excel'
|
||||||
|
|
||||||
template_name = "mainapp/add_data_from_excel.html"
|
template_name = "mainapp/add_data_from_excel.html"
|
||||||
form_class = LoadExcelData
|
form_class = LoadExcelData
|
||||||
@@ -134,8 +137,9 @@ class LoadExcelDataView(LoginRequiredMixin, FormMessageMixin, FormView):
|
|||||||
return reverse_lazy("mainapp:load_excel_data")
|
return reverse_lazy("mainapp:load_excel_data")
|
||||||
|
|
||||||
|
|
||||||
class LoadCsvDataView(LoginRequiredMixin, FormMessageMixin, FormView):
|
class LoadCsvDataView(LoginRequiredMixin, PermissionRequiredMixin, FormMessageMixin, FormView):
|
||||||
"""View for loading data from CSV files."""
|
"""View for loading data from CSV files."""
|
||||||
|
permission_required = 'source_import_csv'
|
||||||
|
|
||||||
template_name = "mainapp/add_data_from_csv.html"
|
template_name = "mainapp/add_data_from_csv.html"
|
||||||
form_class = LoadCsvData
|
form_class = LoadCsvData
|
||||||
|
|||||||
@@ -14,11 +14,13 @@ from openpyxl.styles import Font, Alignment
|
|||||||
|
|
||||||
from mainapp.forms import KubsatFilterForm
|
from mainapp.forms import KubsatFilterForm
|
||||||
from mainapp.models import Source, ObjItem
|
from mainapp.models import Source, ObjItem
|
||||||
|
from mainapp.permissions import PermissionRequiredMixin
|
||||||
from mainapp.utils import calculate_mean_coords
|
from mainapp.utils import calculate_mean_coords
|
||||||
|
|
||||||
|
|
||||||
class KubsatView(LoginRequiredMixin, FormView):
|
class KubsatView(LoginRequiredMixin, PermissionRequiredMixin, FormView):
|
||||||
"""Страница Кубсат с фильтрами и таблицей источников"""
|
"""Страница Кубсат с фильтрами и таблицей источников"""
|
||||||
|
permission_required = 'kubsat_view'
|
||||||
template_name = 'mainapp/kubsat_tabs.html'
|
template_name = 'mainapp/kubsat_tabs.html'
|
||||||
form_class = KubsatFilterForm
|
form_class = KubsatFilterForm
|
||||||
|
|
||||||
@@ -349,8 +351,9 @@ class KubsatView(LoginRequiredMixin, FormView):
|
|||||||
return queryset.distinct()
|
return queryset.distinct()
|
||||||
|
|
||||||
|
|
||||||
class KubsatExportView(LoginRequiredMixin, FormView):
|
class KubsatExportView(LoginRequiredMixin, PermissionRequiredMixin, FormView):
|
||||||
"""Экспорт отфильтрованных данных в Excel"""
|
"""Экспорт отфильтрованных данных в Excel"""
|
||||||
|
permission_required = 'kubsat_view'
|
||||||
form_class = KubsatFilterForm
|
form_class = KubsatFilterForm
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
@@ -595,8 +598,9 @@ class KubsatExportView(LoginRequiredMixin, FormView):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class KubsatCreateRequestsView(LoginRequiredMixin, FormView):
|
class KubsatCreateRequestsView(LoginRequiredMixin, PermissionRequiredMixin, FormView):
|
||||||
"""Массовое создание заявок из отфильтрованных данных"""
|
"""Массовое создание заявок из отфильтрованных данных"""
|
||||||
|
permission_required = 'request_create'
|
||||||
form_class = KubsatFilterForm
|
form_class = KubsatFilterForm
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
@@ -688,8 +692,9 @@ class KubsatCreateRequestsView(LoginRequiredMixin, FormView):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class KubsatRecalculateCoordsView(LoginRequiredMixin, FormView):
|
class KubsatRecalculateCoordsView(LoginRequiredMixin, PermissionRequiredMixin, FormView):
|
||||||
"""API для пересчёта усреднённых координат по списку ObjItem ID"""
|
"""API для пересчёта усреднённых координат по списку ObjItem ID"""
|
||||||
|
permission_required = 'kubsat_view'
|
||||||
form_class = KubsatFilterForm
|
form_class = KubsatFilterForm
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from mainapp.models import (
|
|||||||
Modulation,
|
Modulation,
|
||||||
Standard,
|
Standard,
|
||||||
)
|
)
|
||||||
|
from mainapp.permissions import PermissionRequiredMixin, has_permission
|
||||||
|
|
||||||
|
|
||||||
class SignalMarksView(LoginRequiredMixin, View):
|
class SignalMarksView(LoginRequiredMixin, View):
|
||||||
@@ -324,11 +325,12 @@ class SignalMarksEntryAPIView(LoginRequiredMixin, View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class SaveSignalMarksView(LoginRequiredMixin, View):
|
class SaveSignalMarksView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||||
"""
|
"""
|
||||||
API для сохранения отметок сигналов.
|
API для сохранения отметок сигналов.
|
||||||
Принимает массив отметок и сохраняет их в базу.
|
Принимает массив отметок и сохраняет их в базу.
|
||||||
"""
|
"""
|
||||||
|
permission_required = 'mark_create'
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
try:
|
try:
|
||||||
@@ -404,10 +406,11 @@ class SaveSignalMarksView(LoginRequiredMixin, View):
|
|||||||
}, status=500)
|
}, status=500)
|
||||||
|
|
||||||
|
|
||||||
class CreateTechAnalyzeView(LoginRequiredMixin, View):
|
class CreateTechAnalyzeView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||||
"""
|
"""
|
||||||
API для создания нового теханализа из модального окна.
|
API для создания нового теханализа из модального окна.
|
||||||
"""
|
"""
|
||||||
|
permission_required = 'tech_analyze_create'
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from django.views.generic import CreateView, DeleteView, UpdateView
|
|||||||
from ..forms import GeoForm, ObjItemForm, ParameterForm
|
from ..forms import GeoForm, ObjItemForm, ParameterForm
|
||||||
from ..mixins import CoordinateProcessingMixin, FormMessageMixin, RoleRequiredMixin
|
from ..mixins import CoordinateProcessingMixin, FormMessageMixin, RoleRequiredMixin
|
||||||
from ..models import Geo, Modulation, ObjItem, Polarization, Satellite
|
from ..models import Geo, Modulation, ObjItem, Polarization, Satellite
|
||||||
|
from ..permissions import PermissionRequiredMixin
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
format_coordinate,
|
format_coordinate,
|
||||||
format_coords_display,
|
format_coords_display,
|
||||||
@@ -24,10 +25,9 @@ from ..utils import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DeleteSelectedObjectsView(RoleRequiredMixin, View):
|
class DeleteSelectedObjectsView(PermissionRequiredMixin, View):
|
||||||
"""View for deleting multiple selected objects."""
|
"""View for deleting multiple selected objects."""
|
||||||
|
permission_required = 'objitem_delete'
|
||||||
required_roles = ["admin", "moderator"]
|
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
ids = request.POST.get("ids", "")
|
ids = request.POST.get("ids", "")
|
||||||
@@ -503,7 +503,7 @@ class ObjItemListView(LoginRequiredMixin, View):
|
|||||||
|
|
||||||
|
|
||||||
class ObjItemFormView(
|
class ObjItemFormView(
|
||||||
RoleRequiredMixin, CoordinateProcessingMixin, FormMessageMixin, UpdateView
|
PermissionRequiredMixin, CoordinateProcessingMixin, FormMessageMixin, UpdateView
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Base class for creating and editing ObjItem.
|
Base class for creating and editing ObjItem.
|
||||||
@@ -515,7 +515,7 @@ class ObjItemFormView(
|
|||||||
form_class = ObjItemForm
|
form_class = ObjItemForm
|
||||||
template_name = "mainapp/objitem_form.html"
|
template_name = "mainapp/objitem_form.html"
|
||||||
success_url = reverse_lazy("mainapp:source_list")
|
success_url = reverse_lazy("mainapp:source_list")
|
||||||
required_roles = ["admin", "moderator"]
|
permission_required = 'objitem_edit'
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
"""Returns URL with saved filter parameters."""
|
"""Returns URL with saved filter parameters."""
|
||||||
@@ -651,7 +651,7 @@ class ObjItemFormView(
|
|||||||
|
|
||||||
class ObjItemUpdateView(ObjItemFormView):
|
class ObjItemUpdateView(ObjItemFormView):
|
||||||
"""View for editing ObjItem."""
|
"""View for editing ObjItem."""
|
||||||
|
permission_required = 'objitem_edit'
|
||||||
success_message = "Объект успешно сохранён!"
|
success_message = "Объект успешно сохранён!"
|
||||||
|
|
||||||
def set_user_fields(self):
|
def set_user_fields(self):
|
||||||
@@ -660,7 +660,7 @@ class ObjItemUpdateView(ObjItemFormView):
|
|||||||
|
|
||||||
class ObjItemCreateView(ObjItemFormView, CreateView):
|
class ObjItemCreateView(ObjItemFormView, CreateView):
|
||||||
"""View for creating ObjItem."""
|
"""View for creating ObjItem."""
|
||||||
|
permission_required = 'objitem_create'
|
||||||
success_message = "Объект успешно создан!"
|
success_message = "Объект успешно создан!"
|
||||||
|
|
||||||
def get_object(self, queryset=None):
|
def get_object(self, queryset=None):
|
||||||
@@ -672,14 +672,13 @@ class ObjItemCreateView(ObjItemFormView, CreateView):
|
|||||||
self.object.updated_by = self.request.user.customuser
|
self.object.updated_by = self.request.user.customuser
|
||||||
|
|
||||||
|
|
||||||
class ObjItemDeleteView(RoleRequiredMixin, FormMessageMixin, DeleteView):
|
class ObjItemDeleteView(PermissionRequiredMixin, FormMessageMixin, DeleteView):
|
||||||
"""View for deleting ObjItem."""
|
"""View for deleting ObjItem."""
|
||||||
|
permission_required = 'objitem_delete'
|
||||||
model = ObjItem
|
model = ObjItem
|
||||||
template_name = "mainapp/objitem_confirm_delete.html"
|
template_name = "mainapp/objitem_confirm_delete.html"
|
||||||
success_url = reverse_lazy("mainapp:objitem_list")
|
success_url = reverse_lazy("mainapp:objitem_list")
|
||||||
success_message = "Объект успешно удалён!"
|
success_message = "Объект успешно удалён!"
|
||||||
required_roles = ["admin", "moderator"]
|
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
"""Returns URL with saved filter parameters."""
|
"""Returns URL with saved filter parameters."""
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from django.views import View
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from ..models import ObjItem, Satellite, Source
|
from ..models import ObjItem, Satellite, Source
|
||||||
|
from ..permissions import PermissionRequiredMixin
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
calculate_mean_coords,
|
calculate_mean_coords,
|
||||||
calculate_distance_wgs84,
|
calculate_distance_wgs84,
|
||||||
@@ -24,10 +25,11 @@ from ..utils import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PointsAveragingView(LoginRequiredMixin, View):
|
class PointsAveragingView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||||
"""
|
"""
|
||||||
View for points averaging form with date range selection and grouping.
|
View for points averaging form with date range selection and grouping.
|
||||||
"""
|
"""
|
||||||
|
permission_required = 'source_averaging'
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
# Get satellites that have sources with points with geo data
|
# Get satellites that have sources with points with geo data
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from django.views.generic import CreateView, UpdateView
|
|||||||
from ..forms import SatelliteForm
|
from ..forms import SatelliteForm
|
||||||
from ..mixins import RoleRequiredMixin, FormMessageMixin
|
from ..mixins import RoleRequiredMixin, FormMessageMixin
|
||||||
from ..models import Satellite, Band
|
from ..models import Satellite, Band
|
||||||
|
from ..permissions import PermissionRequiredMixin
|
||||||
from ..utils import parse_pagination_params
|
from ..utils import parse_pagination_params
|
||||||
|
|
||||||
|
|
||||||
@@ -252,15 +253,14 @@ class SatelliteListView(LoginRequiredMixin, View):
|
|||||||
return render(request, "mainapp/satellite_list.html", context)
|
return render(request, "mainapp/satellite_list.html", context)
|
||||||
|
|
||||||
|
|
||||||
class SatelliteCreateView(RoleRequiredMixin, FormMessageMixin, CreateView):
|
class SatelliteCreateView(PermissionRequiredMixin, FormMessageMixin, CreateView):
|
||||||
"""View for creating a new satellite."""
|
"""View for creating a new satellite."""
|
||||||
|
permission_required = 'satellite_create'
|
||||||
model = Satellite
|
model = Satellite
|
||||||
form_class = SatelliteForm
|
form_class = SatelliteForm
|
||||||
template_name = "mainapp/satellite_form.html"
|
template_name = "mainapp/satellite_form.html"
|
||||||
success_url = reverse_lazy("mainapp:satellite_list")
|
success_url = reverse_lazy("mainapp:satellite_list")
|
||||||
success_message = "Спутник успешно создан!"
|
success_message = "Спутник успешно создан!"
|
||||||
required_roles = ["admin", "moderator"]
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
@@ -274,15 +274,14 @@ class SatelliteCreateView(RoleRequiredMixin, FormMessageMixin, CreateView):
|
|||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
class SatelliteUpdateView(RoleRequiredMixin, FormMessageMixin, UpdateView):
|
class SatelliteUpdateView(PermissionRequiredMixin, FormMessageMixin, UpdateView):
|
||||||
"""View for updating an existing satellite."""
|
"""View for updating an existing satellite."""
|
||||||
|
permission_required = 'satellite_edit'
|
||||||
model = Satellite
|
model = Satellite
|
||||||
form_class = SatelliteForm
|
form_class = SatelliteForm
|
||||||
template_name = "mainapp/satellite_form.html"
|
template_name = "mainapp/satellite_form.html"
|
||||||
success_url = reverse_lazy("mainapp:satellite_list")
|
success_url = reverse_lazy("mainapp:satellite_list")
|
||||||
success_message = "Спутник успешно обновлен!"
|
success_message = "Спутник успешно обновлен!"
|
||||||
required_roles = ["admin", "moderator"]
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
import json
|
import json
|
||||||
@@ -320,10 +319,9 @@ class SatelliteUpdateView(RoleRequiredMixin, FormMessageMixin, UpdateView):
|
|||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
class DeleteSelectedSatellitesView(RoleRequiredMixin, View):
|
class DeleteSelectedSatellitesView(PermissionRequiredMixin, View):
|
||||||
"""View for deleting multiple selected satellites with confirmation."""
|
"""View for deleting multiple selected satellites with confirmation."""
|
||||||
|
permission_required = 'satellite_delete'
|
||||||
required_roles = ["admin", "moderator"]
|
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Show confirmation page with details about satellites to be deleted."""
|
"""Show confirmation page with details about satellites to be deleted."""
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from django.views import View
|
|||||||
from ..forms import SourceForm
|
from ..forms import SourceForm
|
||||||
from ..models import Source, Satellite
|
from ..models import Source, Satellite
|
||||||
from ..utils import format_coords_display, parse_pagination_params
|
from ..utils import format_coords_display, parse_pagination_params
|
||||||
|
from ..permissions import PermissionRequiredMixin, permission_required
|
||||||
|
|
||||||
|
|
||||||
class SourceListView(LoginRequiredMixin, View):
|
class SourceListView(LoginRequiredMixin, View):
|
||||||
@@ -818,7 +819,10 @@ class SourceListView(LoginRequiredMixin, View):
|
|||||||
|
|
||||||
|
|
||||||
class AdminModeratorMixin(UserPassesTestMixin):
|
class AdminModeratorMixin(UserPassesTestMixin):
|
||||||
"""Mixin to restrict access to admin and moderator roles only."""
|
"""Mixin to restrict access to admin and moderator roles only.
|
||||||
|
|
||||||
|
DEPRECATED: Use PermissionRequiredMixin instead for granular permissions.
|
||||||
|
"""
|
||||||
|
|
||||||
def test_func(self):
|
def test_func(self):
|
||||||
return (
|
return (
|
||||||
@@ -832,8 +836,9 @@ class AdminModeratorMixin(UserPassesTestMixin):
|
|||||||
return redirect('mainapp:source_list')
|
return redirect('mainapp:source_list')
|
||||||
|
|
||||||
|
|
||||||
class SourceCreateView(LoginRequiredMixin, AdminModeratorMixin, View):
|
class SourceCreateView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||||
"""View for creating new Source."""
|
"""View for creating new Source."""
|
||||||
|
permission_required = 'source_create'
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
form = SourceForm()
|
form = SourceForm()
|
||||||
@@ -874,8 +879,9 @@ class SourceCreateView(LoginRequiredMixin, AdminModeratorMixin, View):
|
|||||||
return render(request, 'mainapp/source_form.html', context)
|
return render(request, 'mainapp/source_form.html', context)
|
||||||
|
|
||||||
|
|
||||||
class SourceUpdateView(LoginRequiredMixin, AdminModeratorMixin, View):
|
class SourceUpdateView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||||
"""View for editing Source with 4 coordinate fields and related ObjItems."""
|
"""View for editing Source with 4 coordinate fields and related ObjItems."""
|
||||||
|
permission_required = 'source_edit'
|
||||||
|
|
||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
source = get_object_or_404(Source, pk=pk)
|
source = get_object_or_404(Source, pk=pk)
|
||||||
@@ -945,8 +951,9 @@ class SourceUpdateView(LoginRequiredMixin, AdminModeratorMixin, View):
|
|||||||
return render(request, 'mainapp/source_form.html', context)
|
return render(request, 'mainapp/source_form.html', context)
|
||||||
|
|
||||||
|
|
||||||
class SourceDeleteView(LoginRequiredMixin, AdminModeratorMixin, View):
|
class SourceDeleteView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||||
"""View for deleting Source."""
|
"""View for deleting Source."""
|
||||||
|
permission_required = 'source_delete'
|
||||||
|
|
||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
source = get_object_or_404(Source, pk=pk)
|
source = get_object_or_404(Source, pk=pk)
|
||||||
@@ -975,8 +982,9 @@ class SourceDeleteView(LoginRequiredMixin, AdminModeratorMixin, View):
|
|||||||
return redirect('mainapp:source_list')
|
return redirect('mainapp:source_list')
|
||||||
|
|
||||||
|
|
||||||
class DeleteSelectedSourcesView(LoginRequiredMixin, AdminModeratorMixin, View):
|
class DeleteSelectedSourcesView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||||
"""View for deleting multiple selected sources with confirmation."""
|
"""View for deleting multiple selected sources with confirmation."""
|
||||||
|
permission_required = 'source_delete'
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Show confirmation page with details about sources to be deleted."""
|
"""Show confirmation page with details about sources to be deleted."""
|
||||||
@@ -1062,8 +1070,9 @@ class DeleteSelectedSourcesView(LoginRequiredMixin, AdminModeratorMixin, View):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
class MergeSourcesView(LoginRequiredMixin, AdminModeratorMixin, View):
|
class MergeSourcesView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||||
"""View for merging multiple sources into one."""
|
"""View for merging multiple sources into one."""
|
||||||
|
permission_required = 'source_merge'
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""Merge selected sources into the first one."""
|
"""Merge selected sources into the first one."""
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from django.utils import timezone
|
|||||||
|
|
||||||
from mainapp.models import SourceRequest, SourceRequestStatusHistory, Source, Satellite
|
from mainapp.models import SourceRequest, SourceRequestStatusHistory, Source, Satellite
|
||||||
from mainapp.forms import SourceRequestForm
|
from mainapp.forms import SourceRequestForm
|
||||||
|
from mainapp.permissions import PermissionRequiredMixin
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@@ -83,8 +84,9 @@ class SourceRequestListView(LoginRequiredMixin, ListView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class SourceRequestCreateView(LoginRequiredMixin, CreateView):
|
class SourceRequestCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||||||
"""Создание заявки на источник."""
|
"""Создание заявки на источник."""
|
||||||
|
permission_required = 'request_create'
|
||||||
model = SourceRequest
|
model = SourceRequest
|
||||||
form_class = SourceRequestForm
|
form_class = SourceRequestForm
|
||||||
template_name = 'mainapp/source_request_form.html'
|
template_name = 'mainapp/source_request_form.html'
|
||||||
@@ -132,8 +134,9 @@ class SourceRequestCreateView(LoginRequiredMixin, CreateView):
|
|||||||
return super().form_invalid(form)
|
return super().form_invalid(form)
|
||||||
|
|
||||||
|
|
||||||
class SourceRequestUpdateView(LoginRequiredMixin, UpdateView):
|
class SourceRequestUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
||||||
"""Редактирование заявки на источник."""
|
"""Редактирование заявки на источник."""
|
||||||
|
permission_required = 'request_edit'
|
||||||
model = SourceRequest
|
model = SourceRequest
|
||||||
form_class = SourceRequestForm
|
form_class = SourceRequestForm
|
||||||
template_name = 'mainapp/source_request_form.html'
|
template_name = 'mainapp/source_request_form.html'
|
||||||
@@ -164,8 +167,9 @@ class SourceRequestUpdateView(LoginRequiredMixin, UpdateView):
|
|||||||
return super().form_invalid(form)
|
return super().form_invalid(form)
|
||||||
|
|
||||||
|
|
||||||
class SourceRequestDeleteView(LoginRequiredMixin, View):
|
class SourceRequestDeleteView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||||
"""Удаление заявки на источник."""
|
"""Удаление заявки на источник."""
|
||||||
|
permission_required = 'request_delete'
|
||||||
|
|
||||||
def post(self, request, pk):
|
def post(self, request, pk):
|
||||||
try:
|
try:
|
||||||
@@ -182,8 +186,9 @@ class SourceRequestDeleteView(LoginRequiredMixin, View):
|
|||||||
}, status=404)
|
}, status=404)
|
||||||
|
|
||||||
|
|
||||||
class SourceRequestBulkDeleteView(LoginRequiredMixin, View):
|
class SourceRequestBulkDeleteView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||||
"""Массовое удаление заявок."""
|
"""Массовое удаление заявок."""
|
||||||
|
permission_required = 'request_delete'
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
import json
|
import json
|
||||||
@@ -688,8 +693,9 @@ class SourceDataAPIView(LoginRequiredMixin, View):
|
|||||||
return JsonResponse(data)
|
return JsonResponse(data)
|
||||||
|
|
||||||
|
|
||||||
class SourceRequestImportView(LoginRequiredMixin, View):
|
class SourceRequestImportView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||||
"""Импорт заявок из Excel файла."""
|
"""Импорт заявок из Excel файла."""
|
||||||
|
permission_required = 'request_import'
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Отображает форму загрузки файла."""
|
"""Отображает форму загрузки файла."""
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ from django.views.generic import TemplateView
|
|||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
|
|
||||||
from ..models import ObjItem, Source, Satellite, Geo, SourceRequest, SourceRequestStatusHistory
|
from ..models import ObjItem, Source, Satellite, Geo, SourceRequest, SourceRequestStatusHistory
|
||||||
|
from ..permissions import PermissionRequiredMixin
|
||||||
from mapsapp.models import Transponders
|
from mapsapp.models import Transponders
|
||||||
|
|
||||||
|
|
||||||
class StatisticsView(TemplateView):
|
class StatisticsView(PermissionRequiredMixin, TemplateView):
|
||||||
"""Страница статистики по данным геолокации."""
|
"""Страница статистики по данным геолокации."""
|
||||||
|
permission_required = 'statistics_view'
|
||||||
|
|
||||||
template_name = 'mainapp/statistics.html'
|
template_name = 'mainapp/statistics.html'
|
||||||
|
|
||||||
|
|||||||
@@ -19,13 +19,15 @@ from ..models import (
|
|||||||
Parameter,
|
Parameter,
|
||||||
)
|
)
|
||||||
from ..mixins import RoleRequiredMixin
|
from ..mixins import RoleRequiredMixin
|
||||||
|
from ..permissions import PermissionRequiredMixin
|
||||||
from ..utils import parse_pagination_params, find_matching_transponder, find_matching_lyngsat
|
from ..utils import parse_pagination_params, find_matching_transponder, find_matching_lyngsat
|
||||||
|
|
||||||
|
|
||||||
class TechAnalyzeEntryView(LoginRequiredMixin, View):
|
class TechAnalyzeEntryView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||||
"""
|
"""
|
||||||
Представление для ввода данных технического анализа.
|
Представление для ввода данных технического анализа.
|
||||||
"""
|
"""
|
||||||
|
permission_required = 'source_tech_analyze'
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
satellites = Satellite.objects.all().order_by('name')
|
satellites = Satellite.objects.all().order_by('name')
|
||||||
@@ -37,10 +39,11 @@ class TechAnalyzeEntryView(LoginRequiredMixin, View):
|
|||||||
return render(request, 'mainapp/tech_analyze_entry.html', context)
|
return render(request, 'mainapp/tech_analyze_entry.html', context)
|
||||||
|
|
||||||
|
|
||||||
class TechAnalyzeSaveView(LoginRequiredMixin, View):
|
class TechAnalyzeSaveView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||||
"""
|
"""
|
||||||
API endpoint для сохранения данных технического анализа.
|
API endpoint для сохранения данных технического анализа.
|
||||||
"""
|
"""
|
||||||
|
permission_required = 'tech_analyze_create'
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
try:
|
try:
|
||||||
@@ -177,7 +180,7 @@ class TechAnalyzeSaveView(LoginRequiredMixin, View):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
class LinkExistingPointsView(LoginRequiredMixin, View):
|
class LinkExistingPointsView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||||
"""
|
"""
|
||||||
API endpoint для привязки существующих точек к данным теханализа.
|
API endpoint для привязки существующих точек к данным теханализа.
|
||||||
|
|
||||||
@@ -194,6 +197,7 @@ class LinkExistingPointsView(LoginRequiredMixin, View):
|
|||||||
* Обновить полосу частот (если 0 или None)
|
* Обновить полосу частот (если 0 или None)
|
||||||
* Подобрать подходящий транспондер
|
* Подобрать подходящий транспондер
|
||||||
"""
|
"""
|
||||||
|
permission_required = 'tech_analyze_edit'
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
try:
|
try:
|
||||||
@@ -388,11 +392,11 @@ class TechAnalyzeListView(LoginRequiredMixin, View):
|
|||||||
return render(request, 'mainapp/tech_analyze_list.html', context)
|
return render(request, 'mainapp/tech_analyze_list.html', context)
|
||||||
|
|
||||||
|
|
||||||
class TechAnalyzeDeleteView(LoginRequiredMixin, RoleRequiredMixin, View):
|
class TechAnalyzeDeleteView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||||
"""
|
"""
|
||||||
API endpoint для удаления выбранных записей теханализа.
|
API endpoint для удаления выбранных записей теханализа.
|
||||||
"""
|
"""
|
||||||
allowed_roles = ['admin', 'moderator']
|
permission_required = 'tech_analyze_delete'
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ from django.views.generic import CreateView, UpdateView
|
|||||||
|
|
||||||
from mapsapp.models import Transponders
|
from mapsapp.models import Transponders
|
||||||
from ..forms import TransponderForm
|
from ..forms import TransponderForm
|
||||||
from ..mixins import RoleRequiredMixin, FormMessageMixin
|
from ..mixins import FormMessageMixin
|
||||||
|
from ..permissions import PermissionRequiredMixin
|
||||||
from ..models import Satellite, Polarization
|
from ..models import Satellite, Polarization
|
||||||
from ..utils import parse_pagination_params
|
from ..utils import parse_pagination_params
|
||||||
|
|
||||||
@@ -246,15 +247,15 @@ class TransponderListView(LoginRequiredMixin, View):
|
|||||||
return render(request, "mainapp/transponder_list.html", context)
|
return render(request, "mainapp/transponder_list.html", context)
|
||||||
|
|
||||||
|
|
||||||
class TransponderCreateView(RoleRequiredMixin, FormMessageMixin, CreateView):
|
class TransponderCreateView(PermissionRequiredMixin, FormMessageMixin, CreateView):
|
||||||
"""View for creating a new transponder."""
|
"""View for creating a new transponder."""
|
||||||
|
|
||||||
|
permission_required = 'transponder_create'
|
||||||
model = Transponders
|
model = Transponders
|
||||||
form_class = TransponderForm
|
form_class = TransponderForm
|
||||||
template_name = "mainapp/transponder_form.html"
|
template_name = "mainapp/transponder_form.html"
|
||||||
success_url = reverse_lazy("mainapp:transponder_list")
|
success_url = reverse_lazy("mainapp:transponder_list")
|
||||||
success_message = "Транспондер успешно создан!"
|
success_message = "Транспондер успешно создан!"
|
||||||
required_roles = ["admin", "moderator"]
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
@@ -268,15 +269,15 @@ class TransponderCreateView(RoleRequiredMixin, FormMessageMixin, CreateView):
|
|||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
class TransponderUpdateView(RoleRequiredMixin, FormMessageMixin, UpdateView):
|
class TransponderUpdateView(PermissionRequiredMixin, FormMessageMixin, UpdateView):
|
||||||
"""View for updating an existing transponder."""
|
"""View for updating an existing transponder."""
|
||||||
|
|
||||||
|
permission_required = 'transponder_edit'
|
||||||
model = Transponders
|
model = Transponders
|
||||||
form_class = TransponderForm
|
form_class = TransponderForm
|
||||||
template_name = "mainapp/transponder_form.html"
|
template_name = "mainapp/transponder_form.html"
|
||||||
success_url = reverse_lazy("mainapp:transponder_list")
|
success_url = reverse_lazy("mainapp:transponder_list")
|
||||||
success_message = "Транспондер успешно обновлен!"
|
success_message = "Транспондер успешно обновлен!"
|
||||||
required_roles = ["admin", "moderator"]
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
@@ -293,10 +294,10 @@ class TransponderUpdateView(RoleRequiredMixin, FormMessageMixin, UpdateView):
|
|||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
class DeleteSelectedTranspondersView(RoleRequiredMixin, View):
|
class DeleteSelectedTranspondersView(PermissionRequiredMixin, View):
|
||||||
"""View for deleting multiple selected transponders with confirmation."""
|
"""View for deleting multiple selected transponders with confirmation."""
|
||||||
|
|
||||||
required_roles = ["admin", "moderator"]
|
permission_required = 'transponder_delete'
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Show confirmation page with details about transponders to be deleted."""
|
"""Show confirmation page with details about transponders to be deleted."""
|
||||||
|
|||||||
180
dbapp/mainapp/views/user_permissions.py
Normal file
180
dbapp/mainapp/views/user_permissions.py
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
"""
|
||||||
|
Views для управления правами пользователей.
|
||||||
|
"""
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
|
from ..models import CustomUser, UserPermission
|
||||||
|
from ..permissions import (
|
||||||
|
PERMISSIONS,
|
||||||
|
DEFAULT_ROLE_PERMISSIONS,
|
||||||
|
PermissionRequiredMixin,
|
||||||
|
has_permission
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserPermissionsListView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||||
|
"""Список пользователей с их правами."""
|
||||||
|
permission_required = 'admin_access'
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
users = CustomUser.objects.select_related('user').prefetch_related(
|
||||||
|
'user_permissions'
|
||||||
|
).order_by('user__username')
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'users': users,
|
||||||
|
'permissions': PERMISSIONS,
|
||||||
|
'default_permissions': DEFAULT_ROLE_PERMISSIONS,
|
||||||
|
}
|
||||||
|
return render(request, 'mainapp/user_permissions_list.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
class UserPermissionsEditView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||||
|
"""Редактирование прав конкретного пользователя."""
|
||||||
|
permission_required = 'admin_access'
|
||||||
|
|
||||||
|
def get(self, request, pk):
|
||||||
|
custom_user = get_object_or_404(CustomUser.objects.select_related('user'), pk=pk)
|
||||||
|
|
||||||
|
# Получаем все разрешения
|
||||||
|
all_permissions = UserPermission.objects.all()
|
||||||
|
|
||||||
|
# Текущие разрешения пользователя
|
||||||
|
user_perm_codes = set(custom_user.user_permissions.values_list('code', flat=True))
|
||||||
|
|
||||||
|
# Права по умолчанию для роли
|
||||||
|
default_perms = set(DEFAULT_ROLE_PERMISSIONS.get(custom_user.role, []))
|
||||||
|
|
||||||
|
# Группируем разрешения по категориям
|
||||||
|
permission_groups = {
|
||||||
|
'Источники': [],
|
||||||
|
'Заявки': [],
|
||||||
|
'Точки ГЛ': [],
|
||||||
|
'Спутники': [],
|
||||||
|
'Транспондеры': [],
|
||||||
|
'Тех. анализ': [],
|
||||||
|
'Отметки': [],
|
||||||
|
'Прочее': [],
|
||||||
|
}
|
||||||
|
|
||||||
|
for code, name, desc in PERMISSIONS:
|
||||||
|
perm_data = {
|
||||||
|
'code': code,
|
||||||
|
'name': name,
|
||||||
|
'description': desc,
|
||||||
|
'has_permission': code in user_perm_codes if custom_user.use_custom_permissions else code in default_perms,
|
||||||
|
'is_default': code in default_perms,
|
||||||
|
}
|
||||||
|
|
||||||
|
if code.startswith('source_'):
|
||||||
|
permission_groups['Источники'].append(perm_data)
|
||||||
|
elif code.startswith('request_'):
|
||||||
|
permission_groups['Заявки'].append(perm_data)
|
||||||
|
elif code.startswith('objitem_'):
|
||||||
|
permission_groups['Точки ГЛ'].append(perm_data)
|
||||||
|
elif code.startswith('satellite_'):
|
||||||
|
permission_groups['Спутники'].append(perm_data)
|
||||||
|
elif code.startswith('transponder_'):
|
||||||
|
permission_groups['Транспондеры'].append(perm_data)
|
||||||
|
elif code.startswith('tech_analyze_'):
|
||||||
|
permission_groups['Тех. анализ'].append(perm_data)
|
||||||
|
elif code.startswith('mark_'):
|
||||||
|
permission_groups['Отметки'].append(perm_data)
|
||||||
|
else:
|
||||||
|
permission_groups['Прочее'].append(perm_data)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'custom_user': custom_user,
|
||||||
|
'permission_groups': permission_groups,
|
||||||
|
'default_perms': default_perms,
|
||||||
|
}
|
||||||
|
return render(request, 'mainapp/user_permissions_edit.html', context)
|
||||||
|
|
||||||
|
def post(self, request, pk):
|
||||||
|
custom_user = get_object_or_404(CustomUser, pk=pk)
|
||||||
|
|
||||||
|
# Получаем выбранные разрешения
|
||||||
|
selected_permissions = request.POST.getlist('permissions')
|
||||||
|
use_custom = request.POST.get('use_custom_permissions') == 'on'
|
||||||
|
|
||||||
|
# Обновляем флаг использования индивидуальных разрешений
|
||||||
|
custom_user.use_custom_permissions = use_custom
|
||||||
|
|
||||||
|
if use_custom:
|
||||||
|
# Очищаем текущие разрешения и добавляем новые
|
||||||
|
custom_user.user_permissions.clear()
|
||||||
|
|
||||||
|
for perm_code in selected_permissions:
|
||||||
|
perm, created = UserPermission.objects.get_or_create(code=perm_code)
|
||||||
|
custom_user.user_permissions.add(perm)
|
||||||
|
|
||||||
|
custom_user.save()
|
||||||
|
|
||||||
|
messages.success(request, f'Права пользователя {custom_user.user.username} обновлены.')
|
||||||
|
return redirect('mainapp:user_permissions_list')
|
||||||
|
|
||||||
|
|
||||||
|
class UserPermissionsApiView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||||
|
"""API для управления правами пользователей."""
|
||||||
|
permission_required = 'admin_access'
|
||||||
|
|
||||||
|
def post(self, request, pk):
|
||||||
|
"""Обновление прав пользователя через AJAX."""
|
||||||
|
import json
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(request.body)
|
||||||
|
custom_user = get_object_or_404(CustomUser, pk=pk)
|
||||||
|
|
||||||
|
use_custom = data.get('use_custom_permissions', False)
|
||||||
|
permissions = data.get('permissions', [])
|
||||||
|
|
||||||
|
custom_user.use_custom_permissions = use_custom
|
||||||
|
|
||||||
|
if use_custom:
|
||||||
|
custom_user.user_permissions.clear()
|
||||||
|
for perm_code in permissions:
|
||||||
|
perm, _ = UserPermission.objects.get_or_create(code=perm_code)
|
||||||
|
custom_user.user_permissions.add(perm)
|
||||||
|
|
||||||
|
custom_user.save()
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'success': True,
|
||||||
|
'message': f'Права пользователя {custom_user.user.username} обновлены'
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': str(e)
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class InitPermissionsView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||||
|
"""Инициализация всех разрешений в базе данных."""
|
||||||
|
permission_required = 'admin_access'
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
from ..permissions import PERMISSIONS
|
||||||
|
|
||||||
|
created_count = 0
|
||||||
|
existing_count = 0
|
||||||
|
|
||||||
|
for code, name, description in PERMISSIONS:
|
||||||
|
perm, created = UserPermission.objects.get_or_create(code=code)
|
||||||
|
if created:
|
||||||
|
created_count += 1
|
||||||
|
else:
|
||||||
|
existing_count += 1
|
||||||
|
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
f'Разрешения инициализированы. Создано: {created_count}, уже существовало: {existing_count}'
|
||||||
|
)
|
||||||
|
return redirect('mainapp:user_permissions_list')
|
||||||
Reference in New Issue
Block a user