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 random import randint
42 from collections import defaultdict
44 from django.db import models
45 from django.contrib.auth.models import User, UserManager, Group
46 from django.utils.translation import ugettext as _
47 from django.core.exceptions import ValidationError
48 from django.db import transaction
49 from django.db.models.signals import pre_save, post_save, post_syncdb, post_delete
50 from django.dispatch import Signal
51 from django.db.models import Q
53 from astakos.im.settings import (DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
54 AUTH_TOKEN_DURATION, BILLING_FIELDS, EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL
56 from astakos.im.endpoints.quotaholder import register_users, send_quota
57 from astakos.im.endpoints.aquarium.producer import report_user_event
59 from astakos.im.tasks import propagate_groupmembers_quota
61 logger = logging.getLogger(__name__)
64 class Service(models.Model):
65 name = models.CharField('Name', max_length=255, unique=True, db_index=True)
66 url = models.FilePathField()
67 icon = models.FilePathField(blank=True)
68 auth_token = models.CharField('Authentication Token', max_length=32,
69 null=True, blank=True)
70 auth_token_created = models.DateTimeField('Token creation date', null=True)
71 auth_token_expires = models.DateTimeField(
72 'Token expiration date', null=True)
74 def save(self, **kwargs):
78 super(Service, self).save(**kwargs)
80 def renew_token(self):
82 md5.update(self.name.encode('ascii', 'ignore'))
83 md5.update(self.url.encode('ascii', 'ignore'))
86 self.auth_token = b64encode(md5.digest())
87 self.auth_token_created = datetime.now()
88 self.auth_token_expires = self.auth_token_created + \
89 timedelta(hours=AUTH_TOKEN_DURATION)
95 class ResourceMetadata(models.Model):
96 key = models.CharField('Name', max_length=255, unique=True, db_index=True)
97 value = models.CharField('Value', max_length=255)
100 class Resource(models.Model):
101 name = models.CharField('Name', max_length=255, unique=True, db_index=True)
102 meta = models.ManyToManyField(ResourceMetadata)
103 service = models.ForeignKey(Service)
106 return '%s : %s' % (self.service, self.name)
109 class GroupKind(models.Model):
110 name = models.CharField('Name', max_length=255, unique=True, db_index=True)
116 class AstakosGroup(Group):
117 kind = models.ForeignKey(GroupKind)
118 homepage = models.URLField(
119 'Homepage Url', max_length=255, null=True, blank=True)
120 desc = models.TextField('Description', null=True)
121 policy = models.ManyToManyField(Resource, null=True, blank=True,
122 through='AstakosGroupQuota'
124 creation_date = models.DateTimeField('Creation date',
125 default=datetime.now()
127 issue_date = models.DateTimeField('Issue date', null=True)
128 expiration_date = models.DateTimeField('Expiration date', null=True)
129 moderation_enabled = models.BooleanField('Moderated membership?',
132 approval_date = models.DateTimeField('Activation date', null=True,
135 estimated_participants = models.PositiveIntegerField('Estimated #members',
140 def is_disabled(self):
141 if not self.approval_date:
146 def is_enabled(self):
149 if not self.issue_date:
151 if not self.expiration_date:
154 if self.issue_date > now:
156 if now >= self.expiration_date:
163 self.approval_date = datetime.now()
165 quota_disturbed.send(sender=self, users=self.approved_members)
166 propagate_groupmembers_quota.apply_async(
167 args=[self], eta=self.issue_date)
168 propagate_groupmembers_quota.apply_async(
169 args=[self], eta=self.expiration_date)
174 self.approval_date = None
176 quota_disturbed.send(sender=self, users=self.approved_members)
178 def approve_member(self, person):
179 m, created = self.membership_set.get_or_create(person=person)
180 # update date_joined in any case
181 m.date_joined = datetime.now()
184 def disapprove_member(self, person):
185 self.membership_set.remove(person=person)
189 return [m.person for m in self.membership_set.all()]
192 def approved_members(self):
193 return [m.person for m in self.membership_set.all() if m.is_approved]
198 for q in self.astakosgroupquota_set.all():
199 d[q.resource] += q.limit
204 return self.owner.all()
209 map(self.approve_member, l)
212 class AstakosUser(User):
214 Extends ``django.contrib.auth.models.User`` by defining additional fields.
216 # Use UserManager to get the create_user method, etc.
217 objects = UserManager()
219 affiliation = models.CharField('Affiliation', max_length=255, blank=True)
220 provider = models.CharField('Provider', max_length=255, blank=True)
223 user_level = DEFAULT_USER_LEVEL
224 level = models.IntegerField('Inviter level', default=user_level)
225 invitations = models.IntegerField(
226 'Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
228 auth_token = models.CharField('Authentication Token', max_length=32,
229 null=True, blank=True)
230 auth_token_created = models.DateTimeField('Token creation date', null=True)
231 auth_token_expires = models.DateTimeField(
232 'Token expiration date', null=True)
234 updated = models.DateTimeField('Update date')
235 is_verified = models.BooleanField('Is verified?', default=False)
237 # ex. screen_name for twitter, eppn for shibboleth
238 third_party_identifier = models.CharField(
239 'Third-party identifier', max_length=255, null=True, blank=True)
241 email_verified = models.BooleanField('Email verified?', default=False)
243 has_credits = models.BooleanField('Has credits?', default=False)
244 has_signed_terms = models.BooleanField(
245 'Agree with the terms?', default=False)
246 date_signed_terms = models.DateTimeField(
247 'Signed terms date', null=True, blank=True)
249 activation_sent = models.DateTimeField(
250 'Activation sent data', null=True, blank=True)
252 policy = models.ManyToManyField(
253 Resource, null=True, through='AstakosUserQuota')
255 astakos_groups = models.ManyToManyField(
256 AstakosGroup, verbose_name=_('agroups'), blank=True,
257 help_text=_("In addition to the permissions manually assigned, this user will also get all permissions granted to each group he/she is in."),
258 through='Membership')
260 __has_signed_terms = False
262 owner = models.ManyToManyField(
263 AstakosGroup, related_name='owner', null=True)
266 unique_together = ("provider", "third_party_identifier")
268 def __init__(self, *args, **kwargs):
269 super(AstakosUser, self).__init__(*args, **kwargs)
270 self.__has_signed_terms = self.has_signed_terms
272 self.is_active = False
276 return '%s %s' % (self.first_name, self.last_name)
279 def realname(self, value):
280 parts = value.split(' ')
282 self.first_name = parts[0]
283 self.last_name = parts[1]
285 self.last_name = parts[0]
288 def invitation(self):
290 return Invitation.objects.get(username=self.email)
291 except Invitation.DoesNotExist:
297 for q in self.astakosuserquota_set.all():
298 d[q.resource.name] += q.limit
299 for m in self.membership_set.all():
300 if not m.is_approved:
305 for r, limit in g.quota.iteritems():
307 # TODO set default for remaining
310 def save(self, update_timestamps=True, **kwargs):
311 if update_timestamps:
313 self.date_joined = datetime.now()
314 self.updated = datetime.now()
316 # update date_signed_terms if necessary
317 if self.__has_signed_terms != self.has_signed_terms:
318 self.date_signed_terms = datetime.now()
322 while not self.username:
323 username = uuid.uuid4().hex[:30]
325 AstakosUser.objects.get(username=username)
326 except AstakosUser.DoesNotExist:
327 self.username = username
328 if not self.provider:
329 self.provider = 'local'
330 self.validate_unique_email_isactive()
331 if self.is_active and self.activation_sent:
332 # reset the activation sent
333 self.activation_sent = None
335 super(AstakosUser, self).save(**kwargs)
337 def renew_token(self):
339 md5.update(self.username)
340 md5.update(self.realname.encode('ascii', 'ignore'))
341 md5.update(asctime())
343 self.auth_token = b64encode(md5.digest())
344 self.auth_token_created = datetime.now()
345 self.auth_token_expires = self.auth_token_created + \
346 timedelta(hours=AUTH_TOKEN_DURATION)
347 msg = 'Token renewed for %s' % self.email
348 logger.log(LOGGING_LEVEL, msg)
350 def __unicode__(self):
351 return '%s (%s)' % (self.realname, self.email)
353 def conflicting_email(self):
354 q = AstakosUser.objects.exclude(username=self.username)
355 q = q.filter(email=self.email)
360 def validate_unique_email_isactive(self):
362 Implements a unique_together constraint for email and is_active fields.
364 q = AstakosUser.objects.exclude(username=self.username)
365 q = q.filter(email=self.email)
366 q = q.filter(is_active=self.is_active)
368 raise ValidationError({'__all__': [_('Another account with the same email & is_active combination found.')]})
371 def signed_terms(self):
372 term = get_latest_terms()
375 if not self.has_signed_terms:
377 if not self.date_signed_terms:
379 if self.date_signed_terms < term.date:
380 self.has_signed_terms = False
381 self.date_signed_terms = None
387 class Membership(models.Model):
388 person = models.ForeignKey(AstakosUser)
389 group = models.ForeignKey(AstakosGroup)
390 date_requested = models.DateField(default=datetime.now(), blank=True)
391 date_joined = models.DateField(null=True, db_index=True, blank=True)
394 unique_together = ("person", "group")
396 def save(self, *args, **kwargs):
398 if not self.group.moderation_enabled:
399 self.date_joined = datetime.now()
400 super(Membership, self).save(*args, **kwargs)
403 def is_approved(self):
409 self.date_joined = datetime.now()
411 quota_disturbed.send(sender=self, users=(self.person,))
413 def disapprove(self):
415 quota_disturbed.send(sender=self, users=(self.person,))
418 class AstakosGroupQuota(models.Model):
419 limit = models.PositiveIntegerField('Limit')
420 resource = models.ForeignKey(Resource)
421 group = models.ForeignKey(AstakosGroup, blank=True)
424 unique_together = ("resource", "group")
427 class AstakosUserQuota(models.Model):
428 limit = models.PositiveIntegerField('Limit')
429 resource = models.ForeignKey(Resource)
430 user = models.ForeignKey(AstakosUser)
433 unique_together = ("resource", "user")
436 class ApprovalTerms(models.Model):
438 Model for approval terms
441 date = models.DateTimeField(
442 'Issue date', db_index=True, default=datetime.now())
443 location = models.CharField('Terms location', max_length=255)
446 class Invitation(models.Model):
448 Model for registring invitations
450 inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
452 realname = models.CharField('Real name', max_length=255)
453 username = models.CharField('Unique ID', max_length=255, unique=True)
454 code = models.BigIntegerField('Invitation code', db_index=True)
455 is_consumed = models.BooleanField('Consumed?', default=False)
456 created = models.DateTimeField('Creation date', auto_now_add=True)
457 consumed = models.DateTimeField('Consumption date', null=True, blank=True)
459 def __init__(self, *args, **kwargs):
460 super(Invitation, self).__init__(*args, **kwargs)
462 self.code = _generate_invitation_code()
465 self.is_consumed = True
466 self.consumed = datetime.now()
469 def __unicode__(self):
470 return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
473 class EmailChangeManager(models.Manager):
474 @transaction.commit_on_success
475 def change_email(self, activation_key):
477 Validate an activation key and change the corresponding
480 If the key is valid and has not expired, return the ``User``
483 If the key is not valid or has expired, return ``None``.
485 If the key is valid but the ``User`` is already active,
488 After successful email change the activation record is deleted.
490 Throws ValueError if there is already
493 email_change = self.model.objects.get(
494 activation_key=activation_key)
495 if email_change.activation_key_expired():
496 email_change.delete()
497 raise EmailChange.DoesNotExist
498 # is there an active user with this address?
500 AstakosUser.objects.get(email=email_change.new_email_address)
501 except AstakosUser.DoesNotExist:
504 raise ValueError(_('The new email address is reserved.'))
506 user = AstakosUser.objects.get(pk=email_change.user_id)
507 user.email = email_change.new_email_address
509 email_change.delete()
511 except EmailChange.DoesNotExist:
512 raise ValueError(_('Invalid activation key'))
515 class EmailChange(models.Model):
516 new_email_address = models.EmailField(_(u'new e-mail address'), help_text=_(u'Your old email address will be used until you verify your new one.'))
517 user = models.ForeignKey(
518 AstakosUser, unique=True, related_name='emailchange_user')
519 requested_at = models.DateTimeField(default=datetime.now())
520 activation_key = models.CharField(
521 max_length=40, unique=True, db_index=True)
523 objects = EmailChangeManager()
525 def activation_key_expired(self):
526 expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
527 return self.requested_at + expiration_date < datetime.now()
530 class AdditionalMail(models.Model):
532 Model for registring invitations
534 owner = models.ForeignKey(AstakosUser)
535 email = models.EmailField()
538 def _generate_invitation_code():
540 code = randint(1, 2L ** 63 - 1)
542 Invitation.objects.get(code=code)
543 # An invitation with this code already exists, try again
544 except Invitation.DoesNotExist:
548 def get_latest_terms():
550 term = ApprovalTerms.objects.order_by('-id')[0]
557 def create_astakos_user(u):
559 AstakosUser.objects.get(user_ptr=u.pk)
560 except AstakosUser.DoesNotExist:
561 extended_user = AstakosUser(user_ptr_id=u.pk)
562 extended_user.__dict__.update(u.__dict__)
563 extended_user.renew_token()
565 except BaseException, e:
570 def fix_superusers(sender, **kwargs):
571 # Associate superusers with AstakosUser
572 admins = User.objects.filter(is_superuser=True)
574 create_astakos_user(u)
577 def user_post_save(sender, instance, created, **kwargs):
580 create_astakos_user(instance)
583 def set_default_group(user):
585 default = AstakosGroup.objects.get(name='default')
587 group=default, person=user, date_joined=datetime.now()).save()
588 except AstakosGroup.DoesNotExist, e:
592 def astakosuser_pre_save(sender, instance, **kwargs):
593 instance.aquarium_report = False
596 db_instance = AstakosUser.objects.get(id=instance.id)
597 except AstakosUser.DoesNotExist:
599 instance.aquarium_report = True
602 get = AstakosUser.__getattribute__
603 l = filter(lambda f: get(db_instance, f) != get(instance, f),
606 instance.aquarium_report = True if l else False
609 def astakosuser_post_save(sender, instance, created, **kwargs):
610 if instance.aquarium_report:
611 report_user_event(instance, create=instance.new)
614 set_default_group(instance)
615 # TODO handle socket.error & IOError
616 register_users((instance,))
619 def send_quota_disturbed(sender, instance, **kwargs):
621 extend = users.extend
622 if sender == Membership:
623 if not instance.group.is_enabled:
625 extend([instance.person])
626 elif sender == AstakosUserQuota:
627 extend([instance.user])
628 elif sender == AstakosGroupQuota:
629 if not instance.group.is_enabled:
631 extend(instance.group.astakosuser_set.all())
632 elif sender == AstakosGroup:
633 if not instance.is_enabled:
635 quota_disturbed.send(sender=sender, users=users)
638 def on_quota_disturbed(sender, users, **kwargs):
639 print '>>>', locals()
644 post_syncdb.connect(fix_superusers)
645 post_save.connect(user_post_save, sender=User)
646 pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
647 post_save.connect(astakosuser_post_save, sender=AstakosUser)
649 quota_disturbed = Signal(providing_args=["users"])
650 quota_disturbed.connect(on_quota_disturbed)
652 post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
653 post_delete.connect(send_quota_disturbed, sender=Membership)
654 post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
655 post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
656 post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
657 post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)