Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ ff9290ec

History | View | Annotate | Download (12.9 kB)

1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
import hashlib
35
import uuid
36
import logging
37
import json
38

    
39
from time import asctime
40
from datetime import datetime, timedelta
41
from base64 import b64encode
42
from urlparse import urlparse, urlunparse
43
from random import randint
44

    
45
from django.db import models, IntegrityError
46
from django.contrib.auth.models import User, UserManager, Group
47
from django.utils.translation import ugettext as _
48
from django.core.exceptions import ValidationError
49
from django.template.loader import render_to_string
50
from django.core.mail import send_mail
51
from django.db import transaction
52
from django.db.models.signals import post_save, post_syncdb
53

    
54
from astakos.im.settings import DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL, \
55
AUTH_TOKEN_DURATION, BILLING_FIELDS, QUEUE_CONNECTION, SITENAME, \
56
EMAILCHANGE_ACTIVATION_DAYS
57

    
58
QUEUE_CLIENT_ID = 3 # Astakos.
59

    
60
logger = logging.getLogger(__name__)
61

    
62
class AstakosUser(User):
63
    """
64
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
65
    """
66
    # Use UserManager to get the create_user method, etc.
67
    objects = UserManager()
68

    
69
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
70
    provider = models.CharField('Provider', max_length=255, blank=True)
71

    
72
    #for invitations
73
    user_level = DEFAULT_USER_LEVEL
74
    level = models.IntegerField('Inviter level', default=user_level)
