1 # Copyright 2011-2012 GRNET S.A. All rights reserved.
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
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.
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.
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.
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
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
54 from django.contrib.contenttypes.models import ContentType
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
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
74 from astakos.im.endpoints.qh import (
75 register_users, send_quota, register_resources
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
82 from astakos.im.notifications import build_notification
84 import astakos.im.messages as astakos_messages
86 logger = logging.getLogger(__name__)
88 DEFAULT_CONTENT_TYPE = None
91 PENDING, APPROVED, REPLACED, UNKNOWN = 'Pending', 'Approved', 'Replaced', 'Unknown'
93 def get_content_type():
95 if _content_type is not None:
99 content_type = ContentType.objects.get(app_label='im', model='astakosuser')
101 content_type = DEFAULT_CONTENT_TYPE
102 _content_type = content_type
105 RESOURCE_SEPARATOR = '.'
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)
119 def renew_token(self):
121 md5.update(self.name.encode('ascii', 'ignore'))
122 md5.update(self.url.encode('ascii', 'ignore'))
123 md5.update(asctime())
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)
135 return self.resource_set.all()
138 def resources(self, resources):
140 self.resource_set.create(**s)
142 def add_resource(self, service, resource, uplimit, update=True):
143 """Raises ObjectDoesNotExist, IntegrityError"""
144 resource = Resource.objects.get(service__name=service, name=resource)
146 AstakosUserQuota.objects.update_or_create(user=self,
148 defaults={'uplimit': uplimit})
150 q = self.astakosuserquota_set
151 q.create(resource=resource, uplimit=uplimit)
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)
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)
168 return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
171 class GroupKind(models.Model):
172 name = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
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(
187 through='AstakosGroupQuota'
189 creation_date = models.DateTimeField(
191 default=datetime.now()
193 issue_date = models.DateTimeField(
197 expiration_date = models.DateTimeField(
198 _('Expiration date'),
201 moderation_enabled = models.BooleanField(
202 _('Moderated membership?'),
205 approval_date = models.DateTimeField(
206 _('Activation date'),
210 estimated_participants = models.PositiveIntegerField(
211 _('Estimated #members'),
215 max_participants = models.PositiveIntegerField(
216 _('Maximum numder of participants'),
222 def is_disabled(self):
223 if not self.approval_date:
228 def is_enabled(self):
231 if not self.issue_date:
233 if not self.expiration_date:
236 if self.issue_date > now:
238 if now >= self.expiration_date:
245 self.approval_date = datetime.now()
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)
256 self.approval_date = None
258 quota_disturbed.send(sender=self, users=self.approved_members)
260 def approve_member(self, person):
261 m, created = self.membership_set.get_or_create(person=person)
266 q = self.membership_set.select_related().all()
267 return [m.person for m in q]
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]
277 for q in self.astakosgroupquota_set.select_related().all():
278 d[q.resource] += q.uplimit or inf
281 def add_policy(self, service, resource, uplimit, update=True):
282 """Raises ObjectDoesNotExist, IntegrityError"""
283 resource = Resource.objects.get(service__name=service, name=resource)
285 AstakosGroupQuota.objects.update_or_create(
288 defaults={'uplimit': uplimit}
291 q = self.astakosgroupquota_set
292 q.create(resource=resource, uplimit=uplimit)
296 return self.astakosgroupquota_set.select_related().all()
299 def policies(self, 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)
309 return self.owner.all()
312 def owner_details(self):
313 return self.owner.select_related().all()
318 map(self.approve_member, l)
322 class AstakosUserManager(UserManager):
324 def get_auth_provider_user(self, provider, **kwargs):
326 Retrieve AstakosUser instance associated with the specified third party
329 kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
331 return self.get(auth_providers__module=provider, **kwargs)
333 class AstakosUser(User):
335 Extends ``django.contrib.auth.models.User`` by defining additional fields.
337 affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
340 # DEPRECATED FIELDS: provider, third_party_identifier moved in
341 # AstakosUserProvider model.
342 provider = models.CharField(_('Provider'), max_length=255, blank=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,
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))
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)
362 updated = models.DateTimeField(_('Update date'))
363 is_verified = models.BooleanField(_('Is verified?'), default=False)
365 email_verified = models.BooleanField(_('Email verified?'), default=False)
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)
373 activation_sent = models.DateTimeField(
374 _('Activation sent data'), null=True, blank=True)
376 policy = models.ManyToManyField(
377 Resource, null=True, through='AstakosUserQuota')
379 astakos_groups = models.ManyToManyField(
380 AstakosGroup, verbose_name=_('agroups'), blank=True,
381 help_text=_(astakos_messages.ASTAKOSUSER_GROUPS_HELP),
382 through='Membership')
384 __has_signed_terms = False
385 disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
386 default=False, db_index=True)
388 objects = AstakosUserManager()
390 owner = models.ManyToManyField(
391 AstakosGroup, related_name='owner', null=True)
394 unique_together = ("provider", "third_party_identifier")
396 def __init__(self, *args, **kwargs):
397 super(AstakosUser, self).__init__(*args, **kwargs)
398 self.__has_signed_terms = self.has_signed_terms
400 self.is_active = False
404 return '%s %s' % (self.first_name, self.last_name)
407 def realname(self, value):
408 parts = value.split(' ')
410 self.first_name = parts[0]
411 self.last_name = parts[1]
413 self.last_name = parts[0]
415 def add_permission(self, pname):
416 if self.has_perm(pname):
418 p, created = Permission.objects.get_or_create(
420 name=pname.capitalize(),
421 content_type=get_content_type())
422 self.user_permissions.add(p)
424 def remove_permission(self, pname):
425 if self.has_perm(pname):
427 p = Permission.objects.get(codename=pname,
428 content_type=get_content_type())
429 self.user_permissions.remove(p)
432 def invitation(self):
434 return Invitation.objects.get(username=self.email)
435 except Invitation.DoesNotExist:
438 def invite(self, email, realname):
439 inv = Invitation(inviter=self, username=email, realname=realname)
442 self.invitations = max(0, self.invitations - 1)
447 """Returns a dict with the sum of quota limits per resource"""
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:
457 grants = p.application.definition.projectresourcegrant_set.all()
459 d[g.resource] += g.member_limit or inf
460 # TODO set default for remaining
465 return self.astakosuserquota_set.select_related().all()
468 def policies(self, 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)
476 def add_policy(self, service, resource, uplimit, update=True):
477 """Raises ObjectDoesNotExist, IntegrityError"""
478 resource = Resource.objects.get(service__name=service, name=resource)
480 AstakosUserQuota.objects.update_or_create(user=self,
482 defaults={'uplimit': uplimit})
484 q = self.astakosuserquota_set
485 q.create(resource=resource, uplimit=uplimit)
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()
493 def extended_groups(self):
494 return self.membership_set.select_related().all()
496 @extended_groups.setter
497 def extended_groups(self, groups):
499 for name in (groups or ()):
500 group = AstakosGroup.objects.get(name=name)
501 self.membership_set.create(group=group)
503 def save(self, update_timestamps=True, **kwargs):
504 if update_timestamps:
506 self.date_joined = datetime.now()
507 self.updated = datetime.now()
509 # update date_signed_terms if necessary
510 if self.__has_signed_terms != self.has_signed_terms:
511 self.date_signed_terms = datetime.now()
515 self.username = self.email
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
522 super(AstakosUser, self).save(**kwargs)
524 def renew_token(self, flush_sessions=False, current_key=None):
526 md5.update(settings.SECRET_KEY)
527 md5.update(self.username)
528 md5.update(self.realname.encode('ascii', 'ignore'))
529 md5.update(asctime())
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)
536 self.flush_sessions(current_key)
537 msg = 'Token renewed for %s' % self.email
538 logger.log(LOGGING_LEVEL, msg)
540 def flush_sessions(self, current_key=None):
543 q = q.exclude(session_key=current_key)
545 keys = q.values_list('session_key', flat=True)
547 msg = 'Flushing sessions: %s' % ','.join(keys)
548 logger.log(LOGGING_LEVEL, msg, [])
549 engine = import_module(settings.SESSION_ENGINE)
551 s = engine.SessionStore(k)
554 def __unicode__(self):
555 return '%s (%s)' % (self.realname, self.email)
557 def conflicting_email(self):
558 q = AstakosUser.objects.exclude(username=self.username)
559 q = q.filter(email__iexact=self.email)
564 def validate_unique_email_isactive(self):
566 Implements a unique_together constraint for email and is_active fields.
568 q = AstakosUser.objects.all()
569 q = q.filter(email = self.email)
570 q = q.filter(is_active = self.is_active)
572 q = q.filter(~Q(id = self.id))
574 raise ValidationError({'__all__': [_(astakos_messages.UNIQUE_EMAIL_IS_ACTIVE_CONSTRAIN_ERR)]})
577 def signed_terms(self):
578 term = get_latest_terms()
581 if not self.has_signed_terms:
583 if not self.date_signed_terms:
585 if self.date_signed_terms < term.date:
586 self.has_signed_terms = False
587 self.date_signed_terms = None
592 def set_invitations_level(self):
594 Update user invitation level
596 level = self.invitation.inviter.level + 1
598 self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
600 def can_login_with_auth_provider(self, provider):
601 if not self.has_auth_provider(provider):
604 return auth_providers.get_provider(provider).is_available_for_login()
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():
611 if self.has_auth_provider(provider) and \
612 provider_settings.one_per_user:
615 if 'identifier' in kwargs:
617 # provider with specified params already exist
618 existing_user = AstakosUser.objects.get_auth_provider_user(provider,
620 except AstakosUser.DoesNotExist:
627 def can_remove_auth_provider(self, provider):
628 if len(self.get_active_auth_providers()) <= 1:
632 def can_change_password(self):
633 return self.has_auth_provider('local', auth_backend='astakos')
635 def has_auth_provider(self, provider, **kwargs):
636 return bool(self.auth_providers.filter(module=provider,
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)
643 raise Exception('Cannot add provider')
645 def add_pending_auth_provider(self, pending):
647 Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
650 if not isinstance(pending, PendingThirdPartyUser):
651 pending = PendingThirdPartyUser.objects.get(token=pending)
653 provider = self.add_auth_provider(pending.provider,
654 identifier=pending.third_party_identifier)
656 if email_re.match(pending.email or '') and pending.email != self.email:
657 self.additionalmail_set.get_or_create(email=pending.email)
662 def remove_auth_provider(self, provider, **kwargs):
663 self.auth_providers.get(module=provider, **kwargs).delete()
666 def get_resend_activation_url(self):
667 return reverse('send_activation', {'user_id': self.pk})
669 def get_activation_url(self, nxt=False):
670 url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
671 quote(self.auth_token))
673 url += "&next=%s" % quote(nxt)
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)})
681 def get_auth_providers(self):
682 return self.auth_providers.all()
684 def get_available_auth_providers(self):
686 Returns a list of providers available for user to connect to.
689 for module, provider_settings in auth_providers.PROVIDERS.iteritems():
690 if self.can_add_auth_provider(module):
691 providers.append(provider_settings(self))
695 def get_active_auth_providers(self):
697 for provider in self.auth_providers.active():
698 if auth_providers.get_provider(provider.module).is_available_for_login():
699 providers.append(provider)
703 def auth_providers_display(self):
704 return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
707 class AstakosUserAuthProviderManager(models.Manager):
710 return self.filter(active=True)
713 class AstakosUserAuthProvider(models.Model):
715 Available user authentication methods.
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,
722 identifier = models.CharField(_('Third-party identifier'),
723 max_length=255, null=True,
725 active = models.BooleanField(default=True)
726 auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
729 objects = AstakosUserAuthProviderManager()
732 unique_together = (('identifier', 'module', 'user'), )
736 return auth_providers.get_provider(self.module)
739 def details_display(self):
740 return self.settings.details_tpl % self.__dict__
742 def can_remove(self):
743 return self.user.can_remove_auth_provider(self.module)
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()
753 return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
755 def __unicode__(self):
757 return "%s:%s" % (self.module, self.identifier)
758 if self.auth_backend:
759 return "%s:%s" % (self.module, self.auth_backend)
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)
771 unique_together = ("person", "group")
773 def save(self, *args, **kwargs):
775 if not self.group.moderation_enabled:
776 self.date_joined = datetime.now()
777 super(Membership, self).save(*args, **kwargs)
780 def is_approved(self):
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()
793 quota_disturbed.send(sender=self, users=(self.person,))
795 def disapprove(self):
796 approved = self.is_approved()
799 quota_disturbed.send(sender=self, users=(self.person,))
801 class ExtendedManager(models.Manager):
802 def _update_or_create(self, **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', {})
808 return obj, True, False
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)
824 return self.get(**kwargs), False, False
825 except self.model.DoesNotExist:
828 update_or_create = _update_or_create
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)
838 unique_together = ("resource", "group")
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)
848 unique_together = ("resource", "user")
851 class ApprovalTerms(models.Model):
853 Model for approval terms
856 date = models.DateTimeField(
857 _('Issue date'), db_index=True, default=datetime.now())
858 location = models.CharField(_('Terms location'), max_length=255)
861 class Invitation(models.Model):
863 Model for registring invitations
865 inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
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)
874 def __init__(self, *args, **kwargs):
875 super(Invitation, self).__init__(*args, **kwargs)
877 self.code = _generate_invitation_code()
880 self.is_consumed = True
881 self.consumed = datetime.now()
884 def __unicode__(self):
885 return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
888 class EmailChangeManager(models.Manager):
889 @transaction.commit_on_success
890 def change_email(self, activation_key):
892 Validate an activation key and change the corresponding
895 If the key is valid and has not expired, return the ``User``
898 If the key is not valid or has expired, return ``None``.
900 If the key is valid but the ``User`` is already active,
903 After successful email change the activation record is deleted.
905 Throws ValueError if there is already
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?
915 AstakosUser.objects.get(email__iexact=email_change.new_email_address)
916 except AstakosUser.DoesNotExist:
919 raise ValueError(_(astakos_messages.NEW_EMAIL_ADDR_RESERVED))
921 user = AstakosUser.objects.get(pk=email_change.user_id)
922 user.email = email_change.new_email_address
924 email_change.delete()
926 except EmailChange.DoesNotExist:
927 raise ValueError(_(astakos_messages.INVALID_ACTIVATION_KEY))
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)
939 objects = EmailChangeManager()
941 def activation_key_expired(self):
942 expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
943 return self.requested_at + expiration_date < datetime.now()
946 class AdditionalMail(models.Model):
948 Model for registring invitations
950 owner = models.ForeignKey(AstakosUser)
951 email = models.EmailField()
954 def _generate_invitation_code():
956 code = randint(1, 2L ** 63 - 1)
958 Invitation.objects.get(code=code)
959 # An invitation with this code already exists, try again
960 except Invitation.DoesNotExist:
964 def get_latest_terms():
966 term = ApprovalTerms.objects.order_by('-id')[0]
972 class PendingThirdPartyUser(models.Model):
974 Model for registring successful third party user authentications
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)
987 unique_together = ("provider", "third_party_identifier")
991 return '%s %s' %(self.first_name, self.last_name)
994 def realname(self, value):
995 parts = value.split(' ')
997 self.first_name = parts[0]
998 self.last_name = parts[1]
1000 self.last_name = parts[0]
1002 def save(self, **kwargs):
1005 while not self.username:
1006 username = uuid.uuid4().hex[:30]
1008 AstakosUser.objects.get(username = username)
1009 except AstakosUser.DoesNotExist, e:
1010 self.username = username
1011 super(PendingThirdPartyUser, self).save(**kwargs)
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)
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)
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)
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)
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
1042 auto_accept = MemberJoinPolicy.objects.get(policy='auto_accept')
1045 _auto_accept_join = auto_accept
1048 _closed_join = False
1049 def get_closed_join():
1051 if _closed_join is not False:
1054 closed = MemberJoinPolicy.objects.get(policy='closed')
1057 _closed_join = closed
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
1066 auto_accept = MemberLeavePolicy.objects.get(policy='auto_accept')
1069 _auto_accept_leave = auto_accept
1072 _closed_leave = False
1073 def get_closed_leave():
1074 global _closed_leave
1075 if _closed_leave is not False:
1076 return _closed_leave
1078 closed = MemberLeavePolicy.objects.get(policy='closed')
1081 _closed_leave = closed
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(
1097 through='ProjectResourceGrant'
1101 self.validate_name()
1102 super(ProjectDefinition, self).save()
1105 def violated_resource_grants(self):
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)
1112 ProjectResourceGrant.objects.update_or_create(
1113 project_definition=self,
1115 defaults={'member_limit': uplimit}
1118 q = self.projectresourcegrant_set
1119 q.create(resource=resource, member_limit=uplimit)
1122 def resource_policies(self):
1123 return self.projectresourcegrant_set.all()
1125 @resource_policies.setter
1126 def resource_policies(self, 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)
1134 def validate_name(self):
1136 Validate name uniqueness among all active projects.
1138 alive_projects = list(get_alive_projects())
1140 lambda p: p.definition.name == self.name and \
1141 p.application.id != self.projectapplication.id,
1145 raise ValidationError(
1146 {'name': [_(astakos_messages.UNIQUE_PROJECT_NAME_CONSTRAIN_ERR)]}
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)
1158 unique_together = ("resource", "project_definition")
1161 class ProjectApplication(models.Model):
1162 states_list = [PENDING, APPROVED, REPLACED, UNKNOWN]
1163 states = dict((k, v) for k, v in enumerate(states_list))
1165 applicant = models.ForeignKey(
1167 related_name='my_project_applications',
1169 owner = models.ForeignKey(
1171 related_name='own_project_applications',
1174 comments = models.TextField(null=True, blank=True)
1175 definition = models.OneToOneField(ProjectDefinition)
1176 issue_date = models.DateTimeField()
1177 precursor_application = models.OneToOneField('ProjectApplication',
1182 state = models.CharField(max_length=80, default=UNKNOWN)
1187 return ProjectApplication.objects.get(precursor_application=self)
1188 except ProjectApplication.DoesNotExist:
1192 self.definition.save()
1193 self.definition = self.definition
1194 super(ProjectApplication, self).save()
1198 def submit(definition, resource_policies, applicant, comments, precursor_application=None, commit=True):
1200 if precursor_application:
1201 precursor_application_id = precursor_application.id
1202 application = precursor_application
1203 application.id = None
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
1214 application.definition.resource_policies = resource_policies
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__
1225 def approve(self, approval_user=None):
1227 If approval_user then during owner membership acceptance
1228 it is checked whether the request_user is eligible.
1230 if self.state != PENDING:
1234 self.precursor_application.project
1241 'creation_date':datetime.now(),
1242 'last_approval_date':datetime.now(),
1244 project = _create_object(Project, **kwargs)
1245 project.accept_member(self.owner, approval_user)
1247 project = self.precursor_application.project
1248 project.application = self
1249 project.last_approval_date = datetime.now()
1251 self.precursor_application.state = REPLACED
1252 self.state = APPROVED
1255 notification = build_notification(
1256 settings.SERVER_EMAIL,
1258 _('Project application has been approved on %s alpha2 testing' % SITENAME),
1259 _('Your application request %(id)s has been apporved.')
1263 rejected = self.project.sync()
1265 # revert to precursor
1266 project.application = app.precursor_application
1267 if project.application:
1268 project.last_approval_date = last_approval_date
1270 rejected = project.sync()
1272 raise Exception(_(astakos_messages.QH_SYNC_ERROR))
1274 project.last_application_synced = app
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
1292 def definition(self):
1293 return self.application.definition
1296 def violated_members_number_limit(self):
1297 return len(self.approved_members) <= self.definition.limit_on_members_number
1300 def is_active(self):
1301 if not self.last_approval_date:
1303 if self.termination_date:
1305 if self.definition.violated_resource_grants:
1307 # if self.violated_members_number_limit:
1312 def is_terminated(self):
1313 if not self.termination_date:
1318 def is_suspended(self):
1319 if not self.termination_date:
1321 if not self.last_approval_date:
1322 if not self.definition.violated_resource_grants:
1324 # if not self.violated_members_number_limit:
1330 return self.is_active or self.is_suspended
1333 def is_inconsistent(self):
1334 now = datetime.now()
1335 if self.creation_date > now:
1337 if self.last_approval_date > now:
1339 if self.terminaton_date > now:
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)
1350 def approved_members(self):
1351 return [m.person for m in self.projectmembership_set.filter(~Q(acceptance_date=None))]
1353 def sync(self, specific_members=()):
1354 if self.is_synchronized:
1356 members = specific_members or self.approved_members
1357 c, rejected = send_quota(self.approved_members)
1360 def accept_member(self, user, request_user=None):
1363 django.exceptions.PermissionDenied
1364 astakos.im.models.AstakosUser.DoesNotExist
1366 if isinstance(user, int):
1368 user = lookup_object(AstakosUser, user, None, None)
1370 raise AstakosUser.DoesNotExist()
1371 m, created = ProjectMembership.objects.get_or_create(
1372 person=user, project=self
1374 m.accept(delete_on_failure=created, request_user=None)
1376 def reject_member(self, user, request_user=None):
1379 django.exceptions.PermissionDenied
1380 astakos.im.models.AstakosUser.DoesNotExist
1381 astakos.im.models.ProjectMembership.DoesNotExist
1383 if isinstance(user, int):
1385 user = lookup_object(AstakosUser, user, None, None)
1387 raise AstakosUser.DoesNotExist()
1388 m = ProjectMembership.objects.get(person=user, project=self)
1391 def remove_member(self, user, request_user=None):
1394 django.exceptions.PermissionDenied
1395 astakos.im.models.AstakosUser.DoesNotExist
1396 astakos.im.models.ProjectMembership.DoesNotExist
1398 if isinstance(user, int):
1400 user = lookup_object(AstakosUser, user, None, None)
1402 raise AstakosUser.DoesNotExist()
1403 m = ProjectMembership.objects.get(person=user, project=self)
1406 def terminate(self):
1407 self.termination_start_date = datetime.now()
1408 self.terminaton_date = None
1411 rejected = self.sync()
1413 self.termination_start_date = None
1414 self.terminaton_date = datetime.now()
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__
1426 self.last_approval_date = None
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__
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)
1444 unique_together = ("person", "project")
1446 def accept(self, delete_on_failure=False, request_user=None):
1449 django.exception.PermissionDenied
1450 astakos.im.notifications.NotificationError
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:
1467 if self.acceptance_date:
1469 self.acceptance_date = datetime.now()
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__
1479 def reject(self, request_user=None):
1482 django.exception.PermissionDenied,
1483 astakos.im.notifications.NotificationError
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(
1493 project=self.project,
1494 request_date=self.request_date,
1495 rejection_date=datetime.now()
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__
1506 def remove(self, request_user=None):
1509 django.exception.PermissionDenied
1510 astakos.im.notifications.NotificationError
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(
1521 project=self.project,
1522 request_date=self.request_date,
1523 removal_date=datetime.now()
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__
1536 leave_policy = self.project.application.definition.member_leave_policy
1537 if leave_policy == get_auto_accept_leave():
1540 self.leave_request_date = datetime.now()
1544 # set membership_dirty flag
1545 self.project.membership_dirty = True
1548 rejected = self.project.sync(specific_members=[self.person])
1550 # if syncing was successful unset membership_dirty flag
1551 self.membership_dirty = False
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)
1563 def filter_queryset_by_property(q, property):
1565 Incorporate list comprehension for filtering querysets by property
1566 since Queryset.filter() operates on the database level.
1568 return (p for p in q if getattr(p, property, False))
1570 def get_alive_projects():
1571 return filter_queryset_by_property(
1572 Project.objects.all(),
1576 def get_active_projects():
1577 return filter_queryset_by_property(
1578 Project.objects.all(),
1582 def _create_object(model, **kwargs):
1583 o = model.objects.create(**kwargs)
1588 def create_astakos_user(u):
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:
1601 def fix_superusers(sender, **kwargs):
1602 # Associate superusers with AstakosUser
1603 admins = User.objects.filter(is_superuser=True)
1605 create_astakos_user(u)
1606 post_syncdb.connect(fix_superusers)
1609 def user_post_save(sender, instance, created, **kwargs):
1612 create_astakos_user(instance)
1613 post_save.connect(user_post_save, sender=User)
1616 def astakosuser_pre_save(sender, instance, **kwargs):
1617 instance.aquarium_report = False
1618 instance.new = False
1620 db_instance = AstakosUser.objects.get(id=instance.id)
1621 except AstakosUser.DoesNotExist:
1623 instance.aquarium_report = True
1626 get = AstakosUser.__getattribute__
1627 l = filter(lambda f: get(db_instance, f) != get(instance, f),
1629 instance.aquarium_report = True if l else False
1630 pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
1632 def set_default_group(user):
1634 default = AstakosGroup.objects.get(name='default')
1636 group=default, person=user, date_joined=datetime.now()).save()
1637 except AstakosGroup.DoesNotExist, e:
1641 def astakosuser_post_save(sender, instance, created, **kwargs):
1642 if instance.aquarium_report:
1643 report_user_event(instance, create=instance.new)
1646 set_default_group(instance)
1647 # TODO handle socket.error & IOError
1648 register_users((instance,))
1649 post_save.connect(astakosuser_post_save, sender=AstakosUser)
1652 def resource_post_save(sender, instance, created, **kwargs):
1655 register_resources((instance,))
1656 post_save.connect(resource_post_save, sender=Resource)
1659 def on_quota_disturbed(sender, users, **kwargs):
1660 # print '>>>', locals()
1665 quota_disturbed = Signal(providing_args=["users"])
1666 quota_disturbed.connect(on_quota_disturbed)
1669 def send_quota_disturbed(sender, instance, **kwargs):
1671 extend = users.extend
1672 if sender == Membership:
1673 if not instance.group.is_enabled:
1675 extend([instance.person])
1676 elif sender == AstakosUserQuota:
1677 extend([instance.user])
1678 elif sender == AstakosGroupQuota:
1679 if not instance.group.is_enabled:
1681 extend(instance.group.astakosuser_set.all())
1682 elif sender == AstakosGroup:
1683 if not instance.is_enabled:
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)
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)
1701 def check_closed_join_membership_policy(sender, instance, **kwargs):
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)
1710 def check_auto_accept_join_membership_policy(sender, instance, created, **kwargs):
1714 join_policy = instance.project.application.definition.member_join_policy
1715 if join_policy == get_auto_accept_join():
1717 post_save.connect(check_auto_accept_join_membership_policy, sender=ProjectMembership)