Coverage for users/models/users.py: 79%
84 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1from django.contrib.auth.base_user import AbstractBaseUser
2from django.contrib.auth.models import (
3 GroupManager as DjangoGroupManager,
4)
5from django.contrib.auth.models import (
6 Permission,
7 PermissionsMixin,
8)
9from django.contrib.auth.models import (
10 UserManager as DjangoUserManager,
11)
12from django.contrib.auth.validators import UnicodeUsernameValidator
13from django.core.exceptions import ValidationError
14from django.core.mail import send_mail
15from django.db import models
16from django.urls import reverse
17from django.utils import timezone
18from django.utils.translation import gettext_lazy as _
20from utilities.querysets import RestrictedQuerySet
22__all__ = (
23 'Group',
24 'GroupManager',
25 'User',
26 'UserManager',
27)
30class GroupManager(DjangoGroupManager.from_queryset(RestrictedQuerySet)):
31 pass
34class Group(models.Model):
35 name = models.CharField(
36 verbose_name=_('name'),
37 max_length=150,
38 unique=True
39 )
40 description = models.CharField(
41 verbose_name=_('description'),
42 max_length=200,
43 blank=True
44 )
45 object_permissions = models.ManyToManyField(
46 to='users.ObjectPermission',
47 blank=True,
48 related_name='groups'
49 )
51 # Replicate legacy Django permissions support from stock Group model
52 # to ensure authentication backend compatibility
53 permissions = models.ManyToManyField(
54 Permission,
55 verbose_name=_("permissions"),
56 blank=True,
57 related_name='groups',
58 related_query_name='group'
59 )
61 objects = GroupManager()
63 class Meta:
64 ordering = ('name',)
65 verbose_name = _('group')
66 verbose_name_plural = _('groups')
68 def __str__(self):
69 return self.name
71 def get_absolute_url(self):
72 return reverse('users:group', args=[self.pk])
74 def natural_key(self):
75 return (self.name,)
78class UserManager(DjangoUserManager.from_queryset(RestrictedQuerySet)):
80 def create_user(self, username, email=None, password=None, **extra_fields):
81 extra_fields.setdefault('is_superuser', False)
82 return self._create_user(username, email, password, **extra_fields)
84 create_user.alters_data = True
86 async def acreate_user(self, username, email=None, password=None, **extra_fields):
87 extra_fields.setdefault('is_superuser', False)
88 return await self._acreate_user(username, email, password, **extra_fields)
90 acreate_user.alters_data = True
92 def create_superuser(self, username, email=None, password=None, **extra_fields):
93 extra_fields.setdefault('is_superuser', True)
95 if extra_fields.get('is_superuser') is not True: 95 ↛ 96line 95 didn't jump to line 96 because the condition on line 95 was never true
96 raise ValueError('Superuser must have is_superuser=True.')
98 return self._create_user(username, email, password, **extra_fields)
100 create_superuser.alters_data = True
102 async def acreate_superuser(self, username, email=None, password=None, **extra_fields):
103 extra_fields.setdefault('is_superuser', True)
105 if extra_fields.get('is_superuser') is not True:
106 raise ValueError('Superuser must have is_superuser=True.')
108 return await self._acreate_user(username, email, password, **extra_fields)
110 acreate_superuser.alters_data = True
113class User(AbstractBaseUser, PermissionsMixin):
114 username = models.CharField(
115 _("username"),
116 max_length=150,
117 unique=True,
118 help_text=_("Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."),
119 validators=[UnicodeUsernameValidator()],
120 error_messages={
121 "unique": _("A user with that username already exists."),
122 },
123 )
124 first_name = models.CharField(
125 _("first name"),
126 max_length=150,
127 blank=True,
128 )
129 last_name = models.CharField(
130 _("last name"),
131 max_length=150,
132 blank=True,
133 )
134 email = models.EmailField(
135 _("email address"),
136 blank=True,
137 )
138 is_active = models.BooleanField(
139 _("active"),
140 default=True,
141 help_text=_(
142 "Designates whether this user should be treated as active. Unselect this instead of deleting accounts."
143 ),
144 )
145 date_joined = models.DateTimeField(
146 _("date joined"),
147 default=timezone.now,
148 )
149 groups = models.ManyToManyField(
150 to='users.Group',
151 verbose_name=_('groups'),
152 blank=True,
153 related_name='users',
154 related_query_name='user'
155 )
156 object_permissions = models.ManyToManyField(
157 to='users.ObjectPermission',
158 blank=True,
159 related_name='users'
160 )
162 objects = UserManager()
164 # Ensure compatibility with Django's stock User model
165 EMAIL_FIELD = "email"
166 USERNAME_FIELD = "username"
167 REQUIRED_FIELDS = ["email"]
169 class Meta:
170 ordering = ('username',)
171 verbose_name = _('user')
172 verbose_name_plural = _('users')
174 def get_absolute_url(self):
175 return reverse('users:user', args=[self.pk])
177 def clean(self):
178 super().clean()
180 # Normalize email address
181 self.email = self.__class__.objects.normalize_email(self.email)
183 # Check for any existing Users with names that differ only in case
184 model = self._meta.model
185 if model.objects.exclude(pk=self.pk).filter(username__iexact=self.username).exists(): 185 ↛ 186line 185 didn't jump to line 186 because the condition on line 185 was never true
186 raise ValidationError(_("A user with this username already exists."))
188 def get_full_name(self):
189 """
190 Return the first_name plus the last_name, with a space in between.
191 """
192 full_name = f"{self.first_name} {self.last_name}"
193 return full_name.strip()
195 def get_short_name(self):
196 """Return the short name for the user."""
197 return self.first_name
199 def email_user(self, subject, message, from_email=None, **kwargs):
200 """Send an email to this user."""
201 send_mail(subject, message, from_email, [self.email], **kwargs)