Statistics
| Branch: | Tag: | Revision:

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

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

    
38
from time import asctime
39
from datetime import datetime, timedelta
40
from base64 import b64encode
41
from random import randint
42
from collections import defaultdict
43

    
44
from django.db import models
45
from django.contrib.auth.models import User, UserManager, Group
46
from django.utils.translation import ugettext as _
47
from django.core.exceptions import ValidationError
48
from django.db import transaction
49
from django.db.models.signals import pre_save, post_save, post_syncdb, post_delete
50
from django.dispatch import Signal
51
from django.db.models import Q
52

    
53
from astakos.im.settings import (DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
54
    AUTH_TOKEN_DURATION, BILLING_FIELDS, EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL
55
)
56
from astakos.im.endpoints.quotaholder import register_users, send_quota
57
from astakos.im.endpoints.aquarium.producer import report_user_event
58

    
59
from astakos.im.tasks import propagate_groupmembers_quota
60

    
61
logger = logging.getLogger(__name__)
62

    
63
class Service(models.Model):
64
    name = models.CharField('Name', max_length=255, unique=True, db_index=True)
65
    url = models.FilePathField()
66
    icon = models.FilePathField(blank=True)
67
    auth_token = models.CharField('Authentication Token', max_length=32,
68
                                  null=True, blank=True)
69
    auth_token_created = models.DateTimeField('Token creation date', null=True)
70
    auth_token_expires = models.DateTimeField('Token expiration date', null=True)
71
    
72
    def save(self, **kwargs):
73
        if not self.id:
74
            self.renew_token()
75
        self.full_clean()
76
        super(Service, self).save(**kwargs)
77
    
78
    def renew_token(self):
79
        md5 = hashlib.md5()
80
        md5.update(self.name.encode('ascii', 'ignore'))
81
        md5.update(self.url.encode('ascii', 'ignore'))
82
        md5.update(asctime())
83

    
84
        self.auth_token = b64encode(md5.digest())
85
        self.auth_token_created = datetime.now()
86
        self.auth_token_expires = self.auth_token_created + \
87
                                  timedelta(hours=AUTH_TOKEN_DURATION)
88
    
89
    def __str__(self):
90
        return self.name
91

    
92
class ResourceMetadata(models.Model):
93
    key = models.CharField('Name', max_length=255, unique=True, db_index=True)
94
    value = models.CharField('Value', max_length=255)
95

    
96
class Resource(models.Model):
97
    name = models.CharField('Name', max_length=255, unique=True, db_index=True)
98
    meta = models.ManyToManyField(ResourceMetadata)
99
    service = models.ForeignKey(Service)
100
    
101
    def __str__(self):
102
        return '%s : %s' % (self.service, self.name)
103

    
104
class GroupKind(models.Model):
105
    name = models.CharField('Name', max_length=255, unique=True, db_index=True)
106
    
107
    def __str__(self):
108
        return self.name
109

    
110
class AstakosGroup(Group):
111
    kind = models.ForeignKey(GroupKind)
112
    desc = models.TextField('Description', null=True)
113
    policy = models.ManyToManyField(Resource, null=True, blank=True,
114
        through='AstakosGroupQuota'
115
    )
116
    creation_date = models.DateTimeField('Creation date',
117
        default=datetime.now()
118
    )
119
    issue_date = models.DateTimeField('Issue date', null=True)
120
    expiration_date = models.DateTimeField('Expiration date', null=True)
121
    moderation_enabled = models.BooleanField('Moderated membership?',
122
        default=True
123
    )
124
    approval_date = models.DateTimeField('Activation date', null=True,
125
        blank=True
126
    )
127
    estimated_participants = models.PositiveIntegerField('Estimated #members',
128
        null=True
129
    )
130
    
131
    @property
132
    def is_disabled(self):
133
        if not self.approval_date:
134
            return True
135
        return False
136
    
