remove has_signed_terms utility, introduce AstakosUser signed_terms function instead
[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 import json
38
39 from time import asctime
40 from datetime import datetime, timedelta
41 from base64 import b64encode
42 from urlparse import urlparse, urlunparse
43 from random import randint
44
45 from django.db import models
46 from django.contrib.auth.models import User, UserManager, Group
47 from django.utils.translation import ugettext as _
48 from django.core.exceptions import ValidationError
49
50 from astakos.im.settings import DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL, AUTH_TOKEN_DURATION, BILLING_FIELDS, QUEUE_CONNECTION
51
52 QUEUE_CLIENT_ID = 3 # Astakos.
53
54 logger = logging.getLogger(__name__)
55
56 class AstakosUser(User):
57     """
58     Extends ``django.contrib.auth.models.User`` by defining additional fields.
59     """
60     # Use UserManager to get the create_user method, etc.
61     objects = UserManager()
62
63     affiliation = models.CharField('Affiliation', max_length=255, blank=True)
64     provider = models.CharField('Provider', max_length=255, blank=True)
65
66     #for invitations
67     user_level = DEFAULT_USER_LEVEL
68     level = models.IntegerField('Inviter level', default=user_level)
69     invitations = models.IntegerField('Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
70
71     auth_token = models.CharField('Authentication Token', max_length=32,
72                                   null=True, blank=True)
73     auth_token_created = models.DateTimeField('Token creation date', null=True)
74     auth_token_expires = models.DateTimeField('Token expiration date', null=True)
75
76     updated = models.DateTimeField('Update date')
77     is_verified = models.BooleanField('Is verified?', default=False)
78
79     # ex. screen_name for twitter, eppn for shibboleth
80     third_party_identifier = models.CharField('Third-party identifier', max_length=255, null=True, blank=True)
81
82     email_verified = models.BooleanField('Email verified?', default=False)
83
84     has_credits = models.BooleanField('Has credits?', default=False)
85     has_signed_terms = models.BooleanField('Agree with the terms?', default=False)
86     date_signed_terms = models.DateTimeField('Signed terms date', null=True, blank=True)
87     
88     __has_signed_terms = False
89     __groupnames = []
90     
91     def __init__(self, *args, **kwargs):
92         super(AstakosUser, self).__init__(*args, **kwargs)
93         self.__has_signed_terms = self.has_signed_terms
94         if self.id:
95             self.__groupnames = [g.name for g in self.groups.all()]
96         else:
97             self.is_active = False
98     
99     @property
100     def realname(self):
101         return '%s %s' %(self.first_name, self.last_name)
102
103     @realname.setter
104     def realname(self, value):
105         parts = value.split(' ')
106         if len(parts) == 2:
107             self.first_name = parts[0]
108             self.last_name = parts[1]
109         else:
110             self.last_name = parts[0]
111
112     @property
113     def invitation(self):
114         try:
115             return Invitation.objects.get(username=self.email)
116         except Invitation.DoesNotExist:
117             return None
118
119     def save(self, update_timestamps=True, **kwargs):
120         if update_timestamps:
121             if not self.id:
122                 self.date_joined = datetime.now()
123             self.updated = datetime.now()
124         
125         # update date_signed_terms if necessary
126         if self.__has_signed_terms != self.has_signed_terms:
127             self.date_signed_terms = datetime.now()
128         
129         if not self.id:
130             # set username
131             while not self.username:
132                 username =  uuid.uuid4().hex[:30]
133                 try:
134                     AstakosUser.objects.get(username = username)
135                 except AstakosUser.DoesNotExist, e:
136                     self.username = username
137             if not self.provider:
138                 self.provider = 'local'
139         report_user_event(self)
140         self.full_clean()
141         super(AstakosUser, self).save(**kwargs)
142         
143         # set group if does not exist
144         groupname = 'shibboleth' if self.provider == 'shibboleth' else 'default'
145         if groupname not in self.__groupnames:
146             try:
147                 group = Group.objects.get(name = groupname)
148                 self.groups.add(group)
149             except Group.DoesNotExist, e:
150                 logger.exception(e)
151     
152     def renew_token(self):
153         md5 = hashlib.md5()
154         md5.update(self.username)
155         md5.update(self.realname.encode('ascii', 'ignore'))
156         md5.update(asctime())
157
158         self.auth_token = b64encode(md5.digest())
159         self.auth_token_created = datetime.now()
160         self.auth_token_expires = self.auth_token_created + \
161                                   timedelta(hours=AUTH_TOKEN_DURATION)
162
163     def __unicode__(self):
164         return self.username
165     
166     def conflicting_email(self):
167         q = AstakosUser.objects.exclude(username = self.username)
168         q = q.filter(email = self.email)
169         if q.count() != 0:
170             return True
171         return False
172     
173     def validate_unique(self, exclude=None):
174         """
175         Implements a unique_together constraint for email and is_active fields.
176         """
177         super(AstakosUser, self).validate_unique(exclude)
178         
179         q = AstakosUser.objects.exclude(username = self.username)
180         q = q.filter(email = self.email)
181         q = q.filter(is_active = self.is_active)
182         if q.count() != 0:
183             raise ValidationError({'__all__':[_('Another account with the same email & is_active combination found.')]})
184     
185     def signed_terms(self):
186         term = get_latest_terms()
187         if not term:
188             return True
189         if not self.has_signed_terms:
190             return False
191         if not self.date_signed_terms:
192             return False
193         if self.date_signed_terms < term.date:
194             self.has_signed_terms = False
195             self.save()
196             return False
197         return True
198
199 class ApprovalTerms(models.Model):
200     """
201     Model for approval terms
202     """
203
204     date = models.DateTimeField('Issue date', db_index=True, default=datetime.now())
205     location = models.CharField('Terms location', max_length=255)
206
207 class Invitation(models.Model):
208     """
209     Model for registring invitations
210     """
211     inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
212                                 null=True)
213     realname = models.CharField('Real name', max_length=255)
214     username = models.CharField('Unique ID', max_length=255, unique=True)
215     code = models.BigIntegerField('Invitation code', db_index=True)
216     #obsolete: we keep it just for transfering the data
217     is_accepted = models.BooleanField('Accepted?', default=False)
218     is_consumed = models.BooleanField('Consumed?', default=False)
219     created = models.DateTimeField('Creation date', auto_now_add=True)
220     #obsolete: we keep it just for transfering the data
221     accepted = models.DateTimeField('Acceptance date', null=True, blank=True)
222     consumed = models.DateTimeField('Consumption date', null=True, blank=True)
223     
224     def __init__(self, *args, **kwargs):
225         super(Invitation, self).__init__(*args, **kwargs)
226         if not self.id:
227             self.code = _generate_invitation_code()
228     
229     def consume(self):
230         self.is_consumed = True
231         self.consumed = datetime.now()
232         self.save()
233
234     def __unicode__(self):
235         return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
236
237 def report_user_event(user):
238     def should_send(user):
239         # report event incase of new user instance
240         # or if specific fields are modified
241         if not user.id:
242             return True
243         db_instance = AstakosUser.objects.get(id = user.id)
244         for f in BILLING_FIELDS:
245             if (db_instance.__getattribute__(f) != user.__getattribute__(f)):
246                 return True
247         return False
248
249     if QUEUE_CONNECTION and should_send(user):
250
251         from astakos.im.queue.userevent import UserEvent
252         from synnefo.lib.queue import exchange_connect, exchange_send, \
253                 exchange_close
254
255         eventType = 'create' if not user.id else 'modify'
256         body = UserEvent(QUEUE_CLIENT_ID, user, eventType, {}).format()
257         conn = exchange_connect(QUEUE_CONNECTION)
258         parts = urlparse(QUEUE_CONNECTION)
259         exchange = parts.path[1:]
260         routing_key = '%s.user' % exchange
261         exchange_send(conn, routing_key, body)
262         exchange_close(conn)
263
264 def _generate_invitation_code():
265     while True:
266         code = randint(1, 2L**63 - 1)
267         try:
268             Invitation.objects.get(code=code)
269             # An invitation with this code already exists, try again
270         except Invitation.DoesNotExist:
271             return code
272
273 def get_latest_terms():
274     try:
275         term = ApprovalTerms.objects.order_by('-id')[0]
276         return term
277     except IndexError:
278         pass
279     return None