You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

504 lines
18 KiB

from django import forms
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.admin import GroupAdmin as BaseGroupAdmin
from django.contrib.auth.models import Group
from django.db import models
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from django.templatetags.static import static
from rest_framework.authtoken.models import TokenProxy
from unfold.admin import ModelAdmin, StackedInline
from unfold.contrib.forms.widgets import WysiwygWidget
from unfold.decorators import display
from unfold.forms import AdminPasswordChangeForm, UserChangeForm, UserCreationForm
from unfold.sections import TableSection
from unfold.contrib.filters.admin import RangeDateTimeFilter
# Import Models
from apps.account.models import User, ClientUser, StudentUser, ProfessorUser, LocationHistory
from apps.course.models import Participant
# Import Admin Sites from utils
from utils.admin import project_admin_site, dovoodi_admin_site , is_dovoodi_panel
from apps.account.admin.location import LocationHistoryInline
from unfold.widgets import UnfoldAdminSelectWidget
# =========================================================
# 1. Base User Admin (Logic Shared by all User types)
# =========================================================
class UserAdminCreationForm(UserCreationForm):
class Meta(UserCreationForm.Meta):
model = User
fields = ("fullname", "email")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'fullname' in self.fields:
self.fields['fullname'].required = True
if 'email' in self.fields:
self.fields['email'].required = True
def clean_email(self):
email = self.cleaned_data.get('email')
if User.objects.filter(email=email).exists():
raise forms.ValidationError(_("A user with this email already exists."))
return email
class UserAdminChangeForm(UserChangeForm):
class Meta(UserChangeForm.Meta):
model = User
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'fullname' in self.fields:
self.fields['fullname'].required = True
if 'email' in self.fields:
self.fields['email'].required = True
class UserAdmin(ModelAdmin, BaseUserAdmin):
form = UserAdminChangeForm
add_form = UserAdminCreationForm
change_password_form = AdminPasswordChangeForm
compressed_fields = False
list_before_template = "account/user_list_section.html"
list_display = ('fullname', 'email', 'is_active', 'display_date_joined',)
ordering = ("-id",)
search_fields = ('email', 'fullname', 'username',)
list_filter = [
"is_active",
"is_staff",
("last_login", RangeDateTimeFilter),
("date_joined", RangeDateTimeFilter),
]
inlines = [LocationHistoryInline]
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': (('fullname', 'email'), 'phone_number', 'birthdate', 'gender', 'avatar', 'skill', 'info'),
}),
(_('Location'), {
'fields': ('city', 'country'),
'classes': ('collapse',),
}),
(_('Password'), {
'fields': ('password1', 'password2'),
'classes': ('collapse',),
}),
(_('Permissions'), {
'fields': ('is_active', 'is_staff', 'is_superuser', 'groups'),
'classes': ('collapse',),
}),
)
fieldsets = (
(None, {"fields": ("email", "fullname")}),
(_("Basic Information"), {
"fields": ("gender", "avatar", "phone_number", "birthdate", 'info', 'skill', "password"),
"classes": ["tab"],
}),
(_('Country & City'), {
'fields': ('city', 'country'),
"classes": ["tab"],
}),
(_('Device Information'), {
'fields': ('device_id', 'device_os', 'fcm', 'language',),
"classes": ["tab"],
}),
(_('Authentication'), {
'fields': ('display_auth_token',),
"classes": ["tab"],
}),
(_('Permissions'), {
'fields': ('user_type', 'is_active', 'is_staff', 'groups'),
"classes": ["tab"],
}),
(_('Important dates'), {
'fields': ('last_login', 'date_joined', 'deleted_at'),
"classes": ["tab"],
}),
)
formfield_overrides = {
models.TextField: {"widget": WysiwygWidget}
}
radio_fields = {"gender": admin.HORIZONTAL}
readonly_fields = ["last_login", "date_joined", "display_auth_token"]
def get_fieldsets(self, request, obj=None):
# UserAdmin.get_fieldsets returns add_fieldsets when obj is None
fieldsets = super().get_fieldsets(request, obj)
if is_dovoodi_panel(request):
new_fieldsets = []
for name, options in fieldsets:
# Hide entire Permissions section
if name == _('Permissions'):
continue
new_options = options.copy()
# Hide skill field inside "Basic Information" (Edit) or the titleless section (Add)
if name == _("Basic Information") or name is None:
fields = list(new_options.get("fields", []))
new_fields = []
for f in fields:
if isinstance(f, (list, tuple)):
inner_f = [inner for inner in f if inner != 'skill']
if inner_f:
new_fields.append(tuple(inner_f))
elif f != 'skill':
new_fields.append(f)
new_options["fields"] = tuple(new_fields)
new_fieldsets.append((name, new_options))
return tuple(new_fieldsets)
return fieldsets
@display(description=_("Date Joined"))
def display_date_joined(self, instance: User):
return instance.date_joined.strftime("%Y-%m-%d %H:%M") if instance.date_joined else "-"
@display(description=_("Last Login"))
def display_last_login(self, instance: User):
return instance.last_login.strftime("%Y-%m-%d %H:%M") if instance.last_login else "-"
@display(description=_("Authentication Token"))
def display_auth_token(self, instance: User):
from rest_framework.authtoken.models import Token
try:
token, created = Token.objects.get_or_create(user=instance)
return format_html('<code style="word-break: break-all;">{}</code>', token.key)
except Exception as e:
return format_html('<span class="error">{}</span>', str(e))
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.filter(email__isnull=False)
# =========================================================
# 2. Specific User Type Admins
# =========================================================
class GuestUserAdmin(UserAdmin):
list_display = ('device_id', 'device_os', 'is_active', 'display_date_joined',)
def has_add_permission(self, request):
if '_popup' in request.GET and request.GET['_popup'] == '1':
return True
return False
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.filter(email__isnull=True)
@display(description=_("Date Joined"))
def display_date_joined(self, instance: User):
return instance.date_joined.strftime("%Y-%m-%d %H:%M") if instance.date_joined else "-"
class StudentParticipantInline(StackedInline):
"""Inline to show courses a student has joined"""
model = Participant
extra = 0
readonly_fields = ('course', 'joined_date', 'course_status', 'course_professor')
fields = ('course', 'course_status', 'course_professor', 'joined_date', 'is_active')
verbose_name = _('Course Participation')
verbose_name_plural = _('Course Participations')
autocomplete_fields = ['course']
tab = True
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related('course', 'course__professor')
@admin.display(description=_('Course Status'))
def course_status(self, obj):
if obj.course:
return obj.course.get_status_display()
return '-'
@admin.display(description=_('Professor'))
def course_professor(self, obj):
if obj.course and obj.course.professor:
return obj.course.professor.fullname or obj.course.professor.email
return '-'
def has_add_permission(self, request, obj=None):
return True
def has_change_permission(self, request, obj=None):
return True
def has_delete_permission(self, request, obj=None):
return True
class StudentUserAdmin(UserAdmin):
form = UserAdminChangeForm
add_form = UserAdminCreationForm
list_display = ('display_header', 'email', 'gender', 'display_age', 'courses_count')
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': (('fullname', 'email'), 'phone_number', 'avatar', 'birthdate', 'gender'),
}),
(_('Location'), {
'fields': (('city', 'country'),),
'classes': ('collapse',),
}),
(_('password'), {
'fields': ('password1', 'password2',),
'classes': ('collapse',),
}),
)
inlines = [StudentParticipantInline, LocationHistoryInline]
@display(description=_("Student"), header=True)
def display_header(self, instance: StudentUser):
avatar_path = instance.avatar.url if instance.avatar else static("images/reading(1).png")
return [
instance.fullname,
None,
None,
{
"path": avatar_path,
"height": 30,
"width": 36,
"borderless": True,
},
]
@display(description=_("Age"))
def display_age(self, instance: StudentUser):
from datetime import date
if not instance.birthdate:
return "-"
today = date.today()
birthdate = instance.birthdate
age = today.year - birthdate.year - ((today.month, today.day) < (birthdate.month, birthdate.day))
formatted_date = birthdate.strftime("%Y-%m-%d")
return format_html('<span title="{}">{}</span>', f"Born on {formatted_date}", age)
@display(description=_("Courses"), dropdown=True)
def courses_count(self, instance: StudentUser):
total = instance.participated_courses.count()
items = []
for participant in instance.participated_courses.all():
course = participant.course
title = format_html(
"""
<div class="flex flex-row gap-2 items-center">
<span class="truncate">{}</span>
<a href="/admin/course/course/{}/change/" class="leading-none ml-auto">
<span class="material-symbols-outlined leading-none text-base-500">visibility</span>
</a>
</div>
""",
course.title,
course.id
)
items.append({"title": title})
if total == 0:
return "-"
return {
"title": f"{total} {_('courses')}",
"items": items,
"striped": True,
}
def get_queryset(self, request):
return super().get_queryset(request).prefetch_related(
"participated_courses",
"participated_courses__course",
)
class CourseTableSection(TableSection):
verbose_name = _("Course Categories")
related_name = "courses"
height = 380
fields = ["title", "status", "edit_link"]
def edit_link(self, instance):
return format_html(
'<a href="/admin/course/course/{}/change/" class="leading-none">'
'<span class="material-symbols-outlined leading-none text-base-500">visibility</span>'
'</a>',
instance.id
)
edit_link.short_description = _("Edit")
class ProfessorUpgradeForm(forms.ModelForm):
existing_user = forms.ModelChoiceField(
queryset=User.objects.exclude(groups__name="Professor Group"),
required=True,
label=_("Select Existing User"),
help_text=_("Choose an existing user to upgrade to Professor."),
widget=UnfoldAdminSelectWidget,
)
class Meta:
model = ProfessorUser
fields = ("existing_user", "is_active", "is_staff", "is_superuser", "groups")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'groups' in self.fields:
self.fields['groups'].required = False
def _post_clean(self):
# جلوگیری از اعتبارسنجی مدل خالی برای جلوگیری از ارور فیلدهای اجباری
pass
def save(self, commit=True):
# کاربر موجود (که هنوز پروفسور نیست) را می‌گیریم
user = self.cleaned_data.get('existing_user')
# ابتدا user_type را تغییر می‌دهیم تا با Manager پروفسور سازگار شود
user.user_type = User.UserType.PROFESSOR
user.is_active = self.cleaned_data.get('is_active', user.is_active)
user.is_staff = self.cleaned_data.get('is_staff', user.is_staff)
user.is_superuser = self.cleaned_data.get('is_superuser', user.is_superuser)
user.save() # ذخیره با مدل User
# حالا که user_type آپدیت شد، می‌توانیم آن را به عنوان ProfessorUser واکشی کنیم
prof_user = ProfessorUser.objects.get(pk=user.pk)
# برای ذخیره‌سازی ManyToMany (مثل groups)، باید instance فرم ست شود
self.instance = prof_user
def save_m2m():
groups = self.cleaned_data.get('groups')
if groups is not None:
self.instance.groups.set(groups)
# اضافه‌کردن کاربر به گروه پروفسورها و ساخت اسلاگ (در صورت نیاز)
self.instance.ensure_professor_profile(commit=True)
self.save_m2m = save_m2m
if commit:
self.save_m2m()
return prof_user
class ProfessorUserAdmin(UserAdmin):
form = UserAdminChangeForm
add_form = ProfessorUpgradeForm # <--- آپدیت شد به فرم ارتقا
list_display = ('display_header', 'email', 'courses_count')
list_sections = [CourseTableSection]
save_as = True
# بازنویسی کامل فیلدست‌های صفحه Add (ساخت)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('existing_user',),
}),
(_('Permissions'), {
'fields': ('is_active', 'is_staff', 'is_superuser', 'groups'),
'classes': ('wide',),
}),
)
@display(description=_("Professor"), header=True)
def display_header(self, instance: ProfessorUser):
avatar_path = instance.avatar.url if instance.avatar else static("images/reading(1).png")
return [
instance.fullname,
None,
None,
{
"path": avatar_path,
"height": 30,
"width": 50,
"borderless": True,
"squared": True,
},
]
@display(description=_("Courses"), dropdown=True)
def courses_count(self, instance: ProfessorUser):
total = instance.courses.count()
items = []
for course in instance.courses.all():
title = format_html(
"""
<div class="flex flex-row gap-2 items-center">
<span class="truncate">{}</span>
<a href="/admin/course/course/{}/change/" class="leading-none ml-auto">
<span class="material-symbols-outlined leading-none text-base-500">visibility</span>
</a>
</div>
""",
course.title,
course.id
)
items.append({"title": title})
if total == 0:
return "-"
return {
"title": f"{total} {_('courses')}",
"items": items,
"striped": True,
}
def get_queryset(self, request):
return super().get_queryset(request).prefetch_related("courses")
class GroupAdmin(BaseGroupAdmin, ModelAdmin):
list_display = ('name', 'permissions_count')
search_fields = ('name',)
ordering = ('name',)
filter_horizontal = ('permissions',)
fieldsets = (
(None, {'fields': ('name',)}),
(_('Permissions'), {'fields': ('permissions',), 'classes': ['tab']}),
)
@display(description=_("Permissions"))
def permissions_count(self, obj):
count = obj.permissions.count()
return f"{count} {_('permissions')}" if count > 0 else "-"
# =========================================================
# 3. Registrations (SAFE METHOD)
# =========================================================
# A. DEFAULT DJANGO ADMIN (SAFE REGISTRATION)
# This is required because plugins like 'django-filer' expect User to be registered here.
try:
admin.site.unregister(User)
except admin.sites.NotRegistered:
pass
try:
admin.site.register(User, UserAdmin)
except admin.sites.AlreadyRegistered:
pass
# B. PROJECT ADMIN SITE (Imam Javad)
project_admin_site.register(User, UserAdmin)
project_admin_site.register(ClientUser, GuestUserAdmin)
project_admin_site.register(StudentUser, StudentUserAdmin)
project_admin_site.register(ProfessorUser, ProfessorUserAdmin)
project_admin_site.register(Group, GroupAdmin)
# C. DOVOODI ADMIN SITE
dovoodi_admin_site.register(User, UserAdmin)
dovoodi_admin_site.register(ClientUser, GuestUserAdmin)
dovoodi_admin_site.register(Group, GroupAdmin)
# D. Unregister TokenProxy safely (Cleaner UI)
try:
admin.site.unregister(TokenProxy)
except admin.sites.NotRegistered:
pass