import random import secrets import base64 import hashlib from cryptography.fernet import Fernet from django.conf import settings from dj_language.field import LanguageField from django.contrib.auth.models import AbstractUser from django.db import models from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ from django.utils import timezone from phonenumber_field.modelfields import PhoneNumberField from utils.validators import validate_possible_number from apps.account.manager import UserManager class User(AbstractUser): class DeviceOs(models.TextChoices): android = 'android', _('android') apple = 'apple', _('apple') web = 'web', _('web') class UserType(models.TextChoices): PROFESSOR = 'professor', _('Professor') CLIENT = 'client', _('Client') STUDENT = 'student', _("Student") ADMIN = 'admin', _('Admin') SUPER_ADMIN = 'super_admin', _('Super Admin') CONSULTANT = 'consultant', _('Consultant') class GenderChoices(models.TextChoices): MALE = 'male', _('Male') FEMALE = 'female', _('Female') last_name = None first_name = None 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/', verbose_name=_('Avatar')) phone_number = PhoneNumberField( validators=[validate_possible_number], null=True, blank=True, verbose_name=_('Phone Number'), help_text=_("e.g., +49 151 12345678") ) language = LanguageField(null=True, verbose_name=_('Language')) gender = models.CharField(max_length=20, choices=GenderChoices.choices, null=True, blank=True, verbose_name=_('Gender'), help_text=_("Select the user's gender.")) user_type = models.CharField(max_length=20, choices=UserType.choices, default=UserType.CLIENT, verbose_name=_("User Type"), help_text=_("Type of the user.")) date_joined = models.DateTimeField(auto_now_add=True, verbose_name=_("Date Joined"), help_text=_("The date and time the user registered.")) city = models.CharField(verbose_name=_('City'), max_length=255, null=True, blank=True) country = models.CharField(max_length=255, verbose_name=_('country'), null=True, blank=True) device_id = models.CharField(verbose_name=_('device id'), max_length=255, null=True, blank=True) device_os = models.CharField(choices=DeviceOs.choices, null=True, max_length=16, verbose_name=_('device os')) user_agent = models.TextField(verbose_name=_('user agent'), null=True, blank=True) client_ip = models.TextField(verbose_name=_('client ip'), null=True, blank=True) fcm = models.CharField(max_length=512, null=True, blank=True, verbose_name=_('fcm')) slug = models.SlugField(max_length=255, unique=True, null=True, blank=True, verbose_name=_('slug')) experience_years = models.PositiveIntegerField(default=0, verbose_name=_('Experience years')) is_staff = models.BooleanField(default=False, verbose_name=_('is staff')) is_active = models.BooleanField(default=True, verbose_name=_("Active"), help_text=_("Designates whether this user should be treated as active. Unselect this instead of deleting accounts.")) deleted_at = models.DateTimeField(null=True, blank=True, verbose_name=_('deleted at')) info = models.TextField(verbose_name=_("Info"), null=True, blank=True) skill = models.CharField(max_length=512, null=True, blank=True, verbose_name=_('skill')) password_enc = models.CharField( max_length=512, blank=True, default='', verbose_name=_("Encrypted Password"), help_text=_( "Encrypted copy of password for displaying in the admin panel." ) ) objects = UserManager() EMAIL_FIELD = "email" USERNAME_FIELD = "email" REQUIRED_FIELDS = [] def __str__(self): username = self.email or self.fullname or self.device_id return f"{username}" def soft_delete(self): self.deleted_at = timezone.now() self.is_active = False self.fullname = f'{self.fullname}:deleted' number = str(random.randint(1000000000, 9999999999)) self.phone_number = f'{self.phone_number}:deleted{number}' self.email = f'{self.email}:deleted{number}' if self.email else None self.device_id = f'{self.device_id}:deleted{number}' if self.device_id else None self.save() def save(self, *args, **kwargs): self.username = self.email if User.objects.filter(username=self.email).exclude(pk=self.pk).exists(): self.username = f'{self.email}:{self.id}' if self.user_type == self.UserType.PROFESSOR: self._ensure_professor_slug() return super().save(*args, **kwargs) 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 @property def user_type_based_on_groups(self): if self.groups.filter(name="Student Group").exists(): return self.UserType.STUDENT elif self.groups.filter(name="Professor Group").exists(): return self.UserType.PROFESSOR elif self.groups.filter(name="Consultant Group").exists(): return self.UserType.CONSULTANT else: return self.UserType.CLIENT @property def primary_role(self): """نقش اصلی کاربر بر اساس اولویت""" if self.groups.filter(name="Professor Group").exists(): return self.UserType.PROFESSOR elif self.groups.filter(name="Consultant Group").exists(): return self.UserType.CONSULTANT elif self.groups.filter(name="Student Group").exists(): return self.UserType.STUDENT elif self.groups.filter(name="Admin Group").exists(): return self.UserType.ADMIN elif self.groups.filter(name="Super Admin Group").exists(): return self.UserType.SUPER_ADMIN else: return self.UserType.CLIENT def has_role(self, role_name): """چک کردن داشتن نقش خاص""" group_name = self._role_to_group_name(role_name) return self.groups.filter(name=group_name).exists() def add_role(self, role_name): """اضافه کردن نقش جدید بدون حذف نقش‌های قبلی""" from django.contrib.auth.models import Group group_name = self._role_to_group_name(role_name) group, created = Group.objects.get_or_create(name=group_name) self.groups.add(group) # بروزرسانی user_type اگر نقش جدید اولویت بالاتری دارد if role_name in ['professor', self.UserType.PROFESSOR] and self.user_type != self.UserType.PROFESSOR: self.user_type = self.UserType.PROFESSOR self.save() elif role_name in ['student', self.UserType.STUDENT] and self.user_type == self.UserType.CLIENT: self.user_type = self.UserType.STUDENT self.save() def remove_role(self, role_name): """حذف نقش خاص""" from django.contrib.auth.models import Group group_name = self._role_to_group_name(role_name) try: group = Group.objects.get(name=group_name) self.groups.remove(group) # بروزرسانی user_type بر اساس نقش‌های باقی‌مانده self.user_type = self.primary_role self.save() except Group.DoesNotExist: pass def get_all_roles(self): """دریافت لیست تمام نقش‌های کاربر""" return [group.name.replace(' Group', '').lower() for group in self.groups.all()] def _role_to_group_name(self, role_name): role_value = role_name.value if hasattr(role_name, 'value') else str(role_name) role_key = role_value.strip().lower() role_group_map = { 'student': 'Student Group', 'professor': 'Professor Group', 'consultant': 'Consultant Group', 'admin': 'Admin Group', 'super_admin': 'Super Admin Group', 'client': 'Client Group', } return role_group_map.get(role_key, f"{role_value.replace('_', ' ').title()} Group") def can_teach_course(self): """آیا می‌تواند دوره تدریس کند؟""" # اولویت اول: staff یا admin if self.is_staff or self.has_role('admin') or self.has_role('super_admin'): return True # اولویت دوم: professor return self.has_role('professor') def is_super_admin_panel_user(self): """آیا کاربر دسترسی کامل پنل را به عنوان سوپر ادمین دارد؟""" return bool( self.is_active and ( self.is_superuser or self.user_type == self.UserType.SUPER_ADMIN or self.has_role('super_admin') ) ) def is_admin_panel_user(self): """آیا کاربر دسترسی کامل پنل را به عنوان ادمین دارد؟""" return bool( self.is_active and ( self.user_type == self.UserType.ADMIN or self.has_role('admin') ) ) def is_professor_panel_user(self): """آیا کاربر از طریق نقش استاد اجازه ورود به پنل را دارد؟""" return bool( self.is_active and ( self.user_type == self.UserType.PROFESSOR or self.has_role('professor') ) ) def can_access_admin_panel(self): """سوپر ادمین‌ها، ادمین‌ها و استادها اجازه ورود به پنل را دارند.""" return ( self.is_super_admin_panel_user() or self.is_admin_panel_user() or self.is_professor_panel_user() ) def can_enroll_course(self): """آیا می‌تواند در دوره ثبت‌نام کند؟""" return True # همه می‌توانند دانش‌آموز باشند def can_manage_course(self, course=None): """آیا می‌تواند دوره خاصی را مدیریت کند؟""" # اولویت اول: staff یا admin - دسترسی کامل if self.is_staff or self.has_role('admin') or self.has_role('super_admin'): return True # اولویت دوم: professor - فقط دوره‌های خودش if course and self.has_role('professor'): return course.professors.filter(id=self.id).exists() return False def ensure_professor_profile(self, commit: bool = True) -> bool: """تضمین می‌کند کاربر نقش استاد دارد، اسلاگ دارد و در گروه استاد است.""" updated_fields = set() if self.user_type != self.UserType.PROFESSOR: self.user_type = self.UserType.PROFESSOR updated_fields.add('user_type') if not self.slug: self._ensure_professor_slug() if self.slug: updated_fields.add('slug') from django.contrib.auth.models import Group group, _ = Group.objects.get_or_create(name="Professor Group") group_added = False if not self.groups.filter(id=group.id).exists(): self.groups.add(group) group_added = True if commit and updated_fields: self.save(update_fields=list(updated_fields)) return bool(updated_fields or group_added) def _ensure_professor_slug(self): if self.slug: return base_candidates = [ self.fullname, (self.email.split('@')[0] if self.email else None), self.username, ] for candidate in base_candidates: if candidate: self.slug = self._build_unique_slug(candidate) if self.slug: return self.slug = self._build_unique_slug(f"professor-{secrets.token_hex(4)}") def _build_unique_slug(self, seed: str) -> str: base_slug = slugify(seed, allow_unicode=True) if not base_slug: base_slug = f"professor-{secrets.token_hex(4)}" slug = base_slug counter = 1 qs = User.objects.all() if self.pk: qs = qs.exclude(pk=self.pk) while qs.filter(slug=slug).exists(): slug = f"{base_slug}-{counter}" counter += 1 return slug[:255] def _get_fernet(self): key_bytes = settings.SECRET_KEY.encode('utf-8') hashed_key = hashlib.sha256(key_bytes).digest() fernet_key = base64.urlsafe_b64encode(hashed_key) return Fernet(fernet_key) def set_plain_password(self, plain_password): if not plain_password: self.password_enc = '' return try: password_bytes = plain_password.encode('utf-8') fernet = self._get_fernet() encrypted_token = fernet.encrypt(password_bytes) self.password_enc = encrypted_token.decode('utf-8') except Exception: self.password_enc = '' def get_plain_password(self): if not self.password_enc: return '' try: encrypted_bytes = self.password_enc.encode('utf-8') fernet = self._get_fernet() decrypted_bytes = fernet.decrypt(encrypted_bytes) return decrypted_bytes.decode('utf-8') except Exception: return '' class Meta: ordering = ("-id",) verbose_name = _("User") verbose_name_plural = _("Users") unique_together = ( 'email', ) class LoginHistory(models.Model): user = models.ForeignKey("account.User", on_delete=models.CASCADE, related_name='login_history', verbose_name=_('user')) lat = models.FloatField(verbose_name=_('lat'), null=True, blank=True) lon = models.FloatField(verbose_name=_('lon'), null=True, blank=True) country = models.CharField(max_length=255, verbose_name=_('country'), null=True, blank=True) city = models.CharField(max_length=255, verbose_name=_('city'), null=True, blank=True) ip = models.CharField(max_length=255, null=True, verbose_name=_('ip')) timezone = models.CharField(max_length=100, null=True, blank=True, verbose_name=_('timezone')) user_agent = models.TextField(verbose_name=_('user agent'), null=True, blank=True) device_os = models.CharField(max_length=16, null=True, blank=True, verbose_name=_('Device os')) at_time = models.DateTimeField(auto_now_add=True, verbose_name=_('at time')) class Meta: verbose_name = _('Login History') verbose_name_plural = _('Login Histories') class LocationHistory(models.Model): user = models.ForeignKey("account.User", on_delete=models.CASCADE, related_name='location_history', verbose_name=_('user')) lat = models.FloatField(verbose_name=_('lat')) lon = models.FloatField(verbose_name=_('lon')) country = models.CharField(max_length=255, verbose_name=_('country'), null=True, blank=True) city = models.CharField(max_length=255, verbose_name=_('city'), null=True, blank=True) selected_manually = models.BooleanField(null=True, blank=True, verbose_name=_('selected manually')) ip = models.CharField(max_length=255, null=True, blank=True, verbose_name=_('ip')) timezone = models.CharField(null=True, blank=True, max_length=60, verbose_name=_('timezone')) at_time = models.DateTimeField(auto_now_add=True, verbose_name=_('at time')) class Meta: verbose_name = _('Location History') verbose_name_plural = _('Location History')