Реализовал систему разрешений

This commit is contained in:
2025-12-15 11:45:25 +03:00
parent ca7709ebff
commit 46dc79b93f
33 changed files with 1340 additions and 124 deletions

View File

@@ -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",
# },
]
# ============================================================================

View File

@@ -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
# ============================================================================

View 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,
}

View 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}')

View 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='Индивидуальные разрешения'),
),
]

View File

@@ -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 = "Пользователь"

View 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')

View File

@@ -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">

View File

@@ -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 %}

View File

@@ -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

View File

@@ -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>

View File

@@ -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"

View File

@@ -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="Редактировать спутник">

View File

@@ -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();
}

View File

@@ -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>
`;

View File

@@ -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 -->

View File

@@ -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="Редактировать транспондер">

View 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 %}

View 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 %}

View 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, [])

View File

@@ -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'),
]

View File

@@ -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

View File

@@ -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):

View File

@@ -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:

View File

@@ -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."""

View File

@@ -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

View File

@@ -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."""

View File

@@ -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."""

View File

@@ -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):
"""Отображает форму загрузки файла."""

View File

@@ -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'

View File

@@ -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:

View File

@@ -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."""

View 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')