Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 222305b7

History | View | Annotate | Download (59.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 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
from synnefo.lib.ordereddict import OrderedDict
44

    
45
from django_tables2 import RequestConfig
46

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

    
75
import astakos.im.messages as astakos_messages
76

    
77
from astakos.im import activation_backends
78
from astakos.im import tables
79
from astakos.im.models import (
80
    AstakosUser, ApprovalTerms,
81
    EmailChange, AstakosUserAuthProvider, PendingThirdPartyUser,
82
    ProjectApplication, ProjectMembership, Project, Service, Resource)
83
from astakos.im.util import (
84
    get_context, prepare_response, get_query, restrict_next, model_to_dict)
85
from astakos.im.forms import (
86
    LoginForm, InvitationForm,
87
    FeedbackForm, SignApprovalTermsForm,
88
    EmailChangeForm,
89
    ProjectApplicationForm, ProjectSortForm,
90
    AddProjectMembersForm, ProjectSearchForm,
91
    ProjectMembersSortForm)
92
from astakos.im.forms import ExtendedProfileForm as ProfileForm
93
from astakos.im.functions import (
94
    send_feedback,
95
    logout as auth_logout,
96
    invite as invite_func,
97
    qh_add_pending_app,
98
    accept_membership, reject_membership, remove_membership, cancel_membership,
99
    leave_project, join_project, enroll_member, can_join_request,
100
    can_leave_request,
101
    get_related_project_id, get_by_chain_or_404,
102
    approve_application, deny_application,
103
    cancel_application, dismiss_application)
104
from astakos.im.settings import (
105
    COOKIE_DOMAIN, LOGOUT_NEXT,
106
    LOGGING_LEVEL, PAGINATE_BY,
107
    PAGINATE_BY_ALL,
108
    ACTIVATION_REDIRECT_URL,
109
    MODERATION_ENABLED)
110
from astakos.im import presentation
111
from astakos.im import settings
112
from astakos.im import auth_providers as auth
113
from snf_django.lib.db.transaction import commit_on_success_strict
114
from astakos.im.ctx import ExceptionHandler
115
from astakos.im import quotas
116
from astakos.im.decorators import cookie_fix
117

    
118
logger = logging.getLogger(__name__)
119

    
120

    
121

    
122
def render_response(template, tab=None, status=200, context_instance=None, **kwargs):
123
    """
124
    Calls ``django.template.loader.render_to_string`` with an additional ``tab``
125
    keyword argument and returns an ``django.http.HttpResponse`` with the
126
    specified ``status``.
127
    """
128
    if tab is None:
129
        tab = template.partition('_')[0].partition('.html')[0]
130
    kwargs.setdefault('tab', tab)
131
    html = template_loader.render_to_string(
132
        template, kwargs, context_instance=context_instance)
133
    response = HttpResponse(html, status=status)
134
    return response
135

    
136
def requires_auth_provider(provider_id, **perms):
137
    """
138
    """
139
    def decorator(func, *args, **kwargs):
140
        @wraps(func)
141
        def wrapper(request, *args, **kwargs):
142
            provider = auth.get_provider(provider_id)
143

    
144
            if not provider or not provider.is_active():
145
                raise PermissionDenied
146

    
147
            for pkey, value in perms.iteritems():
148
                attr = 'get_%s_policy' % pkey.lower()
149
                if getattr(provider, attr) != value:
150
                    #TODO: add session message
151
                    return HttpResponseRedirect(reverse('login'))
152
            return func(request, *args)
153
        return wrapper
154
    return decorator
155

    
156

    
157
def requires_anonymous(func):
158
    """
159
    Decorator checkes whether the request.user is not Anonymous and in that case
160
    redirects to `logout`.
161
    """
162
    @wraps(func)
163
    def wrapper(request, *args):
164
        if not request.user.is_anonymous():
165
            next = urlencode({'next': request.build_absolute_uri()})
166
            logout_uri = reverse(logout) + '?' + next
167
            return HttpResponseRedirect(logout_uri)
168
        return func(request, *args)
169
    return wrapper
170

    
171

    
172
def signed_terms_required(func):
173
    """
174
    Decorator checks whether the request.user is Anonymous and in that case
175
    redirects to `logout`.
176
    """
177
    @wraps(func)
178
    def wrapper(request, *args, **kwargs):
179
        if request.user.is_authenticated() and not request.user.signed_terms:
180
            params = urlencode({'next': request.build_absolute_uri(),
181
                                'show_form': ''})
182
            terms_uri = reverse('latest_terms') + '?' + params
183
            return HttpResponseRedirect(terms_uri)
184
        return func(request, *args, **kwargs)
185
    return wrapper
186

    
187

    
188
def required_auth_methods_assigned(allow_access=False):
189
    """
190
    Decorator that checks whether the request.user has all required auth providers
191
    assigned.
192
    """
193

    
194
    def decorator(func):
195
        @wraps(func)
196
        def wrapper(request, *args, **kwargs):
197
            if request.user.is_authenticated():
198
                missing = request.user.missing_required_providers()
199
                if missing:
200
                    for provider in missing:
201
                        messages.error(request,
202
                                       provider.get_required_msg)
203
                    if not allow_access:
204
                        return HttpResponseRedirect(reverse('edit_profile'))
205
            return func(request, *args, **kwargs)
206
        return wrapper
207
    return decorator
208

    
209

    
210
def valid_astakos_user_required(func):
211
    return signed_terms_required(required_auth_methods_assigned()(login_required(func)))
212

    
213

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

221
    **Arguments**
222

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

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

231
    ``extra_context``
232
        An dictionary of variables to add to the template context.
233

234
    **Template:**
235

236
    im/profile.html or im/login.html or ``template_name`` keyword argument.
237

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

    
244
    third_party_token = request.GET.get('key', False)
245
    if third_party_token:
246
        messages.info(request, astakos_messages.AUTH_PROVIDER_LOGIN_TO_ADD)
247

    
248
    return render_response(
249
        template_name,
250
        login_form = LoginForm(request=request),
251
        context_instance = get_context(request, extra_context)
252
    )
253

    
254

    
255
@require_http_methods(["POST"])
256
@cookie_fix
257
@valid_astakos_user_required
258
def update_token(request):
259
    """
260
    Update api token view.
261
    """
262
    user = request.user
263
    user.renew_token()
264
    user.save()
265
    messages.success(request, astakos_messages.TOKEN_UPDATED)
266
    return HttpResponseRedirect(reverse('edit_profile'))
267

    
268

    
269
@require_http_methods(["GET", "POST"])
270
@cookie_fix
271
@valid_astakos_user_required
272
@transaction.commit_manually
273
def invite(request, template_name='im/invitations.html', extra_context=None):
274
    """
275
    Allows a user to invite somebody else.
276

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

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

284
    If the user isn't logged in, redirects to settings.LOGIN_URL.
285

286
    **Arguments**
287

288
    ``template_name``
289
        A custom template to use. This is optional; if not specified,
290
        this will default to ``im/invitations.html``.
291

292
    ``extra_context``
293
        An dictionary of variables to add to the template context.
294

295
    **Template:**
296

297
    im/invitations.html or ``template_name`` keyword argument.
298

299
    **Settings:**
300

301
    The view expectes the following settings are defined:
302

303
    * LOGIN_URL: login uri
304
    """
305
    extra_context = extra_context or {}
306
    status = None
307
    message = None
308
    form = InvitationForm()
309

    
310
    inviter = request.user
311
    if request.method == 'POST':
312
        form = InvitationForm(request.POST)
313
        if inviter.invitations > 0:
314
            if form.is_valid():
315
                try:
316
                    email = form.cleaned_data.get('username')
317
                    realname = form.cleaned_data.get('realname')
318
                    invite_func(inviter, email, realname)
319
                    message = _(astakos_messages.INVITATION_SENT) % locals()
320
                    messages.success(request, message)
321
                except Exception, e:
322
                    transaction.rollback()
323
                    raise
324
                else:
325
                    transaction.commit()
326
        else:
327
            message = _(astakos_messages.MAX_INVITATION_NUMBER_REACHED)
328
            messages.error(request, message)
329

    
330
    sent = [{'email': inv.username,
331
             'realname': inv.realname,
332
             'is_consumed': inv.is_consumed}
333
            for inv in request.user.invitations_sent.all()]
334
    kwargs = {'inviter': inviter,
335
              'sent': sent}
336
    context = get_context(request, extra_context, **kwargs)
337
    return render_response(template_name,
338
                           invitation_form=form,
339
                           context_instance=context)
340

    
341

    
342
@require_http_methods(["GET", "POST"])
343
@required_auth_methods_assigned(allow_access=True)
344
@login_required
345
@cookie_fix
346
@signed_terms_required
347
def edit_profile(request, template_name='im/profile.html', extra_context=None):
348
    """
349
    Allows a user to edit his/her profile.
350

351
    In case of GET request renders a form for displaying the user information.
352
    In case of POST updates the user informantion and redirects to ``next``
353
    url parameter if exists.
354

355
    If the user isn't logged in, redirects to settings.LOGIN_URL.
356

357
    **Arguments**
358

359
    ``template_name``
360
        A custom template to use. This is optional; if not specified,
361
        this will default to ``im/profile.html``.
362

363
    ``extra_context``
364
        An dictionary of variables to add to the template context.
365

366
    **Template:**
367

368
    im/profile.html or ``template_name`` keyword argument.
369

370
    **Settings:**
371

372
    The view expectes the following settings are defined:
373

374
    * LOGIN_URL: login uri
375
    """
376
    extra_context = extra_context or {}
377
    form = ProfileForm(
378
        instance=request.user,
379
        session_key=request.session.session_key
380
    )
381
    extra_context['next'] = request.GET.get('next')
382
    if request.method == 'POST':
383
        form = ProfileForm(
384
            request.POST,
385
            instance=request.user,
386
            session_key=request.session.session_key
387
        )
388
        if form.is_valid():
389
            try:
390
                prev_token = request.user.auth_token
391
                user = form.save(request=request)
392
                next = restrict_next(
393
                    request.POST.get('next'),
394
                    domain=COOKIE_DOMAIN
395
                )
396
                msg = _(astakos_messages.PROFILE_UPDATED)
397
                messages.success(request, msg)
398

    
399
                if form.email_changed:
400
                    msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
401
                    messages.success(request, msg)
402
                if form.password_changed:
403
                    msg = _(astakos_messages.PASSWORD_CHANGED)
404
                    messages.success(request, msg)
405

    
406
                if next:
407
                    return redirect(next)
408
                else:
409
                    return redirect(reverse('edit_profile'))
410
            except ValueError, ve:
411
                messages.success(request, ve)
412
    elif request.method == "GET":
413
        request.user.is_verified = True
414
        request.user.save()
415

    
416
    # existing providers
417
    user_providers = request.user.get_enabled_auth_providers()
418
    user_disabled_providers = request.user.get_disabled_auth_providers()
419

    
420
    # providers that user can add
421
    user_available_providers = request.user.get_available_auth_providers()
422

    
423
    extra_context['services'] = Service.catalog().values()
424
    return render_response(template_name,
425
                           profile_form=form,
426
                           user_providers=user_providers,
427
                           user_disabled_providers=user_disabled_providers,
428
                           user_available_providers=user_available_providers,
429
                           context_instance=get_context(request,
430
                                                          extra_context))
431

    
432

    
433
@transaction.commit_manually
434
@require_http_methods(["GET", "POST"])
435
@cookie_fix
436
def signup(request, template_name='im/signup.html', on_success='index',
437
           extra_context=None, activation_backend=None):
438
    """
439
    Allows a user to create a local account.
440

441
    In case of GET request renders a form for entering the user information.
442
    In case of POST handles the signup.
443

444
    The user activation will be delegated to the backend specified by the
445
    ``activation_backend`` keyword argument if present, otherwise to the
446
    ``astakos.im.activation_backends.InvitationBackend`` if
447
    settings.ASTAKOS_INVITATIONS_ENABLED is True or
448
    ``astakos.im.activation_backends.SimpleBackend`` if not (see
449
    activation_backends);
450

451
    Upon successful user creation, if ``next`` url parameter is present the
452
    user is redirected there otherwise renders the same page with a success
453
    message.
454

455
    On unsuccessful creation, renders ``template_name`` with an error message.
456

457
    **Arguments**
458

459
    ``template_name``
460
        A custom template to render. This is optional;
461
        if not specified, this will default to ``im/signup.html``.
462

463
    ``extra_context``
464
        An dictionary of variables to add to the template context.
465

466
    ``on_success``
467
        Resolvable view name to redirect on registration success.
468

469
    **Template:**
470

471
    im/signup.html or ``template_name`` keyword argument.
472
    """
473
    extra_context = extra_context or {}
474
    if request.user.is_authenticated():
475
        logger.info("%s already signed in, redirect to index",
476
                    request.user.log_display)
477
        return HttpResponseRedirect(reverse('index'))
478

    
479
    provider = get_query(request).get('provider', 'local')
480
    if not auth.get_provider(provider).get_create_policy:
481
        logger.error("%s provider not available for signup", provider)
482
        raise PermissionDenied
483

    
484
    instance = None
485

    
486
    # user registered using third party provider
487
    third_party_token = request.REQUEST.get('third_party_token', None)
488
    unverified = None
489
    if third_party_token:
490
        # retreive third party entry. This was created right after the initial
491
        # third party provider handshake.
492
        pending = get_object_or_404(PendingThirdPartyUser,
493
                                    token=third_party_token)
494

    
495
        provider = pending.provider
496

    
497
        # clone third party instance into the corresponding AstakosUser
498
        instance = pending.get_user_instance()
499
        get_unverified = AstakosUserAuthProvider.objects.unverified
500

    
501
        # check existing unverified entries
502
        unverified = get_unverified(pending.provider,
503
                                    identifier=pending.third_party_identifier)
504

    
505
        if unverified and request.method == 'GET':
506
            messages.warning(request, unverified.get_pending_registration_msg)
507
            if unverified.user.moderated:
508
                messages.warning(request,
509
                                 unverified.get_pending_resend_activation_msg)
510
            else:
511
                messages.warning(request,
512
                                 unverified.get_pending_moderation_msg)
513

    
514
    # prepare activation backend based on current request
515
    if not activation_backend:
516
        activation_backend = activation_backends.get_backend()
517

    
518
    form_kwargs = {'instance': instance}
519
    if third_party_token:
520
        form_kwargs['third_party_token'] = third_party_token
521

    
522
    form = activation_backend.get_signup_form(
523
        provider, None, **form_kwargs)
524

    
525
    if request.method == 'POST':
526
        form = activation_backend.get_signup_form(
527
            provider,
528
            request.POST,
529
            **form_kwargs)
530

    
531
        if form.is_valid():
532
            commited = False
533
            try:
534
                user = form.save(commit=False)
535

    
536
                # delete previously unverified accounts
537
                if AstakosUser.objects.user_exists(user.email):
538
                    AstakosUser.objects.get_by_identifier(user.email).delete()
539

    
540
                # store_user so that user auth providers get initialized
541
                form.store_user(user, request)
542
                result = activation_backend.handle_registration(user)
543
                if result.status == \
544
                        activation_backend.Result.PENDING_MODERATION:
545
                    # user should be warned that his account is not active yet
546
                    status = messages.WARNING
547
                else:
548
                    status = messages.SUCCESS
549
                message = result.message
550
                activation_backend.send_result_notifications(result, user)
551

    
552
                # commit user entry
553
                transaction.commit()
554
                # commited flag
555
                # in case an exception get raised from this point
556
                commited = True
557

    
558
                if user and user.is_active:
559
                    # activation backend directly activated the user
560
                    # log him in
561
                    next = request.POST.get('next', '')
562
                    response = prepare_response(request, user, next=next)
563
                    return response
564

    
565
                messages.add_message(request, status, message)
566
                return HttpResponseRedirect(reverse(on_success))
567
            except Exception, e:
568
                if not commited:
569
                    transaction.rollback()
570
                raise
571

    
572
    return render_response(template_name,
573
                           signup_form=form,
574
                           third_party_token=third_party_token,
575
                           provider=provider,
576
                           context_instance=get_context(request, extra_context))
577

    
578

    
579
@require_http_methods(["GET", "POST"])
580
@required_auth_methods_assigned(allow_access=True)
581
@login_required
582
@cookie_fix
583
@signed_terms_required
584
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
585
    """
586
    Allows a user to send feedback.
587

588
    In case of GET request renders a form for providing the feedback information.
589
    In case of POST sends an email to support team.
590

591
    If the user isn't logged in, redirects to settings.LOGIN_URL.
592

593
    **Arguments**
594

595
    ``template_name``
596
        A custom template to use. This is optional; if not specified,
597
        this will default to ``im/feedback.html``.
598

599
    ``extra_context``
600
        An dictionary of variables to add to the template context.
601

602
    **Template:**
603

604
    im/signup.html or ``template_name`` keyword argument.
605

606
    **Settings:**
607

608
    * LOGIN_URL: login uri
609
    """
610
    extra_context = extra_context or {}
611
    if request.method == 'GET':
612
        form = FeedbackForm()
613
    if request.method == 'POST':
614
        if not request.user:
615
            return HttpResponse('Unauthorized', status=401)
616

    
617
        form = FeedbackForm(request.POST)
618
        if form.is_valid():
619
            msg = form.cleaned_data['feedback_msg']
620
            data = form.cleaned_data['feedback_data']
621
            send_feedback(msg, data, request.user, email_template_name)
622
            message = _(astakos_messages.FEEDBACK_SENT)
623
            messages.success(request, message)
624
            return HttpResponseRedirect(reverse('feedback'))
625

    
626
    return render_response(template_name,
627
                           feedback_form=form,
628
                           context_instance=get_context(request,
629
                                                        extra_context))
630

    
631

    
632
@require_http_methods(["GET"])
633
@cookie_fix
634
def logout(request, template='registration/logged_out.html',
635
           extra_context=None):
636
    """
637
    Wraps `django.contrib.auth.logout`.
638
    """
639
    extra_context = extra_context or {}
640
    response = HttpResponse()
641
    if request.user.is_authenticated():
642
        email = request.user.email
643
        auth_logout(request)
644
    else:
645
        response['Location'] = reverse('index')
646
        response.status_code = 301
647
        return response
648

    
649
    next = restrict_next(
650
        request.GET.get('next'),
651
        domain=COOKIE_DOMAIN
652
    )
653

    
654
    if next:
655
        response['Location'] = next
656
        response.status_code = 302
657
    elif LOGOUT_NEXT:
658
        response['Location'] = LOGOUT_NEXT
659
        response.status_code = 301
660
    else:
661
        last_provider = request.COOKIES.get('astakos_last_login_method', 'local')
662
        provider = auth.get_provider(last_provider)
663
        message = provider.get_logout_success_msg
664
        extra = provider.get_logout_success_extra_msg
665
        if extra:
666
            message += "<br />"  + extra
667
        messages.success(request, message)
668
        response['Location'] = reverse('index')
669
        response.status_code = 301
670
    return response
671

    
672

    
673
@require_http_methods(["GET", "POST"])
674
@cookie_fix
675
@transaction.commit_manually
676
def activate(request, greeting_email_template_name='im/welcome_email.txt',
677
             helpdesk_email_template_name='im/helpdesk_notification.txt'):
678
    """
679
    Activates the user identified by the ``auth`` request parameter, sends a
680
    welcome email and renews the user token.
681

682
    The view uses commit_manually decorator in order to ensure the user state
683
    will be updated only if the email will be send successfully.
684
    """
685
    token = request.GET.get('auth')
686
    next = request.GET.get('next')
687

    
688
    if request.user.is_authenticated():
689
        message = _(astakos_messages.LOGGED_IN_WARNING)
690
        messages.error(request, message)
691
        return HttpResponseRedirect(reverse('index'))
692

    
693
    try:
694
        user = AstakosUser.objects.get(verification_code=token)
695
    except AstakosUser.DoesNotExist:
696
        raise Http404
697

    
698
    if user.email_verified:
699
        message = _(astakos_messages.ACCOUNT_ALREADY_VERIFIED)
700
        messages.error(request, message)
701
        return HttpResponseRedirect(reverse('index'))
702

    
703
    try:
704
        backend = activation_backends.get_backend()
705
        result = backend.handle_verification(user, token)
706
        backend.send_result_notifications(result, user)
707
        next = ACTIVATION_REDIRECT_URL or next
708
        response = HttpResponseRedirect(reverse('index'))
709
        if user.is_active:
710
            response = prepare_response(request, user, next, renew=True)
711
            messages.success(request, _(result.message))
712
        else:
713
            messages.warning(request, _(result.message))
714
    except Exception:
715
        transaction.rollback()
716
        raise
717
    else:
718
        transaction.commit()
719
        return response
720

    
721

    
722
@require_http_methods(["GET", "POST"])
723
@cookie_fix
724
def approval_terms(request, term_id=None,
725
                   template_name='im/approval_terms.html', extra_context=None):
726
    extra_context = extra_context or {}
727
    term = None
728
    terms = None
729
    if not term_id:
730
        try:
731
            term = ApprovalTerms.objects.order_by('-id')[0]
732
        except IndexError:
733
            pass
734
    else:
735
        try:
736
            term = ApprovalTerms.objects.get(id=term_id)
737
        except ApprovalTerms.DoesNotExist, e:
738
            pass
739

    
740
    if not term:
741
        messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
742
        return HttpResponseRedirect(reverse('index'))
743
    try:
744
        f = open(term.location, 'r')
745
    except IOError:
746
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
747
        return render_response(
748
            template_name, context_instance=get_context(request,
749
                                                        extra_context))
750

    
751
    terms = f.read()
752

    
753
    if request.method == 'POST':
754
        next = restrict_next(
755
            request.POST.get('next'),
756
            domain=COOKIE_DOMAIN
757
        )
758
        if not next:
759
            next = reverse('index')
760
        form = SignApprovalTermsForm(request.POST, instance=request.user)
761
        if not form.is_valid():
762
            return render_response(template_name,
763
                                   terms=terms,
764
                                   approval_terms_form=form,
765
                                   context_instance=get_context(request,
766
                                                                extra_context))
767
        user = form.save()
768
        return HttpResponseRedirect(next)
769
    else:
770
        form = None
771
        if request.user.is_authenticated() and not request.user.signed_terms:
772
            form = SignApprovalTermsForm(instance=request.user)
773
        return render_response(template_name,
774
                               terms=terms,
775
                               approval_terms_form=form,
776
                               context_instance=get_context(request,
777
                                                            extra_context))
778

    
779

    
780
@require_http_methods(["GET", "POST"])
781
@cookie_fix
782
@transaction.commit_manually
783
def change_email(request, activation_key=None,
784
                 email_template_name='registration/email_change_email.txt',
785
                 form_template_name='registration/email_change_form.html',
786
                 confirm_template_name='registration/email_change_done.html',
787
                 extra_context=None):
788
    extra_context = extra_context or {}
789

    
790
    if not settings.EMAILCHANGE_ENABLED:
791
        raise PermissionDenied
792

    
793
    if activation_key:
794
        try:
795
            try:
796
                email_change = EmailChange.objects.get(
797
                    activation_key=activation_key)
798
            except EmailChange.DoesNotExist:
799
                transaction.rollback()
800
                logger.error("[change-email] Invalid or used activation "
801
                             "code, %s", activation_key)
802
                raise Http404
803

    
804
            if (request.user.is_authenticated() and \
805
                request.user == email_change.user) or not \
806
                    request.user.is_authenticated():
807
                user = EmailChange.objects.change_email(activation_key)
808
                msg = _(astakos_messages.EMAIL_CHANGED)
809
                messages.success(request, msg)
810
                transaction.commit()
811
                return HttpResponseRedirect(reverse('edit_profile'))
812
            else:
813
                logger.error("[change-email] Access from invalid user, %s %s",
814
                             email_change.user, request.user.log_display)
815
                transaction.rollback()
816
                raise PermissionDenied
817
        except ValueError, e:
818
            messages.error(request, e)
819
            transaction.rollback()
820
            return HttpResponseRedirect(reverse('index'))
821

    
822
        return render_response(confirm_template_name,
823
                               modified_user=user if 'user' in locals()
824
                               else None, context_instance=get_context(request,
825
                               extra_context))
826

    
827
    if not request.user.is_authenticated():
828
        path = quote(request.get_full_path())
829
        url = request.build_absolute_uri(reverse('index'))
830
        return HttpResponseRedirect(url + '?next=' + path)
831

    
832
    # clean up expired email changes
833
    if request.user.email_change_is_pending():
834
        change = request.user.emailchanges.get()
835
        if change.activation_key_expired():
836
            change.delete()
837
            transaction.commit()
838
            return HttpResponseRedirect(reverse('email_change'))
839

    
840
    form = EmailChangeForm(request.POST or None)
841
    if request.method == 'POST' and form.is_valid():
842
        try:
843
            ec = form.save(request, email_template_name, request)
844
        except Exception, e:
845
            transaction.rollback()
846
            raise
847
        else:
848
            msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
849
            messages.success(request, msg)
850
            transaction.commit()
851
            return HttpResponseRedirect(reverse('edit_profile'))
852

    
853
    if request.user.email_change_is_pending():
854
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
855

    
856
    return render_response(
857
        form_template_name,
858
        form=form,
859
        context_instance=get_context(request, extra_context)
860
    )
861

    
862

    
863
@cookie_fix
864
def send_activation(request, user_id, template_name='im/login.html',
865
                    extra_context=None):
866

    
867
    if request.user.is_authenticated():
868
        return HttpResponseRedirect(reverse('index'))
869

    
870
    extra_context = extra_context or {}
871
    try:
872
        u = AstakosUser.objects.get(id=user_id)
873
    except AstakosUser.DoesNotExist:
874
        messages.error(request, _(astakos_messages.ACCOUNT_UNKNOWN))
875
    else:
876
        if u.email_verified:
877
            logger.warning("[resend activation] Account already verified: %s",
878
                           u.log_display)
879

    
880
            messages.error(request,
881
                           _(astakos_messages.ACCOUNT_ALREADY_VERIFIED))
882
        else:
883
            activation_backend = activation_backends.get_backend()
884
            activation_backend.send_user_verification_email(u)
885
            messages.success(request, astakos_messages.ACTIVATION_SENT)
886

    
887
    return HttpResponseRedirect(reverse('index'))
888

    
889

    
890
@require_http_methods(["GET"])
891
@cookie_fix
892
@valid_astakos_user_required
893
def resource_usage(request):
894

    
895
    resources_meta = presentation.RESOURCES
896

    
897
    current_usage = quotas.get_user_quotas(request.user)
898
    current_usage = json.dumps(current_usage['system'])
899
    resource_catalog, resource_groups = _resources_catalog(for_usage=True)
900
    if resource_catalog is False:
901
        # on fail resource_groups contains the result object
902
        result = resource_groups
903
        messages.error(request, 'Unable to retrieve system resources: %s' %
904
                       result.reason)
905

    
906
    resource_catalog = json.dumps(resource_catalog)
907
    resource_groups = json.dumps(resource_groups)
908
    resources_order = json.dumps(resources_meta.get('resources_order'))
909

    
910
    return render_response('im/resource_usage.html',
911
                           context_instance=get_context(request),
912
                           resource_catalog=resource_catalog,
913
                           resource_groups=resource_groups,
914
                           resources_order=resources_order,
915
                           current_usage=current_usage,
916
                           token_cookie_name=settings.COOKIE_NAME,
917
                           usage_update_interval=
918
                           settings.USAGE_UPDATE_INTERVAL)
919

    
920

    
921
# TODO: action only on POST and user should confirm the removal
922
@require_http_methods(["GET", "POST"])
923
@cookie_fix
924
@valid_astakos_user_required
925
def remove_auth_provider(request, pk):
926
    try:
927
        provider = request.user.auth_providers.get(pk=int(pk)).settings
928
    except AstakosUserAuthProvider.DoesNotExist:
929
        raise Http404
930

    
931
    if provider.get_remove_policy:
932
        messages.success(request, provider.get_removed_msg)
933
        provider.remove_from_user()
934
        return HttpResponseRedirect(reverse('edit_profile'))
935
    else:
936
        raise PermissionDenied
937

    
938

    
939
@cookie_fix
940
def how_it_works(request):
941
    return render_response(
942
        'im/how_it_works.html',
943
        context_instance=get_context(request))
944

    
945

    
946
@commit_on_success_strict()
947
def _create_object(request, model=None, template_name=None,
948
        template_loader=template_loader, extra_context=None, post_save_redirect=None,
949
        login_required=False, context_processors=None, form_class=None,
950
        msg=None):
951
    """
952
    Based of django.views.generic.create_update.create_object which displays a
953
    summary page before creating the object.
954
    """
955
    response = None
956

    
957
    if extra_context is None: extra_context = {}
958
    if login_required and not request.user.is_authenticated():
959
        return redirect_to_login(request.path)
960
    try:
961

    
962
        model, form_class = get_model_and_form_class(model, form_class)
963
        extra_context['edit'] = 0
964
        if request.method == 'POST':
965
            form = form_class(request.POST, request.FILES)
966
            if form.is_valid():
967
                verify = request.GET.get('verify')
968
                edit = request.GET.get('edit')
969
                if verify == '1':
970
                    extra_context['show_form'] = False
971
                    extra_context['form_data'] = form.cleaned_data
972
                elif edit == '1':
973
                    extra_context['show_form'] = True
974
                else:
975
                    new_object = form.save()
976
                    if not msg:
977
                        msg = _("The %(verbose_name)s was created successfully.")
978
                    msg = msg % model._meta.__dict__
979
                    messages.success(request, msg, fail_silently=True)
980
                    response = redirect(post_save_redirect, new_object)
981
        else:
982
            form = form_class()
983
    except (IOError, PermissionDenied), e:
984
        messages.error(request, e)
985
        return None
986
    else:
987
        if response == None:
988
            # Create the template, context, response
989
            if not template_name:
990
                template_name = "%s/%s_form.html" %\
991
                     (model._meta.app_label, model._meta.object_name.lower())
992
            t = template_loader.get_template(template_name)
993
            c = RequestContext(request, {
994
                'form': form
995
            }, context_processors)
996
            apply_extra_context(extra_context, c)
997
            response = HttpResponse(t.render(c))
998
        return response
999

    
1000
@commit_on_success_strict()
1001
def _update_object(request, model=None, object_id=None, slug=None,
1002
        slug_field='slug', template_name=None, template_loader=template_loader,
1003
        extra_context=None, post_save_redirect=None, login_required=False,
1004
        context_processors=None, template_object_name='object',
1005
        form_class=None, msg=None):
1006
    """
1007
    Based of django.views.generic.create_update.update_object which displays a
1008
    summary page before updating the object.
1009
    """
1010
    response = None
1011

    
1012
    if extra_context is None: extra_context = {}
1013
    if login_required and not request.user.is_authenticated():
1014
        return redirect_to_login(request.path)
1015

    
1016
    try:
1017
        model, form_class = get_model_and_form_class(model, form_class)
1018
        obj = lookup_object(model, object_id, slug, slug_field)
1019

    
1020
        if request.method == 'POST':
1021
            form = form_class(request.POST, request.FILES, instance=obj)
1022
            if form.is_valid():
1023
                verify = request.GET.get('verify')
1024
                edit = request.GET.get('edit')
1025
                if verify == '1':
1026
                    extra_context['show_form'] = False
1027
                    extra_context['form_data'] = form.cleaned_data
1028
                elif edit == '1':
1029
                    extra_context['show_form'] = True
1030
                else:
1031
                    obj = form.save()
1032
                    if not msg:
1033
                        msg = _("The %(verbose_name)s was created successfully.")
1034
                    msg = msg % model._meta.__dict__
1035
                    messages.success(request, msg, fail_silently=True)
1036
                    response = redirect(post_save_redirect, obj)
1037
        else:
1038
            form = form_class(instance=obj)
1039
    except (IOError, PermissionDenied), e:
1040
        messages.error(request, e)
1041
        return None
1042
    else:
1043
        if response == None:
1044
            if not template_name:
1045
                template_name = "%s/%s_form.html" %\
1046
                    (model._meta.app_label, model._meta.object_name.lower())
1047
            t = template_loader.get_template(template_name)
1048
            c = RequestContext(request, {
1049
                'form': form,
1050
                template_object_name: obj,
1051
            }, context_processors)
1052
            apply_extra_context(extra_context, c)
1053
            response = HttpResponse(t.render(c))
1054
            populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.attname))
