Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 9eafaa32

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

    
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,
55
                                 EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL)
56
from astakos.im.endpoints.quotaholder import (register_users, send_quota,
57
                                              register_resources)
58
from astakos.im.endpoints.aquarium.producer import report_user_event
59

    
60
from astakos.im.tasks import propagate_groupmembers_quota
61

    
62
logger = logging.getLogger(__name__)
63

    
64

    
65
class Service(models.Model):
66
    name = models.CharField('Name', max_length=255, unique=True, db_index=True)
67
    url = models.FilePathField()
68
    icon = models.FilePathField(blank=True)
69
    auth_token = models.CharField('Authentication Token', max_length=32,
70
                                  null=True, blank=True)
71
    auth_token_created = models.DateTimeField('Token creation date', null=True)
72
    auth_token_expires = models.DateTimeField(
73
        'Token expiration date', null=True)
74

    
75
    def save(self, **kwargs):
76
        if not self.id:
77
            self.renew_token()
78
        self.full_clean()
79
        super(Service, self).save(**kwargs)
80

    
81
    def renew_token(self):
82
        md5 = hashlib.md5()
83
        md5.update(self.name.encode('ascii', 'ignore'))
84
        md5.update(self.url.encode('ascii', 'ignore'))
85
        md5.update(asctime())
86

    
87
        self.auth_token = b64encode(md5.digest())
88
        self.auth_token_created = datetime.now()
89
        self.auth_token_expires = self.auth_token_created + \
90
            timedelta(hours=AUTH_TOKEN_DURATION)
91

    
92
    def __str__(self):
93
        return self.name
94

    
95

    
96
class ResourceMetadata(models.Model):
97
    key = models.CharField('Name', max_length=255, unique=True, db_index=True)
98
    value = models.CharField('Value', max_length=255)
99

    
100

    
101
class Resource(models.Model):
102
    name = models.CharField('Name', max_length=255, unique=True, db_index=True)
103
    meta = models.ManyToManyField(ResourceMetadata)
104
    service = models.ForeignKey(Service)
105

    
106
    def __str__(self):
107
        return '%s.%s' % (self.service, self.name)
108

    
109

    
110
class GroupKind(models.Model):
111
    name = models.CharField('Name', max_length=255, unique=True, db_index=True)
112

    
113
    def __str__(self):
114
        return self.name
115

    
116

    
117
class AstakosGroup(Group):
118
    kind = models.ForeignKey(GroupKind)
119
    homepage = models.URLField(
120
        'Homepage Url', max_length=255, null=True, blank=True)
121
    desc = models.TextField('Description', null=True)
122
    policy = models.ManyToManyField(Resource, null=True, blank=True,
123
                                    through='AstakosGroupQuota')
124
    creation_date = models.DateTimeField('Creation date',
125
                                         default=datetime.now())
126
    issue_date = models.DateTimeField('Issue date', null=True)
127
    expiration_date = models.DateTimeField('Expiration date', null=True)
128
    moderation_enabled = models.BooleanField('Moderated membership?',
129
                                             default=True)
130
    approval_date = models.DateTimeField('Activation date', null=True,
131
                                         blank=True)
132
    estimated_participants = models.PositiveIntegerField('Estimated #members',
133
                                                         null=True)
134

    
135
    @property
136
    def is_disabled(self):
137
        if not self.approval_date:
138
            return True
139
        return False
140

    
141
    @property
142
    def is_enabled(self):
143
        if self.is_disabled:
144
            return False
145
        if not self.issue_date:
146
            return False
147
        if not self.expiration_date:
148
            return True
149
        now = datetime.now()
150
        if self.issue_date > now:
151
            return False
152
        if now >= self.expiration_date:
153
            return False
154
        return True
155

    
156
    def enable(self):
157
        if self.is_enabled:
158
            return
159
        self.approval_date = datetime.now()
160
        self.save()
161
        quota_disturbed.send(sender=self, users=self.approved_members)
