Check if group member is already approved during approve procedure.
[astakos] / snf-astakos-app / astakos / im / models.py
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)