diff --git a/apps/account/admin/user.py b/apps/account/admin/user.py index 955dc9f..3af88c2 100644 --- a/apps/account/admin/user.py +++ b/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 diff --git a/apps/account/migrations/0002_alter_user_email_alter_user_username.py b/apps/account/migrations/0002_alter_user_email_alter_user_username.py new file mode 100644 index 0000000..0ccb1a0 --- /dev/null +++ b/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'), + ), + ] diff --git a/apps/account/models/user.py b/apps/account/models/user.py index aaa5ec4..89e8f57 100644 --- a/apps/account/models/user.py +++ b/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 diff --git a/apps/bookmark/migrations/0002_alter_rate_service.py b/apps/bookmark/migrations/0002_alter_rate_service.py new file mode 100644 index 0000000..6bc1515 --- /dev/null +++ b/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'), + ), + ] diff --git a/apps/video/migrations/0002_alter_video_video_file.py b/apps/video/migrations/0002_alter_video_video_file.py new file mode 100644 index 0000000..5422bcc --- /dev/null +++ b/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'])]), + ), + ]