137
    @property
138
    def is_enabled(self):
139
        if self.is_disabled:
140
            return False
141
        if not self.issue_date:
142
            return False
143
        if not self.expiration_date:
144
            return True
145
        now = datetime.now()
146
        if self.issue_date > now:
147
            return False
148
        if now >= self.expiration_date:
149
            return False
150
        return True
151
    
152
    def enable(self):
153
        if self.is_enabled:
154
            return
155
        self.approval_date = datetime.now()
156
        self.save()
157
        quota_disturbed.send(sender=self, users=self.approved_members)
158
        propagate_groupmembers_quota.apply_async(args=[self], eta=self.issue_date)
159
        propagate_groupmembers_quota.apply_async(args=[self], eta=self.expiration_date)
160
    
161
    def disable(self):
162
        if self.is_disabled:
163
            return
164
        self.approval_date = None
165
        self.save()
166
        quota_disturbed.send(sender=self, users=self.approved_members)
167
    
168
    def approve_member(self, person):
169
        m, created = self.membership_set.get_or_create(person=person)
170
        # update date_joined in any case
171
        m.date_joined=datetime.now()
172
        m.save()
173
    
174
    def disapprove_member(self, person):
175
        self.membership_set.remove(person=person)
176
    
177
    @property
178
    def members(self):
179
        return [m.person for m in self.membership_set.all()]
180
    
181
    @property
182
    def approved_members(self):
183
        return [m.person for m in self.membership_set.all() if m.is_approved]
184
    
185
    @property
186
    def quota(self):
187
        d = defaultdict(int)
188
        for q in self.astakosgroupquota_set.all():
189
            d[q.resource] += q.limit
190
        return d
191
    
192
    @property
193
    def owners(self):
194
        return self.owner.all()
195
    
196
    @owners.setter
197
    def owners(self, l):
198
        self.owner = l
199
        map(self.approve_member, l)
200

    
201
class AstakosUser(User):
202
    """
203
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
204
    """
205
    # Use UserManager to get the create_user method, etc.
206
    objects = UserManager()
207

    
208
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
209
    provider = models.CharField('Provider', max_length=255, blank=True)
210

    
211
    #for invitations
212
    user_level = DEFAULT_USER_LEVEL
213
    level = models.IntegerField('Inviter level', default=user_level)
