Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 2b1a5f5d

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

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

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

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

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

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

    
94

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

    
99

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

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

    
108

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

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

    
115

    
116
class AstakosGroup(Group):
117
    kind = models.ForeignKey(GroupKind)
118
    homepage = models.URLField(
119
        'Homepage Url', max_length=255, null=True, blank=True)
120
    desc = models.TextField('Description', null=True)
121
    policy = models.ManyToManyField(Resource, null=True, blank=True,
122
                                    through='AstakosGroupQuota'
123
                                    )
124
    creation_date = models.DateTimeField('Creation date',
125
                                         default=datetime.now()
126
                                         )
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
                                             )
132
    approval_date = models.DateTimeField('Activation date', null=True,
133
                                         blank=True
134
                                         )
135
    estimated_participants = models.PositiveIntegerField('Estimated #members',
136
                                                         null=True
137
                                                         )
138

    
139
    @property
140
    def is_disabled(self):
141
        if not self.approval_date:
142
            return True
143
        return False
144

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

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

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

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

    
184
    def disapprove_member(self, person):
185
        self.membership_set.remove(person=person)
186

    
187
    @property
188
    def members(self):
189
        return [m.person for m in self.membership_set.all()]
190

    
191
    @property
192
    def approved_members(self):
193
        return [m.person for m in self.membership_set.all() if m.is_approved]
194

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

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

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

    
211

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

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

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

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

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

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

    
241
    email_verified = models.BooleanField('Email verified?', default=False)
242

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

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

    
252
    policy = models.ManyToManyField(
253
        Resource, null=True, through='AstakosUserQuota')
254

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

    
260
    __has_signed_terms = False
261

    
262
    owner = models.ManyToManyField(
263
        AstakosGroup, related_name='owner', null=True)
264

    
265
    class Meta:
266
        unique_together = ("provider", "third_party_identifier")
267

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

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

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

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

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

    
310
    def save(self, update_timestamps=True, **kwargs):
311
        if update_timestamps:
312
            if not self.id:
313
                self.date_joined = datetime.now()
314
            self.updated = datetime.now()
315

    
316
        # update date_signed_terms if necessary
317
        if self.__has_signed_terms != self.has_signed_terms:
318
            self.date_signed_terms = datetime.now()
319

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

    
335
        super(AstakosUser, self).save(**kwargs)
336

    
337
    def renew_token(self):
338
        md5 = hashlib.md5()
339
        md5.update(self.username)
340
        md5.update(self.realname.encode('ascii', 'ignore'))
341
        md5.update(asctime())
342

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

    
350
    def __unicode__(self):
351
        return '%s (%s)' % (self.realname, self.email)
352

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

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

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

    
386

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

    
393
    class Meta:
394
        unique_together = ("person", "group")
395

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

    
402
    @property
403
    def is_approved(self):
404
        if self.date_joined:
405
            return True
406
        return False
407

    
408
    def approve(self):
409
        self.date_joined = datetime.now()
410
        self.save()
411
        quota_disturbed.send(sender=self, users=(self.person,))
412

    
413
    def disapprove(self):
414
        self.delete()
415
        quota_disturbed.send(sender=self, users=(self.person,))
416

    
417

    
418
class AstakosGroupQuota(models.Model):
419
    limit = models.PositiveIntegerField('Limit')
420
    resource = models.ForeignKey(Resource)
421
    group = models.ForeignKey(AstakosGroup, blank=True)
422

    
423
    class Meta:
424
        unique_together = ("resource", "group")
425

    
426

    
427
class AstakosUserQuota(models.Model):
428
    limit = models.PositiveIntegerField('Limit')
429
    resource = models.ForeignKey(Resource)
430
    user = models.ForeignKey(AstakosUser)
431

    
432
    class Meta:
433
        unique_together = ("resource", "user")
434

    
435

    
436
class ApprovalTerms(models.Model):
437
    """
438
    Model for approval terms
439
    """
440

    
441
    date = models.DateTimeField(
442
        'Issue date', db_index=True, default=datetime.now())
