ask acknowledgment for switching local account to shibboleth one
[astakos] / snf-astakos-app / astakos / im / util.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 logging
35 import datetime
36 import time
37
38 from urllib import quote
39 from urlparse import urlsplit, urlunsplit
40 from functools import wraps
41
42 from datetime import tzinfo, timedelta
43 from django.http import HttpResponse, urlencode
44 from django.template import RequestContext
45 from django.contrib.sites.models import Site
46 from django.utils.translation import ugettext as _
47 from django.contrib.auth import login, authenticate
48 from django.core.urlresolvers import reverse
49
50 from astakos.im.models import AstakosUser, Invitation, ApprovalTerms
51 from astakos.im.settings import INVITATIONS_PER_LEVEL, COOKIE_NAME, COOKIE_DOMAIN, COOKIE_SECURE, FORCE_PROFILE_UPDATE
52
53 logger = logging.getLogger(__name__)
54
55 class UTC(tzinfo):
56    def utcoffset(self, dt):
57        return timedelta(0)
58
59    def tzname(self, dt):
60        return 'UTC'
61
62    def dst(self, dt):
63        return timedelta(0)
64
65 def isoformat(d):
66    """Return an ISO8601 date string that includes a timezone."""
67
68    return d.replace(tzinfo=UTC()).isoformat()
69
70 def epoch(datetime):
71     return int(time.mktime(datetime.timetuple())*1000)
72
73 def get_or_create_user(email, realname='', first_name='', last_name='', affiliation='', level=0, provider='local', password=''):
74     """Find or register a user into the internal database
75        and issue a token for subsequent requests.
76     """
77     user, created = AstakosUser.objects.get_or_create(email=email,
78         defaults={
79             'password':password,
80             'affiliation':affiliation,
81             'level':level,
82             'invitations':INVITATIONS_PER_LEVEL.get(level, 0),
83             'provider':provider,
84             'realname':realname,
85             'first_name':first_name,
86             'last_name':last_name
87         })
88     if created:
89         user.renew_token()
90         user.save()
91         logger.info('Created user %s', user)
92     
93     return user
94
95 def get_context(request, extra_context={}, **kwargs):
96     if not extra_context:
97         extra_context = {}
98     extra_context.update(kwargs)
99     return RequestContext(request, extra_context)
100
101 def get_invitation(request):
102     """
103     Returns the invitation identified by the ``code``.
104     
105     Raises ValueError if the invitation is consumed or there is another account
106     associated with this email.
107     """
108     code = request.GET.get('code')
109     if request.method == 'POST':
110         code = request.POST.get('code')
111     if not code:
112         return
113     invitation = Invitation.objects.get(code = code)
114     if invitation.is_consumed:
115         raise ValueError(_('Invitation is used'))
116     if reserved_email(invitation.username):
117         raise ValueError(_('Email: %s is reserved' % invitation.username))
118     return invitation
119
120 def prepare_response(request, user, next='', renew=False):
121     """Return the unique username and the token
122        as 'X-Auth-User' and 'X-Auth-Token' headers,
123        or redirect to the URL provided in 'next'
124        with the 'user' and 'token' as parameters.
125        
126        Reissue the token even if it has not yet
127        expired, if the 'renew' parameter is present
128        or user has not a valid token.
129     """
130     renew = renew or (not user.auth_token)
131     renew = renew or (user.auth_token_expires and user.auth_token_expires < datetime.datetime.now())
132     if renew:
133         user.renew_token()
134         user.save()
135     
136     if FORCE_PROFILE_UPDATE and not user.is_verified and not user.is_superuser:
137         params = ''
138         if next:
139             params = '?' + urlencode({'next': next})
140         next = reverse('astakos.im.views.edit_profile') + params
141     
142     response = HttpResponse()
143     
144     # authenticate before login
145     user = authenticate(email=user.email, auth_token=user.auth_token)
146     login(request, user)
147     set_cookie(response, user)
148     
149     if not next:
150         next = reverse('astakos.im.views.index')
151     
152     response['Location'] = next
153     response.status_code = 302
154     return response
155
156 def set_cookie(response, user):
157     expire_fmt = user.auth_token_expires.strftime('%a, %d-%b-%Y %H:%M:%S %Z')
158     cookie_value = quote(user.email + '|' + user.auth_token)
159     response.set_cookie(COOKIE_NAME, value=cookie_value,
160                         expires=expire_fmt, path='/',
161                         domain=COOKIE_DOMAIN, secure=COOKIE_SECURE)
162
163 class lazy_string(object):
164     def __init__(self, function, *args, **kwargs):
165         self.function=function
166         self.args=args
167         self.kwargs=kwargs
168         
169     def __str__(self):
170         if not hasattr(self, 'str'):
171             self.str=self.function(*self.args, **self.kwargs)
172         return self.str
173
174 def reverse_lazy(*args, **kwargs):
175     return lazy_string(reverse, *args, **kwargs)
176
177 def get_latest_terms():
178     try:
179         term = ApprovalTerms.objects.order_by('-id')[0]
180         return term
181     except IndexError:
182         pass
183     return None
184
185 def has_signed_terms(user):
186     term = get_latest_terms()
187     if not term:
188         return True
189     if not user.has_signed_terms:
190         return False
191     if not user.date_signed_terms:
192         return False
193     if user.date_signed_terms < term.date:
194         user.has_signed_terms = False
195         user.save()
196         return False
197     return True
198
199 def reserved_email(email):
200     return AstakosUser.objects.filter(email = email).count() != 0
201
202 def get_query(request):
203     return request.__getattribute__(request.method)