| 
 | 1 | +from django.conf import settings  | 
 | 2 | +from django.contrib.auth.base_user import BaseUserManager  | 
 | 3 | +from django.contrib.auth.models import PermissionsMixin, AbstractUser  | 
 | 4 | +from django.core.exceptions import ValidationError  | 
 | 5 | +from django.core.validators import RegexValidator  | 
 | 6 | +from django.db import models  | 
 | 7 | +from django.utils import timezone  | 
 | 8 | +from django.utils.translation import gettext_lazy as _  | 
 | 9 | +from simple_history.models import HistoricalRecords  | 
 | 10 | + | 
 | 11 | +from .utils import profile_upload_to_unique  | 
 | 12 | + | 
 | 13 | + | 
 | 14 | +class Profile(models.Model):  | 
 | 15 | +    """  | 
 | 16 | +    Model representing a user's profile.  | 
 | 17 | +    Connects to income, expenses, and allows for personal data storage.  | 
 | 18 | +    """  | 
 | 19 | +    user = models.OneToOneField(  | 
 | 20 | +        settings.AUTH_USER_MODEL,  | 
 | 21 | +        on_delete=models.CASCADE,  | 
 | 22 | +        related_name="profile"  | 
 | 23 | +    )  | 
 | 24 | +    balance = models.DecimalField(  | 
 | 25 | +        max_digits=10,  | 
 | 26 | +        decimal_places=2,  | 
 | 27 | +        default=0.0,  | 
 | 28 | +        help_text="User's current balance."  | 
 | 29 | +    )  | 
 | 30 | +    date_created = models.DateTimeField(auto_now_add=True)  | 
 | 31 | +    profile_pic = models.ImageField(  | 
 | 32 | +        upload_to=profile_upload_to_unique,  | 
 | 33 | +        blank=True,  | 
 | 34 | +        null=True,  | 
 | 35 | +        help_text="User's profile picture."  | 
 | 36 | +    )  | 
 | 37 | + | 
 | 38 | +    # Add historical records field to track changes  | 
 | 39 | +    history = HistoricalRecords()  | 
 | 40 | + | 
 | 41 | +    def __str__(self):  | 
 | 42 | +        """String representation of the profile object, displaying the associated user's username."""  | 
 | 43 | +        return f'Profile of {self.user}'  | 
 | 44 | + | 
 | 45 | + | 
 | 46 | +class Department(models.Model):  | 
 | 47 | +    name = models.CharField(max_length=100, unique=True)  | 
 | 48 | + | 
 | 49 | +    def __str__(self):  | 
 | 50 | +        return self.name  | 
 | 51 | + | 
 | 52 | + | 
 | 53 | +class Role(models.Model):  | 
 | 54 | +    name = models.CharField(max_length=100, unique=True)  | 
 | 55 | +    description = models.TextField(blank=True)  | 
 | 56 | + | 
 | 57 | +    def __str__(self):  | 
 | 58 | +        return self.name  | 
 | 59 | + | 
 | 60 | + | 
 | 61 | +# Custom User Manager  | 
 | 62 | +class UserManager(BaseUserManager):  | 
 | 63 | +    def create_user(self, email, password=None, **extra_fields):  | 
 | 64 | +        if not email:  | 
 | 65 | +            raise ValueError(_('The Email field must be set'))  | 
 | 66 | +        email = self.normalize_email(email)  | 
 | 67 | +        user = self.model(email=email, **extra_fields)  | 
 | 68 | +        user.set_password(password)  | 
 | 69 | +        user.save(using=self._db)  | 
 | 70 | +        return user  | 
 | 71 | + | 
 | 72 | +    def create_superuser(self, email, password=None, **extra_fields):  | 
 | 73 | +        extra_fields.setdefault('is_staff', True)  | 
 | 74 | +        extra_fields.setdefault('is_superuser', True)  | 
 | 75 | +        extra_fields.setdefault('is_active', True)  | 
 | 76 | + | 
 | 77 | +        if extra_fields.get('is_staff') is not True:  | 
 | 78 | +            raise ValueError(_('Superuser must have is_staff=True.'))  | 
 | 79 | +        if extra_fields.get('is_superuser') is not True:  | 
 | 80 | +            raise ValueError(_('Superuser must have is_superuser=True.'))  | 
 | 81 | + | 
 | 82 | +        return self.create_user(email, password, **extra_fields)  | 
 | 83 | + | 
 | 84 | + | 
 | 85 | +# Custom User Model  | 
 | 86 | +class UserAccount(AbstractUser, PermissionsMixin):  | 
 | 87 | +    email = models.EmailField(_('email address'), unique=True)  | 
 | 88 | +    username = models.CharField(_('username'), max_length=30, unique=True, blank=False,  | 
 | 89 | +                                help_text="User's unique username",  | 
 | 90 | +                                validators=[  | 
 | 91 | +                                    RegexValidator(  | 
 | 92 | +                                        regex=r'^[\w-]+$',  | 
 | 93 | +                                        message=_(  | 
 | 94 | +                                            "Username can only contain letters, numbers, underscores, or hyphens.")  | 
 | 95 | +                                    )  | 
 | 96 | +                                ]  | 
 | 97 | +                                )  | 
 | 98 | +    phone_number = models.CharField(_('phone number'), max_length=15, unique=True, null=True, blank=True,  | 
 | 99 | +                                    validators=[  | 
 | 100 | +                                        RegexValidator(  | 
 | 101 | +                                            regex=r'^\+?1?\d{9,15}$',  | 
 | 102 | +                                            message=_(  | 
 | 103 | +                                                "Phone number must be entered in the format: '+999999999'. Up to 15 digits allowed."  | 
 | 104 | +                                            ),  | 
 | 105 | +                                        )  | 
 | 106 | +                                    ], )  | 
 | 107 | +    first_name = models.CharField(_('first name'), max_length=30, blank=True)  | 
 | 108 | +    last_name = models.CharField(_('last name'), max_length=30, blank=True)  | 
 | 109 | +    profile_image = models.ImageField(_('profile image'), upload_to='profile_images/', null=True, blank=True)  | 
 | 110 | +    bio = models.TextField(_('bio'), max_length=500, blank=True)  | 
 | 111 | +    last_login_ip = models.GenericIPAddressField(_('last login IP'), null=True, blank=True)  | 
 | 112 | +    last_login = models.DateTimeField(_('last login'), auto_now=True)  | 
 | 113 | +    date_joined = models.DateTimeField(_('date joined'), default=timezone.now)  | 
 | 114 | + | 
 | 115 | +    is_active = models.BooleanField(_('active'), default=True)  | 
 | 116 | +    is_staff = models.BooleanField(_('staff status'), default=False)  | 
 | 117 | +    is_manager = models.BooleanField(_('manager status'), default=False)  | 
 | 118 | +    is_admin = models.BooleanField(_('admin status'), default=False)  | 
 | 119 | +    roles = models.ManyToManyField(Role, related_name='users', blank=True)  | 
 | 120 | +    department = models.ForeignKey(Department, on_delete=models.SET_NULL, null=True, blank=True)  | 
 | 121 | + | 
 | 122 | +    objects = UserManager()  | 
 | 123 | + | 
 | 124 | +    USERNAME_FIELD = 'email'  | 
 | 125 | +    REQUIRED_FIELDS = ['username', 'phone_number', 'first_name', 'last_name']  | 
 | 126 | + | 
 | 127 | +    @property  | 
 | 128 | +    def is_staff_effective(self):  | 
 | 129 | +        # Check if the user has an associated employee profile (and therefore system access)  | 
 | 130 | +        return hasattr(self, 'employee_profile')  | 
 | 131 | + | 
 | 132 | +    @property  | 
 | 133 | +    def name(self):  | 
 | 134 | +        return f'{self.first_name} {self.last_name}'  | 
 | 135 | + | 
 | 136 | +    def __str__(self):  | 
 | 137 | +        return self.username or self.email  | 
 | 138 | + | 
 | 139 | +    class Meta:  | 
 | 140 | +        verbose_name = _('user')  | 
 | 141 | +        verbose_name_plural = _('users')  | 
 | 142 | +        ordering = ['-pk']  | 
 | 143 | + | 
 | 144 | + | 
 | 145 | +class Employee(models.Model):  | 
 | 146 | +    user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='employee_profile')  | 
 | 147 | +    # Department is mandatory (null is False by default)  | 
 | 148 | +    department = models.ForeignKey(Department, on_delete=models.PROTECT)  | 
 | 149 | +    # Roles is a ManyToManyField; however, Django doesn't enforce non-empty assignments by default.  | 
 | 150 | +    roles = models.ManyToManyField(Role)  | 
 | 151 | +    employee_id = models.CharField(max_length=50, unique=True)  | 
 | 152 | +    hire_date = models.DateField()  | 
 | 153 | + | 
 | 154 | +    def clean(self):  | 
 | 155 | +        # Ensure that department is provided (should always be, since null is not allowed)  | 
 | 156 | +        if not self.department:  | 
 | 157 | +            raise ValidationError("Department must be provided.")  | 
 | 158 | + | 
 | 159 | +        # Check that at least one role is assigned. Be aware that since roles is a M2M field,  | 
 | 160 | +        # this check may need to happen after the instance is saved.  | 
 | 161 | +        if not self.pk or self.roles.count() == 0:  | 
 | 162 | +            raise ValidationError("At least one role must be assigned to the employee.")  | 
 | 163 | + | 
 | 164 | +    def save(self, *args, **kwargs):  | 
 | 165 | +        # Perform full clean before saving to ensure all validations are enforced.  | 
 | 166 | +        self.full_clean()  | 
 | 167 | +        super().save(*args, **kwargs)  | 
 | 168 | + | 
 | 169 | +    def __str__(self):  | 
 | 170 | +        return f"{self.user.username} ({self.employee_id})"  | 
0 commit comments