214
    invitations = models.IntegerField('Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
215

    
216
    auth_token = models.CharField('Authentication Token', max_length=32,
217
                                  null=True, blank=True)
218
    auth_token_created = models.DateTimeField('Token creation date', null=True)
219
    auth_token_expires = models.DateTimeField('Token expiration date', null=True)
220

    
221
    updated = models.DateTimeField('Update date')
222
    is_verified = models.BooleanField('Is verified?', default=False)
223

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

    
227
    email_verified = models.BooleanField('Email verified?', default=False)
228

    
229
    has_credits = models.BooleanField('Has credits?', default=False)
230
    has_signed_terms = models.BooleanField('Agree with the terms?', default=False)
231
    date_signed_terms = models.DateTimeField('Signed terms date', null=True, blank=True)
232
    
233
    activation_sent = models.DateTimeField('Activation sent data', null=True, blank=True)
234
    
235
    policy = models.ManyToManyField(Resource, null=True, through='AstakosUserQuota')
236
    
237
    astakos_groups = models.ManyToManyField(AstakosGroup, verbose_name=_('agroups'), blank=True,
238
        help_text=_("In addition to the permissions manually assigned, this user will also get all permissions granted to each group he/she is in."),
239
        through='Membership')
240
    
241
    __has_signed_terms = False
242
    
243
    owner = models.ManyToManyField(AstakosGroup, related_name='owner', null=True)
244
    
245
    class Meta:
246
        unique_together = ("provider", "third_party_identifier")
247
    
248
    def __init__(self, *args, **kwargs):
249
        super(AstakosUser, self).__init__(*args, **kwargs)
250
        self.__has_signed_terms = self.has_signed_terms
251
        if not self.id:
252
            self.is_active = False
253
    
254
    @property
255
    def realname(self):
256
        return '%s %s' %(self.first_name, self.last_name)
257

    
258
    @realname.setter
259
    def realname(self, value):
260
        parts = value.split(' ')
261
        if len(parts) == 2:
262
            self.first_name = parts[0]
263
            self.last_name = parts[1]
264
        else:
265
            self.last_name = parts[0]
266

    
267
    @property
268
    def invitation(self):
269
        try:
270
            return Invitation.objects.get(username=self.email)
271
        except Invitation.DoesNotExist:
272
            return None
273
    
274
    @property
275
    def quota(self):
276
        d = defaultdict(int)
277
        for q in  self.astakosuserquota_set.all():
278
            d[q.resource.name] += q.limit
279
        for m in self.membership_set.all():
280
            if not m.is_approved:
281
                continue
282
            g = m.group
283
            if not g.is_enabled:
284
                continue
285
            for r, limit in g.quota.iteritems():
286
                d[r] += limit
287
        # TODO set default for remaining
288
        return d
289
        
290
    def save(self, update_timestamps=True, **kwargs):
291
        if update_timestamps:
292
            if not self.id:
293
                self.date_joined = datetime.now()
294
            self.updated = datetime.now()
295
        
296
        # update date_signed_terms if necessary
297
        if self.__has_signed_terms != self.has_signed_terms:
298
            self.date_signed_terms = datetime.now()
299
        
300
        if not self.id:
301
            # set username
302
            while not self.username:
303
                username =  uuid.uuid4().hex[:30]
304
                try:
305
                    AstakosUser.objects.get(username = username)
306
                except AstakosUser.DoesNotExist:
307
                    self.username = username
308
            if not self.provider:
309
                self.provider = 'local'
310
        self.validate_unique_email_isactive()
311
        if self.is_active and self.activation_sent:
312
            # reset the activation sent
313
            self.activation_sent = None
314
        
315
        super(AstakosUser, self).save(**kwargs)
316
    
317
    def renew_token(self):
318
        md5 = hashlib.md5()
319
        md5.update(self.username)
320
        md5.update(self.realname.encode('ascii', 'ignore'))
321
        md5.update(asctime())
322

    
323
        self.auth_token = b64encode(md5.digest())
324
        self.auth_token_created = datetime.now()
325
        self.auth_token_expires = self.auth_token_created + \
326
                                  timedelta(hours=AUTH_TOKEN_DURATION)
327
        msg = 'Token renewed for %s' % self.email
328
        logger.log(LOGGING_LEVEL, msg)
329

    
330
    def __unicode__(self):
331
        return '%s (%s)' % (self.realname, self.email)
332
    
333
    def conflicting_email(self):
334
        q = AstakosUser.objects.exclude(username = self.username)
335
        q = q.filter(email = self.email)
336
        if q.count() != 0:
337
            return True
338
        return False
339
    
340
    def validate_unique_email_isactive(self):
341
        """
342
        Implements a unique_together constraint for email and is_active fields.
343
        """
344
        q = AstakosUser.objects.exclude(username = self.username)
345
        q = q.filter(email = self.email)
346
        q = q.filter(is_active = self.is_active)
347
        if q.count() != 0:
348
            raise ValidationError({'__all__':[_('Another account with the same email & is_active combination found.')]})
349
    
350
    @property
351
    def signed_terms(self):
352
        term = get_latest_terms()
353
        if not term:
354
            return True
355
        if not self.has_signed_terms:
356
            return False
357
        if not self.date_signed_terms:
358
            return False
359
        if self.date_signed_terms < term.date:
360
            self.has_signed_terms = False
361
            self.date_signed_terms = None
362
            self.save()
363
            return False
364
        return True
365

    
366
class Membership(models.Model):
367
    person = models.ForeignKey(AstakosUser)
368
    group = models.ForeignKey(AstakosGroup)
369
    date_requested = models.DateField(default=datetime.now(), blank=True)
370
    date_joined = models.DateField(null=True, db_index=True, blank=True)
371
    
372
    class Meta:
373
        unique_together = ("person", "group")
374
    
375
    def save(self, *args, **kwargs):
376
        if not self.id:
377
            if not self.group.moderation_enabled:
378
                self.date_joined = datetime.now()
379
        super(Membership, self).save(*args, **kwargs)
380
    
381
    @property
382
    def is_approved(self):
383
        if self.date_joined:
384
            return True
385
        return False
386
    
387
    def approve(self):
388
        self.date_joined = datetime.now()
389
        self.save()
390
        quota_disturbed.send(sender=self, users=(self.person,))
391
    
392
    def disapprove(self):
393
        self.delete()
394
        quota_disturbed.send(sender=self, users=(self.person,))
395

    
396
class AstakosGroupQuota(models.Model):
397
    limit = models.PositiveIntegerField('Limit')
398
    resource = models.ForeignKey(Resource)
399
    group = models.ForeignKey(AstakosGroup, blank=True)
400
    
401
    class Meta:
402
        unique_together = ("resource", "group")
403

    
404
class AstakosUserQuota(models.Model):
405
    limit = models.PositiveIntegerField('Limit')
406
    resource = models.ForeignKey(Resource)
407
    user = models.ForeignKey(AstakosUser)
408
    
409
    class Meta:
410
        unique_together = ("resource", "user")
411

    
412
class ApprovalTerms(models.Model):
413
    """
414
    Model for approval terms
415
    """
416

    
417
    date = models.DateTimeField('Issue date', db_index=True, default=datetime.now())
418
    location = models.CharField('Terms location', max_length=255)
419

    
420
class Invitation(models.Model):
421
    """
422
    Model for registring invitations
423
    """
424
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
425
                                null=True)
