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