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