162
        propagate_groupmembers_quota.apply_async(
163
            args=[self], eta=self.issue_date)
164
        propagate_groupmembers_quota.apply_async(
165
            args=[self], eta=self.expiration_date)
166

    
167
    def disable(self):
168
        if self.is_disabled:
169
            return
170
        self.approval_date = None
171
        self.save()
172
        quota_disturbed.send(sender=self, users=self.approved_members)
173

    
174
    def approve_member(self, person):
175
        m, created = self.membership_set.get_or_create(person=person)
176
        # update date_joined in any case
177
        m.date_joined = datetime.now()
178
        m.save()
179

    
180
    def disapprove_member(self, person):
181
        self.membership_set.remove(person=person)
182

    
183
    @property
184
    def members(self):
185
        q = self.membership_set.select_related().all()
186
        return [m.person for m in q]
187

    
188
    @property
189
    def approved_members(self):
190
        q = self.membership_set.select_related().all()
191
        return [m.person for m in q if m.is_approved]
192

    
193
    @property
194
    def quota(self):
195
        d = defaultdict(int)
196
        for q in self.astakosgroupquota_set.select_related().all():
197
            d[q.resource] += q.uplimit
198
        return d
199

    
200
    @property
201
    def owners(self):
202
        return self.owner.all()
203

    
204
    @property
205
    def owner_details(self):
206
        return self.owner.select_related().all()
207

    
208
    @owners.setter
209
    def owners(self, l):
210
        self.owner = l
211
        map(self.approve_member, l)
212

    
213

    
214
class AstakosUser(User):
215
    """
216
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
217
    """
218
    # Use UserManager to get the create_user method, etc.
219
    objects = UserManager()
220

    
221
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
222
    provider = models.CharField('Provider', max_length=255, blank=True)
223

    
224
    #for invitations
225
    user_level = DEFAULT_USER_LEVEL
226
    level = models.IntegerField('Inviter level', default=user_level)