75
    invitations = models.IntegerField('Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
76

    
77
    auth_token = models.CharField('Authentication Token', max_length=32,
78
                                  null=True, blank=True)
79
    auth_token_created = models.DateTimeField('Token creation date', null=True)
80
    auth_token_expires = models.DateTimeField('Token expiration date', null=True)
81

    
82
    updated = models.DateTimeField('Update date')
83
    is_verified = models.BooleanField('Is verified?', default=False)
84

    
85
    # ex. screen_name for twitter, eppn for shibboleth
86
    third_party_identifier = models.CharField('Third-party identifier', max_length=255, null=True, blank=True)
87

    
88
    email_verified = models.BooleanField('Email verified?', default=False)
89

    
90
    has_credits = models.BooleanField('Has credits?', default=False)
91
    has_signed_terms = models.BooleanField('Agree with the terms?', default=False)
92
    date_signed_terms = models.DateTimeField('Signed terms date', null=True, blank=True)
93
    
94
    __has_signed_terms = False
95
    __groupnames = []
96
    
97
    def __init__(self, *args, **kwargs):
98
        super(AstakosUser, self).__init__(*args, **kwargs)
99
        self.__has_signed_terms = self.has_signed_terms
100
        if self.id:
101
            self.__groupnames = [g.name for g in self.groups.all()]
102
        else:
103
            self.is_active = False
104
    
105
    @property
106
    def realname(self):
107
        return '%s %s' %(self.first_name, self.last_name)
108

    
109
    @realname.setter
110
    def realname(self, value):
111
        parts = value.split(' ')
112
        if len(parts) == 2:
113
            self.first_name = parts[0]
114
            self.last_name = parts[1]
115
        else:
116
            self.last_name = parts[0]
117

    
118
    @property
119
    def invitation(self):
120
        try:
121
            return Invitation.objects.get(username=self.email)
122
        except Invitation.DoesNotExist:
123
            return None
124

    
125
    def save(self, update_timestamps=True, **kwargs):
126
        if update_timestamps:
127
            if not self.id:
128
                self.date_joined = datetime.now()
129
            self.updated = datetime.now()
130
        
131
        # update date_signed_terms if necessary
132
        if self.__has_signed_terms != self.has_signed_terms:
133
            self.date_signed_terms = datetime.now()
134
        
135
        if not self.id:
136
            # set username
137
            while not self.username:
138
                username =  uuid.uuid4().hex[:30]
139
                try:
140
                    AstakosUser.objects.get(username = username)
141
                except AstakosUser.DoesNotExist, e:
142
                    self.username = username
143
            if not self.provider:
144
                self.provider = 'local'
145
        report_user_event(self)
146
        self.validate_unique_email_isactive()
147
        super(AstakosUser, self).save(**kwargs)
148
        
149
        # set group if does not exist
150
        groupname = 'shibboleth' if self.provider == 'shibboleth' else 'default'
151
        if groupname not in self.__groupnames:
152
            try:
153
                group = Group.objects.get(name = groupname)
154
                self.groups.add(group)
155
            except Group.DoesNotExist, e:
156
                logger.exception(e)
157
    
158
    def renew_token(self):
159
        md5 = hashlib.md5()
160
        md5.update(self.username)
161
        md5.update(self.realname.encode('ascii', 'ignore'))
162
        md5.update(asctime())
163

    
164
        self.auth_token = b64encode(md5.digest())
165
        self.auth_token_created = datetime.now()
166
        self.auth_token_expires = self.auth_token_created + \
167
                                  timedelta(hours=AUTH_TOKEN_DURATION)
168

    
169
    def __unicode__(self):
170
        return self.username
171
    
172
    def conflicting_email(self):
173
        q = AstakosUser.objects.exclude(username = self.username)
174
        q = q.filter(email = self.email)
175
        if q.count() != 0:
176
            return True
177
        return False
178
    
179
    def validate_unique_email_isactive(self):
180
        """
181
        Implements a unique_together constraint for email and is_active fields.
182
        """
183
        q = AstakosUser.objects.exclude(username = self.username)
184
        q = q.filter(email = self.email)
185
        q = q.filter(is_active = self.is_active)
186
        if q.count() != 0:
187
            raise ValidationError({'__all__':[_('Another account with the same email & is_active combination found.')]})
188
    
189
    def signed_terms(self):
190
        term = get_latest_terms()
191
        if not term:
192
            return True
193
        if not self.has_signed_terms:
194
            return False
195
        if not self.date_signed_terms:
196
            return False
197
        if self.date_signed_terms < term.date:
198
            self.has_signed_terms = False
199
            self.date_signed_terms = None
200
            self.save()
201
            return False
202
        return True
203

    
204
class ApprovalTerms(models.Model):
205
    """
206
    Model for approval terms
207
    """
208

    
209
    date = models.DateTimeField('Issue date', db_index=True, default=datetime.now())
210
    location = models.CharField('Terms location', max_length=255)
211

    
212
class Invitation(models.Model):
213
    """
214
    Model for registring invitations
215
    """
216
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
217
                                null=True)
218
    realname = models.CharField('Real name', max_length=255)
219
    username = models.CharField('Unique ID', max_length=255, unique=True)
220
    code = models.BigIntegerField('Invitation code', db_index=True)
221
    is_consumed = models.BooleanField('Consumed?', default=False)
222
    created = models.DateTimeField('Creation date', auto_now_add=True)
223
    consumed = models.DateTimeField('Consumption date', null=True, blank=True)
224
    
225
    def __init__(self, *args, **kwargs):
226
        super(Invitation, self).__init__(*args, **kwargs)
227
        if not self.id:
228
            self.code = _generate_invitation_code()
229
    
230
    def consume(self):
231
        self.is_consumed = True
232
        self.consumed = datetime.now()
233
        self.save()
234

    
235
    def __unicode__(self):
236
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
237

    
238
def report_user_event(user):
239
    def should_send(user):
240
        # report event incase of new user instance
241
        # or if specific fields are modified
242
        if not user.id:
243
            return True
244
        db_instance = AstakosUser.objects.get(id = user.id)
245
        for f in BILLING_FIELDS:
246
            if (db_instance.__getattribute__(f) != user.__getattribute__(f)):
247
                return True
248
        return False
249

    
250
    if QUEUE_CONNECTION and should_send(user):
251

    
252
        from astakos.im.queue.userevent import UserEvent