443
    location = models.CharField('Terms location', max_length=255)
444

    
445

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

    
459
    def __init__(self, *args, **kwargs):
460
        super(Invitation, self).__init__(*args, **kwargs)
461
        if not self.id:
462
            self.code = _generate_invitation_code()
463

    
464
    def consume(self):
465
        self.is_consumed = True
466
        self.consumed = datetime.now()
467
        self.save()
468

    
469
    def __unicode__(self):
470
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
471

    
472

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

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

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

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

488
        After successful email change the activation record is deleted.
489

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

    
514

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

    
523
    objects = EmailChangeManager()
524

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

    
529

    
530
class AdditionalMail(models.Model):
531
    """
532
    Model for registring invitations
533
    """
534
    owner = models.ForeignKey(AstakosUser)
535
    email = models.EmailField()
536

    
537

    
538
def _generate_invitation_code():
539
    while True:
540
        code = randint(1, 2L ** 63 - 1)
541
        try:
542
            Invitation.objects.get(code=code)
543
            # An invitation with this code already exists, try again
544
        except Invitation.DoesNotExist:
545
            return code
546

    
547

    
548
def get_latest_terms():
549
    try:
550
        term = ApprovalTerms.objects.order_by('-id')[0]
551
        return term
552
    except IndexError:
553
        pass
554
    return None
555

    
556

    
557
def create_astakos_user(u):
558
    try:
559
        AstakosUser.objects.get(user_ptr=u.pk)
560
    except AstakosUser.DoesNotExist:
561
        extended_user = AstakosUser(user_ptr_id=u.pk)
562
        extended_user.__dict__.update(u.__dict__)
563
        extended_user.renew_token()
564
        extended_user.save()
565
    except BaseException, e:
566
        logger.exception(e)
567
        pass
568

    
569

    
570
def fix_superusers(sender, **kwargs):
571
    # Associate superusers with AstakosUser
572
    admins = User.objects.filter(is_superuser=True)
573
    for u in admins:
574
        create_astakos_user(u)
575

    
576

    
577
def user_post_save(sender, instance, created, **kwargs):
578
    if not created:
579
        return
580
    create_astakos_user(instance)
581

    
582

    
583
def set_default_group(user):
584
    try:
585
        default = AstakosGroup.objects.get(name='default')
586
        Membership(
587
            group=default, person=user, date_joined=datetime.now()).save()
588
    except AstakosGroup.DoesNotExist, e:
589
        logger.exception(e)
590

    
591

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

    
608

    
609
def astakosuser_post_save(sender, instance, created, **kwargs):
610
    if instance.aquarium_report:
611
        report_user_event(instance, create=instance.new)
612
    if not created:
613
        return
614
    set_default_group(instance)
615
    # TODO handle socket.error & IOError
616
    register_users((instance,))
617

    
618

    
619
def send_quota_disturbed(sender, instance, **kwargs):
620
    users = []
621
    extend = users.extend
622
    if sender == Membership:
623
        if not instance.group.is_enabled:
624
            return
625
        extend([instance.person])
626
    elif sender == AstakosUserQuota:
627
        extend([instance.user])
628
    elif sender == AstakosGroupQuota:
629
        if not instance.group.is_enabled:
630
            return
631
        extend(instance.group.astakosuser_set.all())
632
    elif sender == AstakosGroup:
633
        if not instance.is_enabled:
634
            return
635
    quota_disturbed.send(sender=sender, users=users)
636

    
637

    
638
def on_quota_disturbed(sender, users, **kwargs):
639
    print '>>>', locals()
640
    if not users:
641
        return
642
    send_quota(users)
643

    
644
post_syncdb.connect(fix_superusers)
645
post_save.connect(user_post_save, sender=User)
646
pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
647
post_save.connect(astakosuser_post_save, sender=AstakosUser)
648

    
649
quota_disturbed = Signal(providing_args=["users"])
650
quota_disturbed.connect(on_quota_disturbed)
651

    
652
post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
653
post_delete.connect(send_quota_disturbed, sender=Membership)
654
post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
655
post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
656
post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
657
post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)