from django.contrib.auth.models import AbstractUser, UserManager as DjangoUserManager from django.utils import timezone from django.db import models import uuid from datetime import timedelta from core.media import ( delete_image_derivatives_by_name, get_image_previous_name, safe_process_public_image, ) from core.models import BaseModel from apps.users.email_identity import normalize_email_identity, normalize_mobile_number class UserManager(DjangoUserManager): def _normalize_required_mobile(self, mobile): normalized = normalize_mobile_number(mobile) if not normalized: raise ValueError("The mobile number must be set") return normalized def create_user(self, username, email=None, password=None, **extra_fields): extra_fields["mobile"] = self._normalize_required_mobile(extra_fields.get("mobile")) return super().create_user(username, email=email, password=password, **extra_fields) def create_superuser(self, username, email=None, password=None, **extra_fields): extra_fields["mobile"] = self._normalize_required_mobile(extra_fields.get("mobile")) extra_fields.setdefault("is_active", True) extra_fields.setdefault("is_mobile_verified", True) return super().create_superuser(username, email=email, password=password, **extra_fields) class University(BaseModel): code = models.CharField(max_length=64, unique=True) name = models.CharField(max_length=255) is_active = models.BooleanField(default=True) class Meta: ordering = ["name"] def __str__(self): return self.name class Major(BaseModel): code = models.CharField(max_length=64, unique=True) name = models.CharField(max_length=255) is_active = models.BooleanField(default=True) class Meta: ordering = ["name"] def __str__(self): return self.name class User(AbstractUser, BaseModel): email = models.EmailField(unique=True, null=True, blank=True, default=None) mobile = models.CharField(max_length=11, unique=True, null=True, blank=True, default=None) bio = models.TextField(null=True, blank=True) profile_picture = models.ImageField(upload_to='profile_pictures/', null=True, blank=True) student_id = models.CharField(max_length=20, null=True) year_of_study = models.IntegerField(null=True, blank=True) major = models.ForeignKey( Major, null=True, blank=True, on_delete=models.SET_NULL, related_name='users', ) university = models.ForeignKey( University, null=True, blank=True, on_delete=models.SET_NULL, related_name='users', ) is_email_verified = models.BooleanField(default=False) is_mobile_verified = models.BooleanField(default=False) email_verification_token = models.UUIDField(default=uuid.uuid4, unique=True) email_verification_sent_at = models.DateTimeField(null=True, blank=True) password_reset_token = models.UUIDField(null=True, blank=True, unique=True) password_reset_token_expires_at = models.DateTimeField(null=True, blank=True) USERNAME_FIELD = 'username' REQUIRED_FIELDS = ['mobile'] objects = UserManager() class Meta: db_table = 'users' verbose_name = 'User' verbose_name_plural = 'Users' def __str__(self): identity = self.mobile or self.email or self.username return f"{self.get_full_name() or self.username} ({identity})" def get_full_name(self): return f"{self.first_name} {self.last_name}".strip() def get_major_display(self): if self.major: return self.major.name return None def get_university_display(self): if self.university: return self.university.name return None @property def requires_mobile_verification(self): return not self.is_mobile_verified @property def has_google_link(self): return self.social_accounts.filter( provider=UserSocialAccount.ProviderType.GOOGLE, is_active=True, ).exists() def regenerate_verification_token(self): self.email_verification_token = uuid.uuid4() self.save(update_fields=['email_verification_token']) def set_password_reset_token(self): """Generates a new password reset token and sets its expiry.""" self.password_reset_token = uuid.uuid4() self.password_reset_token_expires_at = timezone.now() + timedelta(hours=1) self.save(update_fields=['password_reset_token', 'password_reset_token_expires_at']) def save(self, *args, **kwargs): previous_image_name = get_image_previous_name(self, "profile_picture") current_image_name = self.profile_picture.name if self.profile_picture else None self.email = normalize_email_identity(self.email) self.mobile = normalize_mobile_number(self.mobile) super().save(*args, **kwargs) if previous_image_name != current_image_name and previous_image_name: delete_image_derivatives_by_name( self.profile_picture.storage if self.profile_picture else None, previous_image_name, "profile_picture", delete_original=True, ) if previous_image_name != current_image_name and self.profile_picture: safe_process_public_image(self.profile_picture, "profile_picture") class UserSocialAccount(BaseModel): class ProviderType(models.TextChoices): GOOGLE = "google", "google" user = models.ForeignKey( User, on_delete=models.CASCADE, related_name="social_accounts", ) provider = models.CharField(max_length=32, choices=ProviderType.choices) provider_user_id = models.CharField(max_length=255) email = models.EmailField(blank=True, null=True, default=None) email_verified = models.BooleanField(default=False) avatar_url = models.URLField(blank=True, default="") is_active = models.BooleanField(default=True) class Meta: verbose_name = "User Social Account" verbose_name_plural = "User Social Accounts" db_table = "user_social_accounts" ordering = ["-updated_at", "-created_at"] constraints = [ models.UniqueConstraint( fields=("provider", "provider_user_id"), name="user_social_account_provider_uid_uniq", ) ] indexes = [ models.Index(fields=["provider", "provider_user_id"], name="user_social_provider_uid_idx"), models.Index(fields=["provider", "email"], name="user_social_provider_email_idx"), ] def __str__(self): return f"{self.provider}:{self.provider_user_id}" def save(self, *args, **kwargs): self.email = normalize_email_identity(self.email) super().save(*args, **kwargs)