Fix membership views and other bugs
[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 urlparse import urlparse
42 from urllib import quote
43 from random import randint
44 from collections import defaultdict
45
46 from django.db import models, IntegrityError
47 from django.contrib.auth.models import User, UserManager, Group, Permission
48 from django.utils.translation import ugettext as _
49 from django.db import transaction
50 from django.core.exceptions import ValidationError
51 from django.db.models.signals import (
52     pre_save, post_save, post_syncdb, post_delete
53 )
54 from django.contrib.contenttypes.models import ContentType
55
56 from django.dispatch import Signal
57 from django.db.models import Q
58 from django.core.urlresolvers import reverse
59 from django.utils.http import int_to_base36
60 from django.contrib.auth.tokens import default_token_generator
61 from django.conf import settings
62 from django.utils.importlib import import_module
63 from django.core.validators import email_re
64 from django.core.exceptions import PermissionDenied
65 from django.views.generic.create_update import lookup_object
66 from django.core.exceptions import ObjectDoesNotExist
67
68 from astakos.im.settings import (
69     DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
70     AUTH_TOKEN_DURATION, BILLING_FIELDS,
71     EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL,
72     GROUP_CREATION_SUBJECT, SITENAME
73 )
74 from astakos.im.endpoints.qh import (
75     register_users, send_quota, register_resources
76 )
77 from astakos.im import auth_providers
78 from astakos.im.endpoints.aquarium.producer import report_user_event
79 from astakos.im.functions import send_invitation
80 #from astakos.im.tasks import propagate_groupmembers_quota
81
82 from astakos.im.notifications import build_notification
83
84 import astakos.im.messages as astakos_messages
85
86 logger = logging.getLogger(__name__)
87
88 DEFAULT_CONTENT_TYPE = None
89 _content_type = None
90
91 PENDING, APPROVED, REPLACED, UNKNOWN = 'Pending', 'Approved', 'Replaced', 'Unknown'
92
93 def get_content_type():
94     global _content_type
95     if _content_type is not None:
96         return _content_type
97
98     try:
99         content_type = ContentType.objects.get(app_label='im', model='astakosuser')
100     except:
101         content_type = DEFAULT_CONTENT_TYPE
102     _content_type = content_type
103     return content_type
104
105 RESOURCE_SEPARATOR = '.'
106
107 inf = float('inf')
108
109 class Service(models.Model):
110     name = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
111     url = models.FilePathField()
112     icon = models.FilePathField(blank=True)
113     auth_token = models.CharField(_('Authentication Token'), max_length=32,
114                                   null=True, blank=True)
115     auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
116     auth_token_expires = models.DateTimeField(
117         _('Token expiration date'), null=True)
118
119     def renew_token(self):
120         md5 = hashlib.md5()
121         md5.update(self.name.encode('ascii', 'ignore'))
122         md5.update(self.url.encode('ascii', 'ignore'))
123         md5.update(asctime())
124
125         self.auth_token = b64encode(md5.digest())
126         self.auth_token_created = datetime.now()
127         self.auth_token_expires = self.auth_token_created + \
128             timedelta(hours=AUTH_TOKEN_DURATION)
129
130     def __str__(self):
131         return self.name
132
133     @property
134     def resources(self):
135         return self.resource_set.all()
136
137     @resources.setter
138     def resources(self, resources):
139         for s in resources:
140             self.resource_set.create(**s)
141
142     def add_resource(self, service, resource, uplimit, update=True):
143         """Raises ObjectDoesNotExist, IntegrityError"""
144         resource = Resource.objects.get(service__name=service, name=resource)
145         if update:
146             AstakosUserQuota.objects.update_or_create(user=self,
147                                                       resource=resource,
148                                                       defaults={'uplimit': uplimit})
149         else:
150             q = self.astakosuserquota_set
151             q.create(resource=resource, uplimit=uplimit)
152
153
154 class ResourceMetadata(models.Model):
155     key = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
156     value = models.CharField(_('Value'), max_length=255)
157
158
159 class Resource(models.Model):
160     name = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
161     meta = models.ManyToManyField(ResourceMetadata)
162     service = models.ForeignKey(Service)
163     desc = models.TextField(_('Description'), null=True)
164     unit = models.CharField(_('Name'), null=True, max_length=255)
165     group = models.CharField(_('Group'), null=True, max_length=255)
166
167     def __str__(self):
168         return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
169
170
171 class GroupKind(models.Model):
172     name = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
173
174     def __str__(self):
175         return self.name
176
177
178 class AstakosGroup(Group):
179     kind = models.ForeignKey(GroupKind)
180     homepage = models.URLField(
181         _('Homepage Url'), max_length=255, null=True, blank=True)
182     desc = models.TextField(_('Description'), null=True)
183     policy = models.ManyToManyField(
184         Resource,
185         null=True,
186         blank=True,
187         through='AstakosGroupQuota'
188     )
189     creation_date = models.DateTimeField(
190         _('Creation date'),
191         default=datetime.now()
192     )
193     issue_date = models.DateTimeField(
194         _('Start date'),
195         null=True
196     )
197     expiration_date = models.DateTimeField(
198         _('Expiration date'),
199         null=True
200     )
201     moderation_enabled = models.BooleanField(
202         _('Moderated membership?'),
203         default=True
204     )
205     approval_date = models.DateTimeField(
206         _('Activation date'),
207         null=True,
208         blank=True
209     )
210     estimated_participants = models.PositiveIntegerField(
211         _('Estimated #members'),
212         null=True,
213         blank=True,
214     )
215     max_participants = models.PositiveIntegerField(
216         _('Maximum numder of participants'),
217         null=True,
218         blank=True
219     )
220
221     @property
222     def is_disabled(self):
223         if not self.approval_date:
224             return True
225         return False
226
227     @property
228     def is_enabled(self):
229         if self.is_disabled:
230             return False
231         if not self.issue_date:
232             return False
233         if not self.expiration_date:
234             return True
235         now = datetime.now()
236         if self.issue_date > now:
237             return False
238         if now >= self.expiration_date:
239             return False
240         return True
241
242     def enable(self):
243         if self.is_enabled:
244             return
245         self.approval_date = datetime.now()
246         self.save()
247         quota_disturbed.send(sender=self, users=self.approved_members)
248         #propagate_groupmembers_quota.apply_async(
249         #    args=[self], eta=self.issue_date)
250         #propagate_groupmembers_quota.apply_async(
251         #    args=[self], eta=self.expiration_date)
252
253     def disable(self):
254         if self.is_disabled:
255             return
256         self.approval_date = None
257         self.save()
258         quota_disturbed.send(sender=self, users=self.approved_members)
259
260     def approve_member(self, person):
261         m, created = self.membership_set.get_or_create(person=person)
262         m.approve()
263
264     @property
265     def members(self):
266         q = self.membership_set.select_related().all()
267         return [m.person for m in q]
268
269     @property
270     def approved_members(self):
271         q = self.membership_set.select_related().all()
272         return [m.person for m in q if m.is_approved]
273
274     @property
275     def quota(self):
276         d = defaultdict(int)
277         for q in self.astakosgroupquota_set.select_related().all():
278             d[q.resource] += q.uplimit or inf
279         return d
280
281     def add_policy(self, service, resource, uplimit, update=True):
282         """Raises ObjectDoesNotExist, IntegrityError"""
283         resource = Resource.objects.get(service__name=service, name=resource)
284         if update:
285             AstakosGroupQuota.objects.update_or_create(
286                 group=self,
287                 resource=resource,
288                 defaults={'uplimit': uplimit}
289             )
290         else:
291             q = self.astakosgroupquota_set
292             q.create(resource=resource, uplimit=uplimit)
293
294     @property
295     def policies(self):
296         return self.astakosgroupquota_set.select_related().all()
297
298     @policies.setter
299     def policies(self, policies):
300         for p in policies:
301             service = p.get('service', None)
302             resource = p.get('resource', None)
303             uplimit = p.get('uplimit', 0)
304             update = p.get('update', True)
305             self.add_policy(service, resource, uplimit, update)
306
307     @property
308     def owners(self):
309         return self.owner.all()
310
311     @property
312     def owner_details(self):
313         return self.owner.select_related().all()
314
315     @owners.setter
316     def owners(self, l):
317         self.owner = l
318         map(self.approve_member, l)
319
320
321
322 class AstakosUserManager(UserManager):
323
324     def get_auth_provider_user(self, provider, **kwargs):
325         """
326         Retrieve AstakosUser instance associated with the specified third party
327         id.
328         """
329         kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
330                           kwargs.iteritems()))
331         return self.get(auth_providers__module=provider, **kwargs)
332
333 class AstakosUser(User):
334     """
335     Extends ``django.contrib.auth.models.User`` by defining additional fields.
336     """
337     affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
338                                    null=True)
339
340     # DEPRECATED FIELDS: provider, third_party_identifier moved in
341     #                    AstakosUserProvider model.
342     provider = models.CharField(_('Provider'), max_length=255, blank=True,
343                                 null=True)
344     # ex. screen_name for twitter, eppn for shibboleth
345     third_party_identifier = models.CharField(_('Third-party identifier'),
346                                               max_length=255, null=True,
347                                               blank=True)
348
349
350     #for invitations
351     user_level = DEFAULT_USER_LEVEL
352     level = models.IntegerField(_('Inviter level'), default=user_level)
353     invitations = models.IntegerField(
354         _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
355
356     auth_token = models.CharField(_('Authentication Token'), max_length=32,
357                                   null=True, blank=True)
358     auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
359     auth_token_expires = models.DateTimeField(
360         _('Token expiration date'), null=True)
361
362     updated = models.DateTimeField(_('Update date'))
363     is_verified = models.BooleanField(_('Is verified?'), default=False)
364
365     email_verified = models.BooleanField(_('Email verified?'), default=False)
366
367     has_credits = models.BooleanField(_('Has credits?'), default=False)
368     has_signed_terms = models.BooleanField(
369         _('I agree with the terms'), default=False)
370     date_signed_terms = models.DateTimeField(
371         _('Signed terms date'), null=True, blank=True)
372
373     activation_sent = models.DateTimeField(
374         _('Activation sent data'), null=True, blank=True)
375
376     policy = models.ManyToManyField(
377         Resource, null=True, through='AstakosUserQuota')
378
379     astakos_groups = models.ManyToManyField(
380         AstakosGroup, verbose_name=_('agroups'), blank=True,
381         help_text=_(astakos_messages.ASTAKOSUSER_GROUPS_HELP),
382         through='Membership')
383
384     __has_signed_terms = False
385     disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
386                                            default=False, db_index=True)
387
388     objects = AstakosUserManager()
389
390     owner = models.ManyToManyField(
391         AstakosGroup, related_name='owner', null=True)
392
393     class Meta:
394         unique_together = ("provider", "third_party_identifier")
395
396     def __init__(self, *args, **kwargs):
397         super(AstakosUser, self).__init__(*args, **kwargs)
398         self.__has_signed_terms = self.has_signed_terms
399         if not self.id:
400             self.is_active = False
401
402     @property
403     def realname(self):
404         return '%s %s' % (self.first_name, self.last_name)
405
406     @realname.setter
407     def realname(self, value):
408         parts = value.split(' ')
409         if len(parts) == 2:
410             self.first_name = parts[0]
411             self.last_name = parts[1]
412         else:
413             self.last_name = parts[0]
414
415     def add_permission(self, pname):
416         if self.has_perm(pname):
417             return
418         p, created = Permission.objects.get_or_create(
419                                     codename=pname,
420                                     name=pname.capitalize(),
421                                     content_type=get_content_type())
422         self.user_permissions.add(p)
423
424     def remove_permission(self, pname):
425         if self.has_perm(pname):
426             return
427         p = Permission.objects.get(codename=pname,
428                                    content_type=get_content_type())
429         self.user_permissions.remove(p)
430
431     @property
432     def invitation(self):
433         try:
434             return Invitation.objects.get(username=self.email)
435         except Invitation.DoesNotExist:
436             return None
437
438     def invite(self, email, realname):
439         inv = Invitation(inviter=self, username=email, realname=realname)
440         inv.save()
441         send_invitation(inv)
442         self.invitations = max(0, self.invitations - 1)
443         self.save()
444
445     @property
446     def quota(self):
447         """Returns a dict with the sum of quota limits per resource"""
448         d = defaultdict(int)
449         for q in self.policies:
450             d[q.resource] += q.uplimit or inf
451         for m in self.projectmembership_set.select_related().all():
452             if not m.acceptance_date:
453                 continue
454             p = m.project
455             if not p.is_active:
456                 continue
457             grants = p.application.definition.projectresourcegrant_set.all()
458             for g in grants:
459                 d[g.resource] += g.member_limit or inf
460         # TODO set default for remaining
461         return d
462
463     @property
464     def policies(self):
465         return self.astakosuserquota_set.select_related().all()
466
467     @policies.setter
468     def policies(self, policies):
469         for p in policies:
470             service = policies.get('service', None)
471             resource = policies.get('resource', None)
472             uplimit = policies.get('uplimit', 0)
473             update = policies.get('update', True)
474             self.add_policy(service, resource, uplimit, update)
475
476     def add_policy(self, service, resource, uplimit, update=True):
477         """Raises ObjectDoesNotExist, IntegrityError"""
478         resource = Resource.objects.get(service__name=service, name=resource)
479         if update:
480             AstakosUserQuota.objects.update_or_create(user=self,
481                                                       resource=resource,
482                                                       defaults={'uplimit': uplimit})
483         else:
484             q = self.astakosuserquota_set
485             q.create(resource=resource, uplimit=uplimit)
486
487     def remove_policy(self, service, resource):
488         """Raises ObjectDoesNotExist, IntegrityError"""
489         resource = Resource.objects.get(service__name=service, name=resource)
490         q = self.policies.get(resource=resource).delete()
491
492     @property
493     def extended_groups(self):
494         return self.membership_set.select_related().all()
495
496     @extended_groups.setter
497     def extended_groups(self, groups):
498         #TODO exceptions
499         for name in (groups or ()):
500             group = AstakosGroup.objects.get(name=name)
501             self.membership_set.create(group=group)
502
503     def save(self, update_timestamps=True, **kwargs):
504         if update_timestamps:
505             if not self.id:
506                 self.date_joined = datetime.now()
507             self.updated = datetime.now()
508
509         # update date_signed_terms if necessary
510         if self.__has_signed_terms != self.has_signed_terms:
511             self.date_signed_terms = datetime.now()
512
513         if not self.id:
514             # set username
515             self.username = self.email
516
517         self.validate_unique_email_isactive()
518         if self.is_active and self.activation_sent:
519             # reset the activation sent
520             self.activation_sent = None
521
522         super(AstakosUser, self).save(**kwargs)
523
524     def renew_token(self, flush_sessions=False, current_key=None):
525         md5 = hashlib.md5()
526         md5.update(settings.SECRET_KEY)
527         md5.update(self.username)
528         md5.update(self.realname.encode('ascii', 'ignore'))
529         md5.update(asctime())
530
531         self.auth_token = b64encode(md5.digest())
532         self.auth_token_created = datetime.now()
533         self.auth_token_expires = self.auth_token_created + \
534                                   timedelta(hours=AUTH_TOKEN_DURATION)
535         if flush_sessions:
536             self.flush_sessions(current_key)
537         msg = 'Token renewed for %s' % self.email
538         logger.log(LOGGING_LEVEL, msg)
539
540     def flush_sessions(self, current_key=None):
541         q = self.sessions
542         if current_key:
543             q = q.exclude(session_key=current_key)
544
545         keys = q.values_list('session_key', flat=True)
546         if keys:
547             msg = 'Flushing sessions: %s' % ','.join(keys)
548             logger.log(LOGGING_LEVEL, msg, [])
549         engine = import_module(settings.SESSION_ENGINE)
550         for k in keys:
551             s = engine.SessionStore(k)
552             s.flush()
553
554     def __unicode__(self):
555         return '%s (%s)' % (self.realname, self.email)
556
557     def conflicting_email(self):
558         q = AstakosUser.objects.exclude(username=self.username)
559         q = q.filter(email__iexact=self.email)
560         if q.count() != 0:
561             return True
562         return False
563
564     def validate_unique_email_isactive(self):
565         """
566         Implements a unique_together constraint for email and is_active fields.
567         """
568         q = AstakosUser.objects.all()
569         q = q.filter(email = self.email)
570         q = q.filter(is_active = self.is_active)
571         if self.id:
572             q = q.filter(~Q(id = self.id))
573         if q.count() != 0:
574             raise ValidationError({'__all__': [_(astakos_messages.UNIQUE_EMAIL_IS_ACTIVE_CONSTRAIN_ERR)]})
575
576     @property
577     def signed_terms(self):
578         term = get_latest_terms()
579         if not term:
580             return True
581         if not self.has_signed_terms:
582             return False
583         if not self.date_signed_terms:
584             return False
585         if self.date_signed_terms < term.date:
586             self.has_signed_terms = False
587             self.date_signed_terms = None
588             self.save()
589             return False
590         return True
591
592     def set_invitations_level(self):
593         """
594         Update user invitation level
595         """
596         level = self.invitation.inviter.level + 1
597         self.level = level
598         self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
599
600     def can_login_with_auth_provider(self, provider):
601         if not self.has_auth_provider(provider):
602             return False
603         else:
604             return auth_providers.get_provider(provider).is_available_for_login()
605
606     def can_add_auth_provider(self, provider, **kwargs):
607         provider_settings = auth_providers.get_provider(provider)
608         if not provider_settings.is_available_for_login():
609             return False
610
611         if self.has_auth_provider(provider) and \
612            provider_settings.one_per_user:
613             return False
614
615         if 'identifier' in kwargs:
616             try:
617                 # provider with specified params already exist
618                 existing_user = AstakosUser.objects.get_auth_provider_user(provider,
619                                                                    **kwargs)
620             except AstakosUser.DoesNotExist:
621                 return True
622             else:
623                 return False
624
625         return True
626
627     def can_remove_auth_provider(self, provider):
628         if len(self.get_active_auth_providers()) <= 1:
629             return False
630         return True
631
632     def can_change_password(self):
633         return self.has_auth_provider('local', auth_backend='astakos')
634
635     def has_auth_provider(self, provider, **kwargs):
636         return bool(self.auth_providers.filter(module=provider,
637                                                **kwargs).count())
638
639     def add_auth_provider(self, provider, **kwargs):
640         if self.can_add_auth_provider(provider, **kwargs):
641             self.auth_providers.create(module=provider, active=True, **kwargs)
642         else:
643             raise Exception('Cannot add provider')
644
645     def add_pending_auth_provider(self, pending):
646         """
647         Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
648         the current user.
649         """
650         if not isinstance(pending, PendingThirdPartyUser):
651             pending = PendingThirdPartyUser.objects.get(token=pending)
652
653         provider = self.add_auth_provider(pending.provider,
654                                identifier=pending.third_party_identifier)
655
656         if email_re.match(pending.email or '') and pending.email != self.email:
657             self.additionalmail_set.get_or_create(email=pending.email)
658
659         pending.delete()
660         return provider
661
662     def remove_auth_provider(self, provider, **kwargs):
663         self.auth_providers.get(module=provider, **kwargs).delete()
664
665     # user urls
666     def get_resend_activation_url(self):
667         return reverse('send_activation', {'user_id': self.pk})
668
669     def get_activation_url(self, nxt=False):
670         url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
671                                  quote(self.auth_token))
672         if nxt:
673             url += "&next=%s" % quote(nxt)
674         return url
675
676     def get_password_reset_url(self, token_generator=default_token_generator):
677         return reverse('django.contrib.auth.views.password_reset_confirm',
678                           kwargs={'uidb36':int_to_base36(self.id),
679                                   'token':token_generator.make_token(self)})
680
681     def get_auth_providers(self):
682         return self.auth_providers.all()
683
684     def get_available_auth_providers(self):
685         """
686         Returns a list of providers available for user to connect to.
687         """
688         providers = []
689         for module, provider_settings in auth_providers.PROVIDERS.iteritems():
690             if self.can_add_auth_provider(module):
691                 providers.append(provider_settings(self))
692
693         return providers
694
695     def get_active_auth_providers(self):
696         providers = []
697         for provider in self.auth_providers.active():
698             if auth_providers.get_provider(provider.module).is_available_for_login():
699                 providers.append(provider)
700         return providers
701
702     @property
703     def auth_providers_display(self):
704         return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
705
706
707 class AstakosUserAuthProviderManager(models.Manager):
708
709     def active(self):
710         return self.filter(active=True)
711
712
713 class AstakosUserAuthProvider(models.Model):
714     """
715     Available user authentication methods.
716     """
717     affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
718                                    null=True, default=None)
719     user = models.ForeignKey(AstakosUser, related_name='auth_providers')
720     module = models.CharField(_('Provider'), max_length=255, blank=False,
721                                 default='local')
722     identifier = models.CharField(_('Third-party identifier'),
723                                               max_length=255, null=True,
724                                               blank=True)
725     active = models.BooleanField(default=True)
726     auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
727                                    default='astakos')
728
729     objects = AstakosUserAuthProviderManager()
730
731     class Meta:
732         unique_together = (('identifier', 'module', 'user'), )
733
734     @property
735     def settings(self):
736         return auth_providers.get_provider(self.module)
737
738     @property
739     def details_display(self):
740         return self.settings.details_tpl % self.__dict__
741
742     def can_remove(self):
743         return self.user.can_remove_auth_provider(self.module)
744
745     def delete(self, *args, **kwargs):
746         ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
747         if self.module == 'local':
748             self.user.set_unusable_password()
749             self.user.save()
750         return ret
751
752     def __repr__(self):
753         return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
754
755     def __unicode__(self):
756         if self.identifier:
757             return "%s:%s" % (self.module, self.identifier)
758         if self.auth_backend:
759             return "%s:%s" % (self.module, self.auth_backend)
760         return self.module
761
762
763
764 class Membership(models.Model):
765     person = models.ForeignKey(AstakosUser)
766     group = models.ForeignKey(AstakosGroup)
767     date_requested = models.DateField(default=datetime.now(), blank=True)
768     date_joined = models.DateField(null=True, db_index=True, blank=True)
769
770     class Meta:
771         unique_together = ("person", "group")
772
773     def save(self, *args, **kwargs):
774         if not self.id:
775             if not self.group.moderation_enabled:
776                 self.date_joined = datetime.now()
777         super(Membership, self).save(*args, **kwargs)
778
779     @property
780     def is_approved(self):
781         if self.date_joined:
782             return True
783         return False
784
785     def approve(self):
786         if self.is_approved:
787             return
788         if self.group.max_participants:
789             assert len(self.group.approved_members) + 1 <= self.group.max_participants, \
790             'Maximum participant number has been reached.'
791         self.date_joined = datetime.now()
792         self.save()
793         quota_disturbed.send(sender=self, users=(self.person,))
794
795     def disapprove(self):
796         approved = self.is_approved()
797         self.delete()
798         if approved:
799             quota_disturbed.send(sender=self, users=(self.person,))
800
801 class ExtendedManager(models.Manager):
802     def _update_or_create(self, **kwargs):
803         assert kwargs, \
804             'update_or_create() must be passed at least one keyword argument'
805         obj, created = self.get_or_create(**kwargs)
806         defaults = kwargs.pop('defaults', {})
807         if created:
808             return obj, True, False
809         else:
810             try:
811                 params = dict(
812                     [(k, v) for k, v in kwargs.items() if '__' not in k])
813                 params.update(defaults)
814                 for attr, val in params.items():
815                     if hasattr(obj, attr):
816                         setattr(obj, attr, val)
817                 sid = transaction.savepoint()
818                 obj.save(force_update=True)
819                 transaction.savepoint_commit(sid)
820                 return obj, False, True
821             except IntegrityError, e:
822                 transaction.savepoint_rollback(sid)
823                 try:
824                     return self.get(**kwargs), False, False
825                 except self.model.DoesNotExist:
826                     raise e
827
828     update_or_create = _update_or_create
829
830 class AstakosGroupQuota(models.Model):
831     objects = ExtendedManager()
832     limit = models.PositiveIntegerField(_('Limit'), null=True)    # obsolete field
833     uplimit = models.BigIntegerField(_('Up limit'), null=True)
834     resource = models.ForeignKey(Resource)
835     group = models.ForeignKey(AstakosGroup, blank=True)
836
837     class Meta:
838         unique_together = ("resource", "group")
839
840 class AstakosUserQuota(models.Model):
841     objects = ExtendedManager()
842     limit = models.PositiveIntegerField(_('Limit'), null=True)    # obsolete field
843     uplimit = models.BigIntegerField(_('Up limit'), null=True)
844     resource = models.ForeignKey(Resource)
845     user = models.ForeignKey(AstakosUser)
846
847     class Meta:
848         unique_together = ("resource", "user")
849
850
851 class ApprovalTerms(models.Model):
852     """
853     Model for approval terms
854     """
855
856     date = models.DateTimeField(
857         _('Issue date'), db_index=True, default=datetime.now())
858     location = models.CharField(_('Terms location'), max_length=255)
859
860
861 class Invitation(models.Model):
862     """
863     Model for registring invitations
864     """
865     inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
866                                 null=True)
867     realname = models.CharField(_('Real name'), max_length=255)
868     username = models.CharField(_('Unique ID'), max_length=255, unique=True)
869     code = models.BigIntegerField(_('Invitation code'), db_index=True)
870     is_consumed = models.BooleanField(_('Consumed?'), default=False)
871     created = models.DateTimeField(_('Creation date'), auto_now_add=True)
872     consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
873
874     def __init__(self, *args, **kwargs):
875         super(Invitation, self).__init__(*args, **kwargs)
876         if not self.id:
877             self.code = _generate_invitation_code()
878
879     def consume(self):
880         self.is_consumed = True
881         self.consumed = datetime.now()
882         self.save()
883
884     def __unicode__(self):
885         return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
886
887
888 class EmailChangeManager(models.Manager):
889     @transaction.commit_on_success
890     def change_email(self, activation_key):
891         """
892         Validate an activation key and change the corresponding
893         ``User`` if valid.
894
895         If the key is valid and has not expired, return the ``User``
896         after activating.
897
898         If the key is not valid or has expired, return ``None``.
899
900         If the key is valid but the ``User`` is already active,
901         return ``None``.
902
903         After successful email change the activation record is deleted.
904
905         Throws ValueError if there is already
906         """
907         try:
908             email_change = self.model.objects.get(
909                 activation_key=activation_key)
910             if email_change.activation_key_expired():
911                 email_change.delete()
912                 raise EmailChange.DoesNotExist
913             # is there an active user with this address?
914             try:
915                 AstakosUser.objects.get(email__iexact=email_change.new_email_address)
916             except AstakosUser.DoesNotExist:
917                 pass
918             else:
919                 raise ValueError(_(astakos_messages.NEW_EMAIL_ADDR_RESERVED))
920             # update user
921             user = AstakosUser.objects.get(pk=email_change.user_id)
922             user.email = email_change.new_email_address
923             user.save()
924             email_change.delete()
925             return user
926         except EmailChange.DoesNotExist:
927             raise ValueError(_(astakos_messages.INVALID_ACTIVATION_KEY))
928
929
930 class EmailChange(models.Model):
931     new_email_address = models.EmailField(_(u'new e-mail address'),
932                                           help_text=_(astakos_messages.EMAIL_CHANGE_NEW_ADDR_HELP))
933     user = models.ForeignKey(
934         AstakosUser, unique=True, related_name='emailchange_user')
935     requested_at = models.DateTimeField(default=datetime.now())
936     activation_key = models.CharField(
937         max_length=40, unique=True, db_index=True)
938
939     objects = EmailChangeManager()
940
941     def activation_key_expired(self):
942         expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
943         return self.requested_at + expiration_date < datetime.now()
944
945
946 class AdditionalMail(models.Model):
947     """
948     Model for registring invitations
949     """
950     owner = models.ForeignKey(AstakosUser)
951     email = models.EmailField()
952
953
954 def _generate_invitation_code():
955     while True:
956         code = randint(1, 2L ** 63 - 1)
957         try:
958             Invitation.objects.get(code=code)
959             # An invitation with this code already exists, try again
960         except Invitation.DoesNotExist:
961             return code
962
963
964 def get_latest_terms():
965     try:
966         term = ApprovalTerms.objects.order_by('-id')[0]
967         return term
968     except IndexError:
969         pass
970     return None
971
972 class PendingThirdPartyUser(models.Model):
973     """
974     Model for registring successful third party user authentications
975     """
976     third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
977     provider = models.CharField(_('Provider'), max_length=255, blank=True)
978     email = models.EmailField(_('e-mail address'), blank=True, null=True)
979     first_name = models.CharField(_('first name'), max_length=30, blank=True)
980     last_name = models.CharField(_('last name'), max_length=30, blank=True)
981     affiliation = models.CharField('Affiliation', max_length=255, blank=True)
982     username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
983     token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
984     created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
985
986     class Meta:
987         unique_together = ("provider", "third_party_identifier")
988
989     @property
990     def realname(self):
991         return '%s %s' %(self.first_name, self.last_name)
992
993     @realname.setter
994     def realname(self, value):
995         parts = value.split(' ')
996         if len(parts) == 2:
997             self.first_name = parts[0]
998             self.last_name = parts[1]
999         else:
1000             self.last_name = parts[0]
1001
1002     def save(self, **kwargs):
1003         if not self.id:
1004             # set username
1005             while not self.username:
1006                 username =  uuid.uuid4().hex[:30]
1007                 try:
1008                     AstakosUser.objects.get(username = username)
1009                 except AstakosUser.DoesNotExist, e:
1010                     self.username = username
1011         super(PendingThirdPartyUser, self).save(**kwargs)
1012
1013     def generate_token(self):
1014         self.password = self.third_party_identifier
1015         self.last_login = datetime.now()
1016         self.token = default_token_generator.make_token(self)
1017
1018 class SessionCatalog(models.Model):
1019     session_key = models.CharField(_('session key'), max_length=40)
1020     user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1021
1022 class MemberJoinPolicy(models.Model):
1023     policy = models.CharField(_('Policy'), max_length=255, unique=True, db_index=True)
1024     description = models.CharField(_('Description'), max_length=80)
1025
1026     def __str__(self):
1027         return self.policy
1028
1029 class MemberLeavePolicy(models.Model):
1030     policy = models.CharField(_('Policy'), max_length=255, unique=True, db_index=True)
1031     description = models.CharField(_('Description'), max_length=80)
1032
1033     def __str__(self):
1034         return self.policy
1035
1036 _auto_accept_join = False
1037 def get_auto_accept_join():
1038     global _auto_accept_join
1039     if _auto_accept_join is not False:
1040         return _auto_accept_join
1041     try:
1042         auto_accept = MemberJoinPolicy.objects.get(policy='auto_accept')
1043     except:
1044         auto_accept = None
1045     _auto_accept_join = auto_accept
1046     return auto_accept
1047
1048 _closed_join = False
1049 def get_closed_join():
1050     global _closed_join
1051     if _closed_join is not False:
1052         return _closed_join
1053     try:
1054         closed = MemberJoinPolicy.objects.get(policy='closed')
1055     except:
1056         closed = None
1057     _closed_join = closed
1058     return closed
1059
1060 _auto_accept_leave = False
1061 def get_auto_accept_leave():
1062     global _auto_accept_leave
1063     if _auto_accept_leave is not False:
1064         return _auto_accept_leave
1065     try:
1066         auto_accept = MemberLeavePolicy.objects.get(policy='auto_accept')
1067     except:
1068         auto_accept = None
1069     _auto_accept_leave = auto_accept
1070     return auto_accept
1071
1072 _closed_leave = False
1073 def get_closed_leave():
1074     global _closed_leave
1075     if _closed_leave is not False:
1076         return _closed_leave
1077     try:
1078         closed = MemberLeavePolicy.objects.get(policy='closed')
1079     except:
1080         closed = None
1081     _closed_leave = closed
1082     return closeds
1083
1084 class ProjectDefinition(models.Model):
1085     name = models.CharField(max_length=80)
1086     homepage = models.URLField(max_length=255, null=True, blank=True)
1087     description = models.TextField(null=True)
1088     start_date = models.DateTimeField()
1089     end_date = models.DateTimeField()
1090     member_join_policy = models.ForeignKey(MemberJoinPolicy)
1091     member_leave_policy = models.ForeignKey(MemberLeavePolicy)
1092     limit_on_members_number = models.PositiveIntegerField(null=True,blank=True)
1093     resource_grants = models.ManyToManyField(
1094         Resource,
1095         null=True,
1096         blank=True,
1097         through='ProjectResourceGrant'
1098     )
1099     
1100     def save(self):
1101         self.validate_name()
1102         super(ProjectDefinition, self).save()
1103         
1104     @property
1105     def violated_resource_grants(self):
1106         return False
1107     
1108     def add_resource_policy(self, service, resource, uplimit, update=True):
1109         """Raises ObjectDoesNotExist, IntegrityError"""
1110         resource = Resource.objects.get(service__name=service, name=resource)
1111         if update:
1112             ProjectResourceGrant.objects.update_or_create(
1113                 project_definition=self,
1114                 resource=resource,
1115                 defaults={'member_limit': uplimit}
1116             )
1117         else:
1118             q = self.projectresourcegrant_set
1119             q.create(resource=resource, member_limit=uplimit)
1120
1121     @property
1122     def resource_policies(self):
1123         return self.projectresourcegrant_set.all()
1124
1125     @resource_policies.setter
1126     def resource_policies(self, policies):
1127         for p in policies:
1128             service = p.get('service', None)
1129             resource = p.get('resource', None)
1130             uplimit = p.get('uplimit', 0)
1131             update = p.get('update', True)
1132             self.add_resource_policy(service, resource, uplimit, update)
1133     
1134     def validate_name(self):
1135         """
1136         Validate name uniqueness among all active projects.
1137         """
1138         alive_projects = list(get_alive_projects())
1139         q = filter(
1140             lambda p: p.definition.name == self.name and \
1141                 p.application.id != self.projectapplication.id,
1142             alive_projects
1143         )
1144         if q:
1145             raise ValidationError(
1146                 {'name': [_(astakos_messages.UNIQUE_PROJECT_NAME_CONSTRAIN_ERR)]}
1147             )
1148
1149
1150 class ProjectResourceGrant(models.Model):
1151     objects = ExtendedManager()
1152     member_limit = models.BigIntegerField(null=True)
1153     project_limit = models.BigIntegerField(null=True)
1154     resource = models.ForeignKey(Resource)
1155     project_definition = models.ForeignKey(ProjectDefinition, blank=True)
1156
1157     class Meta:
1158         unique_together = ("resource", "project_definition")
1159
1160
1161 class ProjectApplication(models.Model):
1162     states_list = [PENDING, APPROVED, REPLACED, UNKNOWN]
1163     states = dict((k, v) for k, v in enumerate(states_list))
1164
1165     applicant = models.ForeignKey(
1166         AstakosUser,
1167         related_name='my_project_applications',
1168         db_index=True)
1169     owner = models.ForeignKey(
1170         AstakosUser,
1171         related_name='own_project_applications',
1172         db_index=True
1173     )
1174     comments = models.TextField(null=True, blank=True)
1175     definition = models.OneToOneField(ProjectDefinition)
1176     issue_date = models.DateTimeField()
1177     precursor_application = models.OneToOneField('ProjectApplication',
1178         null=True,
1179         blank=True,
1180         db_index=True
1181     )
1182     state = models.CharField(max_length=80, default=UNKNOWN)
1183     
1184     @property
1185     def follower(self):
1186         try:
1187             return ProjectApplication.objects.get(precursor_application=self)
1188         except ProjectApplication.DoesNotExist:
1189             return
1190
1191     def save(self):
1192         self.definition.save()
1193         self.definition = self.definition
1194         super(ProjectApplication, self).save()
1195
1196
1197     @staticmethod
1198     def submit(definition, resource_policies, applicant, comments, precursor_application=None, commit=True):
1199         application = None
1200         if precursor_application:
1201             precursor_application_id = precursor_application.id
1202             application = precursor_application
1203             application.id = None
1204         else:
1205             application = ProjectApplication(owner=applicant)
1206         application.definition = definition
1207         application.definition.id = None
1208         application.applicant = applicant
1209         application.comments = comments
1210         application.issue_date = datetime.now()
1211         application.state = PENDING
1212         if commit:
1213             application.save()
1214             application.definition.resource_policies = resource_policies
1215         else:
1216             notification = build_notification(
1217                 settings.SERVER_EMAIL,
1218                 [i[1] for i in settings.ADMINS],
1219                 _(GROUP_CREATION_SUBJECT) % {'group':application.definition.name},
1220                 _('An new project application identified by %(id)s has been submitted.') % application.__dict__
1221             )
1222             notification.send()
1223         return application
1224         
1225     def approve(self, approval_user=None):
1226         """
1227         If approval_user then during owner membership acceptance
1228         it is checked whether the request_user is eligible.
1229         """
1230         if self.state != PENDING:
1231             return
1232         create = False
1233         try:
1234             self.precursor_application.project
1235         except:
1236             create = True
1237
1238         if create:
1239             kwargs = {
1240                 'application':self,
1241                 'creation_date':datetime.now(),
1242                 'last_approval_date':datetime.now(),
1243             }
1244             project = _create_object(Project, **kwargs)
1245             project.accept_member(self.owner, approval_user)
1246         else:
1247             project = self.precursor_application.project
1248             project.application = self
1249             project.last_approval_date = datetime.now()
1250             project.save()
1251             self.precursor_application.state = REPLACED
1252         self.state = APPROVED
1253         self.save()
1254
1255         notification = build_notification(
1256             settings.SERVER_EMAIL,
1257             [self.owner.email],
1258             _('Project application has been approved on %s alpha2 testing' % SITENAME),
1259             _('Your application request %(id)s has been apporved.')
1260         )
1261         notification.send()
1262
1263         rejected = self.project.sync()
1264         if rejected:
1265             # revert to precursor
1266             project.application = app.precursor_application
1267             if project.application:
1268                 project.last_approval_date = last_approval_date
1269                 project.save()
1270             rejected = project.sync()
1271             if rejected:
1272                 raise Exception(_(astakos_messages.QH_SYNC_ERROR))
1273         else:
1274             project.last_application_synced = app
1275             project.save()
1276
1277
1278 class Project(models.Model):
1279     application = models.OneToOneField(ProjectApplication, related_name='project')
1280     creation_date = models.DateTimeField()
1281     last_approval_date = models.DateTimeField(null=True)
1282     termination_start_date = models.DateTimeField(null=True)
1283     termination_date = models.DateTimeField(null=True)
1284     members = models.ManyToManyField(AstakosUser, through='ProjectMembership')
1285     membership_dirty = models.BooleanField(default=False)
1286     last_application_synced = models.OneToOneField(
1287         ProjectApplication, related_name='last_project', null=True, blank=True
1288     )
1289     
1290     
1291     @property
1292     def definition(self):
1293         return self.application.definition
1294
1295     @property
1296     def violated_members_number_limit(self):
1297         return len(self.approved_members) <= self.definition.limit_on_members_number
1298         
1299     @property
1300     def is_active(self):
1301         if not self.last_approval_date:
1302             return False
1303         if self.termination_date:
1304             return False
1305         if self.definition.violated_resource_grants:
1306             return False
1307 #         if self.violated_members_number_limit:
1308 #             return False
1309         return True
1310     
1311     @property
1312     def is_terminated(self):
1313         if not self.termination_date:
1314             return False
1315         return True
1316     
1317     @property
1318     def is_suspended(self):
1319         if not self.termination_date:
1320             return False
1321         if not self.last_approval_date:
1322             if not self.definition.violated_resource_grants:
1323                 return False
1324 #             if not self.violated_members_number_limit:
1325 #                 return False
1326         return True
1327     
1328     @property
1329     def is_alive(self):
1330         return self.is_active or self.is_suspended
1331     
1332     @property
1333     def is_inconsistent(self):
1334         now = datetime.now()
1335         if self.creation_date > now:
1336             return True
1337         if self.last_approval_date > now:
1338             return True
1339         if self.terminaton_date > now:
1340             return True
1341         return False
1342     
1343     @property
1344     def is_synchronized(self):
1345         return self.last_application_synced == self.application and \
1346             not self.membership_dirty and \
1347             (not self.termination_start_date or termination_date)
1348     
1349     @property
1350     def approved_members(self):
1351         return [m.person for m in self.projectmembership_set.filter(~Q(acceptance_date=None))]
1352         
1353     def sync(self, specific_members=()):
1354         if self.is_synchronized:
1355             return
1356         members = specific_members or self.approved_members
1357         c, rejected = send_quota(self.approved_members)
1358         return rejected
1359     
1360     def accept_member(self, user, request_user=None):
1361         """
1362         Raises:
1363             django.exceptions.PermissionDenied
1364             astakos.im.models.AstakosUser.DoesNotExist
1365         """
1366         if isinstance(user, int):
1367             try:
1368                 user = lookup_object(AstakosUser, user, None, None)
1369             except Http404:
1370                 raise AstakosUser.DoesNotExist()
1371         m, created = ProjectMembership.objects.get_or_create(
1372             person=user, project=self
1373         )
1374         m.accept(delete_on_failure=created, request_user=None)
1375
1376     def reject_member(self, user, request_user=None):
1377         """
1378         Raises:
1379             django.exceptions.PermissionDenied
1380             astakos.im.models.AstakosUser.DoesNotExist
1381             astakos.im.models.ProjectMembership.DoesNotExist
1382         """
1383         if isinstance(user, int):
1384             try:
1385                 user = lookup_object(AstakosUser, user, None, None)
1386             except Http404:
1387                 raise AstakosUser.DoesNotExist()
1388         m = ProjectMembership.objects.get(person=user, project=self)
1389         m.reject()
1390         
1391     def remove_member(self, user, request_user=None):
1392         """
1393         Raises:
1394             django.exceptions.PermissionDenied
1395             astakos.im.models.AstakosUser.DoesNotExist
1396             astakos.im.models.ProjectMembership.DoesNotExist
1397         """
1398         if isinstance(user, int):
1399             try:
1400                 user = lookup_object(AstakosUser, user, None, None)
1401             except Http404:
1402                 raise AstakosUser.DoesNotExist()
1403         m = ProjectMembership.objects.get(person=user, project=self)
1404         m.remove()
1405     
1406     def terminate(self):
1407         self.termination_start_date = datetime.now()
1408         self.terminaton_date = None
1409         self.save()
1410         
1411         rejected = self.sync()
1412         if not rejected:
1413             self.termination_start_date = None
1414             self.terminaton_date = datetime.now()
1415             self.save()
1416             
1417             notification = build_notification(
1418                 settings.SERVER_EMAIL,
1419                 [self.application.owner.email],
1420                 _('Project %(name)s has been terminated.') %  self.definition.__dict__,
1421                 _('Project %(name)s has been terminated.') %  self.definition.__dict__
1422             )
1423             notification.send()
1424
1425     def suspend(self):
1426         self.last_approval_date = None
1427         self.save()
1428         notification = build_notification(
1429             settings.SERVER_EMAIL,
1430             [self.application.owner.email],
1431             _('Project %(name)s has been suspended.') %  self.definition.__dict__,
1432             _('Project %(name)s has been suspended.') %  self.definition.__dict__
1433         )
1434         notification.send()
1435
1436 class ProjectMembership(models.Model):
1437     person = models.ForeignKey(AstakosUser)
1438     project = models.ForeignKey(Project)
1439     request_date = models.DateField(default=datetime.now())
1440     acceptance_date = models.DateField(null=True, db_index=True)
1441     leave_request_date = models.DateField(null=True)
1442
1443     class Meta:
1444         unique_together = ("person", "project")
1445
1446     def accept(self, delete_on_failure=False, request_user=None):
1447         """
1448             Raises:
1449                 django.exception.PermissionDenied
1450                 astakos.im.notifications.NotificationError
1451         """
1452         try:
1453             if request_user and \
1454                 (not self.project.application.owner == request_user and \
1455                     not request_user.is_superuser):
1456                 raise PermissionDenied(_(astakos_messages.NOT_ALLOWED))
1457             if not self.project.is_alive:
1458                 raise PermissionDenied(_(astakos_messages.NOT_ALIVE_PROJECT) % self.project.__dict__)
1459             if self.project.definition.member_join_policy == 'closed':
1460                 raise PermissionDenied(_(astakos_messages.MEMBER_JOIN_POLICY_CLOSED))
1461             if len(self.project.approved_members) + 1 > self.project.definition.limit_on_members_number:
1462                 raise PermissionDenied(_(astakos_messages.MEMBER_NUMBER_LIMIT_REACHED))
1463         except PermissionDenied, e:
1464             if delete_on_failure:
1465                 m.delete()
1466             raise
1467         if self.acceptance_date:
1468             return
1469         self.acceptance_date = datetime.now()
1470         self.save()
1471         notification = build_notification(
1472             settings.SERVER_EMAIL,
1473             [self.person.email],
1474             _('Your membership on project %(name)s has been accepted.') % self.project.definition.__dict__,
1475             _('Your membership on project %(name)s has been accepted.') % self.project.definition.__dict__
1476         ).send()
1477         self.sync()
1478     
1479     def reject(self, request_user=None):
1480         """
1481             Raises:
1482                 django.exception.PermissionDenied,
1483                 astakos.im.notifications.NotificationError
1484         """
1485         if request_user and \
1486             (not self.project.application.owner == request_user and \
1487                 not request_user.is_superuser):
1488             raise PermissionDenied(_(astakos_messages.NOT_ALLOWED))
1489         if not self.project.is_alive:
1490             raise PermissionDenied(_(astakos_messages.NOT_ALIVE_PROJECT) % project.__dict__)
1491         history_item = ProjectMembershipHistory(
1492             person=self.person,
1493             project=self.project,
1494             request_date=self.request_date,
1495             rejection_date=datetime.now()
1496         )
1497         self.delete()
1498         history_item.save()
1499         notification = build_notification(
1500             settings.SERVER_EMAIL,
1501             [self.person.email],
1502             _('Your membership on project %(name)s has been rejected.') % self.project.definition.__dict__,
1503             _('Your membership on project %(name)s has been rejected.') % self.project.definition.__dict__
1504         ).send()
1505     
1506     def remove(self, request_user=None):
1507         """
1508             Raises:
1509                 django.exception.PermissionDenied
1510                 astakos.im.notifications.NotificationError
1511         """
1512         if request_user and \
1513             (not self.project.application.owner == request_user and \
1514                 not request_user.is_superuser):
1515             raise PermissionDenied(_(astakos_messages.NOT_ALLOWED))
1516         if not self.project.is_alive:
1517             raise PermissionDenied(_(astakos_messages.NOT_ALIVE_PROJECT) % self.project.__dict__)
1518         history_item = ProjectMembershipHistory(
1519             id=self.id,
1520             person=self.person,
1521             project=self.project,
1522             request_date=self.request_date,
1523             removal_date=datetime.now()
1524         )
1525         self.delete()
1526         history_item.save()
1527         notification = build_notification(
1528             settings.SERVER_EMAIL,
1529             [self.person.email],
1530             _('Your membership on project %(name)s has been removed.') % self.project.definition.__dict__,
1531             _('Your membership on project %(name)s has been removed.') % self.project.definition.__dict__
1532         ).send()
1533         self.sync()
1534     
1535     def leave(self):
1536         leave_policy = self.project.application.definition.member_leave_policy
1537         if leave_policy == get_auto_accept_leave():
1538             self.remove()
1539         else:
1540             self.leave_request_date = datetime.now()
1541             self.save()
1542
1543     def sync(self):
1544         # set membership_dirty flag
1545         self.project.membership_dirty = True
1546         self.project.save()
1547         
1548         rejected = self.project.sync(specific_members=[self.person])
1549         if not rejected:
1550             # if syncing was successful unset membership_dirty flag
1551             self.membership_dirty = False
1552             self.save()
1553         
1554
1555 class ProjectMembershipHistory(models.Model):
1556     person = models.ForeignKey(AstakosUser)
1557     project = models.ForeignKey(Project)
1558     request_date = models.DateField(default=datetime.now())
1559     removal_date = models.DateField(null=True)
1560     rejection_date = models.DateField(null=True)
1561
1562
1563 def filter_queryset_by_property(q, property):
1564     """
1565     Incorporate list comprehension for filtering querysets by property
1566     since Queryset.filter() operates on the database level.
1567     """
1568     return (p for p in q if getattr(p, property, False))
1569
1570 def get_alive_projects():
1571     return filter_queryset_by_property(
1572         Project.objects.all(),
1573         'is_alive'
1574     )
1575
1576 def get_active_projects():
1577     return filter_queryset_by_property(
1578         Project.objects.all(),
1579         'is_active'
1580     )
1581
1582 def _create_object(model, **kwargs):
1583     o = model.objects.create(**kwargs)
1584     o.save()
1585     return o
1586
1587
1588 def create_astakos_user(u):
1589     try:
1590         AstakosUser.objects.get(user_ptr=u.pk)
1591     except AstakosUser.DoesNotExist:
1592         extended_user = AstakosUser(user_ptr_id=u.pk)
1593         extended_user.__dict__.update(u.__dict__)
1594         extended_user.save()
1595         if not extended_user.has_auth_provider('local'):
1596             extended_user.add_auth_provider('local')
1597     except BaseException, e:
1598         logger.exception(e)
1599
1600
1601 def fix_superusers(sender, **kwargs):
1602     # Associate superusers with AstakosUser
1603     admins = User.objects.filter(is_superuser=True)
1604     for u in admins:
1605         create_astakos_user(u)
1606 post_syncdb.connect(fix_superusers)
1607
1608
1609 def user_post_save(sender, instance, created, **kwargs):
1610     if not created:
1611         return
1612     create_astakos_user(instance)
1613 post_save.connect(user_post_save, sender=User)
1614
1615
1616 def astakosuser_pre_save(sender, instance, **kwargs):
1617     instance.aquarium_report = False
1618     instance.new = False
1619     try:
1620         db_instance = AstakosUser.objects.get(id=instance.id)
1621     except AstakosUser.DoesNotExist:
1622         # create event
1623         instance.aquarium_report = True
1624         instance.new = True
1625     else:
1626         get = AstakosUser.__getattribute__
1627         l = filter(lambda f: get(db_instance, f) != get(instance, f),
1628                    BILLING_FIELDS)
1629         instance.aquarium_report = True if l else False
1630 pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
1631
1632 def set_default_group(user):
1633     try:
1634         default = AstakosGroup.objects.get(name='default')
1635         Membership(
1636             group=default, person=user, date_joined=datetime.now()).save()
1637     except AstakosGroup.DoesNotExist, e:
1638         logger.exception(e)
1639
1640
1641 def astakosuser_post_save(sender, instance, created, **kwargs):
1642     if instance.aquarium_report:
1643         report_user_event(instance, create=instance.new)
1644     if not created:
1645         return
1646     set_default_group(instance)
1647     # TODO handle socket.error & IOError
1648     register_users((instance,))
1649 post_save.connect(astakosuser_post_save, sender=AstakosUser)
1650
1651
1652 def resource_post_save(sender, instance, created, **kwargs):
1653     if not created:
1654         return
1655     register_resources((instance,))
1656 post_save.connect(resource_post_save, sender=Resource)
1657
1658
1659 def on_quota_disturbed(sender, users, **kwargs):
1660 #     print '>>>', locals()
1661     if not users:
1662         return
1663     send_quota(users)
1664
1665 quota_disturbed = Signal(providing_args=["users"])
1666 quota_disturbed.connect(on_quota_disturbed)
1667
1668
1669 def send_quota_disturbed(sender, instance, **kwargs):
1670     users = []
1671     extend = users.extend
1672     if sender == Membership:
1673         if not instance.group.is_enabled:
1674             return
1675         extend([instance.person])
1676     elif sender == AstakosUserQuota:
1677         extend([instance.user])
1678     elif sender == AstakosGroupQuota:
1679         if not instance.group.is_enabled:
1680             return
1681         extend(instance.group.astakosuser_set.all())
1682     elif sender == AstakosGroup:
1683         if not instance.is_enabled:
1684             return
1685     quota_disturbed.send(sender=sender, users=users)
1686 post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
1687 post_delete.connect(send_quota_disturbed, sender=Membership)
1688 post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
1689 post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
1690 post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1691 post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1692
1693
1694 def renew_token(sender, instance, **kwargs):
1695     if not instance.auth_token:
1696         instance.renew_token()
1697 pre_save.connect(renew_token, sender=AstakosUser)
1698 pre_save.connect(renew_token, sender=Service)
1699
1700
1701 def check_closed_join_membership_policy(sender, instance, **kwargs):
1702     if instance.id:
1703         return
1704     join_policy = instance.project.application.definition.member_join_policy
1705     if join_policy == get_closed_join():
1706         raise PermissionDenied(_(astakos_messages.MEMBER_JOIN_POLICY_CLOSED))
1707 pre_save.connect(check_closed_join_membership_policy, sender=ProjectMembership)
1708
1709
1710 def check_auto_accept_join_membership_policy(sender, instance, created, **kwargs):
1711     if not created:
1712         return
1713     if created:
1714         join_policy = instance.project.application.definition.member_join_policy
1715         if join_policy == get_auto_accept_join():
1716             instance.accept()
1717 post_save.connect(check_auto_accept_join_membership_policy, sender=ProjectMembership)