- urlencode next in invitation sent email
[astakos] / astakos / im / views.py
1 # Copyright 2011 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 json
35 import logging
36 import socket
37 import csv
38 import sys
39
40 from datetime import datetime
41 from functools import wraps
42 from math import ceil
43 from random import randint
44 from smtplib import SMTPException
45 from hashlib import new as newhasher
46 from urllib import quote
47
48 from django.conf import settings
49 from django.core.mail import send_mail
50 from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest
51 from django.shortcuts import redirect
52 from django.template.loader import render_to_string
53 from django.shortcuts import render_to_response
54 from django.utils.http import urlencode
55 from django.utils.translation import ugettext as _
56 from django.core.urlresolvers import reverse
57 from django.contrib.auth.forms import AuthenticationForm
58 from django.contrib.auth.models import AnonymousUser
59 from django.contrib.auth.decorators import login_required
60 from django.contrib.sites.models import get_current_site
61 from django.contrib import messages
62 from django.db import transaction
63 from django.contrib.auth.forms import UserCreationForm
64
65 #from astakos.im.openid_store import PithosOpenIDStore
66 from astakos.im.models import AstakosUser, Invitation
67 from astakos.im.util import isoformat, get_or_create_user, get_context
68 from astakos.im.backends import get_backend
69 from astakos.im.forms import ProfileForm, FeedbackForm
70
71 def render_response(template, tab=None, status=200, context_instance=None, **kwargs):
72     """
73     Calls ``django.template.loader.render_to_string`` with an additional ``tab``
74     keyword argument and returns an ``django.http.HttpResponse`` with the
75     specified ``status``.
76     """
77     if tab is None:
78         tab = template.partition('_')[0]
79     kwargs.setdefault('tab', tab)
80     html = render_to_string(template, kwargs, context_instance=context_instance)
81     return HttpResponse(html, status=status)
82
83 def index(request, template_name='index.html', extra_context={}):
84     """
85     Renders the index (login) page
86     
87     **Arguments**
88     
89     ``template_name``
90         A custom template to use. This is optional; if not specified,
91         this will default to ``index.html``.
92     
93     ``extra_context``
94         An dictionary of variables to add to the template context.
95     
96     **Template:**
97     
98     index.html or ``template_name`` keyword argument.
99     
100     """
101     return render_response(template_name,
102                            form = AuthenticationForm(),
103                            context_instance = get_context(request, extra_context))
104
105 def _generate_invitation_code():
106     while True:
107         code = randint(1, 2L**63 - 1)
108         try:
109             Invitation.objects.get(code=code)
110             # An invitation with this code already exists, try again
111         except Invitation.DoesNotExist:
112             return code
113
114 def _send_invitation(request, baseurl, inv):
115     subject = _('Invitation to Astakos')
116     site = get_current_site(request)
117     url = settings.SIGNUP_TARGET % (baseurl, inv.code, quote(site.domain))
118     message = render_to_string('invitation.txt', {
119                 'invitation': inv,
120                 'url': url,
121                 'baseurl': baseurl,
122                 'service': site.name,
123                 'support': settings.DEFAULT_CONTACT_EMAIL})
124     sender = settings.DEFAULT_FROM_EMAIL
125     send_mail(subject, message, sender, [inv.username])
126     logging.info('Sent invitation %s', inv)
127
128 @login_required
129 @transaction.commit_manually
130 def invite(request, template_name='invitations.html', extra_context={}):
131     """
132     Allows a user to invite somebody else.
133     
134     In case of GET request renders a form for providing the invitee information.
135     In case of POST checks whether the user has not run out of invitations and then
136     sends an invitation email to singup to the service.
137     
138     The view uses commit_manually decorator in order to ensure the number of the
139     user invitations is going to be updated only if the email has been successfully sent.
140     
141     If the user isn't logged in, redirects to settings.LOGIN_URL.
142     
143     **Arguments**
144     
145     ``template_name``
146         A custom template to use. This is optional; if not specified,
147         this will default to ``invitations.html``.
148     
149     ``extra_context``
150         An dictionary of variables to add to the template context.
151     
152     **Template:**
153     
154     invitations.html or ``template_name`` keyword argument.
155     
156     **Settings:**
157     
158     The view expectes the following settings are defined:
159     
160     * LOGIN_URL: login uri
161     * SIGNUP_TARGET: Where users should signup with their invitation code
162     * DEFAULT_CONTACT_EMAIL: service support email
163     * DEFAULT_FROM_EMAIL: from email
164     """
165     status = None
166     message = None
167     inviter = request.user
168
169     if request.method == 'POST':
170         username = request.POST.get('uniq')
171         realname = request.POST.get('realname')
172         
173         if inviter.invitations > 0:
174             code = _generate_invitation_code()
175             invitation, created = Invitation.objects.get_or_create(
176                 inviter=inviter,
177                 username=username,
178                 defaults={'code': code, 'realname': realname})
179             
180             try:
181                 baseurl = request.build_absolute_uri('/').rstrip('/')
182                 _send_invitation(request, baseurl, invitation)
183                 if created:
184                     inviter.invitations = max(0, inviter.invitations - 1)
185                     inviter.save()
186                 status = messages.SUCCESS
187                 message = _('Invitation sent to %s' % username)
188                 transaction.commit()
189             except (SMTPException, socket.error) as e:
190                 status = messages.ERROR
191                 message = getattr(e, 'strerror', '')
192                 transaction.rollback()
193         else:
194             status = messages.ERROR
195             message = _('No invitations left')
196     messages.add_message(request, status, message)
197     
198     if request.GET.get('format') == 'json':
199         sent = [{'email': inv.username,
200                  'realname': inv.realname,
201                  'is_accepted': inv.is_accepted}
202                     for inv in inviter.invitations_sent.all()]
203         rep = {'invitations': inviter.invitations, 'sent': sent}
204         return HttpResponse(json.dumps(rep))
205     
206     kwargs = {'user': inviter}
207     context = get_context(request, extra_context, **kwargs)
208     return render_response(template_name,
209                            context_instance = context)
210
211 @login_required
212 def edit_profile(request, template_name='profile.html', extra_context={}):
213     """
214     Allows a user to edit his/her profile.
215     
216     In case of GET request renders a form for displaying the user information.
217     In case of POST updates the user informantion.
218     
219     If the user isn't logged in, redirects to settings.LOGIN_URL.  
220     
221     **Arguments**
222     
223     ``template_name``
224         A custom template to use. This is optional; if not specified,
225         this will default to ``profile.html``.
226     
227     ``extra_context``
228         An dictionary of variables to add to the template context.
229     
230     **Template:**
231     
232     profile.html or ``template_name`` keyword argument.
233     """
234     try:
235         user = AstakosUser.objects.get(username=request.user)
236         form = ProfileForm(instance=user)
237     except AstakosUser.DoesNotExist:
238         token = request.GET.get('auth', None)
239         user = AstakosUser.objects.get(auth_token=token)
240     if request.method == 'POST':
241         form = ProfileForm(request.POST, instance=user)
242         if form.is_valid():
243             try:
244                 form.save()
245                 msg = _('Profile has been updated successfully')
246                 messages.add_message(request, messages.SUCCESS, msg)
247             except ValueError, ve:
248                 messages.add_message(request, messages.ERROR, ve)
249     return render_response(template_name,
250                            form = form,
251                            context_instance = get_context(request,
252                                                           extra_context,
253                                                           user=user))
254
255 @transaction.commit_manually
256 def signup(request, template_name='signup.html', extra_context={}, backend=None):
257     """
258     Allows a user to create a local account.
259     
260     In case of GET request renders a form for providing the user information.
261     In case of POST handles the signup.
262     
263     The user activation will be delegated to the backend specified by the ``backend`` keyword argument
264     if present, otherwise to the ``astakos.im.backends.InvitationBackend``
265     if settings.INVITATIONS_ENABLED is True or ``astakos.im.backends.SimpleBackend`` if not
266     (see backends);
267     
268     Upon successful user creation if ``next`` url parameter is present the user is redirected there
269     otherwise renders the same page with a success message.
270     
271     On unsuccessful creation, renders the same page with an error message.
272     
273     The view uses commit_manually decorator in order to ensure the user will be created
274     only if the procedure has been completed successfully.
275     
276     **Arguments**
277     
278     ``template_name``
279         A custom template to use. This is optional; if not specified,
280         this will default to ``signup.html``.
281     
282     ``extra_context``
283         An dictionary of variables to add to the template context.
284     
285     **Template:**
286     
287     signup.html or ``template_name`` keyword argument.
288     """
289     if not backend:
290             backend = get_backend()
291     try:
292         form = backend.get_signup_form(request)
293         if request.method == 'POST':
294             if form.is_valid():
295                 status, message = backend.signup(request)
296                 # rollback incase of error
297                 if status == messages.ERROR:
298                     transaction.rollback()
299                 else:
300                     transaction.commit()
301                 next = request.POST.get('next')
302                 if next:
303                     return redirect(next)
304                 messages.add_message(request, status, message)
305     except (Invitation.DoesNotExist, Exception), e:
306         messages.add_message(request, messages.ERROR, e)
307     return render_response(template_name,
308                            form = form if 'form' in locals() else UserCreationForm(),
309                            context_instance=get_context(request, extra_context))
310
311 @login_required
312 def send_feedback(request, template_name='feedback.html', email_template_name='feedback_mail.txt', extra_context={}):
313     """
314     Allows a user to send feedback.
315     
316     In case of GET request renders a form for providing the feedback information.
317     In case of POST sends an email to support team.
318     
319     If the user isn't logged in, redirects to settings.LOGIN_URL.  
320     
321     **Arguments**
322     
323     ``template_name``
324         A custom template to use. This is optional; if not specified,
325         this will default to ``feedback.html``.
326     
327     ``extra_context``
328         An dictionary of variables to add to the template context.
329     
330     **Template:**
331     
332     signup.html or ``template_name`` keyword argument.
333     
334     **Settings:**
335     
336     * FEEDBACK_CONTACT_EMAIL: List of feedback recipients
337     """
338     if request.method == 'GET':
339         form = FeedbackForm()
340     if request.method == 'POST':
341         if not request.user:
342             return HttpResponse('Unauthorized', status=401)
343         
344         form = FeedbackForm(request.POST)
345         if form.is_valid():
346             subject = _("Feedback from Okeanos")
347             from_email = request.user.email
348             recipient_list = [settings.FEEDBACK_CONTACT_EMAIL]
349             content = render_to_string(email_template_name, {
350                         'message': form.cleaned_data('feedback_msg'),
351                         'data': form.cleaned_data('feedback_data'),
352                         'request': request})
353             
354             send_mail(subject, content, from_email, recipient_list)
355             
356             resp = json.dumps({'status': 'send'})
357             return HttpResponse(resp)
358     return render_response(template_name,
359                            form = form,
360                            context_instance = get_context(request, extra_context))