Browse Source

feat: implement account role system with custom models, admin integration, and settings

main
sina_sajjadi 3 days ago
parent
commit
e018d6d0b5
  1. 1
      apps/account/admin/__init__.py
  2. 142
      apps/account/admin/role.py
  3. 24
      apps/account/admin/user.py
  4. 387
      apps/account/management/commands/seed_roles.py
  5. 88
      apps/account/migrations/0005_role_user_custom_permissions_user_role.py
  6. 2
      apps/account/models/__init__.py
  7. 188
      apps/account/models/role.py
  8. 55
      apps/account/models/user.py
  9. 15
      config/settings/base.py

1
apps/account/admin/__init__.py

@ -6,6 +6,7 @@ from .location import *
from .login_history import *
from .notification import *
from .verification import *
from .role import *
@register_component

142
apps/account/admin/role.py

@ -0,0 +1,142 @@
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from unfold.admin import ModelAdmin
from unfold.decorators import display
from apps.account.models.role import Role, PERMISSION_CATEGORIES
from utils.admin import project_admin_site
class RoleAdmin(ModelAdmin):
list_display = ('name', 'slug', 'is_default', 'is_system', 'users_count', 'updated_at')
list_filter = ('is_default', 'is_system')
search_fields = ('name', 'slug', 'description')
ordering = ('name',)
readonly_fields = ('created_at', 'updated_at')
fieldsets = (
(None, {
"fields": (
("name", "slug"),
("is_default", "is_system"),
"description",
),
}),
(_("Identity & Profiles"), {
"classes": ["tab"],
"fields": (
("is_searchable", "can_submit_verification"),
("can_view_full_profiles", "can_manage_institutions"),
),
}),
(_("Chat & Communications"), {
"classes": ["tab"],
"fields": (
("can_send_direct_messages", "can_create_group_chats"),
("can_start_video_calls", "can_moderate_chat"),
("is_chat_muted",),
),
}),
(_("CMS & Articles"), {
"classes": ["tab"],
"fields": (
("can_create_posts", "can_publish_posts_directly"),
("can_edit_own_posts", "can_delete_own_posts"),
("can_comment",),
),
}),
(_("LMS & Academics"), {
"classes": ["tab"],
"fields": (
("can_enroll_courses", "can_create_courses"),
("can_publish_courses", "can_issue_certificates"),
("can_grade_submissions", "has_premium_lms_access"),
),
}),
(_("Meetings"), {
"classes": ["tab"],
"fields": (
("can_request_meetings", "can_accept_meetings"),
),
}),
(_("Events"), {
"classes": ["tab"],
"fields": (
("can_register_events", "can_create_events"),
("can_publish_events", "can_checkin_attendees"),
("can_export_attendee_list",),
),
}),
(_("Community Projects"), {
"classes": ["tab"],
"fields": (
("can_volunteer", "can_create_projects"),
("can_manage_project_tasks", "can_approve_projects"),
),
}),
(_("Donations & Charity"), {
"classes": ["tab"],
"fields": (
("can_make_donations", "can_create_donation_campaigns"),
("can_view_donation_reports",),
),
}),
(_("Support & Tickets"), {
"classes": ["tab"],
"fields": (
("can_create_tickets", "can_reply_tickets"),
("can_manage_tickets",),
),
}),
(_("Dynamic Forms"), {
"classes": ["tab"],
"fields": (
("can_submit_forms", "can_create_forms"),
("can_export_form_data",),
),
}),
(_("Diplomacy & Institutional"), {
"classes": ["tab"],
"fields": (
("can_view_diplomatic_reports", "can_create_diplomatic_reports"),
),
}),
(_("Administration & Security"), {
"classes": ["tab"],
"fields": (
("can_access_admin_panel", "can_manage_users"),
("can_ban_users", "can_view_analytics"),
),
}),
(_("Metadata"), {
"classes": ["tab"],
"fields": (
("created_at", "updated_at"),
),
}),
)
@display(description=_("Assigned Users"))
def users_count(self, obj):
count = obj.users.count()
return f"{count} users"
def has_add_permission(self, request):
return bool(request.user and request.user.is_authenticated and (request.user.is_staff or request.user.is_superuser or request.user.can_access_admin_panel()))
def has_change_permission(self, request, obj=None):
return bool(request.user and request.user.is_authenticated and (request.user.is_staff or request.user.is_superuser or request.user.can_access_admin_panel()))
def has_delete_permission(self, request, obj=None):
if obj and getattr(obj, 'is_system', False):
return False # Protect core default roles from accidental deletion
return bool(request.user and request.user.is_authenticated and (request.user.is_staff or request.user.is_superuser or request.user.can_access_admin_panel()))
# Register in both default and custom admin sites
try:
admin.site.register(Role, RoleAdmin)
except admin.sites.AlreadyRegistered:
pass
project_admin_site.register(Role, RoleAdmin)

