Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 373daf6a

History | View | Annotate | Download (25.6 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 logging
35
import socket
36

    
37
from smtplib import SMTPException
38
from urllib import quote
39
from functools import wraps
40

    
41
from django.core.mail import send_mail
42
from django.http import HttpResponse, HttpResponseBadRequest
43
from django.shortcuts import redirect
44
from django.template.loader import render_to_string
45
from django.utils.translation import ugettext as _
46
from django.core.urlresolvers import reverse
47
from django.contrib.auth.decorators import login_required
48
from django.contrib import messages
49
from django.db import transaction
50
from django.utils.http import urlencode
51
from django.http import HttpResponseRedirect, HttpResponseBadRequest
52
from django.db.utils import IntegrityError
53
from django.contrib.auth.views import password_change
54
from django.core.exceptions import ValidationError
55
from django.db.models import Q
56
from django.forms.models import inlineformset_factory
57
from django.forms.models import inlineformset_factory
58
from django.views.generic.create_update import *
59
from django.views.generic.list_detail import *
60

    
61
from astakos.im.models import AstakosUser, Invitation, ApprovalTerms, AstakosGroup
62
from astakos.im.activation_backends import get_backend, SimpleBackend
63
from astakos.im.util import get_context, prepare_response, set_cookie, get_query
64
from astakos.im.forms import *
65
from astakos.im.functions import send_greeting, send_feedback, SendMailError, \
66
    invite as invite_func, logout as auth_logout, activate as activate_func, switch_account_to_shibboleth
67
from astakos.im.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, LOGOUT_NEXT, LOGGING_LEVEL
68

    
69
logger = logging.getLogger(__name__)
70

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

    
86

    
87
def requires_anonymous(func):
88
    """
89
    Decorator checkes whether the request.user is not Anonymous and in that case
90
    redirects to `logout`.
91
    """
92
    @wraps(func)
93
    def wrapper(request, *args):
94
        if not request.user.is_anonymous():
95
            next = urlencode({'next': request.build_absolute_uri()})
96
            logout_uri = reverse(logout) + '?' + next
97
            return HttpResponseRedirect(logout_uri)
98
        return func(request, *args)
99
    return wrapper
100

    
101
def signed_terms_required(func):
102
    """
103
    Decorator checkes whether the request.user is Anonymous and in that case
104
    redirects to `logout`.
105
    """
106
    @wraps(func)
107
    def wrapper(request, *args, **kwargs):
108
        if request.user.is_authenticated() and not request.user.signed_terms():
109
            params = urlencode({'next': request.build_absolute_uri(),
110
                              'show_form':''})
111
            terms_uri = reverse('latest_terms') + '?' + params
112
            return HttpResponseRedirect(terms_uri)
113
        return func(request, *args, **kwargs)
114
    return wrapper
115

    
116
@signed_terms_required
117
def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context={}):
118
    """
119
    If there is logged on user renders the profile page otherwise renders login page.
120

121
    **Arguments**
122

123
    ``login_template_name``
124
        A custom login template to use. This is optional; if not specified,
125
        this will default to ``im/login.html``.
126

127
    ``profile_template_name``
128
        A custom profile template to use. This is optional; if not specified,
129
        this will default to ``im/profile.html``.
130

131
    ``extra_context``
132
        An dictionary of variables to add to the template context.
133

134
    **Template:**
135

136
    im/profile.html or im/login.html or ``template_name`` keyword argument.
137

138
    """
139
    template_name = login_template_name
140
    if request.user.is_authenticated():
141
        return HttpResponseRedirect(reverse('edit_profile'))
142
    return render_response(template_name,
143
                           login_form = LoginForm(request=request),
144
                           context_instance = get_context(request, extra_context))
145

    
146
@login_required
147
@signed_terms_required
148
@transaction.commit_manually
149
def invite(request, template_name='im/invitations.html', extra_context={}):
150
    """
151
    Allows a user to invite somebody else.
152

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

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

160
    If the user isn't logged in, redirects to settings.LOGIN_URL.
161

162
    **Arguments**
163

164
    ``template_name``
165
        A custom template to use. This is optional; if not specified,
166
        this will default to ``im/invitations.html``.
167

168
    ``extra_context``
169
        An dictionary of variables to add to the template context.
170

171
    **Template:**
172

173
    im/invitations.html or ``template_name`` keyword argument.
174

175
    **Settings:**
176

177
    The view expectes the following settings are defined:
178

179
    * LOGIN_URL: login uri
180
    * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
181
    * ASTAKOS_DEFAULT_FROM_EMAIL: from email
182
    """