227
    invitations = models.IntegerField(
228
        'Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
229

    
230
    auth_token = models.CharField('Authentication Token', max_length=32,
231
                                  null=True, blank=True)
232
    auth_token_created = models.DateTimeField('Token creation date', null=True)
233
    auth_token_expires = models.DateTimeField(
234
        'Token expiration date', null=True)
235

    
236
    updated = models.DateTimeField('Update date')
237
    is_verified = models.BooleanField('Is verified?', default=False)
238

    
239
    # ex. screen_name for twitter, eppn for shibboleth
240
    third_party_identifier = models.CharField(
241
        'Third-party identifier', max_length=255, null=True, blank=True)
242

    
243
    email_verified = models.BooleanField('Email verified?', default=False)
244

    
245
    has_credits = models.BooleanField('Has credits?', default=False)
246
    has_signed_terms = models.BooleanField(
247
        'Agree with the terms?', default=False)
248
    date_signed_terms = models.DateTimeField(
249
        'Signed terms date', null=True, blank=True)
250

    
251
    activation_sent = models.DateTimeField(
252
        'Activation sent data', null=True, blank=True)
253

    
254
    policy = models.ManyToManyField(
255
        Resource, null=True, through='AstakosUserQuota')
256

    
257
    astakos_groups = models.ManyToManyField(
258
        AstakosGroup, verbose_name=_('agroups'), blank=True,
259
        help_text=_("In addition to the permissions manually assigned, this user will also get all permissions granted to each group he/she is in."),
260
        through='Membership')
261

    
262
    __has_signed_terms = False
263

    
264
    owner = models.ManyToManyField(
265
        AstakosGroup, related_name='owner', null=True)
266

    
267
    class Meta:
268
        unique_together = ("provider", "third_party_identifier")
269

    
270
    def __init__(self, *args, **kwargs):
271
        super(AstakosUser, self).__init__(*args, **kwargs)
272
        self.__has_signed_terms = self.has_signed_terms
273
        if not self.id:
274
            self.is_active = False
275

    
276
    @property
277
    def realname(self):
278
        return '%s %s' % (self.first_name, self.last_name)
279

    
280
    @realname.setter
281
    def realname(self, value):
282
        parts = value.split(' ')
283
        if len(parts) == 2:
284
            self.first_name = parts[0]
285
            self.last_name = parts[1]
286
        else:
287
            self.last_name = parts[0]
288

    
289
    @property
290
    def invitation(self):
291
        try:
292
            return Invitation.objects.get(username=self.email)
293
        except Invitation.DoesNotExist:
294
            return None
295

    
296
    @property
297
    def quota(self):
298
        d = defaultdict(int)
299
        for q in self.astakosuserquota_set.select_related().all():
300
            d[q.resource] += q.uplimit
301
        for m in self.membership_set.select_related().all():
302
            if not m.is_approved:
303
                continue
304
            g = m.group
305
            if not g.is_enabled:
306
                continue
307
            for r, uplimit in g.quota.iteritems():
308
                d[r] += uplimit
309
        
310
        # TODO set default for remaining
311
        return d
312

    
313
    def save(self, update_timestamps=True, **kwargs):
314
        if update_timestamps:
315
            if not self.id:
316
                self.date_joined = datetime.now()
317
            self.updated = datetime.now()
318

    
319
        # update date_signed_terms if necessary
320
        if self.__has_signed_terms != self.has_signed_terms:
321
            self.date_signed_terms = datetime.now()
322

    
323
        if not self.id:
324
            # set username
325
            while not self.username:
326
                username = uuid.uuid4().hex[:30]
327
                try:
328
                    AstakosUser.objects.get(username=username)
329
                except AstakosUser.DoesNotExist:
330
                    self.username = username
331
            if not self.provider:
332
                self.provider = 'local'
333
        self.validate_unique_email_isactive()
334
        if self.is_active and self.activation_sent:
335
            # reset the activation sent
336
            self.activation_sent = None
337

    
338
        super(AstakosUser, self).save(**kwargs)
339

    
340
    def renew_token(self):
341
        md5 = hashlib.md5()
342
        md5.update(self.username)
343
        md5.update(self.realname.encode('ascii', 'ignore'))
344
        md5.update(asctime())
345

    
346
        self.auth_token = b64encode(md5.digest())
347
        self.auth_token_created = datetime.now()
348
        self.auth_token_expires = self.auth_token_created + \
349
            timedelta(hours=AUTH_TOKEN_DURATION)
350
        msg = 'Token renewed for %s' % self.email
351
        logger.log(LOGGING_LEVEL, msg)
352

    
353
    def __unicode__(self):
354
        return '%s (%s)' % (self.realname, self.email)
355

    
356
    def conflicting_email(self):
357
        q = AstakosUser.objects.exclude(username=self.username)
358
        q = q.filter(email=self.email)
359
        if q.count() != 0:
360
            return True
361
        return False
362

    
363
    def validate_unique_email_isactive(self):
364
        """
365
        Implements a unique_together constraint for email and is_active fields.
366
        """
367
        q = AstakosUser.objects.exclude(username=self.username)
368
        q = q.filter(email=self.email)
369
        q = q.filter(is_active=self.is_active)
370
        if q.count() != 0:
371
            raise ValidationError({'__all__': [_('Another account with the same email & is_active combination found.')]})
372

    
373
    @property
374
    def signed_terms(self):
375
        term = get_latest_terms()
376
        if not term:
377
            return True
378
        if not self.has_signed_terms:
379
            return False
380
        if not self.date_signed_terms:
381
            return False
382
        if self.date_signed_terms < term.date:
383
            self.has_signed_terms = False
384
            self.date_signed_terms = None
385
            self.save()
386
            return False
387
        return True
388

    
389

    
390
class Membership(models.Model):
391
    person = models.ForeignKey(AstakosUser)
392
    group = models.ForeignKey(AstakosGroup)
393
    date_requested = models.DateField(default=datetime.now(), blank=True)
394
    date_joined = models.DateField(null=True, db_index=True, blank=True)
395

    
396
    class Meta:
397
        unique_together = ("person", "group")
398

    
399
    def save(self, *args, **kwargs):
400
        if not self.id:
401
            if not self.group.moderation_enabled:
402
                self.date_joined = datetime.now()
403
        super(Membership, self).save(*args, **kwargs)
404

    
405
    @property
406
    def is_approved(self):
407
        if self.date_joined:
408
            return True
409
        return False
410

    
411
    def approve(self):
412
        self.date_joined = datetime.now()
413
        self.save()
414
        quota_disturbed.send(sender=self, users=(self.person,))
415

    
416
    def disapprove(self):
417
        self.delete()
418
        quota_disturbed.send(sender=self, users=(self.person,))
419

    
420

    
421
class AstakosGroupQuota(models.Model):
422
    limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
423
    uplimit = models.BigIntegerField('Up limit', null=True)
424
    resource = models.ForeignKey(Resource)
425
    group = models.ForeignKey(AstakosGroup, blank=True)
426

    
427
    class Meta:
428
        unique_together = ("resource", "group")
429

    
430

    
431
class AstakosUserQuota(models.Model):
432
    limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
433
    uplimit = models.BigIntegerField('Up limit', null=True)
434
    resource = models.ForeignKey(Resource)
435
    user = models.ForeignKey(AstakosUser)
436

    
437
    class Meta:
438
        unique_together = ("resource", "user")
439

    
440

    
441
class ApprovalTerms(models.Model):
442
    """
443
    Model for approval terms
444
    """
445

    
446
    date = models.DateTimeField(
447
        'Issue date', db_index=True, default=datetime.now())
448
    location = models.CharField('Terms location', max_length=255)
449

    
450

    
451
class Invitation(models.Model):
452
    """
453
    Model for registring invitations
454
    """
455
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
456
                                null=True)
