Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (22.3 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,
50
                                      post_delete)
51
from django.dispatch import Signal
52
from django.db.models import Q
53

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

    
61
from astakos.im.tasks import propagate_groupmembers_quota
62

    
63
logger = logging.getLogger(__name__)
64

    
65

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

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

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

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

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

    
96

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

    
101

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

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

    
110

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

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

    
117

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

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

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

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

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

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

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

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

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

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

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

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

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

    
214

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

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

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

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

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

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

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

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

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

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

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

    
265
    __has_signed_terms = False
266

    
267
    owner = models.ManyToManyField(
268
        AstakosGroup, related_name='owner', null=True)
269

    
270
    class Meta:
271
        unique_together = ("provider", "third_party_identifier")
272

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

    
279
    @property
280
    def realname(self):
281
        return '%s %s' % (self.first_name, self.last_name)
282

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

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

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

    
316
    def save(self, update_timestamps=True, **kwargs):
317
        if update_timestamps:
318
            if not self.id:
319
                self.date_joined = datetime.now()
320
            self.updated = datetime.now()
321

    
322
        # update date_signed_terms if necessary
323
        if self.__has_signed_terms != self.has_signed_terms:
324
            self.date_signed_terms = datetime.now()
325

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

    
342
        super(AstakosUser, self).save(**kwargs)
343

    
344
    def renew_token(self):
345
        md5 = hashlib.md5()
346
        md5.update(self.username)
347
        md5.update(self.realname.encode('ascii', 'ignore'))
348
        md5.update(asctime())
349

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

    
357
    def __unicode__(self):
358
        return '%s (%s)' % (self.realname, self.email)
359

    
360
    def conflicting_email(self):
361
        q = AstakosUser.objects.exclude(username=self.username)
362
        q = q.filter(email=self.email)
363
        if q.count() != 0:
364
            return True
365
        return False
366

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

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

    
393

    
394
class Membership(models.Model):
395
    person = models.ForeignKey(AstakosUser)
396
    group = models.ForeignKey(AstakosGroup)
397
    date_requested = models.DateField(default=datetime.now(), blank=True)
398
    date_joined = models.DateField(null=True, db_index=True, blank=True)
399

    
400
    class Meta:
401
        unique_together = ("person", "group")
402

    
403
    def save(self, *args, **kwargs):
404
        if not self.id:
405
            if not self.group.moderation_enabled:
406
                self.date_joined = datetime.now()
407
        super(Membership, self).save(*args, **kwargs)
408

    
409
    @property
410
    def is_approved(self):
411
        if self.date_joined:
412
            return True
413
        return False
414

    
415
    def approve(self):
416
        self.date_joined = datetime.now()
417
        self.save()
418
        quota_disturbed.send(sender=self, users=(self.person,))
419

    
420
    def disapprove(self):
421
        self.delete()
422
        quota_disturbed.send(sender=self, users=(self.person,))
423

    
424

    
425
class AstakosGroupQuota(models.Model):
426
    limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
427
    uplimit = models.BigIntegerField('Up limit', null=True)
428
    resource = models.ForeignKey(Resource)
429
    group = models.ForeignKey(AstakosGroup, blank=True)
430

    
431
    class Meta:
432
        unique_together = ("resource", "group")
433

    
434

    
435
class AstakosUserQuota(models.Model):
436
    limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
437
    uplimit = models.BigIntegerField('Up limit', null=True)
438
    resource = models.ForeignKey(Resource)
439
    user = models.ForeignKey(AstakosUser)
440

    
441
    class Meta:
442
        unique_together = ("resource", "user")
443

    
444

    
445
class ApprovalTerms(models.Model):
446
    """
447
    Model for approval terms
448
    """
449

    
450
    date = models.DateTimeField(
451
        'Issue date', db_index=True, default=datetime.now())
452
    location = models.CharField('Terms location', max_length=255)
453

    
454

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

    
468
    def __init__(self, *args, **kwargs):
