views.py 14.1 KB
Newer Older
Antony Chazapis's avatar
Antony Chazapis committed
1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
# 
#   1. Redistributions of source code must retain the above
#      copyright notice, this list of conditions and the following
#      disclaimer.
# 
#   2. Redistributions in binary form must reproduce the above
#      copyright notice, this list of conditions and the following
#      disclaimer in the documentation and/or other materials
#      provided with the distribution.
# 
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# 
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.

import json
import logging
import socket
import csv
import sys

from datetime import datetime
from functools import wraps
from math import ceil
from random import randint
from smtplib import SMTPException
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
45
46
from hashlib import new as newhasher
from urllib import quote
47
48
49
50
51
52
53
54
55
56

from django.conf import settings
from django.core.mail import send_mail
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest
from django.shortcuts import redirect
from django.template.loader import render_to_string
from django.shortcuts import render_to_response
from django.utils.http import urlencode
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
57
58
from django.contrib.auth.models import AnonymousUser
from django.contrib.auth.decorators import login_required
root's avatar
root committed
59
from django.contrib.sites.models import Site
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
60
61
62
from django.contrib import messages
from django.db import transaction
from django.contrib.auth.forms import UserCreationForm
63
64

#from astakos.im.openid_store import PithosOpenIDStore
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
65
from astakos.im.models import AstakosUser, Invitation
66
from astakos.im.util import isoformat, get_context, get_current_site
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
67
from astakos.im.backends import get_backend
68
from astakos.im.forms import ProfileForm, FeedbackForm, LoginForm
69

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
70
def render_response(template, tab=None, status=200, context_instance=None, **kwargs):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
71
72
73
74
75
    """
    Calls ``django.template.loader.render_to_string`` with an additional ``tab``
    keyword argument and returns an ``django.http.HttpResponse`` with the
    specified ``status``.
    """
76
77
78
    if tab is None:
        tab = template.partition('_')[0]
    kwargs.setdefault('tab', tab)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
79
    html = render_to_string(template, kwargs, context_instance=context_instance)
80
81
    return HttpResponse(html, status=status)

82
def index(request, login_template_name='login.html', profile_template_name='profile.html', extra_context={}):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
83
    """
84
    If there is logged on user renders the profile page otherwise renders login page.
85
    
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
86
    **Arguments**
87
    
88
89
90
91
92
93
    ``login_template_name``
        A custom login template to use. This is optional; if not specified,
        this will default to ``login.html``.
    
    ``profile_template_name``
        A custom profile template to use. This is optional; if not specified,
94
        this will default to ``profile.html``.
95
    
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
96
97
    ``extra_context``
        An dictionary of variables to add to the template context.
98
    
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
99
    **Template:**
100
    
101
    profile.html or login.html or ``template_name`` keyword argument.
102
    
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
103
    """
104
105
106
107
108
109
110
    template_name = login_template_name
    formclass = 'LoginForm'
    kwargs = {}
    if request.user.is_authenticated():
        template_name = profile_template_name
        formclass = 'ProfileForm'
        kwargs.update({'instance':request.user})
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
111
    return render_response(template_name,
112
                           form = globals()[formclass](**kwargs),
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
113
                           context_instance = get_context(request, extra_context))
114

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
115
def _generate_invitation_code():
116
117
118
119
120
121
122
123
    while True:
        code = randint(1, 2L**63 - 1)
        try:
            Invitation.objects.get(code=code)
            # An invitation with this code already exists, try again
        except Invitation.DoesNotExist:
            return code

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
124
def _send_invitation(request, baseurl, inv):
125
126
127
    sitename, sitedomain = get_current_site(request, use_https=request.is_secure())
    subject = _('Invitation to %s' % sitename)
    url = settings.SIGNUP_TARGET % (baseurl, inv.code, quote(sitedomain))
128
129
130
131
    message = render_to_string('invitation.txt', {
                'invitation': inv,
                'url': url,
                'baseurl': baseurl,
132
133
134
                'service': sitename,
                'support': settings.DEFAULT_CONTACT_EMAIL % sitename.lower()})
    sender = settings.DEFAULT_FROM_EMAIL % sitename
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
135
    send_mail(subject, message, sender, [inv.username])
