Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 357987bc

History | View | Annotate | Download (28.8 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.db import transaction
48
from django.core.exceptions import ValidationError
49
from django.db import transaction
50
from django.db.models.signals import (pre_save, post_save, post_syncdb,
51
                                      post_delete)
52
from django.contrib.contenttypes.models import ContentType
53

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

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

    
67
import astakos.im.messages as astakos_messages
68

    
69
logger = logging.getLogger(__name__)
70

    
71
DEFAULT_CONTENT_TYPE = None
72
try:
73
    content_type = ContentType.objects.get(app_label='im', model='astakosuser')
74
except:
75
    content_type = DEFAULT_CONTENT_TYPE
76

    
77
RESOURCE_SEPARATOR = '.'
78

    
79
inf = float('inf')
80

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

    
91
    def save(self, **kwargs):
92
        if not self.id:
93
            self.renew_token()
94
        super(Service, self).save(**kwargs)
95

    
96
    def renew_token(self):
97
        md5 = hashlib.md5()
98
        md5.update(self.name.encode('ascii', 'ignore'))
99
        md5.update(self.url.encode('ascii', 'ignore'))
100
        md5.update(asctime())
101

    
102
        self.auth_token = b64encode(md5.digest())
103
        self.auth_token_created = datetime.now()
104
        self.auth_token_expires = self.auth_token_created + \
105
            timedelta(hours=AUTH_TOKEN_DURATION)
106

    
107
    def __str__(self):
108
        return self.name
109

    
110
    @property
111
    def resources(self):
112
        return self.resource_set.all()
113

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

    
130

    
131
class ResourceMetadata(models.Model):
132
    key = models.CharField('Name', max_length=255, unique=True, db_index=True)
133
    value = models.CharField('Value', max_length=255)
134

    
135

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

    
144
    def __str__(self):
145
        return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
146

    
147

    
148
class GroupKind(models.Model):
149
    name = models.CharField('Name', max_length=255, unique=True, db_index=True)
150

    
151
    def __str__(self):
152
        return self.name
153

    
154

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

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

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

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

    
234
    @transaction.commit_manually
235
    def approve_member(self, person):
236
        m, created = self.membership_set.get_or_create(person=person)
237
        # update date_joined in any case
238
        try:
239
            m.approve()
240
        except:
241
            transaction.rollback()
242
            raise
243
        else:
244
            transaction.commit()
245

    
246
#     def disapprove_member(self, person):
247
#         self.membership_set.remove(person=person)
248

    
249
    @property
250
    def members(self):
251
        q = self.membership_set.select_related().all()
252
        return [m.person for m in q]
253
    
254
    @property
255
    def approved_members(self):
256
        q = self.membership_set.select_related().all()
257
        return [m.person for m in q if m.is_approved]
258

    
259
    @property
260
    def quota(self):
261
        d = defaultdict(int)
262
        for q in self.astakosgroupquota_set.select_related().all():
263
            d[q.resource] += q.uplimit or inf
264
        return d
265
    
266
    def add_policy(self, service, resource, uplimit, update=True):
267
        """Raises ObjectDoesNotExist, IntegrityError"""
268
        print '#', locals()
269
        resource = Resource.objects.get(service__name=service, name=resource)
270
        if update:
271
            AstakosGroupQuota.objects.update_or_create(
272
                group=self,
273
                resource=resource,
274
                defaults={'uplimit': uplimit}
275
            )
276
        else:
277
            q = self.astakosgroupquota_set
278
            q.create(resource=resource, uplimit=uplimit)
279
    
280
    @property
281
    def policies(self):
282
        return self.astakosgroupquota_set.select_related().all()
283

    
284
    @policies.setter
285
    def policies(self, policies):
286
        for p in policies:
287
            service = p.get('service', None)
288
            resource = p.get('resource', None)
289
            uplimit = p.get('uplimit', 0)
290
            update = p.get('update', True)
291
            self.add_policy(service, resource, uplimit, update)
292
    
293
    @property
294
    def owners(self):
295
        return self.owner.all()
296

    
297
    @property
298
    def owner_details(self):
299
        return self.owner.select_related().all()
300

    
301
    @owners.setter
302
    def owners(self, l):
303
        self.owner = l
304
        map(self.approve_member, l)
305

    
306

    
307
class AstakosUser(User):
308
    """
309
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
310
    """
311
    # Use UserManager to get the create_user method, etc.
312
    objects = UserManager()
313

    
314
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
315
    provider = models.CharField('Provider', max_length=255, blank=True)
316

    
317
    #for invitations
318
    user_level = DEFAULT_USER_LEVEL
319
    level = models.IntegerField('Inviter level', default=user_level)
320
    invitations = models.IntegerField(
321
        'Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
322

    
323
    auth_token = models.CharField('Authentication Token', max_length=32,
324
                                  null=True, blank=True)
325
    auth_token_created = models.DateTimeField('Token creation date', null=True)
326
    auth_token_expires = models.DateTimeField(
327
        'Token expiration date', null=True)
328

    
329
    updated = models.DateTimeField('Update date')
330
    is_verified = models.BooleanField('Is verified?', default=False)
331

    
332
    # ex. screen_name for twitter, eppn for shibboleth
333
    third_party_identifier = models.CharField(
334
        'Third-party identifier', max_length=255, null=True, blank=True)
335

    
336
    email_verified = models.BooleanField('Email verified?', default=False)
337

    
338
    has_credits = models.BooleanField('Has credits?', default=False)
339
    has_signed_terms = models.BooleanField(
340
        'I agree with the terms', default=False)
341
    date_signed_terms = models.DateTimeField(
342
        'Signed terms date', null=True, blank=True)
343

    
344
    activation_sent = models.DateTimeField(
345
        'Activation sent data', null=True, blank=True)
346

    
347
    policy = models.ManyToManyField(
348
        Resource, null=True, through='AstakosUserQuota')
349

    
350
    astakos_groups = models.ManyToManyField(
351
        AstakosGroup, verbose_name=_('agroups'), blank=True,
352
        help_text=_(astakos_messages.ASTAKOSUSER_GROUPS_HELP),
353
        through='Membership')
354

    
355
    __has_signed_terms = False
356
    disturbed_quota = models.BooleanField('Needs quotaholder syncing',
357
                                           default=False, db_index=True)
358

    
359
    owner = models.ManyToManyField(
360
        AstakosGroup, related_name='owner', null=True)
361

    
362
    class Meta:
363
        unique_together = ("provider", "third_party_identifier")
364

    
365
    def __init__(self, *args, **kwargs):
366
        super(AstakosUser, self).__init__(*args, **kwargs)
367
        self.__has_signed_terms = self.has_signed_terms
368
        if not self.id:
369
            self.is_active = False
370

    
371
    @property
372
    def realname(self):
373
        return '%s %s' % (self.first_name, self.last_name)
374

    
375
    @realname.setter
376
    def realname(self, value):
377
        parts = value.split(' ')
378
        if len(parts) == 2:
379
            self.first_name = parts[0]
380
            self.last_name = parts[1]
381
        else:
382
            self.last_name = parts[0]
383

    
384
    def add_permission(self, pname):
385
        if self.has_perm(pname):
386
            return
387
        p, created = Permission.objects.get_or_create(codename=pname,
388
                                                      name=pname.capitalize(),
389
                                                      content_type=content_type)
390
        self.user_permissions.add(p)
391

    
392
    def remove_permission(self, pname):
393
        if self.has_perm(pname):
394
            return
395
        p = Permission.objects.get(codename=pname,
396
                                   content_type=content_type)
397
        self.user_permissions.remove(p)
398

    
399
    @property
400
    def invitation(self):
401
        try:
402
            return Invitation.objects.get(username=self.email)
403
        except Invitation.DoesNotExist:
404
            return None
405

    
406
    def invite(self, email, realname):
407
        inv = Invitation(inviter=self, username=email, realname=realname)
408
        inv.save()
409
        send_invitation(inv)
410
        self.invitations = max(0, self.invitations - 1)
411
        self.save()
412

    
413
    @property
414
    def quota(self):
415
        """Returns a dict with the sum of quota limits per resource"""
416
        d = defaultdict(int)
417
        for q in self.policies:
418
            d[q.resource] += q.uplimit or inf
419
        for m in self.extended_groups:
420
            if not m.is_approved:
421
                continue
422
            g = m.group
423
            if not g.is_enabled:
424
                continue
425
            for r, uplimit in g.quota.iteritems():
426
                d[r] += uplimit or inf
427
        # TODO set default for remaining
428
        return d
429

    
430
    @property
431
    def policies(self):
432
        return self.astakosuserquota_set.select_related().all()
433

    
434
    @policies.setter
435
    def policies(self, policies):
436
        for p in policies:
437
            service = policies.get('service', None)
438
            resource = policies.get('resource', None)
439
            uplimit = policies.get('uplimit', 0)
440
            update = policies.get('update', True)
441
            self.add_policy(service, resource, uplimit, update)
442

    
443
    def add_policy(self, service, resource, uplimit, update=True):
444
        """Raises ObjectDoesNotExist, IntegrityError"""
445
        resource = Resource.objects.get(service__name=service, name=resource)
446
        if update:
447
            AstakosUserQuota.objects.update_or_create(user=self,
448
                                                      resource=resource,
449
                                                      defaults={'uplimit': uplimit})
450
        else:
451
            q = self.astakosuserquota_set
452
            q.create(resource=resource, uplimit=uplimit)
453

    
454
    def remove_policy(self, service, resource):
455
        """Raises ObjectDoesNotExist, IntegrityError"""
456
        resource = Resource.objects.get(service__name=service, name=resource)
457
        q = self.policies.get(resource=resource).delete()
458

    
459
    @property
460
    def extended_groups(self):
461
        return self.membership_set.select_related().all()
462

    
463
    @extended_groups.setter
464
    def extended_groups(self, groups):
465
        #TODO exceptions
466
        for name in (groups or ()):
467
            group = AstakosGroup.objects.get(name=name)
468
            self.membership_set.create(group=group)
469

    
470
    def save(self, update_timestamps=True, **kwargs):
471
        if update_timestamps:
472
            if not self.id:
473
                self.date_joined = datetime.now()
474
            self.updated = datetime.now()
475

    
476
        # update date_signed_terms if necessary
477
        if self.__has_signed_terms != self.has_signed_terms:
478
            self.date_signed_terms = datetime.now()
479

    
480
        if not self.id:
481
            # set username
482
            while not self.username:
483
                username = uuid.uuid4().hex[:30]
484
                try:
485
                    AstakosUser.objects.get(username=username)
486
                except AstakosUser.DoesNotExist:
487
                    self.username = username
488
            if not self.provider:
489
                self.provider = 'local'
490
            self.email = self.email.lower()
491
        self.validate_unique_email_isactive()
492
        if self.is_active and self.activation_sent:
493
            # reset the activation sent
494
            self.activation_sent = None
495

    
496
        super(AstakosUser, self).save(**kwargs)
497

    
498
    def renew_token(self):
499
        md5 = hashlib.md5()
500
        md5.update(self.username)
501
        md5.update(self.realname.encode('ascii', 'ignore'))
502
        md5.update(asctime())
503

    
504
        self.auth_token = b64encode(md5.digest())
505
        self.auth_token_created = datetime.now()
506
        self.auth_token_expires = self.auth_token_created + \
507
            timedelta(hours=AUTH_TOKEN_DURATION)
508
        msg = 'Token renewed for %s' % self.email
509
        logger.log(LOGGING_LEVEL, msg)
510

    
511
    def __unicode__(self):
512
        return '%s (%s)' % (self.realname, self.email)
513

    
514
    def conflicting_email(self):
515
        q = AstakosUser.objects.exclude(username=self.username)
516
        q = q.filter(email=self.email)
517
        if q.count() != 0:
518
            return True
519
        return False
520

    
521
    def validate_unique_email_isactive(self):
522
        """
523
        Implements a unique_together constraint for email and is_active fields.
524
        """
525
        q = AstakosUser.objects.exclude(username=self.username)
526
        q = q.filter(email=self.email)
527
        q = q.filter(is_active=self.is_active)
528
        if q.count() != 0:
529
            raise ValidationError({'__all__': [_(astakos_messages.UNIQUE_EMAIL_IS_ACTIVE_CONSTRAIN_ERR)]})
530

    
531
    @property
532
    def signed_terms(self):
533
        term = get_latest_terms()
534
        if not term:
535
            return True
536
        if not self.has_signed_terms:
537
            return False
538
        if not self.date_signed_terms:
539
            return False
540
        if self.date_signed_terms < term.date:
541
            self.has_signed_terms = False
542
            self.date_signed_terms = None
543
            self.save()
544
            return False
545
        return True
546

    
547
    def store_disturbed_quota(self, set=True):
548
        self.disturbed_qutoa = set
549
        self.save()
550

    
551

    
552
class Membership(models.Model):
553
    person = models.ForeignKey(AstakosUser)
554
    group = models.ForeignKey(AstakosGroup)
555
    date_requested = models.DateField(default=datetime.now(), blank=True)
556
    date_joined = models.DateField(null=True, db_index=True, blank=True)
557

    
558
    class Meta:
559
        unique_together = ("person", "group")
560

    
561
    def save(self, *args, **kwargs):
562
        if not self.id:
563
            if not self.group.moderation_enabled:
564
                self.date_joined = datetime.now()
565
        super(Membership, self).save(*args, **kwargs)
566

    
567
    @property
568
    def is_approved(self):
569
        if self.date_joined:
570
            return True
571
        return False
572

    
573
    def approve(self):
574
            if self.is_approved:
575
                return
576
        if self.group.max_participants:
577
            assert len(self.group.approved_members) + 1 <= self.group.max_participants, \
578
                    'Maximum participant number has been reached.'
579
        self.date_joined = datetime.now()
580
        self.save()
581
        quota_disturbed.send(sender=self, users=(self.person,))
582

    
583
    def disapprove(self):
584
        self.delete()
585
        quota_disturbed.send(sender=self, users=(self.person,))
586

    
587
class AstakosQuotaManager(models.Manager):
588
    def _update_or_create(self, **kwargs):
589
        assert kwargs, \
590
            'update_or_create() must be passed at least one keyword argument'
591
        obj, created = self.get_or_create(**kwargs)
592
        defaults = kwargs.pop('defaults', {})
593
        if created:
594
            return obj, True, False
595
        else:
596
            try:
597
                params = dict(
598
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
599
                params.update(defaults)
600
                for attr, val in params.items():
601
                    if hasattr(obj, attr):
602
                        setattr(obj, attr, val)
603
                sid = transaction.savepoint()
604
                obj.save(force_update=True)
605
                transaction.savepoint_commit(sid)
606
                return obj, False, True
607
            except IntegrityError, e:
608
                transaction.savepoint_rollback(sid)
609
                try:
610
                    return self.get(**kwargs), False, False
611
                except self.model.DoesNotExist:
612
                    raise e
613

    
614
    update_or_create = _update_or_create
615

    
616
class AstakosGroupQuota(models.Model):
617
    objects = AstakosQuotaManager()
618
    limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
619
    uplimit = models.BigIntegerField('Up limit', null=True)
620
    resource = models.ForeignKey(Resource)
621
    group = models.ForeignKey(AstakosGroup, blank=True)
622

    
623
    class Meta:
624
        unique_together = ("resource", "group")
625

    
626
class AstakosUserQuota(models.Model):
627
    objects = AstakosQuotaManager()
628
    limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
629
    uplimit = models.BigIntegerField('Up limit', null=True)
630
    resource = models.ForeignKey(Resource)
631
    user = models.ForeignKey(AstakosUser)
632

    
633
    class Meta:
634
        unique_together = ("resource", "user")
635

    
636

    
637
class ApprovalTerms(models.Model):
638
    """
639
    Model for approval terms
640
    """
641

    
642
    date = models.DateTimeField(
643
        'Issue date', db_index=True, default=datetime.now())
644
    location = models.CharField('Terms location', max_length=255)
645

    
646

    
647
class Invitation(models.Model):
648
    """
649
    Model for registring invitations
650
    """
651
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
652
                                null=True)
653
    realname = models.CharField('Real name', max_length=255)
654
    username = models.CharField('Unique ID', max_length=255, unique=True)
655
    code = models.BigIntegerField('Invitation code', db_index=True)
656
    is_consumed = models.BooleanField('Consumed?', default=False)
657
    created = models.DateTimeField('Creation date', auto_now_add=True)
658
    consumed = models.DateTimeField('Consumption date', null=True, blank=True)
659

    
660
    def __init__(self, *args, **kwargs):
661
        super(Invitation, self).__init__(*args, **kwargs)
662
        if not self.id:
663
            self.code = _generate_invitation_code()
664

    
665
    def consume(self):
666
        self.is_consumed = True
667
        self.consumed = datetime.now()
668
        self.save()
669

    
670
    def __unicode__(self):
671
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
672

    
673

    
674
class EmailChangeManager(models.Manager):
675
    @transaction.commit_on_success
676
    def change_email(self, activation_key):
677
        """
678
        Validate an activation key and change the corresponding
679
        ``User`` if valid.
680

681
        If the key is valid and has not expired, return the ``User``
682
        after activating.
683

684
        If the key is not valid or has expired, return ``None``.
685

686
        If the key is valid but the ``User`` is already active,
687
        return ``None``.
688

689
        After successful email change the activation record is deleted.
690

691
        Throws ValueError if there is already
692
        """
693
        try:
694
            email_change = self.model.objects.get(
695
                activation_key=activation_key)
696
            if email_change.activation_key_expired():
697
                email_change.delete()
698
                raise EmailChange.DoesNotExist
699
            # is there an active user with this address?
700
            try:
701
                AstakosUser.objects.get(email=email_change.new_email_address)
702
            except AstakosUser.DoesNotExist:
703
                pass
704
            else:
705
                raise ValueError(_(astakos_messages.NEW_EMAIL_ADDR_RESERVED))
706
            # update user
707
            user = AstakosUser.objects.get(pk=email_change.user_id)
708
            user.email = email_change.new_email_address
709
            user.save()
710
            email_change.delete()
711
            return user
712
        except EmailChange.DoesNotExist:
713
            raise ValueError(_(astakos_messages.INVALID_ACTIVATION_KEY))
714

    
715

    
716
class EmailChange(models.Model):
717
    new_email_address = models.EmailField(_(u'new e-mail address'),
718
                                          help_text=_(astakos_messages.EMAIL_CHANGE_NEW_ADDR_HELP))
719
    user = models.ForeignKey(
720
        AstakosUser, unique=True, related_name='emailchange_user')
721
    requested_at = models.DateTimeField(default=datetime.now())
722
    activation_key = models.CharField(
723
        max_length=40, unique=True, db_index=True)
724

    
725
    objects = EmailChangeManager()
726

    
727
    def activation_key_expired(self):
728
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
729
        return self.requested_at + expiration_date < datetime.now()
730

    
731

    
732
class AdditionalMail(models.Model):
733
    """
734
    Model for registring invitations
735
    """
736
    owner = models.ForeignKey(AstakosUser)
737
    email = models.EmailField()
738

    
739

    
740
def _generate_invitation_code():
741
    while True:
742
        code = randint(1, 2L ** 63 - 1)
743
        try:
744
            Invitation.objects.get(code=code)
745
            # An invitation with this code already exists, try again
746
        except Invitation.DoesNotExist:
747
            return code
748

    
749

    
750
def get_latest_terms():
751
    try:
752
        term = ApprovalTerms.objects.order_by('-id')[0]
753
        return term
754
    except IndexError:
755
        pass
756
    return None
757

    
758

    
759
def create_astakos_user(u):
760
    try:
761
        AstakosUser.objects.get(user_ptr=u.pk)
762
    except AstakosUser.DoesNotExist:
763
        extended_user = AstakosUser(user_ptr_id=u.pk)
764
        extended_user.__dict__.update(u.__dict__)
765
#         extended_user.renew_token()
766
        extended_user.save()
767
    except BaseException, e:
768
        logger.exception(e)
769
        pass
770

    
771

    
772
def fix_superusers(sender, **kwargs):
773
    # Associate superusers with AstakosUser
774
    admins = User.objects.filter(is_superuser=True)
775
    for u in admins:
776
        create_astakos_user(u)
777

    
778

    
779
def user_post_save(sender, instance, created, **kwargs):
780
    if not created:
781
        return
782
    create_astakos_user(instance)
783

    
784

    
785
def set_default_group(user):
786
    try:
787
        default = AstakosGroup.objects.get(name='default')
788
        Membership(
789
            group=default, person=user, date_joined=datetime.now()).save()
790
    except AstakosGroup.DoesNotExist, e:
791
        logger.exception(e)
792

    
793

    
794
def astakosuser_pre_save(sender, instance, **kwargs):
795
    instance.aquarium_report = False
796
    instance.new = False
797
    try:
798
        db_instance = AstakosUser.objects.get(id=instance.id)
799
    except AstakosUser.DoesNotExist:
800
        # create event
801
        instance.aquarium_report = True
802
        instance.new = True
803
    else:
804
        get = AstakosUser.__getattribute__
805
        l = filter(lambda f: get(db_instance, f) != get(instance, f),
806
                   BILLING_FIELDS)
807
        instance.aquarium_report = True if l else False
808

    
809

    
810
def astakosuser_post_save(sender, instance, created, **kwargs):
811
    if instance.aquarium_report:
812
        report_user_event(instance, create=instance.new)
813
    if not created:
814
        return
815
    set_default_group(instance)
816
    # TODO handle socket.error & IOError
817
    register_users((instance,))
818
    instance.renew_token()
819

    
820

    
821
def resource_post_save(sender, instance, created, **kwargs):
822
    if not created:
823
        return
824
    register_resources((instance,))
825

    
826

    
827
def send_quota_disturbed(sender, instance, **kwargs):
828
    users = []
829
    extend = users.extend
830
    if sender == Membership:
831
        if not instance.group.is_enabled:
832
            return
833
        extend([instance.person])
834
    elif sender == AstakosUserQuota:
835
        extend([instance.user])
836
    elif sender == AstakosGroupQuota:
837
        if not instance.group.is_enabled:
838
            return
839
        extend(instance.group.astakosuser_set.all())
840
    elif sender == AstakosGroup:
841
        if not instance.is_enabled:
842
            return
843
    quota_disturbed.send(sender=sender, users=users)
844

    
845

    
846
def on_quota_disturbed(sender, users, **kwargs):
847
    print '>>>', locals()
848
    if not users:
849
        return
850
    send_quota(users)
851

    
852
post_syncdb.connect(fix_superusers)
853
post_save.connect(user_post_save, sender=User)
854
pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
855
post_save.connect(astakosuser_post_save, sender=AstakosUser)
856
post_save.connect(resource_post_save, sender=Resource)
857

    
858
quota_disturbed = Signal(providing_args=["users"])
859
quota_disturbed.connect(on_quota_disturbed)
860

    
861
post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
862
post_delete.connect(send_quota_disturbed, sender=Membership)
863
post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
864
post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
865
post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
866
post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)