Merge branch 'jordan' of ssh://git.onelab.eu/git/myslice into jordan
[myslice.git] / portal / models.py
1 # -*- coding: utf-8 -*-
2 #
3 # portal/models.py: models for the portal application
4 # This file is part of the Manifold project.
5 #
6 # Authors:
7 #   Jordan AugĂ© <jordan.auge@lip6.fr>
8 # Copyright 2013, UPMC Sorbonne UniversitĂ©s / LIP6
9 #
10 # This program is free software; you can redistribute it and/or modify it under
11 # the terms of the GNU General Public License as published by the Free Software
12 # Foundation; either version 3, or (at your option) any later version.
13
14 # This program is distributed in the hope that it will be useful, but WITHOUT
15 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16 # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
17 # details.
18
19 # You should have received a copy of the GNU General Public License along with
20 # this program; see the file COPYING.  If not, write to the Free Software
21 # Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
22
23 import datetime
24 import hashlib
25 import random
26 import re
27
28 from django.conf              import settings
29 from django.core.mail         import send_mail
30 from django.db                import models
31 from django.db                import transaction
32 from django.utils.translation import ugettext_lazy as _
33 from django.template.loader   import render_to_string
34
35 #from django.core.validators import validate_email
36
37 try:
38     from django.contrib.auth import get_user_model
39     User = get_user_model()
40 except ImportError:
41     from django.contrib.auth.models import User
42
43 try:
44     from django.utils.timezone import now as datetime_now
45 except ImportError:
46     datetime_now = datetime.datetime.now
47
48 SHA1_RE = re.compile('^[a-f0-9]{40}$')
49
50 # Create your models here.
51
52 class Institution(models.Model):
53     name = models.TextField()
54     # list of associated email domains 
55
56 # code borrowed from django-registration
57 # https://bitbucket.org/ubernostrum/django-registration/
58
59 class RegistrationManager(models.Manager):
60     """
61     Custom manager for the ``RegistrationProfile`` model.
62     
63     The methods defined here provide shortcuts for account creation
64     and activation (including generation and emailing of activation
65     keys), and for cleaning out expired inactive accounts.
66     
67     """
68     def activate_user(self, activation_key):
69         """
70         Validate an activation key and activate the corresponding
71         ``User`` if valid.
72         
73         If the key is valid and has not expired, return the ``User``
74         after activating.
75         
76         If the key is not valid or has expired, return ``False``.
77         
78         If the key is valid but the ``User`` is already active,
79         return ``False``.
80         
81         To prevent reactivation of an account which has been
82         deactivated by site administrators, the activation key is
83         reset to the string constant ``RegistrationProfile.ACTIVATED``
84         after successful activation.
85
86         """
87         # Make sure the key we're trying conforms to the pattern of a
88         # SHA1 hash; if it doesn't, no point trying to look it up in
89         # the database.
90         if SHA1_RE.search(activation_key):
91             try:
92                 profile = self.get(activation_key=activation_key)
93             except self.model.DoesNotExist:
94                 return False
95             if not profile.activation_key_expired():
96                 user = profile.user
97                 user.is_active = True
98                 user.save()
99                 profile.activation_key = self.model.ACTIVATED
100                 profile.save()
101                 return user
102         return False
103     
104     def create_user(self, first_name, last_name, email, password):
105         pending_user = self.create(first_name=first_name, last_name=last_name, email=email, password=password)
106         return pending_user
107
108     def create_inactive_user(self, first_name, last_name, email, password, site,
109                              send_email=True):
110         """
111         Create a new, inactive ``User``, generate a
112         ``RegistrationProfile`` and email its activation key to the
113         ``User``, returning the new ``User``.
114
115         By default, an activation email will be sent to the new
116         user. To disable this, pass ``send_email=False``.
117         
118         """
119         salt = hashlib.sha1(str(random.random())).hexdigest()[:5]
120         if isinstance(email, unicode):
121             email = email.encode('utf-8')
122         activation_key = hashlib.sha1(salt+email).hexdigest()
123
124         new_user = PendingUser.objects.create_user(first_name, last_name, email, password)
125         new_user.is_active = False
126         new_user.activation_key=activation_key
127         new_user.save()
128
129         # We might not need this
130         #registration_profile = self.create_profile(new_user)
131
132         if send_email:
133             new_user.send_activation_email(site)
134             #registration_profile.send_activation_email(site)
135
136         return new_user
137     create_inactive_user = transaction.commit_on_success(create_inactive_user)
138
139     def create_profile(self, user):
140         """
141         Create a ``RegistrationProfile`` for a given
142         ``User``, and return the ``RegistrationProfile``.
143         
144         The activation key for the ``RegistrationProfile`` will be a
145         SHA1 hash, generated from a combination of the ``User``'s
146         username and a random salt.
147         
148         """
149         salt = hashlib.sha1(str(random.random())).hexdigest()[:5]
150         username = user.username
151         if isinstance(username, unicode):
152             username = username.encode('utf-8')
153         activation_key = hashlib.sha1(salt+username).hexdigest()
154         return self.create(user=user,
155                            activation_key=activation_key)
156         
157     def delete_expired_users(self):
158         """
159         Remove expired instances of ``RegistrationProfile`` and their
160         associated ``User``s.
161         
162         Accounts to be deleted are identified by searching for
163         instances of ``RegistrationProfile`` with expired activation
164         keys, and then checking to see if their associated ``User``
165         instances have the field ``is_active`` set to ``False``; any
166         ``User`` who is both inactive and has an expired activation
167         key will be deleted.
168         
169         It is recommended that this method be executed regularly as
170         part of your routine site maintenance; this application
171         provides a custom management command which will call this
172         method, accessible as ``manage.py cleanupregistration``.
173         
174         Regularly clearing out accounts which have never been
175         activated serves two useful purposes:
176         
177         1. It alleviates the ocasional need to reset a
178            ``RegistrationProfile`` and/or re-send an activation email
179            when a user does not receive or does not act upon the
180            initial activation email; since the account will be
181            deleted, the user will be able to simply re-register and
182            receive a new activation key.
183         
184         2. It prevents the possibility of a malicious user registering
185            one or more accounts and never activating them (thus
186            denying the use of those usernames to anyone else); since
187            those accounts will be deleted, the usernames will become
188            available for use again.
189         
190         If you have a troublesome ``User`` and wish to disable their
191         account while keeping it in the database, simply delete the
192         associated ``RegistrationProfile``; an inactive ``User`` which
193         does not have an associated ``RegistrationProfile`` will not
194         be deleted.
195         
196         """
197         for profile in self.all():
198             try:
199                 if profile.activation_key_expired():
200                     user = profile.user
201                     if not user.is_active:
202                         user.delete()
203                         profile.delete()
204             except User.DoesNotExist:
205                 profile.delete()
206
207
208 class PendingUser(models.Model):
209     # NOTE We might consider migrating the fields to CharField, which would
210     # simplify form creation in forms.py
211     first_name  = models.TextField()
212     last_name   = models.TextField()
213     affiliation = models.TextField()
214     email       = models.EmailField() #validators=[validate_email])
215     password    = models.TextField()
216     keypair     = models.TextField()
217     # institution
218
219     objects = RegistrationManager()
220
221     class Meta:
222         verbose_name = _('registration profile')
223         verbose_name_plural = _('registration profiles')
224     
225     def __unicode__(self):
226         return u"Registration information for %s" % self.email
227
228     def activation_key_expired(self):
229         """
230         Determine whether this ``RegistrationProfile``'s activation
231         key has expired, returning a boolean -- ``True`` if the key
232         has expired.
233         
234         Key expiration is determined by a two-step process:
235         
236         1. If the user has already activated, the key will have been
237            reset to the string constant ``ACTIVATED``. Re-activating
238            is not permitted, and so this method returns ``True`` in
239            this case.
240
241         2. Otherwise, the date the user signed up is incremented by
242            the number of days specified in the setting
243            ``ACCOUNT_ACTIVATION_DAYS`` (which should be the number of
244            days after signup during which a user is allowed to
245            activate their account); if the result is less than or
246            equal to the current date, the key has expired and this
247            method returns ``True``.
248         
249         """
250         expiration_date = datetime.timedelta(days=settings.ACCOUNT_ACTIVATION_DAYS)
251         return self.activation_key == self.ACTIVATED or \
252                (self.user.date_joined + expiration_date <= datetime_now())
253     activation_key_expired.boolean = True
254
255     def send_activation_email(self, site):
256         """
257         Send an activation email to the user associated with this
258         ``RegistrationProfile``.
259         
260         The activation email will make use of two templates:
261
262         ``user_register_email_subject.txt``
263             This template will be used for the subject line of the
264             email. Because it is used as the subject line of an email,
265             this template's output **must** be only a single line of
266             text; output longer than one line will be forcibly joined
267             into only a single line.
268
269         ``user_register_email.txt``
270             This template will be used for the body of the email.
271
272         These templates will each receive the following context
273         variables:
274
275         ``activation_key``
276             The activation key for the new account.
277
278         ``expiration_days``
279             The number of days remaining during which the account may
280             be activated.
281
282         ``site``
283             An object representing the site on which the user
284             registered; depending on whether ``django.contrib.sites``
285             is installed, this may be an instance of either
286             ``django.contrib.sites.models.Site`` (if the sites
287             application is installed) or
288             ``django.contrib.sites.models.RequestSite`` (if
289             not). Consult the documentation for the Django sites
290             framework for details regarding these objects' interfaces.
291
292         """
293         ctx_dict = {'activation_key': self.activation_key,
294                     'expiration_days': settings.ACCOUNT_ACTIVATION_DAYS,
295                     'site': site}
296         subject = render_to_string('user_register_email_subject.txt',
297                                    ctx_dict)
298         # Email subject *must not* contain newlines
299         subject = ''.join(subject.splitlines())
300
301         message = render_to_string('user_register_email.txt',
302                                    ctx_dict)
303
304         send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [self.email])
305
306
307
308
309 class PendingSlice(models.Model):
310     slice_name  = models.TextField()