Statistics
| Branch: | Tag: | Revision:

root / astakos / im / views.py @ ce86cd44

History | View | Annotate | Download (14.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.util import isoformat, get_context, get_current_site
66
from astakos.im.backends import get_backend
67
from astakos.im.forms import ProfileForm, FeedbackForm, LoginForm
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
    if request.GET.get('format') == 'json':
208
        sent = [{'email': inv.username,
209
                 'realname': inv.realname,
210
                 'is_accepted': inv.is_accepted}
211
                    for inv in inviter.invitations_sent.all()]
212
        rep = {'invitations': inviter.invitations, 'sent': sent}
213
        return HttpResponse(json.dumps(rep))
214
    
215
    kwargs = {'user': inviter}
216
    context = get_context(request, extra_context, **kwargs)
217
    return render_response(template_name,
218
                           context_instance = context)
219

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

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

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