426
    realname = models.CharField('Real name', max_length=255)
427
    username = models.CharField('Unique ID', max_length=255, unique=True)
428
    code = models.BigIntegerField('Invitation code', db_index=True)
429
    is_consumed = models.BooleanField('Consumed?', default=False)
430
    created = models.DateTimeField('Creation date', auto_now_add=True)
431
    consumed = models.DateTimeField('Consumption date', null=True, blank=True)
432
    
433
    def __init__(self, *args, **kwargs):
434
        super(Invitation, self).__init__(*args, **kwargs)
435
        if not self.id:
436
            self.code = _generate_invitation_code()
437
    
438
    def consume(self):
439
        self.is_consumed = True
440
        self.consumed = datetime.now()
441
        self.save()
442

    
443
    def __unicode__(self):
444
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
445

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

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

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

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

461
        After successful email change the activation record is deleted.
462

463
        Throws ValueError if there is already
464
        """
465
        try:
466
            email_change = self.model.objects.get(activation_key=activation_key)
467
            if email_change.activation_key_expired():
468
                email_change.delete()
469
                raise EmailChange.DoesNotExist
470
            # is there an active user with this address?
471
            try:
472
                AstakosUser.objects.get(email=email_change.new_email_address)
473
            except AstakosUser.DoesNotExist:
474
                pass
475
            else:
476
                raise ValueError(_('The new email address is reserved.'))
477
            # update user
478
            user = AstakosUser.objects.get(pk=email_change.user_id)
479
            user.email = email_change.new_email_address
480
            user.save()
481
            email_change.delete()
482
            return user
483
        except EmailChange.DoesNotExist:
484
            raise ValueError(_('Invalid activation key'))
485

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

    
492
    objects = EmailChangeManager()
493

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

    
498
class AdditionalMail(models.Model):
499
    """
500
    Model for registring invitations
