Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (12.1 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

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

    
57
QUEUE_CLIENT_ID = 3 # Astakos.
58

    
59
logger = logging.getLogger(__name__)
60

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
249
    if QUEUE_CONNECTION and should_send(user):
250

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

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

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

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

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

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

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

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

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

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

    
321
class EmailChange(models.Model):
322
    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.'))
323
    user = models.ForeignKey(AstakosUser, unique=True, related_name='emailchange_user')
324
    requested_at = models.DateTimeField(default=datetime.now())
325
    activation_key = models.CharField(max_length=40, unique=True, db_index=True)
326

    
327
    objects = EmailChangeManager()
328

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