Style minor fixes
[astakos] / snf-astakos-app / astakos / im / models.py
index 4f08ca5..af00f51 100644 (file)
@@ -1,18 +1,18 @@
 # Copyright 2011-2012 GRNET S.A. All rights reserved.
-# 
+#
 # Redistribution and use in source and binary forms, with or
 # without modification, are permitted provided that the following
 # conditions are met:
-# 
+#
 #   1. Redistributions of source code must retain the above
 #      copyright notice, this list of conditions and the following
 #      disclaimer.
-# 
+#
 #   2. Redistributions in binary form must reproduce the above
 #      copyright notice, this list of conditions and the following
 #      disclaimer in the documentation and/or other materials
 #      provided with the distribution.
-# 
+#
 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
@@ -25,7 +25,7 @@
 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
-# 
+#
 # The views and conclusions contained in the software and
 # documentation are those of the authors and should not be
 # interpreted as representing official policies, either expressed
 
 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, urlunparse
+from random import randint
 
 from django.db import models
-from django.contrib.auth.models import User, UserManager
+from django.contrib.auth.models import User, UserManager, Group
+from django.utils.translation import ugettext as _
+from django.core.exceptions import ValidationError
 
 from astakos.im.settings import DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL, AUTH_TOKEN_DURATION, BILLING_FIELDS, QUEUE_CONNECTION
-from synnefo.lib.queue import exchange_connect, exchange_send, exchange_close, Receipt
 
 QUEUE_CLIENT_ID = 3 # Astakos.
 
+logger = logging.getLogger(__name__)
+
 class AstakosUser(User):
     """
     Extends ``django.contrib.auth.models.User`` by defining additional fields.
     """
     # Use UserManager to get the create_user method, etc.
     objects = UserManager()
-    
+
     affiliation = models.CharField('Affiliation', max_length=255, blank=True)
     provider = models.CharField('Provider', max_length=255, blank=True)
-    
+
     #for invitations
     user_level = DEFAULT_USER_LEVEL
     level = models.IntegerField('Inviter level', default=user_level)
-    invitations = models.IntegerField('Invitations left', default=INVITATIONS_PER_LEVEL[user_level])
-    
+    invitations = models.IntegerField('Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
+
     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)
-    
+
     updated = models.DateTimeField('Update date')
     is_verified = models.BooleanField('Is verified?', default=False)
-    
+
     # ex. screen_name for twitter, eppn for shibboleth
     third_party_identifier = models.CharField('Third-party identifier', max_length=255, null=True, blank=True)
+
+    email_verified = models.BooleanField('Email verified?', default=False)
+
+    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, blank=True)
+    
+    __has_signed_terms = False
+    __groupnames = []
+    
+    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)
-    
+
     @realname.setter
     def realname(self, value):
         parts = value.split(' ')
@@ -84,19 +108,24 @@ class AstakosUser(User):
             self.last_name = parts[1]
         else:
             self.last_name = parts[0]
-    
+
     @property
     def invitation(self):
         try:
             return Invitation.objects.get(username=self.email)
         except Invitation.DoesNotExist:
             return None
-    
+
     def save(self, update_timestamps=True, **kwargs):
         if update_timestamps:
             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:
@@ -105,25 +134,75 @@ 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.full_clean()
         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)
         md5.update(self.realname.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)
-    
+
     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(self, exclude=None):
+        """
+        Implements a unique_together constraint for email and is_active fields.
+        """
+        super(AstakosUser, self).validate_unique(exclude)
+        
+        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.save()
+            return False
+        return True
+
+class ApprovalTerms(models.Model):
+    """
+    Model for approval terms
+    """
+
+    date = models.DateTimeField('Issue date', db_index=True, default=datetime.now())
+    location = models.CharField('Terms location', max_length=255)
 
 class Invitation(models.Model):
     """
@@ -132,7 +211,7 @@ class Invitation(models.Model):
     inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
                                 null=True)
     realname = models.CharField('Real name', max_length=255)
-    username = models.CharField('Unique ID', 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)
@@ -142,11 +221,16 @@ class Invitation(models.Model):
     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()
         self.save()
-        
+
     def __unicode__(self):
         return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
 
@@ -161,14 +245,35 @@ def report_user_event(user):
             if (db_instance.__getattribute__(f) != user.__getattribute__(f)):
                 return True
         return False
-    
+
     if QUEUE_CONNECTION and should_send(user):
-        l = [[elem, str(user.__getattribute__(elem))] for elem in BILLING_FIELDS]
-        details = dict(l)
-        body = Receipt(QUEUE_CLIENT_ID, user.email, '', 0, details).format()
-        msgsubtype = 'create' if not user.id else 'modify'
-        exchange = '%s.%s.#' %(QUEUE_CONNECTION, msgsubtype)
-        conn = exchange_connect(exchange)
-        routing_key = exchange.replace('#', body['id'])
+
+        from astakos.im.queue.userevent import UserEvent
+        from synnefo.lib.queue import exchange_connect, exchange_send, \
+                exchange_close
+
+        eventType = 'create' if not user.id else 'modify'
+        body = UserEvent(QUEUE_CLIENT_ID, user, eventType, {}).format()
+        conn = exchange_connect(QUEUE_CONNECTION)
+        parts = urlparse(QUEUE_CONNECTION)
+        exchange = parts.path[1:]
+        routing_key = '%s.user' % exchange
         exchange_send(conn, routing_key, body)
-        exchange_close(conn)
\ No newline at end of file
+        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
\ No newline at end of file