501
    """
502
    owner = models.ForeignKey(AstakosUser)
503
    email = models.EmailField()
504

    
505
def _generate_invitation_code():
506
    while True:
507
        code = randint(1, 2L**63 - 1)
508
        try:
509
            Invitation.objects.get(code=code)
510
            # An invitation with this code already exists, try again
511
        except Invitation.DoesNotExist:
512
            return code
513

    
514
def get_latest_terms():
515
    try:
516
        term = ApprovalTerms.objects.order_by('-id')[0]
517
        return term
518
    except IndexError:
519
        pass
520
    return None
521

    
522
def create_astakos_user(u):
523
    try:
524
        AstakosUser.objects.get(user_ptr=u.pk)
525
    except AstakosUser.DoesNotExist:
526
        extended_user = AstakosUser(user_ptr_id=u.pk)
527
        extended_user.__dict__.update(u.__dict__)
528
        extended_user.renew_token()
529
        extended_user.save()
530
    except BaseException, e:
531
        logger.exception(e)
532
        pass
533

    
534
def fix_superusers(sender, **kwargs):
535
    # Associate superusers with AstakosUser
536
    admins = User.objects.filter(is_superuser=True)
537
    for u in admins:
538
        create_astakos_user(u)
539

    
540
def user_post_save(sender, instance, created, **kwargs):
541
    if not created:
542
        return
543
    create_astakos_user(instance)
544

    
545
def set_default_group(user):
546
    try:
547
        default = AstakosGroup.objects.get(name = 'default')
548
        Membership(group=default, person=user, date_joined=datetime.now()).save()
549
    except AstakosGroup.DoesNotExist, e:
550
        logger.exception(e)
551

    
552
def astakosuser_pre_save(sender, instance, **kwargs):
553
    instance.aquarium_report = False
554
    instance.new = False
555
    try:
556
        db_instance = AstakosUser.objects.get(id = instance.id)
557
    except AstakosUser.DoesNotExist:
558
        # create event
559
        instance.aquarium_report = True
560
        instance.new = True
561
    else:
562
        get = AstakosUser.__getattribute__
563
        l = filter(lambda f: get(db_instance, f) != get(instance, f),
564
            BILLING_FIELDS
565
        )
566
        instance.aquarium_report = True if l else False
567

    
568
def astakosuser_post_save(sender, instance, created, **kwargs):
569
    if instance.aquarium_report:
570
        report_user_event(instance, create=instance.new)
571
    if not created:
572
        return
573
    set_default_group(instance)
574
    # TODO handle socket.error & IOError
575
    register_users((instance,))
576

    
577
def send_quota_disturbed(sender, instance, **kwargs):
578
    users = []
579
    extend = users.extend
580
    if sender == Membership:
581
        if not instance.group.is_enabled:
582
            return
583
        extend([instance.person])
584
    elif sender == AstakosUserQuota:
585
        extend([instance.user])
586
    elif sender == AstakosGroupQuota:
587
        if not instance.group.is_enabled:
588
            return
589
        extend(instance.group.astakosuser_set.all())
590
    elif sender == AstakosGroup:
591
        if not instance.is_enabled:
592
            return
593
    quota_disturbed.send(sender=sender, users=users)
594

    
595
def on_quota_disturbed(sender, users, **kwargs):
596
    print '>>>', locals()
597
    if not users:
598
        return
599
    send_quota(users)
600

    
601
post_syncdb.connect(fix_superusers)
602
post_save.connect(user_post_save, sender=User)
603
pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
604
post_save.connect(astakosuser_post_save, sender=AstakosUser)
605

    
606
quota_disturbed = Signal(providing_args=["users"])
607
quota_disturbed.connect(on_quota_disturbed)
608

    
609
post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
610
post_delete.connect(send_quota_disturbed, sender=Membership)
611
post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
612
post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
613
post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
614
post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)