183
    status = None
184
    message = None
185
    form = InvitationForm()
186
    
187
    inviter = request.user
188
    if request.method == 'POST':
189
        form = InvitationForm(request.POST)
190
        if inviter.invitations > 0:
191
            if form.is_valid():
192
                try:
193
                    invitation = form.save()
194
                    invite_func(invitation, inviter)
195
                    message = _('Invitation sent to %s' % invitation.username)
196
                    messages.success(request, message)
197
                except SendMailError, e:
198
                    message = e.message
199
                    messages.error(request, message)
200
                    transaction.rollback()
201
                except BaseException, e:
202
                    message = _('Something went wrong.')
203
                    messages.error(request, message)
204
                    logger.exception(e)
205
                    transaction.rollback()
206
                else:
207
                    transaction.commit()
208
        else:
209
            message = _('No invitations left')
210
            messages.error(request, message)
211

    
212
    sent = [{'email': inv.username,
213
             'realname': inv.realname,
214
             'is_consumed': inv.is_consumed}
215
             for inv in request.user.invitations_sent.all()]
216
    kwargs = {'inviter': inviter,
217
              'sent':sent}
218
    context = get_context(request, extra_context, **kwargs)
219
    return render_response(template_name,
220
                           invitation_form = form,
221
                           context_instance = context)
222

    
223
@login_required
224
@signed_terms_required
225
def edit_profile(request, template_name='im/profile.html', extra_context={}):
226
    """
227
    Allows a user to edit his/her profile.
228

229
    In case of GET request renders a form for displaying the user information.
230
    In case of POST updates the user informantion and redirects to ``next``
231
    url parameter if exists.
232

233
    If the user isn't logged in, redirects to settings.LOGIN_URL.
234

235
    **Arguments**
236

237
    ``template_name``
238
        A custom template to use. This is optional; if not specified,
239
        this will default to ``im/profile.html``.
240

241
    ``extra_context``
242
        An dictionary of variables to add to the template context.
243

244
    **Template:**
245

246
    im/profile.html or ``template_name`` keyword argument.
247

248
    **Settings:**
249

250
    The view expectes the following settings are defined:
251

252
    * LOGIN_URL: login uri
253
    """
254
    form = ProfileForm(instance=request.user)
255
    extra_context['next'] = request.GET.get('next')
256
    reset_cookie = False
257
    if request.method == 'POST':
258
        form = ProfileForm(request.POST, instance=request.user)
259
        if form.is_valid():
260
            try:
261
                prev_token = request.user.auth_token
262
                user = form.save()
263
                reset_cookie = user.auth_token != prev_token
264
                form = ProfileForm(instance=user)
265
                next = request.POST.get('next')
266
                if next:
267
                    return redirect(next)
268
                msg = _('Profile has been updated successfully')
269
                messages.success(request, msg)
270
            except ValueError, ve:
271
                messages.success(request, ve)
272
    elif request.method == "GET":
273
        request.user.is_verified = True
274
        request.user.save()
275
    return render_response(template_name,
276
                           reset_cookie = reset_cookie,
277
                           profile_form = form,
278
                           context_instance = get_context(request,
279
                                                          extra_context))
280

    
281
def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context={}, backend=None):
282
    """
283
    Allows a user to create a local account.
284

285
    In case of GET request renders a form for entering the user information.
286
    In case of POST handles the signup.
287

288
    The user activation will be delegated to the backend specified by the ``backend`` keyword argument
289
    if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
290
    if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
291
    (see activation_backends);
292
    
293
    Upon successful user creation, if ``next`` url parameter is present the user is redirected there
294
    otherwise renders the same page with a success message.
295
    
296
    On unsuccessful creation, renders ``template_name`` with an error message.
297
    
298
    **Arguments**
299
    
300
    ``template_name``
301
        A custom template to render. This is optional;
302
        if not specified, this will default to ``im/signup.html``.
303

304
    ``on_success``
305
        A custom template to render in case of success. This is optional;
306
        if not specified, this will default to ``im/signup_complete.html``.
307

308
    ``extra_context``
309
        An dictionary of variables to add to the template context.
310

311
    **Template:**
312
    
313
    im/signup.html or ``template_name`` keyword argument.
314
    im/signup_complete.html or ``on_success`` keyword argument. 
315
    """
