Browse Source

feat: implement custom user admin interface and update field constraints for bookmark, video, and user models

master
Mohsen Taba 1 month ago
parent
commit
ab76d2d177
  1. 42
      apps/account/admin/user.py
  2. 23
      apps/account/migrations/0002_alter_user_email_alter_user_username.py
  3. 23
      apps/account/models/user.py
  4. 18
      apps/bookmark/migrations/0002_alter_rate_service.py
  5. 19
      apps/video/migrations/0002_alter_video_video_file.py

42
apps/account/admin/user.py

@ -2,7 +2,6 @@ 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.forms import UserChangeForm, UserCreationForm
from django.contrib.auth.models import Group
from django.db import models
from django.utils.html import format_html
@ -13,7 +12,7 @@ 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
from unfold.forms import AdminPasswordChangeForm, UserChangeForm, UserCreationForm
from unfold.sections import TableSection
from unfold.contrib.filters.admin import RangeDateTimeFilter
@ -29,9 +28,38 @@ from apps.account.admin.location import LocationHistoryInline
# 1. Base User Admin (Logic Shared by all User types)
# =========================================================
class UserAdmin(BaseUserAdmin, ModelAdmin):
form = UserChangeForm
add_form = UserCreationForm
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"
@ -205,6 +233,8 @@ class StudentParticipantInline(StackedInline):
class StudentUserAdmin(UserAdmin):
form = UserAdminChangeForm
add_form = UserAdminCreationForm
list_display = ('display_header', 'email', 'gender', 'display_age', 'courses_count')
add_fieldsets = (
@ -302,6 +332,8 @@ class CourseTableSection(TableSection):
class ProfessorUserAdmin(UserAdmin):
form = UserAdminChangeForm
add_form = UserAdminCreationForm
list_display = ('display_header', 'email', 'courses_count')
list_sections = [CourseTableSection]
save_as = True

23
apps/account/migrations/0002_alter_user_email_alter_user_username.py

@ -0,0 +1,23 @@
# Generated by Django 5.2.12 on 2026-04-11 11:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='user',
name='email',
field=models.EmailField(blank=True, error_messages={'unique': 'A user with that email already exists.'}, help_text="Enter the user's email address.", max_length=254, null=True, unique=True, verbose_name='Email Address'),
),
migrations.AlterField(
model_name='user',
name='username',
field=models.CharField(blank=True, error_messages={'unique': 'A user with that username already exists.'}, max_length=150, null=True, unique=True, verbose_name='Username'),
),
]

23
apps/account/models/user.py

@ -31,9 +31,16 @@ class User(AbstractUser):
last_name = None
first_name = None
username = models.CharField(unique=True, null=True, blank=True, max_length=150)
email = models.EmailField(unique=True, verbose_name="Email Address", help_text="Enter the user's email address.", null=True, blank=True)
fullname = models.CharField(max_length=255, verbose_name="Full Name", help_text="Enter the full name of the user.", null=True, blank=True)
username = models.CharField(unique=True, null=True, blank=True, max_length=150,
verbose_name=_("Username"),
error_messages={'unique': _("A user with that username already exists.")})
email = models.EmailField(unique=True, verbose_name=_("Email Address"),
help_text=_("Enter the user's email address."),
null=True, blank=True,
error_messages={'unique': _("A user with that email already exists.")})
fullname = models.CharField(max_length=255, verbose_name=_("Full Name"),
help_text=_("Enter the full name of the user."),
null=True, blank=True)
birthdate = models.DateField(verbose_name=_('birthdate'), null=True, blank=True)
avatar = models.ImageField(max_length=512, null=True, blank=True, upload_to='users/avatars/%Y/%m/')
@ -101,6 +108,16 @@ class User(AbstractUser):
def get_full_name(self):
return self.fullname
def clean(self):
super().clean()
# Enforce email for non-guest users
# Users without email are considered "guests" (device_id login)
if not self.email and not self.device_id and not self.is_superuser:
from django.core.exceptions import ValidationError
raise ValidationError({
'email': _("Email is required for all regular users.")
})
@property
def is_guest(self):
return self.email is None

18
apps/bookmark/migrations/0002_alter_rate_service.py

@ -0,0 +1,18 @@
# Generated by Django 5.2.12 on 2026-04-11 11:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bookmark', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='rate',
name='service',
field=models.CharField(choices=[('library', 'Library'), ('podcast', 'Podcast'), ('podcast_playlist', 'Podcast Playlist'), ('hadith', 'Hadith'), ('hadith_correction', 'Hadith Correction'), ('video', 'Video'), ('video_playlist', 'Video Playlist'), ('article', 'Article')], max_length=20, verbose_name='Service'),
),
]

19
apps/video/migrations/0002_alter_video_video_file.py

@ -0,0 +1,19 @@
# Generated by Django 5.2.12 on 2026-04-11 11:10
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('video', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='video',
name='video_file',
field=models.FileField(blank=True, null=True, upload_to='video/videos/', validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['mp4', 'mov', 'avi', 'mkv', 'webm'])]),
),
]
Loading…
Cancel
Save