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.
39 from time import asctime
40 from datetime import datetime, timedelta
41 from base64 import b64encode
42 from urlparse import urlparse
43 from urllib import quote
44 from random import randint
45 from collections import defaultdict
47 from django.db import models, IntegrityError
48 from django.contrib.auth.models import User, UserManager, Group, Permission
49 from django.utils.translation import ugettext as _
50 from django.db import transaction
51 from django.core.exceptions import ValidationError
52 from django.db.models.signals import (
53 pre_save, post_save, post_syncdb, post_delete
55 from django.contrib.contenttypes.models import ContentType
57 from django.dispatch import Signal
58 from django.db.models import Q
59 from django.core.urlresolvers import reverse
60 from django.utils.http import int_to_base36
61 from django.contrib.auth.tokens import default_token_generator
62 from django.conf import settings
63 from django.utils.importlib import import_module
64 from django.utils.safestring import mark_safe
65 from django.core.validators import email_re
67 from astakos.im.settings import (DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
68 AUTH_TOKEN_DURATION, BILLING_FIELDS,
69 EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL)
70 from astakos.im import settings as astakos_settings
71 from astakos.im.endpoints.qh import (
72 register_users, send_quota, register_resources
74 from astakos.im import auth_providers
75 from astakos.im.endpoints.aquarium.producer import report_user_event
76 from astakos.im.functions import send_invitation
77 #from astakos.im.tasks import propagate_groupmembers_quota
79 import astakos.im.messages as astakos_messages
81 logger = logging.getLogger(__name__)
83 DEFAULT_CONTENT_TYPE = None
85 content_type = ContentType.objects.get(app_label='im', model='astakosuser')
87 content_type = DEFAULT_CONTENT_TYPE
89 RESOURCE_SEPARATOR = '.'
93 class Service(models.Model):
94 name = models.CharField('Name', max_length=255, unique=True, db_index=True)
95 url = models.FilePathField()
96 icon = models.FilePathField(blank=True)
97 auth_token = models.CharField('Authentication Token', max_length=32,
98 null=True, blank=True)
99 auth_token_created = models.DateTimeField('Token creation date', null=True)
100 auth_token_expires = models.DateTimeField(
101 'Token expiration date', null=True)
103 def renew_token(self):
105 md5.update(self.name.encode('ascii', 'ignore'))
106 md5.update(self.url.encode('ascii', 'ignore'))
107 md5.update(asctime())
109 self.auth_token = b64encode(md5.digest())
110 self.auth_token_created = datetime.now()
111 self.auth_token_expires = self.auth_token_created + \
112 timedelta(hours=AUTH_TOKEN_DURATION)
119 return self.resource_set.all()
122 def resources(self, resources):
124 self.resource_set.create(**s)
126 def add_resource(self, service, resource, uplimit, update=True):
127 """Raises ObjectDoesNotExist, IntegrityError"""
128 resource = Resource.objects.get(service__name=service, name=resource)
130 AstakosUserQuota.objects.update_or_create(user=self,
132 defaults={'uplimit': uplimit})
134 q = self.astakosuserquota_set
135 q.create(resource=resource, uplimit=uplimit)
138 class ResourceMetadata(models.Model):
139 key = models.CharField('Name', max_length=255, unique=True, db_index=True)
140 value = models.CharField('Value', max_length=255)
143 class Resource(models.Model):
144 name = models.CharField('Name', max_length=255, unique=True, db_index=True)
145 meta = models.ManyToManyField(ResourceMetadata)
146 service = models.ForeignKey(Service)
147 desc = models.TextField('Description', null=True)
148 unit = models.CharField('Name', null=True, max_length=255)
149 group = models.CharField('Group', null=True, max_length=255)
152 return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
155 class GroupKind(models.Model):
156 name = models.CharField('Name', max_length=255, unique=True, db_index=True)
162 class AstakosGroup(Group):
163 kind = models.ForeignKey(GroupKind)
164 homepage = models.URLField(
165 'Homepage Url', max_length=255, null=True, blank=True)
166 desc = models.TextField('Description', null=True)
167 policy = models.ManyToManyField(
171 through='AstakosGroupQuota'
173 creation_date = models.DateTimeField(
175 default=datetime.now()
177 issue_date = models.DateTimeField('Start date', null=True)
178 expiration_date = models.DateTimeField(
182 moderation_enabled = models.BooleanField(
183 'Moderated membership?',
186 approval_date = models.DateTimeField(
191 estimated_participants = models.PositiveIntegerField(
192 'Estimated #members',
196 max_participants = models.PositiveIntegerField(
197 'Maximum numder of participants',
203 def is_disabled(self):
204 if not self.approval_date:
209 def is_enabled(self):
212 if not self.issue_date:
214 if not self.expiration_date:
217 if self.issue_date > now:
219 if now >= self.expiration_date:
226 self.approval_date = datetime.now()
228 quota_disturbed.send(sender=self, users=self.approved_members)
229 #propagate_groupmembers_quota.apply_async(
230 # args=[self], eta=self.issue_date)
231 #propagate_groupmembers_quota.apply_async(
232 # args=[self], eta=self.expiration_date)
237 self.approval_date = None
239 quota_disturbed.send(sender=self, users=self.approved_members)
241 @transaction.commit_manually
242 def approve_member(self, person):
243 m, created = self.membership_set.get_or_create(person=person)
247 transaction.rollback()
252 # def disapprove_member(self, person):
253 # self.membership_set.remove(person=person)
257 q = self.membership_set.select_related().all()
258 return [m.person for m in q]
261 def approved_members(self):
262 q = self.membership_set.select_related().all()
263 return [m.person for m in q if m.is_approved]
268 for q in self.astakosgroupquota_set.select_related().all():
269 d[q.resource] += q.uplimit or inf
272 def add_policy(self, service, resource, uplimit, update=True):
273 """Raises ObjectDoesNotExist, IntegrityError"""
274 resource = Resource.objects.get(service__name=service, name=resource)
276 AstakosGroupQuota.objects.update_or_create(
279 defaults={'uplimit': uplimit}
282 q = self.astakosgroupquota_set
283 q.create(resource=resource, uplimit=uplimit)
287 return self.astakosgroupquota_set.select_related().all()
290 def policies(self, policies):
292 service = p.get('service', None)
293 resource = p.get('resource', None)
294 uplimit = p.get('uplimit', 0)
295 update = p.get('update', True)
296 self.add_policy(service, resource, uplimit, update)
300 return self.owner.all()
303 def owner_details(self):
304 return self.owner.select_related().all()
309 map(self.approve_member, l)
313 class AstakosUserManager(UserManager):
315 def get_auth_provider_user(self, provider, **kwargs):
317 Retrieve AstakosUser instance associated with the specified third party
320 kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
322 return self.get(auth_providers__module=provider, **kwargs)
324 def get_by_email(self, email):
325 return self.get(email=email)
327 def get_by_identifier(self, email_or_username, **kwargs):
329 return self.get(email__iexact=email_or_username, **kwargs)
330 except AstakosUser.DoesNotExist:
331 return self.get(username__iexact=email_or_username, **kwargs)
333 def user_exists(self, email_or_username, **kwargs):
334 qemail = Q(email__iexact=email_or_username)
335 qusername = Q(username__iexact=email_or_username)
336 return self.filter(qemail | qusername).exists()
339 class AstakosUser(User):
341 Extends ``django.contrib.auth.models.User`` by defining additional fields.
343 affiliation = models.CharField('Affiliation', max_length=255, blank=True,
346 # DEPRECATED FIELDS: provider, third_party_identifier moved in
347 # AstakosUserProvider model.
348 provider = models.CharField('Provider', max_length=255, blank=True,
350 # ex. screen_name for twitter, eppn for shibboleth
351 third_party_identifier = models.CharField('Third-party identifier',
352 max_length=255, null=True,
357 user_level = DEFAULT_USER_LEVEL
358 level = models.IntegerField('Inviter level', default=user_level)
359 invitations = models.IntegerField(
360 'Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
362 auth_token = models.CharField('Authentication Token', max_length=32,
363 null=True, blank=True)
364 auth_token_created = models.DateTimeField('Token creation date', null=True)
365 auth_token_expires = models.DateTimeField(
366 'Token expiration date', null=True)
368 updated = models.DateTimeField('Update date')
369 is_verified = models.BooleanField('Is verified?', default=False)
371 email_verified = models.BooleanField('Email verified?', default=False)
373 has_credits = models.BooleanField('Has credits?', default=False)
374 has_signed_terms = models.BooleanField(
375 'I agree with the terms', default=False)
376 date_signed_terms = models.DateTimeField(
377 'Signed terms date', null=True, blank=True)
379 activation_sent = models.DateTimeField(
380 'Activation sent data', null=True, blank=True)
382 policy = models.ManyToManyField(
383 Resource, null=True, through='AstakosUserQuota')
385 uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
387 astakos_groups = models.ManyToManyField(
388 AstakosGroup, verbose_name=_('agroups'), blank=True,
389 help_text=_(astakos_messages.ASTAKOSUSER_GROUPS_HELP),
390 through='Membership')
392 __has_signed_terms = False
393 disturbed_quota = models.BooleanField('Needs quotaholder syncing',
394 default=False, db_index=True)
396 objects = AstakosUserManager()
399 owner = models.ManyToManyField(
400 AstakosGroup, related_name='owner', null=True)
402 def __init__(self, *args, **kwargs):
403 super(AstakosUser, self).__init__(*args, **kwargs)
404 self.__has_signed_terms = self.has_signed_terms
406 self.is_active = False
410 return '%s %s' % (self.first_name, self.last_name)
413 def realname(self, value):
414 parts = value.split(' ')
416 self.first_name = parts[0]
417 self.last_name = parts[1]
419 self.last_name = parts[0]
421 def add_permission(self, pname):
422 if self.has_perm(pname):
424 p, created = Permission.objects.get_or_create(codename=pname,
425 name=pname.capitalize(),
426 content_type=content_type)
427 self.user_permissions.add(p)
429 def remove_permission(self, pname):
430 if self.has_perm(pname):
432 p = Permission.objects.get(codename=pname,
433 content_type=content_type)
434 self.user_permissions.remove(p)
437 def invitation(self):
439 return Invitation.objects.get(username=self.email)
440 except Invitation.DoesNotExist:
443 def invite(self, email, realname):
444 inv = Invitation(inviter=self, username=email, realname=realname)
447 self.invitations = max(0, self.invitations - 1)
452 """Returns a dict with the sum of quota limits per resource"""
454 for q in self.policies:
455 d[q.resource] += q.uplimit or inf
456 for m in self.extended_groups:
457 if not m.is_approved:
462 for r, uplimit in g.quota.iteritems():
463 d[r] += uplimit or inf
464 # TODO set default for remaining
469 return self.astakosuserquota_set.select_related().all()
472 def policies(self, policies):
474 service = policies.get('service', None)
475 resource = policies.get('resource', None)
476 uplimit = policies.get('uplimit', 0)
477 update = policies.get('update', True)
478 self.add_policy(service, resource, uplimit, update)
480 def add_policy(self, service, resource, uplimit, update=True):
481 """Raises ObjectDoesNotExist, IntegrityError"""
482 resource = Resource.objects.get(service__name=service, name=resource)
484 AstakosUserQuota.objects.update_or_create(user=self,
486 defaults={'uplimit': uplimit})
488 q = self.astakosuserquota_set
489 q.create(resource=resource, uplimit=uplimit)
491 def remove_policy(self, service, resource):
492 """Raises ObjectDoesNotExist, IntegrityError"""
493 resource = Resource.objects.get(service__name=service, name=resource)
494 q = self.policies.get(resource=resource).delete()
496 def update_uuid(self):
498 uuid_val = str(uuid.uuid4())
500 AstakosUser.objects.get(uuid=uuid_val)
501 except AstakosUser.DoesNotExist, e:
506 def extended_groups(self):
507 return self.membership_set.select_related().all()
509 @extended_groups.setter
510 def extended_groups(self, groups):
512 for name in (groups or ()):
513 group = AstakosGroup.objects.get(name=name)
514 self.membership_set.create(group=group)
516 def save(self, update_timestamps=True, **kwargs):
517 if update_timestamps:
519 self.date_joined = datetime.now()
520 self.updated = datetime.now()
522 # update date_signed_terms if necessary
523 if self.__has_signed_terms != self.has_signed_terms:
524 self.date_signed_terms = datetime.now()
528 if self.username != self.email.lower():
530 self.username = self.email.lower()
532 self.validate_unique_email_isactive()
534 super(AstakosUser, self).save(**kwargs)
536 def renew_token(self, flush_sessions=False, current_key=None):
538 md5.update(settings.SECRET_KEY)
539 md5.update(self.username)
540 md5.update(self.realname.encode('ascii', 'ignore'))
541 md5.update(asctime())
543 self.auth_token = b64encode(md5.digest())
544 self.auth_token_created = datetime.now()
545 self.auth_token_expires = self.auth_token_created + \
546 timedelta(hours=AUTH_TOKEN_DURATION)
548 self.flush_sessions(current_key)
549 msg = 'Token renewed for %s' % self.email
550 logger.log(LOGGING_LEVEL, msg)
552 def flush_sessions(self, current_key=None):
555 q = q.exclude(session_key=current_key)
557 keys = q.values_list('session_key', flat=True)
559 msg = 'Flushing sessions: %s' % ','.join(keys)
560 logger.log(LOGGING_LEVEL, msg, [])
561 engine = import_module(settings.SESSION_ENGINE)
563 s = engine.SessionStore(k)
566 def __unicode__(self):
567 return '%s (%s)' % (self.realname, self.email)
569 def conflicting_email(self):
570 q = AstakosUser.objects.exclude(username=self.username)
571 q = q.filter(email__iexact=self.email)
576 def validate_unique_email_isactive(self):
578 Implements a unique_together constraint for email and is_active fields.
580 q = AstakosUser.objects.all()
581 q = q.filter(email = self.email)
583 q = q.filter(~Q(id = self.id))
585 raise ValidationError({'__all__': [_(astakos_messages.UNIQUE_EMAIL_IS_ACTIVE_CONSTRAIN_ERR)]})
587 def email_change_is_pending(self):
588 return self.emailchanges.count() > 0
591 def signed_terms(self):
592 term = get_latest_terms()
595 if not self.has_signed_terms:
597 if not self.date_signed_terms:
599 if self.date_signed_terms < term.date:
600 self.has_signed_terms = False
601 self.date_signed_terms = None
606 def set_invitations_level(self):
608 Update user invitation level
610 level = self.invitation.inviter.level + 1
612 self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
614 def can_login_with_auth_provider(self, provider):
615 if not self.has_auth_provider(provider):
618 return auth_providers.get_provider(provider).is_available_for_login()
620 def can_add_auth_provider(self, provider, **kwargs):
621 provider_settings = auth_providers.get_provider(provider)
622 if not provider_settings.is_available_for_login():
625 if self.has_auth_provider(provider) and \
626 provider_settings.one_per_user:
629 if 'provider_info' in kwargs:
630 kwargs.pop('provider_info')
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):
658 if 'provider_info' in kwargs:
659 info_data = kwargs.pop('provider_info')
660 if isinstance(info_data, dict):
661 info_data = json.dumps(info_data)
663 if self.can_add_auth_provider(provider, **kwargs):
664 self.auth_providers.create(module=provider, active=True,
668 raise Exception('Cannot add provider')
670 def add_pending_auth_provider(self, pending):
672 Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
675 if not isinstance(pending, PendingThirdPartyUser):
676 pending = PendingThirdPartyUser.objects.get(token=pending)
678 provider = self.add_auth_provider(pending.provider,
679 identifier=pending.third_party_identifier,
680 affiliation=pending.affiliation,
681 provider_info=pending.info)
683 if email_re.match(pending.email or '') and pending.email != self.email:
684 self.additionalmail_set.get_or_create(email=pending.email)
689 def remove_auth_provider(self, provider, **kwargs):
690 self.auth_providers.get(module=provider, **kwargs).delete()
693 def get_resend_activation_url(self):
694 return reverse('send_activation', kwargs={'user_id': self.pk})
696 def get_provider_remove_url(self, module, **kwargs):
697 return reverse('remove_auth_provider', kwargs={
698 'pk': self.auth_providers.get(module=module, **kwargs).pk})
700 def get_activation_url(self, nxt=False):
701 url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
702 quote(self.auth_token))
704 url += "&next=%s" % quote(nxt)
707 def get_password_reset_url(self, token_generator=default_token_generator):
708 return reverse('django.contrib.auth.views.password_reset_confirm',
709 kwargs={'uidb36':int_to_base36(self.id),
710 'token':token_generator.make_token(self)})
712 def get_auth_providers(self):
713 return self.auth_providers.all()
715 def get_available_auth_providers(self):
717 Returns a list of providers available for user to connect to.
720 for module, provider_settings in auth_providers.PROVIDERS.iteritems():
721 if self.can_add_auth_provider(module):
722 providers.append(provider_settings(self))
726 def get_active_auth_providers(self):
728 for provider in self.auth_providers.active():
729 if auth_providers.get_provider(provider.module).is_available_for_login():
730 providers.append(provider)
734 def auth_providers_display(self):
735 return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
737 def get_inactive_message(self):
740 if self.activation_sent:
741 if self.email_verified:
742 message = _(astakos_messages.ACCOUNT_INACTIVE)
744 message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
745 if astakos_settings.MODERATION_ENABLED:
746 msg_extra = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
748 url = self.get_resend_activation_url()
749 msg_extra = mark_safe(_(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP) + \
751 _('<a href="%s">%s?</a>') % (url,
752 _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
754 if astakos_settings.MODERATION_ENABLED:
755 message = _(astakos_messages.ACCOUNT_PENDING_MODERATION)
757 message = astakos_messages.ACCOUNT_PENDING_ACTIVATION
758 url = self.get_resend_activation_url()
759 msg_extra = mark_safe(_('<a href="%s">%s?</a>') % (url,
760 _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
762 return mark_safe(message + u' '+ msg_extra)
765 class AstakosUserAuthProviderManager(models.Manager):
768 return self.filter(active=True)
771 class AstakosUserAuthProvider(models.Model):
773 Available user authentication methods.
775 affiliation = models.CharField('Affiliation', max_length=255, blank=True,
776 null=True, default=None)
777 user = models.ForeignKey(AstakosUser, related_name='auth_providers')
778 module = models.CharField('Provider', max_length=255, blank=False,
780 identifier = models.CharField('Third-party identifier',
781 max_length=255, null=True,
783 active = models.BooleanField(default=True)
784 auth_backend = models.CharField('Backend', max_length=255, blank=False,
786 info_data = models.TextField(default="", null=True, blank=True)
787 created = models.DateTimeField('Creation date', auto_now_add=True)
789 objects = AstakosUserAuthProviderManager()
792 unique_together = (('identifier', 'module', 'user'), )
793 ordering = ('module', 'created')
795 def __init__(self, *args, **kwargs):
796 super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
798 self.info = json.loads(self.info_data)
804 for key,value in self.info.iteritems():
805 setattr(self, 'info_%s' % key, value)
810 return auth_providers.get_provider(self.module)
813 def details_display(self):
815 return self.settings.get_details_tpl_display % self.__dict__
820 def title_display(self):
821 title_tpl = self.settings.get_title_display
823 if self.settings.get_user_title_display:
824 title_tpl = self.settings.get_user_title_display
828 return title_tpl % self.__dict__
830 return self.settings.get_title_display % self.__dict__
832 def can_remove(self):
833 return self.user.can_remove_auth_provider(self.module)
835 def delete(self, *args, **kwargs):
836 ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
837 if self.module == 'local':
838 self.user.set_unusable_password()
843 return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
845 def __unicode__(self):
847 return "%s:%s" % (self.module, self.identifier)
848 if self.auth_backend:
849 return "%s:%s" % (self.module, self.auth_backend)
852 def save(self, *args, **kwargs):
853 self.info_data = json.dumps(self.info)
854 return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
857 class Membership(models.Model):
858 person = models.ForeignKey(AstakosUser)
859 group = models.ForeignKey(AstakosGroup)
860 date_requested = models.DateField(default=datetime.now(), blank=True)
861 date_joined = models.DateField(null=True, db_index=True, blank=True)
864 unique_together = ("person", "group")
866 def save(self, *args, **kwargs):
868 if not self.group.moderation_enabled:
869 self.date_joined = datetime.now()
870 super(Membership, self).save(*args, **kwargs)
873 def is_approved(self):
881 if self.group.max_participants:
882 assert len(self.group.approved_members) + 1 <= self.group.max_participants, \
883 'Maximum participant number has been reached.'
884 self.date_joined = datetime.now()
886 quota_disturbed.send(sender=self, users=(self.person,))
888 def disapprove(self):
890 quota_disturbed.send(sender=self, users=(self.person,))
892 class AstakosQuotaManager(models.Manager):
893 def _update_or_create(self, **kwargs):
895 'update_or_create() must be passed at least one keyword argument'
896 obj, created = self.get_or_create(**kwargs)
897 defaults = kwargs.pop('defaults', {})
899 return obj, True, False
903 [(k, v) for k, v in kwargs.items() if '__' not in k])
904 params.update(defaults)
905 for attr, val in params.items():
906 if hasattr(obj, attr):
907 setattr(obj, attr, val)
908 sid = transaction.savepoint()
909 obj.save(force_update=True)
910 transaction.savepoint_commit(sid)
911 return obj, False, True
912 except IntegrityError, e:
913 transaction.savepoint_rollback(sid)
915 return self.get(**kwargs), False, False
916 except self.model.DoesNotExist:
919 update_or_create = _update_or_create
921 class AstakosGroupQuota(models.Model):
922 objects = AstakosQuotaManager()
923 limit = models.PositiveIntegerField('Limit', null=True) # obsolete field
924 uplimit = models.BigIntegerField('Up limit', null=True)
925 resource = models.ForeignKey(Resource)
926 group = models.ForeignKey(AstakosGroup, blank=True)
929 unique_together = ("resource", "group")
931 class AstakosUserQuota(models.Model):
932 objects = AstakosQuotaManager()
933 limit = models.PositiveIntegerField('Limit', null=True) # obsolete field
934 uplimit = models.BigIntegerField('Up limit', null=True)
935 resource = models.ForeignKey(Resource)
936 user = models.ForeignKey(AstakosUser)
939 unique_together = ("resource", "user")
942 class ApprovalTerms(models.Model):
944 Model for approval terms
947 date = models.DateTimeField(
948 'Issue date', db_index=True, default=datetime.now())
949 location = models.CharField('Terms location', max_length=255)
952 class Invitation(models.Model):
954 Model for registring invitations
956 inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
958 realname = models.CharField('Real name', max_length=255)
959 username = models.CharField('Unique ID', max_length=255, unique=True)
960 code = models.BigIntegerField('Invitation code', db_index=True)
961 is_consumed = models.BooleanField('Consumed?', default=False)
962 created = models.DateTimeField('Creation date', auto_now_add=True)
963 consumed = models.DateTimeField('Consumption date', null=True, blank=True)
965 def __init__(self, *args, **kwargs):
966 super(Invitation, self).__init__(*args, **kwargs)
968 self.code = _generate_invitation_code()
971 self.is_consumed = True
972 self.consumed = datetime.now()
975 def __unicode__(self):
976 return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
979 class EmailChangeManager(models.Manager):
981 @transaction.commit_on_success
982 def change_email(self, activation_key):
984 Validate an activation key and change the corresponding
987 If the key is valid and has not expired, return the ``User``
990 If the key is not valid or has expired, return ``None``.
992 If the key is valid but the ``User`` is already active,
995 After successful email change the activation record is deleted.
997 Throws ValueError if there is already
1000 email_change = self.model.objects.get(
1001 activation_key=activation_key)
1002 if email_change.activation_key_expired():
1003 email_change.delete()
1004 raise EmailChange.DoesNotExist
1005 # is there an active user with this address?
1007 AstakosUser.objects.get(email__iexact=email_change.new_email_address)
1008 except AstakosUser.DoesNotExist:
1011 raise ValueError(_(astakos_messages.NEW_EMAIL_ADDR_RESERVED))
1013 user = AstakosUser.objects.get(pk=email_change.user_id)
1014 old_email = user.email
1015 user.email = email_change.new_email_address
1017 email_change.delete()
1018 msg = "User %d changed email from %s to %s" % (user.pk, old_email,
1020 logger.log(LOGGING_LEVEL, msg)
1022 except EmailChange.DoesNotExist:
1023 raise ValueError(_(astakos_messages.INVALID_ACTIVATION_KEY))
1026 class EmailChange(models.Model):
1027 new_email_address = models.EmailField(_(u'new e-mail address'),
1028 help_text=_(astakos_messages.EMAIL_CHANGE_NEW_ADDR_HELP))
1029 user = models.ForeignKey(
1030 AstakosUser, unique=True, related_name='emailchanges')
1031 requested_at = models.DateTimeField(default=datetime.now())
1032 activation_key = models.CharField(
1033 max_length=40, unique=True, db_index=True)
1035 objects = EmailChangeManager()
1038 return reverse('email_change_confirm',
1039 kwargs={'activation_key': self.activation_key})
1041 def activation_key_expired(self):
1042 expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1043 return self.requested_at + expiration_date < datetime.now()
1046 class AdditionalMail(models.Model):
1048 Model for registring invitations
1050 owner = models.ForeignKey(AstakosUser)
1051 email = models.EmailField()
1054 def _generate_invitation_code():
1056 code = randint(1, 2L ** 63 - 1)
1058 Invitation.objects.get(code=code)
1059 # An invitation with this code already exists, try again
1060 except Invitation.DoesNotExist:
1064 def get_latest_terms():
1066 term = ApprovalTerms.objects.order_by('-id')[0]
1072 class PendingThirdPartyUser(models.Model):
1074 Model for registring successful third party user authentications
1076 third_party_identifier = models.CharField('Third-party identifier', max_length=255, null=True, blank=True)
1077 provider = models.CharField('Provider', max_length=255, blank=True)
1078 email = models.EmailField(_('e-mail address'), blank=True, null=True)
1079 first_name = models.CharField(_('first name'), max_length=30, blank=True)
1080 last_name = models.CharField(_('last name'), max_length=30, blank=True)
1081 affiliation = models.CharField('Affiliation', max_length=255, blank=True)
1082 username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1083 token = models.CharField('Token', max_length=255, null=True, blank=True)
1084 created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1085 info = models.TextField(default="", null=True, blank=True)
1088 unique_together = ("provider", "third_party_identifier")
1090 def get_user_instance(self):
1092 d.pop('_state', None)
1094 d.pop('token', None)
1095 d.pop('created', None)
1097 user = AstakosUser(**d)
1103 return '%s %s' %(self.first_name, self.last_name)
1106 def realname(self, value):
1107 parts = value.split(' ')
1109 self.first_name = parts[0]
1110 self.last_name = parts[1]
1112 self.last_name = parts[0]
1114 def save(self, **kwargs):
1117 while not self.username:
1118 username = uuid.uuid4().hex[:30]
1120 AstakosUser.objects.get(username = username)
1121 except AstakosUser.DoesNotExist, e:
1122 self.username = username
1123 super(PendingThirdPartyUser, self).save(**kwargs)
1125 def generate_token(self):
1126 self.password = self.third_party_identifier
1127 self.last_login = datetime.now()
1128 self.token = default_token_generator.make_token(self)
1130 class SessionCatalog(models.Model):
1131 session_key = models.CharField(_('session key'), max_length=40)
1132 user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1135 def create_astakos_user(u):
1137 AstakosUser.objects.get(user_ptr=u.pk)
1138 except AstakosUser.DoesNotExist:
1139 extended_user = AstakosUser(user_ptr_id=u.pk)
1140 extended_user.__dict__.update(u.__dict__)
1141 extended_user.save()
1142 if not extended_user.has_auth_provider('local'):
1143 extended_user.add_auth_provider('local')
1144 except BaseException, e:
1148 def fix_superusers(sender, **kwargs):
1149 # Associate superusers with AstakosUser
1150 admins = User.objects.filter(is_superuser=True)
1152 create_astakos_user(u)
1155 def user_post_save(sender, instance, created, **kwargs):
1158 create_astakos_user(instance)
1161 def set_default_group(user):
1163 default = AstakosGroup.objects.get(name='default')
1165 group=default, person=user, date_joined=datetime.now()).save()
1166 except AstakosGroup.DoesNotExist, e:
1170 def astakosuser_pre_save(sender, instance, **kwargs):
1171 instance.aquarium_report = False
1172 instance.new = False
1174 db_instance = AstakosUser.objects.get(id=instance.id)
1175 except AstakosUser.DoesNotExist:
1177 instance.aquarium_report = True
1180 get = AstakosUser.__getattribute__
1181 l = filter(lambda f: get(db_instance, f) != get(instance, f),
1183 instance.aquarium_report = True if l else False
1186 def astakosuser_post_save(sender, instance, created, **kwargs):
1187 if instance.aquarium_report:
1188 report_user_event(instance, create=instance.new)
1191 set_default_group(instance)
1192 # TODO handle socket.error & IOError
1193 register_users((instance,))
1196 def resource_post_save(sender, instance, created, **kwargs):
1199 register_resources((instance,))
1202 def send_quota_disturbed(sender, instance, **kwargs):
1204 extend = users.extend
1205 if sender == Membership:
1206 if not instance.group.is_enabled:
1208 extend([instance.person])
1209 elif sender == AstakosUserQuota:
1210 extend([instance.user])
1211 elif sender == AstakosGroupQuota:
1212 if not instance.group.is_enabled:
1214 extend(instance.group.astakosuser_set.all())
1215 elif sender == AstakosGroup:
1216 if not instance.is_enabled:
1218 quota_disturbed.send(sender=sender, users=users)
1221 def on_quota_disturbed(sender, users, **kwargs):
1222 # print '>>>', locals()
1227 def renew_token(sender, instance, **kwargs):
1228 if not instance.auth_token:
1229 instance.renew_token()
1231 post_syncdb.connect(fix_superusers)
1232 post_save.connect(user_post_save, sender=User)
1233 pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
1234 post_save.connect(astakosuser_post_save, sender=AstakosUser)
1235 post_save.connect(resource_post_save, sender=Resource)
1237 quota_disturbed = Signal(providing_args=["users"])
1238 quota_disturbed.connect(on_quota_disturbed)
1240 post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
1241 post_delete.connect(send_quota_disturbed, sender=Membership)
1242 post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
1243 post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
1244 post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1245 post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1247 pre_save.connect(renew_token, sender=AstakosUser)
1248 pre_save.connect(renew_token, sender=Service)