316
    if request.user.is_authenticated():
317
        return HttpResponseRedirect(reverse('edit_profile'))
318
    
319
    provider = get_query(request).get('provider', 'local')
320
    try:
321
        if not backend:
322
            backend = get_backend(request)
323
        form = backend.get_signup_form(provider)
324
    except Exception, e:
325
        form = SimpleBackend(request).get_signup_form(provider)
326
        messages.error(request, e)
327
    if request.method == 'POST':
328
        if form.is_valid():
329
            user = form.save(commit=False)
330
            try:
331
                result = backend.handle_activation(user)
332
                status = messages.SUCCESS
333
                message = result.message
334
                user.save()
335
                if 'additional_email' in form.cleaned_data:
336
                    additional_email = form.cleaned_data['additional_email']
337
                    if additional_email != user.email:
338
                        user.additionalmail_set.create(email=additional_email)
339
                        msg = 'Additional email: %s saved for user %s.' % (additional_email, user.email)
340
                        logger._log(LOGGING_LEVEL, msg, [])
341
                if user and user.is_active:
342
                    next = request.POST.get('next', '')
343
                    return prepare_response(request, user, next=next)
344
                messages.add_message(request, status, message)
345
                return render_response(on_success,
346
                                       context_instance=get_context(request, extra_context))
347
            except SendMailError, e:
348
                message = e.message
349
                messages.error(request, message)
350
            except BaseException, e:
351
                message = _('Something went wrong.')
352
                messages.error(request, message)
353
                logger.exception(e)
354
    return render_response(template_name,
355
                           signup_form = form,
356
                           provider = provider,
357
                           context_instance=get_context(request, extra_context))
358

    
359
@login_required
360
@signed_terms_required
361
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context={}):
362
    """
363
    Allows a user to send feedback.
364

365
    In case of GET request renders a form for providing the feedback information.
366
    In case of POST sends an email to support team.
367

368
    If the user isn't logged in, redirects to settings.LOGIN_URL.
369

370
    **Arguments**
371

372
    ``template_name``
373
        A custom template to use. This is optional; if not specified,
374
        this will default to ``im/feedback.html``.
375

376
    ``extra_context``
377
        An dictionary of variables to add to the template context.
378

379
    **Template:**
380

381
    im/signup.html or ``template_name`` keyword argument.
382

383
    **Settings:**
384

385
    * LOGIN_URL: login uri
386
    * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
387
    """
388
    if request.method == 'GET':
389
        form = FeedbackForm()
390
    if request.method == 'POST':
391
        if not request.user:
392
            return HttpResponse('Unauthorized', status=401)
393

    
394
        form = FeedbackForm(request.POST)
395
        if form.is_valid():
396
            msg = form.cleaned_data['feedback_msg']
397
            data = form.cleaned_data['feedback_data']
398
            try:
399
                send_feedback(msg, data, request.user, email_template_name)
400
            except SendMailError, e:
401
                status = messages.ERROR
402
                messages.error(request, message)
403
            else:
404
                message = _('Feedback successfully sent')
405
                messages.succeess(request, message)
406
    return render_response(template_name,
407
                           feedback_form = form,
408
                           context_instance = get_context(request, extra_context))
409

    
410
@signed_terms_required
411
def logout(request, template='registration/logged_out.html', extra_context={}):
412
    """
413
    Wraps `django.contrib.auth.logout` and delete the cookie.
414
    """
415
    response = HttpResponse()
416
    if request.user.is_authenticated():
417
        email = request.user.email
418
        auth_logout(request)
419
        response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
420
        msg = 'Cookie deleted for %s' % email
421
        logger._log(LOGGING_LEVEL, msg, [])
422
    next = request.GET.get('next')
423
    if next:
424
        response['Location'] = next
425
        response.status_code = 302
426
        return response
