Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 64b5136c

History | View | Annotate | Download (48.1 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 calendar
36
import inflect
37

    
38
engine = inflect.engine()
39

    
40
from urllib import quote
41
from functools import wraps
42
from datetime import datetime
43

    
44
from django_tables2 import RequestConfig
45

    
46
from django.shortcuts import get_object_or_404
47
from django.contrib import messages
48
from django.contrib.auth.decorators import login_required
49
from django.core.urlresolvers import reverse
50
from django.db import transaction
51
from django.db.utils import IntegrityError
52
from django.http import (
53
    HttpResponse, HttpResponseBadRequest,
54
    HttpResponseForbidden, HttpResponseRedirect,
55
    HttpResponseBadRequest, Http404)
56
from django.shortcuts import redirect
57
from django.template import RequestContext, loader as template_loader
58
from django.utils.http import urlencode
59
from django.utils.safestring import mark_safe
60
from django.utils.translation import ugettext as _
61
from django.views.generic.create_update import (
62
    apply_extra_context, lookup_object, delete_object, get_model_and_form_class)
63
from django.views.generic.list_detail import object_list, object_detail
64
from django.core.xheaders import populate_xheaders
65
from django.core.exceptions import ValidationError, PermissionDenied
66
from django.template.loader import render_to_string
67
from django.views.decorators.http import require_http_methods
68
from django.db.models import Q
69
from django.core.exceptions import PermissionDenied
70
from django.utils import simplejson as json
71

    
72
import astakos.im.messages as astakos_messages
73

    
74
from astakos.im.activation_backends import get_backend, SimpleBackend
75
from astakos.im import tables
76
from astakos.im.models import (
77
    AstakosUser, ApprovalTerms,
78
    EmailChange, RESOURCE_SEPARATOR,
79
    AstakosUserAuthProvider, PendingThirdPartyUser,
80
    ProjectApplication, ProjectMembership, Project)
81
from astakos.im.util import (
82
    get_context, prepare_response, get_query, restrict_next)
83
from astakos.im.forms import (
84
    LoginForm, InvitationForm, ProfileForm,
85
    FeedbackForm, SignApprovalTermsForm,
86
    EmailChangeForm,
87
    ProjectApplicationForm, ProjectSortForm,
88
    AddProjectMembersForm, ProjectSearchForm,
89
    ProjectMembersSortForm)
90
from astakos.im.functions import (
91
    send_feedback, SendMailError,
92
    logout as auth_logout,
93
    activate as activate_func,
94
    invite,
95
    send_activation as send_activation_func,
96
    SendNotificationError,
97
    accept_membership, reject_membership, remove_membership,
98
    leave_project, join_project, enroll_member)
99
from astakos.im.settings import (
100
    COOKIE_DOMAIN, LOGOUT_NEXT,
101
    LOGGING_LEVEL, PAGINATE_BY,
102
    RESOURCES_PRESENTATION_DATA, PAGINATE_BY_ALL,
103
    MODERATION_ENABLED)
104
from astakos.im.api import get_services_dict
105
from astakos.im import settings as astakos_settings
106
from astakos.im.api.callpoint import AstakosCallpoint
107
from astakos.im import auth_providers
108

    
109
logger = logging.getLogger(__name__)
110

    
111
callpoint = AstakosCallpoint()
112

    
113
def render_response(template, tab=None, status=200, context_instance=None, **kwargs):
114
    """
115
    Calls ``django.template.loader.render_to_string`` with an additional ``tab``
116
    keyword argument and returns an ``django.http.HttpResponse`` with the
117
    specified ``status``.
118
    """
119
    if tab is None:
120
        tab = template.partition('_')[0].partition('.html')[0]
121
    kwargs.setdefault('tab', tab)
122
    html = template_loader.render_to_string(
123
        template, kwargs, context_instance=context_instance)
124
    response = HttpResponse(html, status=status)
125
    return response
126

    
127
def requires_auth_provider(provider_id, **perms):
128
    """
129
    """
130
    def decorator(func, *args, **kwargs):
131
        @wraps(func)
132
        def wrapper(request, *args, **kwargs):
133
            provider = auth_providers.get_provider(provider_id)
134

    
135
            if not provider or not provider.is_active():
136
                raise PermissionDenied
137

    
138
            if provider:
139
                for pkey, value in perms.iteritems():
140
                    attr = 'is_available_for_%s' % pkey.lower()
141
                    if getattr(provider, attr)() != value:
142
                        #TODO: add session message
143
                        return HttpResponseRedirect(reverse('login'))
144
            return func(request, *args)
145
        return wrapper
146
    return decorator
147

    
148

    
149
def requires_anonymous(func):
150
    """
151
    Decorator checkes whether the request.user is not Anonymous and in that case
152
    redirects to `logout`.
153
    """
154
    @wraps(func)
155
    def wrapper(request, *args):
156
        if not request.user.is_anonymous():
157
            next = urlencode({'next': request.build_absolute_uri()})
158
            logout_uri = reverse(logout) + '?' + next
159
            return HttpResponseRedirect(logout_uri)
160
        return func(request, *args)
161
    return wrapper
162

    
163

    
164
def signed_terms_required(func):
165
    """
166
    Decorator checks whether the request.user is Anonymous and in that case
167
    redirects to `logout`.
168
    """
169
    @wraps(func)
170
    def wrapper(request, *args, **kwargs):
171
        if request.user.is_authenticated() and not request.user.signed_terms:
172
            params = urlencode({'next': request.build_absolute_uri(),
173
                                'show_form': ''})
174
            terms_uri = reverse('latest_terms') + '?' + params
175
            return HttpResponseRedirect(terms_uri)
176
        return func(request, *args, **kwargs)
177
    return wrapper
178

    
179

    
180
def required_auth_methods_assigned(only_warn=False):
181
    """
182
    Decorator that checks whether the request.user has all required auth providers
183
    assigned.
184
    """
185
    required_providers = auth_providers.REQUIRED_PROVIDERS.keys()
186

    
187
    def decorator(func):
188
        if not required_providers:
189
            return func
190

    
191
        @wraps(func)
192
        def wrapper(request, *args, **kwargs):
193
            if request.user.is_authenticated():
194
                for required in required_providers:
195
                    if not request.user.has_auth_provider(required):
196
                        provider = auth_providers.get_provider(required)
197
                        if only_warn:
198
                            messages.error(request,
199
                                           _(astakos_messages.AUTH_PROVIDER_REQUIRED  % {
200
                                               'provider': provider.get_title_display}))
201
                        else:
202
                            return HttpResponseRedirect(reverse('edit_profile'))
203
            return func(request, *args, **kwargs)
204
        return wrapper
205
    return decorator
206

    
207

    
208
def valid_astakos_user_required(func):
209
    return signed_terms_required(required_auth_methods_assigned()(login_required(func)))
210

    
211

    
212
@require_http_methods(["GET", "POST"])
213
@signed_terms_required
214
def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context=None):
215
    """
216
    If there is logged on user renders the profile page otherwise renders login page.
217

218
    **Arguments**
219

220
    ``login_template_name``
221
        A custom login template to use. This is optional; if not specified,
222
        this will default to ``im/login.html``.
223

224
    ``profile_template_name``
225
        A custom profile template to use. This is optional; if not specified,
226
        this will default to ``im/profile.html``.
227

228
    ``extra_context``
229
        An dictionary of variables to add to the template context.
230

231
    **Template:**
232

233
    im/profile.html or im/login.html or ``template_name`` keyword argument.
234

235
    """
