add support for groups
[astakos] / snf-astakos-app / astakos / im / models.py
1 # Copyright 2011-2012 GRNET S.A. All rights reserved.
2
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
5 # conditions are met:
6
7 #   1. Redistributions of source code must retain the above
8 #      copyright notice, this list of conditions and the following
9 #      disclaimer.
10
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.
15
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.
28
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.
33
34 import hashlib
35 import uuid
36 import logging
37
38 from time import asctime
39 from datetime import datetime, timedelta
40 from base64 import b64encode
41 from urlparse import urlparse
42 from random import randint
43
44 from django.db import models
45 from django.contrib.auth.models import User, UserManager, Group
46
47 from astakos.im.settings import DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL, AUTH_TOKEN_DURATION, BILLING_FIELDS, QUEUE_CONNECTION
48 from astakos.im.queue.userevent import UserEvent
49 from synnefo.lib.queue import exchange_connect, exchange_send, exchange_close, Receipt
50
51 QUEUE_CLIENT_ID = 3 # Astakos.
52
53 logger = logging.getLogger(__name__)
54
55 class AstakosUser(User):
56     """
57     Extends ``django.contrib.auth.models.User`` by defining additional fields.
58     """
59     # Use UserManager to get the create_user method, etc.
60     objects = UserManager()
61     
62     affiliation = models.CharField('Affiliation', max_length=255, blank=True)
63     provider = models.CharField('Provider', max_length=255, blank=True)
64     
65     #for invitations
66     user_level = DEFAULT_USER_LEVEL
67     level = models.IntegerField('Inviter level', default=user_level)
68     invitations = models.IntegerField('Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
69     
70     auth_token = models.CharField('Authentication Token', max_length=32,
71                                   null=True, blank=True)
72     auth_token_created = models.DateTimeField('Token creation date', null=True)
73     auth_token_expires = models.DateTimeField('Token expiration date', null=True)
74     
75     updated = models.DateTimeField('Update date')
76     is_verified = models.BooleanField('Is verified?', default=False)
77     
78     # ex. screen_name for twitter, eppn for shibboleth
79     third_party_identifier = models.CharField('Third-party identifier', max_length=255, null=True, blank=True)
80     
81     email_verified = models.BooleanField('Email verified?', default=False)
82     
83     has_credits = models.BooleanField('Has credits?', default=False)
84     has_signed_terms = models.BooleanField('Agree with the terms?', default=False)
85     date_signed_terms = models.DateTimeField('Signed terms date', null=True)
86     
87     __has_signed_terms = False
88     __groupnames = []
89     
90     def __init__(self, *args, **kwargs):
91         super(AstakosUser, self).__init__(*args, **kwargs)
92         self.__has_signed_terms = self.has_signed_terms
93         if self.id:
94             self.__groupnames = [g.name for g in self.groups.all()]
95         else:
96             self.is_active = False
97     
98     @property
99     def realname(self):
100         return '%s %s' %(self.first_name, self.last_name)
101     
102     @realname.setter
103     def realname(self, value):
104         parts = value.split(' ')
105         if len(parts) == 2:
106             self.first_name = parts[0]
107             self.last_name = parts[1]
108         else:
109             self.last_name = parts[0]
110     
111     @property
112     def invitation(self):
113         try:
114             return Invitation.objects.get(username=self.email)
115         except Invitation.DoesNotExist:
116             return None
117     
118     def save(self, update_timestamps=True, **kwargs):
119         if update_timestamps:
120             if not self.id:
121                 self.date_joined = datetime.now()
122             self.updated = datetime.now()
123         
124         # update date_signed_terms if necessary
125         if self.__has_signed_terms != self.has_signed_terms:
126             self.date_signed_terms = datetime.now()
127         
128         if not self.id:
129             # set username
130             while not self.username:
131                 username =  uuid.uuid4().hex[:30]
132                 try:
133                     AstakosUser.objects.get(username = username)
134                 except AstakosUser.DoesNotExist, e:
135                     self.username = username
136             if not self.provider:
137                 self.provider = 'local'
138         report_user_event(self)
139         super(AstakosUser, self).save(**kwargs)
140         
141         # set group if does not exist
142         groupname = 'shibboleth' if self.provider == 'shibboleth' else 'default'
143         if groupname not in self.__groupnames:
144             try:
145                 group = Group.objects.get(name = groupname)
146                 self.groups.add(group)
147             except Group.DoesNotExist, e:
148                 logger.exception(e)
149     
150     def renew_token(self):
151         md5 = hashlib.md5()
152         md5.update(self.username)
153         md5.update(self.realname.encode('ascii', 'ignore'))
154         md5.update(asctime())
155         
156         self.auth_token = b64encode(md5.digest())
157         self.auth_token_created = datetime.now()
158         self.auth_token_expires = self.auth_token_created + \
159                                   timedelta(hours=AUTH_TOKEN_DURATION)
160     
161     def __unicode__(self):
162         return self.username
163
164 class ApprovalTerms(models.Model):
165     """
166     Model for approval terms
167     """
168     
169     date = models.DateTimeField('Issue date', db_index=True, default=datetime.now())
170     location = models.CharField('Terms location', max_length=255)
171
172 class Invitation(models.Model):
173     """
174     Model for registring invitations
175     """
176     inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
177                                 null=True)
178     realname = models.CharField('Real name', max_length=255)
179     username = models.CharField('Unique ID', max_length=255, unique=True)
180     code = models.BigIntegerField('Invitation code', db_index=True)
181     #obsolete: we keep it just for transfering the data
182     is_accepted = models.BooleanField('Accepted?', default=False)
183     is_consumed = models.BooleanField('Consumed?', default=False)
184     created = models.DateTimeField('Creation date', auto_now_add=True)
185     #obsolete: we keep it just for transfering the data
186     accepted = models.DateTimeField('Acceptance date', null=True, blank=True)
187     consumed = models.DateTimeField('Consumption date', null=True, blank=True)
188     
189     def __init__(self, *args, **kwargs):
190         super(Invitation, self).__init__(*args, **kwargs)
191         if not self.id:
192             self.code = _generate_invitation_code()
193     
194     def consume(self):
195         self.is_consumed = True
196         self.consumed = datetime.now()
197         self.save()
198         
199     def __unicode__(self):
200         return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
201
202 def report_user_event(user):
203     def should_send(user):
204         # report event incase of new user instance
205         # or if specific fields are modified
206         if not user.id:
207             return True
208         db_instance = AstakosUser.objects.get(id = user.id)
209         for f in BILLING_FIELDS:
210             if (db_instance.__getattribute__(f) != user.__getattribute__(f)):
211                 return True
212         return False
213     
214     if QUEUE_CONNECTION and should_send(user):
215         eventType = 'create' if not user.id else 'modify'
216         body = UserEvent(QUEUE_CLIENT_ID, user, eventType, {}).format()
217         conn = exchange_connect(QUEUE_CONNECTION)
218         parts = urlparse(QUEUE_CONNECTION)
219         exchange = parts.path[1:]
220         routing_key = '%s.user' % exchange
221         exchange_send(conn, routing_key, body)
222         exchange_close(conn)
223
224 def _generate_invitation_code():
225     while True:
226         code = randint(1, 2L**63 - 1)
227         try:
228             Invitation.objects.get(code=code)
229             # An invitation with this code already exists, try again
230         except Invitation.DoesNotExist:
231             return code