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.
 
 

298 lines
11 KiB

import random
import base64
import hashlib
from cryptography.fernet import Fernet
from django.conf import settings
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 iOS')
web = 'web', _('Web')
class UserType(models.TextChoices):
CLIENT = 'client', _('Client / User')
STUDENT = 'student', _('Student / Member')
PROFESSOR = 'professor', _('Professor / Scholar')
CONSULTANT = 'consultant', _('Consultant')
ADMIN = 'admin', _('Admin')
SUPER_ADMIN = 'super_admin', _('Super Admin')
REGIONAL_ADMIN = 'regional_admin', _('Regional Admin')
INSTITUTION_ADMIN = 'institution_admin', _('Institution Admin')
EDITOR = 'editor', _('Editor')
VIEWER = 'viewer', _('Viewer')
class GenderChoices(models.TextChoices):
MALE = 'male', _('Male')
FEMALE = 'female', _('Female')
OTHER = 'other', _('Other')
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=_("User primary 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=_("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., +1 555 1234567")
)
language = models.CharField(
max_length=10,
default='en',
choices=settings.LANGUAGES,
null=True,
blank=True,
verbose_name=_('Language')
)
gender = models.CharField(
max_length=20,
choices=GenderChoices.choices,
null=True,
blank=True,
verbose_name=_('Gender')
)
user_type = models.CharField(
max_length=20,
choices=UserType.choices,
default=UserType.CLIENT,
verbose_name=_("User Type")
)
date_joined = models.DateTimeField(
auto_now_add=True,
verbose_name=_("Date Joined")
)
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, blank=True, max_length=16, verbose_name=_('Device OS'))
user_agent = models.TextField(verbose_name=_('User Agent'), null=True, blank=True)
client_ip = models.CharField(max_length=64, verbose_name=_('Client IP'), null=True, blank=True)
fcm = models.CharField(max_length=512, null=True, blank=True, verbose_name=_('FCM Token'))
slug = models.SlugField(max_length=255, unique=True, null=True, blank=True, verbose_name=_('Slug'))
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.")
)
deleted_at = models.DateTimeField(null=True, blank=True, verbose_name=_('Deleted At'))
info = models.TextField(verbose_name=_("Bio / Info"), null=True, blank=True)
skill = models.CharField(max_length=512, null=True, blank=True, verbose_name=_('Skill / Role'))
languages = models.JSONField(default=list, blank=True, verbose_name=_('Languages'))
skills = models.JSONField(default=list, blank=True, verbose_name=_('Skills'))
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 if required.")
)
objects = UserManager()
EMAIL_FIELD = "email"
USERNAME_FIELD = "email"
REQUIRED_FIELDS = []
def __str__(self):
return self.email or self.fullname or self.username or f"User #{self.id}"
def soft_delete(self):
self.deleted_at = timezone.now()
self.is_active = False
self.fullname = f'{self.fullname}:deleted' if self.fullname else 'deleted_user'
number = str(random.randint(1000000000, 9999999999))
if self.phone_number:
self.phone_number = f'{self.phone_number}:deleted{number}'
if self.email:
self.email = f'{self.email}:deleted{number}'
if self.device_id:
self.device_id = f'{self.device_id}:deleted{number}'
self.save()
def save(self, *args, **kwargs):
if not self.username and self.email:
self.username = self.email
elif self.email and User.objects.filter(username=self.email).exclude(pk=self.pk).exists():
self.username = f'{self.email}:{self.id or random.randint(1000, 9999)}'
return super().save(*args, **kwargs)
def get_full_name(self):
return self.fullname or self.email or ""
@property
def is_guest(self):
return self.email is None
def has_role(self, role_name):
return self.groups.filter(name__iexact=f"{role_name} Group").exists() or self.user_type == role_name
@property
def is_super_admin(self):
return self.is_superuser or self.user_type == self.UserType.SUPER_ADMIN
@property
def is_regional_admin(self):
return self.is_super_admin or self.user_type == self.UserType.REGIONAL_ADMIN
@property
def is_institution_admin(self):
return self.is_regional_admin or self.user_type == self.UserType.INSTITUTION_ADMIN
@property
def is_editor(self):
return self.is_institution_admin or self.user_type == self.UserType.EDITOR
def is_super_admin_panel_user(self):
return self.is_superuser or self.user_type == self.UserType.SUPER_ADMIN
def is_admin_panel_user(self):
return self.is_staff or self.is_superuser or self.user_type in [
self.UserType.SUPER_ADMIN,
self.UserType.ADMIN,
self.UserType.REGIONAL_ADMIN,
self.UserType.INSTITUTION_ADMIN,
]
def is_professor_panel_user(self):
return self.user_type == self.UserType.PROFESSOR
def can_access_admin_panel(self):
return (
self.is_staff or
self.is_superuser or
self.user_type in [
self.UserType.SUPER_ADMIN,
self.UserType.ADMIN,
self.UserType.REGIONAL_ADMIN,
self.UserType.INSTITUTION_ADMIN,
self.UserType.EDITOR,
]
)
@property
def verification_status(self):
latest = self.verification_documents.first()
if latest:
return latest.status
return 'unverified'
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")
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=_('Latitude'), null=True, blank=True)
lon = models.FloatField(verbose_name=_('Longitude'), 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, blank=True, verbose_name=_('IP Address'))
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=_('Timestamp'))
class Meta:
verbose_name = _('Login History')
verbose_name_plural = _('Login Histories')
ordering = ('-at_time',)
def __str__(self):
return f"{self.user} - {self.ip} ({self.at_time})"
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=_('Latitude'), null=True, blank=True)
lon = models.FloatField(verbose_name=_('Longitude'), 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)
selected_manually = models.BooleanField(default=False, verbose_name=_('Selected Manually'))
ip = models.CharField(max_length=255, null=True, blank=True, verbose_name=_('IP Address'))
timezone = models.CharField(null=True, blank=True, max_length=60, verbose_name=_('Timezone'))
at_time = models.DateTimeField(auto_now_add=True, verbose_name=_('Timestamp'))
class Meta:
verbose_name = _('Location History')
verbose_name_plural = _('Location History')
ordering = ('-at_time',)
def __str__(self):
return f"{self.user} - {self.city}, {self.country}"