236
    extra_context = extra_context or {}
237
    template_name = login_template_name
238
    if request.user.is_authenticated():
239
        return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
240

    
241
    third_party_token = request.GET.get('key', False)
242
    if third_party_token:
243
        messages.info(request, astakos_messages.AUTH_PROVIDER_LOGIN_TO_ADD)
244

    
245
    return render_response(
246
        template_name,
247
        login_form = LoginForm(request=request),
248
        context_instance = get_context(request, extra_context)
249
    )
250

    
251

    
252
@require_http_methods(["GET", "POST"])
253
@valid_astakos_user_required
254
@transaction.commit_manually
255
def invite(request, template_name='im/invitations.html', extra_context=None):
256
    """
257
    Allows a user to invite somebody else.
258

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

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

266
    If the user isn't logged in, redirects to settings.LOGIN_URL.
267

268
    **Arguments**
269

270
    ``template_name``
271
        A custom template to use. This is optional; if not specified,
272
        this will default to ``im/invitations.html``.
273

274
    ``extra_context``
275
        An dictionary of variables to add to the template context.
276

277
    **Template:**
278

279
    im/invitations.html or ``template_name`` keyword argument.
280

281
    **Settings:**
282

283
    The view expectes the following settings are defined:
284

285
    * LOGIN_URL: login uri
286
    """
287
    extra_context = extra_context or {}
288
    status = None
289
    message = None
290
    form = InvitationForm()
291

    
292
    inviter = request.user
293
    if request.method == 'POST':
294
        form = InvitationForm(request.POST)
295
        if inviter.invitations > 0:
296
            if form.is_valid():
297
                try:
298
                    email = form.cleaned_data.get('username')
299
                    realname = form.cleaned_data.get('realname')
300
                    invite(inviter, email, realname)
301
                    message = _(astakos_messages.INVITATION_SENT) % locals()
302
                    messages.success(request, message)
303
                except SendMailError, e:
304
                    message = e.message
305
                    messages.error(request, message)
306
                    transaction.rollback()
307
                except BaseException, e:
308
                    message = _(astakos_messages.GENERIC_ERROR)
309
                    messages.error(request, message)
310
                    logger.exception(e)
311
                    transaction.rollback()
312
                else:
313
                    transaction.commit()
314
        else:
315
            message = _(astakos_messages.MAX_INVITATION_NUMBER_REACHED)
316
            messages.error(request, message)
317

    
318
    sent = [{'email': inv.username,
319
             'realname': inv.realname,
320
             'is_consumed': inv.is_consumed}
321
            for inv in request.user.invitations_sent.all()]
322
    kwargs = {'inviter': inviter,
323
              'sent': sent}
324
    context = get_context(request, extra_context, **kwargs)
325
    return render_response(template_name,
326
                           invitation_form=form,
327
                           context_instance=context)
328

    
329

    
330
@require_http_methods(["GET", "POST"])
331
@required_auth_methods_assigned(only_warn=True)
332
@login_required
333
@signed_terms_required
334
def edit_profile(request, template_name='im/profile.html', extra_context=None):
335
    """
336
    Allows a user to edit his/her profile.
337

338
    In case of GET request renders a form for displaying the user information.
339
    In case of POST updates the user informantion and redirects to ``next``
340
    url parameter if exists.
341

342
    If the user isn't logged in, redirects to settings.LOGIN_URL.
343

344
    **Arguments**
345

346
    ``template_name``
347
        A custom template to use. This is optional; if not specified,
348
        this will default to ``im/profile.html``.
349

350
    ``extra_context``
351
        An dictionary of variables to add to the template context.
352

353
    **Template:**
354

355
    im/profile.html or ``template_name`` keyword argument.
356

357
    **Settings:**
358

359
    The view expectes the following settings are defined:
360

361
    * LOGIN_URL: login uri
362
    """
