views.py 13.4 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
66
67
from astakos.im.models import AstakosUser, Invitation
from astakos.im.util import isoformat, get_or_create_user, get_context
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)

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
82
def index(request, template_name='index.html', extra_context={}):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
83
84
    """
    Renders the index (login) page
85
    
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
86
    **Arguments**
87
    
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
88
89
90
    ``template_name``
        A custom template to use. This is optional; if not specified,
        this will default to ``index.html``.
91
    
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
92
93
    ``extra_context``
        An dictionary of variables to add to the template context.
94
    
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
95
    **Template:**
96
    
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
97
    index.html or ``template_name`` keyword argument.
98
    
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
99
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
100
    return render_response(template_name,
101
                           form = LoginForm(),
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
102
                           context_instance = get_context(request, extra_context))
103

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
104
def _generate_invitation_code():
105
106
107
108
109
110
111
112
    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
113
114
def _send_invitation(request, baseurl, inv):
    subject = _('Invitation to Astakos')
root's avatar
root committed
115
    site = Site.objects.get_current()
116
    url = settings.SIGNUP_TARGET % (baseurl, inv.code, quote(site.domain))
117
118
119
120
    message = render_to_string('invitation.txt', {
                'invitation': inv,
                'url': url,
                'baseurl': baseurl,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
121
                'service': site.name,
122
123
                'support': settings.DEFAULT_CONTACT_EMAIL})
    sender = settings.DEFAULT_FROM_EMAIL
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
124
    send_mail(subject, message, sender, [inv.username])
125
126
    logging.info('Sent invitation %s', inv)

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
127
128
@login_required
@transaction.commit_manually
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
129
def invite(request, template_name='invitations.html', extra_context={}):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
    """
    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
    """
164
165
    status = None
    message = None
166
167
    inviter = AstakosUser.objects.get(username = request.user.username)
    
168
    if request.method == 'POST':
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
169
        username = request.POST.get('uniq')
170
171
172
        realname = request.POST.get('realname')
        
        if inviter.invitations > 0:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
173
            code = _generate_invitation_code()
174
175
            invitation, created = Invitation.objects.get_or_create(
                inviter=inviter,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
176
                username=username,
177
178
179
                defaults={'code': code, 'realname': realname})
            
            try:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
180
181
                baseurl = request.build_absolute_uri('/').rstrip('/')
                _send_invitation(request, baseurl, invitation)
182
183
184
                if created:
                    inviter.invitations = max(0, inviter.invitations - 1)
                    inviter.save()
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
185
                status = messages.SUCCESS
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
186
                message = _('Invitation sent to %s' % username)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
187
                transaction.commit()
188
            except (SMTPException, socket.error) as e:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
189
                status = messages.ERROR
190
                message = getattr(e, 'strerror', '')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
191
                transaction.rollback()
192
        else:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
193
            status = messages.ERROR
194
            message = _('No invitations left')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
195
196
    messages.add_message(request, status, message)
    
197
    if request.GET.get('format') == 'json':
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
198
        sent = [{'email': inv.username,
199
200
201
202
203
204
                 '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
205
    kwargs = {'user': inviter}
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
206
207
208
    context = get_context(request, extra_context, **kwargs)
    return render_response(template_name,
                           context_instance = context)
209

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
210
211
212
213
@login_required
def edit_profile(request, template_name='profile.html', extra_context={}):
    """
    Allows a user to edit his/her profile.
214
    
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
    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.
    """
233
    try:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
234
        user = AstakosUser.objects.get(username=request.user)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
235
        form = ProfileForm(instance=user)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
236
237
238
    except AstakosUser.DoesNotExist:
        token = request.GET.get('auth', None)
        user = AstakosUser.objects.get(auth_token=token)
239
    if request.method == 'POST':
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
240
241
        form = ProfileForm(request.POST, instance=user)
        if form.is_valid():
242
            try:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
243
244
245
246
247
                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
248
    return render_response(template_name,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
249
                           form = form,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
250
251
252
                           context_instance = get_context(request,
                                                          extra_context,
                                                          user=user))
253

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
254
255
256
257
@transaction.commit_manually
def signup(request, template_name='signup.html', extra_context={}, backend=None):
    """
    Allows a user to create a local account.
258
    
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
259
260
261
262
263
264
265
266
267
268
    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.
269
    
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
    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.
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
288
    if not backend:
289
            backend = get_backend(request)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
290
    try:
291
        form = backend.get_signup_form()
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
292
293
        if request.method == 'POST':
            if form.is_valid():
294
                status, message = backend.signup(form)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
295
296
297
298
299
                # rollback incase of error
                if status == messages.ERROR:
                    transaction.rollback()
                else:
                    transaction.commit()
300
301
302
                    next = request.POST.get('next')
                    if next:
                        return redirect(next)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
303
                messages.add_message(request, status, message)
304
    except Invitation.DoesNotExist, e:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
305
306
307
308
        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))
309

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
@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:**
330
    
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
331
332
333
334
335
336
    signup.html or ``template_name`` keyword argument.
    
    **Settings:**
    
    * FEEDBACK_CONTACT_EMAIL: List of feedback recipients
    """
337
    if request.method == 'GET':
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
338
339
340
341
        form = FeedbackForm()
    if request.method == 'POST':
        if not request.user:
            return HttpResponse('Unauthorized', status=401)
342
        
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
343
344
345
346
347
348
349
350
351
        form = FeedbackForm(request.POST)
        if form.is_valid():
            subject = _("Feedback from Okeanos")
            from_email = request.user.email
            recipient_list = [settings.FEEDBACK_CONTACT_EMAIL]
            content = render_to_string(email_template_name, {
                        'message': form.cleaned_data('feedback_msg'),
                        'data': form.cleaned_data('feedback_data'),
                        'request': request})
352
            
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
353
354
355
356
357
358
            send_mail(subject, content, from_email, recipient_list)
            
            resp = json.dumps({'status': 'send'})
            return HttpResponse(resp)
    return render_response(template_name,
                           form = form,
359
                           context_instance = get_context(request, extra_context))