change email templates
[astakos] / snf-astakos-app / astakos / im / views.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 socket
36
37 from smtplib import SMTPException
38 from urllib import quote
39 from functools import wraps
40
41 from django.core.mail import send_mail
42 from django.http import HttpResponse
43 from django.shortcuts import redirect
44 from django.template.loader import render_to_string
45 from django.utils.translation import ugettext as _
46 from django.core.urlresolvers import reverse
47 from django.contrib.auth.decorators import login_required
48 from django.contrib import messages
49 from django.db import transaction
50 from django.contrib.auth import logout as auth_logout
51 from django.utils.http import urlencode
52 from django.http import HttpResponseRedirect
53 from django.db.utils import IntegrityError
54
55 from astakos.im.models import AstakosUser, Invitation
56 from astakos.im.backends import get_backend
57 from astakos.im.util import get_context, prepare_response, set_cookie
58 from astakos.im.forms import *
59 from astakos.im.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, BASEURL, LOGOUT_NEXT
60 from astakos.im.functions import invite as invite_func
61
62 logger = logging.getLogger(__name__)
63
64 def render_response(template, tab=None, status=200, reset_cookie=False, context_instance=None, **kwargs):
65     """
66     Calls ``django.template.loader.render_to_string`` with an additional ``tab``
67     keyword argument and returns an ``django.http.HttpResponse`` with the
68     specified ``status``.
69     """
70     if tab is None:
71         tab = template.partition('_')[0].partition('.html')[0]
72     kwargs.setdefault('tab', tab)
73     html = render_to_string(template, kwargs, context_instance=context_instance)
74     response = HttpResponse(html, status=status)
75     if reset_cookie:
76         set_cookie(response, context_instance['request'].user)
77     return response
78
79
80 def requires_anonymous(func):
81     """
82     Decorator checkes whether the request.user is Anonymous and in that case
83     redirects to `logout`.
84     """
85     @wraps(func)
86     def wrapper(request, *args):
87         if not request.user.is_anonymous():
88             next = urlencode({'next': request.build_absolute_uri()})
89             login_uri = reverse(logout) + '?' + next
90             return HttpResponseRedirect(login_uri)
91         return func(request, *args)
92     return wrapper
93
94 def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context={}):
95     """
96     If there is logged on user renders the profile page otherwise renders login page.
97     
98     **Arguments**
99     
100     ``login_template_name``
101         A custom login template to use. This is optional; if not specified,
102         this will default to ``im/login.html``.
103     
104     ``profile_template_name``
105         A custom profile template to use. This is optional; if not specified,
106         this will default to ``im/profile.html``.
107     
108     ``extra_context``
109         An dictionary of variables to add to the template context.
110     
111     **Template:**
112     
113     im/profile.html or im/login.html or ``template_name`` keyword argument.
114     
115     """
116     template_name = login_template_name
117     formclass = 'LoginForm'
118     kwargs = {}
119     if request.user.is_authenticated():
120         return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
121     return render_response(template_name,
122                            form = globals()[formclass](**kwargs),
123                            context_instance = get_context(request, extra_context))
124
125 @login_required
126 @transaction.commit_manually
127 def invite(request, template_name='im/invitations.html', extra_context={}):
128     """
129     Allows a user to invite somebody else.
130     
131     In case of GET request renders a form for providing the invitee information.
132     In case of POST checks whether the user has not run out of invitations and then
133     sends an invitation email to singup to the service.
134     
135     The view uses commit_manually decorator in order to ensure the number of the
136     user invitations is going to be updated only if the email has been successfully sent.
137     
138     If the user isn't logged in, redirects to settings.LOGIN_URL.
139     
140     **Arguments**
141     
142     ``template_name``
143         A custom template to use. This is optional; if not specified,
144         this will default to ``im/invitations.html``.
145     
146     ``extra_context``
147         An dictionary of variables to add to the template context.
148     
149     **Template:**
150     
151     im/invitations.html or ``template_name`` keyword argument.
152     
153     **Settings:**
154     
155     The view expectes the following settings are defined:
156     
157     * LOGIN_URL: login uri
158     * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
159     * ASTAKOS_DEFAULT_FROM_EMAIL: from email
160     """
161     status = None
162     message = None
163     inviter = AstakosUser.objects.get(username = request.user.username)
164     
165     if request.method == 'POST':
166         username = request.POST.get('uniq')
167         realname = request.POST.get('realname')
168         
169         if inviter.invitations > 0:
170             try:
171                 invite_func(inviter, username, realname)
172                 status = messages.SUCCESS
173                 message = _('Invitation sent to %s' % username)
174                 transaction.commit()
175             except (SMTPException, socket.error) as e:
176                 status = messages.ERROR
177                 message = getattr(e, 'strerror', '')
178                 transaction.rollback()
179             except IntegrityError, e:
180                 status = messages.ERROR
181                 message = _('There is already invitation for %s' % username)
182                 transaction.rollback()
183         else:
184             status = messages.ERROR
185             message = _('No invitations left')
186     messages.add_message(request, status, message)
187     
188     sent = [{'email': inv.username,
189              'realname': inv.realname,
190              'is_consumed': inv.is_consumed}
191              for inv in inviter.invitations_sent.all()]
192     kwargs = {'inviter': inviter,
193               'sent':sent}
194     context = get_context(request, extra_context, **kwargs)
195     return render_response(template_name,
196                            context_instance = context)
197
198 @login_required
199 def edit_profile(request, template_name='im/profile.html', extra_context={}):
200     """
201     Allows a user to edit his/her profile.
202     
203     In case of GET request renders a form for displaying the user information.
204     In case of POST updates the user informantion and redirects to ``next``
205     url parameter if exists.
206     
207     If the user isn't logged in, redirects to settings.LOGIN_URL.
208     
209     **Arguments**
210     
211     ``template_name``
212         A custom template to use. This is optional; if not specified,
213         this will default to ``im/profile.html``.
214     
215     ``extra_context``
216         An dictionary of variables to add to the template context.
217     
218     **Template:**
219     
220     im/profile.html or ``template_name`` keyword argument.
221     
222     **Settings:**
223     
224     The view expectes the following settings are defined:
225     
226     * LOGIN_URL: login uri
227     """
228     form = ProfileForm(instance=request.user)
229     extra_context['next'] = request.GET.get('next')
230     reset_cookie = False
231     if request.method == 'POST':
232         form = ProfileForm(request.POST, instance=request.user)
233         if form.is_valid():
234             try:
235                 prev_token = request.user.auth_token
236                 user = form.save()
237                 reset_cookie = user.auth_token != prev_token
238                 form = ProfileForm(instance=user)
239                 next = request.POST.get('next')
240                 if next:
241                     return redirect(next)
242                 msg = _('Profile has been updated successfully')
243                 messages.add_message(request, messages.SUCCESS, msg)
244             except ValueError, ve:
245                 messages.add_message(request, messages.ERROR, ve)
246     return render_response(template_name,
247                            reset_cookie = reset_cookie,
248                            form = form,
249                            context_instance = get_context(request,
250                                                           extra_context))
251
252 def signup(request, on_failure='im/signup.html', on_success='im/signup_complete.html', extra_context={}, backend=None):
253     """
254     Allows a user to create a local account.
255     
256     In case of GET request renders a form for providing the user information.
257     In case of POST handles the signup.
258     
259     The user activation will be delegated to the backend specified by the ``backend`` keyword argument
260     if present, otherwise to the ``astakos.im.backends.InvitationBackend``
261     if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.backends.SimpleBackend`` if not
262     (see backends);
263     
264     Upon successful user creation if ``next`` url parameter is present the user is redirected there
265     otherwise renders the same page with a success message.
266     
267     On unsuccessful creation, renders ``on_failure`` with an error message.
268     
269     **Arguments**
270     
271     ``on_failure``
272         A custom template to render in case of failure. This is optional;
273         if not specified, this will default to ``im/signup.html``.
274     
275     
276     ``on_success``
277         A custom template to render in case of success. This is optional;
278         if not specified, this will default to ``im/signup_complete.html``.
279     
280     ``extra_context``
281         An dictionary of variables to add to the template context.
282     
283     **Template:**
284     
285     im/signup.html or ``on_failure`` keyword argument.
286     im/signup_complete.html or ``on_success`` keyword argument. 
287     """
288     if request.user.is_authenticated():
289         return HttpResponseRedirect(reverse('astakos.im.views.index'))
290     try:
291         if not backend:
292             backend = get_backend(request)
293         for provider in IM_MODULES:
294             extra_context['%s_form' % provider] = backend.get_signup_form(provider)
295         if request.method == 'POST':
296             provider = request.POST.get('provider')
297             next = request.POST.get('next', '')
298             form = extra_context['%s_form' % provider]
299             if form.is_valid():
300                 if provider != 'local':
301                     url = reverse('astakos.im.target.%s.login' % provider)
302                     url = '%s?email=%s&next=%s' % (url, form.data['email'], next)
303                     if backend.invitation:
304                         url = '%s&code=%s' % (url, backend.invitation.code)
305                     return redirect(url)
306                 else:
307                     status, message, user = backend.signup(form)
308                     if user and user.is_active:
309                         return prepare_response(request, user, next=next)
310                     messages.add_message(request, status, message)
311                     return render_response(on_success,
312                                            context_instance=get_context(request, extra_context))
313     except (Invitation.DoesNotExist, ValueError), e:
314         messages.add_message(request, messages.ERROR, e)
315         for provider in IM_MODULES:
316             main = provider.capitalize() if provider == 'local' else 'ThirdParty'
317             formclass = '%sUserCreationForm' % main
318             extra_context['%s_form' % provider] = globals()[formclass]()
319     return render_response(on_failure,
320                            context_instance=get_context(request, extra_context))
321
322 @login_required
323 def send_feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context={}):
324     """
325     Allows a user to send feedback.
326     
327     In case of GET request renders a form for providing the feedback information.
328     In case of POST sends an email to support team.
329     
330     If the user isn't logged in, redirects to settings.LOGIN_URL.
331     
332     **Arguments**
333     
334     ``template_name``
335         A custom template to use. This is optional; if not specified,
336         this will default to ``im/feedback.html``.
337     
338     ``extra_context``
339         An dictionary of variables to add to the template context.
340     
341     **Template:**
342     
343     im/signup.html or ``template_name`` keyword argument.
344     
345     **Settings:**
346     
347     * LOGIN_URL: login uri
348     * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
349     """
350     if request.method == 'GET':
351         form = FeedbackForm()
352     if request.method == 'POST':
353         if not request.user:
354             return HttpResponse('Unauthorized', status=401)
355         
356         form = FeedbackForm(request.POST)
357         if form.is_valid():
358             subject = _("Feedback from %s alpha2 testing" % SITENAME)
359             from_email = request.user.email
360             recipient_list = [DEFAULT_CONTACT_EMAIL]
361             content = render_to_string(email_template_name, {
362                         'message': form.cleaned_data['feedback_msg'],
363                         'data': form.cleaned_data['feedback_data'],
364                         'request': request})
365             
366             try:
367                 send_mail(subject, content, from_email, recipient_list)
368                 message = _('Feedback successfully sent')
369                 status = messages.SUCCESS
370             except (SMTPException, socket.error) as e:
371                 status = messages.ERROR
372                 message = getattr(e, 'strerror', '')
373             messages.add_message(request, status, message)
374     return render_response(template_name,
375                            form = form,
376                            context_instance = get_context(request, extra_context))
377
378 def logout(request, template='registration/logged_out.html', extra_context={}):
379     """
380     Wraps `django.contrib.auth.logout` and delete the cookie.
381     """
382     auth_logout(request)
383     response = HttpResponse()
384     response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
385     next = request.GET.get('next')
386     if next:
387         response['Location'] = next
388         response.status_code = 302
389         return response
390     elif LOGOUT_NEXT:
391         response['Location'] = LOGOUT_NEXT
392         response.status_code = 301
393         return response
394     messages.add_message(request, messages.SUCCESS, _('You have successfully logged out.'))
395     context = get_context(request, extra_context)
396     response.write(render_to_string(template, context_instance=context))
397     return response
398
399 def activate(request):
400     """
401     Activates the user identified by the ``auth`` request parameter
402     """
403     token = request.GET.get('auth')
404     next = request.GET.get('next')
405     try:
406         user = AstakosUser.objects.get(auth_token=token)
407     except AstakosUser.DoesNotExist:
408         return HttpResponseBadRequest('No such user')
409     
410     user.is_active = True
411     user.email_verified = True
412     user.save()
413     return prepare_response(request, user, next, renew=True)