1055
        return response
1056

    
1057

    
1058

    
1059
def _resources_catalog(for_project=False, for_usage=False):
1060
    """
1061
    `resource_catalog` contains a list of tuples. Each tuple contains the group
1062
    key the resource is assigned to and resources list of dicts that contain
1063
    resource information.
1064
    `resource_groups` contains information about the groups
1065
    """
1066
    # presentation data
1067
    resources_meta = presentation.RESOURCES
1068
    resource_groups = resources_meta.get('groups', {})
1069
    resource_catalog = ()
1070
    resource_keys = []
1071

    
1072
    # resources in database
1073
    resource_details = map(lambda obj: model_to_dict(obj, exclude=[]),
1074
                           Resource.objects.all())
1075
    # initialize resource_catalog to contain all group/resource information
1076
    for r in resource_details:
1077
        if not r.get('group') in resource_groups:
1078
            resource_groups[r.get('group')] = {'icon': 'unknown'}
1079

    
1080
    resource_keys = [r.get('str_repr') for r in resource_details]
1081
    resource_catalog = [[g, filter(lambda r: r.get('group', '') == g,
1082
                                   resource_details)] for g in resource_groups]
1083

    
1084
    # order groups, also include unknown groups
1085
    groups_order = resources_meta.get('groups_order')
