models.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. # Copyright 2013 The Distro Tracker Developers
  2. # See the COPYRIGHT file at the top-level directory of this distribution and
  3. # at http://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 http://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. from __future__ import unicode_literals
  11. from django.db import models
  12. from django.conf import settings
  13. from django.utils.encoding import python_2_unicode_compatible
  14. from django.contrib.auth.models import AbstractBaseUser
  15. from django.contrib.auth.models import BaseUserManager
  16. from django.contrib.auth.models import PermissionsMixin
  17. from django.db.utils import IntegrityError
  18. from django.utils import timezone
  19. import string
  20. import random
  21. import hashlib
  22. class ConfirmationException(Exception):
  23. """
  24. An exception which is raised when the :py:class:`ConfirmationManager`
  25. is unable to generate a unique key for a given identifier.
  26. """
  27. pass
  28. class ConfirmationManager(models.Manager):
  29. """
  30. A custom manager for the :py:class:`Confirmation` model.
  31. """
  32. MAX_TRIES = 10
  33. def generate_key(self, identifier):
  34. """
  35. Generates a random key for the given identifier.
  36. :param identifier: A string representation of an identifier for the
  37. confirmation instance.
  38. """
  39. chars = string.ascii_letters + string.digits
  40. random_string = ''.join(random.choice(chars) for _ in range(16))
  41. random_string = random_string.encode('ascii')
  42. salt = hashlib.sha1(random_string).hexdigest()
  43. hash_input = (salt + identifier).encode('ascii')
  44. return hashlib.sha1(hash_input).hexdigest()
  45. def create_confirmation(self, identifier='', **kwargs):
  46. """
  47. Creates a :py:class:`Confirmation` object with the given identifier and
  48. all the given keyword arguments passed.
  49. :param identifier: A string representation of an identifier for the
  50. confirmation instance.
  51. :raises pts.mail.models.ConfirmationException: If it is unable to
  52. generate a unique key.
  53. """
  54. errors = 0
  55. while errors < self.MAX_TRIES:
  56. confirmation_key = self.generate_key(identifier)
  57. try:
  58. return self.create(confirmation_key=confirmation_key, **kwargs)
  59. except IntegrityError:
  60. errors += 1
  61. raise ConfirmationException(
  62. 'Unable to generate a confirmation key for {identifier}'.format(
  63. identifier=identifier))
  64. def clean_up_expired(self):
  65. """
  66. Removes all expired confirmation keys.
  67. """
  68. for confirmation in self.all():
  69. if confirmation.is_expired():
  70. confirmation.delete()
  71. def get(self, *args, **kwargs):
  72. """
  73. Overrides the default :py:class:`django.db.models.Manager` method so
  74. that expired :py:class:`Confirmation` instances are never
  75. returned.
  76. :rtype: :py:class:`Confirmation` or ``None``
  77. """
  78. instance = super(ConfirmationManager, self).get(*args, **kwargs)
  79. return instance if not instance.is_expired() else None
  80. @python_2_unicode_compatible
  81. class Confirmation(models.Model):
  82. """
  83. An abstract model allowing its subclasses to store and create confirmation
  84. keys.
  85. """
  86. confirmation_key = models.CharField(max_length=40, unique=True)
  87. date_created = models.DateTimeField(auto_now_add=True)
  88. objects = ConfirmationManager()
  89. class Meta:
  90. abstract = True
  91. def __str__(self):
  92. return self.confirmation_key
  93. def is_expired(self):
  94. """
  95. :returns True: if the confirmation key has expired
  96. :returns False: if the confirmation key is still valid
  97. """
  98. delta = timezone.now() - self.date_created
  99. return delta.days >= \
  100. settings.DISTRO_TRACKER_CONFIRMATION_EXPIRATION_DAYS
  101. class UserManager(BaseUserManager):
  102. """
  103. A custom manager for :class:`User`
  104. """
  105. def _create_user(self, main_email, password,
  106. is_staff, is_superuser, is_active=True, **extra_fields):
  107. """
  108. Creates and saves a User with the given username, email and password.
  109. """
  110. main_email = self.normalize_email(main_email)
  111. user = self.model(main_email=main_email,
  112. is_staff=is_staff,
  113. is_active=is_active,
  114. is_superuser=is_superuser,
  115. **extra_fields)
  116. user.set_password(password)
  117. user.save()
  118. # Match the email with a UserEmail instance and add it to the set of
  119. # associated emails for the user.
  120. email_user, _ = UserEmail.objects.get_or_create(email=main_email)
  121. user.emails.add(email_user)
  122. return user
  123. def create_user(self, main_email, password=None, **extra_fields):
  124. return self._create_user(main_email, password, False, False,
  125. **extra_fields)
  126. def create(self, main_email, password=None, **extra_fields):
  127. return self._create_user(main_email, password, False, False, False,
  128. **extra_fields)
  129. def create_superuser(self, main_email, password, **extra_fields):
  130. return self._create_user(main_email, password, True, True,
  131. **extra_fields)
  132. class User(AbstractBaseUser, PermissionsMixin):
  133. main_email = models.EmailField(
  134. max_length=255,
  135. unique=True,
  136. verbose_name='email')
  137. first_name = models.CharField(max_length=100, blank=True, null=True)
  138. last_name = models.CharField(max_length=100, blank=True, null=True)
  139. is_active = models.BooleanField(default=False)
  140. is_staff = models.BooleanField(default=False)
  141. USERNAME_FIELD = 'main_email'
  142. objects = UserManager()
  143. def get_full_name(self):
  144. first_name = self.first_name or ''
  145. last_name = self.last_name or ''
  146. separator = ' ' if first_name and last_name else ''
  147. return first_name + separator + last_name
  148. def get_short_name(self):
  149. return self.get_full_name()
  150. class UserEmailManager(models.Manager):
  151. def get_or_create(self, *args, **kwargs):
  152. """
  153. Replaces the default get_or_create() with one that matches
  154. the email case-insensitively.
  155. """
  156. defaults = kwargs.get('defaults', {})
  157. if 'email' in kwargs:
  158. kwargs['email__iexact'] = kwargs['email']
  159. defaults['email'] = kwargs['email']
  160. del kwargs['email']
  161. kwargs['defaults'] = defaults
  162. return UserEmail.default_manager.get_or_create(*args, **kwargs)
  163. @python_2_unicode_compatible
  164. class UserEmail(models.Model):
  165. email = models.EmailField(max_length=244, unique=True)
  166. user = models.ForeignKey(User, related_name='emails', null=True)
  167. objects = UserEmailManager()
  168. default_manager = models.Manager()
  169. def __str__(self):
  170. return self.email
  171. class UserRegistrationConfirmation(Confirmation):
  172. """
  173. A model for user registration confirmations.
  174. """
  175. user = models.OneToOneField(User, related_name='confirmation')
  176. class ResetPasswordConfirmation(Confirmation):
  177. """
  178. A model for account password reset confirmations.
  179. """
  180. user = models.ForeignKey(
  181. User, related_name='reset_password_confirmations')
  182. class AddEmailConfirmation(Confirmation):
  183. user = models.ForeignKey(User)
  184. email = models.ForeignKey('UserEmail')
  185. class MergeAccountConfirmation(Confirmation):
  186. initial_user = models.ForeignKey(
  187. User, related_name='merge_account_initial_set')
  188. merge_with = models.ForeignKey(
  189. User, related_name='merge_account_with_set')