363
    extra_context = extra_context or {}
364
    form = ProfileForm(
365
        instance=request.user,
366
        session_key=request.session.session_key
367
    )
368
    extra_context['next'] = request.GET.get('next')
369
    if request.method == 'POST':
370
        form = ProfileForm(
371
            request.POST,
372
            instance=request.user,
373
            session_key=request.session.session_key
374
        )
375
        if form.is_valid():
376
            try:
377
                prev_token = request.user.auth_token
378
                user = form.save()
379
                form = ProfileForm(
380
                    instance=user,
381
                    session_key=request.session.session_key
382
                )
383
                next = restrict_next(
384
                    request.POST.get('next'),
385
                    domain=COOKIE_DOMAIN
386
                )
387
                if next:
388
                    return redirect(next)
389
                msg = _(astakos_messages.PROFILE_UPDATED)
390
                messages.success(request, msg)
391
            except ValueError, ve:
392
                messages.success(request, ve)
393
    elif request.method == "GET":
394
        request.user.is_verified = True
395
        request.user.save()
396

    
397
    # existing providers
398
    user_providers = request.user.get_active_auth_providers()
399

    
400
    # providers that user can add
401
    user_available_providers = request.user.get_available_auth_providers()
402

    
403
    extra_context['services'] = get_services_dict()
404
    return render_response(template_name,
405
                           profile_form = form,
406
                           user_providers = user_providers,
407
                           user_available_providers = user_available_providers,
408
                           context_instance = get_context(request,
409
                                                          extra_context))
410

    
411

    
412
@transaction.commit_manually
413
@require_http_methods(["GET", "POST"])
414
def signup(request, template_name='im/signup.html', on_success='index', extra_context=None, backend=None):
415
    """
416
    Allows a user to create a local account.
417

418
    In case of GET request renders a form for entering the user information.
419
    In case of POST handles the signup.
420

421
    The user activation will be delegated to the backend specified by the ``backend`` keyword argument
422
    if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
423
    if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
424
    (see activation_backends);
425

426
    Upon successful user creation, if ``next`` url parameter is present the user is redirected there
427
    otherwise renders the same page with a success message.
428

429
    On unsuccessful creation, renders ``template_name`` with an error message.
430

431
    **Arguments**
432

433
    ``template_name``
434
        A custom template to render. This is optional;
435
        if not specified, this will default to ``im/signup.html``.
436

437
    ``extra_context``
438
        An dictionary of variables to add to the template context.
439

440
    ``on_success``
441
        Resolvable view name to redirect on registration success.
442

443
    **Template:**
444

445
    im/signup.html or ``template_name`` keyword argument.
446
    """
447
    extra_context = extra_context or {}
448
    if request.user.is_authenticated():
449
        return HttpResponseRedirect(reverse('edit_profile'))
450

    
451
    provider = get_query(request).get('provider', 'local')
452
    if not auth_providers.get_provider(provider).is_available_for_create():
453
        raise PermissionDenied
454

    
455
    id = get_query(request).get('id')
456
    try:
457
        instance = AstakosUser.objects.get(id=id) if id else None
458
    except AstakosUser.DoesNotExist:
459
        instance = None
460

    
461
    third_party_token = request.REQUEST.get('third_party_token', None)
462
    if third_party_token:
463
        pending = get_object_or_404(PendingThirdPartyUser,
464
                                    token=third_party_token)
465
        provider = pending.provider
466
        instance = pending.get_user_instance()
467

    
468
    try:
469
        if not backend:
470
            backend = get_backend(request)
471
        form = backend.get_signup_form(provider, instance)
472
    except Exception, e:
473
        form = SimpleBackend(request).get_signup_form(provider)
474
        messages.error(request, e)
475
    if request.method == 'POST':
476
        if form.is_valid():
477
            user = form.save(commit=False)
478

    
479
            # delete previously unverified accounts
480
            if AstakosUser.objects.user_exists(user.email):
481
                AstakosUser.objects.get_by_identifier(user.email).delete()
482

    
483
            try:
484
                result = backend.handle_activation(user)
485
                status = messages.SUCCESS
486
                message = result.message
487

    
488
                form.store_user(user, request)
489

    
490
                if 'additional_email' in form.cleaned_data:
491
                    additional_email = form.cleaned_data['additional_email']
492
                    if additional_email != user.email:
493
                        user.additionalmail_set.create(email=additional_email)
494
                        msg = 'Additional email: %s saved for user %s.' % (
495
                            additional_email,
496
                            user.email
497
                        )
498
                        logger._log(LOGGING_LEVEL, msg, [])
499

    
500
                if user and user.is_active:
501
                    next = request.POST.get('next', '')
502
                    response = prepare_response(request, user, next=next)
503
                    transaction.commit()
504
                    return response
505

    
506
                transaction.commit()
507
                messages.add_message(request, status, message)
508
                return HttpResponseRedirect(reverse(on_success))
509

    
510
            except SendMailError, e:
511
                logger.exception(e)
512
                status = messages.ERROR
513
                message = e.message
514
                messages.error(request, message)
515
                transaction.rollback()
516
            except BaseException, e:
517
                logger.exception(e)
518
                message = _(astakos_messages.GENERIC_ERROR)
519
                messages.error(request, message)
520
                logger.exception(e)
521
                transaction.rollback()
522

    
523
    return render_response(template_name,
524
                           signup_form=form,
525
                           third_party_token=third_party_token,
526
                           provider=provider,
527
                           context_instance=get_context(request, extra_context))
