Statistics
| Branch: | Tag: | Revision:

root / astakos / im / views.py @ 13858d75

History | View | Annotate | Download (15.5 kB)

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.models import AstakosUser, Invitation
65
from astakos.im.backends import get_backend
66
from astakos.im.util import get_context, get_current_site, get_invitation
67
from astakos.im.forms import *
68

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

    
81
def index(request, login_template_name='login.html', profile_template_name='profile.html', extra_context={}):
82
    """
83
    If there is logged on user renders the profile page otherwise renders login page.
84

85
    **Arguments**
86

87
    ``login_template_name``
88
        A custom login template to use. This is optional; if not specified,
89
        this will default to ``login.html``.
90

91
    ``profile_template_name``
92
        A custom profile template to use. This is optional; if not specified,
93
        this will default to ``profile.html``.
94

95
    ``extra_context``
96
        An dictionary of variables to add to the template context.
97

98
    **Template:**
99

100
    profile.html or login.html or ``template_name`` keyword argument.
101

102
    """
103
    template_name = login_template_name
104
    formclass = 'LoginForm'
105
    kwargs = {}
106
    if request.user.is_authenticated():
107
        template_name = profile_template_name
108
        formclass = 'ProfileForm'
109
        kwargs.update({'instance':request.user})
110
    return render_response(template_name,
111
                           form = globals()[formclass](**kwargs),
112
                           context_instance = get_context(request, extra_context))
113

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

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

    
137
@login_required
138
@transaction.commit_manually
139
def invite(request, template_name='invitations.html', extra_context={}):
140
    """
141
    Allows a user to invite somebody else.
142

143
    In case of GET request renders a form for providing the invitee information.
144
    In case of POST checks whether the user has not run out of invitations and then
145
    sends an invitation email to singup to the service.
146

147
    The view uses commit_manually decorator in order to ensure the number of the
148
    user invitations is going to be updated only if the email has been successfully sent.
149

150
    If the user isn't logged in, redirects to settings.LOGIN_URL.
151

152
    **Arguments**
153

154
    ``template_name``
155
        A custom template to use. This is optional; if not specified,
156
        this will default to ``invitations.html``.
157

158
    ``extra_context``
159
        An dictionary of variables to add to the template context.
160

161
    **Template:**
162

163
    invitations.html or ``template_name`` keyword argument.
164

165
    **Settings:**
166

167
    The view expectes the following settings are defined:
168

169
    * LOGIN_URL: login uri
170
    * SIGNUP_TARGET: Where users should signup with their invitation code
171
    * DEFAULT_CONTACT_EMAIL: service support email
172
    * DEFAULT_FROM_EMAIL: from email
173
    """
174
    status = None
175
    message = None
176
    inviter = AstakosUser.objects.get(username = request.user.username)
177

    
178
    if request.method == 'POST':
179
        username = request.POST.get('uniq')
180
        realname = request.POST.get('realname')
181

    
182
        if inviter.invitations > 0:
183
            code = _generate_invitation_code()
184
            invitation, created = Invitation.objects.get_or_create(
185
                inviter=inviter,
186
                username=username,
187
                defaults={'code': code, 'realname': realname})
188

    
189
            try:
190
                baseurl = request.build_absolute_uri('/').rstrip('/')
191
                _send_invitation(request, baseurl, invitation)
192
                if created:
193
                    inviter.invitations = max(0, inviter.invitations - 1)
194
                    inviter.save()
195
                status = messages.SUCCESS
196
                message = _('Invitation sent to %s' % username)
197
                transaction.commit()
198
            except (SMTPException, socket.error) as e:
199
                status = messages.ERROR
200
                message = getattr(e, 'strerror', '')
201
                transaction.rollback()
202
        else:
203
            status = messages.ERROR
204
            message = _('No invitations left')
205
    messages.add_message(request, status, message)
206

    
207
    sent = [{'email': inv.username,
208
                 'realname': inv.realname,
209
                 'is_accepted': inv.is_accepted}
210
                    for inv in inviter.invitations_sent.all()]
211
    kwargs = {'user': inviter,
212
              'sent':sent}
213
    context = get_context(request, extra_context, **kwargs)
214
    return render_response(template_name,
215
                           context_instance = context)
216

    
217
@login_required
218
def edit_profile(request, template_name='profile.html', extra_context={}):
219
    """
220
    Allows a user to edit his/her profile.
221

222
    In case of GET request renders a form for displaying the user information.
223
    In case of POST updates the user informantion and redirects to ``next``
224
    url parameter if exists.
225

226
    If the user isn't logged in, redirects to settings.LOGIN_URL.
227

228
    **Arguments**
229

230
    ``template_name``
231
        A custom template to use. This is optional; if not specified,
232
        this will default to ``profile.html``.
233

234
    ``extra_context``
235
        An dictionary of variables to add to the template context.
236

237
    **Template:**
238

239
    profile.html or ``template_name`` keyword argument.
240
    """
241
    form = ProfileForm(instance=request.user)
242
    extra_context['next'] = request.GET.get('next')
243
    if request.method == 'POST':
244
        form = ProfileForm(request.POST, instance=request.user)
245
        if form.is_valid():
246
            try:
247
                form.save()
248
                msg = _('Profile has been updated successfully')
249
                messages.add_message(request, messages.SUCCESS, msg)
250
            except ValueError, ve:
251
                messages.add_message(request, messages.ERROR, ve)
252
        next = request.POST.get('next')
253
        if next:
254
            return redirect(next)
255
    return render_response(template_name,
256
                           form = form,
257
                           context_instance = get_context(request,
258
                                                          extra_context,
259
                                                          user=request.user))
