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.
410 lines
17 KiB
410 lines
17 KiB
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')
|
|
|
|
|