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.
506 lines
18 KiB
506 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 _, ngettext
|
|
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>', _("Born on {date}").format(date=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": ngettext("{total} course", "{total} courses", total).format(total=total),
|
|
"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.filter(is_active=True, email__isnull=False).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": ngettext("{total} course", "{total} courses", total).format(total=total),
|
|
"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 ngettext("{count} permission", "{count} permissions", count).format(count=count) 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
|