Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (28.7 kB)

1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
import hashlib
35
import uuid
36
import logging
37

    
38
from time import asctime
39
from datetime import datetime, timedelta
40
from base64 import b64encode
41
from random import randint
42
from collections import defaultdict
43

    
44
from django.db import models, 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.group.max_participants:
575
            assert len(self.group.approved_members) + 1 <= self.group.max_participants, \
576
                    'Maximum participant number has been reached.'
577
        self.date_joined = datetime.now()
578
        self.save()
579
        quota_disturbed.send(sender=self, users=(self.person,))
580

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

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

    
612
    update_or_create = _update_or_create
613

    
614
class AstakosGroupQuota(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
    group = models.ForeignKey(AstakosGroup, blank=True)
620

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

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

    
631
    class Meta:
632
        unique_together = ("resource", "user")
633

    
634

    
635
class ApprovalTerms(models.Model):
636
    """
637
    Model for approval terms
638
    """
639

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

    
644

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

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

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

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

    
671

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

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

682
        If the key is not valid or has expired, return ``None``.
683

684
        If the key is valid but the ``User`` is already active,
685
        return ``None``.
686

687
        After successful email change the activation record is deleted.
688

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

    
713

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

    
723
    objects = EmailChangeManager()
724

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

    
729

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

    
737

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

    
747

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

    
756

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

    
769

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

    
776

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

    
782

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

    
791

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

    
807

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

    
818

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

    
824

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

    
843

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

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

    
856
quota_disturbed = Signal(providing_args=["users"])
857
quota_disturbed.connect(on_quota_disturbed)
858

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