Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (28.5 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, IntegrityError
45
from django.contrib.auth.models import User, UserManager, Group, Permission
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.contrib.contenttypes.models import ContentType
52

    
53
from django.dispatch import Signal
54
from django.db.models import Q
55

    
56
from astakos.im.settings import (DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
57
                                 AUTH_TOKEN_DURATION, BILLING_FIELDS,
58
                                 EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL)
59
from astakos.im.endpoints.quotaholder import (register_users, send_quota,
60
                                              register_resources)
61
from astakos.im.endpoints.aquarium.producer import report_user_event
62
from astakos.im.functions import send_invitation
63
from astakos.im.tasks import propagate_groupmembers_quota
64
from astakos.im.functions import send_invitation
65

    
66
logger = logging.getLogger(__name__)
67

    
68
DEFAULT_CONTENT_TYPE = None
69
try:
70
    content_type = ContentType.objects.get(app_label='im', model='astakosuser')
71
except:
72
    content_type = DEFAULT_CONTENT_TYPE
73

    
74
RESOURCE_SEPARATOR = '.'
75

    
76

    
77
class Service(models.Model):
78
    name = models.CharField('Name', max_length=255, unique=True, db_index=True)
79
    url = models.FilePathField()
80
    icon = models.FilePathField(blank=True)
81
    auth_token = models.CharField('Authentication Token', max_length=32,
82
                                  null=True, blank=True)
83
    auth_token_created = models.DateTimeField('Token creation date', null=True)
84
    auth_token_expires = models.DateTimeField(
85
        'Token expiration date', null=True)
86

    
87
    def save(self, **kwargs):
88
        if not self.id:
89
            self.renew_token()
90
        super(Service, self).save(**kwargs)
91

    
92
    def renew_token(self):
93
        md5 = hashlib.md5()
94
        md5.update(self.name.encode('ascii', 'ignore'))
95
        md5.update(self.url.encode('ascii', 'ignore'))
96
        md5.update(asctime())
97

    
98
        self.auth_token = b64encode(md5.digest())
99
        self.auth_token_created = datetime.now()
100
        self.auth_token_expires = self.auth_token_created + \
101
            timedelta(hours=AUTH_TOKEN_DURATION)
102

    
103
    def __str__(self):
104
        return self.name
105

    
106
    @property
107
    def resources(self):
108
        return self.resource_set.all()
109

    
110
    @resources.setter
111
    def resources(self, resources):
112
        for s in resources:
113
            self.resource_set.create(**s)
114
    
115
    def add_resource(self, service, resource, uplimit, update=True):
116
        """Raises ObjectDoesNotExist, IntegrityError"""
117
        resource = Resource.objects.get(service__name=service, name=resource)
118
        if update:
119
            AstakosUserQuota.objects.update_or_create(user=self,
120
                                                      resource=resource,
121
                                                      defaults={'uplimit': uplimit})
122
        else:
123
            q = self.astakosuserquota_set
124
            q.create(resource=resource, uplimit=uplimit)
125

    
126

    
127
class ResourceMetadata(models.Model):
128
    key = models.CharField('Name', max_length=255, unique=True, db_index=True)
129
    value = models.CharField('Value', max_length=255)
130

    
131

    
132
class Resource(models.Model):
133
    name = models.CharField('Name', max_length=255, unique=True, db_index=True)
134
    meta = models.ManyToManyField(ResourceMetadata)
135
    service = models.ForeignKey(Service)
136
    desc = models.TextField('Description', null=True)
137
    unit = models.CharField('Name', null=True, max_length=255)
138
    group = models.CharField('Group', null=True, max_length=255)
139

    
140
    def __str__(self):
141
        return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
142

    
143

    
144
class GroupKind(models.Model):
145
    name = models.CharField('Name', max_length=255, unique=True, db_index=True)
146

    
147
    def __str__(self):
148
        return self.name
149

    
150

    
151
class AstakosGroup(Group):
152
    kind = models.ForeignKey(GroupKind)
153
    homepage = models.URLField(
154
        'Homepage Url', max_length=255, null=True, blank=True)
155
    desc = models.TextField('Description', null=True)
156
    policy = models.ManyToManyField(
157
        Resource,
158
        null=True,
159
        blank=True,
160
        through='AstakosGroupQuota'
161
    )
162
    creation_date = models.DateTimeField(
163
        'Creation date',
164
        default=datetime.now()
165
    )
166
    issue_date = models.DateTimeField('Issue date', null=True)
167
    expiration_date = models.DateTimeField(
168
        'Expiration date',
169
         null=True
170
    )
171
    moderation_enabled = models.BooleanField(
172
        'Moderated membership?',
173
        default=True
174
    )
175
    approval_date = models.DateTimeField(
176
        'Activation date',
177
        null=True,
178
        blank=True
179
    )
180
    estimated_participants = models.PositiveIntegerField(
181
        'Estimated #members',
182
        null=True,
183
        blank=True,
184
    )
185
    max_participants = models.PositiveIntegerField(
186
        'Maximum numder of participants',
187
        null=True,
188
        blank=True
189
    )
190
    
191
    @property
192
    def is_disabled(self):
193
        if not self.approval_date:
194
            return True
195
        return False
196

    
197
    @property
198
    def is_enabled(self):
199
        if self.is_disabled:
200
            return False
201
        if not self.issue_date:
202
            return False
203
        if not self.expiration_date:
204
            return True
205
        now = datetime.now()
206
        if self.issue_date > now:
207
            return False
208
        if now >= self.expiration_date:
209
            return False
210
        return True
211

    
212
    def enable(self):
213
        if self.is_enabled:
214
            return
215
        self.approval_date = datetime.now()
216
        self.save()
217
        quota_disturbed.send(sender=self, users=self.approved_members)
218
        propagate_groupmembers_quota.apply_async(
219
            args=[self], eta=self.issue_date)
220
        propagate_groupmembers_quota.apply_async(
221
            args=[self], eta=self.expiration_date)
222

    
223
    def disable(self):
224
        if self.is_disabled:
225
            return
226
        self.approval_date = None
227
        self.save()
228
        quota_disturbed.send(sender=self, users=self.approved_members)
229

    
230
    def approve_member(self, person):
231
        m, created = self.membership_set.get_or_create(person=person)
232
        # update date_joined in any case
233
        m.date_joined = datetime.now()
234
        m.save()
235

    
236
    def disapprove_member(self, person):
237
        self.membership_set.remove(person=person)
238

    
239
    @property
240
    def members(self):
241
        q = self.membership_set.select_related().all()
242
        return [m.person for m in q]
243
    
244
    @property
245
    def approved_members(self):
246
        q = self.membership_set.select_related().all()
247
        return [m.person for m in q if m.is_approved]
248

    
249
    @property
250
    def quota(self):
251
        d = defaultdict(int)
252
        for q in self.astakosgroupquota_set.select_related().all():
253
            d[q.resource] += q.uplimit
254
        return d
255
    
256
    def add_policy(self, service, resource, uplimit, update=True):
257
        """Raises ObjectDoesNotExist, IntegrityError"""
258
        print '#', locals()
259
        resource = Resource.objects.get(service__name=service, name=resource)
260
        if update:
261
            AstakosGroupQuota.objects.update_or_create(
262
                group=self,
263
                resource=resource,
264
                defaults={'uplimit': uplimit}
265
            )
266
        else:
267
            q = self.astakosgroupquota_set
268
            q.create(resource=resource, uplimit=uplimit)
269
    
270
    @property
271
    def policies(self):
272
        return self.astakosgroupquota_set.select_related().all()
273

    
274
    @policies.setter
275
    def policies(self, policies):
276
        for p in policies:
277
            service = p.get('service', None)
278
            resource = p.get('resource', None)
279
            uplimit = p.get('uplimit', 0)
280
            update = p.get('update', True)
281
            self.add_policy(service, resource, uplimit, update)
282
    
283
    @property
284
    def owners(self):
285
        return self.owner.all()
286

    
287
    @property
288
    def owner_details(self):
289
        return self.owner.select_related().all()
290

    
291
    @owners.setter
292
    def owners(self, l):
293
        self.owner = l
294
        map(self.approve_member, l)
295

    
296

    
297
class AstakosUser(User):
298
    """
299
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
300
    """
301
    # Use UserManager to get the create_user method, etc.
302
    objects = UserManager()
303

    
304
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
305
    provider = models.CharField('Provider', max_length=255, blank=True)
306

    
307
    #for invitations
308
    user_level = DEFAULT_USER_LEVEL
309
    level = models.IntegerField('Inviter level', default=user_level)
310
    invitations = models.IntegerField(
311
        'Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
312

    
313
    auth_token = models.CharField('Authentication Token', max_length=32,
314
                                  null=True, blank=True)
315
    auth_token_created = models.DateTimeField('Token creation date', null=True)
316
    auth_token_expires = models.DateTimeField(
317
        'Token expiration date', null=True)
318

    
319
    updated = models.DateTimeField('Update date')
320
    is_verified = models.BooleanField('Is verified?', default=False)
321

    
322
    # ex. screen_name for twitter, eppn for shibboleth
323
    third_party_identifier = models.CharField(
324
        'Third-party identifier', max_length=255, null=True, blank=True)
325

    
326
    email_verified = models.BooleanField('Email verified?', default=False)
327

    
328
    has_credits = models.BooleanField('Has credits?', default=False)
329
    has_signed_terms = models.BooleanField(
330
        'I agree with the terms', default=False)
331
    date_signed_terms = models.DateTimeField(
332
        'Signed terms date', null=True, blank=True)
333

    
334
    activation_sent = models.DateTimeField(
335
        'Activation sent data', null=True, blank=True)
336

    
337
    policy = models.ManyToManyField(
338
        Resource, null=True, through='AstakosUserQuota')
339

    
340
    astakos_groups = models.ManyToManyField(
341
        AstakosGroup, verbose_name=_('agroups'), blank=True,
342
        help_text=_("""In addition to the permissions manually assigned, this
343
                    user will also get all permissions granted to each group
344
                    he/she is in."""),
345
        through='Membership')
346

    
347
    __has_signed_terms = False
348
    disturbed_quota = models.BooleanField('Needs quotaholder syncing',
349
                                           default=False, db_index=True)
350

    
351
    owner = models.ManyToManyField(
352
        AstakosGroup, related_name='owner', null=True)
353

    
354
    class Meta:
355
        unique_together = ("provider", "third_party_identifier")
356

    
357
    def __init__(self, *args, **kwargs):
358
        super(AstakosUser, self).__init__(*args, **kwargs)
359
        self.__has_signed_terms = self.has_signed_terms
360
        if not self.id and not self.is_active:
361
            self.is_active = False
362

    
363
    @property
364
    def realname(self):
365
        return '%s %s' % (self.first_name, self.last_name)
366

    
367
    @realname.setter
368
    def realname(self, value):
369
        parts = value.split(' ')
370
        if len(parts) == 2:
371
            self.first_name = parts[0]
372
            self.last_name = parts[1]
373
        else:
374
            self.last_name = parts[0]
375

    
376
    def add_permission(self, pname):
377
        if self.has_perm(pname):
378
            return
379
        p, created = Permission.objects.get_or_create(codename=pname,
380
                                                      name=pname.capitalize(),
381
                                                      content_type=content_type)
382
        self.user_permissions.add(p)
383

    
384
    def remove_permission(self, pname):
385
        if self.has_perm(pname):
386
            return
387
        p = Permission.objects.get(codename=pname,
388
                                   content_type=content_type)
389
        self.user_permissions.remove(p)
390

    
391
    @property
392
    def invitation(self):
393
        try:
394
            return Invitation.objects.get(username=self.email)
395
        except Invitation.DoesNotExist:
396
            return None
397

    
398
    def invite(self, email, realname):
399
        inv = Invitation(inviter=self, username=email, realname=realname)
400
        inv.save()
401
        send_invitation(inv)
402
        self.invitations = max(0, self.invitations - 1)
403
        self.save()
404

    
405
    @property
406
    def quota(self):
407
        """Returns a dict with the sum of quota limits per resource"""
408
        d = defaultdict(int)
409
        for q in self.policies:
410
            d[q.resource] += q.uplimit
411
        for m in self.extended_groups:
412
            if not m.is_approved:
413
                continue
414
            g = m.group
415
            if not g.is_enabled:
416
                continue
417
            for r, uplimit in g.quota.iteritems():
418
                d[r] += uplimit
419

    
420
        # TODO set default for remaining
421
        return d
422

    
423
    @property
424
    def policies(self):
425
        return self.astakosuserquota_set.select_related().all()
426

    
427
    @policies.setter
428
    def policies(self, policies):
429
        for p in policies:
430
            service = policies.get('service', None)
431
            resource = policies.get('resource', None)
432
            uplimit = policies.get('uplimit', 0)
433
            update = policies.get('update', True)
434
            self.add_policy(service, resource, uplimit, update)
435

    
436
    def add_policy(self, service, resource, uplimit, update=True):
437
        """Raises ObjectDoesNotExist, IntegrityError"""
438
        resource = Resource.objects.get(service__name=service, name=resource)
439
        if update:
440
            AstakosUserQuota.objects.update_or_create(user=self,
441
                                                      resource=resource,
442
                                                      defaults={'uplimit': uplimit})
443
        else:
444
            q = self.astakosuserquota_set
445
            q.create(resource=resource, uplimit=uplimit)
446

    
447
    def remove_policy(self, service, resource):
448
        """Raises ObjectDoesNotExist, IntegrityError"""
449
        resource = Resource.objects.get(service__name=service, name=resource)
450
        q = self.policies.get(resource=resource).delete()
451

    
452
    @property
453
    def extended_groups(self):
454
        return self.membership_set.select_related().all()
455

    
456
    @extended_groups.setter
457
    def extended_groups(self, groups):
458
        #TODO exceptions
459
        for name in groups:
460
            group = AstakosGroup.objects.get(name=name)
461
            self.membership_set.create(group=group)
462

    
463
    def save(self, update_timestamps=True, **kwargs):
464
        if update_timestamps:
465
            if not self.id:
466
                self.date_joined = datetime.now()
467
            self.updated = datetime.now()
468

    
469
        # update date_signed_terms if necessary
470
        if self.__has_signed_terms != self.has_signed_terms:
471
            self.date_signed_terms = datetime.now()
472

    
473
        if not self.id:
474
            # set username
475
            while not self.username:
476
                username = uuid.uuid4().hex[:30]
477
                try:
478
                    AstakosUser.objects.get(username=username)
479
                except AstakosUser.DoesNotExist:
480
                    self.username = username
481
            if not self.provider:
482
                self.provider = 'local'
483
            self.email = self.email.lower()
484
        self.validate_unique_email_isactive()
485
        if self.is_active and self.activation_sent:
486
            # reset the activation sent
487
            self.activation_sent = None
488

    
489
        super(AstakosUser, self).save(**kwargs)
490

    
491
    def renew_token(self):
492
        md5 = hashlib.md5()
493
        md5.update(self.username)
494
        md5.update(self.realname.encode('ascii', 'ignore'))
495
        md5.update(asctime())
496

    
497
        self.auth_token = b64encode(md5.digest())
498
        self.auth_token_created = datetime.now()
499
        self.auth_token_expires = self.auth_token_created + \
500
            timedelta(hours=AUTH_TOKEN_DURATION)
501
        msg = 'Token renewed for %s' % self.email
502
        logger.log(LOGGING_LEVEL, msg)
503

    
504
    def __unicode__(self):
505
        return '%s (%s)' % (self.realname, self.email)
506

    
507
    def conflicting_email(self):
508
        q = AstakosUser.objects.exclude(username=self.username)
509
        q = q.filter(email=self.email)
510
        if q.count() != 0:
511
            return True
512
        return False
513

    
514
    def validate_unique_email_isactive(self):
515
        """
516
        Implements a unique_together constraint for email and is_active fields.
517
        """
518
        q = AstakosUser.objects.exclude(username=self.username)
519
        q = q.filter(email=self.email)
520
        q = q.filter(is_active=self.is_active)
521
        if q.count() != 0:
522
            raise ValidationError({'__all__': [_('Another account with the same email & is_active combination found.')]})
523

    
524
    @property
525
    def signed_terms(self):
526
        term = get_latest_terms()
527
        if not term:
528
            return True
529
        if not self.has_signed_terms:
530
            return False
531
        if not self.date_signed_terms:
532
            return False
533
        if self.date_signed_terms < term.date:
534
            self.has_signed_terms = False
535
            self.date_signed_terms = None
536
            self.save()
537
            return False
538
        return True
539

    
540
    def store_disturbed_quota(self, set=True):
541
        self.disturbed_qutoa = set
542
        self.save()
543

    
544

    
545
class Membership(models.Model):
546
    person = models.ForeignKey(AstakosUser)
547
    group = models.ForeignKey(AstakosGroup)
548
    date_requested = models.DateField(default=datetime.now(), blank=True)
549
    date_joined = models.DateField(null=True, db_index=True, blank=True)
550

    
551
    class Meta:
552
        unique_together = ("person", "group")
553

    
554
    def save(self, *args, **kwargs):
555
        if not self.id:
556
            if not self.group.moderation_enabled:
557
                self.date_joined = datetime.now()
558
        super(Membership, self).save(*args, **kwargs)
559

    
560
    @property
561
    def is_approved(self):
562
        if self.date_joined:
563
            return True
564
        return False
565

    
566
    def approve(self):
567
        self.date_joined = datetime.now()
568
        self.save()
569
        quota_disturbed.send(sender=self, users=(self.person,))
570

    
571
    def disapprove(self):
572
        self.delete()
573
        quota_disturbed.send(sender=self, users=(self.person,))
574

    
575
class AstakosQuotaManager(models.Manager):
576
    def _update_or_create(self, **kwargs):
577
        assert kwargs, \
578
            'update_or_create() must be passed at least one keyword argument'
579
        obj, created = self.get_or_create(**kwargs)
580
        defaults = kwargs.pop('defaults', {})
581
        if created:
582
            return obj, True, False
583
        else:
584
            try:
585
                params = dict(
586
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
587
                params.update(defaults)
588
                for attr, val in params.items():
589
                    if hasattr(obj, attr):
590
                        setattr(obj, attr, val)
591
                sid = transaction.savepoint()
592
                obj.save(force_update=True)
593
                transaction.savepoint_commit(sid)
594
                return obj, False, True
595
            except IntegrityError, e:
596
                transaction.savepoint_rollback(sid)
597
                try:
598
                    return self.get(**kwargs), False, False
599
                except self.model.DoesNotExist:
600
                    raise e
601

    
602
    update_or_create = _update_or_create
603

    
604
class AstakosGroupQuota(models.Model):
605
    objects = AstakosQuotaManager()
606
    limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
607
    uplimit = models.BigIntegerField('Up limit', null=True)
608
    resource = models.ForeignKey(Resource)
609
    group = models.ForeignKey(AstakosGroup, blank=True)
610

    
611
    class Meta:
612
        unique_together = ("resource", "group")
613

    
614
class AstakosUserQuota(models.Model):
615
    objects = AstakosQuotaManager()
616
    limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
617
    uplimit = models.BigIntegerField('Up limit', null=True)
618
    resource = models.ForeignKey(Resource)
619
    user = models.ForeignKey(AstakosUser)
620

    
621
    class Meta:
622
        unique_together = ("resource", "user")
623

    
624

    
625
class ApprovalTerms(models.Model):
626
    """
627
    Model for approval terms
628
    """
629

    
630
    date = models.DateTimeField(
631
        'Issue date', db_index=True, default=datetime.now())
632
    location = models.CharField('Terms location', max_length=255)
633

    
634

    
635
class Invitation(models.Model):
636
    """
637
    Model for registring invitations
638
    """
639
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
640
                                null=True)
641
    realname = models.CharField('Real name', max_length=255)
642
    username = models.CharField('Unique ID', max_length=255, unique=True)
643
    code = models.BigIntegerField('Invitation code', db_index=True)
644
    is_consumed = models.BooleanField('Consumed?', default=False)
645
    created = models.DateTimeField('Creation date', auto_now_add=True)
646
    consumed = models.DateTimeField('Consumption date', null=True, blank=True)
647

    
648
    def __init__(self, *args, **kwargs):
649
        super(Invitation, self).__init__(*args, **kwargs)
650
        if not self.id:
651
            self.code = _generate_invitation_code()
652

    
653
    def consume(self):
654
        self.is_consumed = True
655
        self.consumed = datetime.now()
656
        self.save()
657

    
658
    def __unicode__(self):
659
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
660

    
661

    
662
class EmailChangeManager(models.Manager):
663
    @transaction.commit_on_success
664
    def change_email(self, activation_key):
665
        """
666
        Validate an activation key and change the corresponding
667
        ``User`` if valid.
668

669
        If the key is valid and has not expired, return the ``User``
670
        after activating.
671

672
        If the key is not valid or has expired, return ``None``.
673

674
        If the key is valid but the ``User`` is already active,
675
        return ``None``.
676

677
        After successful email change the activation record is deleted.
678

679
        Throws ValueError if there is already
680
        """
681
        try:
682
            email_change = self.model.objects.get(
683
                activation_key=activation_key)
684
            if email_change.activation_key_expired():
685
                email_change.delete()
686
                raise EmailChange.DoesNotExist
687
            # is there an active user with this address?
688
            try:
689
                AstakosUser.objects.get(email=email_change.new_email_address)
690
            except AstakosUser.DoesNotExist:
691
                pass
692
            else:
693
                raise ValueError(_('The new email address is reserved.'))
694
            # update user
695
            user = AstakosUser.objects.get(pk=email_change.user_id)
696
            user.email = email_change.new_email_address
697
            user.save()
698
            email_change.delete()
699
            return user
700
        except EmailChange.DoesNotExist:
701
            raise ValueError(_('Invalid activation key'))
702

    
703

    
704
class EmailChange(models.Model):
705
    new_email_address = models.EmailField(_(u'new e-mail address'),
706
                                          help_text=_(u'Your old email address will be used until you verify your new one.'))
707
    user = models.ForeignKey(
708
        AstakosUser, unique=True, related_name='emailchange_user')
709
    requested_at = models.DateTimeField(default=datetime.now())
710
    activation_key = models.CharField(
711
        max_length=40, unique=True, db_index=True)
712

    
713
    objects = EmailChangeManager()
714

    
715
    def activation_key_expired(self):
716
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
717
        return self.requested_at + expiration_date < datetime.now()
718

    
719

    
720
class AdditionalMail(models.Model):
721
    """
722
    Model for registring invitations
723
    """
724
    owner = models.ForeignKey(AstakosUser)
725
    email = models.EmailField()
726

    
727

    
728
def _generate_invitation_code():
729
    while True:
730
        code = randint(1, 2L ** 63 - 1)
731
        try:
732
            Invitation.objects.get(code=code)
733
            # An invitation with this code already exists, try again
734
        except Invitation.DoesNotExist:
735
            return code
736

    
737

    
738
def get_latest_terms():
739
    try:
740
        term = ApprovalTerms.objects.order_by('-id')[0]
741
        return term
742
    except IndexError:
743
        pass
744
    return None
745

    
746

    
747
def create_astakos_user(u):
748
    try:
749
        AstakosUser.objects.get(user_ptr=u.pk)
750
    except AstakosUser.DoesNotExist:
751
        extended_user = AstakosUser(user_ptr_id=u.pk)
752
        extended_user.__dict__.update(u.__dict__)
753
        extended_user.renew_token()
754
        extended_user.save()
755
    except BaseException, e:
756
        logger.exception(e)
757
        pass
758

    
759

    
760
def fix_superusers(sender, **kwargs):
761
    # Associate superusers with AstakosUser
762
    admins = User.objects.filter(is_superuser=True)
763
    for u in admins:
764
        create_astakos_user(u)
765

    
766

    
767
def user_post_save(sender, instance, created, **kwargs):
768
    if not created:
769
        return
770
    create_astakos_user(instance)
771

    
772

    
773
def set_default_group(user):
774
    try:
775
        default = AstakosGroup.objects.get(name='default')
776
        Membership(
777
            group=default, person=user, date_joined=datetime.now()).save()
778
    except AstakosGroup.DoesNotExist, e:
779
        logger.exception(e)
780

    
781

    
782
def astakosuser_pre_save(sender, instance, **kwargs):
783
    instance.aquarium_report = False
784
    instance.new = False
785
    try:
786
        db_instance = AstakosUser.objects.get(id=instance.id)
787
    except AstakosUser.DoesNotExist:
788
        # create event
789
        instance.aquarium_report = True
790
        instance.new = True
791
    else:
792
        get = AstakosUser.__getattribute__
793
        l = filter(lambda f: get(db_instance, f) != get(instance, f),
794
                   BILLING_FIELDS)
795
        instance.aquarium_report = True if l else False
796

    
797

    
798
def astakosuser_post_save(sender, instance, created, **kwargs):
799
    if instance.aquarium_report:
800
        report_user_event(instance, create=instance.new)
801
    if not created:
802
        return
803
    set_default_group(instance)
804
    # TODO handle socket.error & IOError
805
    register_users((instance,))
806
    instance.renew_token()
807

    
808

    
809
def resource_post_save(sender, instance, created, **kwargs):
810
    if not created:
811
        return
812
    register_resources((instance,))
813

    
814

    
815
def send_quota_disturbed(sender, instance, **kwargs):
816
    users = []
817
    extend = users.extend
818
    if sender == Membership:
819
        if not instance.group.is_enabled:
820
            return
821
        extend([instance.person])
822
    elif sender == AstakosUserQuota:
823
        extend([instance.user])
824
    elif sender == AstakosGroupQuota:
825
        if not instance.group.is_enabled:
826
            return
827
        extend(instance.group.astakosuser_set.all())
828
    elif sender == AstakosGroup:
829
        if not instance.is_enabled:
830
            return
831
    quota_disturbed.send(sender=sender, users=users)
832

    
833

    
834
def on_quota_disturbed(sender, users, **kwargs):
835
    print '>>>', locals()
836
    if not users:
837
        return
838
    send_quota(users)
839

    
840
post_syncdb.connect(fix_superusers)
841
post_save.connect(user_post_save, sender=User)
842
pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
843
post_save.connect(astakosuser_post_save, sender=AstakosUser)
844
post_save.connect(resource_post_save, sender=Resource)
845

    
846
quota_disturbed = Signal(providing_args=["users"])
847
quota_disturbed.connect(on_quota_disturbed)
848

    
849
post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
850
post_delete.connect(send_quota_disturbed, sender=Membership)
851
post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
852
post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
853
post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
854
post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)