457
    realname = models.CharField('Real name', max_length=255)
458
    username = models.CharField('Unique ID', max_length=255, unique=True)
459
    code = models.BigIntegerField('Invitation code', db_index=True)
460
    is_consumed = models.BooleanField('Consumed?', default=False)
461
    created = models.DateTimeField('Creation date', auto_now_add=True)
462
    consumed = models.DateTimeField('Consumption date', null=True, blank=True)
463

    
464
    def __init__(self, *args, **kwargs):
465
        super(Invitation, self).__init__(*args, **kwargs)
466
        if not self.id:
467
            self.code = _generate_invitation_code()
468

    
469
    def consume(self):
470
        self.is_consumed = True
471
        self.consumed = datetime.now()
472
        self.save()
473

    
474
    def __unicode__(self):
475
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
476

    
477

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

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

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

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

493
        After successful email change the activation record is deleted.
494

495
        Throws ValueError if there is already
496
        """
497
        try:
498
            email_change = self.model.objects.get(
499
                activation_key=activation_key)
500
            if email_change.activation_key_expired():
501
                email_change.delete()
502
                raise EmailChange.DoesNotExist
503
            # is there an active user with this address?
504
            try:
505
                AstakosUser.objects.get(email=email_change.new_email_address)
506
            except AstakosUser.DoesNotExist:
507
                pass
508
            else:
509
                raise ValueError(_('The new email address is reserved.'))
510
            # update user
511
            user = AstakosUser.objects.get(pk=email_change.user_id)
512
            user.email = email_change.new_email_address
513
            user.save()
514
            email_change.delete()
515
            return user
516
        except EmailChange.DoesNotExist:
517
            raise ValueError(_('Invalid activation key'))
518

    
519

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

    
528
    objects = EmailChangeManager()
529

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

    
534

    
535
class AdditionalMail(models.Model):
536
    """
537
    Model for registring invitations
