Merged demo
[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.core.exceptions import ValidationError
48 from django.db import transaction
49 from django.db.models.signals import (pre_save, post_save, post_syncdb,
50                                       post_delete)
51 from django.contrib.contenttypes.models import ContentType
52
53 from django.dispatch import Signal
54 from django.db.models import Q
55
56 from astakos.im.settings import (DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
57                                  AUTH_TOKEN_DURATION, BILLING_FIELDS,
58                                  EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL)
59 from astakos.im.endpoints.quotaholder import (register_users, send_quota,
60                                               register_resources)
61 from astakos.im.endpoints.aquarium.producer import report_user_event
62 from astakos.im.functions import send_invitation
63 from astakos.im.tasks import propagate_groupmembers_quota
64 from astakos.im.functions import send_invitation
65
66 logger = logging.getLogger(__name__)
67
68 DEFAULT_CONTENT_TYPE = None
69 try:
70     content_type = ContentType.objects.get(app_label='im', model='astakosuser')
71 except:
72     content_type = DEFAULT_CONTENT_TYPE
73
74 RESOURCE_SEPARATOR = '.'
75
76
77 class Service(models.Model):
78     name = models.CharField('Name', max_length=255, unique=True, db_index=True)
79     url = models.FilePathField()
80     icon = models.FilePathField(blank=True)
81     auth_token = models.CharField('Authentication Token', max_length=32,
82                                   null=True, blank=True)
83     auth_token_created = models.DateTimeField('Token creation date', null=True)
84     auth_token_expires = models.DateTimeField(
85         'Token expiration date', null=True)
86
87     def save(self, **kwargs):
88         if not self.id:
89             self.renew_token()
90         super(Service, self).save(**kwargs)
91
92     def renew_token(self):
93         md5 = hashlib.md5()
94         md5.update(self.name.encode('ascii', 'ignore'))
95         md5.update(self.url.encode('ascii', 'ignore'))
96         md5.update(asctime())
97
98         self.auth_token = b64encode(md5.digest())
99         self.auth_token_created = datetime.now()
100         self.auth_token_expires = self.auth_token_created + \
101             timedelta(hours=AUTH_TOKEN_DURATION)
102
103     def __str__(self):
104         return self.name
105
106     @property
107     def resources(self):
108         return self.resource_set.all()
109
110     @resources.setter
111     def resources(self, resources):
112         for s in resources:
113             self.resource_set.create(**s)
114     
115     def add_resource(self, service, resource, uplimit, update=True):
116         """Raises ObjectDoesNotExist, IntegrityError"""
117         resource = Resource.objects.get(service__name=service, name=resource)
118         if update:
119             AstakosUserQuota.objects.update_or_create(user=self,
120                                                       resource=resource,
121                                                       defaults={'uplimit': uplimit})
122         else:
123             q = self.astakosuserquota_set
124             q.create(resource=resource, uplimit=uplimit)
125
126
127 class ResourceMetadata(models.Model):
128     key = models.CharField('Name', max_length=255, unique=True, db_index=True)
129     value = models.CharField('Value', max_length=255)
130
131
132 class Resource(models.Model):
133     name = models.CharField('Name', max_length=255, unique=True, db_index=True)
134     meta = models.ManyToManyField(ResourceMetadata)
135     service = models.ForeignKey(Service)
136     desc = models.TextField('Description', null=True)
137     unit = models.CharField('Name', null=True, max_length=255)
138     group = models.CharField('Group', null=True, max_length=255)
139
140     def __str__(self):
141         return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
142
143
144 class GroupKind(models.Model):
145     name = models.CharField('Name', max_length=255, unique=True, db_index=True)
146
147     def __str__(self):
148         return self.name
149
150
151 class AstakosGroup(Group):
152     kind = models.ForeignKey(GroupKind)
153     homepage = models.URLField(
154         'Homepage Url', max_length=255, null=True, blank=True)
155     desc = models.TextField('Description', null=True)
156     policy = models.ManyToManyField(
157         Resource,
158         null=True,
159         blank=True,
160         through='AstakosGroupQuota'
161     )
162     creation_date = models.DateTimeField(
163         'Creation date',
164         default=datetime.now()
165     )
166     issue_date = models.DateTimeField('Issue date', null=True)
167     expiration_date = models.DateTimeField(
168         'Expiration date',
169          null=True
170     )
171     moderation_enabled = models.BooleanField(
172         'Moderated membership?',
173         default=True
174     )
175     approval_date = models.DateTimeField(
176         'Activation date',
177         null=True,
178         blank=True
179     )
180     estimated_participants = models.PositiveIntegerField(
181         'Estimated #members',
182         null=True,
183         blank=True,
184     )
185     max_participants = models.PositiveIntegerField(
186         'Maximum numder of participants',
187         null=True,
188         blank=True
189     )
190     
191     @property
192     def is_disabled(self):
193         if not self.approval_date:
194             return True
195         return False
196
197     @property
198     def is_enabled(self):
199         if self.is_disabled:
200             return False
201         if not self.issue_date:
202             return False
203         if not self.expiration_date:
204             return True
205         now = datetime.now()
206         if self.issue_date > now:
207             return False
208         if now >= self.expiration_date:
209             return False
210         return True
211
212     def enable(self):
213         if self.is_enabled:
214             return
215         self.approval_date = datetime.now()
216         self.save()
217         quota_disturbed.send(sender=self, users=self.approved_members)
218         propagate_groupmembers_quota.apply_async(
219             args=[self], eta=self.issue_date)
220         propagate_groupmembers_quota.apply_async(
221             args=[self], eta=self.expiration_date)
222
223     def disable(self):
224         if self.is_disabled:
225             return
226         self.approval_date = None
227         self.save()
228         quota_disturbed.send(sender=self, users=self.approved_members)
229
230     def approve_member(self, person):
231         m, created = self.membership_set.get_or_create(person=person)
232         # update date_joined in any case
233         m.date_joined = datetime.now()
234         m.save()
235
236     def disapprove_member(self, person):
237         self.membership_set.remove(person=person)
238
239     @property
240     def members(self):
241         q = self.membership_set.select_related().all()
242         return [m.person for m in q]
243     
244     @property
245     def approved_members(self):
246         q = self.membership_set.select_related().all()
247         return [m.person for m in q if m.is_approved]
248
249     @property
250     def quota(self):
251         d = defaultdict(int)
252         for q in self.astakosgroupquota_set.select_related().all():
253             d[q.resource] += q.uplimit
254         return d
255     
256     def add_policy(self, service, resource, uplimit, update=True):
257         """Raises ObjectDoesNotExist, IntegrityError"""
258         print '#', locals()
259         resource = Resource.objects.get(service__name=service, name=resource)
260         if update:
261             AstakosGroupQuota.objects.update_or_create(
262                 group=self,
263                 resource=resource,
264                 defaults={'uplimit': uplimit}
265             )
266         else:
267             q = self.astakosgroupquota_set
268             q.create(resource=resource, uplimit=uplimit)
269     
270     @property
271     def policies(self):
272         return self.astakosgroupquota_set.select_related().all()
273
274     @policies.setter
275     def policies(self, policies):
276         for p in policies:
277             service = p.get('service', None)
278             resource = p.get('resource', None)
279             uplimit = p.get('uplimit', 0)
280             update = p.get('update', True)
281             self.add_policy(service, resource, uplimit, update)
282     
283     @property
284     def owners(self):
285         return self.owner.all()
286
287     @property
288     def owner_details(self):
289         return self.owner.select_related().all()
290
291     @owners.setter
292     def owners(self, l):
293         self.owner = l
294         map(self.approve_member, l)
295
296
297 class AstakosUser(User):
298     """
299     Extends ``django.contrib.auth.models.User`` by defining additional fields.
300     """
301     # Use UserManager to get the create_user method, etc.
302     objects = UserManager()
303
304     affiliation = models.CharField('Affiliation', max_length=255, blank=True)
305     provider = models.CharField('Provider', max_length=255, blank=True)
306
307     #for invitations
308     user_level = DEFAULT_USER_LEVEL
309     level = models.IntegerField('Inviter level', default=user_level)
310     invitations = models.IntegerField(
311         'Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
312
313     auth_token = models.CharField('Authentication Token', max_length=32,
314                                   null=True, blank=True)
315     auth_token_created = models.DateTimeField('Token creation date', null=True)
316     auth_token_expires = models.DateTimeField(
317         'Token expiration date', null=True)
318
319     updated = models.DateTimeField('Update date')
320     is_verified = models.BooleanField('Is verified?', default=False)
321
322     # ex. screen_name for twitter, eppn for shibboleth
323     third_party_identifier = models.CharField(
324         'Third-party identifier', max_length=255, null=True, blank=True)
325
326     email_verified = models.BooleanField('Email verified?', default=False)
327
328     has_credits = models.BooleanField('Has credits?', default=False)
329     has_signed_terms = models.BooleanField(
330         'I agree with the terms', default=False)
331     date_signed_terms = models.DateTimeField(
332         'Signed terms date', null=True, blank=True)
333
334     activation_sent = models.DateTimeField(
335         'Activation sent data', null=True, blank=True)
336
337     policy = models.ManyToManyField(
338         Resource, null=True, through='AstakosUserQuota')
339
340     astakos_groups = models.ManyToManyField(
341         AstakosGroup, verbose_name=_('agroups'), blank=True,
342         help_text=_("""In addition to the permissions manually assigned, this
343                     user will also get all permissions granted to each group
344                     he/she is in."""),
345         through='Membership')
346
347     __has_signed_terms = False
348     disturbed_quota = models.BooleanField('Needs quotaholder syncing',
349                                            default=False, db_index=True)
350
351     owner = models.ManyToManyField(
352         AstakosGroup, related_name='owner', null=True)
353
354     class Meta:
355         unique_together = ("provider", "third_party_identifier")
356
357     def __init__(self, *args, **kwargs):
358         super(AstakosUser, self).__init__(*args, **kwargs)
359         self.__has_signed_terms = self.has_signed_terms
360         if not self.id and not self.is_active:
361             self.is_active = False
362
363     @property
364     def realname(self):
365         return '%s %s' % (self.first_name, self.last_name)
366
367     @realname.setter
368     def realname(self, value):
369         parts = value.split(' ')
370         if len(parts) == 2:
371             self.first_name = parts[0]
372             self.last_name = parts[1]
373         else:
374             self.last_name = parts[0]
375
376     def add_permission(self, pname):
377         if self.has_perm(pname):
378             return
379         p, created = Permission.objects.get_or_create(codename=pname,
380                                                       name=pname.capitalize(),
381                                                       content_type=content_type)
382         self.user_permissions.add(p)
383
384     def remove_permission(self, pname):
385         if self.has_perm(pname):
386             return
387         p = Permission.objects.get(codename=pname,
388                                    content_type=content_type)
389         self.user_permissions.remove(p)
390
391     @property
392     def invitation(self):
393         try:
394             return Invitation.objects.get(username=self.email)
395         except Invitation.DoesNotExist:
396             return None
397
398     def invite(self, email, realname):
399         inv = Invitation(inviter=self, username=email, realname=realname)
400         inv.save()
401         send_invitation(inv)
402         self.invitations = max(0, self.invitations - 1)
403         self.save()
404
405     @property
406     def quota(self):
407         """Returns a dict with the sum of quota limits per resource"""
408         d = defaultdict(int)
409         for q in self.policies:
410             d[q.resource] += q.uplimit
411         for m in self.extended_groups:
412             if not m.is_approved:
413                 continue
414             g = m.group
415             if not g.is_enabled:
416                 continue
417             for r, uplimit in g.quota.iteritems():
418                 d[r] += uplimit
419
420         # TODO set default for remaining
421         return d
422
423     @property
424     def policies(self):
425         return self.astakosuserquota_set.select_related().all()
426
427     @policies.setter
428     def policies(self, policies):
429         for p in policies:
430             service = policies.get('service', None)
431             resource = policies.get('resource', None)
432             uplimit = policies.get('uplimit', 0)
433             update = policies.get('update', True)
434             self.add_policy(service, resource, uplimit, update)
435
436     def add_policy(self, service, resource, uplimit, update=True):
437         """Raises ObjectDoesNotExist, IntegrityError"""
438         resource = Resource.objects.get(service__name=service, name=resource)
439         if update:
440             AstakosUserQuota.objects.update_or_create(user=self,
441                                                       resource=resource,
442                                                       defaults={'uplimit': uplimit})
443         else:
444             q = self.astakosuserquota_set
445             q.create(resource=resource, uplimit=uplimit)
446
447     def remove_policy(self, service, resource):
448         """Raises ObjectDoesNotExist, IntegrityError"""
449         resource = Resource.objects.get(service__name=service, name=resource)
450         q = self.policies.get(resource=resource).delete()
451
452     @property
453     def extended_groups(self):
454         return self.membership_set.select_related().all()
455
456     @extended_groups.setter
457     def extended_groups(self, groups):
458         #TODO exceptions
459         for name in groups:
460             group = AstakosGroup.objects.get(name=name)
461             self.membership_set.create(group=group)
462
463     def save(self, update_timestamps=True, **kwargs):
464         if update_timestamps:
465             if not self.id:
466                 self.date_joined = datetime.now()
467             self.updated = datetime.now()
468
469         # update date_signed_terms if necessary
470         if self.__has_signed_terms != self.has_signed_terms:
471             self.date_signed_terms = datetime.now()
472
473         if not self.id:
474             # set username
475             while not self.username:
476                 username = uuid.uuid4().hex[:30]
477                 try:
478                     AstakosUser.objects.get(username=username)
479                 except AstakosUser.DoesNotExist:
480                     self.username = username
481             if not self.provider:
482                 self.provider = 'local'
483             self.email = self.email.lower()
484         self.validate_unique_email_isactive()
485         if self.is_active and self.activation_sent:
486             # reset the activation sent
487             self.activation_sent = None
488
489         super(AstakosUser, self).save(**kwargs)
490
491     def renew_token(self):
492         md5 = hashlib.md5()
493         md5.update(self.username)
494         md5.update(self.realname.encode('ascii', 'ignore'))
495         md5.update(asctime())
496
497         self.auth_token = b64encode(md5.digest())
498         self.auth_token_created = datetime.now()
499         self.auth_token_expires = self.auth_token_created + \
500             timedelta(hours=AUTH_TOKEN_DURATION)
501         msg = 'Token renewed for %s' % self.email
502         logger.log(LOGGING_LEVEL, msg)
503
504     def __unicode__(self):
505         return '%s (%s)' % (self.realname, self.email)
506
507     def conflicting_email(self):
508         q = AstakosUser.objects.exclude(username=self.username)
509         q = q.filter(email=self.email)
510         if q.count() != 0:
511             return True
512         return False
513
514     def validate_unique_email_isactive(self):
515         """
516         Implements a unique_together constraint for email and is_active fields.
517         """
518         q = AstakosUser.objects.exclude(username=self.username)
519         q = q.filter(email=self.email)
520         q = q.filter(is_active=self.is_active)
521         if q.count() != 0:
522             raise ValidationError({'__all__': [_('Another account with the same email & is_active combination found.')]})
523
524     @property
525     def signed_terms(self):
526         term = get_latest_terms()
527         if not term:
528             return True
529         if not self.has_signed_terms:
530             return False
531         if not self.date_signed_terms:
532             return False
533         if self.date_signed_terms < term.date:
534             self.has_signed_terms = False
535             self.date_signed_terms = None
536             self.save()
537             return False
538         return True
539
540     def store_disturbed_quota(self, set=True):
541         self.disturbed_qutoa = set
542         self.save()
543
544
545 class Membership(models.Model):
546     person = models.ForeignKey(AstakosUser)
547     group = models.ForeignKey(AstakosGroup)
548     date_requested = models.DateField(default=datetime.now(), blank=True)
549     date_joined = models.DateField(null=True, db_index=True, blank=True)
550
551     class Meta:
552         unique_together = ("person", "group")
553
554     def save(self, *args, **kwargs):
555         if not self.id:
556             if not self.group.moderation_enabled:
557                 self.date_joined = datetime.now()
558         super(Membership, self).save(*args, **kwargs)
559
560     @property
561     def is_approved(self):
562         if self.date_joined:
563             return True
564         return False
565
566     def approve(self):
567         self.date_joined = datetime.now()
568         self.save()
569         quota_disturbed.send(sender=self, users=(self.person,))
570
571     def disapprove(self):
572         self.delete()
573         quota_disturbed.send(sender=self, users=(self.person,))
574
575 class AstakosQuotaManager(models.Manager):
576     def _update_or_create(self, **kwargs):
577         assert kwargs, \
578             'update_or_create() must be passed at least one keyword argument'
579         obj, created = self.get_or_create(**kwargs)
580         defaults = kwargs.pop('defaults', {})
581         if created:
582             return obj, True, False
583         else:
584             try:
585                 params = dict(
586                     [(k, v) for k, v in kwargs.items() if '__' not in k])
587                 params.update(defaults)
588                 for attr, val in params.items():
589                     if hasattr(obj, attr):
590                         setattr(obj, attr, val)
591                 sid = transaction.savepoint()
592                 obj.save(force_update=True)
593                 transaction.savepoint_commit(sid)
594                 return obj, False, True
595             except IntegrityError, e:
596                 transaction.savepoint_rollback(sid)
597                 try:
598                     return self.get(**kwargs), False, False
599                 except self.model.DoesNotExist:
600                     raise e
601
602     update_or_create = _update_or_create
603
604 class AstakosGroupQuota(models.Model):
605     objects = AstakosQuotaManager()
606     limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
607     uplimit = models.BigIntegerField('Up limit', null=True)
608     resource = models.ForeignKey(Resource)
609     group = models.ForeignKey(AstakosGroup, blank=True)
610
611     class Meta:
612         unique_together = ("resource", "group")
613
614 class AstakosUserQuota(models.Model):
615     objects = AstakosQuotaManager()
616     limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
617     uplimit = models.BigIntegerField('Up limit', null=True)
618     resource = models.ForeignKey(Resource)
619     user = models.ForeignKey(AstakosUser)
620
621     class Meta:
622         unique_together = ("resource", "user")
623
624
625 class ApprovalTerms(models.Model):
626     """
627     Model for approval terms
628     """
629
630     date = models.DateTimeField(
631         'Issue date', db_index=True, default=datetime.now())
632     location = models.CharField('Terms location', max_length=255)
633
634
635 class Invitation(models.Model):
636     """
637     Model for registring invitations
638     """
639     inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
640                                 null=True)
641     realname = models.CharField('Real name', max_length=255)
642     username = models.CharField('Unique ID', max_length=255, unique=True)
643     code = models.BigIntegerField('Invitation code', db_index=True)
644     is_consumed = models.BooleanField('Consumed?', default=False)
645     created = models.DateTimeField('Creation date', auto_now_add=True)
646     consumed = models.DateTimeField('Consumption date', null=True, blank=True)
647
648     def __init__(self, *args, **kwargs):
649         super(Invitation, self).__init__(*args, **kwargs)
650         if not self.id:
651             self.code = _generate_invitation_code()
652
653     def consume(self):
654         self.is_consumed = True
655         self.consumed = datetime.now()
656         self.save()
657
658     def __unicode__(self):
659         return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
660
661
662 class EmailChangeManager(models.Manager):
663     @transaction.commit_on_success
664     def change_email(self, activation_key):
665         """
666         Validate an activation key and change the corresponding
667         ``User`` if valid.
668
669         If the key is valid and has not expired, return the ``User``
670         after activating.
671
672         If the key is not valid or has expired, return ``None``.
673
674         If the key is valid but the ``User`` is already active,
675         return ``None``.
676
677         After successful email change the activation record is deleted.
678
679         Throws ValueError if there is already
680         """
681         try:
682             email_change = self.model.objects.get(
683                 activation_key=activation_key)
684             if email_change.activation_key_expired():
685                 email_change.delete()
686                 raise EmailChange.DoesNotExist
687             # is there an active user with this address?
688             try:
689                 AstakosUser.objects.get(email=email_change.new_email_address)
690             except AstakosUser.DoesNotExist:
691                 pass
692             else:
693                 raise ValueError(_('The new email address is reserved.'))
694             # update user
695             user = AstakosUser.objects.get(pk=email_change.user_id)
696             user.email = email_change.new_email_address
697             user.save()
698             email_change.delete()
699             return user
700         except EmailChange.DoesNotExist:
701             raise ValueError(_('Invalid activation key'))
702
703
704 class EmailChange(models.Model):
705     new_email_address = models.EmailField(_(u'new e-mail address'),
706                                           help_text=_(u'Your old email address will be used until you verify your new one.'))
707     user = models.ForeignKey(
708         AstakosUser, unique=True, related_name='emailchange_user')
709     requested_at = models.DateTimeField(default=datetime.now())
710     activation_key = models.CharField(
711         max_length=40, unique=True, db_index=True)
712
713     objects = EmailChangeManager()
714
715     def activation_key_expired(self):
716         expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
717         return self.requested_at + expiration_date < datetime.now()
718
719
720 class AdditionalMail(models.Model):
721     """
722     Model for registring invitations
723     """
724     owner = models.ForeignKey(AstakosUser)
725     email = models.EmailField()
726
727
728 def _generate_invitation_code():
729     while True:
730         code = randint(1, 2L ** 63 - 1)
731         try:
732             Invitation.objects.get(code=code)
733             # An invitation with this code already exists, try again
734         except Invitation.DoesNotExist:
735             return code
736
737
738 def get_latest_terms():
739     try:
740         term = ApprovalTerms.objects.order_by('-id')[0]
741         return term
742     except IndexError:
743         pass
744     return None
745
746
747 def create_astakos_user(u):
748     try:
749         AstakosUser.objects.get(user_ptr=u.pk)
750     except AstakosUser.DoesNotExist:
751         extended_user = AstakosUser(user_ptr_id=u.pk)
752         extended_user.__dict__.update(u.__dict__)
753         extended_user.renew_token()
754         extended_user.save()
755     except BaseException, e:
756         logger.exception(e)
757         pass
758
759
760 def fix_superusers(sender, **kwargs):
761     # Associate superusers with AstakosUser
762     admins = User.objects.filter(is_superuser=True)
763     for u in admins:
764         create_astakos_user(u)
765
766
767 def user_post_save(sender, instance, created, **kwargs):
768     if not created:
769         return
770     create_astakos_user(instance)
771
772
773 def set_default_group(user):
774     try:
775         default = AstakosGroup.objects.get(name='default')
776         Membership(
777             group=default, person=user, date_joined=datetime.now()).save()
778     except AstakosGroup.DoesNotExist, e:
779         logger.exception(e)
780
781
782 def astakosuser_pre_save(sender, instance, **kwargs):
783     instance.aquarium_report = False
784     instance.new = False
785     try:
786         db_instance = AstakosUser.objects.get(id=instance.id)
787     except AstakosUser.DoesNotExist:
788         # create event
789         instance.aquarium_report = True
790         instance.new = True
791     else:
792         get = AstakosUser.__getattribute__
793         l = filter(lambda f: get(db_instance, f) != get(instance, f),
794                    BILLING_FIELDS)
795         instance.aquarium_report = True if l else False
796
797
798 def astakosuser_post_save(sender, instance, created, **kwargs):
799     if instance.aquarium_report:
800         report_user_event(instance, create=instance.new)
801     if not created:
802         return
803     set_default_group(instance)
804     # TODO handle socket.error & IOError
805     register_users((instance,))
806     instance.renew_token()
807
808
809 def resource_post_save(sender, instance, created, **kwargs):
810     if not created:
811         return
812     register_resources((instance,))
813
814
815 def send_quota_disturbed(sender, instance, **kwargs):
816     users = []
817     extend = users.extend
818     if sender == Membership:
819         if not instance.group.is_enabled:
820             return
821         extend([instance.person])
822     elif sender == AstakosUserQuota:
823         extend([instance.user])
824     elif sender == AstakosGroupQuota:
825         if not instance.group.is_enabled:
826             return
827         extend(instance.group.astakosuser_set.all())
828     elif sender == AstakosGroup:
829         if not instance.is_enabled:
830             return
831     quota_disturbed.send(sender=sender, users=users)
832
833
834 def on_quota_disturbed(sender, users, **kwargs):
835     print '>>>', locals()
836     if not users:
837         return
838     send_quota(users)
839
840 post_syncdb.connect(fix_superusers)
841 post_save.connect(user_post_save, sender=User)
842 pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
843 post_save.connect(astakosuser_post_save, sender=AstakosUser)
844 post_save.connect(resource_post_save, sender=Resource)
845
846 quota_disturbed = Signal(providing_args=["users"])
847 quota_disturbed.connect(on_quota_disturbed)
848
849 post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
850 post_delete.connect(send_quota_disturbed, sender=Membership)
851 post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
852 post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
853 post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
854 post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)