1# Copyright 2013 The Distro Tracker Developers
2# See the COPYRIGHT file at the top-level directory of this distribution and
3# at https://deb.li/DTAuthors
4#
5# This file is part of Distro Tracker. It is subject to the license terms
6# in the LICENSE file found in the top-level directory of this
7# distribution and at https://deb.li/DTLicense. No part of Distro Tracker,
8# including this file, may be copied, modified, propagated, or distributed
9# except according to the terms contained in the LICENSE file.
10"""Django models for django_email_accounts."""
12import hashlib
13import random
14import string
16from django.conf import settings
17from django.contrib.auth.models import (
18 AbstractBaseUser,
19 BaseUserManager,
20 PermissionsMixin
21)
22from django.db import models
23from django.db.utils import IntegrityError
24from django.utils import timezone
27class ConfirmationException(Exception):
28 """
29 An exception which is raised when the :py:class:`ConfirmationManager`
30 is unable to generate a unique key for a given identifier.
31 """
32 pass
35class ConfirmationManager(models.Manager):
36 """
37 A custom manager for the :py:class:`Confirmation` model.
38 """
40 MAX_TRIES = 10
42 def generate_key(self, identifier):
43 """
44 Generates a random key for the given identifier.
46 :param identifier: A string representation of an identifier for the
47 confirmation instance.
48 """
49 chars = string.ascii_letters + string.digits
50 random_string = ''.join(random.choice(chars) for _ in range(16))
51 random_string = random_string.encode('ascii')
52 salt = hashlib.sha1(random_string).hexdigest()
53 hash_input = (salt + identifier).encode('ascii')
54 return hashlib.sha1(hash_input).hexdigest()
56 def create_confirmation(self, identifier='', **kwargs):
57 """
58 Creates a :py:class:`Confirmation` object with the given identifier and
59 all the given keyword arguments passed.
61 :param identifier: A string representation of an identifier for the
62 confirmation instance.
63 :raises pts.mail.models.ConfirmationException: If it is unable to
64 generate a unique key.
65 """
66 errors = 0
67 while errors < self.MAX_TRIES: 67 ↛ 74line 67 didn't jump to line 74, because the condition on line 67 was never false
68 confirmation_key = self.generate_key(identifier)
69 try:
70 return self.create(confirmation_key=confirmation_key, **kwargs)
71 except IntegrityError:
72 errors += 1
74 raise ConfirmationException(
75 'Unable to generate a confirmation key for {identifier}'.format(
76 identifier=identifier))
78 def clean_up_expired(self):
79 """
80 Removes all expired confirmation keys.
81 """
82 for confirmation in self.all():
83 if confirmation.is_expired():
84 confirmation.delete()
86 def get(self, *args, **kwargs):
87 """
88 Overrides the default :py:class:`django.db.models.Manager` method so
89 that expired :py:class:`Confirmation` instances are never
90 returned.
92 :rtype: :py:class:`Confirmation` or ``None``
93 """
94 instance = super(ConfirmationManager, self).get(*args, **kwargs)
95 return instance if not instance.is_expired() else None
98class Confirmation(models.Model):
99 """
100 An abstract model allowing its subclasses to store and create confirmation
101 keys.
102 """
103 confirmation_key = models.CharField(max_length=40, unique=True)
104 date_created = models.DateTimeField(auto_now_add=True)
106 objects = ConfirmationManager()
108 class Meta:
109 abstract = True
111 def __str__(self):
112 return self.confirmation_key
114 def is_expired(self):
115 """
116 :returns True: if the confirmation key has expired
117 :returns False: if the confirmation key is still valid
118 """
119 delta = timezone.now() - self.date_created
120 return delta.days >= \
121 settings.DISTRO_TRACKER_CONFIRMATION_EXPIRATION_DAYS
124class UserManager(BaseUserManager):
125 """
126 A custom manager for :class:`User`
127 """
128 def _create_user(self, main_email, password,
129 is_staff, is_superuser, is_active=True, **extra_fields):
130 """
131 Creates and saves a User with the given username, email and password.
132 """
133 main_email = self.normalize_email(main_email)
134 user = self.model(main_email=main_email,
135 is_staff=is_staff,
136 is_active=is_active,
137 is_superuser=is_superuser,
138 **extra_fields)
139 user.set_password(password)
140 user.save()
142 # Match the email with a UserEmail instance and add it to the set of
143 # associated emails for the user.
144 email_user, _ = UserEmail.objects.get_or_create(
145 email__iexact=main_email,
146 defaults={'email': main_email}
147 )
148 user.emails.add(email_user)
150 return user
152 def create_user(self, main_email, password=None, **extra_fields):
153 return self._create_user(main_email, password, False, False,
154 **extra_fields)
156 def create(self, main_email, password=None, **extra_fields):
157 return self._create_user(main_email, password, False, False, False,
158 **extra_fields)
160 def create_superuser(self, main_email, password, **extra_fields):
161 return self._create_user(main_email, password, True, True,
162 **extra_fields)
165class User(AbstractBaseUser, PermissionsMixin):
166 main_email = models.EmailField(
167 max_length=255,
168 unique=True,
169 verbose_name='email')
170 first_name = models.CharField(max_length=100, blank=True, null=True)
171 last_name = models.CharField(max_length=100, blank=True, null=True)
173 is_active = models.BooleanField(default=False)
174 is_staff = models.BooleanField(default=False)
176 USERNAME_FIELD = 'main_email'
178 objects = UserManager()
180 def get_full_name(self):
181 first_name = self.first_name or ''
182 last_name = self.last_name or ''
183 separator = ' ' if first_name and last_name else ''
184 return first_name + separator + last_name
186 def get_short_name(self):
187 return self.get_full_name()
190class UserEmailManager(models.Manager):
192 def get_or_create(self, *args, **kwargs):
193 """
194 Replaces the default method with one that matches
195 the email case-insensitively.
196 """
197 defaults = kwargs.get('defaults', {})
198 if 'email' in kwargs:
199 kwargs['email__iexact'] = kwargs['email']
200 defaults['email'] = kwargs['email']
201 del kwargs['email']
202 kwargs['defaults'] = defaults
203 return UserEmail.default_manager.get_or_create(*args, **kwargs)
206class UserEmail(models.Model):
207 email = models.EmailField(max_length=244, unique=True)
208 user = models.ForeignKey(User, related_name='emails', blank=True, null=True,
209 on_delete=models.CASCADE)
211 objects = UserEmailManager()
212 default_manager = models.Manager()
214 def __str__(self):
215 return self.email
217 def save(self, *args, **kwargs):
218 self.full_clean()
219 super(UserEmail, self).save(*args, **kwargs)
222class UserRegistrationConfirmation(Confirmation):
223 """
224 A model for user registration confirmations.
225 """
226 user = models.OneToOneField(User, related_name='confirmation',
227 on_delete=models.CASCADE)
230class ResetPasswordConfirmation(Confirmation):
231 """
232 A model for account password reset confirmations.
233 """
234 user = models.ForeignKey(
235 User, related_name='reset_password_confirmations',
236 on_delete=models.CASCADE)
239class AddEmailConfirmation(Confirmation):
240 user = models.ForeignKey(User, on_delete=models.CASCADE)
241 email = models.ForeignKey('UserEmail', on_delete=models.CASCADE)
244class MergeAccountConfirmation(Confirmation):
245 initial_user = models.ForeignKey(
246 User, related_name='merge_account_initial_set',
247 on_delete=models.CASCADE)
248 merge_with = models.ForeignKey(
249 User, related_name='merge_account_with_set', on_delete=models.CASCADE)