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,
55 EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL)
56 from astakos.im.endpoints.quotaholder import (register_users, send_quota,
58 from astakos.im.endpoints.aquarium.producer import report_user_event
60 from astakos.im.tasks import propagate_groupmembers_quota
62 logger = logging.getLogger(__name__)
65 class Service(models.Model):
66 name = models.CharField('Name', max_length=255, unique=True, db_index=True)
67 url = models.FilePathField()
68 icon = models.FilePathField(blank=True)
69 auth_token = models.CharField('Authentication Token', max_length=32,
70 null=True, blank=True)
71 auth_token_created = models.DateTimeField('Token creation date', null=True)
72 auth_token_expires = models.DateTimeField(
73 'Token expiration date', null=True)
75 def save(self, **kwargs):
79 super(Service, self).save(**kwargs)
81 def renew_token(self):
83 md5.update(self.name.encode('ascii', 'ignore'))
84 md5.update(self.url.encode('ascii', 'ignore'))
87 self.auth_token = b64encode(md5.digest())
88 self.auth_token_created = datetime.now()
89 self.auth_token_expires = self.auth_token_created + \
90 timedelta(hours=AUTH_TOKEN_DURATION)
96 class ResourceMetadata(models.Model):
97 key = models.CharField('Name', max_length=255, unique=True, db_index=True)
98 value = models.CharField('Value', max_length=255)
101 class Resource(models.Model):
102 name = models.CharField('Name', max_length=255, unique=True, db_index=True)
103 meta = models.ManyToManyField(ResourceMetadata)
104 service = models.ForeignKey(Service)
107 return '%s : %s' % (self.service, self.name)
110 class GroupKind(models.Model):
111 name = models.CharField('Name', max_length=255, unique=True, db_index=True)
117 class AstakosGroup(Group):
118 kind = models.ForeignKey(GroupKind)
119 homepage = models.URLField(
120 'Homepage Url', max_length=255, null=True, blank=True)
121 desc = models.TextField('Description', null=True)
122 policy = models.ManyToManyField(Resource, null=True, blank=True,
123 through='AstakosGroupQuota'
125 creation_date = models.DateTimeField('Creation date',
126 default=datetime.now()
128 issue_date = models.DateTimeField('Issue date', null=True)
129 expiration_date = models.DateTimeField('Expiration date', null=True)
130 moderation_enabled = models.BooleanField('Moderated membership?',
133 approval_date = models.DateTimeField('Activation date', null=True,
136 estimated_participants = models.PositiveIntegerField('Estimated #members',
141 def is_disabled(self):
142 if not self.approval_date:
147 def is_enabled(self):
150 if not self.issue_date:
152 if not self.expiration_date:
155 if self.issue_date > now:
157 if now >= self.expiration_date:
164 self.approval_date = datetime.now()
166 quota_disturbed.send(sender=self, users=self.approved_members)
167 propagate_groupmembers_quota.apply_async(
168 args=[self], eta=self.issue_date)
169 propagate_groupmembers_quota.apply_async(
170 args=[self], eta=self.expiration_date)
175 self.approval_date = None
177 quota_disturbed.send(sender=self, users=self.approved_members)
179 def approve_member(self, person):
180 m, created = self.membership_set.get_or_create(person=person)
181 # update date_joined in any case
182 m.date_joined = datetime.now()
185 def disapprove_member(self, person):
186 self.membership_set.remove(person=person)
190 return [m.person for m in self.membership_set.all()]
193 def approved_members(self):
194 return [m.person for m in self.membership_set.all() if m.is_approved]
199 for q in self.astakosgroupquota_set.all():
200 d[q.resource] += q.uplimit
205 return self.owner.all()
210 map(self.approve_member, l)
213 class AstakosUser(User):
215 Extends ``django.contrib.auth.models.User`` by defining additional fields.
217 # Use UserManager to get the create_user method, etc.
218 objects = UserManager()
220 affiliation = models.CharField('Affiliation', max_length=255, blank=True)
221 provider = models.CharField('Provider', max_length=255, blank=True)
224 user_level = DEFAULT_USER_LEVEL
225 level = models.IntegerField('Inviter level', default=user_level)
226 invitations = models.IntegerField(
227 'Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
229 auth_token = models.CharField('Authentication Token', max_length=32,
230 null=True, blank=True)
231 auth_token_created = models.DateTimeField('Token creation date', null=True)
232 auth_token_expires = models.DateTimeField(
233 'Token expiration date', null=True)
235 updated = models.DateTimeField('Update date')
236 is_verified = models.BooleanField('Is verified?', default=False)
238 # ex. screen_name for twitter, eppn for shibboleth
239 third_party_identifier = models.CharField(
240 'Third-party identifier', max_length=255, null=True, blank=True)
242 email_verified = models.BooleanField('Email verified?', default=False)
244 has_credits = models.BooleanField('Has credits?', default=False)
245 has_signed_terms = models.BooleanField(
246 'Agree with the terms?', default=False)
247 date_signed_terms = models.DateTimeField(
248 'Signed terms date', null=True, blank=True)
250 activation_sent = models.DateTimeField(
251 'Activation sent data', null=True, blank=True)
253 policy = models.ManyToManyField(
254 Resource, null=True, through='AstakosUserQuota')
256 astakos_groups = models.ManyToManyField(
257 AstakosGroup, verbose_name=_('agroups'), blank=True,
258 help_text=_("In addition to the permissions manually assigned, this user will also get all permissions granted to each group he/she is in."),
259 through='Membership')
261 __has_signed_terms = False
263 owner = models.ManyToManyField(
264 AstakosGroup, related_name='owner', null=True)
267 unique_together = ("provider", "third_party_identifier")
269 def __init__(self, *args, **kwargs):
270 super(AstakosUser, self).__init__(*args, **kwargs)
271 self.__has_signed_terms = self.has_signed_terms
273 self.is_active = False
277 return '%s %s' % (self.first_name, self.last_name)
280 def realname(self, value):
281 parts = value.split(' ')
283 self.first_name = parts[0]
284 self.last_name = parts[1]
286 self.last_name = parts[0]
289 def invitation(self):
291 return Invitation.objects.get(username=self.email)
292 except Invitation.DoesNotExist:
298 for q in self.astakosuserquota_set.all():
299 d[q.resource.name] += q.uplimit
300 for m in self.membership_set.all():
301 if not m.is_approved:
306 for r, uplimit in g.quota.iteritems():
308 # TODO set default for remaining
311 def save(self, update_timestamps=True, **kwargs):
312 if update_timestamps:
314 self.date_joined = datetime.now()
315 self.updated = datetime.now()
317 # update date_signed_terms if necessary
318 if self.__has_signed_terms != self.has_signed_terms:
319 self.date_signed_terms = datetime.now()
323 while not self.username:
324 username = uuid.uuid4().hex[:30]
326 AstakosUser.objects.get(username=username)
327 except AstakosUser.DoesNotExist:
328 self.username = username
329 if not self.provider:
330 self.provider = 'local'
331 self.validate_unique_email_isactive()
332 if self.is_active and self.activation_sent:
333 # reset the activation sent
334 self.activation_sent = None
336 super(AstakosUser, self).save(**kwargs)
338 def renew_token(self):
340 md5.update(self.username)
341 md5.update(self.realname.encode('ascii', 'ignore'))
342 md5.update(asctime())
344 self.auth_token = b64encode(md5.digest())
345 self.auth_token_created = datetime.now()
346 self.auth_token_expires = self.auth_token_created + \
347 timedelta(hours=AUTH_TOKEN_DURATION)
348 msg = 'Token renewed for %s' % self.email
349 logger.log(LOGGING_LEVEL, msg)
351 def __unicode__(self):
352 return '%s (%s)' % (self.realname, self.email)
354 def conflicting_email(self):
355 q = AstakosUser.objects.exclude(username=self.username)
356 q = q.filter(email=self.email)
361 def validate_unique_email_isactive(self):
363 Implements a unique_together constraint for email and is_active fields.
365 q = AstakosUser.objects.exclude(username=self.username)
366 q = q.filter(email=self.email)
367 q = q.filter(is_active=self.is_active)
369 raise ValidationError({'__all__': [_('Another account with the same email & is_active combination found.')]})
372 def signed_terms(self):
373 term = get_latest_terms()
376 if not self.has_signed_terms:
378 if not self.date_signed_terms:
380 if self.date_signed_terms < term.date:
381 self.has_signed_terms = False
382 self.date_signed_terms = None
388 class Membership(models.Model):
389 person = models.ForeignKey(AstakosUser)
390 group = models.ForeignKey(AstakosGroup)
391 date_requested = models.DateField(default=datetime.now(), blank=True)
392 date_joined = models.DateField(null=True, db_index=True, blank=True)
395 unique_together = ("person", "group")
397 def save(self, *args, **kwargs):
399 if not self.group.moderation_enabled:
400 self.date_joined = datetime.now()
401 super(Membership, self).save(*args, **kwargs)
404 def is_approved(self):
410 self.date_joined = datetime.now()
412 quota_disturbed.send(sender=self, users=(self.person,))
414 def disapprove(self):
416 quota_disturbed.send(sender=self, users=(self.person,))
419 class AstakosGroupQuota(models.Model):
420 limit = models.PositiveIntegerField('Limit') # obsolete field
421 uplimit = models.BigIntegerField('Up limit', null=True)
422 resource = models.ForeignKey(Resource)
423 group = models.ForeignKey(AstakosGroup, blank=True)
426 unique_together = ("resource", "group")
429 class AstakosUserQuota(models.Model):
430 limit = models.PositiveIntegerField('Limit') # obsolete field
431 uplimit = models.BigIntegerField('Up limit', null=True)
432 resource = models.ForeignKey(Resource)
433 user = models.ForeignKey(AstakosUser)
436 unique_together = ("resource", "user")
439 class ApprovalTerms(models.Model):
441 Model for approval terms
444 date = models.DateTimeField(
445 'Issue date', db_index=True, default=datetime.now())
446 location = models.CharField('Terms location', max_length=255)
449 class Invitation(models.Model):
451 Model for registring invitations
453 inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
455 realname = models.CharField('Real name', max_length=255)
456 username = models.CharField('Unique ID', max_length=255, unique=True)
457 code = models.BigIntegerField('Invitation code', db_index=True)
458 is_consumed = models.BooleanField('Consumed?', default=False)
459 created = models.DateTimeField('Creation date', auto_now_add=True)
460 consumed = models.DateTimeField('Consumption date', null=True, blank=True)
462 def __init__(self, *args, **kwargs):
463 super(Invitation, self).__init__(*args, **kwargs)
465 self.code = _generate_invitation_code()
468 self.is_consumed = True
469 self.consumed = datetime.now()
472 def __unicode__(self):
473 return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
476 class EmailChangeManager(models.Manager):
477 @transaction.commit_on_success
478 def change_email(self, activation_key):
480 Validate an activation key and change the corresponding
483 If the key is valid and has not expired, return the ``User``
486 If the key is not valid or has expired, return ``None``.
488 If the key is valid but the ``User`` is already active,
491 After successful email change the activation record is deleted.
493 Throws ValueError if there is already
496 email_change = self.model.objects.get(
497 activation_key=activation_key)
498 if email_change.activation_key_expired():
499 email_change.delete()
500 raise EmailChange.DoesNotExist
501 # is there an active user with this address?
503 AstakosUser.objects.get(email=email_change.new_email_address)
504 except AstakosUser.DoesNotExist:
507 raise ValueError(_('The new email address is reserved.'))
509 user = AstakosUser.objects.get(pk=email_change.user_id)
510 user.email = email_change.new_email_address
512 email_change.delete()
514 except EmailChange.DoesNotExist:
515 raise ValueError(_('Invalid activation key'))
518 class EmailChange(models.Model):
519 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.'))
520 user = models.ForeignKey(
521 AstakosUser, unique=True, related_name='emailchange_user')
522 requested_at = models.DateTimeField(default=datetime.now())
523 activation_key = models.CharField(
524 max_length=40, unique=True, db_index=True)
526 objects = EmailChangeManager()
528 def activation_key_expired(self):
529 expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
530 return self.requested_at + expiration_date < datetime.now()
533 class AdditionalMail(models.Model):
535 Model for registring invitations
537 owner = models.ForeignKey(AstakosUser)
538 email = models.EmailField()
541 def _generate_invitation_code():
543 code = randint(1, 2L ** 63 - 1)
545 Invitation.objects.get(code=code)
546 # An invitation with this code already exists, try again
547 except Invitation.DoesNotExist:
551 def get_latest_terms():
553 term = ApprovalTerms.objects.order_by('-id')[0]
560 def create_astakos_user(u):
562 AstakosUser.objects.get(user_ptr=u.pk)
563 except AstakosUser.DoesNotExist:
564 extended_user = AstakosUser(user_ptr_id=u.pk)
565 extended_user.__dict__.update(u.__dict__)
566 extended_user.renew_token()
568 except BaseException, e:
573 def fix_superusers(sender, **kwargs):
574 # Associate superusers with AstakosUser
575 admins = User.objects.filter(is_superuser=True)
577 create_astakos_user(u)
580 def user_post_save(sender, instance, created, **kwargs):
583 create_astakos_user(instance)
586 def set_default_group(user):
588 default = AstakosGroup.objects.get(name='default')
590 group=default, person=user, date_joined=datetime.now()).save()
591 except AstakosGroup.DoesNotExist, e:
595 def astakosuser_pre_save(sender, instance, **kwargs):
596 instance.aquarium_report = False
599 db_instance = AstakosUser.objects.get(id=instance.id)
600 except AstakosUser.DoesNotExist:
602 instance.aquarium_report = True
605 get = AstakosUser.__getattribute__
606 l = filter(lambda f: get(db_instance, f) != get(instance, f),
609 instance.aquarium_report = True if l else False
612 def astakosuser_post_save(sender, instance, created, **kwargs):
613 if instance.aquarium_report:
614 report_user_event(instance, create=instance.new)
617 set_default_group(instance)
618 # TODO handle socket.error & IOError
619 register_users((instance,))
622 def resource_post_save(sender, instance, created, **kwargs):
625 register_resources((instance,))
628 def send_quota_disturbed(sender, instance, **kwargs):
630 extend = users.extend
631 if sender == Membership:
632 if not instance.group.is_enabled:
634 extend([instance.person])
635 elif sender == AstakosUserQuota:
636 extend([instance.user])
637 elif sender == AstakosGroupQuota:
638 if not instance.group.is_enabled:
640 extend(instance.group.astakosuser_set.all())
641 elif sender == AstakosGroup:
642 if not instance.is_enabled:
644 quota_disturbed.send(sender=sender, users=users)
647 def on_quota_disturbed(sender, users, **kwargs):
648 print '>>>', locals()
653 post_syncdb.connect(fix_superusers)
654 post_save.connect(user_post_save, sender=User)
655 pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
656 post_save.connect(astakosuser_post_save, sender=AstakosUser)
657 post_save.connect(resource_post_save, sender=Resource)
659 quota_disturbed = Signal(providing_args=["users"])
660 quota_disturbed.connect(on_quota_disturbed)
662 post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
663 post_delete.connect(send_quota_disturbed, sender=Membership)
664 post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
665 post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
666 post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
667 post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)