X-Git-Url: https://code.grnet.gr/git/astakos/blobdiff_plain/6c736ed722d943f8f707935eab57fb587805f6f4..678b2236de5d9cd11bf4cc65519d57b95ffa053b:/snf-astakos-app/astakos/im/models.py diff --git a/snf-astakos-app/astakos/im/models.py b/snf-astakos-app/astakos/im/models.py index 80ac3cc..7099b0c 100644 --- a/snf-astakos-app/astakos/im/models.py +++ b/snf-astakos-app/astakos/im/models.py @@ -33,19 +33,32 @@ import hashlib import uuid +import logging +import json from time import asctime from datetime import datetime, timedelta from base64 import b64encode from urlparse import urlparse +from random import randint -from django.db import models -from django.contrib.auth.models import User, UserManager +from django.db import models, IntegrityError +from django.contrib.auth.models import User, UserManager, Group +from django.utils.translation import ugettext as _ +from django.core.exceptions import ValidationError +from django.template.loader import render_to_string +from django.core.mail import send_mail +from django.db import transaction +from django.db.models.signals import post_save, post_syncdb -from astakos.im.settings import DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL, AUTH_TOKEN_DURATION, BILLING_FIELDS, QUEUE_CONNECTION +from astakos.im.settings import DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL, \ + AUTH_TOKEN_DURATION, BILLING_FIELDS, QUEUE_CONNECTION, SITENAME, \ + EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL QUEUE_CLIENT_ID = 3 # Astakos. +logger = logging.getLogger(__name__) + class AstakosUser(User): """ Extends ``django.contrib.auth.models.User`` by defining additional fields. @@ -76,8 +89,24 @@ class AstakosUser(User): has_credits = models.BooleanField('Has credits?', default=False) has_signed_terms = models.BooleanField('Agree with the terms?', default=False) - date_signed_terms = models.DateTimeField('Signed terms date', null=True) - + date_signed_terms = models.DateTimeField('Signed terms date', null=True, blank=True) + + activation_sent = models.DateTimeField('Activation sent data', null=True, blank=True) + + __has_signed_terms = False + __groupnames = [] + + class Meta: + unique_together = ("provider", "third_party_identifier") + + def __init__(self, *args, **kwargs): + super(AstakosUser, self).__init__(*args, **kwargs) + self.__has_signed_terms = self.has_signed_terms + if self.id: + self.__groupnames = [g.name for g in self.groups.all()] + else: + self.is_active = False + @property def realname(self): return '%s %s' %(self.first_name, self.last_name) @@ -103,6 +132,11 @@ class AstakosUser(User): if not self.id: self.date_joined = datetime.now() self.updated = datetime.now() + + # update date_signed_terms if necessary + if self.__has_signed_terms != self.has_signed_terms: + self.date_signed_terms = datetime.now() + if not self.id: # set username while not self.username: @@ -111,12 +145,24 @@ class AstakosUser(User): AstakosUser.objects.get(username = username) except AstakosUser.DoesNotExist, e: self.username = username - self.is_active = False if not self.provider: self.provider = 'local' report_user_event(self) + self.validate_unique_email_isactive() + if self.is_active and self.activation_sent: + # reset the activation sent + self.activation_sent = None super(AstakosUser, self).save(**kwargs) - + + # set group if does not exist + groupname = 'shibboleth' if self.provider == 'shibboleth' else 'default' + if groupname not in self.__groupnames: + try: + group = Group.objects.get(name = groupname) + self.groups.add(group) + except Group.DoesNotExist, e: + logger.exception(e) + def renew_token(self): md5 = hashlib.md5() md5.update(self.username) @@ -127,9 +173,43 @@ class AstakosUser(User): self.auth_token_created = datetime.now() self.auth_token_expires = self.auth_token_created + \ timedelta(hours=AUTH_TOKEN_DURATION) + msg = 'Token renewed for %s' % self.email + logger._log(LOGGING_LEVEL, msg, []) def __unicode__(self): return self.username + + def conflicting_email(self): + q = AstakosUser.objects.exclude(username = self.username) + q = q.filter(email = self.email) + if q.count() != 0: + return True + return False + + def validate_unique_email_isactive(self): + """ + Implements a unique_together constraint for email and is_active fields. + """ + q = AstakosUser.objects.exclude(username = self.username) + q = q.filter(email = self.email) + q = q.filter(is_active = self.is_active) + if q.count() != 0: + raise ValidationError({'__all__':[_('Another account with the same email & is_active combination found.')]}) + + def signed_terms(self): + term = get_latest_terms() + if not term: + return True + if not self.has_signed_terms: + return False + if not self.date_signed_terms: + return False + if self.date_signed_terms < term.date: + self.has_signed_terms = False + self.date_signed_terms = None + self.save() + return False + return True class ApprovalTerms(models.Model): """ @@ -148,14 +228,15 @@ class Invitation(models.Model): realname = models.CharField('Real name', max_length=255) username = models.CharField('Unique ID', max_length=255, unique=True) code = models.BigIntegerField('Invitation code', db_index=True) - #obsolete: we keep it just for transfering the data - is_accepted = models.BooleanField('Accepted?', default=False) is_consumed = models.BooleanField('Consumed?', default=False) created = models.DateTimeField('Creation date', auto_now_add=True) - #obsolete: we keep it just for transfering the data - accepted = models.DateTimeField('Acceptance date', null=True, blank=True) consumed = models.DateTimeField('Consumption date', null=True, blank=True) - + + def __init__(self, *args, **kwargs): + super(Invitation, self).__init__(*args, **kwargs) + if not self.id: + self.code = _generate_invitation_code() + def consume(self): self.is_consumed = True self.consumed = datetime.now() @@ -191,3 +272,169 @@ def report_user_event(user): exchange_send(conn, routing_key, body) exchange_close(conn) +def _generate_invitation_code(): + while True: + code = randint(1, 2L**63 - 1) + try: + Invitation.objects.get(code=code) + # An invitation with this code already exists, try again + except Invitation.DoesNotExist: + return code + +def get_latest_terms(): + try: + term = ApprovalTerms.objects.order_by('-id')[0] + return term + except IndexError: + pass + return None + +class EmailChangeManager(models.Manager): + @transaction.commit_on_success + def change_email(self, activation_key): + """ + Validate an activation key and change the corresponding + ``User`` if valid. + + If the key is valid and has not expired, return the ``User`` + after activating. + + If the key is not valid or has expired, return ``None``. + + If the key is valid but the ``User`` is already active, + return ``None``. + + After successful email change the activation record is deleted. + + Throws ValueError if there is already + """ + try: + email_change = self.model.objects.get(activation_key=activation_key) + if email_change.activation_key_expired(): + email_change.delete() + raise EmailChange.DoesNotExist + # is there an active user with this address? + try: + AstakosUser.objects.get(email=email_change.new_email_address) + except AstakosUser.DoesNotExist: + pass + else: + raise ValueError(_('The new email address is reserved.')) + # update user + user = AstakosUser.objects.get(pk=email_change.user_id) + user.email = email_change.new_email_address + user.save() + email_change.delete() + return user + except EmailChange.DoesNotExist: + raise ValueError(_('Invalid activation key')) + +class EmailChange(models.Model): + 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.')) + user = models.ForeignKey(AstakosUser, unique=True, related_name='emailchange_user') + requested_at = models.DateTimeField(default=datetime.now()) + activation_key = models.CharField(max_length=40, unique=True, db_index=True) + + objects = EmailChangeManager() + + def activation_key_expired(self): + expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS) + return self.requested_at + expiration_date < datetime.now() + +class Service(models.Model): + name = models.CharField('Name', max_length=255, unique=True) + url = models.FilePathField() + icon = models.FilePathField(blank=True) + auth_token = models.CharField('Authentication Token', max_length=32, + null=True, blank=True) + auth_token_created = models.DateTimeField('Token creation date', null=True) + auth_token_expires = models.DateTimeField('Token expiration date', null=True) + + def save(self, **kwargs): + if not self.id: + self.renew_token() + self.full_clean() + super(Service, self).save(**kwargs) + + def renew_token(self): + md5 = hashlib.md5() + md5.update(self.name.encode('ascii', 'ignore')) + md5.update(self.url.encode('ascii', 'ignore')) + md5.update(asctime()) + + self.auth_token = b64encode(md5.digest()) + self.auth_token_created = datetime.now() + self.auth_token_expires = self.auth_token_created + \ + timedelta(hours=AUTH_TOKEN_DURATION) + +class AdditionalMail(models.Model): + """ + Model for registring invitations + """ + owner = models.ForeignKey(AstakosUser) + email = models.EmailField() + +class PendingThirdPartyUser(models.Model): + """ + Model for registring successful third party user authentications + """ + third_party_identifier = models.CharField('Third-party identifier', max_length=255, null=True, blank=True) + provider = models.CharField('Provider', max_length=255, blank=True) + email = models.EmailField(_('e-mail address'), blank=True, null=True) + first_name = models.CharField(_('first name'), max_length=30, blank=True) + last_name = models.CharField(_('last name'), max_length=30, blank=True) + affiliation = models.CharField('Affiliation', max_length=255, blank=True) + username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters")) + + class Meta: + unique_together = ("provider", "third_party_identifier") + + @property + def realname(self): + return '%s %s' %(self.first_name, self.last_name) + + @realname.setter + def realname(self, value): + parts = value.split(' ') + if len(parts) == 2: + self.first_name = parts[0] + self.last_name = parts[1] + else: + self.last_name = parts[0] + + def save(self, **kwargs): + if not self.id: + # set username + while not self.username: + username = uuid.uuid4().hex[:30] + try: + AstakosUser.objects.get(username = username) + except AstakosUser.DoesNotExist, e: + self.username = username + super(PendingThirdPartyUser, self).save(**kwargs) + +def create_astakos_user(u): + try: + AstakosUser.objects.get(user_ptr=u.pk) + except AstakosUser.DoesNotExist: + extended_user = AstakosUser(user_ptr_id=u.pk) + extended_user.__dict__.update(u.__dict__) + extended_user.renew_token() + extended_user.save() + except: + pass + +def superuser_post_syncdb(sender, **kwargs): + # if there was created a superuser + # associate it with an AstakosUser + admins = User.objects.filter(is_superuser=True) + for u in admins: + create_astakos_user(u) + +post_syncdb.connect(superuser_post_syncdb) + +def superuser_post_save(sender, instance, **kwargs): + if instance.is_superuser: + create_astakos_user(instance) + +post_save.connect(superuser_post_save, sender=User) \ No newline at end of file