427
    elif LOGOUT_NEXT:
428
        response['Location'] = LOGOUT_NEXT
429
        response.status_code = 301
430
        return response
431
    messages.success(request, _('You have successfully logged out.'))
432
    context = get_context(request, extra_context)
433
    response.write(render_to_string(template, context_instance=context))
434
    return response
435

    
436
@transaction.commit_manually
437
def activate(request, greeting_email_template_name='im/welcome_email.txt', helpdesk_email_template_name='im/helpdesk_notification.txt'):
438
    """
439
    Activates the user identified by the ``auth`` request parameter, sends a welcome email
440
    and renews the user token.
441

442
    The view uses commit_manually decorator in order to ensure the user state will be updated
443
    only if the email will be send successfully.
444
    """
445
    token = request.GET.get('auth')
446
    next = request.GET.get('next')
447
    try:
448
        user = AstakosUser.objects.get(auth_token=token)
449
    except AstakosUser.DoesNotExist:
450
        return HttpResponseBadRequest(_('No such user'))
451
    
452
    if user.is_active:
453
        message = _('Account already active.')
454
        messages.error(request, message)
455
        return index(request)
456
        
457
    try:
458
        local_user = AstakosUser.objects.get(~Q(id = user.id), email=user.email, is_active=True)
459
    except AstakosUser.DoesNotExist:
460
        try:
461
            activate_func(user, greeting_email_template_name, helpdesk_email_template_name, verify_email=True)
462
            response = prepare_response(request, user, next, renew=True)
463
            transaction.commit()
464
            return response
465
        except SendMailError, e:
466
            message = e.message
467
            messages.error(request, message)
468
            transaction.rollback()
469
            return index(request)
470
        except BaseException, e:
471
            message = _('Something went wrong.')
472
            messages.error(request, message)
473
            logger.exception(e)
474
            transaction.rollback()
475
            return index(request)
476
    else:
477
        try:
478
            user = switch_account_to_shibboleth(user, local_user, greeting_email_template_name)
479
            response = prepare_response(request, user, next, renew=True)
480
            transaction.commit()
481
            return response
482
        except SendMailError, e:
483
            message = e.message
484
            messages.error(request, message)
485
            transaction.rollback()
486
            return index(request)
487
        except BaseException, e:
488
            message = _('Something went wrong.')
489
            messages.error(request, message)
490
            logger.exception(e)
491
            transaction.rollback()
492
            return index(request)
493

    
494
def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context={}):
495
    term = None
496
    terms = None
497
    if not term_id:
498
        try:
499
            term = ApprovalTerms.objects.order_by('-id')[0]
500
        except IndexError:
501
            pass
502
    else:
503
        try:
504
             term = ApprovalTerms.objects.get(id=term_id)
505
        except ApprovalTermDoesNotExist, e:
506
            pass
507

    
508
    if not term:
509
        return HttpResponseRedirect(reverse('index'))
510
    f = open(term.location, 'r')
511
    terms = f.read()
512

    
513
    if request.method == 'POST':
514
        next = request.POST.get('next')
515
        if not next:
516
            next = reverse('index')
517
        form = SignApprovalTermsForm(request.POST, instance=request.user)
518
        if not form.is_valid():
519
            return render_response(template_name,
520
                           terms = terms,
521
                           approval_terms_form = form,
522
                           context_instance = get_context(request, extra_context))
523
        user = form.save()
524
        return HttpResponseRedirect(next)
525
    else:
526
        form = None
527
        if request.user.is_authenticated() and not request.user.signed_terms():
528
            form = SignApprovalTermsForm(instance=request.user)
529
        return render_response(template_name,
530
                               terms = terms,
531
                               approval_terms_form = form,
532
                               context_instance = get_context(request, extra_context))
533

    
534
@signed_terms_required
535
def change_password(request):
536
    return password_change(request,
537
                            post_change_redirect=reverse('edit_profile'),
538
                            password_change_form=ExtendedPasswordChangeForm)
539

    
540
@signed_terms_required
541
@login_required
542
@transaction.commit_manually
543
def change_email(request, activation_key=None,
544
                 email_template_name='registration/email_change_email.txt',
545
                 form_template_name='registration/email_change_form.html',
546
                 confirm_template_name='registration/email_change_done.html',
547
                 extra_context={}):
