views.py 13.3 KB
Newer Older
1
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
# Copyright 2011 GRNET S.A. All rights reserved.
# 
# 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
59
60
61
62
63
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.models import AnonymousUser
from django.contrib.auth.decorators import login_required
from django.contrib.sites.models import get_current_site
from django.contrib import messages
from django.db import transaction
from django.contrib.auth.forms import UserCreationForm
64
65

#from astakos.im.openid_store import PithosOpenIDStore
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
66
67
68
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
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
69
from astakos.im.forms import ProfileForm, FeedbackForm
70

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

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

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
128
129
@login_required
@transaction.commit_manually
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
130
def invite(request, template_name='invitations.html', extra_context={}):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
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
164
    """
    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
    """
165
166
167
168
169
    status = None
    message = None
    inviter = request.user

    if request.method == 'POST':
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
170
        username = request.POST.get('uniq')
171
172
173
        realname = request.POST.get('realname')
        
        if inviter.invitations > 0:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
174
            code = _generate_invitation_code()
175
176
            invitation, created = Invitation.objects.get_or_create(
                inviter=inviter,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
177
                username=username,
178
179
180
                defaults={'code': code, 'realname': realname})
            
            try:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
181
                _send_invitation(request.build_absolute_uri('/').rstrip('/'), 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
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
@login_required
def edit_profile(request, template_name='profile.html', extra_context={}):
    """
    Allows a user to edit his/her profile.
    
    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)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
239
240
241
242
243
244
245
246
247
    if request.method == 'POST':
        form = ProfileForm(request.POST, instance=user)
        if form.is_valid():
            try:
                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
258
259
260
261
262
263
264
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.
    
    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:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
            backend = get_backend()
    try:
        form = backend.get_signup_form(request)
        if request.method == 'POST':
            if form.is_valid():
                status, message = backend.signup(request)
                # rollback incase of error
                if status == messages.ERROR:
                    transaction.rollback()
                else:
                    transaction.commit()
                next = request.POST.get('next')
                if next:
                    return redirect(next)
                messages.add_message(request, status, message)
    except (Invitation.DoesNotExist), e:
        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
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
    signup.html or ``template_name`` keyword argument.
    
    **Settings:**
    
    * FEEDBACK_CONTACT_EMAIL: List of feedback recipients
    """
    if request.method == 'GET':
        form = FeedbackForm()
    if request.method == 'POST':
        if not request.user:
            return HttpResponse('Unauthorized', status=401)
        
        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})
            
            send_mail(subject, content, from_email, recipient_list)
            
            resp = json.dumps({'status': 'send'})
            return HttpResponse(resp)
    return render_response(template_name,
                           form = form,
                           context_instance = get_context(request, extra_context))