Isolate astakos messages in separate module
[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 import astakos.im.messages as astakos_messages
67
68 logger = logging.getLogger(__name__)
69
70 DEFAULT_CONTENT_TYPE = None
71 try:
72     content_type = ContentType.objects.get(app_label='im', model='astakosuser')
73 except:
74     content_type = DEFAULT_CONTENT_TYPE
75
76 RESOURCE_SEPARATOR = '.'
77
78 inf = float('inf')
79
80 class Service(models.Model):
81     name = models.CharField('Name', max_length=255, unique=True, db_index=True)
82     url = models.FilePathField()
83     icon = models.FilePathField(blank=True)
84     auth_token = models.CharField('Authentication Token', max_length=32,
85                                   null=True, blank=True)
86     auth_token_created = models.DateTimeField('Token creation date', null=True)
87     auth_token_expires = models.DateTimeField(
88         'Token expiration date', null=True)
89
90     def save(self, **kwargs):
91         if not self.id:
92             self.renew_token()
93         super(Service, self).save(**kwargs)
94
95     def renew_token(self):
96         md5 = hashlib.md5()
97         md5.update(self.name.encode('ascii', 'ignore'))
98         md5.update(self.url.encode('ascii', 'ignore'))
99         md5.update(asctime())
100
101         self.auth_token = b64encode(md5.digest())
102         self.auth_token_created = datetime.now()
103         self.auth_token_expires = self.auth_token_created + \
104             timedelta(hours=AUTH_TOKEN_DURATION)
105
106     def __str__(self):
107         return self.name
108
109     @property
110     def resources(self):
111         return self.resource_set.all()
112
113     @resources.setter
114     def resources(self, resources):
115         for s in resources:
116             self.resource_set.create(**s)
117     
118     def add_resource(self, service, resource, uplimit, update=True):
119         """Raises ObjectDoesNotExist, IntegrityError"""
120         resource = Resource.objects.get(service__name=service, name=resource)
121         if update:
122             AstakosUserQuota.objects.update_or_create(user=self,
123                                                       resource=resource,
124                                                       defaults={'uplimit': uplimit})
125         else:
126             q = self.astakosuserquota_set
127             q.create(resource=resource, uplimit=uplimit)
128
129
130 class ResourceMetadata(models.Model):
131     key = models.CharField('Name', max_length=255, unique=True, db_index=True)
132     value = models.CharField('Value', max_length=255)
133
134
135 class Resource(models.Model):
136     name = models.CharField('Name', max_length=255, unique=True, db_index=True)
137     meta = models.ManyToManyField(ResourceMetadata)
138     service = models.ForeignKey(Service)
139     desc = models.TextField('Description', null=True)
140     unit = models.CharField('Name', null=True, max_length=255)
141     group = models.CharField('Group', null=True, max_length=255)
142
143     def __str__(self):
144         return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
145
146
147 class GroupKind(models.Model):
148     name = models.CharField('Name', max_length=255, unique=True, db_index=True)
149
150     def __str__(self):
151         return self.name
152
153
154 class AstakosGroup(Group):
155     kind = models.ForeignKey(GroupKind)
156     homepage = models.URLField(
157         'Homepage Url', max_length=255, null=True, blank=True)
158     desc = models.TextField('Description', null=True)
159     policy = models.ManyToManyField(
160         Resource,
161         null=True,
162         blank=True,
163         through='AstakosGroupQuota'
164     )
165     creation_date = models.DateTimeField(
166         'Creation date',
167         default=datetime.now()
168     )
169     issue_date = models.DateTimeField('Issue date', null=True)
170     expiration_date = models.DateTimeField(
171         'Expiration date',
172          null=True
173     )
174     moderation_enabled = models.BooleanField(
175         'Moderated membership?',
176         default=True
177     )
178     approval_date = models.DateTimeField(
179         'Activation date',
180         null=True,
181         blank=True
182     )
183     estimated_participants = models.PositiveIntegerField(
184         'Estimated #members',
185         null=True,
186         blank=True,
187     )
188     max_participants = models.PositiveIntegerField(
189         'Maximum numder of participants',
190         null=True,
191         blank=True
192     )
193     
194     @property
195     def is_disabled(self):
196         if not self.approval_date:
197             return True
198         return False
199
200     @property
201     def is_enabled(self):
202         if self.is_disabled:
203             return False
204         if not self.issue_date:
205             return False
206         if not self.expiration_date:
207             return True
208         now = datetime.now()
209         if self.issue_date > now:
210             return False
211         if now >= self.expiration_date:
212             return False
213         return True
214
215     def enable(self):
216         if self.is_enabled:
217             return
218         self.approval_date = datetime.now()
219         self.save()
220         quota_disturbed.send(sender=self, users=self.approved_members)
221         propagate_groupmembers_quota.apply_async(
222             args=[self], eta=self.issue_date)
223         propagate_groupmembers_quota.apply_async(
224             args=[self], eta=self.expiration_date)
225
226     def disable(self):
227         if self.is_disabled:
228             return
229         self.approval_date = None
230         self.save()
231         quota_disturbed.send(sender=self, users=self.approved_members)
232
233     def approve_member(self, person):
234         m, created = self.membership_set.get_or_create(person=person)
235         # update date_joined in any case
236         m.date_joined = datetime.now()
237         m.save()
238
239     def disapprove_member(self, person):
240         self.membership_set.remove(person=person)
241
242     @property
243     def members(self):
244         q = self.membership_set.select_related().all()
245         return [m.person for m in q]
246     
247     @property
248     def approved_members(self):
249         q = self.membership_set.select_related().all()
250         return [m.person for m in q if m.is_approved]
251
252     @property
253     def quota(self):
254         d = defaultdict(int)
255         for q in self.astakosgroupquota_set.select_related().all():
256             d[q.resource] += q.uplimit or inf
257         return d
258     
259     def add_policy(self, service, resource, uplimit, update=True):
260         """Raises ObjectDoesNotExist, IntegrityError"""
261         print '#', locals()
262         resource = Resource.objects.get(service__name=service, name=resource)
263         if update:
264             AstakosGroupQuota.objects.update_or_create(
265                 group=self,
266                 resource=resource,
267                 defaults={'uplimit': uplimit}
268             )
269         else:
270             q = self.astakosgroupquota_set
271             q.create(resource=resource, uplimit=uplimit)
272     
273     @property
274     def policies(self):
275         return self.astakosgroupquota_set.select_related().all()
276
277     @policies.setter
278     def policies(self, policies):
279         for p in policies:
280             service = p.get('service', None)
281             resource = p.get('resource', None)
282             uplimit = p.get('uplimit', 0)
283             update = p.get('update', True)
284             self.add_policy(service, resource, uplimit, update)
285     
286     @property
287     def owners(self):
288         return self.owner.all()
289
290     @property
291     def owner_details(self):
292         return self.owner.select_related().all()
293
294     @owners.setter
295     def owners(self, l):
296         self.owner = l
297         map(self.approve_member, l)
298
299
300 class AstakosUser(User):
301     """
302     Extends ``django.contrib.auth.models.User`` by defining additional fields.
303     """
304     # Use UserManager to get the create_user method, etc.
305     objects = UserManager()
306
307     affiliation = models.CharField('Affiliation', max_length=255, blank=True)
308     provider = models.CharField('Provider', max_length=255, blank=True)
309
310     #for invitations
311     user_level = DEFAULT_USER_LEVEL
312     level = models.IntegerField('Inviter level', default=user_level)
313     invitations = models.IntegerField(
314         'Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
315
316     auth_token = models.CharField('Authentication Token', max_length=32,
317                                   null=True, blank=True)
318     auth_token_created = models.DateTimeField('Token creation date', null=True)
319     auth_token_expires = models.DateTimeField(
320         'Token expiration date', null=True)
321
322     updated = models.DateTimeField('Update date')
323     is_verified = models.BooleanField('Is verified?', default=False)
324
325     # ex. screen_name for twitter, eppn for shibboleth
326     third_party_identifier = models.CharField(
327         'Third-party identifier', max_length=255, null=True, blank=True)
328
329     email_verified = models.BooleanField('Email verified?', default=False)
330
331     has_credits = models.BooleanField('Has credits?', default=False)
332     has_signed_terms = models.BooleanField(
333         'I agree with the terms', default=False)
334     date_signed_terms = models.DateTimeField(
335         'Signed terms date', null=True, blank=True)
336
337     activation_sent = models.DateTimeField(
338         'Activation sent data', null=True, blank=True)
339
340     policy = models.ManyToManyField(
341         Resource, null=True, through='AstakosUserQuota')
342
343     astakos_groups = models.ManyToManyField(
344         AstakosGroup, verbose_name=_('agroups'), blank=True,
345         help_text=_(astakos_messages.ASTAKOSUSER_GROUPS_HELP),
346         through='Membership')
347
348     __has_signed_terms = False
349     disturbed_quota = models.BooleanField('Needs quotaholder syncing',
350                                            default=False, db_index=True)
351
352     owner = models.ManyToManyField(
353         AstakosGroup, related_name='owner', null=True)
354
355     class Meta:
356         unique_together = ("provider", "third_party_identifier")
357
358     def __init__(self, *args, **kwargs):
359         super(AstakosUser, self).__init__(*args, **kwargs)
360         self.__has_signed_terms = self.has_signed_terms
361         if not self.id:
362             self.is_active = False
363
364     @property
365     def realname(self):
366         return '%s %s' % (self.first_name, self.last_name)
367
368     @realname.setter
369     def realname(self, value):
370         parts = value.split(' ')
371         if len(parts) == 2:
372             self.first_name = parts[0]
373             self.last_name = parts[1]
374         else:
375             self.last_name = parts[0]
376
377     def add_permission(self, pname):
378         if self.has_perm(pname):
379             return
380         p, created = Permission.objects.get_or_create(codename=pname,
381                                                       name=pname.capitalize(),
382                                                       content_type=content_type)
383         self.user_permissions.add(p)
384
385     def remove_permission(self, pname):
386         if self.has_perm(pname):
387             return
388         p = Permission.objects.get(codename=pname,
389                                    content_type=content_type)
390         self.user_permissions.remove(p)
391
392     @property
393     def invitation(self):
394         try:
395             return Invitation.objects.get(username=self.email)
396         except Invitation.DoesNotExist:
397             return None
398
399     def invite(self, email, realname):
400         inv = Invitation(inviter=self, username=email, realname=realname)
401         inv.save()
402         send_invitation(inv)
403         self.invitations = max(0, self.invitations - 1)
404         self.save()
405
406     @property
407     def quota(self):
408         """Returns a dict with the sum of quota limits per resource"""
409         d = defaultdict(int)
410         for q in self.policies:
411             d[q.resource] += q.uplimit or inf
412         for m in self.extended_groups:
413             if not m.is_approved:
414                 continue
415             g = m.group
416             if not g.is_enabled:
417                 continue
418             for r, uplimit in g.quota.iteritems():
419                 d[r] += uplimit or inf
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 or ()):
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__': [_(astakos_messages.UNIQUE_EMAIL_IS_ACTIVE_CONSTRAIN_ERR)]})
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(_(astakos_messages.NEW_EMAIL_ADDR_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(_(astakos_messages.INVALID_ACTIVATION_KEY))
702
703
704 class EmailChange(models.Model):
705     new_email_address = models.EmailField(_(u'new e-mail address'),
706                                           help_text=_(astakos_messages.EMAIL_CHANGE_NEW_ADDR_HELP))
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)