1086
    for g in resource_groups.keys():
1087
        if not g in groups_order:
1088
            groups_order.append(g)
1089

    
1090
    # order resources, also include unknown resources
1091
    resources_order = resources_meta.get('resources_order')
1092
    for r in resource_keys:
1093
        if not r in resources_order:
1094
            resources_order.append(r)
1095

    
1096
    # sort catalog groups
1097
    resource_catalog = sorted(resource_catalog,
1098
                              key=lambda g: groups_order.index(g[0]))
1099

    
1100
    # sort groups
1101
    def groupindex(g):
1102
        return groups_order.index(g[0])
1103
    resource_groups_list = sorted([(k, v) for k, v in resource_groups.items()],
1104
                                  key=groupindex)
1105
    resource_groups = OrderedDict(resource_groups_list)
1106

    
1107
    # sort resources
1108
    def resourceindex(r):
1109
        return resources_order.index(r['str_repr'])
1110

    
1111
    for index, group in enumerate(resource_catalog):
1112
        resource_catalog[index][1] = sorted(resource_catalog[index][1],
1113
                                            key=resourceindex)
1114
        if len(resource_catalog[index][1]) == 0:
1115
            resource_catalog.pop(index)
1116
            for gindex, g in enumerate(resource_groups):
1117
                if g[0] == group[0]:
1118
                    resource_groups.pop(gindex)
1119

    
1120
    # filter out resources which user cannot request in a project application
1121
    exclude = resources_meta.get('exclude_from_usage', [])
1122
    for group_index, group_resources in enumerate(list(resource_catalog)):
1123
        group, resources = group_resources
1124
        for index, resource in list(enumerate(resources)):
1125
            if for_project and not resource.get('allow_in_projects'):
1126
                resources.remove(resource)
1127
            if resource.get('str_repr') in exclude and for_usage:
1128
                resources.remove(resource)
1129

    
1130
    # cleanup empty groups
1131
    for group_index, group_resources in enumerate(list(resource_catalog)):
1132
        group, resources = group_resources
1133
        if len(resources) == 0:
1134
            resource_catalog.pop(group_index)
1135
            resource_groups.pop(group)
1136

    
1137

    
1138
    return resource_catalog, resource_groups
1139

    
1140

    
1141
@require_http_methods(["GET", "POST"])
1142
@cookie_fix
1143
@valid_astakos_user_required
1144
def project_add(request):
1145
    user = request.user
1146
    if not user.is_project_admin():
1147
        ok, limit = qh_add_pending_app(user, dry_run=True)
1148
        if not ok:
1149
            m = _(astakos_messages.PENDING_APPLICATION_LIMIT_ADD) % limit