528

    
529

    
530
@require_http_methods(["GET", "POST"])
531
@required_auth_methods_assigned(only_warn=True)
532
@login_required
533
@signed_terms_required
534
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
535
    """
536
    Allows a user to send feedback.
537

538
    In case of GET request renders a form for providing the feedback information.
539
    In case of POST sends an email to support team.
540

541
    If the user isn't logged in, redirects to settings.LOGIN_URL.
542

543
    **Arguments**
544

545
    ``template_name``
546
        A custom template to use. This is optional; if not specified,
547
        this will default to ``im/feedback.html``.
548

549
    ``extra_context``
550
        An dictionary of variables to add to the template context.
551

552
    **Template:**
553

554
    im/signup.html or ``template_name`` keyword argument.
555

556
    **Settings:**
557

558
    * LOGIN_URL: login uri
559
    """
560
    extra_context = extra_context or {}
561
    if request.method == 'GET':
562
        form = FeedbackForm()
563
    if request.method == 'POST':
564
        if not request.user:
565
            return HttpResponse('Unauthorized', status=401)
566

    
567
        form = FeedbackForm(request.POST)
568
        if form.is_valid():
569
            msg = form.cleaned_data['feedback_msg']
570
            data = form.cleaned_data['feedback_data']
571
            try:
572
                send_feedback(msg, data, request.user, email_template_name)
573
            except SendMailError, e:
574
                messages.error(request, message)
575
            else:
576
                message = _(astakos_messages.FEEDBACK_SENT)
577
                messages.success(request, message)
578
    return render_response(template_name,
579
                           feedback_form=form,
580
                           context_instance=get_context(request, extra_context))
581

    
582

    
583
@require_http_methods(["GET"])
584
@signed_terms_required
585
def logout(request, template='registration/logged_out.html', extra_context=None):
586
    """
587
    Wraps `django.contrib.auth.logout`.
588
    """
589
    extra_context = extra_context or {}
590
    response = HttpResponse()
591
    if request.user.is_authenticated():
592
        email = request.user.email
593
        auth_logout(request)
594
    else:
595
        response['Location'] = reverse('index')
596
        response.status_code = 301
597
        return response
598

    
599
    next = restrict_next(
600
        request.GET.get('next'),
601
        domain=COOKIE_DOMAIN
602
    )
603

    
604
    if next:
605
        response['Location'] = next
606
        response.status_code = 302
607
    elif LOGOUT_NEXT:
608
        response['Location'] = LOGOUT_NEXT
609
        response.status_code = 301
610
    else:
611
        message = _(astakos_messages.LOGOUT_SUCCESS)
612
        last_provider = request.COOKIES.get('astakos_last_login_method', None)
613
        if last_provider:
614
            provider = auth_providers.get_provider(last_provider)
615
            extra_message = provider.get_logout_message_display
616
            if extra_message:
617
                message += '<br />' + extra_message
618
        messages.add_message(request, messages.SUCCESS, mark_safe(message))
619
        response['Location'] = reverse('index')
620
        response.status_code = 301
621
    return response
622

    
623

    
624
@require_http_methods(["GET", "POST"])
625
@transaction.commit_manually
626
def activate(request, greeting_email_template_name='im/welcome_email.txt',
627
             helpdesk_email_template_name='im/helpdesk_notification.txt'):
628
    """
629
    Activates the user identified by the ``auth`` request parameter, sends a welcome email
630
    and renews the user token.
631

632
    The view uses commit_manually decorator in order to ensure the user state will be updated
633
    only if the email will be send successfully.
634
    """
635
    token = request.GET.get('auth')
636
    next = request.GET.get('next')
637
    try:
638
        user = AstakosUser.objects.get(auth_token=token)
639
    except AstakosUser.DoesNotExist:
640
        return HttpResponseBadRequest(_(astakos_messages.ACCOUNT_UNKNOWN))
641

    
642
    if user.is_active:
643
        message = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE)
644
        messages.error(request, message)
645
        return index(request)
646

    
647
    try:
648
        activate_func(user, greeting_email_template_name, helpdesk_email_template_name, verify_email=True)
649
        response = prepare_response(request, user, next, renew=True)
650
        transaction.commit()
651
        return response
652
    except SendMailError, e:
653
        message = e.message
654
        messages.add_message(request, messages.ERROR, message)
655
        transaction.rollback()
656
        return index(request)
657
    except BaseException, e:
658
        status = messages.ERROR
659
        message = _(astakos_messages.GENERIC_ERROR)
660
        messages.add_message(request, messages.ERROR, message)
661
        logger.exception(e)
662
        transaction.rollback()
663
        return index(request)
664

    
665

    
666
@require_http_methods(["GET", "POST"])
667
def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
668
    extra_context = extra_context or {}
669
    term = None
670
    terms = None
671
    if not term_id:
672
        try:
673
            term = ApprovalTerms.objects.order_by('-id')[0]
674
        except IndexError:
675
            pass
676
    else:
677
        try:
678
            term = ApprovalTerms.objects.get(id=term_id)
679
        except ApprovalTerms.DoesNotExist, e:
680
            pass
681

    
682
    if not term:
683
        messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
684
        return HttpResponseRedirect(reverse('index'))
685
    try:
686
        f = open(term.location, 'r')
687
    except IOError:
688
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
689
        return render_response(
690
            template_name, context_instance=get_context(request, extra_context))
691

    
692
    terms = f.read()
693

    
694
    if request.method == 'POST':
695
        next = restrict_next(
696
            request.POST.get('next'),
697
            domain=COOKIE_DOMAIN
698
        )
699
        if not next:
700
            next = reverse('index')
701
        form = SignApprovalTermsForm(request.POST, instance=request.user)
702
        if not form.is_valid():
703
            return render_response(template_name,
704
                                   terms=terms,
705
                                   approval_terms_form=form,
706
                                   context_instance=get_context(request, extra_context))
