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
65 from astakos.im.settings import (DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
66 AUTH_TOKEN_DURATION, BILLING_FIELDS,
67 EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL)
68 from astakos.im.endpoints.qh import (
69 register_users, send_quota, register_resources
71 from astakos.im import auth_providers
72 from astakos.im.endpoints.aquarium.producer import report_user_event
73 from astakos.im.functions import send_invitation
74 #from astakos.im.tasks import propagate_groupmembers_quota
76 import astakos.im.messages as astakos_messages
78 logger = logging.getLogger(__name__)
80 DEFAULT_CONTENT_TYPE = None
82 content_type = ContentType.objects.get(app_label='im', model='astakosuser')
84 content_type = DEFAULT_CONTENT_TYPE
86 RESOURCE_SEPARATOR = '.'
90 class Service(models.Model):
91 name = models.CharField('Name', max_length=255, unique=True, db_index=True)
92 url = models.FilePathField()
93 icon = models.FilePathField(blank=True)
94 auth_token = models.CharField('Authentication Token', max_length=32,
95 null=True, blank=True)
96 auth_token_created = models.DateTimeField('Token creation date', null=True)
97 auth_token_expires = models.DateTimeField(
98 'Token expiration date', null=True)
100 def renew_token(self):
102 md5.update(self.name.encode('ascii', 'ignore'))
103 md5.update(self.url.encode('ascii', 'ignore'))
104 md5.update(asctime())
106 self.auth_token = b64encode(md5.digest())
107 self.auth_token_created = datetime.now()
108 self.auth_token_expires = self.auth_token_created + \
109 timedelta(hours=AUTH_TOKEN_DURATION)
116 return self.resource_set.all()
119 def resources(self, resources):
121 self.resource_set.create(**s)
123 def add_resource(self, service, resource, uplimit, update=True):
124 """Raises ObjectDoesNotExist, IntegrityError"""
125 resource = Resource.objects.get(service__name=service, name=resource)
127 AstakosUserQuota.objects.update_or_create(user=self,
129 defaults={'uplimit': uplimit})
131 q = self.astakosuserquota_set
132 q.create(resource=resource, uplimit=uplimit)
135 class ResourceMetadata(models.Model):
136 key = models.CharField('Name', max_length=255, unique=True, db_index=True)
137 value = models.CharField('Value', max_length=255)
140 class Resource(models.Model):
141 name = models.CharField('Name', max_length=255, unique=True, db_index=True)
142 meta = models.ManyToManyField(ResourceMetadata)
143 service = models.ForeignKey(Service)
144 desc = models.TextField('Description', null=True)
145 unit = models.CharField('Name', null=True, max_length=255)
146 group = models.CharField('Group', null=True, max_length=255)
149 return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
152 class GroupKind(models.Model):
153 name = models.CharField('Name', max_length=255, unique=True, db_index=True)
159 class AstakosGroup(Group):
160 kind = models.ForeignKey(GroupKind)
161 homepage = models.URLField(
162 'Homepage Url', max_length=255, null=True, blank=True)
163 desc = models.TextField('Description', null=True)
164 policy = models.ManyToManyField(
168 through='AstakosGroupQuota'
170 creation_date = models.DateTimeField(
172 default=datetime.now()
174 issue_date = models.DateTimeField('Issue date', null=True)
175 expiration_date = models.DateTimeField(
179 moderation_enabled = models.BooleanField(
180 'Moderated membership?',
183 approval_date = models.DateTimeField(
188 estimated_participants = models.PositiveIntegerField(
189 'Estimated #members',
193 max_participants = models.PositiveIntegerField(
194 'Maximum numder of participants',
200 def is_disabled(self):
201 if not self.approval_date:
206 def is_enabled(self):
209 if not self.issue_date:
211 if not self.expiration_date:
214 if self.issue_date > now:
216 if now >= self.expiration_date:
223 self.approval_date = datetime.now()
225 quota_disturbed.send(sender=self, users=self.approved_members)
226 #propagate_groupmembers_quota.apply_async(
227 # args=[self], eta=self.issue_date)
228 #propagate_groupmembers_quota.apply_async(
229 # args=[self], eta=self.expiration_date)
234 self.approval_date = None
236 quota_disturbed.send(sender=self, users=self.approved_members)
238 @transaction.commit_manually
239 def approve_member(self, person):
240 m, created = self.membership_set.get_or_create(person=person)
244 transaction.rollback()
249 # def disapprove_member(self, person):
250 # self.membership_set.remove(person=person)
254 q = self.membership_set.select_related().all()
255 return [m.person for m in q]
258 def approved_members(self):
259 q = self.membership_set.select_related().all()
260 return [m.person for m in q if m.is_approved]
265 for q in self.astakosgroupquota_set.select_related().all():
266 d[q.resource] += q.uplimit or inf
269 def add_policy(self, service, resource, uplimit, update=True):
270 """Raises ObjectDoesNotExist, IntegrityError"""
271 resource = Resource.objects.get(service__name=service, name=resource)
273 AstakosGroupQuota.objects.update_or_create(
276 defaults={'uplimit': uplimit}
279 q = self.astakosgroupquota_set
280 q.create(resource=resource, uplimit=uplimit)
284 return self.astakosgroupquota_set.select_related().all()
287 def policies(self, policies):
289 service = p.get('service', None)
290 resource = p.get('resource', None)
291 uplimit = p.get('uplimit', 0)
292 update = p.get('update', True)
293 self.add_policy(service, resource, uplimit, update)
297 return self.owner.all()
300 def owner_details(self):
301 return self.owner.select_related().all()
306 map(self.approve_member, l)
310 class AstakosUserManager(UserManager):
312 def get_auth_provider_user(self, provider, **kwargs):
314 Retrieve AstakosUser instance associated with the specified third party
317 kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
319 return self.get(auth_providers__module=provider, **kwargs)
321 class AstakosUser(User):
323 Extends ``django.contrib.auth.models.User`` by defining additional fields.
325 affiliation = models.CharField('Affiliation', max_length=255, blank=True,
328 # DEPRECATED FIELDS: provider, third_party_identifier moved in
329 # AstakosUserProvider model.
330 provider = models.CharField('Provider', max_length=255, blank=True,
332 # ex. screen_name for twitter, eppn for shibboleth
333 third_party_identifier = models.CharField('Third-party identifier',
334 max_length=255, null=True,
339 user_level = DEFAULT_USER_LEVEL
340 level = models.IntegerField('Inviter level', default=user_level)
341 invitations = models.IntegerField(
342 'Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
344 auth_token = models.CharField('Authentication Token', max_length=32,
345 null=True, blank=True)
346 auth_token_created = models.DateTimeField('Token creation date', null=True)
347 auth_token_expires = models.DateTimeField(
348 'Token expiration date', null=True)
350 updated = models.DateTimeField('Update date')
351 is_verified = models.BooleanField('Is verified?', default=False)
353 email_verified = models.BooleanField('Email verified?', default=False)
355 has_credits = models.BooleanField('Has credits?', default=False)
356 has_signed_terms = models.BooleanField(
357 'I agree with the terms', default=False)
358 date_signed_terms = models.DateTimeField(
359 'Signed terms date', null=True, blank=True)
361 activation_sent = models.DateTimeField(
362 'Activation sent data', null=True, blank=True)
364 policy = models.ManyToManyField(
365 Resource, null=True, through='AstakosUserQuota')
367 astakos_groups = models.ManyToManyField(
368 AstakosGroup, verbose_name=_('agroups'), blank=True,
369 help_text=_(astakos_messages.ASTAKOSUSER_GROUPS_HELP),
370 through='Membership')
372 __has_signed_terms = False
373 disturbed_quota = models.BooleanField('Needs quotaholder syncing',
374 default=False, db_index=True)
376 objects = AstakosUserManager()
378 owner = models.ManyToManyField(
379 AstakosGroup, related_name='owner', null=True)
382 unique_together = ("provider", "third_party_identifier")
384 def __init__(self, *args, **kwargs):
385 super(AstakosUser, self).__init__(*args, **kwargs)
386 self.__has_signed_terms = self.has_signed_terms
388 self.is_active = False
392 return '%s %s' % (self.first_name, self.last_name)
395 def realname(self, value):
396 parts = value.split(' ')
398 self.first_name = parts[0]
399 self.last_name = parts[1]
401 self.last_name = parts[0]
403 def add_permission(self, pname):
404 if self.has_perm(pname):
406 p, created = Permission.objects.get_or_create(codename=pname,
407 name=pname.capitalize(),
408 content_type=content_type)
409 self.user_permissions.add(p)
411 def remove_permission(self, pname):
412 if self.has_perm(pname):
414 p = Permission.objects.get(codename=pname,
415 content_type=content_type)
416 self.user_permissions.remove(p)
419 def invitation(self):
421 return Invitation.objects.get(username=self.email)
422 except Invitation.DoesNotExist:
425 def invite(self, email, realname):
426 inv = Invitation(inviter=self, username=email, realname=realname)
429 self.invitations = max(0, self.invitations - 1)
434 """Returns a dict with the sum of quota limits per resource"""
436 for q in self.policies:
437 d[q.resource] += q.uplimit or inf
438 for m in self.extended_groups:
439 if not m.is_approved:
444 for r, uplimit in g.quota.iteritems():
445 d[r] += uplimit or inf
446 # TODO set default for remaining
451 return self.astakosuserquota_set.select_related().all()
454 def policies(self, policies):
456 service = policies.get('service', None)
457 resource = policies.get('resource', None)
458 uplimit = policies.get('uplimit', 0)
459 update = policies.get('update', True)
460 self.add_policy(service, resource, uplimit, update)
462 def add_policy(self, service, resource, uplimit, update=True):
463 """Raises ObjectDoesNotExist, IntegrityError"""
464 resource = Resource.objects.get(service__name=service, name=resource)
466 AstakosUserQuota.objects.update_or_create(user=self,
468 defaults={'uplimit': uplimit})
470 q = self.astakosuserquota_set
471 q.create(resource=resource, uplimit=uplimit)
473 def remove_policy(self, service, resource):
474 """Raises ObjectDoesNotExist, IntegrityError"""
475 resource = Resource.objects.get(service__name=service, name=resource)
476 q = self.policies.get(resource=resource).delete()
479 def extended_groups(self):
480 return self.membership_set.select_related().all()
482 @extended_groups.setter
483 def extended_groups(self, groups):
485 for name in (groups or ()):
486 group = AstakosGroup.objects.get(name=name)
487 self.membership_set.create(group=group)
489 def save(self, update_timestamps=True, **kwargs):
490 if update_timestamps:
492 self.date_joined = datetime.now()
493 self.updated = datetime.now()
495 # update date_signed_terms if necessary
496 if self.__has_signed_terms != self.has_signed_terms:
497 self.date_signed_terms = datetime.now()
501 self.username = self.email
503 self.validate_unique_email_isactive()
504 if self.is_active and self.activation_sent:
505 # reset the activation sent
506 self.activation_sent = None
508 super(AstakosUser, self).save(**kwargs)
510 def renew_token(self, flush_sessions=False, current_key=None):
512 md5.update(settings.SECRET_KEY)
513 md5.update(self.username)
514 md5.update(self.realname.encode('ascii', 'ignore'))
515 md5.update(asctime())
517 self.auth_token = b64encode(md5.digest())
518 self.auth_token_created = datetime.now()
519 self.auth_token_expires = self.auth_token_created + \
520 timedelta(hours=AUTH_TOKEN_DURATION)
522 self.flush_sessions(current_key)
523 msg = 'Token renewed for %s' % self.email
524 logger.log(LOGGING_LEVEL, msg)
526 def flush_sessions(self, current_key=None):
529 q = q.exclude(session_key=current_key)
531 keys = q.values_list('session_key', flat=True)
533 msg = 'Flushing sessions: %s' % ','.join(keys)
534 logger.log(LOGGING_LEVEL, msg, [])
535 engine = import_module(settings.SESSION_ENGINE)
537 s = engine.SessionStore(k)
540 def __unicode__(self):
541 return '%s (%s)' % (self.realname, self.email)
543 def conflicting_email(self):
544 q = AstakosUser.objects.exclude(username=self.username)
545 q = q.filter(email__iexact=self.email)
550 def validate_unique_email_isactive(self):
552 Implements a unique_together constraint for email and is_active fields.
554 q = AstakosUser.objects.all()
555 q = q.filter(email = self.email)
556 q = q.filter(is_active = self.is_active)
558 q = q.filter(~Q(id = self.id))
560 raise ValidationError({'__all__': [_(astakos_messages.UNIQUE_EMAIL_IS_ACTIVE_CONSTRAIN_ERR)]})
563 def signed_terms(self):
564 term = get_latest_terms()
567 if not self.has_signed_terms:
569 if not self.date_signed_terms:
571 if self.date_signed_terms < term.date:
572 self.has_signed_terms = False
573 self.date_signed_terms = None
578 def set_invitations_level(self):
580 Update user invitation level
582 level = self.invitation.inviter.level + 1
584 self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
586 def can_login_with_auth_provider(self, provider):
587 if not self.has_auth_provider(provider):
590 return auth_providers.get_provider(provider).is_available_for_login()
592 def can_add_auth_provider(self, provider, **kwargs):
593 provider_settings = auth_providers.get_provider(provider)
594 if not provider_settings.is_available_for_login():
597 if self.has_auth_provider(provider) and \
598 provider_settings.one_per_user:
601 if 'identifier' in kwargs:
603 # provider with specified params already exist
604 existing_user = AstakosUser.objects.get_auth_provider_user(provider,
606 except AstakosUser.DoesNotExist:
613 def can_remove_auth_provider(self, provider):
614 if len(self.get_active_auth_providers()) <= 1:
618 def can_change_password(self):
619 return self.has_auth_provider('local', auth_backend='astakos')
621 def has_auth_provider(self, provider, **kwargs):
622 return bool(self.auth_providers.filter(module=provider,
625 def add_auth_provider(self, provider, **kwargs):
626 if self.can_add_auth_provider(provider, **kwargs):
627 self.auth_providers.create(module=provider, active=True, **kwargs)
629 raise Exception('Cannot add provider')
631 def add_pending_auth_provider(self, pending):
633 Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
636 if not isinstance(pending, PendingThirdPartyUser):
637 pending = PendingThirdPartyUser.objects.get(token=pending)
639 provider = self.add_auth_provider(pending.provider,
640 identifier=pending.third_party_identifier)
642 if email_re.match(pending.email) and pending.email != self.email:
643 self.additionalmail_set.get_or_create(email=pending.email)
648 def remove_auth_provider(self, provider, **kwargs):
649 self.auth_providers.get(module=provider, **kwargs).delete()
652 def get_resend_activation_url(self):
653 return reverse('send_activation', {'user_id': self.pk})
655 def get_activation_url(self, nxt=False):
656 url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
657 quote(self.auth_token))
659 url += "&next=%s" % quote(nxt)
662 def get_password_reset_url(self, token_generator=default_token_generator):
663 return reverse('django.contrib.auth.views.password_reset_confirm',
664 kwargs={'uidb36':int_to_base36(self.id),
665 'token':token_generator.make_token(self)})
667 def get_auth_providers(self):
668 return self.auth_providers.all()
670 def get_available_auth_providers(self):
672 Returns a list of providers available for user to connect to.
675 for module, provider_settings in auth_providers.PROVIDERS.iteritems():
676 if self.can_add_auth_provider(module):
677 providers.append(provider_settings(self))
681 def get_active_auth_providers(self):
683 for provider in self.auth_providers.active():
684 if auth_providers.get_provider(provider.module).is_available_for_login():
685 providers.append(provider)
689 def auth_providers_display(self):
690 return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
693 class AstakosUserAuthProviderManager(models.Manager):
696 return self.filter(active=True)
699 class AstakosUserAuthProvider(models.Model):
701 Available user authentication methods.
703 affiliation = models.CharField('Affiliation', max_length=255, blank=True,
704 null=True, default=None)
705 user = models.ForeignKey(AstakosUser, related_name='auth_providers')
706 module = models.CharField('Provider', max_length=255, blank=False,
708 identifier = models.CharField('Third-party identifier',
709 max_length=255, null=True,
711 active = models.BooleanField(default=True)
712 auth_backend = models.CharField('Backend', max_length=255, blank=False,
715 objects = AstakosUserAuthProviderManager()
718 unique_together = (('identifier', 'module', 'user'), )
722 return auth_providers.get_provider(self.module)
725 def details_display(self):
726 return self.settings.details_tpl % self.__dict__
728 def can_remove(self):
729 return self.user.can_remove_auth_provider(self.module)
731 def delete(self, *args, **kwargs):
732 ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
733 if self.module == 'local':
734 self.user.set_unusable_password()
739 return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
741 def __unicode__(self):
743 return "%s:%s" % (self.module, self.identifier)
744 if self.auth_backend:
745 return "%s:%s" % (self.module, self.auth_backend)
750 class Membership(models.Model):
751 person = models.ForeignKey(AstakosUser)
752 group = models.ForeignKey(AstakosGroup)
753 date_requested = models.DateField(default=datetime.now(), blank=True)
754 date_joined = models.DateField(null=True, db_index=True, blank=True)
757 unique_together = ("person", "group")
759 def save(self, *args, **kwargs):
761 if not self.group.moderation_enabled:
762 self.date_joined = datetime.now()
763 super(Membership, self).save(*args, **kwargs)
766 def is_approved(self):
774 if self.group.max_participants:
775 assert len(self.group.approved_members) + 1 <= self.group.max_participants, \
776 'Maximum participant number has been reached.'
777 self.date_joined = datetime.now()
779 quota_disturbed.send(sender=self, users=(self.person,))
781 def disapprove(self):
783 quota_disturbed.send(sender=self, users=(self.person,))
785 class AstakosQuotaManager(models.Manager):
786 def _update_or_create(self, **kwargs):
788 'update_or_create() must be passed at least one keyword argument'
789 obj, created = self.get_or_create(**kwargs)
790 defaults = kwargs.pop('defaults', {})
792 return obj, True, False
796 [(k, v) for k, v in kwargs.items() if '__' not in k])
797 params.update(defaults)
798 for attr, val in params.items():
799 if hasattr(obj, attr):
800 setattr(obj, attr, val)
801 sid = transaction.savepoint()
802 obj.save(force_update=True)
803 transaction.savepoint_commit(sid)
804 return obj, False, True
805 except IntegrityError, e:
806 transaction.savepoint_rollback(sid)
808 return self.get(**kwargs), False, False
809 except self.model.DoesNotExist:
812 update_or_create = _update_or_create
814 class AstakosGroupQuota(models.Model):
815 objects = AstakosQuotaManager()
816 limit = models.PositiveIntegerField('Limit', null=True) # obsolete field
817 uplimit = models.BigIntegerField('Up limit', null=True)
818 resource = models.ForeignKey(Resource)
819 group = models.ForeignKey(AstakosGroup, blank=True)
822 unique_together = ("resource", "group")
824 class AstakosUserQuota(models.Model):
825 objects = AstakosQuotaManager()
826 limit = models.PositiveIntegerField('Limit', null=True) # obsolete field
827 uplimit = models.BigIntegerField('Up limit', null=True)
828 resource = models.ForeignKey(Resource)
829 user = models.ForeignKey(AstakosUser)
832 unique_together = ("resource", "user")
835 class ApprovalTerms(models.Model):
837 Model for approval terms
840 date = models.DateTimeField(
841 'Issue date', db_index=True, default=datetime.now())
842 location = models.CharField('Terms location', max_length=255)
845 class Invitation(models.Model):
847 Model for registring invitations
849 inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
851 realname = models.CharField('Real name', max_length=255)
852 username = models.CharField('Unique ID', max_length=255, unique=True)
853 code = models.BigIntegerField('Invitation code', db_index=True)
854 is_consumed = models.BooleanField('Consumed?', default=False)
855 created = models.DateTimeField('Creation date', auto_now_add=True)
856 consumed = models.DateTimeField('Consumption date', null=True, blank=True)
858 def __init__(self, *args, **kwargs):
859 super(Invitation, self).__init__(*args, **kwargs)
861 self.code = _generate_invitation_code()
864 self.is_consumed = True
865 self.consumed = datetime.now()
868 def __unicode__(self):
869 return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
872 class EmailChangeManager(models.Manager):
873 @transaction.commit_on_success
874 def change_email(self, activation_key):
876 Validate an activation key and change the corresponding
879 If the key is valid and has not expired, return the ``User``
882 If the key is not valid or has expired, return ``None``.
884 If the key is valid but the ``User`` is already active,
887 After successful email change the activation record is deleted.
889 Throws ValueError if there is already
892 email_change = self.model.objects.get(
893 activation_key=activation_key)
894 if email_change.activation_key_expired():
895 email_change.delete()
896 raise EmailChange.DoesNotExist
897 # is there an active user with this address?
899 AstakosUser.objects.get(email__iexact=email_change.new_email_address)
900 except AstakosUser.DoesNotExist:
903 raise ValueError(_(astakos_messages.NEW_EMAIL_ADDR_RESERVED))
905 user = AstakosUser.objects.get(pk=email_change.user_id)
906 user.email = email_change.new_email_address
908 email_change.delete()
910 except EmailChange.DoesNotExist:
911 raise ValueError(_(astakos_messages.INVALID_ACTIVATION_KEY))
914 class EmailChange(models.Model):
915 new_email_address = models.EmailField(_(u'new e-mail address'),
916 help_text=_(astakos_messages.EMAIL_CHANGE_NEW_ADDR_HELP))
917 user = models.ForeignKey(
918 AstakosUser, unique=True, related_name='emailchange_user')
919 requested_at = models.DateTimeField(default=datetime.now())
920 activation_key = models.CharField(
921 max_length=40, unique=True, db_index=True)
923 objects = EmailChangeManager()
925 def activation_key_expired(self):
926 expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
927 return self.requested_at + expiration_date < datetime.now()
930 class AdditionalMail(models.Model):
932 Model for registring invitations
934 owner = models.ForeignKey(AstakosUser)
935 email = models.EmailField()
938 def _generate_invitation_code():
940 code = randint(1, 2L ** 63 - 1)
942 Invitation.objects.get(code=code)
943 # An invitation with this code already exists, try again
944 except Invitation.DoesNotExist:
948 def get_latest_terms():
950 term = ApprovalTerms.objects.order_by('-id')[0]
956 class PendingThirdPartyUser(models.Model):
958 Model for registring successful third party user authentications
960 third_party_identifier = models.CharField('Third-party identifier', max_length=255, null=True, blank=True)
961 provider = models.CharField('Provider', max_length=255, blank=True)
962 email = models.EmailField(_('e-mail address'), blank=True, null=True)
963 first_name = models.CharField(_('first name'), max_length=30, blank=True)
964 last_name = models.CharField(_('last name'), max_length=30, blank=True)
965 affiliation = models.CharField('Affiliation', max_length=255, blank=True)
966 username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
967 token = models.CharField('Token', max_length=255, null=True, blank=True)
968 created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
971 unique_together = ("provider", "third_party_identifier")
975 return '%s %s' %(self.first_name, self.last_name)
978 def realname(self, value):
979 parts = value.split(' ')
981 self.first_name = parts[0]
982 self.last_name = parts[1]
984 self.last_name = parts[0]
986 def save(self, **kwargs):
989 while not self.username:
990 username = uuid.uuid4().hex[:30]
992 AstakosUser.objects.get(username = username)
993 except AstakosUser.DoesNotExist, e:
994 self.username = username
995 super(PendingThirdPartyUser, self).save(**kwargs)
997 def generate_token(self):
998 self.password = self.third_party_identifier
999 self.last_login = datetime.now()
1000 self.token = default_token_generator.make_token(self)
1002 class SessionCatalog(models.Model):
1003 session_key = models.CharField(_('session key'), max_length=40)
1004 user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1007 def create_astakos_user(u):
1009 AstakosUser.objects.get(user_ptr=u.pk)
1010 except AstakosUser.DoesNotExist:
1011 extended_user = AstakosUser(user_ptr_id=u.pk)
1012 extended_user.__dict__.update(u.__dict__)
1013 extended_user.save()
1014 except BaseException, e:
1018 def fix_superusers(sender, **kwargs):
1019 # Associate superusers with AstakosUser
1020 admins = User.objects.filter(is_superuser=True)
1022 create_astakos_user(u)
1025 def user_post_save(sender, instance, created, **kwargs):
1028 create_astakos_user(instance)
1031 def set_default_group(user):
1033 default = AstakosGroup.objects.get(name='default')
1035 group=default, person=user, date_joined=datetime.now()).save()
1036 except AstakosGroup.DoesNotExist, e:
1040 def astakosuser_pre_save(sender, instance, **kwargs):
1041 instance.aquarium_report = False
1042 instance.new = False
1044 db_instance = AstakosUser.objects.get(id=instance.id)
1045 except AstakosUser.DoesNotExist:
1047 instance.aquarium_report = True
1050 get = AstakosUser.__getattribute__
1051 l = filter(lambda f: get(db_instance, f) != get(instance, f),
1053 instance.aquarium_report = True if l else False
1056 def astakosuser_post_save(sender, instance, created, **kwargs):
1057 if instance.aquarium_report:
1058 report_user_event(instance, create=instance.new)
1061 set_default_group(instance)
1062 # TODO handle socket.error & IOError
1063 register_users((instance,))
1066 def resource_post_save(sender, instance, created, **kwargs):
1069 register_resources((instance,))
1072 def send_quota_disturbed(sender, instance, **kwargs):
1074 extend = users.extend
1075 if sender == Membership:
1076 if not instance.group.is_enabled:
1078 extend([instance.person])
1079 elif sender == AstakosUserQuota:
1080 extend([instance.user])
1081 elif sender == AstakosGroupQuota:
1082 if not instance.group.is_enabled:
1084 extend(instance.group.astakosuser_set.all())
1085 elif sender == AstakosGroup:
1086 if not instance.is_enabled:
1088 quota_disturbed.send(sender=sender, users=users)
1091 def on_quota_disturbed(sender, users, **kwargs):
1092 # print '>>>', locals()
1097 def renew_token(sender, instance, **kwargs):
1098 if not instance.auth_token:
1099 instance.renew_token()
1101 post_syncdb.connect(fix_superusers)
1102 post_save.connect(user_post_save, sender=User)
1103 pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
1104 post_save.connect(astakosuser_post_save, sender=AstakosUser)
1105 post_save.connect(resource_post_save, sender=Resource)
1107 quota_disturbed = Signal(providing_args=["users"])
1108 quota_disturbed.connect(on_quota_disturbed)
1110 post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
1111 post_delete.connect(send_quota_disturbed, sender=Membership)
1112 post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
1113 post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
1114 post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1115 post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1117 pre_save.connect(renew_token, sender=AstakosUser)
1118 pre_save.connect(renew_token, sender=Service)