1150
            messages.error(request, m)
1151
            next = reverse('astakos.im.views.project_list')
1152
            next = restrict_next(next, domain=COOKIE_DOMAIN)
1153
            return redirect(next)
1154

    
1155
    details_fields = ["name", "homepage", "description", "start_date",
1156
                      "end_date", "comments"]
1157
    membership_fields = ["member_join_policy", "member_leave_policy",
1158
                         "limit_on_members_number"]
1159
    resource_catalog, resource_groups = _resources_catalog(for_project=True)
1160
    if resource_catalog is False:
1161
        # on fail resource_groups contains the result object
1162
        result = resource_groups
1163
        messages.error(request, 'Unable to retrieve system resources: %s' %
1164
                       result.reason)
1165
    extra_context = {
1166
        'resource_catalog': resource_catalog,
1167
        'resource_groups': resource_groups,
1168
        'show_form': True,
1169
        'details_fields': details_fields,
1170
        'membership_fields': membership_fields}
1171

    
1172
    response = None
1173
    with ExceptionHandler(request):
1174
        response = _create_object(
1175
            request,
1176
            template_name='im/projects/projectapplication_form.html',
1177
            extra_context=extra_context,
1178
            post_save_redirect=reverse('project_list'),
1179
            form_class=ProjectApplicationForm,
1180
            msg=_("The %(verbose_name)s has been received and "
1181
                  "is under consideration."),
1182
            )