538
    """
539
    owner = models.ForeignKey(AstakosUser)
540
    email = models.EmailField()
541

    
542

    
543
def _generate_invitation_code():
544
    while True:
545
        code = randint(1, 2L ** 63 - 1)
546
        try:
547
            Invitation.objects.get(code=code)
548
            # An invitation with this code already exists, try again
549
        except Invitation.DoesNotExist:
550
            return code
551

    
552

    
553
def get_latest_terms():
554
    try:
555
        term = ApprovalTerms.objects.order_by('-id')[0]
556
        return term
557
    except IndexError:
558
        pass
559
    return None
560

    
561

    
562
def create_astakos_user(u):
563
    try:
564
        AstakosUser.objects.get(user_ptr=u.pk)
565
    except AstakosUser.DoesNotExist:
566
        extended_user = AstakosUser(user_ptr_id=u.pk)
567
        extended_user.__dict__.update(u.__dict__)
568
        extended_user.renew_token()
569
        extended_user.save()
570
    except BaseException, e:
571
        logger.exception(e)
572
        pass
573

    
574

    
575
def fix_superusers(sender, **kwargs):
576
    # Associate superusers with AstakosUser
577
    admins = User.objects.filter(is_superuser=True)
578
    for u in admins:
579
        create_astakos_user(u)
580

    
581

    
582
def user_post_save(sender, instance, created, **kwargs):
583
    if not created:
584
        return
585
    create_astakos_user(instance)
586

    
587

    
588
def set_default_group(user):
589
    try:
590
        default = AstakosGroup.objects.get(name='default')
591
        Membership(
592
            group=default, person=user, date_joined=datetime.now()).save()
593
    except AstakosGroup.DoesNotExist, e:
594
        logger.exception(e)
595

    
596

    
597
def astakosuser_pre_save(sender, instance, **kwargs):
598
    instance.aquarium_report = False
599
    instance.new = False
600
    try:
601
        db_instance = AstakosUser.objects.get(id=instance.id)
602
    except AstakosUser.DoesNotExist:
603
        # create event
604
        instance.aquarium_report = True
605
        instance.new = True
606
    else:
607
        get = AstakosUser.__getattribute__
608
        l = filter(lambda f: get(db_instance, f) != get(instance, f),
609
                   BILLING_FIELDS
610
                   )
611
        instance.aquarium_report = True if l else False
612

    
613

    
614
def astakosuser_post_save(sender, instance, created, **kwargs):
615
    if instance.aquarium_report:
616
        report_user_event(instance, create=instance.new)
617
    if not created:
618
        return
619
    set_default_group(instance)
620
    # TODO handle socket.error & IOError
621
    register_users((instance,))
622

    
623

    
624
def resource_post_save(sender, instance, created, **kwargs):
625
    if not created:
626
        return
627
    register_resources((instance,))
628

    
629

    
630
def send_quota_disturbed(sender, instance, **kwargs):
631
    users = []
632
    extend = users.extend
633
    if sender == Membership:
634
        if not instance.group.is_enabled:
635
            return
636
        extend([instance.person])
637
    elif sender == AstakosUserQuota:
638
        extend([instance.user])
639
    elif sender == AstakosGroupQuota:
640
        if not instance.group.is_enabled:
641
            return
642
        extend(instance.group.astakosuser_set.all())
643
    elif sender == AstakosGroup:
644
        if not instance.is_enabled:
645
            return
646
    quota_disturbed.send(sender=sender, users=users)
647

    
648

    
649
def on_quota_disturbed(sender, users, **kwargs):
650
    print '>>>', locals()
651
    if not users:
652
        return
653
    send_quota(users)
654

    
655
post_syncdb.connect(fix_superusers)
656
post_save.connect(user_post_save, sender=User)
657
pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
658
post_save.connect(astakosuser_post_save, sender=AstakosUser)
659
post_save.connect(resource_post_save, sender=Resource)
660

    
661
quota_disturbed = Signal(providing_args=["users"])
662
quota_disturbed.connect(on_quota_disturbed)
663

    
664
post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
665
post_delete.connect(send_quota_disturbed, sender=Membership)
666
post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
667
post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
668
post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
669
post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)