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.""" 

11 

12import hashlib 

13import random 

14import string 

15 

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 

25 

26 

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 

33 

34 

35class ConfirmationManager(models.Manager): 

36 """ 

37 A custom manager for the :py:class:`Confirmation` model. 

38 """ 

39 

40 MAX_TRIES = 10 

41 

42 def generate_key(self, identifier): 

43 """ 

44 Generates a random key for the given identifier. 

45 

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() 

55 

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. 

60 

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 

73 

74 raise ConfirmationException( 

75 'Unable to generate a confirmation key for {identifier}'.format( 

76 identifier=identifier)) 

77 

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() 

85 

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. 

91 

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 

96 

97 

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) 

105 

106 objects = ConfirmationManager() 

107 

108 class Meta: 

109 abstract = True 

110 

111 def __str__(self): 

112 return self.confirmation_key 

113 

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 

122 

123 

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() 

141 

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) 

149 

150 return user 

151 

152 def create_user(self, main_email, password=None, **extra_fields): 

153 return self._create_user(main_email, password, False, False, 

154 **extra_fields) 

155 

156 def create(self, main_email, password=None, **extra_fields): 

157 return self._create_user(main_email, password, False, False, False, 

158 **extra_fields) 

159 

160 def create_superuser(self, main_email, password, **extra_fields): 

161 return self._create_user(main_email, password, True, True, 

162 **extra_fields) 

163 

164 

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) 

172 

173 is_active = models.BooleanField(default=False) 

174 is_staff = models.BooleanField(default=False) 

175 

176 USERNAME_FIELD = 'main_email' 

177 

178 objects = UserManager() 

179 

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 

185 

186 def get_short_name(self): 

187 return self.get_full_name() 

188 

189 

190class UserEmailManager(models.Manager): 

191 

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) 

204 

205 

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) 

210 

211 objects = UserEmailManager() 

212 default_manager = models.Manager() 

213 

214 def __str__(self): 

215 return self.email 

216 

217 def save(self, *args, **kwargs): 

218 self.full_clean() 

219 super(UserEmail, self).save(*args, **kwargs) 

220 

221 

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) 

228 

229 

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) 

237 

238 

239class AddEmailConfirmation(Confirmation): 

240 user = models.ForeignKey(User, on_delete=models.CASCADE) 

241 email = models.ForeignKey('UserEmail', on_delete=models.CASCADE) 

242 

243 

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)