1183

    
1184
    if response is not None:
1185
        return response
1186

    
1187
    next = reverse('astakos.im.views.project_list')
1188
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1189
    return redirect(next)
1190

    
1191

    
1192
@require_http_methods(["GET"])
1193
@cookie_fix
1194
@valid_astakos_user_required
1195
def project_list(request):
1196
    projects = ProjectApplication.objects.user_accessible_projects(request.user).select_related()
1197
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1198
                                                prefix="my_projects_")
1199
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1200

    
1201
    return object_list(
1202
        request,
1203
        projects,
1204
        template_name='im/projects/project_list.html',
1205
        extra_context={
1206
            'is_search':False,
1207
            'table': table,
1208
        })
1209

    
1210

    
1211
@require_http_methods(["POST"])
1212
@cookie_fix
1213
@valid_astakos_user_required
1214
def project_app_cancel(request, application_id):
1215
    next = request.GET.get('next')
1216
    chain_id = None
1217

    
1218
    with ExceptionHandler(request):
1219
        chain_id = _project_app_cancel(request, application_id)
1220

    
1221
    if not next:
1222
        if chain_id:
1223
            next = reverse('astakos.im.views.project_detail', args=(chain_id,))
1224
        else:
1225
            next = reverse('astakos.im.views.project_list')
1226

    
1227
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1228
    return redirect(next)
1229

    
1230
@commit_on_success_strict()
1231
def _project_app_cancel(request, application_id):
1232
    chain_id = None
1233
    try:
1234
        application_id = int(application_id)
1235
        chain_id = get_related_project_id(application_id)
1236
        cancel_application(application_id, request.user)
1237
    except (IOError, PermissionDenied), e:
1238
        messages.error(request, e)
1239

    
1240
    else:
1241
        msg = _(astakos_messages.APPLICATION_CANCELLED)
1242
        messages.success(request, msg)
1243
        return chain_id
1244

    
1245

    
1246
@require_http_methods(["GET", "POST"])
1247
@cookie_fix
1248
@valid_astakos_user_required
1249
def project_modify(request, application_id):
1250

    
1251
    try:
1252
        app = ProjectApplication.objects.get(id=application_id)
1253
    except ProjectApplication.DoesNotExist:
1254
        raise Http404
1255

    
1256
    user = request.user
1257
    if not (user.owns_application(app) or user.is_project_admin(app.id)):
1258
        m = _(astakos_messages.NOT_ALLOWED)
1259
        raise PermissionDenied(m)
1260

    
1261
    if not user.is_project_admin():
1262
        owner = app.owner
1263
        ok, limit = qh_add_pending_app(owner, precursor=app, dry_run=True)
1264
        if not ok:
1265
            m = _(astakos_messages.PENDING_APPLICATION_LIMIT_MODIFY) % limit
1266
            messages.error(request, m)
1267
            next = reverse('astakos.im.views.project_list')
1268
            next = restrict_next(next, domain=COOKIE_DOMAIN)
1269
            return redirect(next)
1270

    
1271
    details_fields = ["name", "homepage", "description", "start_date",
1272
                      "end_date", "comments"]
1273
    membership_fields = ["member_join_policy", "member_leave_policy",
1274
                         "limit_on_members_number"]
1275
    resource_catalog, resource_groups = _resources_catalog(for_project=True)
1276
    if resource_catalog is False:
1277
        # on fail resource_groups contains the result object
1278
        result = resource_groups
1279
        messages.error(request, 'Unable to retrieve system resources: %s' %
1280
                       result.reason)
1281
    extra_context = {
1282
        'resource_catalog': resource_catalog,
1283
        'resource_groups': resource_groups,
1284
        'show_form': True,
1285
        'details_fields': details_fields,
1286
        'update_form': True,
1287
        'membership_fields': membership_fields
1288
    }
1289

    
1290
    response = None
1291
    with ExceptionHandler(request):
1292
        response = _update_object(
1293
            request,
1294
            object_id=application_id,
1295
            template_name='im/projects/projectapplication_form.html',
1296
            extra_context=extra_context,
1297
            post_save_redirect=reverse('project_list'),
1298
            form_class=ProjectApplicationForm,
1299
            msg=_("The %(verbose_name)s has been received and is under "
1300
                  "consideration."))
1301

    
1302
    if response is not None:
1303
        return response
1304

    
1305
    next = reverse('astakos.im.views.project_list')
1306
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1307
    return redirect(next)
1308

    
1309
@require_http_methods(["GET", "POST"])
1310
@cookie_fix
1311
@valid_astakos_user_required
1312
def project_app(request, application_id):
1313
    return common_detail(request, application_id, project_view=False)
1314

    
1315
@require_http_methods(["GET", "POST"])
1316
@cookie_fix
1317
@valid_astakos_user_required
1318
def project_detail(request, chain_id):
1319
    return common_detail(request, chain_id)