24
apps/account/admin/user.py

@ -55,10 +55,11 @@ class UserAdmin(ModelAdmin, BaseUserAdmin):
add_form = UserAdminCreationForm
change_password_form = AdminPasswordChangeForm
compressed_fields = False
list_display = ('first_name', 'last_name', 'fullname', 'email', 'user_type', 'is_staff', 'is_active', 'display_date_joined',)
list_display = ('first_name', 'last_name', 'fullname', 'email', 'role', 'user_type', 'is_staff', 'is_active', 'display_date_joined',)
ordering = ("-id",)
search_fields = ('email', 'first_name', 'last_name', 'fullname', 'username',)
list_filter = [
"role",
"user_type",
"is_active",
"is_staff",
@ -73,6 +74,7 @@ class UserAdmin(ModelAdmin, BaseUserAdmin):
'fields': (
('first_name', 'last_name'),
('fullname', 'email'),
'role',
'phone_number',
('title', 'rank'),
('institution_name', 'website'),
@ -107,14 +109,21 @@ class UserAdmin(ModelAdmin, BaseUserAdmin):
("info",),
),
}),
(_("Access & Status"), {
(_("Access & Role"), {
"classes": ["tab"],
"fields": (
("user_type",),
("role", "user_type"),
("is_active", "is_staff", "is_superuser"),
("groups", "user_permissions"),
),
}),
(_("Custom Permissions"), {
"classes": ["tab"],
"description": _("Override specific permissions for this individual user (JSON dictionary of {permission_name: True/False}). Overrides take precedence over the role defaults."),
"fields": (
"custom_permissions",
),
}),
(_("Timestamps & Metadata"), {
"classes": ["tab"],
"fields": (
@ -147,6 +156,15 @@ class GroupAdmin(BaseGroupAdmin, ModelAdmin):
count = obj.permissions.count()
return ngettext("{count} permission", "{count} permissions", count).format(count=count) if count > 0 else "-"
def has_add_permission(self, request):
return bool(request.user and request.user.is_authenticated and (request.user.is_staff or request.user.is_superuser or request.user.can_access_admin_panel()))
def has_change_permission(self, request, obj=None):
return bool(request.user and request.user.is_authenticated and (request.user.is_staff or request.user.is_superuser or request.user.can_access_admin_panel()))
def has_delete_permission(self, request, obj=None):
return bool(request.user and request.user.is_authenticated and (request.user.is_staff or request.user.is_superuser or request.user.can_access_admin_panel()))
# =========================================================
# 2. Registrations

387
apps/account/management/commands/seed_roles.py

@ -0,0 +1,387 @@
from django.core.management.base import BaseCommand
from django.db import transaction
from apps.account.models.role import Role, ALL_PERMISSION_FIELDS
DEFAULT_ROLES = [
{
"name": "User",
"slug": "user",
"description": "Standard community member with basic profile, messaging, learning, and participation privileges.",
"is_default": True,
"is_system": True,
"permissions": {
"is_searchable": True,
"can_submit_verification": True,
"can_view_full_profiles": True,
"can_manage_institutions": False,
"can_send_direct_messages": True,
"can_create_group_chats": True,
"can_start_video_calls": False,
"can_moderate_chat": False,
"is_chat_muted": False,
"can_create_posts": True,
"can_publish_posts_directly": False,
"can_edit_own_posts": True,
"can_delete_own_posts": True,
"can_comment": True,
"can_enroll_courses": True,
"can_create_courses": False,
"can_publish_courses": False,
"can_issue_certificates": False,
"can_grade_submissions": False,
"has_premium_lms_access": False,
"can_request_meetings": True,
"can_accept_meetings": False,
"can_register_events": True,
"can_create_events": False,
"can_publish_events": False,
"can_checkin_attendees": False,
"can_export_attendee_list": False,
"can_volunteer": True,
"can_create_projects": False,
"can_manage_project_tasks": False,
"can_approve_projects": False,
"can_make_donations": True,
"can_create_donation_campaigns": False,
"can_view_donation_reports": False,
"can_create_tickets": True,
"can_reply_tickets": True,
"can_manage_tickets": False,
"can_submit_forms": True,
"can_create_forms": False,
"can_export_form_data": False,
"can_view_diplomatic_reports": False,
"can_create_diplomatic_reports": False,
"can_access_admin_panel": False,
"can_manage_users": False,
"can_ban_users": False,
"can_view_analytics": False,
}
},
{
"name": "Verified User",
"slug": "verified_user",
"description": "Verified community member with authenticated identity, video calling, and direct content publishing.",
"is_default": False,
"is_system": True,
"permissions": {
"is_searchable": True,
"can_submit_verification": True,
"can_view_full_profiles": True,
"can_manage_institutions": False,
"can_send_direct_messages": True,
"can_create_group_chats": True,
"can_start_video_calls": True,
"can_moderate_chat": False,
"is_chat_muted": False,
"can_create_posts": True,
"can_publish_posts_directly": True,
"can_edit_own_posts": True,
"can_delete_own_posts": True,
"can_comment": True,
"can_enroll_courses": True,
"can_create_courses": False,
"can_publish_courses": False,
"can_issue_certificates": False,
"can_grade_submissions": False,
"has_premium_lms_access": True,
"can_request_meetings": True,
"can_accept_meetings": False,
"can_register_events": True,
"can_create_events": True,
"can_publish_events": False,
"can_checkin_attendees": False,
"can_export_attendee_list": False,
"can_volunteer": True,
"can_create_projects": False,
"can_manage_project_tasks": False,
"can_approve_projects": False,
"can_make_donations": True,
"can_create_donation_campaigns": False,
"can_view_donation_reports": False,
"can_create_tickets": True,
"can_reply_tickets": True,
"can_manage_tickets": False,
"can_submit_forms": True,
"can_create_forms": False,
"can_export_form_data": False,
"can_view_diplomatic_reports": False,
"can_create_diplomatic_reports": False,
"can_access_admin_panel": False,
"can_manage_users": False,
"can_ban_users": False,
"can_view_analytics": False,
}
},
{
"name": "Institution Admin",
"slug": "institution_admin",
"description": "Administrator of an Islamic center, university, or institution with management over courses, events, meetings, and campaigns.",
"is_default": False,
"is_system": True,
"permissions": {
"is_searchable": True,
"can_submit_verification": True,
"can_view_full_profiles": True,
"can_manage_institutions": True,
"can_send_direct_messages": True,
"can_create_group_chats": True,
"can_start_video_calls": True,
"can_moderate_chat": False,
"is_chat_muted": False,
"can_create_posts": True,
"can_publish_posts_directly": True,
"can_edit_own_posts": True,
"can_delete_own_posts": True,
"can_comment": True,
"can_enroll_courses": True,
"can_create_courses": True,
"can_publish_courses": True,
"can_issue_certificates": True,
"can_grade_submissions": True,
"has_premium_lms_access": True,
"can_request_meetings": True,
"can_accept_meetings": True,
"can_register_events": True,
"can_create_events": True,
"can_publish_events": True,
"can_checkin_attendees": True,
"can_export_attendee_list": True,
"can_volunteer": True,
"can_create_projects": True,
"can_manage_project_tasks": True,
"can_approve_projects": False,
"can_make_donations": True,
"can_create_donation_campaigns": True,
"can_view_donation_reports": True,
"can_create_tickets": True,
"can_reply_tickets": True,
"can_manage_tickets": False,
"can_submit_forms": True,
"can_create_forms": True,
"can_export_form_data": True,
"can_view_diplomatic_reports": True,
"can_create_diplomatic_reports": False,
"can_access_admin_panel": False,
"can_manage_users": False,
"can_ban_users": False,
"can_view_analytics": False,
}
},
{
"name": "Institution Owner",
"slug": "institution_owner",
"description": "Owner / Primary Representative of an institution with full oversight over institutional projects, campaigns, and diplomacy.",
"is_default": False,
"is_system": True,
"permissions": {
"is_searchable": True,
"can_submit_verification": True,
"can_view_full_profiles": True,
"can_manage_institutions": True,
"can_send_direct_messages": True,
"can_create_group_chats": True,
"can_start_video_calls": True,
"can_moderate_chat": False,
"is_chat_muted": False,
"can_create_posts": True,
"can_publish_posts_directly": True,
"can_edit_own_posts": True,
"can_delete_own_posts": True,
"can_comment": True,
"can_enroll_courses": True,
"can_create_courses": True,
"can_publish_courses": True,
"can_issue_certificates": True,
"can_grade_submissions": True,
"has_premium_lms_access": True,
"can_request_meetings": True,
"can_accept_meetings": True,
"can_register_events": True,
"can_create_events": True,
"can_publish_events": True,
"can_checkin_attendees": True,
"can_export_attendee_list": True,
"can_volunteer": True,
"can_create_projects": True,
"can_manage_project_tasks": True,
"can_approve_projects": True,
"can_make_donations": True,
"can_create_donation_campaigns": True,
"can_view_donation_reports": True,
"can_create_tickets": True,
"can_reply_tickets": True,
"can_manage_tickets": False,
"can_submit_forms": True,
"can_create_forms": True,
"can_export_form_data": True,
"can_view_diplomatic_reports": True,
"can_create_diplomatic_reports": True,
"can_access_admin_panel": False,
"can_manage_users": False,
"can_ban_users": False,
"can_view_analytics": False,
}
},
{
"name": "Content Moderator",
"slug": "content_moderator",
"description": "Community moderator responsible for chat safety, content review, and ticket management.",
"is_default": False,
"is_system": True,
"permissions": {
"is_searchable": True,
"can_submit_verification": True,
"can_view_full_profiles": True,
"can_manage_institutions": False,
"can_send_direct_messages": True,
"can_create_group_chats": True,
"can_start_video_calls": False,
"can_moderate_chat": True,
"is_chat_muted": False,
"can_create_posts": True,
"can_publish_posts_directly": True,
"can_edit_own_posts": True,
"can_delete_own_posts": True,
"can_comment": True,
"can_enroll_courses": True,
"can_create_courses": False,
"can_publish_courses": False,
"can_issue_certificates": False,
"can_grade_submissions": False,
"has_premium_lms_access": False,
"can_request_meetings": True,
"can_accept_meetings": False,
"can_register_events": True,
"can_create_events": False,
"can_publish_events": False,
"can_checkin_attendees": False,
"can_export_attendee_list": False,
"can_volunteer": True,
"can_create_projects": False,
"can_manage_project_tasks": False,
"can_approve_projects": False,
"can_make_donations": True,
"can_create_donation_campaigns": False,
"can_view_donation_reports": False,
"can_create_tickets": True,
"can_reply_tickets": True,
"can_manage_tickets": True,
"can_submit_forms": True,
"can_create_forms": False,
"can_export_form_data": False,
"can_view_diplomatic_reports": False,
"can_create_diplomatic_reports": False,
"can_access_admin_panel": True,
"can_manage_users": False,
"can_ban_users": False,
"can_view_analytics": False,
}
},
{
"name": "Regional Admin",
"slug": "regional_admin",
"description": "Regional supervisor with authority over regional institutions, user accounts, and community analytics.",
"is_default": False,
"is_system": True,
"permissions": {
"is_searchable": True,
"can_submit_verification": True,
"can_view_full_profiles": True,
"can_manage_institutions": True,
"can_send_direct_messages": True,
"can_create_group_chats": True,
"can_start_video_calls": True,
"can_moderate_chat": True,
"is_chat_muted": False,
"can_create_posts": True,
"can_publish_posts_directly": True,
"can_edit_own_posts": True,
"can_delete_own_posts": True,
"can_comment": True,
"can_enroll_courses": True,
"can_create_courses": True,
"can_publish_courses": True,
"can_issue_certificates": True,
"can_grade_submissions": True,
"has_premium_lms_access": True,
"can_request_meetings": True,
"can_accept_meetings": True,
"can_register_events": True,
"can_create_events": True,
"can_publish_events": True,
"can_checkin_attendees": True,
"can_export_attendee_list": True,
"can_volunteer": True,
"can_create_projects": True,
"can_manage_project_tasks": True,
"can_approve_projects": True,
"can_make_donations": True,
"can_create_donation_campaigns": True,
"can_view_donation_reports": True,
"can_create_tickets": True,
"can_reply_tickets": True,
"can_manage_tickets": True,
"can_submit_forms": True,
"can_create_forms": True,
"can_export_form_data": True,
"can_view_diplomatic_reports": True,
"can_create_diplomatic_reports": True,
"can_access_admin_panel": True,
"can_manage_users": True,
"can_ban_users": True,
"can_view_analytics": True,
}
},
{
"name": "Super Administrator",
"slug": "super_admin",
"description": "Master administrative role with all permissions, total access control, and full platform authority.",
"is_default": False,
"is_system": True,
"permissions": {perm: (perm != "is_chat_muted") for perm in ALL_PERMISSION_FIELDS}
},
]
class Command(BaseCommand):
help = "Seed the 7 default configurable system roles with their exact permission matrix"
@transaction.atomic
def handle(self, *args, **options):
self.stdout.write("Seeding default system roles...")
created_count = 0
updated_count = 0
for role_data in DEFAULT_ROLES:
perms = role_data.pop("permissions")
role, created = Role.objects.get_or_create(
slug=role_data["slug"],
defaults={
"name": role_data["name"],
"description": role_data["description"],
"is_default": role_data["is_default"],
"is_system": role_data["is_system"],
**perms
}
)
if not created:
# Update role configuration if existing
role.name = role_data["name"]
role.description = role_data["description"]
role.is_default = role_data["is_default"]
role.is_system = role_data["is_system"]
for perm, val in perms.items():
setattr(role, perm, val)
role.save()
updated_count += 1
self.stdout.write(self.style.SUCCESS(f" [✓] Updated role: {role.name} ({role.slug})"))
else:
created_count += 1
self.stdout.write(self.style.SUCCESS(f" [+] Created role: {role.name} ({role.slug})"))
self.stdout.write(self.style.SUCCESS(
f"Successfully finished seeding roles: {created_count} created, {updated_count} updated."
))

88
apps/account/migrations/0005_role_user_custom_permissions_user_role.py

@ -0,0 +1,88 @@
# Generated by Django 4.2.30 on 2026-10-06 12:37
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('account', '0004_user_address_user_institution_name_user_rank_and_more'),
]
operations = [
migrations.CreateModel(
name='Role',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True, verbose_name='Role Name')),
('slug', models.SlugField(blank=True, max_length=100, unique=True, verbose_name='Slug / Identifier')),
('description', models.TextField(blank=True, verbose_name='Description')),
('is_default', models.BooleanField(default=False, help_text='Automatically assigned to new registrations if no other role is selected.', verbose_name='Default Role for New Users')),
('is_system', models.BooleanField(default=False, help_text='Marks default system roles. Admins can configure permissions, but deletion is restricted.', verbose_name='System Core Role')),
('is_searchable', models.BooleanField(default=True, verbose_name='Appear in User Searches / Directory')),
('can_submit_verification', models.BooleanField(default=True, verbose_name='Submit Verification Documents')),
('can_view_full_profiles', models.BooleanField(default=True, verbose_name='View Full Profiles')),
('can_manage_institutions', models.BooleanField(default=False, verbose_name='Manage Institutions')),
('can_send_direct_messages', models.BooleanField(default=True, verbose_name='Send Direct Messages')),
('can_create_group_chats', models.BooleanField(default=True, verbose_name='Create Group Chats')),
('can_start_video_calls', models.BooleanField(default=False, verbose_name='Start Video Calls')),
('can_moderate_chat', models.BooleanField(default=False, verbose_name='Moderate Chat Rooms')),
('is_chat_muted', models.BooleanField(default=False, verbose_name='Chat Muted (Kill-switch)')),
('can_create_posts', models.BooleanField(default=True, verbose_name='Create Posts')),
('can_publish_posts_directly', models.BooleanField(default=False, verbose_name='Publish Posts Directly (Bypass Moderation)')),
('can_edit_own_posts', models.BooleanField(default=True, verbose_name='Edit Own Posts')),
('can_delete_own_posts', models.BooleanField(default=True, verbose_name='Delete Own Posts')),
('can_comment', models.BooleanField(default=True, verbose_name='Comment on Posts')),
('can_enroll_courses', models.BooleanField(default=True, verbose_name='Enroll in Courses')),
('can_create_courses', models.BooleanField(default=False, verbose_name='Create Courses')),
('can_publish_courses', models.BooleanField(default=False, verbose_name='Publish Courses Directly')),
('can_issue_certificates', models.BooleanField(default=False, verbose_name='Issue Certificates')),
('can_grade_submissions', models.BooleanField(default=False, verbose_name='Grade Submissions')),
('has_premium_lms_access', models.BooleanField(default=False, verbose_name='Premium LMS Access')),
('can_request_meetings', models.BooleanField(default=True, verbose_name='Send Meeting Requests')),
('can_accept_meetings', models.BooleanField(default=False, verbose_name='Accept / Host Meetings')),
('can_register_events', models.BooleanField(default=True, verbose_name='Register for Events')),
('can_create_events', models.BooleanField(default=False, verbose_name='Create Events')),
('can_publish_events', models.BooleanField(default=False, verbose_name='Publish Events Directly')),
('can_checkin_attendees', models.BooleanField(default=False, verbose_name='Check-in Attendees (Scan Tickets)')),
('can_export_attendee_list', models.BooleanField(default=False, verbose_name='Export Attendee List')),
('can_volunteer', models.BooleanField(default=True, verbose_name='Volunteer for Projects')),
('can_create_projects', models.BooleanField(default=False, verbose_name='Create Projects')),
('can_manage_project_tasks', models.BooleanField(default=False, verbose_name='Manage Project Tasks')),
('can_approve_projects', models.BooleanField(default=False, verbose_name='Approve Projects')),
('can_make_donations', models.BooleanField(default=True, verbose_name='Make Donations')),
('can_create_donation_campaigns', models.BooleanField(default=False, verbose_name='Create Donation Campaigns')),
('can_view_donation_reports', models.BooleanField(default=False, verbose_name='View Donation Reports')),
('can_create_tickets', models.BooleanField(default=True, verbose_name='Create Support Tickets')),
('can_reply_tickets', models.BooleanField(default=True, verbose_name='Reply to Support Tickets')),
('can_manage_tickets', models.BooleanField(default=False, verbose_name='Manage Support Tickets')),
('can_submit_forms', models.BooleanField(default=True, verbose_name='Submit Dynamic Forms')),
('can_create_forms', models.BooleanField(default=False, verbose_name='Create Dynamic Forms')),
('can_export_form_data', models.BooleanField(default=False, verbose_name='Export Form Responses')),
('can_view_diplomatic_reports', models.BooleanField(default=False, verbose_name='View Diplomatic Reports')),
('can_create_diplomatic_reports', models.BooleanField(default=False, verbose_name='Create Diplomatic Reports')),
('can_access_admin_panel', models.BooleanField(default=False, verbose_name='Access Admin Panel')),
('can_manage_users', models.BooleanField(default=False, verbose_name='Manage Users')),
('can_ban_users', models.BooleanField(default=False, verbose_name='Ban / Suspend Users')),
('can_view_analytics', models.BooleanField(default=False, verbose_name='View Analytics Dashboard')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
],
options={
'verbose_name': 'Role',
'verbose_name_plural': 'Roles',
'ordering': ('name',),
},
),
migrations.AddField(
model_name='user',
name='custom_permissions',
field=models.JSONField(blank=True, default=dict, help_text='Dictionary of permission overrides: {permission_name: True/False}', verbose_name='Custom Permission Overrides'),
),
migrations.AddField(
model_name='user',
name='role',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='users', to='account.role', verbose_name='Assigned Role'),
),
]

2
apps/account/models/__init__.py

@ -2,3 +2,5 @@ from .user import *
from .groups import *
from .notification import *
from .verification import *
from .role import *

188
apps/account/models/role.py

@ -0,0 +1,188 @@
from django.db import models
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
PERMISSION_CATEGORIES = {
_("Identity & Profiles"): [
"is_searchable",
"can_submit_verification",
"can_view_full_profiles",
"can_manage_institutions",
],
_("Chat & Communications"): [
"can_send_direct_messages",
"can_create_group_chats",
"can_start_video_calls",
"can_moderate_chat",
"is_chat_muted",
],
_("CMS & Articles"): [
"can_create_posts",
"can_publish_posts_directly",
"can_edit_own_posts",
"can_delete_own_posts",
"can_comment",
],
_("LMS & Academics"): [
"can_enroll_courses",
"can_create_courses",
"can_publish_courses",
"can_issue_certificates",
"can_grade_submissions",
"has_premium_lms_access",
],
_("Meetings"): [
"can_request_meetings",
"can_accept_meetings",
],
_("Events"): [
"can_register_events",
"can_create_events",
"can_publish_events",
"can_checkin_attendees",
"can_export_attendee_list",
],
_("Community Projects"): [
"can_volunteer",
"can_create_projects",
"can_manage_project_tasks",
"can_approve_projects",
],
_("Donations & Charity"): [
"can_make_donations",
"can_create_donation_campaigns",
"can_view_donation_reports",
],
_("Support & Tickets"): [
"can_create_tickets",
"can_reply_tickets",
"can_manage_tickets",
],
_("Dynamic Forms"): [
"can_submit_forms",
"can_create_forms",
"can_export_form_data",
],
_("Diplomacy & Institutional"): [
"can_view_diplomatic_reports",
"can_create_diplomatic_reports",
],
_("Administration & Security"): [
"can_access_admin_panel",
"can_manage_users",
"can_ban_users",
"can_view_analytics",
],
}
ALL_PERMISSION_FIELDS = [perm for perms in PERMISSION_CATEGORIES.values() for perm in perms]
class Role(models.Model):
name = models.CharField(max_length=100, unique=True, verbose_name=_("Role Name"))
slug = models.SlugField(max_length=100, unique=True, blank=True, verbose_name=_("Slug / Identifier"))
description = models.TextField(blank=True, verbose_name=_("Description"))
is_default = models.BooleanField(
default=False,
verbose_name=_("Default Role for New Users"),
help_text=_("Automatically assigned to new registrations if no other role is selected.")
)
is_system = models.BooleanField(
default=False,
verbose_name=_("System Core Role"),
help_text=_("Marks default system roles. Admins can configure permissions, but deletion is restricted.")
)
# 1. Identity & Profiles
is_searchable = models.BooleanField(default=True, verbose_name=_("Appear in User Searches / Directory"))
can_submit_verification = models.BooleanField(default=True, verbose_name=_("Submit Verification Documents"))
can_view_full_profiles = models.BooleanField(default=True, verbose_name=_("View Full Profiles"))
can_manage_institutions = models.BooleanField(default=False, verbose_name=_("Manage Institutions"))
# 2. Chat & Communications
can_send_direct_messages = models.BooleanField(default=True, verbose_name=_("Send Direct Messages"))
can_create_group_chats = models.BooleanField(default=True, verbose_name=_("Create Group Chats"))
can_start_video_calls = models.BooleanField(default=False, verbose_name=_("Start Video Calls"))
can_moderate_chat = models.BooleanField(default=False, verbose_name=_("Moderate Chat Rooms"))
is_chat_muted = models.BooleanField(default=False, verbose_name=_("Chat Muted (Kill-switch)"))
# 3. CMS & Articles
can_create_posts = models.BooleanField(default=True, verbose_name=_("Create Posts"))
can_publish_posts_directly = models.BooleanField(default=False, verbose_name=_("Publish Posts Directly (Bypass Moderation)"))
can_edit_own_posts = models.BooleanField(default=True, verbose_name=_("Edit Own Posts"))
can_delete_own_posts = models.BooleanField(default=True, verbose_name=_("Delete Own Posts"))
can_comment = models.BooleanField(default=True, verbose_name=_("Comment on Posts"))
# 4. LMS & Academics
can_enroll_courses = models.BooleanField(default=True, verbose_name=_("Enroll in Courses"))
can_create_courses = models.BooleanField(default=False, verbose_name=_("Create Courses"))
can_publish_courses = models.BooleanField(default=False, verbose_name=_("Publish Courses Directly"))
can_issue_certificates = models.BooleanField(default=False, verbose_name=_("Issue Certificates"))
can_grade_submissions = models.BooleanField(default=False, verbose_name=_("Grade Submissions"))
has_premium_lms_access = models.BooleanField(default=False, verbose_name=_("Premium LMS Access"))
# 5. Meetings
can_request_meetings = models.BooleanField(default=True, verbose_name=_("Send Meeting Requests"))
can_accept_meetings = models.BooleanField(default=False, verbose_name=_("Accept / Host Meetings"))
# 6. Events
can_register_events = models.BooleanField(default=True, verbose_name=_("Register for Events"))
can_create_events = models.BooleanField(default=False, verbose_name=_("Create Events"))
can_publish_events = models.BooleanField(default=False, verbose_name=_("Publish Events Directly"))
can_checkin_attendees = models.BooleanField(default=False, verbose_name=_("Check-in Attendees (Scan Tickets)"))
can_export_attendee_list = models.BooleanField(default=False, verbose_name=_("Export Attendee List"))
# 7. Community Projects
can_volunteer = models.BooleanField(default=True, verbose_name=_("Volunteer for Projects"))
can_create_projects = models.BooleanField(default=False, verbose_name=_("Create Projects"))
can_manage_project_tasks = models.BooleanField(default=False, verbose_name=_("Manage Project Tasks"))
can_approve_projects = models.BooleanField(default=False, verbose_name=_("Approve Projects"))
# 8. Donations & Charity
can_make_donations = models.BooleanField(default=True, verbose_name=_("Make Donations"))
can_create_donation_campaigns = models.BooleanField(default=False, verbose_name=_("Create Donation Campaigns"))
can_view_donation_reports = models.BooleanField(default=False, verbose_name=_("View Donation Reports"))
# 9. Support & Tickets
can_create_tickets = models.BooleanField(default=True, verbose_name=_("Create Support Tickets"))
can_reply_tickets = models.BooleanField(default=True, verbose_name=_("Reply to Support Tickets"))
can_manage_tickets = models.BooleanField(default=False, verbose_name=_("Manage Support Tickets"))
# 10. Dynamic Forms
can_submit_forms = models.BooleanField(default=True, verbose_name=_("Submit Dynamic Forms"))
can_create_forms = models.BooleanField(default=False, verbose_name=_("Create Dynamic Forms"))
can_export_form_data = models.BooleanField(default=False, verbose_name=_("Export Form Responses"))
# 11. Diplomacy & Institutional
can_view_diplomatic_reports = models.BooleanField(default=False, verbose_name=_("View Diplomatic Reports"))
can_create_diplomatic_reports = models.BooleanField(default=False, verbose_name=_("Create Diplomatic Reports"))
# 12. Administration & Security
can_access_admin_panel = models.BooleanField(default=False, verbose_name=_("Access Admin Panel"))
can_manage_users = models.BooleanField(default=False, verbose_name=_("Manage Users"))
can_ban_users = models.BooleanField(default=False, verbose_name=_("Ban / Suspend Users"))
can_view_analytics = models.BooleanField(default=False, verbose_name=_("View Analytics Dashboard"))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
class Meta:
ordering = ('name',)
verbose_name = _("Role")
verbose_name_plural = _("Roles")
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
if self.is_default:
# Ensure only one default role exists
Role.objects.filter(is_default=True).exclude(pk=self.pk).update(is_default=False)
super().save(*args, **kwargs)
def get_permissions_dict(self):
"""Returns a dict of all permission flags and their boolean values."""
return {perm: getattr(self, perm, False) for perm in ALL_PERMISSION_FIELDS}

55
apps/account/models/user.py

@ -146,6 +146,21 @@ class User(AbstractUser):
help_text=_("Encrypted copy of password for displaying in the admin panel if required.")
)
role = models.ForeignKey(
'account.Role',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='users',
verbose_name=_("Assigned Role")
)
custom_permissions = models.JSONField(
default=dict,
blank=True,
verbose_name=_("Custom Permission Overrides"),
help_text=_("Dictionary of permission overrides: {permission_name: True/False}")
)
objects = UserManager()
EMAIL_FIELD = "email"
@ -186,6 +201,15 @@ class User(AbstractUser):
self.first_name = parts[0]
self.last_name = ""
if not self.role_id:
try:
from apps.account.models.role import Role
default_role = Role.objects.filter(is_default=True).first()
if default_role:
self.role = default_role
except Exception:
pass
return super().save(*args, **kwargs)
def get_full_name(self):
@ -202,27 +226,47 @@ class User(AbstractUser):
def has_role(self, role_name):
return self.groups.filter(name__iexact=f"{role_name} Group").exists() or self.user_type == role_name
def has_perm_flag(self, perm_name: str) -> bool:
"""
Check effective boolean permission flag:
1. Superuser always returns True.
2. Custom permission override on the user takes precedence if set.
3. Falls back to assigned Role value if user has a role.
4. Otherwise returns False.
"""
if not self.is_active:
return False
if self.is_superuser:
return True
if self.custom_permissions and perm_name in self.custom_permissions:
val = self.custom_permissions[perm_name]
if val is not None:
return bool(val)
if self.role and hasattr(self.role, perm_name):
return getattr(self.role, perm_name)
return False
@property
def is_super_admin(self):
return self.is_superuser or self.user_type == self.UserType.SUPER_ADMIN
return self.is_superuser or self.user_type == self.UserType.SUPER_ADMIN or (self.role and self.role.slug == 'super_admin')
@property
def is_regional_admin(self):
return self.is_super_admin or self.user_type == self.UserType.REGIONAL_ADMIN
return self.is_super_admin or self.user_type == self.UserType.REGIONAL_ADMIN or (self.role and self.role.slug == 'regional_admin')
@property
def is_institution_admin(self):
return self.is_regional_admin or self.user_type == self.UserType.INSTITUTION_ADMIN
return self.is_regional_admin or self.user_type == self.UserType.INSTITUTION_ADMIN or (self.role and self.role.slug in ['institution_admin', 'institution_owner'])
@property
def is_editor(self):
return self.is_institution_admin or self.user_type == self.UserType.EDITOR
return self.is_institution_admin or self.user_type == self.UserType.EDITOR or (self.role and self.role.slug == 'content_moderator')
def is_super_admin_panel_user(self):
return self.is_superuser or self.user_type == self.UserType.SUPER_ADMIN
def is_admin_panel_user(self):
return self.is_staff or self.is_superuser or self.user_type in [
return self.is_staff or self.is_superuser or self.has_perm_flag('can_access_admin_panel') or self.user_type in [
self.UserType.SUPER_ADMIN,
self.UserType.ADMIN,
self.UserType.REGIONAL_ADMIN,
@ -236,6 +280,7 @@ class User(AbstractUser):
return (
self.is_staff or
self.is_superuser or
self.has_perm_flag('can_access_admin_panel') or
self.user_type in [
self.UserType.SUPER_ADMIN,
self.UserType.ADMIN,

15
config/settings/base.py

@ -351,13 +351,18 @@ UNFOLD = {
"TABS": [
{
"page": "accounts",
"models": ["account.user", "auth.group"],
"models": ["account.user", "account.role", "auth.group"],
"items": [
{
"title": _("Users"),
"icon": "people",
"link": lambda request: admin_url_generator(request, "account_user_changelist"),
},
{
"title": _("Roles & Permissions"),
"icon": "admin_panel_settings",
"link": lambda request: admin_url_generator(request, "account_role_changelist"),
},
{
"title": _("Groups"),
"icon": "shield",
@ -391,8 +396,14 @@ UNFOLD = {
"link": lambda request: admin_url_generator(request, "account_user_changelist"),
},
{
"title": _("Groups & Roles"),
"title": _("Roles & Permissions"),
"icon": "admin_panel_settings",
"link": lambda request: admin_url_generator(request, "account_role_changelist"),
"permission": lambda request: request.user.can_access_admin_panel(),
},
{
"title": _("Django Groups"),
"icon": "shield",
"link": lambda request: admin_url_generator(request, "auth_group_changelist"),
"permission": lambda request: request.user.is_staff,
},

Loading…
Cancel
Save