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

import hashlib
35
import uuid
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
36
import logging
37
import json
38
39
40
41

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

45
from django.db import models, IntegrityError
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
46
from django.contrib.auth.models import User, UserManager, Group
47
48
from django.utils.translation import ugettext as _
from django.core.exceptions import ValidationError
49
50
51
from django.template.loader import render_to_string
from django.core.mail import send_mail
from django.db import transaction
52

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

QUEUE_CLIENT_ID = 3 # Astakos.
58

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
59
60
logger = logging.getLogger(__name__)

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

68
69
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
    provider = models.CharField('Provider', max_length=255, blank=True)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
70

71
    #for invitations
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
72
    user_level = DEFAULT_USER_LEVEL
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
73
    level = models.IntegerField('Inviter level', default=user_level)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
74
    invitations = models.IntegerField('Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
75

76
    auth_token = models.CharField('Authentication Token', max_length=32,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
77
78
79
                                  null=True, blank=True)
    auth_token_created = models.DateTimeField('Token creation date', null=True)
    auth_token_expires = models.DateTimeField('Token expiration date', null=True)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
80

81
    updated = models.DateTimeField('Update date')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
82
    is_verified = models.BooleanField('Is verified?', default=False)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
83

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
87
    email_verified = models.BooleanField('Email verified?', default=False)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
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
    
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
93
94
95
96
97
98
99
100
101
102
103
    __has_signed_terms = False
    __groupnames = []
    
    def __init__(self, *args, **kwargs):
        super(AstakosUser, self).__init__(*args, **kwargs)
        self.__has_signed_terms = self.has_signed_terms
        if self.id:
            self.__groupnames = [g.name for g in self.groups.all()]
        else:
            self.is_active = False
    
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
104
105
106
    @property
    def realname(self):
        return '%s %s' %(self.first_name, self.last_name)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
107

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

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

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

163
164
165
        self.auth_token = b64encode(md5.digest())
        self.auth_token_created = datetime.now()
        self.auth_token_expires = self.auth_token_created + \
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
166
                                  timedelta(hours=AUTH_TOKEN_DURATION)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
167

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

202
203
204
205
class ApprovalTerms(models.Model):
    """
    Model for approval terms
    """
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
206

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

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

233
    def __unicode__(self):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
234
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
235
236
237
238
239
240
241
242
243
244
245
246

def report_user_event(user):
    def should_send(user):
        # report event incase of new user instance
        # or if specific fields are modified
        if not user.id:
            return True
        db_instance = AstakosUser.objects.get(id = user.id)
        for f in BILLING_FIELDS:
            if (db_instance.__getattribute__(f) != user.__getattribute__(f)):
                return True
        return False
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
247

248
    if QUEUE_CONNECTION and should_send(user):
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
249
250
251
252
253

        from astakos.im.queue.userevent import UserEvent
        from synnefo.lib.queue import exchange_connect, exchange_send, \
                exchange_close

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

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

def get_latest_terms():
    try:
        term = ApprovalTerms.objects.order_by('-id')[0]
        return term
    except IndexError:
        pass
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
    return None

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

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

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

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

        After successful email change the activation record is deleted.

        Throws ValueError if there is already
        """
        try:
            email_change = self.model.objects.get(activation_key=activation_key)
            if email_change.activation_key_expired():
                email_change.delete()
                raise EmailChange.DoesNotExist
            # is there an active user with this address?
            try:
                AstakosUser.objects.get(email=email_change.new_email_address)
            except AstakosUser.DoesNotExist:
                pass
            else:
                raise ValueError(_('The new email address is reserved.'))
            # update user
            user = AstakosUser.objects.get(pk=email_change.user_id)
            user.email = email_change.new_email_address
            user.save()
            email_change.delete()
            return user
        except EmailChange.DoesNotExist:
            raise ValueError(_('Invalid activation key'))

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

    objects = EmailChangeManager()

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