1320

    
1321
@commit_on_success_strict()
1322
def addmembers(request, chain_id, addmembers_form):
1323
    if addmembers_form.is_valid():
1324
        try:
1325
            chain_id = int(chain_id)
1326
            map(lambda u: enroll_member(
1327
                    chain_id,
1328
                    u,
1329
                    request_user=request.user),
1330
                addmembers_form.valid_users)
1331
        except (IOError, PermissionDenied), e:
1332
            messages.error(request, e)
1333

    
1334
def common_detail(request, chain_or_app_id, project_view=True):
1335
    project = None
1336
    if project_view:
1337
        chain_id = chain_or_app_id
1338
        if request.method == 'POST':
1339
            addmembers_form = AddProjectMembersForm(
1340
                request.POST,
1341
                chain_id=int(chain_id),
1342
                request_user=request.user)
1343
            with ExceptionHandler(request):
1344
                addmembers(request, chain_id, addmembers_form)
1345

    
1346
            if addmembers_form.is_valid():
1347
                addmembers_form = AddProjectMembersForm()  # clear form data
1348
        else:
1349
            addmembers_form = AddProjectMembersForm()  # initialize form
1350

    
1351
        project, application = get_by_chain_or_404(chain_id)
1352
        if project:
1353
            members = project.projectmembership_set.select_related()
1354
            members_table = tables.ProjectMembersTable(project,
1355
                                                       members,
1356
                                                       user=request.user,
1357
                                                       prefix="members_")
1358
            RequestConfig(request, paginate={"per_page": PAGINATE_BY}
1359
                          ).configure(members_table)
1360

    
1361
        else:
1362
            members_table = None
1363

    
1364
    else: # is application
1365
        application_id = chain_or_app_id
1366
        application = get_object_or_404(ProjectApplication, pk=application_id)
1367
        members_table = None
1368
        addmembers_form = None
1369

    
1370
    modifications_table = None
1371

    
1372
    user = request.user
1373
    is_project_admin = user.is_project_admin(application_id=application.id)
1374
    is_owner = user.owns_application(application)
1375
    if not (is_owner or is_project_admin) and not project_view:
1376
        m = _(astakos_messages.NOT_ALLOWED)
1377
        raise PermissionDenied(m)
1378

    
1379
    if (not (is_owner or is_project_admin) and project_view and
1380
        not user.non_owner_can_view(project)):
1381
        m = _(astakos_messages.NOT_ALLOWED)
1382
        raise PermissionDenied(m)
1383

    
1384
    following_applications = list(application.pending_modifications())
1385
    following_applications.reverse()
1386
    modifications_table = (
1387
        tables.ProjectModificationApplicationsTable(following_applications,
1388
                                                    user=request.user,
1389
                                                    prefix="modifications_"))
1390

    
1391
    mem_display = user.membership_display(project) if project else None
1392
    can_join_req = can_join_request(project, user) if project else False
1393
    can_leave_req = can_leave_request(project, user) if project else False
1394

    
1395
    return object_detail(
1396
        request,
1397
        queryset=ProjectApplication.objects.select_related(),
1398
        object_id=application.id,
1399
        template_name='im/projects/project_detail.html',
1400
        extra_context={
1401
            'project_view': project_view,
1402
            'addmembers_form':addmembers_form,
1403
            'members_table': members_table,
1404
            'owner_mode': is_owner,
1405
            'admin_mode': is_project_admin,
1406
            'modifications_table': modifications_table,
1407
            'mem_display': mem_display,
1408
            'can_join_request': can_join_req,
1409
            'can_leave_request': can_leave_req,
1410
            })
1411

    
1412
@require_http_methods(["GET", "POST"])
1413
@cookie_fix
1414
@valid_astakos_user_required
1415
def project_search(request):
1416
    q = request.GET.get('q', '')
1417
    form = ProjectSearchForm()
1418
    q = q.strip()
1419

    
1420
    if request.method == "POST":
1421
        form = ProjectSearchForm(request.POST)
1422
        if form.is_valid():
1423
            q = form.cleaned_data['q'].strip()
1424
        else:
1425
            q = None
1426

    
1427
    if q is None:
1428
        projects = ProjectApplication.objects.none()
1429
    else:
1430
        accepted_projects = request.user.projectmembership_set.filter(
1431
            ~Q(acceptance_date__isnull=True)).values_list('project', flat=True)
1432
        projects = ProjectApplication.objects.search_by_name(q)
1433
        projects = projects.filter(~Q(project__last_approval_date__isnull=True))
1434
        projects = projects.exclude(project__in=accepted_projects)
1435

    
1436
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1437
                                                prefix="my_projects_")
1438
    if request.method == "POST":
1439
        table.caption = _('SEARCH RESULTS')
1440
    else:
1441
        table.caption = _('ALL PROJECTS')
1442

    
1443
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1444

    
1445
    return object_list(
1446
        request,
1447
        projects,
1448
        template_name='im/projects/project_list.html',
1449
        extra_context={
1450
          'form': form,
1451
          'is_search': True,
1452
          'q': q,
1453
          'table': table
1454
        })
1455

    
1456
@require_http_methods(["POST"])
1457
@cookie_fix
1458
@valid_astakos_user_required
1459
def project_join(request, chain_id):
1460
    next = request.GET.get('next')
1461
    if not next:
1462
        next = reverse('astakos.im.views.project_detail',
1463
                       args=(chain_id,))
1464

    
1465
    with ExceptionHandler(request):
1466
        _project_join(request, chain_id)
1467

    
1468

    
1469
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1470
    return redirect(next)
1471

    
1472

    
1473
@commit_on_success_strict()
1474
def _project_join(request, chain_id):
1475
    try:
1476
        chain_id = int(chain_id)
1477
        auto_accepted = join_project(chain_id, request.user)
1478
        if auto_accepted:
1479
            m = _(astakos_messages.USER_JOINED_PROJECT)
1480
        else:
1481
            m = _(astakos_messages.USER_JOIN_REQUEST_SUBMITTED)
1482
        messages.success(request, m)
1483
    except (IOError, PermissionDenied), e:
1484
        messages.error(request, e)
1485

    
1486

    
1487
@require_http_methods(["POST"])
1488
@cookie_fix
1489
@valid_astakos_user_required
1490
def project_leave(request, chain_id):
1491
    next = request.GET.get('next')
1492
    if not next:
1493
        next = reverse('astakos.im.views.project_list')
1494

    
1495
    with ExceptionHandler(request):
1496
        _project_leave(request, chain_id)
1497

    
1498
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1499
    return redirect(next)
1500

    
1501

    
1502
@commit_on_success_strict()
1503
def _project_leave(request, chain_id):
1504
    try:
1505
        chain_id = int(chain_id)
1506
        auto_accepted = leave_project(chain_id, request.user)
1507
        if auto_accepted:
1508
            m = _(astakos_messages.USER_LEFT_PROJECT)
1509
        else:
1510
            m = _(astakos_messages.USER_LEAVE_REQUEST_SUBMITTED)
1511
        messages.success(request, m)
1512
    except (IOError, PermissionDenied), e:
1513
        messages.error(request, e)