136
137
    logging.info('Sent invitation %s', inv)

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
138
139
@login_required
@transaction.commit_manually
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
140
def invite(request, template_name='invitations.html', extra_context={}):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
    """
    Allows a user to invite somebody else.
    
    In case of GET request renders a form for providing the invitee information.
    In case of POST checks whether the user has not run out of invitations and then
    sends an invitation email to singup to the service.
    
    The view uses commit_manually decorator in order to ensure the number of the
    user invitations is going to be updated only if the email has been successfully sent.
    
    If the user isn't logged in, redirects to settings.LOGIN_URL.
    
    **Arguments**
    
    ``template_name``
        A custom template to use. This is optional; if not specified,
        this will default to ``invitations.html``.
    
    ``extra_context``
        An dictionary of variables to add to the template context.
    
    **Template:**
    
    invitations.html or ``template_name`` keyword argument.
    
    **Settings:**
    
    The view expectes the following settings are defined:
    
    * LOGIN_URL: login uri
    * SIGNUP_TARGET: Where users should signup with their invitation code
    * DEFAULT_CONTACT_EMAIL: service support email
    * DEFAULT_FROM_EMAIL: from email
    """
175
176
    status = None
    message = None
177
178
    inviter = AstakosUser.objects.get(username = request.user.username)
    
179
    if request.method == 'POST':
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
180
        username = request.POST.get('uniq')
181
182
183
        realname = request.POST.get('realname')
        
        if inviter.invitations > 0:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
184
            code = _generate_invitation_code()
185
186
            invitation, created = Invitation.objects.get_or_create(
                inviter=inviter,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
187
                username=username,
188
189
190
                defaults={'code': code, 'realname': realname})
            
            try:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
191
192
                baseurl = request.build_absolute_uri('/').rstrip('/')
                _send_invitation(request, baseurl, invitation)
193
194
195
                if created:
                    inviter.invitations = max(0, inviter.invitations - 1)
                    inviter.save()
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
196
                status = messages.SUCCESS
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
197
                message = _('Invitation sent to %s' % username)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
198
                transaction.commit()
199
            except (SMTPException, socket.error) as e:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
200
                status = messages.ERROR
201
                message = getattr(e, 'strerror', '')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
202
                transaction.rollback()
203
        else:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
204
            status = messages.ERROR
205
            message = _('No invitations left')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
206
207
    messages.add_message(request, status, message)
    
208
    if request.GET.get('format') == 'json':
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
209
        sent = [{'email': inv.username,
210
211
212
213
214
215
                 'realname': inv.realname,
                 'is_accepted': inv.is_accepted}
                    for inv in inviter.invitations_sent.all()]
        rep = {'invitations': inviter.invitations, 'sent': sent}
        return HttpResponse(json.dumps(rep))
    
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
216
    kwargs = {'user': inviter}
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
217
218
219
    context = get_context(request, extra_context, **kwargs)
    return render_response(template_name,
                           context_instance = context)
220

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
221
222
223
224
@login_required
def edit_profile(request, template_name='profile.html', extra_context={}):
    """
    Allows a user to edit his/her profile.
225
    
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
    In case of GET request renders a form for displaying the user information.
    In case of POST updates the user informantion.
    
    If the user isn't logged in, redirects to settings.LOGIN_URL.  
    
    **Arguments**
    
    ``template_name``
        A custom template to use. This is optional; if not specified,
        this will default to ``profile.html``.
    
    ``extra_context``
        An dictionary of variables to add to the template context.
    
    **Template:**
    
    profile.html or ``template_name`` keyword argument.
    """
244
    try:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
245
        user = AstakosUser.objects.get(username=request.user)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
246
        form = ProfileForm(instance=user)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
247
248
249
    except AstakosUser.DoesNotExist:
        token = request.GET.get('auth', None)
        user = AstakosUser.objects.get(auth_token=token)
250
    if request.method == 'POST':
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
251
252
        form = ProfileForm(request.POST, instance=user)
        if form.is_valid():
253
            try:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