548
    if activation_key:
549
        try:
550
            user = EmailChange.objects.change_email(activation_key)
551
            if request.user.is_authenticated() and request.user == user:
552
                msg = _('Email changed successfully.')
553
                messages.success(request, msg)
554
                auth_logout(request)
555
                response = prepare_response(request, user)
556
                transaction.commit()
557
                return response
558
        except ValueError, e:
559
            messages.error(request, e)
560
        return render_response(confirm_template_name,
561
                               modified_user = user if 'user' in locals() else None,
562
                               context_instance = get_context(request,
563
                                                              extra_context))
564
    
565
    if not request.user.is_authenticated():
566
        path = quote(request.get_full_path())
567
        url = request.build_absolute_uri(reverse('index'))
568
        return HttpResponseRedirect(url + '?next=' + path)
569
    form = EmailChangeForm(request.POST or None)
570
    if request.method == 'POST' and form.is_valid():
571
        try:
572
            ec = form.save(email_template_name, request)
573
        except SendMailError, e:
574
            msg = e
575
            messages.error(request, msg)
576
            transaction.rollback()
577
        except IntegrityError, e:
578
            msg = _('There is already a pending change email request.')
579
            messages.error(request, msg)
580
        else:
581
            msg = _('Change email request has been registered succefully.\
582
                    You are going to receive a verification email in the new address.')
583
            messages.success(request, msg)
584
            transaction.commit()
585
    return render_response(form_template_name,
586
                           form = form,
587
                           context_instance = get_context(request,
588
                                                          extra_context))
589

    
590
@signed_terms_required
591
def group_add(request):
592
    return create_object(request,
593
                            form_class=get_astakos_group_creation_form(request),
594
                            login_required = True,
595
                            post_save_redirect = '/im/group/%(id)s/')
596

    
597
@signed_terms_required
598
@login_required
599
def group_list(request):
600
    relation = get_query(request).get('relation', 'member')
601
    if relation == 'member':
602
        list = AstakosGroup.objects.filter(membership__person=request.user)
603
    else:
604
        list = AstakosGroup.objects.filter(owner__id=request.user.id)
605
    return object_list(request, queryset=list)
606

    
607
@signed_terms_required
608
@login_required
609
def group_detail(request, group_id):
610
    try:
611
        group = AstakosGroup.objects.select_related().get(id=group_id)
612
    except AstakosGroup.DoesNotExist:
613
        return HttpResponseBadRequest(_('Invalid group.'))
614
    d = {}
615
    related_resources = group.policy.through.objects
616
    for r in group.policy.all():
617
        d[r.name] = related_resources.get(resource__id=r.id, 
618
                                            group__id=group_id).limit
619
    members = map(lambda m:{m.person.realname:m.is_approved}, group.membership_set.all())
620
    return object_detail(request,
621
                            AstakosGroup.objects.all(),
622
                            object_id=group_id,
623
                            extra_context = {'quota':d,
624
                                             'members':members,
625
                                             'form':get_astakos_group_policy_creation_form(group)})
626

    
627
@signed_terms_required
628
@login_required
629
def group_policies_list(request, group_id):
630
    list = AstakosGroupQuota.objects.filter(group__id=group_id)
631
    return object_list(request, queryset=list)
632

    
633
@signed_terms_required
634
def group_policies_add(request, group_id):
635
    try:
636
        group = AstakosGroup.objects.select_related().get(id=group_id)
637
    except AstakosGroup.DoesNotExist:
638
        return HttpResponseBadRequest(_('Invalid group.'))
639
    d = {}
640
    for resource in group.policy.all():
641
        d[resource.name] = group.policy.through.objects.get(resource__id=resource.id,
642
                                                            group__id=group_id).limit
643
    return create_object(request,
644
                            form_class=get_astakos_group_policy_creation_form(group),
645
                            login_required=True,
646
                            template_name = 'im/astakosgroup_detail.html',
647
                            post_save_redirect = reverse('group_detail', kwargs=dict(group_id=group_id)),
648
                            extra_context = {'group':group,
649
                                             'quota':d})
650
@signed_terms_required
651
@login_required
652
def group_approval_request(request, group_id):
653
    return HttpResponse()
654