views.py 13.4 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
115
def _send_invitation(request, baseurl, inv):
    subject = _('Invitation to Astakos')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
116
    site = get_current_site(request)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
117
    url = settings.SIGNUP_TARGET % (baseurl, inv.code, site.domain)
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
182
                baseurl = request.build_absolute_uri('/').rstrip('/')
                _send_invitation(request, baseurl, invitation)
183
184
185
                if created:
                    inviter.invitations = max(0, inviter.invitations - 1)
                    inviter.save()
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
186
                status = messages.SUCCESS
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
187
                message = _('Invitation sent to %s' % username)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
188
                transaction.commit()
189
            except (SMTPException, socket.error) as e:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
190
                status = messages.ERROR
191
                message = getattr(e, 'strerror', '')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
192
                transaction.rollback()
193
        else:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
194
            status = messages.ERROR
195
            message = _('No invitations left')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
196
197
    messages.add_message(request, status, message)
    
198
    if request.GET.get('format') == 'json':
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
199
        sent = [{'email': inv.username,
200
201
202
203
204
205
                 '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
206
    kwargs = {'user': inviter}
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
207
208
209
    context = get_context(request, extra_context, **kwargs)
    return render_response(template_name,
                           context_instance = context)
210

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
@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.
270
    
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
    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
289
    if not backend:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
            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)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
305
    except (Invitation.DoesNotExist, Exception), e:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
306
307
308
309
        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))
310

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
@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:**
331
    
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
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
360
    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))