Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (21.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, 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
#     @property
153
#     def participants(self):
154
#         return len(self.approved_members)
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=approved_members)
162
        update_groupmembers_quota.apply_async(args=[self], eta=self.issue_date)
163
        update_groupmembers_quota.apply_async(args=[self], eta=self.expiration_date)
164
    
165
    def disable(self):
166
        if self.is_disabled:
167
            return
168
        self.approval_date = None
169
        self.save()
170
        quota_disturbed.send(sender=self, users=self.approved_members)
171
    
172
    def approve_member(self, person):
173
        m, created = self.membership_set.get_or_create(person=person)
174
        # update date_joined in any case
175
        m.date_joined=datetime.now()
176
        m.save()
177
    
178
    def disapprove_member(self, person):
179
        self.membership_set.remove(person=person)
180
    
181
    @property
182
    def members(self):
183
        return [m.person for m in self.membership_set.all()]
184
    
185
    @property
186
    def approved_members(self):
187
        return [m.person for m in self.membership_set.all() if m.is_approved]
188
    
189
    @property
190
    def quota(self):
191
        d = defaultdict(int)
192
        for q in self.astakosgroupquota_set.all():
193
            d[q.resource] += q.limit
194
        return d
195
    
196
    @property
197
    def owners(self):
198
        return self.owner.all()
199
    
200
    @owners.setter
201
    def owners(self, l):
202
        self.owner = l
203
        map(self.approve_member, l)
204

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

    
212
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
213
    provider = models.CharField('Provider', max_length=255, blank=True)
214

    
215
    #for invitations
216
    user_level = DEFAULT_USER_LEVEL
217
    level = models.IntegerField('Inviter level', default=user_level)
218
    invitations = models.IntegerField('Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
219

    
220
    auth_token = models.CharField('Authentication Token', max_length=32,
221
                                  null=True, blank=True)
222
    auth_token_created = models.DateTimeField('Token creation date', null=True)
223
    auth_token_expires = models.DateTimeField('Token expiration date', null=True)
224

    
225
    updated = models.DateTimeField('Update date')
226
    is_verified = models.BooleanField('Is verified?', default=False)
227

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

    
231
    email_verified = models.BooleanField('Email verified?', default=False)
232

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

    
262
    @realname.setter
263
    def realname(self, value):
264
        parts = value.split(' ')
265
        if len(parts) == 2:
266
            self.first_name = parts[0]
267
            self.last_name = parts[1]
268
        else:
269
            self.last_name = parts[0]
270

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

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

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

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

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

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

    
415
class ApprovalTerms(models.Model):
416
    """
417
    Model for approval terms
418
    """
419

    
420
    date = models.DateTimeField('Issue date', db_index=True, default=datetime.now())
421
    location = models.CharField('Terms location', max_length=255)
422

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

    
446
    def __unicode__(self):
447
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
448

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

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

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

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

464
        After successful email change the activation record is deleted.
465

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

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

    
495
    objects = EmailChangeManager()
496

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

    
501
class AdditionalMail(models.Model):
502
    """
503
    Model for registring invitations
504
    """
505
    owner = models.ForeignKey(AstakosUser)
506
    email = models.EmailField()
507

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

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

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

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

    
543
def user_post_save(sender, instance, created, **kwargs):
544
    if not created:
545
        return
546
    create_astakos_user(instance)
547

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

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

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

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

    
598
def on_quota_disturbed(sender, users, **kwargs):
599
    print '>>>', locals()
600
    if not users:
601
        return
602
    send_quota(users)
603

    
604
post_syncdb.connect(fix_superusers)
605
post_save.connect(user_post_save, sender=User)
606
pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
607
post_save.connect(astakosuser_post_save, sender=AstakosUser)
608

    
609
quota_disturbed = Signal(providing_args=["users"])
610
quota_disturbed.connect(on_quota_disturbed)
611

    
612
post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
613
post_delete.connect(send_quota_disturbed, sender=Membership)
614
post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
615
post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
616
post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
617
post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)