260

    
261
def signup(request, template_name='signup.html', extra_context={}, backend=None):
262
    """
263
    Allows a user to create a local account.
264

265
    In case of GET request renders a form for providing the user information.
266
    In case of POST handles the signup.
267

268
    The user activation will be delegated to the backend specified by the ``backend`` keyword argument
269
    if present, otherwise to the ``astakos.im.backends.InvitationBackend``
270
    if settings.INVITATIONS_ENABLED is True or ``astakos.im.backends.SimpleBackend`` if not
271
    (see backends);
272

273
    Upon successful user creation if ``next`` url parameter is present the user is redirected there
274
    otherwise renders the same page with a success message.
275

276
    On unsuccessful creation, renders the same page with an error message.
277

278
    **Arguments**
279

280
    ``template_name``
281
        A custom template to use. This is optional; if not specified,
282
        this will default to ``signup.html``.
283

284
    ``extra_context``
285
        An dictionary of variables to add to the template context.
286

287
    **Template:**
288

289
    signup.html or ``template_name`` keyword argument.
290
    """
291
    try:
292
        if not backend:
293
            backend = get_backend(request)
294
        for provider in settings.IM_MODULES:
295
            extra_context['%s_form' % provider] = backend.get_signup_form(provider)
296
        if request.method == 'POST':
297
            provider = request.POST.get('provider')
298
            next = request.POST.get('next', '')
299
            form = extra_context['%s_form' % provider]
300
            if form.is_valid():
301
                if provider != 'local':
302
                    url = reverse('astakos.im.target.%s.login' % provider)
303
                    url = '%s?email=%s&next=%s' % (url, form.data['email'], next)
304
                    if backend.invitation:
305
                        url = '%s&code=%s' % (url, backend.invitation.code)
306
                    return redirect(url)
307
                else:
308
                    status, message, user = backend.signup(form)
309
                    if status == messages.SUCCESS:
310
                        if next:
311
                            return redirect(next)
312
                    messages.add_message(request, status, message)
313
    except (Invitation.DoesNotExist, ValueError), e:
314
        messages.add_message(request, messages.ERROR, e)
315
        for provider in settings.IM_MODULES:
316
            main = provider.capitalize() if provider == 'local' else 'ThirdParty'
317
            formclass = '%sUserCreationForm' % main
318
            extra_context['%s_form' % provider] = globals()[formclass]()
319
    return render_response(template_name,
320
                           context_instance=get_context(request, extra_context))
321

    
322
@login_required
323
def send_feedback(request, template_name='feedback.html', email_template_name='feedback_mail.txt', extra_context={}):
324
    """
325
    Allows a user to send feedback.
326

327
    In case of GET request renders a form for providing the feedback information.
328
    In case of POST sends an email to support team.
329

330
    If the user isn't logged in, redirects to settings.LOGIN_URL.
331

332
    **Arguments**
333

334
    ``template_name``
335
        A custom template to use. This is optional; if not specified,
336
        this will default to ``feedback.html``.
337

338
    ``extra_context``
339
        An dictionary of variables to add to the template context.
340

341
    **Template:**
342

343
    signup.html or ``template_name`` keyword argument.
344

345
    **Settings:**
346

347
    * DEFAULT_CONTACT_EMAIL: List of feedback recipients
348
    """
349
    if request.method == 'GET':
350
        form = FeedbackForm()
351
    if request.method == 'POST':
352
        if not request.user:
353
            return HttpResponse('Unauthorized', status=401)
354

    
355
        form = FeedbackForm(request.POST)
356
        if form.is_valid():
357
            sitename, sitedomain = get_current_site(request, use_https=request.is_secure())
358
            subject = _("Feedback from %s" % sitename)
359
            from_email = request.user.email
360
            recipient_list = [settings.DEFAULT_CONTACT_EMAIL % sitename.lower()]
361
            content = render_to_string(email_template_name, {
362
                        'message': form.cleaned_data['feedback_msg'],
363
                        'data': form.cleaned_data['feedback_data'],
364
                        'request': request})
365

    
366
            try:
367
                send_mail(subject, content, from_email, recipient_list)
368
                message = _('Feedback successfully sent')
369
                status = messages.SUCCESS
370
            except (SMTPException, socket.error) as e:
371
                status = messages.ERROR
372
                message = getattr(e, 'strerror', '')
373
            messages.add_message(request, status, message)
374
    return render_response(template_name,
375
                           form = form,
376
                           context_instance = get_context(request, extra_context))
377

    
378
def create_user(request, form, backend=None, post_data={}, next = None, template_name='login.html', extra_context={}):
379
    try:
380
        if not backend:
381
            backend = get_backend(request)
382
        if form.is_valid():
383
            status, message, user = backend.signup(form)
384
            if status == messages.SUCCESS:
385
                for k,v in post_data.items():
386
                    setattr(user,k, v)
387
                user.save()
388
                if next:
389
                    return redirect(next)
390
            messages.add_message(request, status, message)
391
        else:
392
            messages.add_message(request, messages.ERROR, form.errors)
393
    except (Invitation.DoesNotExist, ValueError), e:
394
        messages.add_message(request, messages.ERROR, e)
395
    return render_response(template_name,
396
                           form = LocalUserCreationForm(),
397
                           context_instance=get_context(request, extra_context))
398

    
399
def user_logout(request):
400
    response = HttpResponse()
401
    response.delete_cookie(settings.COOKIE_NAME)
402
    response['Location'] = reverse('django.contrib.auth.views.logout')
403
    response.status_code = 302
404
    return response