707
        user = form.save()
708
        return HttpResponseRedirect(next)
709
    else:
710
        form = None
711
        if request.user.is_authenticated() and not request.user.signed_terms:
712
            form = SignApprovalTermsForm(instance=request.user)
713
        return render_response(template_name,
714
                               terms=terms,
715
                               approval_terms_form=form,
716
                               context_instance=get_context(request, extra_context))
717

    
718

    
719
@require_http_methods(["GET", "POST"])
720
@valid_astakos_user_required
721
@transaction.commit_manually
722
def change_email(request, activation_key=None,
723
                 email_template_name='registration/email_change_email.txt',
724
                 form_template_name='registration/email_change_form.html',
725
                 confirm_template_name='registration/email_change_done.html',
726
                 extra_context=None):
727
    extra_context = extra_context or {}
728

    
729

    
730
    if activation_key:
731
        try:
732
            user = EmailChange.objects.change_email(activation_key)
733
            if request.user.is_authenticated() and request.user == user:
734
                msg = _(astakos_messages.EMAIL_CHANGED)
735
                messages.success(request, msg)
736
                auth_logout(request)
737
                response = prepare_response(request, user)
738
                transaction.commit()
739
                return HttpResponseRedirect(reverse('edit_profile'))
740
        except ValueError, e:
741
            messages.error(request, e)
742
            transaction.rollback()
743
            return HttpResponseRedirect(reverse('index'))
744

    
745
        return render_response(confirm_template_name,
746
                               modified_user=user if 'user' in locals() \
747
                               else None, context_instance=get_context(request,
748
                                                            extra_context))
749

    
750
    if not request.user.is_authenticated():
751
        path = quote(request.get_full_path())
752
        url = request.build_absolute_uri(reverse('index'))
753
        return HttpResponseRedirect(url + '?next=' + path)
754

    
755
    # clean up expired email changes
756
    if request.user.email_change_is_pending():
757
        change = request.user.emailchanges.get()
758
        if change.activation_key_expired():
759
            change.delete()
760
            transaction.commit()
761
            return HttpResponseRedirect(reverse('email_change'))
762

    
763
    form = EmailChangeForm(request.POST or None)
764
    if request.method == 'POST' and form.is_valid():
765
        try:
766
            # delete pending email changes
767
            request.user.emailchanges.all().delete()
768
            ec = form.save(email_template_name, request)
769
        except SendMailError, e:
770
            msg = e
771
            messages.error(request, msg)
772
            transaction.rollback()
773
            return HttpResponseRedirect(reverse('edit_profile'))
774
        else:
775
            msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
776
            messages.success(request, msg)
777
            transaction.commit()
778
            return HttpResponseRedirect(reverse('edit_profile'))
779

    
780
    if request.user.email_change_is_pending():
781
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
782

    
783
    return render_response(
784
        form_template_name,
785
        form=form,
786
        context_instance=get_context(request, extra_context)
787
    )
788

    
789

    
790
def send_activation(request, user_id, template_name='im/login.html', extra_context=None):
791

    
792
    if request.user.is_authenticated():
793
        messages.error(request, _(astakos_messages.ALREADY_LOGGED_IN))
794
        return HttpResponseRedirect(reverse('edit_profile'))
795

    
796
    if astakos_settings.MODERATION_ENABLED:
797
        raise PermissionDenied
798

    
799
    extra_context = extra_context or {}
800
    try:
801
        u = AstakosUser.objects.get(id=user_id)
802
    except AstakosUser.DoesNotExist:
803
        messages.error(request, _(astakos_messages.ACCOUNT_UNKNOWN))
804
    else:
805
        try:
806
            send_activation_func(u)
807
            msg = _(astakos_messages.ACTIVATION_SENT)
808
            messages.success(request, msg)
809
        except SendMailError, e:
810
            messages.error(request, e)
811
    return render_response(
812
        template_name,
813
        login_form = LoginForm(request=request),
814
        context_instance = get_context(
815
            request,
816
            extra_context
817
        )
818
    )
819

    
820

    
821
@require_http_methods(["GET"])
822
@valid_astakos_user_required
823
def resource_usage(request):
824

    
825
    def with_class(entry):
826
         entry['load_class'] = 'red'
827
         max_value = float(entry['maxValue'])
828
         curr_value = float(entry['currValue'])
829
         entry['ratio_limited']= 0
830
         if max_value > 0 :
831
             entry['ratio'] = (curr_value / max_value) * 100
832
         else:
833
             entry['ratio'] = 0
834
         if entry['ratio'] < 66:
835
             entry['load_class'] = 'yellow'
836
         if entry['ratio'] < 33:
837
             entry['load_class'] = 'green'
838
         if entry['ratio']<0:
839
             entry['ratio'] = 0
840
         if entry['ratio']>100:
841
             entry['ratio_limited'] = 100
842
         else:
843
             entry['ratio_limited'] = entry['ratio']
844
         return entry
845

    
846
    def pluralize(entry):
847
        entry['plural'] = engine.plural(entry.get('name'))
848
        return entry
849

    
850
    resource_usage = None
851
    result = callpoint.get_user_usage(request.user.id)
852
    if result.is_success:
853
        resource_usage = result.data
854
        backenddata = map(with_class, result.data)
855
        backenddata = map(pluralize , backenddata)
856
    else:
857
        messages.error(request, result.reason)
858
        backenddata = []
859
        resource_usage = []
860

    
861
    if request.REQUEST.get('json', None):
862
        return HttpResponse(json.dumps(backenddata),
863
                            mimetype="application/json")