254
255
256
257
258
                form.save()
                msg = _('Profile has been updated successfully')
                messages.add_message(request, messages.SUCCESS, msg)
            except ValueError, ve:
                messages.add_message(request, messages.ERROR, ve)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
259
    return render_response(template_name,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
260
                           form = form,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
261
262
263
                           context_instance = get_context(request,
                                                          extra_context,
                                                          user=user))
264

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
265
266
267
268
@transaction.commit_manually
def signup(request, template_name='signup.html', extra_context={}, backend=None):
    """
    Allows a user to create a local account.
269
    
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
270
271
272
273
274
275
276
277
278
279
    In case of GET request renders a form for providing the user information.
    In case of POST handles the signup.
    
    The user activation will be delegated to the backend specified by the ``backend`` keyword argument
    if present, otherwise to the ``astakos.im.backends.InvitationBackend``
    if settings.INVITATIONS_ENABLED is True or ``astakos.im.backends.SimpleBackend`` if not
    (see backends);
    
    Upon successful user creation if ``next`` url parameter is present the user is redirected there
    otherwise renders the same page with a success message.
280
    
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
    On unsuccessful creation, renders the same page with an error message.
    
    The view uses commit_manually decorator in order to ensure the user will be created
    only if the procedure has been completed successfully.
    
    **Arguments**
    
    ``template_name``
        A custom template to use. This is optional; if not specified,
        this will default to ``signup.html``.
    
    ``extra_context``
        An dictionary of variables to add to the template context.
    
    **Template:**
    
    signup.html or ``template_name`` keyword argument.
    """
    try:
300
301
        if not backend:
            backend = get_backend(request)
302
        form = backend.get_signup_form()
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
303
304
        if request.method == 'POST':
            if form.is_valid():
305
                status, message = backend.signup(form)
306
                # rollback in case of error
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
307
308
309
310
                if status == messages.ERROR:
                    transaction.rollback()
                else:
                    transaction.commit()
311
312
313
                    next = request.POST.get('next')
                    if next:
                        return redirect(next)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
314
                messages.add_message(request, status, message)
315
    except (Invitation.DoesNotExist, Exception), e:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
316
317
318
319
        messages.add_message(request, messages.ERROR, e)
    return render_response(template_name,
                           form = form if 'form' in locals() else UserCreationForm(),
                           context_instance=get_context(request, extra_context))
320

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
@login_required
def send_feedback(request, template_name='feedback.html', email_template_name='feedback_mail.txt', extra_context={}):
    """
    Allows a user to send feedback.
    
    In case of GET request renders a form for providing the feedback information.
    In case of POST sends an email to support team.
    
    If the user isn't logged in, redirects to settings.LOGIN_URL.  
    
    **Arguments**
    
    ``template_name``
        A custom template to use. This is optional; if not specified,
        this will default to ``feedback.html``.
    
    ``extra_context``
        An dictionary of variables to add to the template context.
    
    **Template:**
341
    
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
342
343
344
345
    signup.html or ``template_name`` keyword argument.
    
    **Settings:**
    
346
    * DEFAULT_CONTACT_EMAIL: List of feedback recipients
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
347
    """
348
    if request.method == 'GET':
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
349
350
351
352
        form = FeedbackForm()
    if request.method == 'POST':
        if not request.user:
            return HttpResponse('Unauthorized', status=401)
353
        
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
354
355
        form = FeedbackForm(request.POST)
        if form.is_valid():
356
357
            sitename, sitedomain = get_current_site(request, use_https=request.is_secure())
            subject = _("Feedback from %s" % sitename)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
358
            from_email = request.user.email
359
            recipient_list = [settings.DEFAULT_CONTACT_EMAIL % sitename.lower()]
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
360
361
362
363
            content = render_to_string(email_template_name, {
                        'message': form.cleaned_data('feedback_msg'),
                        'data': form.cleaned_data('feedback_data'),
                        'request': request})
364
            
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
365
366
367
368
369
370
            send_mail(subject, content, from_email, recipient_list)
            
            resp = json.dumps({'status': 'send'})
            return HttpResponse(resp)
    return render_response(template_name,
                           form = form,
371
                           context_instance = get_context(request, extra_context))