Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ e7cb4085

History | View | Annotate | Download (58.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

    
117
logger = logging.getLogger(__name__)
118

    
119

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

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

    
142
            if not provider or not provider.is_active():
143
                raise PermissionDenied
144

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

    
154

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

    
169

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

    
185

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

    
192
    def decorator(func):
193
        @wraps(func)
194
        def wrapper(request, *args, **kwargs):
195
            if request.user.is_authenticated():
196
                missing = request.user.missing_required_providers()
197
                if missing:
198
                    for provider in missing:
199
                        messages.error(request,
200
                                       provider.get_required_msg)
201
                    if not allow_access:
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(["POST"])
253
@valid_astakos_user_required
254
def update_token(request):
255
    """
256
    Update api token view.
257
    """
258
    user = request.user
259
    user.renew_token()
260
    user.save()
261
    messages.success(request, astakos_messages.TOKEN_UPDATED)
262
    return HttpResponseRedirect(reverse('edit_profile'))
263

    
264

    
265
@require_http_methods(["GET", "POST"])
266
@valid_astakos_user_required
267
@transaction.commit_manually
268
def invite(request, template_name='im/invitations.html', extra_context=None):
269
    """
270
    Allows a user to invite somebody else.
271

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

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

279
    If the user isn't logged in, redirects to settings.LOGIN_URL.
280

281
    **Arguments**
282

283
    ``template_name``
284
        A custom template to use. This is optional; if not specified,
285
        this will default to ``im/invitations.html``.
286

287
    ``extra_context``
288
        An dictionary of variables to add to the template context.
289

290
    **Template:**
291

292
    im/invitations.html or ``template_name`` keyword argument.
293

294
    **Settings:**
295

296
    The view expectes the following settings are defined:
297

298
    * LOGIN_URL: login uri
299
    """
300
    extra_context = extra_context or {}
301
    status = None
302
    message = None
303
    form = InvitationForm()
304

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

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

    
336

    
337
@require_http_methods(["GET", "POST"])
338
@required_auth_methods_assigned(allow_access=True)
339
@login_required
340
@signed_terms_required
341
def edit_profile(request, template_name='im/profile.html', extra_context=None):
342
    """
343
    Allows a user to edit his/her profile.
344

345
    In case of GET request renders a form for displaying the user information.
346
    In case of POST updates the user informantion and redirects to ``next``
347
    url parameter if exists.
348

349
    If the user isn't logged in, redirects to settings.LOGIN_URL.
350

351
    **Arguments**
352

353
    ``template_name``
354
        A custom template to use. This is optional; if not specified,
355
        this will default to ``im/profile.html``.
356

357
    ``extra_context``
358
        An dictionary of variables to add to the template context.
359

360
    **Template:**
361

362
    im/profile.html or ``template_name`` keyword argument.
363

364
    **Settings:**
365

366
    The view expectes the following settings are defined:
367

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

    
393
                if form.email_changed:
394
                    msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
395
                    messages.success(request, msg)
396
                if form.password_changed:
397
                    msg = _(astakos_messages.PASSWORD_CHANGED)
398
                    messages.success(request, msg)
399

    
400
                if next:
401
                    return redirect(next)
402
                else:
403
                    return redirect(reverse('edit_profile'))
404
            except ValueError, ve:
405
                messages.success(request, ve)
406
    elif request.method == "GET":
407
        request.user.is_verified = True
408
        request.user.save()
409

    
410
    # existing providers
411
    user_providers = request.user.get_enabled_auth_providers()
412
    user_disabled_providers = request.user.get_disabled_auth_providers()
413

    
414
    # providers that user can add
415
    user_available_providers = request.user.get_available_auth_providers()
416

    
417
    extra_context['services'] = Service.catalog().values()
418
    return render_response(template_name,
419
                           profile_form=form,
420
                           user_providers=user_providers,
421
                           user_disabled_providers=user_disabled_providers,
422
                           user_available_providers=user_available_providers,
423
                           context_instance=get_context(request,
424
                                                          extra_context))
425

    
426

    
427
@transaction.commit_manually
428
@require_http_methods(["GET", "POST"])
429
def signup(request, template_name='im/signup.html', on_success='index',
430
           extra_context=None, activation_backend=None):
431
    """
432
    Allows a user to create a local account.
433

434
    In case of GET request renders a form for entering the user information.
435
    In case of POST handles the signup.
436

437
    The user activation will be delegated to the backend specified by the
438
    ``activation_backend`` keyword argument if present, otherwise to the
439
    ``astakos.im.activation_backends.InvitationBackend`` if
440
    settings.ASTAKOS_INVITATIONS_ENABLED is True or
441
    ``astakos.im.activation_backends.SimpleBackend`` if not (see
442
    activation_backends);
443

444
    Upon successful user creation, if ``next`` url parameter is present the
445
    user is redirected there otherwise renders the same page with a success
446
    message.
447

448
    On unsuccessful creation, renders ``template_name`` with an error message.
449

450
    **Arguments**
451

452
    ``template_name``
453
        A custom template to render. This is optional;
454
        if not specified, this will default to ``im/signup.html``.
455

456
    ``extra_context``
457
        An dictionary of variables to add to the template context.
458

459
    ``on_success``
460
        Resolvable view name to redirect on registration success.
461

462
    **Template:**
463

464
    im/signup.html or ``template_name`` keyword argument.
465
    """
466
    extra_context = extra_context or {}
467
    if request.user.is_authenticated():
468
        logger.info("%s already signed in, redirect to index",
469
                    request.user.log_display)
470
        return HttpResponseRedirect(reverse('index'))
471

    
472
    provider = get_query(request).get('provider', 'local')
473
    if not auth.get_provider(provider).get_create_policy:
474
        logger.error("%s provider not available for signup", provider)
475
        raise PermissionDenied
476

    
477
    instance = None
478

    
479
    # user registered using third party provider
480
    third_party_token = request.REQUEST.get('third_party_token', None)
481
    unverified = None
482
    if third_party_token:
483
        # retreive third party entry. This was created right after the initial
484
        # third party provider handshake.
485
        pending = get_object_or_404(PendingThirdPartyUser,
486
                                    token=third_party_token)
487

    
488
        provider = pending.provider
489

    
490
        # clone third party instance into the corresponding AstakosUser
491
        instance = pending.get_user_instance()
492
        get_unverified = AstakosUserAuthProvider.objects.unverified
493

    
494
        # check existing unverified entries
495
        unverified = get_unverified(pending.provider,
496
                                    identifier=pending.third_party_identifier)
497

    
498
        if unverified and request.method == 'GET':
499
            messages.warning(request, unverified.get_pending_registration_msg)
500
            if unverified.user.moderated:
501
                messages.warning(request,
502
                                 unverified.get_pending_resend_activation_msg)
503
            else:
504
                messages.warning(request,
505
                                 unverified.get_pending_moderation_msg)
506

    
507
    # prepare activation backend based on current request
508
    if not activation_backend:
509
        activation_backend = activation_backends.get_backend()
510

    
511
    form_kwargs = {'instance': instance}
512
    if third_party_token:
513
        form_kwargs['third_party_token'] = third_party_token
514

    
515
    form = activation_backend.get_signup_form(
516
        provider, None, **form_kwargs)
517

    
518
    if request.method == 'POST':
519
        form = activation_backend.get_signup_form(
520
            provider,
521
            request.POST,
522
            **form_kwargs)
523

    
524
        if form.is_valid():
525
            commited = False
526
            try:
527
                user = form.save(commit=False)
528

    
529
                # delete previously unverified accounts
530
                if AstakosUser.objects.user_exists(user.email):
531
                    AstakosUser.objects.get_by_identifier(user.email).delete()
532

    
533
                # store_user so that user auth providers get initialized
534
                form.store_user(user, request)
535
                result = activation_backend.handle_registration(user)
536
                if result.status == \
537
                        activation_backend.Result.PENDING_MODERATION:
538
                    # user should be warned that his account is not active yet
539
                    status = messages.WARNING
540
                else:
541
                    status = messages.SUCCESS
542
                message = result.message
543
                activation_backend.send_result_notifications(result, user)
544

    
545
                # commit user entry
546
                transaction.commit()
547
                # commited flag
548
                # in case an exception get raised from this point
549
                commited = True
550

    
551
                if user and user.is_active:
552
                    # activation backend directly activated the user
553
                    # log him in
554
                    next = request.POST.get('next', '')
555
                    response = prepare_response(request, user, next=next)
556
                    return response
557

    
558
                messages.add_message(request, status, message)
559
                return HttpResponseRedirect(reverse(on_success))
560
            except Exception, e:
561
                if not commited:
562
                    transaction.rollback()
563
                raise
564

    
565
    return render_response(template_name,
566
                           signup_form=form,
567
                           third_party_token=third_party_token,
568
                           provider=provider,
569
                           context_instance=get_context(request, extra_context))
570

    
571

    
572
@require_http_methods(["GET", "POST"])
573
@required_auth_methods_assigned(allow_access=True)
574
@login_required
575
@signed_terms_required
576
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
577
    """
578
    Allows a user to send feedback.
579

580
    In case of GET request renders a form for providing the feedback information.
581
    In case of POST sends an email to support team.
582

583
    If the user isn't logged in, redirects to settings.LOGIN_URL.
584

585
    **Arguments**
586

587
    ``template_name``
588
        A custom template to use. This is optional; if not specified,
589
        this will default to ``im/feedback.html``.
590

591
    ``extra_context``
592
        An dictionary of variables to add to the template context.
593

594
    **Template:**
595

596
    im/signup.html or ``template_name`` keyword argument.
597

598
    **Settings:**
599

600
    * LOGIN_URL: login uri
601
    """
602
    extra_context = extra_context or {}
603
    if request.method == 'GET':
604
        form = FeedbackForm()
605
    if request.method == 'POST':
606
        if not request.user:
607
            return HttpResponse('Unauthorized', status=401)
608

    
609
        form = FeedbackForm(request.POST)
610
        if form.is_valid():
611
            msg = form.cleaned_data['feedback_msg']
612
            data = form.cleaned_data['feedback_data']
613
            send_feedback(msg, data, request.user, email_template_name)
614
            message = _(astakos_messages.FEEDBACK_SENT)
615
            messages.success(request, message)
616
            return HttpResponseRedirect(reverse('feedback'))
617

    
618
    return render_response(template_name,
619
                           feedback_form=form,
620
                           context_instance=get_context(request,
621
                                                        extra_context))
622

    
623

    
624
@require_http_methods(["GET"])
625
def logout(request, template='registration/logged_out.html',
626
           extra_context=None):
627
    """
628
    Wraps `django.contrib.auth.logout`.
629
    """
630
    extra_context = extra_context or {}
631
    response = HttpResponse()
632
    if request.user.is_authenticated():
633
        email = request.user.email
634
        auth_logout(request)
635
    else:
636
        response['Location'] = reverse('index')
637
        response.status_code = 301
638
        return response
639

    
640
    next = restrict_next(
641
        request.GET.get('next'),
642
        domain=COOKIE_DOMAIN
643
    )
644

    
645
    if next:
646
        response['Location'] = next
647
        response.status_code = 302
648
    elif LOGOUT_NEXT:
649
        response['Location'] = LOGOUT_NEXT
650
        response.status_code = 301
651
    else:
652
        last_provider = request.COOKIES.get('astakos_last_login_method', 'local')
653
        provider = auth.get_provider(last_provider)
654
        message = provider.get_logout_success_msg
655
        extra = provider.get_logout_success_extra_msg
656
        if extra:
657
            message += "<br />"  + extra
658
        messages.success(request, message)
659
        response['Location'] = reverse('index')
660
        response.status_code = 301
661
    return response
662

    
663

    
664
@require_http_methods(["GET", "POST"])
665
@transaction.commit_manually
666
def activate(request, greeting_email_template_name='im/welcome_email.txt',
667
             helpdesk_email_template_name='im/helpdesk_notification.txt'):
668
    """
669
    Activates the user identified by the ``auth`` request parameter, sends a
670
    welcome email and renews the user token.
671

672
    The view uses commit_manually decorator in order to ensure the user state
673
    will be updated only if the email will be send successfully.
674
    """
675
    token = request.GET.get('auth')
676
    next = request.GET.get('next')
677

    
678
    if request.user.is_authenticated():
679
        message = _(astakos_messages.LOGGED_IN_WARNING)
680
        messages.error(request, message)
681
        return HttpResponseRedirect(reverse('index'))
682

    
683
    try:
684
        user = AstakosUser.objects.get(verification_code=token)
685
    except AstakosUser.DoesNotExist:
686
        raise Http404
687

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

    
693
    try:
694
        backend = activation_backends.get_backend()
695
        result = backend.handle_verification(user, token)
696
        backend.send_result_notifications(result, user)
697
        next = ACTIVATION_REDIRECT_URL or next
698
        response = HttpResponseRedirect(reverse('index'))
699
        if user.is_active:
700
            response = prepare_response(request, user, next, renew=True)
701
            messages.success(request, _(result.message))
702
        else:
703
            messages.warning(request, _(result.message))
704
    except Exception:
705
        transaction.rollback()
706
        raise
707
    else:
708
        transaction.commit()
709
        return response
710

    
711

    
712
@require_http_methods(["GET", "POST"])
713
def approval_terms(request, term_id=None,
714
                   template_name='im/approval_terms.html', extra_context=None):
715
    extra_context = extra_context or {}
716
    term = None
717
    terms = None
718
    if not term_id:
719
        try:
720
            term = ApprovalTerms.objects.order_by('-id')[0]
721
        except IndexError:
722
            pass
723
    else:
724
        try:
725
            term = ApprovalTerms.objects.get(id=term_id)
726
        except ApprovalTerms.DoesNotExist, e:
727
            pass
728

    
729
    if not term:
730
        messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
731
        return HttpResponseRedirect(reverse('index'))
732
    try:
733
        f = open(term.location, 'r')
734
    except IOError:
735
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
736
        return render_response(
737
            template_name, context_instance=get_context(request,
738
                                                        extra_context))
739

    
740
    terms = f.read()
741

    
742
    if request.method == 'POST':
743
        next = restrict_next(
744
            request.POST.get('next'),
745
            domain=COOKIE_DOMAIN
746
        )
747
        if not next:
748
            next = reverse('index')
749
        form = SignApprovalTermsForm(request.POST, instance=request.user)
750
        if not form.is_valid():
751
            return render_response(template_name,
752
                                   terms=terms,
753
                                   approval_terms_form=form,
754
                                   context_instance=get_context(request,
755
                                                                extra_context))
756
        user = form.save()
757
        return HttpResponseRedirect(next)
758
    else:
759
        form = None
760
        if request.user.is_authenticated() and not request.user.signed_terms:
761
            form = SignApprovalTermsForm(instance=request.user)
762
        return render_response(template_name,
763
                               terms=terms,
764
                               approval_terms_form=form,
765
                               context_instance=get_context(request,
766
                                                            extra_context))
767

    
768

    
769
@require_http_methods(["GET", "POST"])
770
@transaction.commit_manually
771
def change_email(request, activation_key=None,
772
                 email_template_name='registration/email_change_email.txt',
773
                 form_template_name='registration/email_change_form.html',
774
                 confirm_template_name='registration/email_change_done.html',
775
                 extra_context=None):
776
    extra_context = extra_context or {}
777

    
778
    if not settings.EMAILCHANGE_ENABLED:
779
        raise PermissionDenied
780

    
781
    if activation_key:
782
        try:
783
            user = EmailChange.objects.change_email(activation_key)
784
            if request.user.is_authenticated() and \
785
                request.user == user or not \
786
                    request.user.is_authenticated():
787
                msg = _(astakos_messages.EMAIL_CHANGED)
788
                messages.success(request, msg)
789
                transaction.commit()
790
                return HttpResponseRedirect(reverse('edit_profile'))
791
        except ValueError, e:
792
            messages.error(request, e)
793
            transaction.rollback()
794
            return HttpResponseRedirect(reverse('index'))
795

    
796
        return render_response(confirm_template_name,
797
                               modified_user=user if 'user' in locals()
798
                               else None, context_instance=get_context(request,
799
                               extra_context))
800

    
801
    if not request.user.is_authenticated():
802
        path = quote(request.get_full_path())
803
        url = request.build_absolute_uri(reverse('index'))
804
        return HttpResponseRedirect(url + '?next=' + path)
805

    
806
    # clean up expired email changes
807
    if request.user.email_change_is_pending():
808
        change = request.user.emailchanges.get()
809
        if change.activation_key_expired():
810
            change.delete()
811
            transaction.commit()
812
            return HttpResponseRedirect(reverse('email_change'))
813

    
814
    form = EmailChangeForm(request.POST or None)
815
    if request.method == 'POST' and form.is_valid():
816
        try:
817
            ec = form.save(request, email_template_name, request)
818
        except Exception, e:
819
            transaction.rollback()
820
            raise
821
        else:
822
            msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
823
            messages.success(request, msg)
824
            transaction.commit()
825
            return HttpResponseRedirect(reverse('edit_profile'))
826

    
827
    if request.user.email_change_is_pending():
828
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
829

    
830
    return render_response(
831
        form_template_name,
832
        form=form,
833
        context_instance=get_context(request, extra_context)
834
    )
835

    
836

    
837
def send_activation(request, user_id, template_name='im/login.html',
838
                    extra_context=None):
839

    
840
    if request.user.is_authenticated():
841
        return HttpResponseRedirect(reverse('index'))
842

    
843
    extra_context = extra_context or {}
844
    try:
845
        u = AstakosUser.objects.get(id=user_id)
846
    except AstakosUser.DoesNotExist:
847
        messages.error(request, _(astakos_messages.ACCOUNT_UNKNOWN))
848
    else:
849
        if u.email_verified:
850
            logger.warning("[resend activation] Account already verified: %s",
851
                           u.log_display)
852

    
853
            messages.error(request,
854
                           _(astakos_messages.ACCOUNT_ALREADY_VERIFIED))
855
        else:
856
            activation_backend = activation_backends.get_backend()
857
            activation_backend.send_user_verification_email(u)
858
            messages.success(request, astakos_messages.ACTIVATION_SENT)
859

    
860
    return HttpResponseRedirect(reverse('index'))
861

    
862

    
863
@require_http_methods(["GET"])
864
@valid_astakos_user_required
865
def resource_usage(request):
866

    
867
    resources_meta = presentation.RESOURCES
868

    
869
    current_usage = quotas.get_user_quotas(request.user)
870
    current_usage = json.dumps(current_usage['system'])
871
    resource_catalog, resource_groups = _resources_catalog(for_usage=True)
872
    if resource_catalog is False:
873
        # on fail resource_groups contains the result object
874
        result = resource_groups
875
        messages.error(request, 'Unable to retrieve system resources: %s' %
876
                       result.reason)
877

    
878
    resource_catalog = json.dumps(resource_catalog)
879
    resource_groups = json.dumps(resource_groups)
880
    resources_order = json.dumps(resources_meta.get('resources_order'))
881

    
882
    return render_response('im/resource_usage.html',
883
                           context_instance=get_context(request),
884
                           resource_catalog=resource_catalog,
885
                           resource_groups=resource_groups,
886
                           resources_order=resources_order,
887
                           current_usage=current_usage,
888
                           token_cookie_name=settings.COOKIE_NAME,
889
                           usage_update_interval=
890
                           settings.USAGE_UPDATE_INTERVAL)
891

    
892

    
893
# TODO: action only on POST and user should confirm the removal
894
@require_http_methods(["GET", "POST"])
895
@valid_astakos_user_required
896
def remove_auth_provider(request, pk):
897
    try:
898
        provider = request.user.auth_providers.get(pk=int(pk)).settings
899
    except AstakosUserAuthProvider.DoesNotExist:
900
        raise Http404
901

    
902
    if provider.get_remove_policy:
903
        messages.success(request, provider.get_removed_msg)
904
        provider.remove_from_user()
905
        return HttpResponseRedirect(reverse('edit_profile'))
906
    else:
907
        raise PermissionDenied
908

    
909

    
910
def how_it_works(request):
911
    return render_response(
912
        'im/how_it_works.html',
913
        context_instance=get_context(request))
914

    
915

    
916
@commit_on_success_strict()
917
def _create_object(request, model=None, template_name=None,
918
        template_loader=template_loader, extra_context=None, post_save_redirect=None,
919
        login_required=False, context_processors=None, form_class=None,
920
        msg=None):
921
    """
922
    Based of django.views.generic.create_update.create_object which displays a
923
    summary page before creating the object.
924
    """
925
    response = None
926

    
927
    if extra_context is None: extra_context = {}
928
    if login_required and not request.user.is_authenticated():
929
        return redirect_to_login(request.path)
930
    try:
931

    
932
        model, form_class = get_model_and_form_class(model, form_class)
933
        extra_context['edit'] = 0
934
        if request.method == 'POST':
935
            form = form_class(request.POST, request.FILES)
936
            if form.is_valid():
937
                verify = request.GET.get('verify')
938
                edit = request.GET.get('edit')
939
                if verify == '1':
940
                    extra_context['show_form'] = False
941
                    extra_context['form_data'] = form.cleaned_data
942
                elif edit == '1':
943
                    extra_context['show_form'] = True
944
                else:
945
                    new_object = form.save()
946
                    if not msg:
947
                        msg = _("The %(verbose_name)s was created successfully.")
948
                    msg = msg % model._meta.__dict__
949
                    messages.success(request, msg, fail_silently=True)
950
                    response = redirect(post_save_redirect, new_object)
951
        else:
952
            form = form_class()
953
    except (IOError, PermissionDenied), e:
954
        messages.error(request, e)
955
        return None
956
    else:
957
        if response == None:
958
            # Create the template, context, response
959
            if not template_name:
960
                template_name = "%s/%s_form.html" %\
961
                     (model._meta.app_label, model._meta.object_name.lower())
962
            t = template_loader.get_template(template_name)
963
            c = RequestContext(request, {
964
                'form': form
965
            }, context_processors)
966
            apply_extra_context(extra_context, c)
967
            response = HttpResponse(t.render(c))
968
        return response
969

    
970
@commit_on_success_strict()
971
def _update_object(request, model=None, object_id=None, slug=None,
972
        slug_field='slug', template_name=None, template_loader=template_loader,
973
        extra_context=None, post_save_redirect=None, login_required=False,
974
        context_processors=None, template_object_name='object',
975
        form_class=None, msg=None):
976
    """
977
    Based of django.views.generic.create_update.update_object which displays a
978
    summary page before updating the object.
979
    """
980
    response = None
981

    
982
    if extra_context is None: extra_context = {}
983
    if login_required and not request.user.is_authenticated():
984
        return redirect_to_login(request.path)
985

    
986
    try:
987
        model, form_class = get_model_and_form_class(model, form_class)
988
        obj = lookup_object(model, object_id, slug, slug_field)
989

    
990
        if request.method == 'POST':
991
            form = form_class(request.POST, request.FILES, instance=obj)
992
            if form.is_valid():
993
                verify = request.GET.get('verify')
994
                edit = request.GET.get('edit')
995
                if verify == '1':
996
                    extra_context['show_form'] = False
997
                    extra_context['form_data'] = form.cleaned_data
998
                elif edit == '1':
999
                    extra_context['show_form'] = True
1000
                else:
1001
                    obj = form.save()
1002
                    if not msg:
1003
                        msg = _("The %(verbose_name)s was created successfully.")
1004
                    msg = msg % model._meta.__dict__
1005
                    messages.success(request, msg, fail_silently=True)
1006
                    response = redirect(post_save_redirect, obj)
1007
        else:
1008
            form = form_class(instance=obj)
1009
    except (IOError, PermissionDenied), e:
1010
        messages.error(request, e)
1011
        return None
1012
    else:
1013
        if response == None:
1014
            if not template_name:
1015
                template_name = "%s/%s_form.html" %\
1016
                    (model._meta.app_label, model._meta.object_name.lower())
1017
            t = template_loader.get_template(template_name)
1018
            c = RequestContext(request, {
1019
                'form': form,
1020
                template_object_name: obj,
1021
            }, context_processors)
1022
            apply_extra_context(extra_context, c)
1023
            response = HttpResponse(t.render(c))
1024
            populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.attname))
1025
        return response
1026

    
1027

    
1028
def _resources_catalog(for_project=False, for_usage=False):
1029
    """
1030
    `resource_catalog` contains a list of tuples. Each tuple contains the group
1031
    key the resource is assigned to and resources list of dicts that contain
1032
    resource information.
1033
    `resource_groups` contains information about the groups
1034
    """
1035
    # presentation data
1036
    resources_meta = presentation.RESOURCES
1037
    resource_groups = resources_meta.get('groups', {})
1038
    resource_catalog = ()
1039
    resource_keys = []
1040

    
1041
    # resources in database
1042
    resource_details = map(lambda obj: model_to_dict(obj, exclude=[]),
1043
                           Resource.objects.all())
1044
    # initialize resource_catalog to contain all group/resource information
1045
    for r in resource_details:
1046
        if not r.get('group') in resource_groups:
1047
            resource_groups[r.get('group')] = {'icon': 'unknown'}
1048

    
1049
    resource_keys = [r.get('str_repr') for r in resource_details]
1050
    resource_catalog = [[g, filter(lambda r: r.get('group', '') == g,
1051
                                   resource_details)] for g in resource_groups]
1052

    
1053
    # order groups, also include unknown groups
1054
    groups_order = resources_meta.get('groups_order')
1055
    for g in resource_groups.keys():
1056
        if not g in groups_order:
1057
            groups_order.append(g)
1058

    
1059
    # order resources, also include unknown resources
1060
    resources_order = resources_meta.get('resources_order')
1061
    for r in resource_keys:
1062
        if not r in resources_order:
1063
            resources_order.append(r)
1064

    
1065
    # sort catalog groups
1066
    resource_catalog = sorted(resource_catalog,
1067
                              key=lambda g: groups_order.index(g[0]))
1068

    
1069
    # sort groups
1070
    def groupindex(g):
1071
        return groups_order.index(g[0])
1072
    resource_groups_list = sorted([(k, v) for k, v in resource_groups.items()],
1073
                                  key=groupindex)
1074
    resource_groups = OrderedDict(resource_groups_list)
1075

    
1076
    # sort resources
1077
    def resourceindex(r):
1078
        return resources_order.index(r['str_repr'])
1079

    
1080
    for index, group in enumerate(resource_catalog):
1081
        resource_catalog[index][1] = sorted(resource_catalog[index][1],
1082
                                            key=resourceindex)
1083
        if len(resource_catalog[index][1]) == 0:
1084
            resource_catalog.pop(index)
1085
            for gindex, g in enumerate(resource_groups):
1086
                if g[0] == group[0]:
1087
                    resource_groups.pop(gindex)
1088

    
1089
    # filter out resources which user cannot request in a project application
1090
    exclude = resources_meta.get('exclude_from_usage', [])
1091
    for group_index, group_resources in enumerate(list(resource_catalog)):
1092
        group, resources = group_resources
1093
        for index, resource in list(enumerate(resources)):
1094
            if for_project and not resource.get('allow_in_projects'):
1095
                resources.remove(resource)
1096
            if resource.get('str_repr') in exclude and for_usage:
1097
                resources.remove(resource)
1098

    
1099
    # cleanup empty groups
1100
    for group_index, group_resources in enumerate(list(resource_catalog)):
1101
        group, resources = group_resources
1102
        if len(resources) == 0:
1103
            resource_catalog.pop(group_index)
1104
            resource_groups.pop(group)
1105

    
1106

    
1107
    return resource_catalog, resource_groups
1108

    
1109

    
1110
@require_http_methods(["GET", "POST"])
1111
@valid_astakos_user_required
1112
def project_add(request):
1113
    user = request.user
1114
    if not user.is_project_admin():
1115
        ok, limit = qh_add_pending_app(user, dry_run=True)
1116
        if not ok:
1117
            m = _(astakos_messages.PENDING_APPLICATION_LIMIT_ADD) % limit
1118
            messages.error(request, m)
1119
            next = reverse('astakos.im.views.project_list')
1120
            next = restrict_next(next, domain=COOKIE_DOMAIN)
1121
            return redirect(next)
1122

    
1123
    details_fields = ["name", "homepage", "description", "start_date",
1124
                      "end_date", "comments"]
1125
    membership_fields = ["member_join_policy", "member_leave_policy",
1126
                         "limit_on_members_number"]
1127
    resource_catalog, resource_groups = _resources_catalog(for_project=True)
1128
    if resource_catalog is False:
1129
        # on fail resource_groups contains the result object
1130
        result = resource_groups
1131
        messages.error(request, 'Unable to retrieve system resources: %s' %
1132
                       result.reason)
1133
    extra_context = {
1134
        'resource_catalog': resource_catalog,
1135
        'resource_groups': resource_groups,
1136
        'show_form': True,
1137
        'details_fields': details_fields,
1138
        'membership_fields': membership_fields}
1139

    
1140
    response = None
1141
    with ExceptionHandler(request):
1142
        response = _create_object(
1143
            request,
1144
            template_name='im/projects/projectapplication_form.html',
1145
            extra_context=extra_context,
1146
            post_save_redirect=reverse('project_list'),
1147
            form_class=ProjectApplicationForm,
1148
            msg=_("The %(verbose_name)s has been received and "
1149
                  "is under consideration."),
1150
            )
1151

    
1152
    if response is not None:
1153
        return response
1154

    
1155
    next = reverse('astakos.im.views.project_list')
1156
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1157
    return redirect(next)
1158

    
1159

    
1160
@require_http_methods(["GET"])
1161
@valid_astakos_user_required
1162
def project_list(request):
1163
    projects = ProjectApplication.objects.user_accessible_projects(request.user).select_related()
1164
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1165
                                                prefix="my_projects_")
1166
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1167

    
1168
    return object_list(
1169
        request,
1170
        projects,
1171
        template_name='im/projects/project_list.html',
1172
        extra_context={
1173
            'is_search':False,
1174
            'table': table,
1175
        })
1176

    
1177

    
1178
@require_http_methods(["POST"])
1179
@valid_astakos_user_required
1180
def project_app_cancel(request, application_id):
1181
    next = request.GET.get('next')
1182
    chain_id = None
1183

    
1184
    with ExceptionHandler(request):
1185
        chain_id = _project_app_cancel(request, application_id)
1186

    
1187
    if not next:
1188
        if chain_id:
1189
            next = reverse('astakos.im.views.project_detail', args=(chain_id,))
1190
        else:
1191
            next = reverse('astakos.im.views.project_list')
1192

    
1193
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1194
    return redirect(next)
1195

    
1196
@commit_on_success_strict()
1197
def _project_app_cancel(request, application_id):
1198
    chain_id = None
1199
    try:
1200
        application_id = int(application_id)
1201
        chain_id = get_related_project_id(application_id)
1202
        cancel_application(application_id, request.user)
1203
    except (IOError, PermissionDenied), e:
1204
        messages.error(request, e)
1205
    else:
1206
        msg = _(astakos_messages.APPLICATION_CANCELLED)
1207
        messages.success(request, msg)
1208
        return chain_id
1209

    
1210

    
1211
@require_http_methods(["GET", "POST"])
1212
@valid_astakos_user_required
1213
def project_modify(request, application_id):
1214

    
1215
    try:
1216
        app = ProjectApplication.objects.get(id=application_id)
1217
    except ProjectApplication.DoesNotExist:
1218
        raise Http404
1219

    
1220
    user = request.user
1221
    if not (user.owns_application(app) or user.is_project_admin(app.id)):
1222
        m = _(astakos_messages.NOT_ALLOWED)
1223
        raise PermissionDenied(m)
1224

    
1225
    if not user.is_project_admin():
1226
        owner = app.owner
1227
        ok, limit = qh_add_pending_app(owner, precursor=app, dry_run=True)
1228
        if not ok:
1229
            m = _(astakos_messages.PENDING_APPLICATION_LIMIT_MODIFY) % limit
1230
            messages.error(request, m)
1231
            next = reverse('astakos.im.views.project_list')
1232
            next = restrict_next(next, domain=COOKIE_DOMAIN)
1233
            return redirect(next)
1234

    
1235
    details_fields = ["name", "homepage", "description", "start_date",
1236
                      "end_date", "comments"]
1237
    membership_fields = ["member_join_policy", "member_leave_policy",
1238
                         "limit_on_members_number"]
1239
    resource_catalog, resource_groups = _resources_catalog(for_project=True)
1240
    if resource_catalog is False:
1241
        # on fail resource_groups contains the result object
1242
        result = resource_groups
1243
        messages.error(request, 'Unable to retrieve system resources: %s' %
1244
                       result.reason)
1245
    extra_context = {
1246
        'resource_catalog': resource_catalog,
1247
        'resource_groups': resource_groups,
1248
        'show_form': True,
1249
        'details_fields': details_fields,
1250
        'update_form': True,
1251
        'membership_fields': membership_fields
1252
    }
1253

    
1254
    response = None
1255
    with ExceptionHandler(request):
1256
        response = _update_object(
1257
            request,
1258
            object_id=application_id,
1259
            template_name='im/projects/projectapplication_form.html',
1260
            extra_context=extra_context,
1261
            post_save_redirect=reverse('project_list'),
1262
            form_class=ProjectApplicationForm,
1263
            msg=_("The %(verbose_name)s has been received and is under "
1264
                  "consideration."))
1265

    
1266
    if response is not None:
1267
        return response
1268

    
1269
    next = reverse('astakos.im.views.project_list')
1270
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1271
    return redirect(next)
1272

    
1273
@require_http_methods(["GET", "POST"])
1274
@valid_astakos_user_required
1275
def project_app(request, application_id):
1276
    return common_detail(request, application_id, project_view=False)
1277

    
1278
@require_http_methods(["GET", "POST"])
1279
@valid_astakos_user_required
1280
def project_detail(request, chain_id):
1281
    return common_detail(request, chain_id)
1282

    
1283
@commit_on_success_strict()
1284
def addmembers(request, chain_id, addmembers_form):
1285
    if addmembers_form.is_valid():
1286
        try:
1287
            chain_id = int(chain_id)
1288
            map(lambda u: enroll_member(
1289
                    chain_id,
1290
                    u,
1291
                    request_user=request.user),
1292
                addmembers_form.valid_users)
1293
        except (IOError, PermissionDenied), e:
1294
            messages.error(request, e)
1295

    
1296
def common_detail(request, chain_or_app_id, project_view=True):
1297
    project = None
1298
    if project_view:
1299
        chain_id = chain_or_app_id
1300
        if request.method == 'POST':
1301
            addmembers_form = AddProjectMembersForm(
1302
                request.POST,
1303
                chain_id=int(chain_id),
1304
                request_user=request.user)
1305
            with ExceptionHandler(request):
1306
                addmembers(request, chain_id, addmembers_form)
1307

    
1308
            if addmembers_form.is_valid():
1309
                addmembers_form = AddProjectMembersForm()  # clear form data
1310
        else:
1311
            addmembers_form = AddProjectMembersForm()  # initialize form
1312

    
1313
        project, application = get_by_chain_or_404(chain_id)
1314
        if project:
1315
            members = project.projectmembership_set.select_related()
1316
            members_table = tables.ProjectMembersTable(project,
1317
                                                       members,
1318
                                                       user=request.user,
1319
                                                       prefix="members_")
1320
            RequestConfig(request, paginate={"per_page": PAGINATE_BY}
1321
                          ).configure(members_table)
1322

    
1323
        else:
1324
            members_table = None
1325

    
1326
    else: # is application
1327
        application_id = chain_or_app_id
1328
        application = get_object_or_404(ProjectApplication, pk=application_id)
1329
        members_table = None
1330
        addmembers_form = None
1331

    
1332
    modifications_table = None
1333

    
1334
    user = request.user
1335
    is_project_admin = user.is_project_admin(application_id=application.id)
1336
    is_owner = user.owns_application(application)
1337
    if not (is_owner or is_project_admin) and not project_view:
1338
        m = _(astakos_messages.NOT_ALLOWED)
1339
        raise PermissionDenied(m)
1340

    
1341
    if (not (is_owner or is_project_admin) and project_view and
1342
        not user.non_owner_can_view(project)):
1343
        m = _(astakos_messages.NOT_ALLOWED)
1344
        raise PermissionDenied(m)
1345

    
1346
    following_applications = list(application.pending_modifications())
1347
    following_applications.reverse()
1348
    modifications_table = (
1349
        tables.ProjectModificationApplicationsTable(following_applications,
1350
                                                    user=request.user,
1351
                                                    prefix="modifications_"))
1352

    
1353
    mem_display = user.membership_display(project) if project else None
1354
    can_join_req = can_join_request(project, user) if project else False
1355
    can_leave_req = can_leave_request(project, user) if project else False
1356

    
1357
    return object_detail(
1358
        request,
1359
        queryset=ProjectApplication.objects.select_related(),
1360
        object_id=application.id,
1361
        template_name='im/projects/project_detail.html',
1362
        extra_context={
1363
            'project_view': project_view,
1364
            'addmembers_form':addmembers_form,
1365
            'members_table': members_table,
1366
            'owner_mode': is_owner,
1367
            'admin_mode': is_project_admin,
1368
            'modifications_table': modifications_table,
1369
            'mem_display': mem_display,
1370
            'can_join_request': can_join_req,
1371
            'can_leave_request': can_leave_req,
1372
            })
1373

    
1374
@require_http_methods(["GET", "POST"])
1375
@valid_astakos_user_required
1376
def project_search(request):
1377
    q = request.GET.get('q', '')
1378
    form = ProjectSearchForm()
1379
    q = q.strip()
1380

    
1381
    if request.method == "POST":
1382
        form = ProjectSearchForm(request.POST)
1383
        if form.is_valid():
1384
            q = form.cleaned_data['q'].strip()
1385
        else:
1386
            q = None
1387

    
1388
    if q is None:
1389
        projects = ProjectApplication.objects.none()
1390
    else:
1391
        accepted_projects = request.user.projectmembership_set.filter(
1392
            ~Q(acceptance_date__isnull=True)).values_list('project', flat=True)
1393
        projects = ProjectApplication.objects.search_by_name(q)
1394
        projects = projects.filter(~Q(project__last_approval_date__isnull=True))
1395
        projects = projects.exclude(project__in=accepted_projects)
1396

    
1397
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1398
                                                prefix="my_projects_")
1399
    if request.method == "POST":
1400
        table.caption = _('SEARCH RESULTS')
1401
    else:
1402
        table.caption = _('ALL PROJECTS')
1403

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

    
1406
    return object_list(
1407
        request,
1408
        projects,
1409
        template_name='im/projects/project_list.html',
1410
        extra_context={
1411
          'form': form,
1412
          'is_search': True,
1413
          'q': q,
1414
          'table': table
1415
        })
1416

    
1417
@require_http_methods(["POST"])
1418
@valid_astakos_user_required
1419
def project_join(request, chain_id):
1420
    next = request.GET.get('next')
1421
    if not next:
1422
        next = reverse('astakos.im.views.project_detail',
1423
                       args=(chain_id,))
1424

    
1425
    with ExceptionHandler(request):
1426
        _project_join(request, chain_id)
1427

    
1428

    
1429
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1430
    return redirect(next)
1431

    
1432

    
1433
@commit_on_success_strict()
1434
def _project_join(request, chain_id):
1435
    try:
1436
        chain_id = int(chain_id)
1437
        auto_accepted = join_project(chain_id, request.user)
1438
        if auto_accepted:
1439
            m = _(astakos_messages.USER_JOINED_PROJECT)
1440
        else:
1441
            m = _(astakos_messages.USER_JOIN_REQUEST_SUBMITTED)
1442
        messages.success(request, m)
1443
    except (IOError, PermissionDenied), e:
1444
        messages.error(request, e)
1445

    
1446

    
1447
@require_http_methods(["POST"])
1448
@valid_astakos_user_required
1449
def project_leave(request, chain_id):
1450
    next = request.GET.get('next')
1451
    if not next:
1452
        next = reverse('astakos.im.views.project_list')
1453

    
1454
    with ExceptionHandler(request):
1455
        _project_leave(request, chain_id)
1456

    
1457
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1458
    return redirect(next)
1459

    
1460

    
1461
@commit_on_success_strict()
1462
def _project_leave(request, chain_id):
1463
    try:
1464
        chain_id = int(chain_id)
1465
        auto_accepted = leave_project(chain_id, request.user)
1466
        if auto_accepted:
1467
            m = _(astakos_messages.USER_LEFT_PROJECT)
1468
        else:
1469
            m = _(astakos_messages.USER_LEAVE_REQUEST_SUBMITTED)
1470
        messages.success(request, m)
1471
    except (IOError, PermissionDenied), e:
1472
        messages.error(request, e)
1473

    
1474

    
1475
@require_http_methods(["POST"])
1476
@valid_astakos_user_required
1477
def project_cancel(request, chain_id):
1478
    next = request.GET.get('next')
1479
    if not next:
1480
        next = reverse('astakos.im.views.project_list')
1481

    
1482
    with ExceptionHandler(request):
1483
        _project_cancel(request, chain_id)
1484

    
1485
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1486
    return redirect(next)
1487

    
1488

    
1489
@commit_on_success_strict()
1490
def _project_cancel(request, chain_id):
1491
    try:
1492
        chain_id = int(chain_id)
1493
        cancel_membership(chain_id, request.user)
1494
        m = _(astakos_messages.USER_REQUEST_CANCELLED)
1495
        messages.success(request, m)
1496
    except (IOError, PermissionDenied), e:
1497
        messages.error(request, e)
1498

    
1499

    
1500
@require_http_methods(["POST"])
1501
@valid_astakos_user_required
1502
def project_accept_member(request, chain_id, memb_id):
1503

    
1504
    with ExceptionHandler(request):
1505
        _project_accept_member(request, chain_id, memb_id)
1506

    
1507
    return redirect(reverse('project_detail', args=(chain_id,)))
1508

    
1509

    
1510
@commit_on_success_strict()
1511
def _project_accept_member(request, chain_id, memb_id):
1512
    try:
1513
        chain_id = int(chain_id)
1514
        memb_id = int(memb_id)
1515
        m = accept_membership(chain_id, memb_id, request.user)
1516
    except (IOError, PermissionDenied), e:
1517
        messages.error(request, e)
1518
    else:
1519
        email = escape(m.person.email)
1520
        msg = _(astakos_messages.USER_MEMBERSHIP_ACCEPTED) % email
1521
        messages.success(request, msg)
1522

    
1523

    
1524
@require_http_methods(["POST"])
1525
@valid_astakos_user_required
1526
def project_remove_member(request, chain_id, memb_id):
1527

    
1528
    with ExceptionHandler(request):
1529
        _project_remove_member(request, chain_id, memb_id)
1530

    
1531
    return redirect(reverse('project_detail', args=(chain_id,)))
1532

    
1533

    
1534
@commit_on_success_strict()
1535
def _project_remove_member(request, chain_id, memb_id):
1536
    try:
1537
        chain_id = int(chain_id)
1538
        memb_id = int(memb_id)
1539
        m = remove_membership(chain_id, memb_id, request.user)
1540
    except (IOError, PermissionDenied), e:
1541
        messages.error(request, e)
1542
    else:
1543
        email = escape(m.person.email)
1544
        msg = _(astakos_messages.USER_MEMBERSHIP_REMOVED) % email
1545
        messages.success(request, msg)
1546

    
1547

    
1548
@require_http_methods(["POST"])
1549
@valid_astakos_user_required
1550
def project_reject_member(request, chain_id, memb_id):
1551

    
1552
    with ExceptionHandler(request):
1553
        _project_reject_member(request, chain_id, memb_id)
1554

    
1555
    return redirect(reverse('project_detail', args=(chain_id,)))
1556

    
1557

    
1558
@commit_on_success_strict()
1559
def _project_reject_member(request, chain_id, memb_id):
1560
    try:
1561
        chain_id = int(chain_id)
1562
        memb_id = int(memb_id)
1563
        m = reject_membership(chain_id, memb_id, request.user)
1564
    except (IOError, PermissionDenied), e:
1565
        messages.error(request, e)
1566
    else:
1567
        email = escape(m.person.email)
1568
        msg = _(astakos_messages.USER_MEMBERSHIP_REJECTED) % email
1569
        messages.success(request, msg)
1570

    
1571

    
1572
@require_http_methods(["POST"])
1573
@signed_terms_required
1574
@login_required
1575
def project_app_approve(request, application_id):
1576

    
1577
    if not request.user.is_project_admin():
1578
        m = _(astakos_messages.NOT_ALLOWED)
1579
        raise PermissionDenied(m)
1580

    
1581
    try:
1582
        app = ProjectApplication.objects.get(id=application_id)
1583
    except ProjectApplication.DoesNotExist:
1584
        raise Http404
1585

    
1586
    with ExceptionHandler(request):
1587
        _project_app_approve(request, application_id)
1588

    
1589
    chain_id = get_related_project_id(application_id)
1590
    return redirect(reverse('project_detail', args=(chain_id,)))
1591

    
1592

    
1593
@commit_on_success_strict()
1594
def _project_app_approve(request, application_id):
1595
    approve_application(application_id)
1596

    
1597

    
1598
@require_http_methods(["POST"])
1599
@signed_terms_required
1600
@login_required
1601
def project_app_deny(request, application_id):
1602

    
1603
    reason = request.POST.get('reason', None)
1604
    if not reason:
1605
        reason = None
1606

    
1607
    if not request.user.is_project_admin():
1608
        m = _(astakos_messages.NOT_ALLOWED)
1609
        raise PermissionDenied(m)
1610

    
1611
    try:
1612
        app = ProjectApplication.objects.get(id=application_id)
1613
    except ProjectApplication.DoesNotExist:
1614
        raise Http404
1615

    
1616
    with ExceptionHandler(request):
1617
        _project_app_deny(request, application_id, reason)
1618

    
1619
    return redirect(reverse('project_list'))
1620

    
1621

    
1622
@commit_on_success_strict()
1623
def _project_app_deny(request, application_id, reason):
1624
    deny_application(application_id, reason=reason)
1625

    
1626

    
1627
@require_http_methods(["POST"])
1628
@signed_terms_required
1629
@login_required
1630
def project_app_dismiss(request, application_id):
1631
    try:
1632
        app = ProjectApplication.objects.get(id=application_id)
1633
    except ProjectApplication.DoesNotExist:
1634
        raise Http404
1635

    
1636
    if not request.user.owns_application(app):
1637
        m = _(astakos_messages.NOT_ALLOWED)
1638
        raise PermissionDenied(m)
1639

    
1640
    with ExceptionHandler(request):
1641
        _project_app_dismiss(request, application_id)
1642

    
1643
    chain_id = None
1644
    chain_id = get_related_project_id(application_id)
1645
    if chain_id:
1646
        next = reverse('project_detail', args=(chain_id,))
1647
    else:
1648
        next = reverse('project_list')
1649
    return redirect(next)
1650

    
1651

    
1652
def _project_app_dismiss(request, application_id):
1653
    # XXX: dismiss application also does authorization
1654
    dismiss_application(application_id, request_user=request.user)
1655

    
1656

    
1657
@require_http_methods(["GET"])
1658
@required_auth_methods_assigned(allow_access=True)
1659
@login_required
1660
@signed_terms_required
1661
def landing(request):
1662
    context = {'services': Service.catalog(orderfor='dashboard')}
1663
    return render_response(
1664
        'im/landing.html',
1665
        context_instance=get_context(request), **context)
1666

    
1667

    
1668
def api_access(request):
1669
    return render_response(
1670
        'im/api_access.html',
1671
        context_instance=get_context(request))