864

    
865
    return render_response('im/resource_usage.html',
866
                           context_instance=get_context(request),
867
                           resource_usage=backenddata,
868
                           usage_update_interval=astakos_settings.USAGE_UPDATE_INTERVAL,
869
                           result=result)
870

    
871
# TODO: action only on POST and user should confirm the removal
872
@require_http_methods(["GET", "POST"])
873
@login_required
874
@signed_terms_required
875
def remove_auth_provider(request, pk):
876
    try:
877
        provider = request.user.auth_providers.get(pk=pk)
878
    except AstakosUserAuthProvider.DoesNotExist:
879
        raise Http404
880

    
881
    if provider.can_remove():
882
        provider.delete()
883
        return HttpResponseRedirect(reverse('edit_profile'))
884
    else:
885
        raise PermissionDenied
886

    
887

    
888
def how_it_works(request):
889
    return render_response(
890
        'im/how_it_works.html',
891
        context_instance=get_context(request))
892

    
893
@transaction.commit_manually
894
def _create_object(request, model=None, template_name=None,
895
        template_loader=template_loader, extra_context=None, post_save_redirect=None,
896
        login_required=False, context_processors=None, form_class=None,
897
        msg=None):
898
    """
899
    Based of django.views.generic.create_update.create_object which displays a
900
    summary page before creating the object.
901
    """
902
    rollback = False
903
    response = None
904

    
905
    if extra_context is None: extra_context = {}
906
    if login_required and not request.user.is_authenticated():
907
        return redirect_to_login(request.path)
908
    try:
909

    
910
        model, form_class = get_model_and_form_class(model, form_class)
911
        extra_context['edit'] = 0
912
        if request.method == 'POST':
913
            form = form_class(request.POST, request.FILES)
914
            if form.is_valid():
915
                verify = request.GET.get('verify')
916
                edit = request.GET.get('edit')
917
                if verify == '1':
918
                    extra_context['show_form'] = False
919
                    extra_context['form_data'] = form.cleaned_data
920
                elif edit == '1':
921
                    extra_context['show_form'] = True
922
                else:
923
                    new_object = form.save()
924
                    if not msg:
925
                        msg = _("The %(verbose_name)s was created successfully.")
926
                    msg = msg % model._meta.__dict__
927
                    messages.success(request, msg, fail_silently=True)
928
                    response = redirect(post_save_redirect, new_object)
929
        else:
930
            form = form_class()
931
    except BaseException, e:
932
        logger.exception(e)
933
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
934
        rollback = True
935
    finally:
936
        if rollback:
937
            transaction.rollback()
938
        else:
939
            transaction.commit()
940

    
941
        if response == None:
942
            # Create the template, context, response
943
            if not template_name:
944
                template_name = "%s/%s_form.html" %\
945
                     (model._meta.app_label, model._meta.object_name.lower())
946
            t = template_loader.get_template(template_name)
947
            c = RequestContext(request, {
948
                'form': form
949
            }, context_processors)
950
            apply_extra_context(extra_context, c)
951
            response = HttpResponse(t.render(c))
952
        return response
953

    
954
@transaction.commit_manually
955
def _update_object(request, model=None, object_id=None, slug=None,
956
        slug_field='slug', template_name=None, template_loader=template_loader,
957
        extra_context=None, post_save_redirect=None, login_required=False,
958
        context_processors=None, template_object_name='object',
959
        form_class=None, msg=None):
960
    """
961
    Based of django.views.generic.create_update.update_object which displays a
962
    summary page before updating the object.
963
    """
964
    rollback = False
965
    response = None
966

    
967
    if extra_context is None: extra_context = {}
968
    if login_required and not request.user.is_authenticated():
969
        return redirect_to_login(request.path)
970

    
971
    try:
972
        model, form_class = get_model_and_form_class(model, form_class)
973
        obj = lookup_object(model, object_id, slug, slug_field)
974

    
975
        if request.method == 'POST':
976
            form = form_class(request.POST, request.FILES, instance=obj)
977
            if form.is_valid():
978
                verify = request.GET.get('verify')
979
                edit = request.GET.get('edit')
980
                if verify == '1':
981
                    extra_context['show_form'] = False
982
                    extra_context['form_data'] = form.cleaned_data
983
                elif edit == '1':
984
                    extra_context['show_form'] = True
985
                else:
986
                    obj = form.save()
987
                    if not msg:
988
                        msg = _("The %(verbose_name)s was created successfully.")
989
                    msg = msg % model._meta.__dict__
990
                    messages.success(request, msg, fail_silently=True)
991
                    response = redirect(post_save_redirect, obj)
992
        else:
993
            form = form_class(instance=obj)
994
    except BaseException, e:
995
        logger.exception(e)
996
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
997
        rollback = True
998
    finally:
999
        if rollback:
1000
            transaction.rollback()
1001
        else:
1002
            transaction.commit()
1003
        if response == None:
1004
            if not template_name:
1005
                template_name = "%s/%s_form.html" %\
1006
                    (model._meta.app_label, model._meta.object_name.lower())
1007
            t = template_loader.get_template(template_name)
1008
            c = RequestContext(request, {
1009
                'form': form,
1010
                template_object_name: obj,
1011
            }, context_processors)
1012
            apply_extra_context(extra_context, c)
1013
            response = HttpResponse(t.render(c))
1014
            populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.attname))
1015
        return response
1016

    
1017
@require_http_methods(["GET", "POST"])
1018
@signed_terms_required
1019
@login_required
1020
def project_add(request):
1021
    resource_groups = RESOURCES_PRESENTATION_DATA.get('groups', {})
1022
    resource_catalog = ()
1023
    result = callpoint.list_resources()
1024
    details_fields = [
1025
        "name", "homepage", "description","start_date","end_date", "comments"]