253
        from synnefo.lib.queue import exchange_connect, exchange_send, \
254
                exchange_close
255

    
256
        eventType = 'create' if not user.id else 'modify'
257
        body = UserEvent(QUEUE_CLIENT_ID, user, eventType, {}).format()
258
        conn = exchange_connect(QUEUE_CONNECTION)
259
        parts = urlparse(QUEUE_CONNECTION)
260
        exchange = parts.path[1:]
261
        routing_key = '%s.user' % exchange
262
        exchange_send(conn, routing_key, body)
263
        exchange_close(conn)
264

    
265
def _generate_invitation_code():
266
    while True:
267
        code = randint(1, 2L**63 - 1)
268
        try:
269
            Invitation.objects.get(code=code)
270
            # An invitation with this code already exists, try again
271
        except Invitation.DoesNotExist:
272
            return code
273

    
274
def get_latest_terms():
275
    try:
276
        term = ApprovalTerms.objects.order_by('-id')[0]
277
        return term
278
    except IndexError:
279
        pass
280
    return None
281

    
282
class EmailChangeManager(models.Manager):
283
    @transaction.commit_on_success
284
    def change_email(self, activation_key):
285
        """
286
        Validate an activation key and change the corresponding
287
        ``User`` if valid.
288

289
        If the key is valid and has not expired, return the ``User``
290
        after activating.
291

292
        If the key is not valid or has expired, return ``None``.
293

294
        If the key is valid but the ``User`` is already active,
295
        return ``None``.
296

297
        After successful email change the activation record is deleted.
298

299
        Throws ValueError if there is already
300
        """
301
        try:
302
            email_change = self.model.objects.get(activation_key=activation_key)
303
            if email_change.activation_key_expired():
304
                email_change.delete()
305
                raise EmailChange.DoesNotExist
306
            # is there an active user with this address?
307
            try:
308
                AstakosUser.objects.get(email=email_change.new_email_address)
309
            except AstakosUser.DoesNotExist:
310
                pass
311
            else:
312
                raise ValueError(_('The new email address is reserved.'))
313
            # update user
314
            user = AstakosUser.objects.get(pk=email_change.user_id)
315
            user.email = email_change.new_email_address
316
            user.save()
317
            email_change.delete()
318
            return user
319
        except EmailChange.DoesNotExist:
320
            raise ValueError(_('Invalid activation key'))
321

    
322
class EmailChange(models.Model):
323
    new_email_address = models.EmailField(_(u'new e-mail address'), help_text=_(u'Your old email address will be used until you verify your new one.'))
324
    user = models.ForeignKey(AstakosUser, unique=True, related_name='emailchange_user')
325
    requested_at = models.DateTimeField(default=datetime.now())
326
    activation_key = models.CharField(max_length=40, unique=True, db_index=True)
327

    
328
    objects = EmailChangeManager()
329

    
330
    def activation_key_expired(self):
331
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
332
        return self.requested_at + expiration_date < datetime.now()
333

    
334
def create_astakos_user(u):
335
    try:
336
        AstakosUser.objects.get(user_ptr=u.pk)
337
    except AstakosUser.DoesNotExist:
338
        extended_user = AstakosUser(user_ptr_id=u.pk)
339
        extended_user.__dict__.update(u.__dict__)
340
        extended_user.renew_token()
341
        extended_user.save()
342
    except:
343
        pass
344

    
345
def superuser_post_syncdb(sender, **kwargs):
346
    # if there was created a superuser
347
    # associate it with an AstakosUser
348
    admins = User.objects.filter(is_superuser=True)
349
    for u in admins:
350
        create_astakos_user(u)
351

    
352
post_syncdb.connect(superuser_post_syncdb)
353

    
354
def superuser_post_save(sender, instance, **kwargs):
355
    if instance.is_superuser:
356
        create_astakos_user(instance)
357

    
358
post_save.connect(superuser_post_save, sender=User)