Fix send feedback view
[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, HttpResponseBadRequest
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.utils.http import urlencode
51 from django.http import HttpResponseRedirect, HttpResponseBadRequest
52 from django.db.utils import IntegrityError
53 from django.contrib.auth.views import password_change
54 from django.core.exceptions import ValidationError
55
56 from astakos.im.models import AstakosUser, Invitation, ApprovalTerms
57 from astakos.im.activation_backends import get_backend, SimpleBackend
58 from astakos.im.util import get_context, prepare_response, set_cookie, get_query
59 from astakos.im.forms import *
60 from astakos.im.functions import send_greeting, send_feedback, SendMailError, \
61     invite as invite_func, logout as auth_logout
62 from astakos.im.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, LOGOUT_NEXT
63
64 logger = logging.getLogger(__name__)
65
66 def render_response(template, tab=None, status=200, reset_cookie=False, context_instance=None, **kwargs):
67     """
68     Calls ``django.template.loader.render_to_string`` with an additional ``tab``
69     keyword argument and returns an ``django.http.HttpResponse`` with the
70     specified ``status``.
71     """
72     if tab is None:
73         tab = template.partition('_')[0].partition('.html')[0]
74     kwargs.setdefault('tab', tab)
75     html = render_to_string(template, kwargs, context_instance=context_instance)
76     response = HttpResponse(html, status=status)
77     if reset_cookie:
78         set_cookie(response, context_instance['request'].user)
79     return response
80
81
82 def requires_anonymous(func):
83     """
84     Decorator checkes whether the request.user is not Anonymous and in that case
85     redirects to `logout`.
86     """
87     @wraps(func)
88     def wrapper(request, *args):
89         if not request.user.is_anonymous():
90             next = urlencode({'next': request.build_absolute_uri()})
91             logout_uri = reverse(logout) + '?' + next
92             return HttpResponseRedirect(logout_uri)
93         return func(request, *args)
94     return wrapper
95
96 def signed_terms_required(func):
97     """
98     Decorator checkes whether the request.user is Anonymous and in that case
99     redirects to `logout`.
100     """
101     @wraps(func)
102     def wrapper(request, *args, **kwargs):
103         if request.user.is_authenticated() and not request.user.signed_terms():
104             params = urlencode({'next': request.build_absolute_uri(),
105                               'show_form':''})
106             terms_uri = reverse('latest_terms') + '?' + params
107             return HttpResponseRedirect(terms_uri)
108         return func(request, *args, **kwargs)
109     return wrapper
110
111 @signed_terms_required
112 def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context={}):
113     """
114     If there is logged on user renders the profile page otherwise renders login page.
115
116     **Arguments**
117
118     ``login_template_name``
119         A custom login template to use. This is optional; if not specified,
120         this will default to ``im/login.html``.
121
122     ``profile_template_name``
123         A custom profile template to use. This is optional; if not specified,
124         this will default to ``im/profile.html``.
125
126     ``extra_context``
127         An dictionary of variables to add to the template context.
128
129     **Template:**
130
131     im/profile.html or im/login.html or ``template_name`` keyword argument.
132
133     """
134     template_name = login_template_name
135     if request.user.is_authenticated():
136         return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
137     return render_response(template_name,
138                            login_form = LoginForm(request=request),
139                            context_instance = get_context(request, extra_context))
140
141 @login_required
142 @signed_terms_required
143 @transaction.commit_manually
144 def invite(request, template_name='im/invitations.html', extra_context={}):
145     """
146     Allows a user to invite somebody else.
147
148     In case of GET request renders a form for providing the invitee information.
149     In case of POST checks whether the user has not run out of invitations and then
150     sends an invitation email to singup to the service.
151
152     The view uses commit_manually decorator in order to ensure the number of the
153     user invitations is going to be updated only if the email has been successfully sent.
154
155     If the user isn't logged in, redirects to settings.LOGIN_URL.
156
157     **Arguments**
158
159     ``template_name``
160         A custom template to use. This is optional; if not specified,
161         this will default to ``im/invitations.html``.
162
163     ``extra_context``
164         An dictionary of variables to add to the template context.
165
166     **Template:**
167
168     im/invitations.html or ``template_name`` keyword argument.
169
170     **Settings:**
171
172     The view expectes the following settings are defined:
173
174     * LOGIN_URL: login uri
175     * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
176     * ASTAKOS_DEFAULT_FROM_EMAIL: from email
177     """
178     status = None
179     message = None
180     form = InvitationForm()
181     
182     inviter = request.user
183     if request.method == 'POST':
184         form = InvitationForm(request.POST)
185         if inviter.invitations > 0:
186             if form.is_valid():
187                 try:
188                     invitation = form.save()
189                     invite_func(invitation, inviter)
190                     status = messages.SUCCESS
191                     message = _('Invitation sent to %s' % invitation.username)
192                 except SendMailError, e:
193                     status = messages.ERROR
194                     message = e.message
195                     transaction.rollback()
196                 except BaseException, e:
197                     status = messages.ERROR
198                     message = _('Something went wrong.')
199                     logger.exception(e)
200                     transaction.rollback()
201                 else:
202                     transaction.commit()
203         else:
204             status = messages.ERROR
205             message = _('No invitations left')
206     messages.add_message(request, status, message)
207
208     sent = [{'email': inv.username,
209              'realname': inv.realname,
210              'is_consumed': inv.is_consumed}
211              for inv in request.user.invitations_sent.all()]
212     kwargs = {'inviter': inviter,
213               'sent':sent}
214     context = get_context(request, extra_context, **kwargs)
215     return render_response(template_name,
216                            invitation_form = form,
217                            context_instance = context)
218
219 @login_required
220 @signed_terms_required
221 def edit_profile(request, template_name='im/profile.html', extra_context={}):
222     """
223     Allows a user to edit his/her profile.
224
225     In case of GET request renders a form for displaying the user information.
226     In case of POST updates the user informantion and redirects to ``next``
227     url parameter if exists.
228
229     If the user isn't logged in, redirects to settings.LOGIN_URL.
230
231     **Arguments**
232
233     ``template_name``
234         A custom template to use. This is optional; if not specified,
235         this will default to ``im/profile.html``.
236
237     ``extra_context``
238         An dictionary of variables to add to the template context.
239
240     **Template:**
241
242     im/profile.html or ``template_name`` keyword argument.
243
244     **Settings:**
245
246     The view expectes the following settings are defined:
247
248     * LOGIN_URL: login uri
249     """
250     form = ProfileForm(instance=request.user)
251     extra_context['next'] = request.GET.get('next')
252     reset_cookie = False
253     if request.method == 'POST':
254         form = ProfileForm(request.POST, instance=request.user)
255         if form.is_valid():
256             try:
257                 prev_token = request.user.auth_token
258                 user = form.save()
259                 reset_cookie = user.auth_token != prev_token
260                 form = ProfileForm(instance=user)
261                 next = request.POST.get('next')
262                 if next:
263                     return redirect(next)
264                 msg = _('Profile has been updated successfully')
265                 messages.add_message(request, messages.SUCCESS, msg)
266             except ValueError, ve:
267                 messages.add_message(request, messages.ERROR, ve)
268     return render_response(template_name,
269                            reset_cookie = reset_cookie,
270                            profile_form = form,
271                            context_instance = get_context(request,
272                                                           extra_context))
273
274 def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context={}, backend=None):
275     """
276     Allows a user to create a local account.
277
278     In case of GET request renders a form for providing the user information.
279     In case of POST handles the signup.
280
281     The user activation will be delegated to the backend specified by the ``backend`` keyword argument
282     if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
283     if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
284     (see activation_backends);
285     
286     Upon successful user creation if ``next`` url parameter is present the user is redirected there
287     otherwise renders the same page with a success message.
288     
289     On unsuccessful creation, renders ``template_name`` with an error message.
290     
291     **Arguments**
292     
293     ``template_name``
294         A custom template to render. This is optional;
295         if not specified, this will default to ``im/signup.html``.
296
297
298     ``on_success``
299         A custom template to render in case of success. This is optional;
300         if not specified, this will default to ``im/signup_complete.html``.
301
302     ``extra_context``
303         An dictionary of variables to add to the template context.
304
305     **Template:**
306     
307     im/signup.html or ``template_name`` keyword argument.
308     im/signup_complete.html or ``on_success`` keyword argument. 
309     """
310     if request.user.is_authenticated():
311         return HttpResponseRedirect(reverse('astakos.im.views.index'))
312     
313     provider = get_query(request).get('provider', 'local')
314     try:
315         if not backend:
316             backend = get_backend(request)
317         form = backend.get_signup_form(provider)
318     except Exception, e:
319         form = SimpleBackend(request).get_signup_form(provider)
320         messages.add_message(request, messages.ERROR, e)
321     if request.method == 'POST':
322         if form.is_valid():
323             user = form.save(commit=False)
324             try:
325                 result = backend.handle_activation(user)
326                 status = messages.SUCCESS
327                 message = result.message
328                 user.save()
329                 if 'additional_email' in form.cleaned_data:
330                     additional_email = form.cleaned_data['additional_email']
331                     if additional_email != user.email:
332                         user.additionalmail_set.create(email=additional_email)
333                 if user and user.is_active:
334                     next = request.POST.get('next', '')
335                     return prepare_response(request, user, next=next)
336                 messages.add_message(request, status, message)
337                 return render_response(on_success,
338                                        context_instance=get_context(request, extra_context))
339             except SendMailError, e:
340                 status = messages.ERROR
341                 message = e.message
342                 messages.add_message(request, status, message)
343             except BaseException, e:
344                 status = messages.ERROR
345                 message = _('Something went wrong.')
346                 messages.add_message(request, status, message)
347                 logger.exception(e)
348     return render_response(template_name,
349                            signup_form = form,
350                            provider = provider,
351                            context_instance=get_context(request, extra_context))
352
353 @login_required
354 @signed_terms_required
355 def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context={}):
356     """
357     Allows a user to send feedback.
358
359     In case of GET request renders a form for providing the feedback information.
360     In case of POST sends an email to support team.
361
362     If the user isn't logged in, redirects to settings.LOGIN_URL.
363
364     **Arguments**
365
366     ``template_name``
367         A custom template to use. This is optional; if not specified,
368         this will default to ``im/feedback.html``.
369
370     ``extra_context``
371         An dictionary of variables to add to the template context.
372
373     **Template:**
374
375     im/signup.html or ``template_name`` keyword argument.
376
377     **Settings:**
378
379     * LOGIN_URL: login uri
380     * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
381     """
382     if request.method == 'GET':
383         form = FeedbackForm()
384     if request.method == 'POST':
385         if not request.user:
386             return HttpResponse('Unauthorized', status=401)
387
388         form = FeedbackForm(request.POST)
389         if form.is_valid():
390             msg = form.cleaned_data['feedback_msg']
391             data = form.cleaned_data['feedback_data']
392             try:
393                 send_feedback(msg, data, request.user, email_template_name)
394             except SendMailError, e:
395                 message = e.message
396                 status = messages.ERROR
397             else:
398                 message = _('Feedback successfully sent')
399                 status = messages.SUCCESS
400             messages.add_message(request, status, message)
401     return render_response(template_name,
402                            feedback_form = form,
403                            context_instance = get_context(request, extra_context))
404
405 def logout(request, template='registration/logged_out.html', extra_context={}):
406     """
407     Wraps `django.contrib.auth.logout` and delete the cookie.
408     """
409     auth_logout(request)
410     response = HttpResponse()
411     response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
412     next = request.GET.get('next')
413     if next:
414         response['Location'] = next
415         response.status_code = 302
416         return response
417     elif LOGOUT_NEXT:
418         response['Location'] = LOGOUT_NEXT
419         response.status_code = 301
420         return response
421     messages.add_message(request, messages.SUCCESS, _('You have successfully logged out.'))
422     context = get_context(request, extra_context)
423     response.write(render_to_string(template, context_instance=context))
424     return response
425
426 @transaction.commit_manually
427 def activate(request, email_template_name='im/welcome_email.txt', on_failure='im/signup.html'):
428     """
429     Activates the user identified by the ``auth`` request parameter, sends a welcome email
430     and renews the user token.
431
432     The view uses commit_manually decorator in order to ensure the user state will be updated
433     only if the email will be send successfully.
434     """
435     token = request.GET.get('auth')
436     next = request.GET.get('next')
437     try:
438         user = AstakosUser.objects.get(auth_token=token)
439     except AstakosUser.DoesNotExist:
440         return HttpResponseBadRequest(_('No such user'))
441     
442     try:
443         local_user = AstakosUser.objects.get(email=user.email, is_active=True)
444     except AstakosUser.DoesNotExist:
445         user.is_active = True
446         user.email_verified = True
447         try:
448             user.save()
449         except ValidationError, e:
450             return HttpResponseBadRequest(e)
451     else:
452         # switch the existing account to shibboleth one
453         local_user.provider = 'shibboleth'
454         local_user.set_unusable_password()
455         local_user.third_party_identifier = user.third_party_identifier
456         try:
457             local_user.save()
458         except ValidationError, e:
459             return HttpResponseBadRequest(e)
460         user.delete()
461         user = local_user
462     
463     try:
464         send_greeting(user, email_template_name)
465         response = prepare_response(request, user, next, renew=True)
466         transaction.commit()
467         return response
468     except SendMailError, e:
469         message = e.message
470         messages.add_message(request, messages.ERROR, message)
471         transaction.rollback()
472         return render_response(on_failure)
473     except BaseException, e:
474         status = messages.ERROR
475         message = _('Something went wrong.')
476         messages.add_message(request, messages.ERROR, message)
477         logger.exception(e)
478         transaction.rollback()
479         return signup(request, on_failure)
480
481 def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context={}):
482     term = None
483     terms = None
484     if not term_id:
485         try:
486             term = ApprovalTerms.objects.order_by('-id')[0]
487         except IndexError:
488             pass
489     else:
490         try:
491              term = ApprovalTerms.objects.get(id=term_id)
492         except ApprovalTermDoesNotExist, e:
493             pass
494
495     if not term:
496         return HttpResponseRedirect(reverse('astakos.im.views.index'))
497     f = open(term.location, 'r')
498     terms = f.read()
499
500     if request.method == 'POST':
501         next = request.POST.get('next')
502         if not next:
503             next = reverse('astakos.im.views.index')
504         form = SignApprovalTermsForm(request.POST, instance=request.user)
505         if not form.is_valid():
506             return render_response(template_name,
507                            terms = terms,
508                            approval_terms_form = form,
509                            context_instance = get_context(request, extra_context))
510         user = form.save()
511         return HttpResponseRedirect(next)
512     else:
513         form = None
514         if request.user.is_authenticated() and not request.user.signed_terms():
515             form = SignApprovalTermsForm(instance=request.user)
516         return render_response(template_name,
517                                terms = terms,
518                                approval_terms_form = form,
519                                context_instance = get_context(request, extra_context))
520
521 @signed_terms_required
522 def change_password(request):
523     return password_change(request, post_change_redirect=reverse('astakos.im.views.edit_profile'))
524
525 @transaction.commit_manually
526 def change_email(request, activation_key=None,
527                  email_template_name='registration/email_change_email.txt',
528                  form_template_name='registration/email_change_form.html',
529                  confirm_template_name='registration/email_change_done.html',
530                  extra_context={}):
531     if activation_key:
532         try:
533             user = EmailChange.objects.change_email(activation_key)
534             if request.user.is_authenticated() and request.user == user:
535                 msg = _('Email changed successfully.')
536                 messages.add_message(request, messages.SUCCESS, msg)
537                 auth_logout(request)
538                 response = prepare_response(request, user)
539                 transaction.commit()
540                 return response
541         except ValueError, e:
542             messages.add_message(request, messages.ERROR, e)
543         return render_response(confirm_template_name,
544                                modified_user = user if 'user' in locals() else None,
545                                context_instance = get_context(request,
546                                                               extra_context))
547     
548     if not request.user.is_authenticated():
549         path = quote(request.get_full_path())
550         url = request.build_absolute_uri(reverse('astakos.im.views.index'))
551         return HttpResponseRedirect(url + '?next=' + path)
552     form = EmailChangeForm(request.POST or None)
553     if request.method == 'POST' and form.is_valid():
554         try:
555             ec = form.save(email_template_name, request)
556         except SendMailError, e:
557             status = messages.ERROR
558             msg = e
559             transaction.rollback()
560         except IntegrityError, e:
561             status = messages.ERROR
562             msg = _('There is already a pending change email request.')
563         else:
564             status = messages.SUCCESS
565             msg = _('Change email request has been registered succefully.\
566                     You are going to receive a verification email in the new address.')
567             transaction.commit()
568         messages.add_message(request, status, msg)
569     return render_response(form_template_name,
570                            form = form,
571                            context_instance = get_context(request,
572                                                           extra_context))