Реализовал систему разрешений
This commit is contained in:
@@ -116,6 +116,7 @@ TEMPLATES = [
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"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.MinimumLengthValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||
},
|
||||
# {
|
||||
# "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
# },
|
||||
# {
|
||||
# "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||
# },
|
||||
# {
|
||||
# "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||
# },
|
||||
]
|
||||
|
||||
# ============================================================================
|
||||
|
||||
@@ -32,12 +32,14 @@ from .models import (
|
||||
Geo,
|
||||
ObjItem,
|
||||
CustomUser,
|
||||
UserPermission,
|
||||
Band,
|
||||
Source,
|
||||
TechAnalyze,
|
||||
SourceRequest,
|
||||
SourceRequestStatusHistory,
|
||||
)
|
||||
from .permissions import PERMISSIONS, DEFAULT_ROLE_PERMISSIONS
|
||||
from .filters import (
|
||||
GeoKupDistanceFilter,
|
||||
GeoValidDistanceFilter,
|
||||
@@ -99,6 +101,19 @@ class CustomUserInline(admin.StackedInline):
|
||||
model = CustomUser
|
||||
can_delete = False
|
||||
verbose_name_plural = "Дополнительная информация пользователя"
|
||||
filter_horizontal = ('user_permissions',)
|
||||
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': ('role',)
|
||||
}),
|
||||
('Индивидуальные разрешения', {
|
||||
'fields': ('use_custom_permissions', 'user_permissions'),
|
||||
'classes': ('collapse',),
|
||||
'description': 'Если включено "Использовать индивидуальные разрешения", '
|
||||
'будут использоваться выбранные разрешения вместо прав роли по умолчанию.'
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
class LocationForm(forms.ModelForm):
|
||||
@@ -195,6 +210,88 @@ class UserAdmin(BaseUserAdmin):
|
||||
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
|
||||
# ============================================================================
|
||||
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
Расширенная модель пользователя с ролями.
|
||||
@@ -54,6 +85,22 @@ class CustomUser(models.Model):
|
||||
db_index=True,
|
||||
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):
|
||||
return (
|
||||
@@ -61,6 +108,19 @@ class CustomUser(models.Model):
|
||||
if self.user.first_name and self.user.last_name
|
||||
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:
|
||||
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' %}
|
||||
{% endcomment %}
|
||||
{% load permission_tags %}
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container">
|
||||
@@ -37,18 +38,27 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'mainapp:signal_marks' %}">Отметки сигналов</a>
|
||||
</li>
|
||||
{% if user|has_perm:'kubsat_view' %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'mainapp:kubsat' %}">Кубсат</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<!-- <li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'mapsapp:3dmap' %}">3D карта</a>
|
||||
</li> -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'mapsapp:2dmap' %}">Карта</a>
|
||||
</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">
|
||||
<a class="nav-link" href="{% url 'admin:index' %}">Админ панель</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
<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 permission_tags %}
|
||||
<!-- Вкладка заявок на источники -->
|
||||
<link href="{% static 'tabulator/css/tabulator_bootstrap5.min.css' %}" rel="stylesheet">
|
||||
<style>
|
||||
@@ -22,15 +23,19 @@
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0"><i class="bi bi-list-task"></i> Заявки на источники</h5>
|
||||
<div>
|
||||
{% if user|has_perm:'request_delete' %}
|
||||
<button type="button" class="btn btn-outline-danger btn-sm me-2" id="bulkDeleteBtn" onclick="bulkDeleteRequests()">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-outline-success btn-sm me-2" onclick="exportRequests()">
|
||||
<i class="bi bi-file-earmark-excel"></i> Экспорт
|
||||
</button>
|
||||
{% if user|has_perm:'request_create' %}
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="openCreateRequestModal()">
|
||||
<i class="bi bi-plus-circle"></i> Создать
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@@ -193,22 +198,37 @@ function commentFormatter(cell) {
|
||||
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) {
|
||||
const id = cell.getData().id;
|
||||
return `
|
||||
let buttons = `
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button type="button" class="btn btn-outline-info btn-sm" onclick="showHistory(${id})" title="История">
|
||||
<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="Редактировать">
|
||||
<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="Удалить">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
</button>`;
|
||||
}
|
||||
|
||||
buttons += `</div>`;
|
||||
return buttons;
|
||||
}
|
||||
|
||||
// Инициализация Tabulator
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{% load static %}
|
||||
{% load static leaflet_tags %}
|
||||
{% load l10n %}
|
||||
{% load permission_tags %}
|
||||
|
||||
{% 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">
|
||||
<h2>{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать новый объект{% endif %}</h2>
|
||||
<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 %}
|
||||
<a href="{% url 'mainapp:objitem_delete' object.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}"
|
||||
class="btn btn-danger btn-action">Удалить</a>
|
||||
{% endif %}
|
||||
{% if user|has_perm:'objitem_edit' %}
|
||||
<button type="submit" form="objitem-form" class="btn btn-primary btn-action">Сохранить</button>
|
||||
{% 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 %}
|
||||
<a href="{% url 'mainapp:objitem_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}"
|
||||
class="btn btn-secondary btn-action">Назад</a>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
{% load static %}
|
||||
{% load permission_tags %}
|
||||
|
||||
{% block title %}Список объектов{% endblock %}
|
||||
{% block extra_css %}
|
||||
@@ -47,10 +48,12 @@
|
||||
|
||||
<!-- Action buttons bar -->
|
||||
<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="Создать новый объект">
|
||||
<i class="bi bi-plus-circle"></i> Создать
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if user|has_perm:'objitem_delete' %}
|
||||
<button type="button" class="btn btn-danger btn-sm" title="Удалить"
|
||||
onclick="deleteSelectedObjects()">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
@@ -390,7 +393,7 @@
|
||||
<input type="checkbox" class="form-check-input item-checkbox" value="{{ item.id }}">
|
||||
</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>
|
||||
{% if item.satellite_id %}
|
||||
<a href="#" class="text-decoration-underline"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
{% load permission_tags %}
|
||||
|
||||
{% block title %}Список спутников{% endblock %}
|
||||
|
||||
@@ -60,10 +61,12 @@
|
||||
|
||||
<!-- Action buttons -->
|
||||
<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="Создать">
|
||||
<i class="bi bi-plus-circle"></i> Создать
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if user|has_perm:'satellite_delete' %}
|
||||
<button type="button" class="btn btn-danger btn-sm" title="Удалить"
|
||||
onclick="deleteSelectedSatellites()">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
@@ -355,7 +358,7 @@
|
||||
<td>{{ satellite.updated_at|date:"d.m.Y H:i" }}</td>
|
||||
<td class="text-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 %}"
|
||||
class="btn btn-sm btn-outline-warning"
|
||||
title="Редактировать спутник">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends "mainapp/base.html" %}
|
||||
{% load static %}
|
||||
{% load permission_tags %}
|
||||
|
||||
{% block title %}Отметки сигналов{% endblock %}
|
||||
|
||||
@@ -192,13 +193,17 @@
|
||||
placeholder="Поиск по имени..." style="width: 200px;">
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
{% if user|has_perm:'tech_analyze_create' %}
|
||||
<button class="btn btn-outline-primary btn-sm" onclick="openCreateModal()">
|
||||
<i class="bi bi-plus-lg"></i> Создать теханализ
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if user|has_perm:'mark_create' %}
|
||||
<button class="btn btn-success" id="save-marks-btn" onclick="saveMarks()" disabled>
|
||||
<i class="bi bi-check-lg"></i> Сохранить
|
||||
<span class="badge bg-light text-dark" id="marks-count">0</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@@ -262,6 +267,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if user|has_perm:'tech_analyze_create' %}
|
||||
<!-- Modal for creating TechAnalyze -->
|
||||
<div class="modal fade" id="createTechAnalyzeModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
@@ -321,12 +327,14 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{% if user|has_perm:'mark_create' %}
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="ta-add-mark" checked>
|
||||
<label class="form-check-label" for="ta-add-mark">
|
||||
Сразу добавить отметку "Есть сигнал"
|
||||
</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -336,6 +344,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
@@ -343,6 +352,8 @@
|
||||
<script>
|
||||
const SATELLITE_ID = {% if selected_satellite_id %}{{ selected_satellite_id }}{% else %}null{% endif %};
|
||||
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 pendingMarks = {};
|
||||
@@ -360,6 +371,48 @@ function selectSatellite() {
|
||||
function initEntryTable() {
|
||||
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", {
|
||||
ajaxURL: "{% url 'mainapp:signal_marks_entry_api' %}",
|
||||
ajaxParams: { satellite_id: SATELLITE_ID },
|
||||
@@ -370,41 +423,7 @@ function initEntryTable() {
|
||||
layout: "fitColumns",
|
||||
height: "65vh",
|
||||
placeholder: "Нет данных",
|
||||
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>`;
|
||||
}
|
||||
},
|
||||
],
|
||||
columns: columns,
|
||||
});
|
||||
|
||||
// Делегирование событий для кнопок отметок - без перерисовки таблицы
|
||||
@@ -600,12 +619,21 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// Modal
|
||||
function openCreateModal() {
|
||||
if (!CAN_CREATE_TECH_ANALYZE) {
|
||||
alert('У вас нет прав для создания теханализа');
|
||||
return;
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
function createTechAnalyze() {
|
||||
if (!CAN_CREATE_TECH_ANALYZE) {
|
||||
alert('У вас нет прав для создания теханализа');
|
||||
return;
|
||||
}
|
||||
const name = document.getElementById('ta-name').value.trim();
|
||||
if (!name) { alert('Укажите имя'); return; }
|
||||
|
||||
@@ -627,7 +655,8 @@ function createTechAnalyze() {
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
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;
|
||||
updateMarksCount();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
{% load static %}
|
||||
{% load static leaflet_tags %}
|
||||
{% load permission_tags %}
|
||||
|
||||
{% block title %}Список объектов{% endblock %}
|
||||
|
||||
@@ -74,21 +75,22 @@
|
||||
|
||||
<!-- Action buttons -->
|
||||
<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="Создать новый источник">
|
||||
<i class="bi bi-plus-circle"></i> Создать
|
||||
</a>
|
||||
{% endif %}
|
||||
<!-- <a href="{% url 'mainapp:data_entry' %}" class="btn btn-info btn-sm" title="Ввод данных точек спутников">
|
||||
Передача точек
|
||||
</a> -->
|
||||
{% if user|has_perm:'source_import_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
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if user|has_perm:'source_import_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
|
||||
</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="Удалить"
|
||||
onclick="deleteSelectedSources()">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
@@ -98,15 +100,21 @@
|
||||
onclick="showSelectedOnMap()">
|
||||
<i class="bi bi-map"></i> Карта
|
||||
</button>
|
||||
{% if user|has_perm:'source_averaging' %}
|
||||
<a href="{% url 'mainapp:points_averaging' %}" class="btn btn-warning btn-sm" title="Усреднение точек">
|
||||
<i class="bi bi-calculator"></i> Усреднение
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if user|has_perm:'source_tech_analyze' %}
|
||||
<a href="{% url 'mainapp:tech_analyze_list' %}" class="btn btn-info btn-sm" title="Технический анализ">
|
||||
<i class="bi bi-gear-wide-connected"></i> Тех. анализ
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if user|has_perm:'statistics_view' %}
|
||||
<a href="{% url 'mainapp:statistics' %}" class="btn btn-secondary btn-sm" title="Статистика">
|
||||
<i class="bi bi-bar-chart-line"></i> Статистика
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Add to List Button -->
|
||||
@@ -729,7 +737,7 @@
|
||||
<i class="bi bi-list-task"></i>
|
||||
</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 %}"
|
||||
class="btn btn-sm btn-outline-warning"
|
||||
title="Редактировать объект">
|
||||
@@ -2224,7 +2232,7 @@ function showTransponderModal(transponderId) {
|
||||
<button type="button" class="btn btn-outline-info btn-sm" onclick="showPlaybackAnimation()" title="Анимация движения объектов">
|
||||
<i class="bi bi-play-circle"></i> Анимация
|
||||
</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()">
|
||||
<i class="bi bi-union"></i> Объединить
|
||||
</button>
|
||||
@@ -2384,11 +2392,13 @@ function showTransponderModal(transponderId) {
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{% if user|has_perm:'request_create' %}
|
||||
<div class="mb-3">
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="openCreateRequestModalForSource()">
|
||||
<i class="bi bi-plus-circle"></i> Создать заявку
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div id="requestsLoadingSpinner" class="text-center py-4">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<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="История">
|
||||
<i class="bi bi-clock-history"></i>
|
||||
</button>
|
||||
{% if user|has_perm:'request_edit' %}
|
||||
<button type="button" class="btn btn-outline-warning" onclick="editSourceRequest(${req.id})" title="Редактировать">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if user|has_perm:'request_delete' %}
|
||||
<button type="button" class="btn btn-outline-danger" onclick="deleteSourceRequest(${req.id})" title="Удалить">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
{% load static %}
|
||||
{% load permission_tags %}
|
||||
|
||||
{% block title %}Тех. анализ - Список{% endblock %}
|
||||
|
||||
@@ -53,17 +54,21 @@
|
||||
|
||||
<!-- Action buttons -->
|
||||
<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="Ввод данных">
|
||||
<i class="bi bi-plus-circle"></i> Ввод данных
|
||||
</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()">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if user|has_perm:'tech_analyze_edit' %}
|
||||
<button type="button" class="btn btn-info btn-sm" title="Привязать к существующим точкам" onclick="showLinkModal()">
|
||||
<i class="bi bi-link-45deg"></i> Привязать к точкам
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Filter Toggle Button -->
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
{% load permission_tags %}
|
||||
|
||||
{% block title %}Список транспондеров{% endblock %}
|
||||
|
||||
@@ -57,13 +58,17 @@
|
||||
|
||||
<!-- Action buttons -->
|
||||
<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="Создать">
|
||||
<i class="bi bi-plus-circle"></i> Создать
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if user|has_perm:'transponder_import_xml' %}
|
||||
<a href="{% url 'mainapp:add_trans' %}" class="btn btn-warning btn-sm" title="Загрузить из XML">
|
||||
<i class="bi bi-upload"></i> Загрузить XML
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if user|has_perm:'transponder_delete' %}
|
||||
<button type="button" class="btn btn-danger btn-sm" title="Удалить"
|
||||
onclick="deleteSelectedTransponders()">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
@@ -354,7 +359,7 @@
|
||||
<td>{{ transponder.created_at|date:"d.m.Y H:i" }}</td>
|
||||
<td>{{ transponder.updated_at|date:"d.m.Y H:i" }}</td>
|
||||
<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 %}"
|
||||
class="btn btn-sm btn-outline-warning"
|
||||
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.statistics import StatisticsView, StatisticsAPIView, ExtendedStatisticsAPIView
|
||||
from .views.secret_stats import SecretStatsView
|
||||
from .views.user_permissions import (
|
||||
UserPermissionsListView,
|
||||
UserPermissionsEditView,
|
||||
UserPermissionsApiView,
|
||||
InitPermissionsView,
|
||||
)
|
||||
|
||||
app_name = 'mainapp'
|
||||
|
||||
@@ -196,4 +202,10 @@ urlpatterns = [
|
||||
path('api/statistics/extended/', ExtendedStatisticsAPIView.as_view(), name='extended_statistics_api'),
|
||||
path('secret-stat/', SecretStatsView.as_view(), name='secret_stats'),
|
||||
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,
|
||||
)
|
||||
from ..mixins import FormMessageMixin
|
||||
from ..permissions import PermissionRequiredMixin
|
||||
from ..utils import (
|
||||
add_satellite_list,
|
||||
compare_and_link_vch_load,
|
||||
@@ -41,9 +42,10 @@ class AddSatellitesView(LoginRequiredMixin, View):
|
||||
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."""
|
||||
|
||||
permission_required = 'transponder_import_xml'
|
||||
template_name = "mainapp/transponders_upload.html"
|
||||
form_class = UploadFileForm
|
||||
success_message = "Файл успешно обработан"
|
||||
@@ -85,8 +87,9 @@ class AddTranspondersView(LoginRequiredMixin, FormMessageMixin, FormView):
|
||||
return reverse_lazy("mainapp:add_trans")
|
||||
|
||||
|
||||
class LoadExcelDataView(LoginRequiredMixin, FormMessageMixin, FormView):
|
||||
class LoadExcelDataView(LoginRequiredMixin, PermissionRequiredMixin, FormMessageMixin, FormView):
|
||||
"""View for loading data from Excel files."""
|
||||
permission_required = 'source_import_excel'
|
||||
|
||||
template_name = "mainapp/add_data_from_excel.html"
|
||||
form_class = LoadExcelData
|
||||
@@ -134,8 +137,9 @@ class LoadExcelDataView(LoginRequiredMixin, FormMessageMixin, FormView):
|
||||
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."""
|
||||
permission_required = 'source_import_csv'
|
||||
|
||||
template_name = "mainapp/add_data_from_csv.html"
|
||||
form_class = LoadCsvData
|
||||
|
||||
@@ -14,11 +14,13 @@ from openpyxl.styles import Font, Alignment
|
||||
|
||||
from mainapp.forms import KubsatFilterForm
|
||||
from mainapp.models import Source, ObjItem
|
||||
from mainapp.permissions import PermissionRequiredMixin
|
||||
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'
|
||||
form_class = KubsatFilterForm
|
||||
|
||||
@@ -349,8 +351,9 @@ class KubsatView(LoginRequiredMixin, FormView):
|
||||
return queryset.distinct()
|
||||
|
||||
|
||||
class KubsatExportView(LoginRequiredMixin, FormView):
|
||||
class KubsatExportView(LoginRequiredMixin, PermissionRequiredMixin, FormView):
|
||||
"""Экспорт отфильтрованных данных в Excel"""
|
||||
permission_required = 'kubsat_view'
|
||||
form_class = KubsatFilterForm
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
@@ -595,8 +598,9 @@ class KubsatExportView(LoginRequiredMixin, FormView):
|
||||
return response
|
||||
|
||||
|
||||
class KubsatCreateRequestsView(LoginRequiredMixin, FormView):
|
||||
class KubsatCreateRequestsView(LoginRequiredMixin, PermissionRequiredMixin, FormView):
|
||||
"""Массовое создание заявок из отфильтрованных данных"""
|
||||
permission_required = 'request_create'
|
||||
form_class = KubsatFilterForm
|
||||
|
||||
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"""
|
||||
permission_required = 'kubsat_view'
|
||||
form_class = KubsatFilterForm
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
@@ -23,6 +23,7 @@ from mainapp.models import (
|
||||
Modulation,
|
||||
Standard,
|
||||
)
|
||||
from mainapp.permissions import PermissionRequiredMixin, has_permission
|
||||
|
||||
|
||||
class SignalMarksView(LoginRequiredMixin, View):
|
||||
@@ -324,11 +325,12 @@ class SignalMarksEntryAPIView(LoginRequiredMixin, View):
|
||||
})
|
||||
|
||||
|
||||
class SaveSignalMarksView(LoginRequiredMixin, View):
|
||||
class SaveSignalMarksView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||
"""
|
||||
API для сохранения отметок сигналов.
|
||||
Принимает массив отметок и сохраняет их в базу.
|
||||
"""
|
||||
permission_required = 'mark_create'
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
@@ -404,10 +406,11 @@ class SaveSignalMarksView(LoginRequiredMixin, View):
|
||||
}, status=500)
|
||||
|
||||
|
||||
class CreateTechAnalyzeView(LoginRequiredMixin, View):
|
||||
class CreateTechAnalyzeView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||
"""
|
||||
API для создания нового теханализа из модального окна.
|
||||
"""
|
||||
permission_required = 'tech_analyze_create'
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
|
||||
@@ -15,6 +15,7 @@ from django.views.generic import CreateView, DeleteView, UpdateView
|
||||
from ..forms import GeoForm, ObjItemForm, ParameterForm
|
||||
from ..mixins import CoordinateProcessingMixin, FormMessageMixin, RoleRequiredMixin
|
||||
from ..models import Geo, Modulation, ObjItem, Polarization, Satellite
|
||||
from ..permissions import PermissionRequiredMixin
|
||||
from ..utils import (
|
||||
format_coordinate,
|
||||
format_coords_display,
|
||||
@@ -24,10 +25,9 @@ from ..utils import (
|
||||
)
|
||||
|
||||
|
||||
class DeleteSelectedObjectsView(RoleRequiredMixin, View):
|
||||
class DeleteSelectedObjectsView(PermissionRequiredMixin, View):
|
||||
"""View for deleting multiple selected objects."""
|
||||
|
||||
required_roles = ["admin", "moderator"]
|
||||
permission_required = 'objitem_delete'
|
||||
|
||||
def post(self, request):
|
||||
ids = request.POST.get("ids", "")
|
||||
@@ -503,7 +503,7 @@ class ObjItemListView(LoginRequiredMixin, View):
|
||||
|
||||
|
||||
class ObjItemFormView(
|
||||
RoleRequiredMixin, CoordinateProcessingMixin, FormMessageMixin, UpdateView
|
||||
PermissionRequiredMixin, CoordinateProcessingMixin, FormMessageMixin, UpdateView
|
||||
):
|
||||
"""
|
||||
Base class for creating and editing ObjItem.
|
||||
@@ -515,7 +515,7 @@ class ObjItemFormView(
|
||||
form_class = ObjItemForm
|
||||
template_name = "mainapp/objitem_form.html"
|
||||
success_url = reverse_lazy("mainapp:source_list")
|
||||
required_roles = ["admin", "moderator"]
|
||||
permission_required = 'objitem_edit'
|
||||
|
||||
def get_success_url(self):
|
||||
"""Returns URL with saved filter parameters."""
|
||||
@@ -651,7 +651,7 @@ class ObjItemFormView(
|
||||
|
||||
class ObjItemUpdateView(ObjItemFormView):
|
||||
"""View for editing ObjItem."""
|
||||
|
||||
permission_required = 'objitem_edit'
|
||||
success_message = "Объект успешно сохранён!"
|
||||
|
||||
def set_user_fields(self):
|
||||
@@ -660,7 +660,7 @@ class ObjItemUpdateView(ObjItemFormView):
|
||||
|
||||
class ObjItemCreateView(ObjItemFormView, CreateView):
|
||||
"""View for creating ObjItem."""
|
||||
|
||||
permission_required = 'objitem_create'
|
||||
success_message = "Объект успешно создан!"
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
@@ -672,14 +672,13 @@ class ObjItemCreateView(ObjItemFormView, CreateView):
|
||||
self.object.updated_by = self.request.user.customuser
|
||||
|
||||
|
||||
class ObjItemDeleteView(RoleRequiredMixin, FormMessageMixin, DeleteView):
|
||||
class ObjItemDeleteView(PermissionRequiredMixin, FormMessageMixin, DeleteView):
|
||||
"""View for deleting ObjItem."""
|
||||
|
||||
permission_required = 'objitem_delete'
|
||||
model = ObjItem
|
||||
template_name = "mainapp/objitem_confirm_delete.html"
|
||||
success_url = reverse_lazy("mainapp:objitem_list")
|
||||
success_message = "Объект успешно удалён!"
|
||||
required_roles = ["admin", "moderator"]
|
||||
|
||||
def get_success_url(self):
|
||||
"""Returns URL with saved filter parameters."""
|
||||
|
||||
@@ -10,6 +10,7 @@ from django.views import View
|
||||
from django.utils import timezone
|
||||
|
||||
from ..models import ObjItem, Satellite, Source
|
||||
from ..permissions import PermissionRequiredMixin
|
||||
from ..utils import (
|
||||
calculate_mean_coords,
|
||||
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.
|
||||
"""
|
||||
permission_required = 'source_averaging'
|
||||
|
||||
def get(self, request):
|
||||
# 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 ..mixins import RoleRequiredMixin, FormMessageMixin
|
||||
from ..models import Satellite, Band
|
||||
from ..permissions import PermissionRequiredMixin
|
||||
from ..utils import parse_pagination_params
|
||||
|
||||
|
||||
@@ -252,15 +253,14 @@ class SatelliteListView(LoginRequiredMixin, View):
|
||||
return render(request, "mainapp/satellite_list.html", context)
|
||||
|
||||
|
||||
class SatelliteCreateView(RoleRequiredMixin, FormMessageMixin, CreateView):
|
||||
class SatelliteCreateView(PermissionRequiredMixin, FormMessageMixin, CreateView):
|
||||
"""View for creating a new satellite."""
|
||||
|
||||
permission_required = 'satellite_create'
|
||||
model = Satellite
|
||||
form_class = SatelliteForm
|
||||
template_name = "mainapp/satellite_form.html"
|
||||
success_url = reverse_lazy("mainapp:satellite_list")
|
||||
success_message = "Спутник успешно создан!"
|
||||
required_roles = ["admin", "moderator"]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@@ -274,15 +274,14 @@ class SatelliteCreateView(RoleRequiredMixin, FormMessageMixin, CreateView):
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class SatelliteUpdateView(RoleRequiredMixin, FormMessageMixin, UpdateView):
|
||||
class SatelliteUpdateView(PermissionRequiredMixin, FormMessageMixin, UpdateView):
|
||||
"""View for updating an existing satellite."""
|
||||
|
||||
permission_required = 'satellite_edit'
|
||||
model = Satellite
|
||||
form_class = SatelliteForm
|
||||
template_name = "mainapp/satellite_form.html"
|
||||
success_url = reverse_lazy("mainapp:satellite_list")
|
||||
success_message = "Спутник успешно обновлен!"
|
||||
required_roles = ["admin", "moderator"]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
import json
|
||||
@@ -320,10 +319,9 @@ class SatelliteUpdateView(RoleRequiredMixin, FormMessageMixin, UpdateView):
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class DeleteSelectedSatellitesView(RoleRequiredMixin, View):
|
||||
class DeleteSelectedSatellitesView(PermissionRequiredMixin, View):
|
||||
"""View for deleting multiple selected satellites with confirmation."""
|
||||
|
||||
required_roles = ["admin", "moderator"]
|
||||
permission_required = 'satellite_delete'
|
||||
|
||||
def get(self, request):
|
||||
"""Show confirmation page with details about satellites to be deleted."""
|
||||
|
||||
@@ -17,6 +17,7 @@ from django.views import View
|
||||
from ..forms import SourceForm
|
||||
from ..models import Source, Satellite
|
||||
from ..utils import format_coords_display, parse_pagination_params
|
||||
from ..permissions import PermissionRequiredMixin, permission_required
|
||||
|
||||
|
||||
class SourceListView(LoginRequiredMixin, View):
|
||||
@@ -818,7 +819,10 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
|
||||
|
||||
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):
|
||||
return (
|
||||
@@ -832,8 +836,9 @@ class AdminModeratorMixin(UserPassesTestMixin):
|
||||
return redirect('mainapp:source_list')
|
||||
|
||||
|
||||
class SourceCreateView(LoginRequiredMixin, AdminModeratorMixin, View):
|
||||
class SourceCreateView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||
"""View for creating new Source."""
|
||||
permission_required = 'source_create'
|
||||
|
||||
def get(self, request):
|
||||
form = SourceForm()
|
||||
@@ -874,8 +879,9 @@ class SourceCreateView(LoginRequiredMixin, AdminModeratorMixin, View):
|
||||
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."""
|
||||
permission_required = 'source_edit'
|
||||
|
||||
def get(self, request, 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)
|
||||
|
||||
|
||||
class SourceDeleteView(LoginRequiredMixin, AdminModeratorMixin, View):
|
||||
class SourceDeleteView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||
"""View for deleting Source."""
|
||||
permission_required = 'source_delete'
|
||||
|
||||
def get(self, request, pk):
|
||||
source = get_object_or_404(Source, pk=pk)
|
||||
@@ -975,8 +982,9 @@ class SourceDeleteView(LoginRequiredMixin, AdminModeratorMixin, View):
|
||||
return redirect('mainapp:source_list')
|
||||
|
||||
|
||||
class DeleteSelectedSourcesView(LoginRequiredMixin, AdminModeratorMixin, View):
|
||||
class DeleteSelectedSourcesView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||
"""View for deleting multiple selected sources with confirmation."""
|
||||
permission_required = 'source_delete'
|
||||
|
||||
def get(self, request):
|
||||
"""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."""
|
||||
permission_required = 'source_merge'
|
||||
|
||||
def post(self, request):
|
||||
"""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.forms import SourceRequestForm
|
||||
from mainapp.permissions import PermissionRequiredMixin
|
||||
|
||||
import re
|
||||
import pandas as pd
|
||||
@@ -83,8 +84,9 @@ class SourceRequestListView(LoginRequiredMixin, ListView):
|
||||
return context
|
||||
|
||||
|
||||
class SourceRequestCreateView(LoginRequiredMixin, CreateView):
|
||||
class SourceRequestCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||||
"""Создание заявки на источник."""
|
||||
permission_required = 'request_create'
|
||||
model = SourceRequest
|
||||
form_class = SourceRequestForm
|
||||
template_name = 'mainapp/source_request_form.html'
|
||||
@@ -132,8 +134,9 @@ class SourceRequestCreateView(LoginRequiredMixin, CreateView):
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class SourceRequestUpdateView(LoginRequiredMixin, UpdateView):
|
||||
class SourceRequestUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
||||
"""Редактирование заявки на источник."""
|
||||
permission_required = 'request_edit'
|
||||
model = SourceRequest
|
||||
form_class = SourceRequestForm
|
||||
template_name = 'mainapp/source_request_form.html'
|
||||
@@ -164,8 +167,9 @@ class SourceRequestUpdateView(LoginRequiredMixin, UpdateView):
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class SourceRequestDeleteView(LoginRequiredMixin, View):
|
||||
class SourceRequestDeleteView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||
"""Удаление заявки на источник."""
|
||||
permission_required = 'request_delete'
|
||||
|
||||
def post(self, request, pk):
|
||||
try:
|
||||
@@ -182,8 +186,9 @@ class SourceRequestDeleteView(LoginRequiredMixin, View):
|
||||
}, status=404)
|
||||
|
||||
|
||||
class SourceRequestBulkDeleteView(LoginRequiredMixin, View):
|
||||
class SourceRequestBulkDeleteView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||
"""Массовое удаление заявок."""
|
||||
permission_required = 'request_delete'
|
||||
|
||||
def post(self, request):
|
||||
import json
|
||||
@@ -688,8 +693,9 @@ class SourceDataAPIView(LoginRequiredMixin, View):
|
||||
return JsonResponse(data)
|
||||
|
||||
|
||||
class SourceRequestImportView(LoginRequiredMixin, View):
|
||||
class SourceRequestImportView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||
"""Импорт заявок из Excel файла."""
|
||||
permission_required = 'request_import'
|
||||
|
||||
def get(self, request):
|
||||
"""Отображает форму загрузки файла."""
|
||||
|
||||
@@ -10,11 +10,13 @@ from django.views.generic import TemplateView
|
||||
from django.http import JsonResponse
|
||||
|
||||
from ..models import ObjItem, Source, Satellite, Geo, SourceRequest, SourceRequestStatusHistory
|
||||
from ..permissions import PermissionRequiredMixin
|
||||
from mapsapp.models import Transponders
|
||||
|
||||
|
||||
class StatisticsView(TemplateView):
|
||||
class StatisticsView(PermissionRequiredMixin, TemplateView):
|
||||
"""Страница статистики по данным геолокации."""
|
||||
permission_required = 'statistics_view'
|
||||
|
||||
template_name = 'mainapp/statistics.html'
|
||||
|
||||
|
||||
@@ -19,13 +19,15 @@ from ..models import (
|
||||
Parameter,
|
||||
)
|
||||
from ..mixins import RoleRequiredMixin
|
||||
from ..permissions import PermissionRequiredMixin
|
||||
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):
|
||||
satellites = Satellite.objects.all().order_by('name')
|
||||
@@ -37,10 +39,11 @@ class TechAnalyzeEntryView(LoginRequiredMixin, View):
|
||||
return render(request, 'mainapp/tech_analyze_entry.html', context)
|
||||
|
||||
|
||||
class TechAnalyzeSaveView(LoginRequiredMixin, View):
|
||||
class TechAnalyzeSaveView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||
"""
|
||||
API endpoint для сохранения данных технического анализа.
|
||||
"""
|
||||
permission_required = 'tech_analyze_create'
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
@@ -177,7 +180,7 @@ class TechAnalyzeSaveView(LoginRequiredMixin, View):
|
||||
|
||||
|
||||
|
||||
class LinkExistingPointsView(LoginRequiredMixin, View):
|
||||
class LinkExistingPointsView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||
"""
|
||||
API endpoint для привязки существующих точек к данным теханализа.
|
||||
|
||||
@@ -194,6 +197,7 @@ class LinkExistingPointsView(LoginRequiredMixin, View):
|
||||
* Обновить полосу частот (если 0 или None)
|
||||
* Подобрать подходящий транспондер
|
||||
"""
|
||||
permission_required = 'tech_analyze_edit'
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
@@ -388,11 +392,11 @@ class TechAnalyzeListView(LoginRequiredMixin, View):
|
||||
return render(request, 'mainapp/tech_analyze_list.html', context)
|
||||
|
||||
|
||||
class TechAnalyzeDeleteView(LoginRequiredMixin, RoleRequiredMixin, View):
|
||||
class TechAnalyzeDeleteView(LoginRequiredMixin, PermissionRequiredMixin, View):
|
||||
"""
|
||||
API endpoint для удаления выбранных записей теханализа.
|
||||
"""
|
||||
allowed_roles = ['admin', 'moderator']
|
||||
permission_required = 'tech_analyze_delete'
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
|
||||
@@ -15,7 +15,8 @@ from django.views.generic import CreateView, UpdateView
|
||||
|
||||
from mapsapp.models import Transponders
|
||||
from ..forms import TransponderForm
|
||||
from ..mixins import RoleRequiredMixin, FormMessageMixin
|
||||
from ..mixins import FormMessageMixin
|
||||
from ..permissions import PermissionRequiredMixin
|
||||
from ..models import Satellite, Polarization
|
||||
from ..utils import parse_pagination_params
|
||||
|
||||
@@ -246,15 +247,15 @@ class TransponderListView(LoginRequiredMixin, View):
|
||||
return render(request, "mainapp/transponder_list.html", context)
|
||||
|
||||
|
||||
class TransponderCreateView(RoleRequiredMixin, FormMessageMixin, CreateView):
|
||||
class TransponderCreateView(PermissionRequiredMixin, FormMessageMixin, CreateView):
|
||||
"""View for creating a new transponder."""
|
||||
|
||||
permission_required = 'transponder_create'
|
||||
model = Transponders
|
||||
form_class = TransponderForm
|
||||
template_name = "mainapp/transponder_form.html"
|
||||
success_url = reverse_lazy("mainapp:transponder_list")
|
||||
success_message = "Транспондер успешно создан!"
|
||||
required_roles = ["admin", "moderator"]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@@ -268,15 +269,15 @@ class TransponderCreateView(RoleRequiredMixin, FormMessageMixin, CreateView):
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class TransponderUpdateView(RoleRequiredMixin, FormMessageMixin, UpdateView):
|
||||
class TransponderUpdateView(PermissionRequiredMixin, FormMessageMixin, UpdateView):
|
||||
"""View for updating an existing transponder."""
|
||||
|
||||
permission_required = 'transponder_edit'
|
||||
model = Transponders
|
||||
form_class = TransponderForm
|
||||
template_name = "mainapp/transponder_form.html"
|
||||
success_url = reverse_lazy("mainapp:transponder_list")
|
||||
success_message = "Транспондер успешно обновлен!"
|
||||
required_roles = ["admin", "moderator"]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@@ -293,10 +294,10 @@ class TransponderUpdateView(RoleRequiredMixin, FormMessageMixin, UpdateView):
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class DeleteSelectedTranspondersView(RoleRequiredMixin, View):
|
||||
class DeleteSelectedTranspondersView(PermissionRequiredMixin, View):
|
||||
"""View for deleting multiple selected transponders with confirmation."""
|
||||
|
||||
required_roles = ["admin", "moderator"]
|
||||
permission_required = 'transponder_delete'
|
||||
|
||||
def get(self, request):
|
||||
"""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