feat: add user roles management UI with owner access control
- Added role management views (list, create, edit, delete) - Created user_roles URL routing - Added role management templates with Bootstrap styling - Updated navbar with Roles link for owners and superusers - Enhanced decorators and mixins with superuser bypass - Added assign_owner_role.py utility script 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,10 @@ def role_required(*role_codes):
|
||||
if not request.user.is_authenticated:
|
||||
return redirect('login')
|
||||
|
||||
# Superuser имеет полный доступ
|
||||
if request.user.is_superuser:
|
||||
return view_func(request, *args, **kwargs)
|
||||
|
||||
if RoleService.user_has_role(request.user, *role_codes):
|
||||
return view_func(request, *args, **kwargs)
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ class RoleBasedAdminMixin:
|
||||
if not super().has_module_permission(request):
|
||||
return False
|
||||
|
||||
# Superuser имеет полный доступ
|
||||
if request.user.is_superuser:
|
||||
return True
|
||||
|
||||
if not self.required_roles:
|
||||
return True # Нет ограничений
|
||||
|
||||
@@ -28,6 +32,10 @@ class RoleBasedAdminMixin:
|
||||
if not super().has_view_permission(request, obj):
|
||||
return False
|
||||
|
||||
# Superuser имеет полный доступ
|
||||
if request.user.is_superuser:
|
||||
return True
|
||||
|
||||
if not self.required_roles:
|
||||
return True
|
||||
|
||||
@@ -38,6 +46,10 @@ class RoleBasedAdminMixin:
|
||||
if not super().has_add_permission(request):
|
||||
return False
|
||||
|
||||
# Superuser имеет полный доступ
|
||||
if request.user.is_superuser:
|
||||
return True
|
||||
|
||||
if not self.required_roles:
|
||||
return True
|
||||
|
||||
@@ -48,6 +60,10 @@ class RoleBasedAdminMixin:
|
||||
if not super().has_change_permission(request, obj):
|
||||
return False
|
||||
|
||||
# Superuser имеет полный доступ
|
||||
if request.user.is_superuser:
|
||||
return True
|
||||
|
||||
if not self.required_roles:
|
||||
return True
|
||||
|
||||
@@ -58,6 +74,10 @@ class RoleBasedAdminMixin:
|
||||
if not super().has_delete_permission(request, obj):
|
||||
return False
|
||||
|
||||
# Superuser имеет полный доступ
|
||||
if request.user.is_superuser:
|
||||
return True
|
||||
|
||||
if not self.required_roles:
|
||||
return True
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Добавить пользователя{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="mb-0">Добавить пользователя</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email *</label>
|
||||
<input type="email" class="form-control" id="email" name="email" required>
|
||||
<div class="form-text">Email пользователя для входа</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Имя *</label>
|
||||
<input type="text" class="form-control" id="name" name="name" required>
|
||||
<div class="form-text">Полное имя пользователя</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="role" class="form-label">Роль *</label>
|
||||
<select class="form-select" id="role" name="role" required>
|
||||
<option value="">Выберите роль...</option>
|
||||
{% for role in roles %}
|
||||
<option value="{{ role.code }}">{{ role.name }} - {{ role.description }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Пароль</label>
|
||||
<input type="text" class="form-control" id="password" name="password">
|
||||
<div class="form-text">Оставьте пустым для автогенерации. Пароль будет показан после создания.</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{% url 'user_roles:list' %}" class="btn btn-secondary">Отмена</a>
|
||||
<button type="submit" class="btn btn-primary">Создать пользователя</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,61 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Удалить пользователя{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card border-danger">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h4 class="mb-0">Удалить доступ пользователя</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
<strong>Внимание!</strong> Вы собираетесь удалить доступ пользователя к системе.
|
||||
</div>
|
||||
|
||||
<dl class="row">
|
||||
<dt class="col-sm-4">Email:</dt>
|
||||
<dd class="col-sm-8">{{ user_role.user.email }}</dd>
|
||||
|
||||
<dt class="col-sm-4">Имя:</dt>
|
||||
<dd class="col-sm-8">{{ user_role.user.name }}</dd>
|
||||
|
||||
<dt class="col-sm-4">Роль:</dt>
|
||||
<dd class="col-sm-8">
|
||||
<span class="badge
|
||||
{% if user_role.role.code == 'owner' %}bg-danger
|
||||
{% elif user_role.role.code == 'manager' %}bg-primary
|
||||
{% elif user_role.role.code == 'florist' %}bg-success
|
||||
{% elif user_role.role.code == 'courier' %}bg-info
|
||||
{% endif %}">
|
||||
{{ user_role.role.name }}
|
||||
</span>
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-4">Создан:</dt>
|
||||
<dd class="col-sm-8">{{ user_role.created_at|date:"d.m.Y H:i" }}</dd>
|
||||
</dl>
|
||||
|
||||
<p class="text-muted">
|
||||
Пользователь больше не сможет войти в систему через этот магазин.
|
||||
Учетная запись пользователя не будет удалена.
|
||||
</p>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{% url 'user_roles:list' %}" class="btn btn-secondary">Отмена</a>
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="bi bi-trash"></i> Да, удалить доступ
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,63 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Изменить роль пользователя{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="mb-0">Изменить роль пользователя</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Пользователь:</label>
|
||||
<p class="form-control-plaintext">{{ user_role.user.email }} ({{ user_role.user.name }})</p>
|
||||
</div>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="role" class="form-label">Роль *</label>
|
||||
<select class="form-select" id="role" name="role" required>
|
||||
{% for role in roles %}
|
||||
<option value="{{ role.code }}" {% if role.code == user_role.role.code %}selected{% endif %}>
|
||||
{{ role.name }} - {{ role.description }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="is_active" name="is_active"
|
||||
{% if user_role.is_active %}checked{% endif %}>
|
||||
<label class="form-check-label" for="is_active">
|
||||
Активен
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-text">Снимите галочку, чтобы временно отключить доступ</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{% url 'user_roles:list' %}" class="btn btn-secondary">Отмена</a>
|
||||
<button type="submit" class="btn btn-primary">Сохранить изменения</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,93 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Управление ролями пользователей{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2>Управление ролями пользователей</h2>
|
||||
<a href="{% url 'user_roles:create' %}" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Добавить пользователя
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{% if user_roles %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<th>Имя</th>
|
||||
<th>Роль</th>
|
||||
<th>Статус</th>
|
||||
<th>Создан</th>
|
||||
<th>Создал</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user_role in user_roles %}
|
||||
<tr>
|
||||
<td>{{ user_role.user.email }}</td>
|
||||
<td>{{ user_role.user.name }}</td>
|
||||
<td>
|
||||
<span class="badge
|
||||
{% if user_role.role.code == 'owner' %}bg-danger
|
||||
{% elif user_role.role.code == 'manager' %}bg-primary
|
||||
{% elif user_role.role.code == 'florist' %}bg-success
|
||||
{% elif user_role.role.code == 'courier' %}bg-info
|
||||
{% endif %}">
|
||||
{{ user_role.role.name }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if user_role.is_active %}
|
||||
<span class="badge bg-success">Активен</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Неактивен</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ user_role.created_at|date:"d.m.Y H:i" }}</td>
|
||||
<td>
|
||||
{% if user_role.created_by %}
|
||||
{{ user_role.created_by.name }}
|
||||
{% else %}
|
||||
—
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'user_roles:edit' user_role.pk %}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-pencil"></i> Изменить
|
||||
</a>
|
||||
<a href="{% url 'user_roles:delete' user_role.pk %}" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<p class="text-muted">Нет пользователей с ролями</p>
|
||||
<a href="{% url 'user_roles:create' %}" class="btn btn-primary">
|
||||
Добавить первого пользователя
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
11
myproject/user_roles/urls.py
Normal file
11
myproject/user_roles/urls.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'user_roles'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.user_role_list, name='list'),
|
||||
path('create/', views.user_role_create, name='create'),
|
||||
path('<int:pk>/edit/', views.user_role_edit, name='edit'),
|
||||
path('<int:pk>/delete/', views.user_role_delete, name='delete'),
|
||||
]
|
||||
@@ -1,3 +1,110 @@
|
||||
from django.shortcuts import render
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import get_user_model
|
||||
from user_roles.models import Role, UserRole
|
||||
from user_roles.services import RoleService
|
||||
from user_roles.decorators import owner_required
|
||||
|
||||
# Create your views here.
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
@login_required
|
||||
@owner_required
|
||||
def user_role_list(request):
|
||||
"""Список пользователей с их ролями"""
|
||||
user_roles = UserRole.objects.select_related('user', 'role', 'created_by').all()
|
||||
roles = Role.objects.all()
|
||||
|
||||
context = {
|
||||
'user_roles': user_roles,
|
||||
'roles': roles,
|
||||
}
|
||||
return render(request, 'user_roles/user_role_list.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@owner_required
|
||||
def user_role_create(request):
|
||||
"""Создание нового пользователя с ролью"""
|
||||
if request.method == 'POST':
|
||||
email = request.POST.get('email')
|
||||
name = request.POST.get('name')
|
||||
role_code = request.POST.get('role')
|
||||
password = request.POST.get('password', User.objects.make_random_password(12))
|
||||
|
||||
try:
|
||||
# Создаем пользователя
|
||||
user = User.objects.create_user(
|
||||
email=email,
|
||||
name=name,
|
||||
password=password,
|
||||
is_email_confirmed=True
|
||||
)
|
||||
|
||||
# Назначаем роль
|
||||
RoleService.assign_role_to_user(user, role_code, created_by=request.user)
|
||||
|
||||
messages.success(request, f'Пользователь {email} создан с ролью {role_code}. Пароль: {password}')
|
||||
return redirect('user_roles:list')
|
||||
|
||||
except Exception as e:
|
||||
messages.error(request, f'Ошибка при создании пользователя: {str(e)}')
|
||||
|
||||
roles = Role.objects.all()
|
||||
context = {
|
||||
'roles': roles,
|
||||
}
|
||||
return render(request, 'user_roles/user_role_create.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@owner_required
|
||||
def user_role_edit(request, pk):
|
||||
"""Изменение роли пользователя"""
|
||||
user_role = get_object_or_404(UserRole, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
role_code = request.POST.get('role')
|
||||
is_active = request.POST.get('is_active') == 'on'
|
||||
|
||||
try:
|
||||
# Обновляем роль
|
||||
role = RoleService.get_role_by_code(role_code)
|
||||
if not role:
|
||||
raise ValueError(f"Роль '{role_code}' не найдена")
|
||||
|
||||
user_role.role = role
|
||||
user_role.is_active = is_active
|
||||
user_role.save()
|
||||
|
||||
messages.success(request, f'Роль пользователя {user_role.user.email} обновлена')
|
||||
return redirect('user_roles:list')
|
||||
|
||||
except Exception as e:
|
||||
messages.error(request, f'Ошибка при обновлении роли: {str(e)}')
|
||||
|
||||
roles = Role.objects.all()
|
||||
context = {
|
||||
'user_role': user_role,
|
||||
'roles': roles,
|
||||
}
|
||||
return render(request, 'user_roles/user_role_edit.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@owner_required
|
||||
def user_role_delete(request, pk):
|
||||
"""Удаление роли пользователя (отключение доступа)"""
|
||||
user_role = get_object_or_404(UserRole, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
email = user_role.user.email
|
||||
user_role.delete()
|
||||
messages.success(request, f'Доступ пользователя {email} удален')
|
||||
return redirect('user_roles:list')
|
||||
|
||||
context = {
|
||||
'user_role': user_role,
|
||||
}
|
||||
return render(request, 'user_roles/user_role_delete.html', context)
|
||||
|
||||
Reference in New Issue
Block a user