469
        super(Invitation, self).__init__(*args, **kwargs)
470
        if not self.id:
471
            self.code = _generate_invitation_code()
472

    
473
    def consume(self):
474
        self.is_consumed = True
475
        self.consumed = datetime.now()
476
        self.save()
477

    
478
    def __unicode__(self):
479
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
480

    
481

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

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

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

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

497
        After successful email change the activation record is deleted.
498

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

    
523

    
524
class EmailChange(models.Model):
525
    new_email_address = models.EmailField(_(u'new e-mail address'),
526
        help_text=_(u'Your old email address will be used until you verify your new one.'))
527
    user = models.ForeignKey(
528
        AstakosUser, unique=True, related_name='emailchange_user')
529
    requested_at = models.DateTimeField(default=datetime.now())
530
    activation_key = models.CharField(
531
        max_length=40, unique=True, db_index=True)
532

    
533
    objects = EmailChangeManager()
534

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

    
539

    
540
class AdditionalMail(models.Model):
541
    """
542
    Model for registring invitations
543
    """
544
    owner = models.ForeignKey(AstakosUser)
545
    email = models.EmailField()
546

    
547

    
548
def _generate_invitation_code():
549
    while True:
550
        code = randint(1, 2L ** 63 - 1)
551
        try:
552
            Invitation.objects.get(code=code)
553
            # An invitation with this code already exists, try again
554
        except Invitation.DoesNotExist:
555
            return code
556

    
557

    
558
def get_latest_terms():
559
    try:
560
        term = ApprovalTerms.objects.order_by('-id')[0]
561
        return term
562
    except IndexError:
563
        pass
564
    return None
565

    
566

    
567
def create_astakos_user(u):
568
    try:
569
        AstakosUser.objects.get(user_ptr=u.pk)
570
    except AstakosUser.DoesNotExist:
571
        extended_user = AstakosUser(user_ptr_id=u.pk)
572
        extended_user.__dict__.update(u.__dict__)
573
        extended_user.renew_token()
574
        extended_user.save()
575
    except BaseException, e:
576
        logger.exception(e)
577
        pass
578

    
579

    
580
def fix_superusers(sender, **kwargs):
581
    # Associate superusers with AstakosUser
582
    admins = User.objects.filter(is_superuser=True)
583
    for u in admins:
584
        create_astakos_user(u)
585

    
586

    
587
def user_post_save(sender, instance, created, **kwargs):
588
    if not created:
589
        return
590
    create_astakos_user(instance)
591

    
592

    
593
def set_default_group(user):
594
    try:
595
        default = AstakosGroup.objects.get(name='default')
596
        Membership(
597
            group=default, person=user, date_joined=datetime.now()).save()
598
    except AstakosGroup.DoesNotExist, e:
599
        logger.exception(e)
600

    
601

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

    
618

    
619
def astakosuser_post_save(sender, instance, created, **kwargs):
620
    if instance.aquarium_report:
621
        report_user_event(instance, create=instance.new)
622
    if not created:
623
        return
624
    set_default_group(instance)
625
    # TODO handle socket.error & IOError
626
    register_users((instance,))
627

    
628

    
629
def resource_post_save(sender, instance, created, **kwargs):
630
    if not created:
631
        return
632
    register_resources((instance,))
633

    
634

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

    
653

    
654
def on_quota_disturbed(sender, users, **kwargs):
655
    print '>>>', locals()
656
    if not users:
657
        return
658
    send_quota(users)
659

    
660
post_syncdb.connect(fix_superusers)
661
post_save.connect(user_post_save, sender=User)
662
pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
663
post_save.connect(astakosuser_post_save, sender=AstakosUser)
664
post_save.connect(resource_post_save, sender=Resource)
665

    
666
quota_disturbed = Signal(providing_args=["users"])
667
quota_disturbed.connect(on_quota_disturbed)
668

    
669
post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
670
post_delete.connect(send_quota_disturbed, sender=Membership)
671
post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
672
post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
673
post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
674
post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)