1514

    
1515

    
1516
@require_http_methods(["POST"])
1517
@cookie_fix
1518
@valid_astakos_user_required
1519
def project_cancel(request, chain_id):
1520
    next = request.GET.get('next')
1521
    if not next:
1522
        next = reverse('astakos.im.views.project_list')
1523

    
1524
    with ExceptionHandler(request):
1525
        _project_cancel(request, chain_id)
1526

    
1527
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1528
    return redirect(next)
1529

    
1530

    
1531
@commit_on_success_strict()
1532
def _project_cancel(request, chain_id):
1533
    try:
1534
        chain_id = int(chain_id)
1535
        cancel_membership(chain_id, request.user)
1536
        m = _(astakos_messages.USER_REQUEST_CANCELLED)
1537
        messages.success(request, m)
1538
    except (IOError, PermissionDenied), e:
1539
        messages.error(request, e)
1540

    
1541

    
1542

    
1543
@require_http_methods(["POST"])
1544
@cookie_fix
1545
@valid_astakos_user_required
1546
def project_accept_member(request, chain_id, memb_id):
1547

    
1548
    with ExceptionHandler(request):
1549
        _project_accept_member(request, chain_id, memb_id)
1550

    
1551
    return redirect(reverse('project_detail', args=(chain_id,)))
1552

    
1553

    
1554
@commit_on_success_strict()
1555
def _project_accept_member(request, chain_id, memb_id):
1556
    try:
1557
        chain_id = int(chain_id)
1558
        memb_id = int(memb_id)
1559
        m = accept_membership(chain_id, memb_id, request.user)
1560
    except (IOError, PermissionDenied), e:
1561
        messages.error(request, e)
1562

    
1563
    else:
1564
        email = escape(m.person.email)
1565
        msg = _(astakos_messages.USER_MEMBERSHIP_ACCEPTED) % email
1566
        messages.success(request, msg)
1567

    
1568

    
1569
@require_http_methods(["POST"])
1570
@cookie_fix
1571
@valid_astakos_user_required
1572
def project_remove_member(request, chain_id, memb_id):
1573

    
1574
    with ExceptionHandler(request):
1575
        _project_remove_member(request, chain_id, memb_id)
1576

    
1577
    return redirect(reverse('project_detail', args=(chain_id,)))
1578

    
1579

    
1580
@commit_on_success_strict()
1581
def _project_remove_member(request, chain_id, memb_id):
1582
    try:
1583
        chain_id = int(chain_id)
1584
        memb_id = int(memb_id)
1585
        m = remove_membership(chain_id, memb_id, request.user)
1586
    except (IOError, PermissionDenied), e:
1587
        messages.error(request, e)
1588
    else:
1589
        email = escape(m.person.email)
1590
        msg = _(astakos_messages.USER_MEMBERSHIP_REMOVED) % email
1591
        messages.success(request, msg)
1592

    
1593

    
1594
@require_http_methods(["POST"])
1595
@cookie_fix
1596
@valid_astakos_user_required
1597
def project_reject_member(request, chain_id, memb_id):
1598

    
1599
    with ExceptionHandler(request):
1600
        _project_reject_member(request, chain_id, memb_id)
1601

    
1602
    return redirect(reverse('project_detail', args=(chain_id,)))
1603

    
1604

    
1605
@commit_on_success_strict()
1606
def _project_reject_member(request, chain_id, memb_id):
1607
    try:
1608
        chain_id = int(chain_id)
1609
        memb_id = int(memb_id)
1610
        m = reject_membership(chain_id, memb_id, request.user)
1611
    except (IOError, PermissionDenied), e:
1612
        messages.error(request, e)
1613
    else:
1614
        email = escape(m.person.email)
1615
        msg = _(astakos_messages.USER_MEMBERSHIP_REJECTED) % email
1616
        messages.success(request, msg)
1617

    
1618

    
1619
@require_http_methods(["POST"])
1620
@signed_terms_required
1621
@login_required
1622
@cookie_fix
1623
def project_app_approve(request, application_id):
1624

    
1625
    if not request.user.is_project_admin():
1626
        m = _(astakos_messages.NOT_ALLOWED)
1627
        raise PermissionDenied(m)
1628

    
1629
    try:
1630
        app = ProjectApplication.objects.get(id=application_id)
1631
    except ProjectApplication.DoesNotExist:
1632
        raise Http404
1633

    
1634
    with ExceptionHandler(request):
1635
        _project_app_approve(request, application_id)
1636

    
1637
    chain_id = get_related_project_id(application_id)
1638
    return redirect(reverse('project_detail', args=(chain_id,)))
1639

    
1640

    
1641
@commit_on_success_strict()
1642
def _project_app_approve(request, application_id):
1643
    approve_application(application_id)
1644

    
1645

    
1646
@require_http_methods(["POST"])
1647
@signed_terms_required
1648
@login_required
1649
@cookie_fix
1650
def project_app_deny(request, application_id):
1651

    
1652
    reason = request.POST.get('reason', None)
1653
    if not reason:
1654
        reason = None
1655

    
1656
    if not request.user.is_project_admin():
1657
        m = _(astakos_messages.NOT_ALLOWED)
1658
        raise PermissionDenied(m)
1659

    
1660
    try:
1661
        app = ProjectApplication.objects.get(id=application_id)
1662
    except ProjectApplication.DoesNotExist:
1663
        raise Http404
1664

    
1665
    with ExceptionHandler(request):
1666
        _project_app_deny(request, application_id, reason)
1667

    
1668
    return redirect(reverse('project_list'))
1669

    
1670

    
1671
@commit_on_success_strict()
1672
def _project_app_deny(request, application_id, reason):
1673
    deny_application(application_id, reason=reason)
1674

    
1675

    
1676
@require_http_methods(["POST"])
1677
@signed_terms_required
1678
@login_required
1679
@cookie_fix
1680
def project_app_dismiss(request, application_id):
1681
    try:
1682
        app = ProjectApplication.objects.get(id=application_id)
1683
    except ProjectApplication.DoesNotExist:
1684
        raise Http404
1685

    
1686
    if not request.user.owns_application(app):
1687
        m = _(astakos_messages.NOT_ALLOWED)
1688
        raise PermissionDenied(m)
1689

    
1690
    with ExceptionHandler(request):
1691
        _project_app_dismiss(request, application_id)
1692

    
1693
    chain_id = None
1694
    chain_id = get_related_project_id(application_id)
1695
    if chain_id:
1696
        next = reverse('project_detail', args=(chain_id,))
1697
    else:
1698
        next = reverse('project_list')
1699
    return redirect(next)
1700

    
1701

    
1702
def _project_app_dismiss(request, application_id):
1703
    # XXX: dismiss application also does authorization
1704
    dismiss_application(application_id, request_user=request.user)
1705

    
1706

    
1707
@require_http_methods(["GET"])
1708
@required_auth_methods_assigned(allow_access=True)
1709
@login_required
1710
@cookie_fix
1711
@signed_terms_required
1712
def landing(request):
1713
    context = {'services': Service.catalog(orderfor='dashboard')}
1714
    return render_response(
1715
        'im/landing.html',
1716
        context_instance=get_context(request), **context)
1717

    
1718

    
1719
def api_access(request):
1720
    return render_response(
1721
        'im/api_access.html',
1722
        context_instance=get_context(request))