1026
    membership_fields =[
1027
        "member_join_policy", "member_leave_policy", "limit_on_members_number"]
1028
    if not result.is_success:
1029
        messages.error(
1030
            request,
1031
            'Unable to retrieve system resources: %s' % result.reason
1032
    )
1033
    else:
1034
        resource_catalog = [
1035
            (g, filter(lambda r: r.get('group', '') == g, result.data)) \
1036
                for g in resource_groups]
1037
    extra_context = {
1038
        'resource_catalog':resource_catalog,
1039
        'resource_groups':resource_groups,
1040
        'show_form':True,
1041
        'details_fields':details_fields,
1042
        'membership_fields':membership_fields}
1043
    return _create_object(
1044
        request,
1045
        template_name='im/projects/projectapplication_form.html',
1046
        extra_context=extra_context,
1047
        post_save_redirect=reverse('project_list'),
1048
        form_class=ProjectApplicationForm,
1049
        msg=_("The %(verbose_name)s has been received and \
1050
                 is under consideration."))
1051

    
1052

    
1053
@require_http_methods(["GET"])
1054
@signed_terms_required
1055
@login_required
1056
def project_list(request):
1057
    projects = ProjectApplication.objects.user_accessible_projects(request.user).select_related()
1058
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1059
                                                prefix="my_projects_")
1060
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1061

    
1062
    return object_list(
1063
        request,
1064
        projects,
1065
        template_name='im/projects/project_list.html',
1066
        extra_context={
1067
            'is_search':False,
1068
            'table': table,
1069
        })
1070

    
1071

    
1072
@require_http_methods(["GET", "POST"])
1073
@signed_terms_required
1074
@login_required
1075
def project_update(request, application_id):
1076
    resource_groups = RESOURCES_PRESENTATION_DATA.get('groups', {})
1077
    resource_catalog = ()
1078
    result = callpoint.list_resources()
1079
    details_fields = [
1080
        "name", "homepage", "description","start_date","end_date", "comments"]
1081
    membership_fields =[
1082
        "member_join_policy", "member_leave_policy", "limit_on_members_number"]
1083
    if not result.is_success:
1084
        messages.error(
1085
            request,
1086
            'Unable to retrieve system resources: %s' % result.reason
1087
    )
1088
    else:
1089
        resource_catalog = [
1090
            (g, filter(lambda r: r.get('group', '') == g, result.data)) \
1091
                for g in resource_groups]
1092
    extra_context = {
1093
        'resource_catalog':resource_catalog,
1094
        'resource_groups':resource_groups,
1095
        'show_form':True,
1096
        'details_fields':details_fields,
1097
        'update_form': True,
1098
        'membership_fields':membership_fields}
1099
    return _update_object(
1100
        request,
1101
        object_id=application_id,
1102
        template_name='im/projects/projectapplication_form.html',
1103
        extra_context=extra_context, post_save_redirect=reverse('project_list'),
1104
        form_class=ProjectApplicationForm,
1105
        msg = _("The %(verbose_name)s has been received and \
1106
                    is under consideration."))
1107

    
1108

    
1109
@require_http_methods(["GET", "POST"])
1110
@signed_terms_required
1111
@login_required
1112
@transaction.commit_on_success
1113
def project_detail(request, application_id):
1114
    addmembers_form = AddProjectMembersForm()
1115
    if request.method == 'POST':
1116
        addmembers_form = AddProjectMembersForm(
1117
            request.POST,
1118
            application_id=int(application_id),
1119
            request_user=request.user)
1120
        if addmembers_form.is_valid():
1121
            try:
1122
                rollback = False
1123
                application_id = int(application_id)
1124
                map(lambda u: enroll_member(
1125
                        application_id,
1126
                        u,
1127
                        request_user=request.user),
1128
                    addmembers_form.valid_users)
1129
            except (IOError, PermissionDenied), e:
1130
                messages.error(request, e)
1131
            except BaseException, e:
1132
                rollback = True
1133
                messages.error(request, e)
1134
            finally:
1135
                if rollback == True:
1136
                    transaction.rollback()
1137
                else:
1138
                    transaction.commit()
1139
            addmembers_form = AddProjectMembersForm()
1140

    
1141
    rollback = False
1142

    
1143
    application = get_object_or_404(ProjectApplication, pk=application_id)
1144
    try:
1145
        members = application.project.projectmembership_set.select_related()
1146
    except Project.DoesNotExist:
1147
        members = ProjectMembership.objects.none()
1148

    
1149
    members_table = tables.ProjectApplicationMembersTable(application,
1150
                                                          members,
1151
                                                          user=request.user,
1152
                                                          prefix="members_")
1153
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(members_table)
1154

    
1155
    modifications_table = None
1156
    if application.follower:
1157
        following_applications = list(application.followers())
1158
        following_applications.reverse()
1159
        modifications_table = \
1160
            tables.ProjectModificationApplicationsTable(following_applications,
1161
                                                       user=request.user,
1162
                                                       prefix="modifications_")
1163

    
1164
    return object_detail(
1165
        request,
1166
        queryset=ProjectApplication.objects.select_related(),
1167
        object_id=application_id,
1168
        template_name='im/projects/project_detail.html',
1169
        extra_context={
1170
            'addmembers_form':addmembers_form,
1171
            'members_table': members_table,
1172
            'user_owns_project': request.user.owns_project(application),
1173
            'modifications_table': modifications_table,
1174
            'member_status': application.user_status(request.user)
1175
            })
1176

    
1177
@require_http_methods(["GET", "POST"])
1178
@signed_terms_required
1179
@login_required
1180
def project_search(request):
1181
    q = request.GET.get('q', '')
1182
    form = ProjectSearchForm()
1183
    q = q.strip()
1184

    
1185
    if request.method == "POST":
1186
        form = ProjectSearchForm(request.POST)
1187
        if form.is_valid():
1188
            q = form.cleaned_data['q'].strip()
1189
        else:
1190
            q = None
1191

    
1192
    if q is None:
1193
        projects = ProjectApplication.objects.none()
1194
    else:
1195
        accepted_projects = request.user.projectmembership_set.filter(
1196
            ~Q(acceptance_date__isnull=True)).values_list('project', flat=True)
1197
        projects = ProjectApplication.objects.search_by_name(q)
1198
        projects = projects.filter(~Q(project__last_approval_date__isnull=True))
1199
        projects = projects.exclude(project__in=accepted_projects)
1200

    
1201
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1202
                                                prefix="my_projects_")
1203
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1204

    
1205
    return object_list(
1206
        request,
1207
        projects,
1208
        template_name='im/projects/project_list.html',
1209
        extra_context={
1210
          'form': form,
1211
          'is_search': True,
1212
          'q': q,
1213
          'table': table
1214
        })
1215

    
1216
@require_http_methods(["POST", "GET"])
1217
@signed_terms_required
1218
@login_required
1219
@transaction.commit_manually
1220
def project_join(request, application_id):
1221
    next = request.GET.get('next')
1222
    if not next:
1223
        next = reverse('astakos.im.views.project_detail',
1224
                       args=(application_id,))
1225

    
1226
    rollback = False
1227
    try:
1228
        application_id = int(application_id)
1229
        join_project(application_id, request.user)
1230
        # TODO: distinct messages for request/auto accept ???
1231
        messages.success(request, _(astakos_messages.USER_JOIN_REQUEST_SUBMITED))
1232
    except (IOError, PermissionDenied), e:
1233
        messages.error(request, e)
1234
    except BaseException, e:
1235
        logger.exception(e)
1236
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1237
        rollback = True
1238
    finally:
1239
        if rollback:
1240
            transaction.rollback()
1241
        else:
1242
            transaction.commit()
1243
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1244
    return redirect(next)
1245

    
1246
@require_http_methods(["POST"])
1247
@signed_terms_required
1248
@login_required
1249
@transaction.commit_manually
1250
def project_leave(request, application_id):
1251
    next = request.GET.get('next')
1252
    if not next:
1253
        next = reverse('astakos.im.views.project_list')
1254

    
1255
    rollback = False
1256
    try:
1257
        application_id = int(application_id)
1258
        leave_project(application_id, request.user)
1259
    except (IOError, PermissionDenied), e:
1260
        messages.error(request, e)
1261
    except BaseException, e:
1262
        logger.exception(e)
1263
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1264
        rollback = True
1265
    finally:
1266
        if rollback:
1267
            transaction.rollback()
1268
        else:
1269
            transaction.commit()
1270

    
1271
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1272
    return redirect(next)
1273

    
1274
@require_http_methods(["POST"])
1275
@signed_terms_required
1276
@login_required
1277
@transaction.commit_manually
1278
def project_accept_member(request, application_id, user_id):
1279
    rollback = False
1280
    try:
1281
        application_id = int(application_id)
1282
        user_id = int(user_id)
1283
        m = accept_membership(application_id, user_id, request.user)
1284
    except (IOError, PermissionDenied), e:
1285
        messages.error(request, e)
1286
    except BaseException, e:
1287
        logger.exception(e)
1288
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1289
        rollback = True
1290
    else:
1291
        realname = m.person.realname
1292
        msg = _(astakos_messages.USER_JOINED_PROJECT) % locals()
1293
        messages.success(request, msg)
1294
    finally:
1295
        if rollback:
1296
            transaction.rollback()
1297
        else:
1298
            transaction.commit()
1299
    return redirect(reverse('project_detail', args=(application_id,)))
1300

    
1301
@require_http_methods(["POST"])
1302
@signed_terms_required
1303
@login_required
1304
@transaction.commit_manually
1305
def project_remove_member(request, application_id, user_id):
1306
    rollback = False
1307
    try:
1308
        application_id = int(application_id)
1309
        user_id = int(user_id)
1310
        m = remove_membership(application_id, user_id, request.user)
1311
    except (IOError, PermissionDenied), e:
1312
        messages.error(request, e)
1313
    except BaseException, e:
1314
        logger.exception(e)
1315
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1316
        rollback = True
1317
    else:
1318
        realname = m.person.realname
1319
        msg = _(astakos_messages.USER_LEFT_PROJECT) % locals()
1320
        messages.success(request, msg)
1321
    finally:
1322
        if rollback:
1323
            transaction.rollback()
1324
        else:
1325
            transaction.commit()
1326
    return redirect(reverse('project_detail', args=(application_id,)))
1327

    
1328
@require_http_methods(["POST"])
1329
@signed_terms_required
1330
@login_required
1331
@transaction.commit_manually
1332
def project_reject_member(request, application_id, user_id):
1333
    rollback = False
1334
    try:
1335
        application_id = int(application_id)
1336
        user_id = int(user_id)
1337
        m = reject_membership(application_id, user_id, request.user)
1338
    except (IOError, PermissionDenied), e:
1339
        messages.error(request, e)
1340
    except BaseException, e:
1341
        logger.exception(e)
1342
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1343
        rollback = True
1344
    else:
1345
        realname = m.person.realname
1346
        msg = _(astakos_messages.USER_LEFT_PROJECT) % locals()
1347
        messages.success(request, msg)
1348
    finally:
1349
        if rollback:
1350
            transaction.rollback()
1351
        else:
1352
            transaction.commit()
1353
    return redirect(reverse('project_detail', args=(application_id,)))
1354

    
1355