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, SERVICES
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)
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 unique_together = ("name", "service")
171 return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
174 class GroupKind(models.Model):
175 name = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
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(
190 through='AstakosGroupQuota'
192 creation_date = models.DateTimeField(
194 default=datetime.now()
196 issue_date = models.DateTimeField(
200 expiration_date = models.DateTimeField(
201 _('Expiration date'),
204 moderation_enabled = models.BooleanField(
205 _('Moderated membership?'),
208 approval_date = models.DateTimeField(
209 _('Activation date'),
213 estimated_participants = models.PositiveIntegerField(
214 _('Estimated #members'),
218 max_participants = models.PositiveIntegerField(
219 _('Maximum numder of participants'),
225 def is_disabled(self):
226 if not self.approval_date:
231 def is_enabled(self):
234 if not self.issue_date:
236 if not self.expiration_date:
239 if self.issue_date > now:
241 if now >= self.expiration_date:
248 self.approval_date = datetime.now()
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)
259 self.approval_date = None
261 quota_disturbed.send(sender=self, users=self.approved_members)
263 def approve_member(self, person):
264 m, created = self.membership_set.get_or_create(person=person)
269 q = self.membership_set.select_related().all()
270 return [m.person for m in q]
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]
280 for q in self.astakosgroupquota_set.select_related().all():
281 d[q.resource] += q.uplimit or inf
284 def add_policy(self, service, resource, uplimit, update=True):
285 """Raises ObjectDoesNotExist, IntegrityError"""
286 resource = Resource.objects.get(service__name=service, name=resource)
288 AstakosGroupQuota.objects.update_or_create(
291 defaults={'uplimit': uplimit}
294 q = self.astakosgroupquota_set
295 q.create(resource=resource, uplimit=uplimit)
299 return self.astakosgroupquota_set.select_related().all()
302 def policies(self, 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)
312 return self.owner.all()
315 def owner_details(self):
316 return self.owner.select_related().all()
321 map(self.approve_member, l)
324 def get_default_quota():
325 global _default_quota
327 return _default_quota
328 for s, data in SERVICES.iteritems():
330 lambda d:_default_quota.update(
331 {'%s%s%s' % (s, RESOURCE_SEPARATOR, d.get('name')):d.get('uplimit', 0)}
333 data.get('resources', {})
335 return _default_quota
337 class AstakosUserManager(UserManager):
339 def get_auth_provider_user(self, provider, **kwargs):
341 Retrieve AstakosUser instance associated with the specified third party
344 kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
346 return self.get(auth_providers__module=provider, **kwargs)
348 class AstakosUser(User):
350 Extends ``django.contrib.auth.models.User`` by defining additional fields.
352 affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
355 # DEPRECATED FIELDS: provider, third_party_identifier moved in
356 # AstakosUserProvider model.
357 provider = models.CharField(_('Provider'), max_length=255, blank=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,
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))
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)
377 updated = models.DateTimeField(_('Update date'))
378 is_verified = models.BooleanField(_('Is verified?'), default=False)
380 email_verified = models.BooleanField(_('Email verified?'), default=False)
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)
388 activation_sent = models.DateTimeField(
389 _('Activation sent data'), null=True, blank=True)
391 policy = models.ManyToManyField(
392 Resource, null=True, through='AstakosUserQuota')
394 astakos_groups = models.ManyToManyField(
395 AstakosGroup, verbose_name=_('agroups'), blank=True,
396 help_text=_(astakos_messages.ASTAKOSUSER_GROUPS_HELP),
397 through='Membership')
399 __has_signed_terms = False
400 disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
401 default=False, db_index=True)
403 objects = AstakosUserManager()
405 owner = models.ManyToManyField(
406 AstakosGroup, related_name='owner', null=True)
409 unique_together = ("provider", "third_party_identifier")
411 def __init__(self, *args, **kwargs):
412 super(AstakosUser, self).__init__(*args, **kwargs)
413 self.__has_signed_terms = self.has_signed_terms
415 self.is_active = False
419 return '%s %s' % (self.first_name, self.last_name)
422 def realname(self, value):
423 parts = value.split(' ')
425 self.first_name = parts[0]
426 self.last_name = parts[1]
428 self.last_name = parts[0]
430 def add_permission(self, pname):
431 if self.has_perm(pname):
433 p, created = Permission.objects.get_or_create(
435 name=pname.capitalize(),
436 content_type=get_content_type())
437 self.user_permissions.add(p)
439 def remove_permission(self, pname):
440 if self.has_perm(pname):
442 p = Permission.objects.get(codename=pname,
443 content_type=get_content_type())
444 self.user_permissions.remove(p)
447 def invitation(self):
449 return Invitation.objects.get(username=self.email)
450 except Invitation.DoesNotExist:
453 def invite(self, email, realname):
454 inv = Invitation(inviter=self, username=email, realname=realname)
457 self.invitations = max(0, self.invitations - 1)
462 """Returns a dict with the sum of quota limits per resource"""
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:
474 grants = p.application.definition.projectresourcegrant_set.all()
476 d[str(g.resource)] += g.member_limit or inf
477 # TODO set default for remaining
482 return self.astakosuserquota_set.select_related().all()
485 def policies(self, 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)
493 def add_policy(self, service, resource, uplimit, update=True):
494 """Raises ObjectDoesNotExist, IntegrityError"""
495 resource = Resource.objects.get(service__name=service, name=resource)
497 AstakosUserQuota.objects.update_or_create(user=self,
499 defaults={'uplimit': uplimit})
501 q = self.astakosuserquota_set
502 q.create(resource=resource, uplimit=uplimit)
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()
510 def extended_groups(self):
511 return self.membership_set.select_related().all()
513 @extended_groups.setter
514 def extended_groups(self, groups):
516 for name in (groups or ()):
517 group = AstakosGroup.objects.get(name=name)
518 self.membership_set.create(group=group)
520 def save(self, update_timestamps=True, **kwargs):
521 if update_timestamps:
523 self.date_joined = datetime.now()
524 self.updated = datetime.now()
526 # update date_signed_terms if necessary
527 if self.__has_signed_terms != self.has_signed_terms:
528 self.date_signed_terms = datetime.now()
532 self.username = self.email
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
539 super(AstakosUser, self).save(**kwargs)
541 def renew_token(self, flush_sessions=False, current_key=None):
543 md5.update(settings.SECRET_KEY)
544 md5.update(self.username)
545 md5.update(self.realname.encode('ascii', 'ignore'))
546 md5.update(asctime())
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)
553 self.flush_sessions(current_key)
554 msg = 'Token renewed for %s' % self.email
555 logger.log(LOGGING_LEVEL, msg)
557 def flush_sessions(self, current_key=None):
560 q = q.exclude(session_key=current_key)
562 keys = q.values_list('session_key', flat=True)
564 msg = 'Flushing sessions: %s' % ','.join(keys)
565 logger.log(LOGGING_LEVEL, msg, [])
566 engine = import_module(settings.SESSION_ENGINE)
568 s = engine.SessionStore(k)
571 def __unicode__(self):
572 return '%s (%s)' % (self.realname, self.email)
574 def conflicting_email(self):
575 q = AstakosUser.objects.exclude(username=self.username)
576 q = q.filter(email__iexact=self.email)
581 def validate_unique_email_isactive(self):
583 Implements a unique_together constraint for email and is_active fields.
585 q = AstakosUser.objects.all()
586 q = q.filter(email = self.email)
587 q = q.filter(is_active = self.is_active)
589 q = q.filter(~Q(id = self.id))
591 raise ValidationError({'__all__': [_(astakos_messages.UNIQUE_EMAIL_IS_ACTIVE_CONSTRAIN_ERR)]})
594 def signed_terms(self):
595 term = get_latest_terms()
598 if not self.has_signed_terms:
600 if not self.date_signed_terms:
602 if self.date_signed_terms < term.date:
603 self.has_signed_terms = False
604 self.date_signed_terms = None
609 def set_invitations_level(self):
611 Update user invitation level
613 level = self.invitation.inviter.level + 1
615 self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
617 def can_login_with_auth_provider(self, provider):
618 if not self.has_auth_provider(provider):
621 return auth_providers.get_provider(provider).is_available_for_login()
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():
628 if self.has_auth_provider(provider) and \
629 provider_settings.one_per_user:
632 if 'identifier' in kwargs:
634 # provider with specified params already exist
635 existing_user = AstakosUser.objects.get_auth_provider_user(provider,
637 except AstakosUser.DoesNotExist:
644 def can_remove_auth_provider(self, provider):
645 if len(self.get_active_auth_providers()) <= 1:
649 def can_change_password(self):
650 return self.has_auth_provider('local', auth_backend='astakos')
652 def has_auth_provider(self, provider, **kwargs):
653 return bool(self.auth_providers.filter(module=provider,
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)
660 raise Exception('Cannot add provider')
662 def add_pending_auth_provider(self, pending):
664 Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
667 if not isinstance(pending, PendingThirdPartyUser):
668 pending = PendingThirdPartyUser.objects.get(token=pending)
670 provider = self.add_auth_provider(pending.provider,
671 identifier=pending.third_party_identifier)
673 if email_re.match(pending.email or '') and pending.email != self.email:
674 self.additionalmail_set.get_or_create(email=pending.email)
679 def remove_auth_provider(self, provider, **kwargs):
680 self.auth_providers.get(module=provider, **kwargs).delete()
683 def get_resend_activation_url(self):
684 return reverse('send_activation', {'user_id': self.pk})
686 def get_activation_url(self, nxt=False):
687 url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
688 quote(self.auth_token))
690 url += "&next=%s" % quote(nxt)
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)})
698 def get_auth_providers(self):
699 return self.auth_providers.all()
701 def get_available_auth_providers(self):
703 Returns a list of providers available for user to connect to.
706 for module, provider_settings in auth_providers.PROVIDERS.iteritems():
707 if self.can_add_auth_provider(module):
708 providers.append(provider_settings(self))
712 def get_active_auth_providers(self):
714 for provider in self.auth_providers.active():
715 if auth_providers.get_provider(provider.module).is_available_for_login():
716 providers.append(provider)
720 def auth_providers_display(self):
721 return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
724 class AstakosUserAuthProviderManager(models.Manager):
727 return self.filter(active=True)
730 class AstakosUserAuthProvider(models.Model):
732 Available user authentication methods.
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,
739 identifier = models.CharField(_('Third-party identifier'),
740 max_length=255, null=True,
742 active = models.BooleanField(default=True)
743 auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
746 objects = AstakosUserAuthProviderManager()
749 unique_together = (('identifier', 'module', 'user'), )
753 return auth_providers.get_provider(self.module)
756 def details_display(self):
757 return self.settings.details_tpl % self.__dict__
759 def can_remove(self):
760 return self.user.can_remove_auth_provider(self.module)
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()
770 return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
772 def __unicode__(self):
774 return "%s:%s" % (self.module, self.identifier)
775 if self.auth_backend:
776 return "%s:%s" % (self.module, self.auth_backend)
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)
788 unique_together = ("person", "group")
790 def save(self, *args, **kwargs):
792 if not self.group.moderation_enabled:
793 self.date_joined = datetime.now()
794 super(Membership, self).save(*args, **kwargs)
797 def is_approved(self):
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()
810 quota_disturbed.send(sender=self, users=(self.person,))
812 def disapprove(self):
813 approved = self.is_approved()
816 quota_disturbed.send(sender=self, users=(self.person,))
818 class ExtendedManager(models.Manager):
819 def _update_or_create(self, **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', {})
825 return obj, True, False
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)
841 return self.get(**kwargs), False, False
842 except self.model.DoesNotExist:
845 update_or_create = _update_or_create
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)
855 unique_together = ("resource", "group")
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)
865 unique_together = ("resource", "user")
868 class ApprovalTerms(models.Model):
870 Model for approval terms
873 date = models.DateTimeField(
874 _('Issue date'), db_index=True, default=datetime.now())
875 location = models.CharField(_('Terms location'), max_length=255)
878 class Invitation(models.Model):
880 Model for registring invitations
882 inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
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)
891 def __init__(self, *args, **kwargs):
892 super(Invitation, self).__init__(*args, **kwargs)
894 self.code = _generate_invitation_code()
897 self.is_consumed = True
898 self.consumed = datetime.now()
901 def __unicode__(self):
902 return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
905 class EmailChangeManager(models.Manager):
906 @transaction.commit_on_success
907 def change_email(self, activation_key):
909 Validate an activation key and change the corresponding
912 If the key is valid and has not expired, return the ``User``
915 If the key is not valid or has expired, return ``None``.
917 If the key is valid but the ``User`` is already active,
920 After successful email change the activation record is deleted.
922 Throws ValueError if there is already
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?
932 AstakosUser.objects.get(email__iexact=email_change.new_email_address)
933 except AstakosUser.DoesNotExist:
936 raise ValueError(_(astakos_messages.NEW_EMAIL_ADDR_RESERVED))
938 user = AstakosUser.objects.get(pk=email_change.user_id)
939 user.email = email_change.new_email_address
941 email_change.delete()
943 except EmailChange.DoesNotExist:
944 raise ValueError(_(astakos_messages.INVALID_ACTIVATION_KEY))
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)
956 objects = EmailChangeManager()
958 def activation_key_expired(self):
959 expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
960 return self.requested_at + expiration_date < datetime.now()
963 class AdditionalMail(models.Model):
965 Model for registring invitations
967 owner = models.ForeignKey(AstakosUser)
968 email = models.EmailField()
971 def _generate_invitation_code():
973 code = randint(1, 2L ** 63 - 1)
975 Invitation.objects.get(code=code)
976 # An invitation with this code already exists, try again
977 except Invitation.DoesNotExist:
981 def get_latest_terms():
983 term = ApprovalTerms.objects.order_by('-id')[0]
989 class PendingThirdPartyUser(models.Model):
991 Model for registring successful third party user authentications
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)
1004 unique_together = ("provider", "third_party_identifier")
1008 return '%s %s' %(self.first_name, self.last_name)
1011 def realname(self, value):
1012 parts = value.split(' ')
1014 self.first_name = parts[0]
1015 self.last_name = parts[1]
1017 self.last_name = parts[0]
1019 def save(self, **kwargs):
1022 while not self.username:
1023 username = uuid.uuid4().hex[:30]
1025 AstakosUser.objects.get(username = username)
1026 except AstakosUser.DoesNotExist, e:
1027 self.username = username
1028 super(PendingThirdPartyUser, self).save(**kwargs)
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)
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)
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)
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)
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
1059 auto_accept = MemberJoinPolicy.objects.get(policy='auto_accept')
1062 _auto_accept_join = auto_accept
1065 _closed_join = False
1066 def get_closed_join():
1068 if _closed_join is not False:
1071 closed = MemberJoinPolicy.objects.get(policy='closed')
1074 _closed_join = closed
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
1083 auto_accept = MemberLeavePolicy.objects.get(policy='auto_accept')
1086 _auto_accept_leave = auto_accept
1089 _closed_leave = False
1090 def get_closed_leave():
1091 global _closed_leave
1092 if _closed_leave is not False:
1093 return _closed_leave
1095 closed = MemberLeavePolicy.objects.get(policy='closed')
1098 _closed_leave = closed
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(
1114 through='ProjectResourceGrant'
1118 def violated_resource_grants(self):
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)
1125 ProjectResourceGrant.objects.update_or_create(
1126 project_definition=self,
1128 defaults={'member_limit': uplimit}
1131 q = self.projectresourcegrant_set
1132 q.create(resource=resource, member_limit=uplimit)
1135 def resource_policies(self):
1136 return self.projectresourcegrant_set.all()
1138 @resource_policies.setter
1139 def resource_policies(self, 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)
1147 def validate_name(self):
1149 Validate name uniqueness among all active projects.
1151 alive_projects = list(get_alive_projects())
1153 lambda p: p.definition.name == self.name and \
1154 p.application.id != self.projectapplication.id,
1158 raise ValidationError(
1159 _(astakos_messages.UNIQUE_PROJECT_NAME_CONSTRAIN_ERR)
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)
1171 unique_together = ("resource", "project_definition")
1174 class ProjectApplication(models.Model):
1175 states_list = [PENDING, APPROVED, REPLACED, UNKNOWN]
1176 states = dict((k, v) for k, v in enumerate(states_list))
1178 applicant = models.ForeignKey(
1180 related_name='my_project_applications',
1182 owner = models.ForeignKey(
1184 related_name='own_project_applications',
1187 comments = models.TextField(null=True, blank=True)
1188 definition = models.OneToOneField(ProjectDefinition)
1189 issue_date = models.DateTimeField()
1190 precursor_application = models.OneToOneField('ProjectApplication',
1195 state = models.CharField(max_length=80, default=UNKNOWN)
1200 return ProjectApplication.objects.get(precursor_application=self)
1201 except ProjectApplication.DoesNotExist:
1205 self.definition.save()
1206 self.definition = self.definition
1207 super(ProjectApplication, self).save()
1211 def submit(definition, resource_policies, applicant, comments, precursor_application=None, commit=True):
1213 if precursor_application:
1214 precursor_application_id = precursor_application.id
1215 application = precursor_application
1216 application.id = None
1217 application.precursor_application = None
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
1228 application.definition.resource_policies = resource_policies
1229 # better implementation ???
1230 if precursor_application:
1232 precursor = ProjectApplication.objects.get(id=precursor_application_id)
1235 application.precursor_application = precursor
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__
1247 def approve(self, approval_user=None):
1249 If approval_user then during owner membership acceptance
1250 it is checked whether the request_user is eligible.
1253 ValidationError: if there is other alive project with the same name
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))
1264 self.precursor_application.project
1271 'creation_date':datetime.now(),
1272 'last_approval_date':datetime.now(),
1274 project = _create_object(Project, **kwargs)
1275 project.accept_member(self.owner, approval_user)
1277 project = self.precursor_application.project
1278 project.application = self
1279 project.last_approval_date = datetime.now()
1281 precursor = self.precursor_application
1283 precursor.state = REPLACED
1285 precursor = precursor.precursor_application
1286 self.state = APPROVED
1289 # self.definition.validate_name()
1291 notification = build_notification(
1292 settings.SERVER_EMAIL,
1294 _('Project application has been approved on %s alpha2 testing' % SITENAME),
1295 _('Your application request %(id)s has been approved.') % self.id
1299 rejected = self.project.sync()
1301 # revert to precursor
1302 project.application = app.precursor_application
1303 if project.application:
1304 project.last_approval_date = last_approval_date
1306 rejected = project.sync()
1308 raise Exception(_(astakos_messages.QH_SYNC_ERROR))
1310 project.last_application_synced = self
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
1328 def definition(self):
1329 return self.application.definition
1332 def violated_members_number_limit(self):
1333 return len(self.approved_members) <= self.definition.limit_on_members_number
1336 def is_active(self):
1337 if not self.last_approval_date:
1339 if self.termination_date:
1341 if self.definition.violated_resource_grants:
1343 # if self.violated_members_number_limit:
1348 def is_terminated(self):
1349 if not self.termination_date:
1354 def is_suspended(self):
1355 if self.termination_date:
1357 if self.last_approval_date:
1358 if not self.definition.violated_resource_grants:
1360 # if not self.violated_members_number_limit:
1366 return self.is_active or self.is_suspended
1369 def is_inconsistent(self):
1370 now = datetime.now()
1371 if self.creation_date > now:
1373 if self.last_approval_date > now:
1375 if self.terminaton_date > now:
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)
1386 def approved_members(self):
1387 return [m.person for m in self.projectmembership_set.filter(~Q(acceptance_date=None))]
1389 def sync(self, specific_members=()):
1390 if self.is_synchronized:
1392 members = specific_members or self.approved_members
1393 c, rejected = send_quota(self.approved_members)
1396 def accept_member(self, user, request_user=None):
1399 django.exceptions.PermissionDenied
1400 astakos.im.models.AstakosUser.DoesNotExist
1402 if isinstance(user, int):
1404 user = lookup_object(AstakosUser, user, None, None)
1406 raise AstakosUser.DoesNotExist()
1407 m, created = ProjectMembership.objects.get_or_create(
1408 person=user, project=self
1410 m.accept(delete_on_failure=created, request_user=None)
1412 def reject_member(self, user, request_user=None):
1415 django.exceptions.PermissionDenied
1416 astakos.im.models.AstakosUser.DoesNotExist
1417 astakos.im.models.ProjectMembership.DoesNotExist
1419 if isinstance(user, int):
1421 user = lookup_object(AstakosUser, user, None, None)
1423 raise AstakosUser.DoesNotExist()
1424 m = ProjectMembership.objects.get(person=user, project=self)
1427 def remove_member(self, user, request_user=None):
1430 django.exceptions.PermissionDenied
1431 astakos.im.models.AstakosUser.DoesNotExist
1432 astakos.im.models.ProjectMembership.DoesNotExist
1434 if isinstance(user, int):
1436 user = lookup_object(AstakosUser, user, None, None)
1438 raise AstakosUser.DoesNotExist()
1439 m = ProjectMembership.objects.get(person=user, project=self)
1442 def terminate(self):
1443 self.termination_start_date = datetime.now()
1444 self.terminaton_date = None
1447 rejected = self.sync()
1449 self.termination_start_date = None
1450 self.terminaton_date = datetime.now()
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__
1462 self.last_approval_date = None
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__
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)
1480 unique_together = ("person", "project")
1482 def accept(self, delete_on_failure=False, request_user=None):
1485 django.exception.PermissionDenied
1486 astakos.im.notifications.NotificationError
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:
1503 if self.acceptance_date:
1505 self.acceptance_date = datetime.now()
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__
1515 def reject(self, request_user=None):
1518 django.exception.PermissionDenied,
1519 astakos.im.notifications.NotificationError
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(
1529 project=self.project,
1530 request_date=self.request_date,
1531 rejection_date=datetime.now()
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__
1542 def remove(self, request_user=None):
1545 django.exception.PermissionDenied
1546 astakos.im.notifications.NotificationError
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(
1557 project=self.project,
1558 request_date=self.request_date,
1559 removal_date=datetime.now()
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__
1572 leave_policy = self.project.application.definition.member_leave_policy
1573 if leave_policy == get_auto_accept_leave():
1576 self.leave_request_date = datetime.now()
1580 # set membership_dirty flag
1581 self.project.membership_dirty = True
1584 rejected = self.project.sync(specific_members=[self.person])
1586 # if syncing was successful unset membership_dirty flag
1587 self.membership_dirty = False
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)
1599 def filter_queryset_by_property(q, property):
1601 Incorporate list comprehension for filtering querysets by property
1602 since Queryset.filter() operates on the database level.
1604 return (p for p in q if getattr(p, property, False))
1606 def get_alive_projects():
1607 return filter_queryset_by_property(
1608 Project.objects.all(),
1612 def get_active_projects():
1613 return filter_queryset_by_property(
1614 Project.objects.all(),
1618 def _create_object(model, **kwargs):
1619 o = model.objects.create(**kwargs)
1624 def create_astakos_user(u):
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:
1637 def fix_superusers(sender, **kwargs):
1638 # Associate superusers with AstakosUser
1639 admins = User.objects.filter(is_superuser=True)
1641 create_astakos_user(u)
1642 post_syncdb.connect(fix_superusers)
1645 def user_post_save(sender, instance, created, **kwargs):
1648 create_astakos_user(instance)
1649 post_save.connect(user_post_save, sender=User)
1652 def astakosuser_pre_save(sender, instance, **kwargs):
1653 instance.aquarium_report = False
1654 instance.new = False
1656 db_instance = AstakosUser.objects.get(id=instance.id)
1657 except AstakosUser.DoesNotExist:
1659 instance.aquarium_report = True
1662 get = AstakosUser.__getattribute__
1663 l = filter(lambda f: get(db_instance, f) != get(instance, f),
1665 instance.aquarium_report = True if l else False
1666 pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
1668 def set_default_group(user):
1670 default = AstakosGroup.objects.get(name='default')
1672 group=default, person=user, date_joined=datetime.now()).save()
1673 except AstakosGroup.DoesNotExist, e:
1677 def astakosuser_post_save(sender, instance, created, **kwargs):
1678 if instance.aquarium_report:
1679 report_user_event(instance, create=instance.new)
1682 set_default_group(instance)
1683 # TODO handle socket.error & IOError
1684 register_users((instance,))
1685 post_save.connect(astakosuser_post_save, sender=AstakosUser)
1688 def resource_post_save(sender, instance, created, **kwargs):
1691 register_resources((instance,))
1692 post_save.connect(resource_post_save, sender=Resource)
1695 def on_quota_disturbed(sender, users, **kwargs):
1696 # print '>>>', locals()
1701 quota_disturbed = Signal(providing_args=["users"])
1702 quota_disturbed.connect(on_quota_disturbed)
1705 def send_quota_disturbed(sender, instance, **kwargs):
1707 extend = users.extend
1708 if sender == Membership:
1709 if not instance.group.is_enabled:
1711 extend([instance.person])
1712 elif sender == AstakosUserQuota:
1713 extend([instance.user])
1714 elif sender == AstakosGroupQuota:
1715 if not instance.group.is_enabled:
1717 extend(instance.group.astakosuser_set.all())
1718 elif sender == AstakosGroup:
1719 if not instance.is_enabled:
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)
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)
1737 def check_closed_join_membership_policy(sender, instance, **kwargs):
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)
1746 def check_auto_accept_join_membership_policy(sender, instance, created, **kwargs):
1749 join_policy = instance.project.application.definition.member_join_policy
1750 if join_policy == get_auto_accept_join():
1752 post_save.connect(check_auto_accept_join_membership_policy, sender=ProjectMembership)