Statistics
| Branch: | Tag: | Revision:

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

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
        self.date_joined = datetime.now()
577
        self.save()
578
        quota_disturbed.send(sender=self, users=(self.person,))
579

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

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

    
611
    update_or_create = _update_or_create
612

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

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

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

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

    
633

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

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

    
643

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

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

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

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

    
670

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

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

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

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

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

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

    
712

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

    
722
    objects = EmailChangeManager()
723

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

    
728

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

    
736

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

    
746

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

    
755

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

    
768

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

    
775

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

    
781

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

    
790

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

    
806

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

    
817

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

    
823

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

    
842

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

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

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

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