Statistics
| Branch: | Tag: | Revision:

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

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
inf = float('inf')
77

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

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

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

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

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

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

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

    
127

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

    
132

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

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

    
144

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

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

    
151

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

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

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

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

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

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

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

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

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

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

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

    
297

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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