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