Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 0b817216

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

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

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

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

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

    
155

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

    
170

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

    
186

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

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

    
208

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

    
212

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

219
    **Arguments**
220

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

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

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

232
    **Template:**
233

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

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

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

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

    
252

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

    
265

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

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

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

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

282
    **Arguments**
283

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

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

291
    **Template:**
292

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

295
    **Settings:**
296

297
    The view expectes the following settings are defined:
298

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

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

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

    
337

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

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

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

352
    **Arguments**
353

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

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

361
    **Template:**
362

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

365
    **Settings:**
366

367
    The view expectes the following settings are defined:
368

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

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

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

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

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

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

    
427

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

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

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

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

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

451
    **Arguments**
452

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

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

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

463
    **Template:**
464

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

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

    
478
    instance = None
479

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

    
489
        provider = pending.provider
490

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
572

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

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

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

586
    **Arguments**
587

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

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

595
    **Template:**
596

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

599
    **Settings:**
600

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

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

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

    
624

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

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

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

    
664

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

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

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

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

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

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

    
712

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

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

    
741
    terms = f.read()
742

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

    
769

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

    
779
    if not settings.EMAILCHANGE_ENABLED:
780
        raise PermissionDenied
781

    
782
    if activation_key:
783
        try:
784
            try:
785
                email_change = EmailChange.objects.get(
786
                    activation_key=activation_key)
787
            except EmailChange.DoesNotExist:
788
                transaction.rollback()
789
                logger.error("[change-email] Invalid or used activation "
790
                             "code, %s", activation_key)
791
                raise Http404
792

    
793
            if (request.user.is_authenticated() and \
794
                request.user == email_change.user) or not \
795
                    request.user.is_authenticated():
796
                user = EmailChange.objects.change_email(activation_key)
797
                msg = _(astakos_messages.EMAIL_CHANGED)
798
                messages.success(request, msg)
799
                transaction.commit()
800
                return HttpResponseRedirect(reverse('edit_profile'))
801
            else:
802
                logger.error("[change-email] Access from invalid user, %s %s",
803
                             email_change.user, request.user.log_display)
804
                transaction.rollback()
805
                raise PermissionDenied
806
        except ValueError, e:
807
            messages.error(request, e)
808
            transaction.rollback()
809
            return HttpResponseRedirect(reverse('index'))
810

    
811
        return render_response(confirm_template_name,
812
                               modified_user=user if 'user' in locals()
813
                               else None, context_instance=get_context(request,
814
                               extra_context))
815

    
816
    if not request.user.is_authenticated():
817
        path = quote(request.get_full_path())
818
        url = request.build_absolute_uri(reverse('index'))
819
        return HttpResponseRedirect(url + '?next=' + path)
820

    
821
    # clean up expired email changes
822
    if request.user.email_change_is_pending():
823
        change = request.user.emailchanges.get()
824
        if change.activation_key_expired():
825
            change.delete()
826
            transaction.commit()
827
            return HttpResponseRedirect(reverse('email_change'))
828

    
829
    form = EmailChangeForm(request.POST or None)
830
    if request.method == 'POST' and form.is_valid():
831
        try:
832
            ec = form.save(request, email_template_name, request)
833
        except Exception, e:
834
            transaction.rollback()
835
            raise
836
        else:
837
            msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
838
            messages.success(request, msg)
839
            transaction.commit()
840
            return HttpResponseRedirect(reverse('edit_profile'))
841

    
842
    if request.user.email_change_is_pending():
843
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
844

    
845
    return render_response(
846
        form_template_name,
847
        form=form,
848
        context_instance=get_context(request, extra_context)
849
    )
850

    
851

    
852
def send_activation(request, user_id, template_name='im/login.html',
853
                    extra_context=None):
854

    
855
    if request.user.is_authenticated():
856
        return HttpResponseRedirect(reverse('index'))
857

    
858
    extra_context = extra_context or {}
859
    try:
860
        u = AstakosUser.objects.get(id=user_id)
861
    except AstakosUser.DoesNotExist:
862
        messages.error(request, _(astakos_messages.ACCOUNT_UNKNOWN))
863
    else:
864
        if u.email_verified:
865
            logger.warning("[resend activation] Account already verified: %s",
866
                           u.log_display)
867

    
868
            messages.error(request,
869
                           _(astakos_messages.ACCOUNT_ALREADY_VERIFIED))
870
        else:
871
            activation_backend = activation_backends.get_backend()
872
            activation_backend.send_user_verification_email(u)
873
            messages.success(request, astakos_messages.ACTIVATION_SENT)
874

    
875
    return HttpResponseRedirect(reverse('index'))
876

    
877

    
878
@require_http_methods(["GET"])
879
@valid_astakos_user_required
880
def resource_usage(request):
881

    
882
    resources_meta = presentation.RESOURCES
883

    
884
    current_usage = quotas.get_user_quotas(request.user)
885
    current_usage = json.dumps(current_usage['system'])
886
    resource_catalog, resource_groups = _resources_catalog(for_usage=True)
887
    if resource_catalog is False:
888
        # on fail resource_groups contains the result object
889
        result = resource_groups
890
        messages.error(request, 'Unable to retrieve system resources: %s' %
891
                       result.reason)
892

    
893
    resource_catalog = json.dumps(resource_catalog)
894
    resource_groups = json.dumps(resource_groups)
895
    resources_order = json.dumps(resources_meta.get('resources_order'))
896

    
897
    return render_response('im/resource_usage.html',
898
                           context_instance=get_context(request),
899
                           resource_catalog=resource_catalog,
900
                           resource_groups=resource_groups,
901
                           resources_order=resources_order,
902
                           current_usage=current_usage,
903
                           token_cookie_name=settings.COOKIE_NAME,
904
                           usage_update_interval=
905
                           settings.USAGE_UPDATE_INTERVAL)
906

    
907

    
908
# TODO: action only on POST and user should confirm the removal
909
@require_http_methods(["GET", "POST"])
910
@valid_astakos_user_required
911
def remove_auth_provider(request, pk):
912
    try:
913
        provider = request.user.auth_providers.get(pk=int(pk)).settings
914
    except AstakosUserAuthProvider.DoesNotExist:
915
        raise Http404
916

    
917
    if provider.get_remove_policy:
918
        messages.success(request, provider.get_removed_msg)
919
        provider.remove_from_user()
920
        return HttpResponseRedirect(reverse('edit_profile'))
921
    else:
922
        raise PermissionDenied
923

    
924

    
925
def how_it_works(request):
926
    return render_response(
927
        'im/how_it_works.html',
928
        context_instance=get_context(request))
929

    
930

    
931
@commit_on_success_strict()
932
def _create_object(request, model=None, template_name=None,
933
        template_loader=template_loader, extra_context=None, post_save_redirect=None,
934
        login_required=False, context_processors=None, form_class=None,
935
        msg=None):
936
    """
937
    Based of django.views.generic.create_update.create_object which displays a
938
    summary page before creating the object.
939
    """
940
    response = None
941

    
942
    if extra_context is None: extra_context = {}
943
    if login_required and not request.user.is_authenticated():
944
        return redirect_to_login(request.path)
945
    try:
946

    
947
        model, form_class = get_model_and_form_class(model, form_class)
948
        extra_context['edit'] = 0
949
        if request.method == 'POST':
950
            form = form_class(request.POST, request.FILES)
951
            if form.is_valid():
952
                verify = request.GET.get('verify')
953
                edit = request.GET.get('edit')
954
                if verify == '1':
955
                    extra_context['show_form'] = False
956
                    extra_context['form_data'] = form.cleaned_data
957
                elif edit == '1':
958
                    extra_context['show_form'] = True
959
                else:
960
                    new_object = form.save()
961
                    if not msg:
962
                        msg = _("The %(verbose_name)s was created successfully.")
963
                    msg = msg % model._meta.__dict__
964
                    messages.success(request, msg, fail_silently=True)
965
                    response = redirect(post_save_redirect, new_object)
966
        else:
967
            form = form_class()
968
    except (IOError, PermissionDenied), e:
969
        messages.error(request, e)
970
        return None
971
    else:
972
        if response == None:
973
            # Create the template, context, response
974
            if not template_name:
975
                template_name = "%s/%s_form.html" %\
976
                     (model._meta.app_label, model._meta.object_name.lower())
977
            t = template_loader.get_template(template_name)
978
            c = RequestContext(request, {
979
                'form': form
980
            }, context_processors)
981
            apply_extra_context(extra_context, c)
982
            response = HttpResponse(t.render(c))
983
        return response
984

    
985
@commit_on_success_strict()
986
def _update_object(request, model=None, object_id=None, slug=None,
987
        slug_field='slug', template_name=None, template_loader=template_loader,
988
        extra_context=None, post_save_redirect=None, login_required=False,
989
        context_processors=None, template_object_name='object',
990
        form_class=None, msg=None):
991
    """
992
    Based of django.views.generic.create_update.update_object which displays a
993
    summary page before updating the object.
994
    """
995
    response = None
996

    
997
    if extra_context is None: extra_context = {}
998
    if login_required and not request.user.is_authenticated():
999
        return redirect_to_login(request.path)
1000

    
1001
    try:
1002
        model, form_class = get_model_and_form_class(model, form_class)
1003
        obj = lookup_object(model, object_id, slug, slug_field)
1004

    
1005
        if request.method == 'POST':
1006
            form = form_class(request.POST, request.FILES, instance=obj)
1007
            if form.is_valid():
1008
                verify = request.GET.get('verify')
1009
                edit = request.GET.get('edit')
1010
                if verify == '1':
1011
                    extra_context['show_form'] = False
1012
                    extra_context['form_data'] = form.cleaned_data
1013
                elif edit == '1':
1014
                    extra_context['show_form'] = True
1015
                else:
1016
                    obj = form.save()
1017
                    if not msg:
1018
                        msg = _("The %(verbose_name)s was created successfully.")
1019
                    msg = msg % model._meta.__dict__
1020
                    messages.success(request, msg, fail_silently=True)
1021
                    response = redirect(post_save_redirect, obj)
1022
        else:
1023
            form = form_class(instance=obj)
1024
    except (IOError, PermissionDenied), e:
1025
        messages.error(request, e)
1026
        return None
1027
    else:
1028
        if response == None:
1029
            if not template_name:
1030
                template_name = "%s/%s_form.html" %\
1031
                    (model._meta.app_label, model._meta.object_name.lower())
1032
            t = template_loader.get_template(template_name)
1033
            c = RequestContext(request, {
1034
                'form': form,
1035
                template_object_name: obj,
1036
            }, context_processors)
1037
            apply_extra_context(extra_context, c)
1038
            response = HttpResponse(t.render(c))
1039
            populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.attname))
1040
        return response
1041

    
1042

    
1043

    
1044
def _resources_catalog(for_project=False, for_usage=False):
1045
    """
1046
    `resource_catalog` contains a list of tuples. Each tuple contains the group
1047
    key the resource is assigned to and resources list of dicts that contain
1048
    resource information.
1049
    `resource_groups` contains information about the groups
1050
    """
1051
    # presentation data
1052
    resources_meta = presentation.RESOURCES
1053
    resource_groups = resources_meta.get('groups', {})
1054
    resource_catalog = ()
1055
    resource_keys = []
1056

    
1057
    # resources in database
1058
    resource_details = map(lambda obj: model_to_dict(obj, exclude=[]),
1059
                           Resource.objects.all())
1060
    # initialize resource_catalog to contain all group/resource information
1061
    for r in resource_details:
1062
        if not r.get('group') in resource_groups:
1063
            resource_groups[r.get('group')] = {'icon': 'unknown'}
1064

    
1065
    resource_keys = [r.get('str_repr') for r in resource_details]
1066
    resource_catalog = [[g, filter(lambda r: r.get('group', '') == g,
1067
                                   resource_details)] for g in resource_groups]
1068

    
1069
    # order groups, also include unknown groups
1070
    groups_order = resources_meta.get('groups_order')
1071
    for g in resource_groups.keys():
1072
        if not g in groups_order:
1073
            groups_order.append(g)
1074

    
1075
    # order resources, also include unknown resources
1076
    resources_order = resources_meta.get('resources_order')
1077
    for r in resource_keys:
1078
        if not r in resources_order:
1079
            resources_order.append(r)
1080

    
1081
    # sort catalog groups
1082
    resource_catalog = sorted(resource_catalog,
1083
                              key=lambda g: groups_order.index(g[0]))
1084

    
1085
    # sort groups
1086
    def groupindex(g):
1087
        return groups_order.index(g[0])
1088
    resource_groups_list = sorted([(k, v) for k, v in resource_groups.items()],
1089
                                  key=groupindex)
1090
    resource_groups = OrderedDict(resource_groups_list)
1091

    
1092
    # sort resources
1093
    def resourceindex(r):
1094
        return resources_order.index(r['str_repr'])
1095

    
1096
    for index, group in enumerate(resource_catalog):
1097
        resource_catalog[index][1] = sorted(resource_catalog[index][1],
1098
                                            key=resourceindex)
1099
        if len(resource_catalog[index][1]) == 0:
1100
            resource_catalog.pop(index)
1101
            for gindex, g in enumerate(resource_groups):
1102
                if g[0] == group[0]:
1103
                    resource_groups.pop(gindex)
1104

    
1105
    # filter out resources which user cannot request in a project application
1106
    exclude = resources_meta.get('exclude_from_usage', [])
1107
    for group_index, group_resources in enumerate(list(resource_catalog)):
1108
        group, resources = group_resources
1109
        for index, resource in list(enumerate(resources)):
1110
            if for_project and not resource.get('allow_in_projects'):
1111
                resources.remove(resource)
1112
            if resource.get('str_repr') in exclude and for_usage:
1113
                resources.remove(resource)
1114

    
1115
    # cleanup empty groups
1116
    for group_index, group_resources in enumerate(list(resource_catalog)):
1117
        group, resources = group_resources
1118
        if len(resources) == 0:
1119
            resource_catalog.pop(group_index)
1120
            resource_groups.pop(group)
1121

    
1122

    
1123
    return resource_catalog, resource_groups
1124

    
1125

    
1126
@require_http_methods(["GET", "POST"])
1127
@valid_astakos_user_required
1128
def project_add(request):
1129
    user = request.user
1130
    if not user.is_project_admin():
1131
        ok, limit = qh_add_pending_app(user, dry_run=True)
1132
        if not ok:
1133
            m = _(astakos_messages.PENDING_APPLICATION_LIMIT_ADD) % limit
1134
            messages.error(request, m)
1135
            next = reverse('astakos.im.views.project_list')
1136
            next = restrict_next(next, domain=COOKIE_DOMAIN)
1137
            return redirect(next)
1138

    
1139
    details_fields = ["name", "homepage", "description", "start_date",
1140
                      "end_date", "comments"]
1141
    membership_fields = ["member_join_policy", "member_leave_policy",
1142
                         "limit_on_members_number"]
1143
    resource_catalog, resource_groups = _resources_catalog(for_project=True)
1144
    if resource_catalog is False:
1145
        # on fail resource_groups contains the result object
1146
        result = resource_groups
1147
        messages.error(request, 'Unable to retrieve system resources: %s' %
1148
                       result.reason)
1149
    extra_context = {
1150
        'resource_catalog': resource_catalog,
1151
        'resource_groups': resource_groups,
1152
        'show_form': True,
1153
        'details_fields': details_fields,
1154
        'membership_fields': membership_fields}
1155

    
1156
    response = None
1157
    with ExceptionHandler(request):
1158
        response = _create_object(
1159
            request,
1160
            template_name='im/projects/projectapplication_form.html',
1161
            extra_context=extra_context,
1162
            post_save_redirect=reverse('project_list'),
1163
            form_class=ProjectApplicationForm,
1164
            msg=_("The %(verbose_name)s has been received and "
1165
                  "is under consideration."),
1166
            )
1167

    
1168
    if response is not None:
1169
        return response
1170

    
1171
    next = reverse('astakos.im.views.project_list')
1172
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1173
    return redirect(next)
1174

    
1175

    
1176
@require_http_methods(["GET"])
1177
@valid_astakos_user_required
1178
def project_list(request):
1179
    projects = ProjectApplication.objects.user_accessible_projects(request.user).select_related()
1180
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1181
                                                prefix="my_projects_")
1182
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1183

    
1184
    return object_list(
1185
        request,
1186
        projects,
1187
        template_name='im/projects/project_list.html',
1188
        extra_context={
1189
            'is_search':False,
1190
            'table': table,
1191
        })
1192

    
1193

    
1194
@require_http_methods(["POST"])
1195
@valid_astakos_user_required
1196
def project_app_cancel(request, application_id):
1197
    next = request.GET.get('next')
1198
    chain_id = None
1199

    
1200
    with ExceptionHandler(request):
1201
        chain_id = _project_app_cancel(request, application_id)
1202

    
1203
    if not next:
1204
        if chain_id:
1205
            next = reverse('astakos.im.views.project_detail', args=(chain_id,))
1206
        else:
1207
            next = reverse('astakos.im.views.project_list')
1208

    
1209
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1210
    return redirect(next)
1211

    
1212
@commit_on_success_strict()
1213
def _project_app_cancel(request, application_id):
1214
    chain_id = None
1215
    try:
1216
        application_id = int(application_id)
1217
        chain_id = get_related_project_id(application_id)
1218
        cancel_application(application_id, request.user)
1219
    except (IOError, PermissionDenied), e:
1220
        messages.error(request, e)
1221

    
1222
    else:
1223
        msg = _(astakos_messages.APPLICATION_CANCELLED)
1224
        messages.success(request, msg)
1225
        return chain_id
1226

    
1227

    
1228
@require_http_methods(["GET", "POST"])
1229
@valid_astakos_user_required
1230
def project_modify(request, application_id):
1231

    
1232
    try:
1233
        app = ProjectApplication.objects.get(id=application_id)
1234
    except ProjectApplication.DoesNotExist:
1235
        raise Http404
1236

    
1237
    user = request.user
1238
    if not (user.owns_application(app) or user.is_project_admin(app.id)):
1239
        m = _(astakos_messages.NOT_ALLOWED)
1240
        raise PermissionDenied(m)
1241

    
1242
    if not user.is_project_admin():
1243
        owner = app.owner
1244
        ok, limit = qh_add_pending_app(owner, precursor=app, dry_run=True)
1245
        if not ok:
1246
            m = _(astakos_messages.PENDING_APPLICATION_LIMIT_MODIFY) % limit
1247
            messages.error(request, m)
1248
            next = reverse('astakos.im.views.project_list')
1249
            next = restrict_next(next, domain=COOKIE_DOMAIN)
1250
            return redirect(next)
1251

    
1252
    details_fields = ["name", "homepage", "description", "start_date",
1253
                      "end_date", "comments"]
1254
    membership_fields = ["member_join_policy", "member_leave_policy",
1255
                         "limit_on_members_number"]
1256
    resource_catalog, resource_groups = _resources_catalog(for_project=True)
1257
    if resource_catalog is False:
1258
        # on fail resource_groups contains the result object
1259
        result = resource_groups
1260
        messages.error(request, 'Unable to retrieve system resources: %s' %
1261
                       result.reason)
1262
    extra_context = {
1263
        'resource_catalog': resource_catalog,
1264
        'resource_groups': resource_groups,
1265
        'show_form': True,
1266
        'details_fields': details_fields,
1267
        'update_form': True,
1268
        'membership_fields': membership_fields
1269
    }
1270

    
1271
    response = None
1272
    with ExceptionHandler(request):
1273
        response = _update_object(
1274
            request,
1275
            object_id=application_id,
1276
            template_name='im/projects/projectapplication_form.html',
1277
            extra_context=extra_context,
1278
            post_save_redirect=reverse('project_list'),
1279
            form_class=ProjectApplicationForm,
1280
            msg=_("The %(verbose_name)s has been received and is under "
1281
                  "consideration."))
1282

    
1283
    if response is not None:
1284
        return response
1285

    
1286
    next = reverse('astakos.im.views.project_list')
1287
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1288
    return redirect(next)
1289

    
1290
@require_http_methods(["GET", "POST"])
1291
@valid_astakos_user_required
1292
def project_app(request, application_id):
1293
    return common_detail(request, application_id, project_view=False)
1294

    
1295
@require_http_methods(["GET", "POST"])
1296
@valid_astakos_user_required
1297
def project_detail(request, chain_id):
1298
    return common_detail(request, chain_id)
1299

    
1300
@commit_on_success_strict()
1301
def addmembers(request, chain_id, addmembers_form):
1302
    if addmembers_form.is_valid():
1303
        try:
1304
            chain_id = int(chain_id)
1305
            map(lambda u: enroll_member(
1306
                    chain_id,
1307
                    u,
1308
                    request_user=request.user),
1309
                addmembers_form.valid_users)
1310
        except (IOError, PermissionDenied), e:
1311
            messages.error(request, e)
1312

    
1313
def common_detail(request, chain_or_app_id, project_view=True):
1314
    project = None
1315
    if project_view:
1316
        chain_id = chain_or_app_id
1317
        if request.method == 'POST':
1318
            addmembers_form = AddProjectMembersForm(
1319
                request.POST,
1320
                chain_id=int(chain_id),
1321
                request_user=request.user)
1322
            with ExceptionHandler(request):
1323
                addmembers(request, chain_id, addmembers_form)
1324

    
1325
            if addmembers_form.is_valid():
1326
                addmembers_form = AddProjectMembersForm()  # clear form data
1327
        else:
1328
            addmembers_form = AddProjectMembersForm()  # initialize form
1329

    
1330
        project, application = get_by_chain_or_404(chain_id)
1331
        if project:
1332
            members = project.projectmembership_set.select_related()
1333
            members_table = tables.ProjectMembersTable(project,
1334
                                                       members,
1335
                                                       user=request.user,
1336
                                                       prefix="members_")
1337
            RequestConfig(request, paginate={"per_page": PAGINATE_BY}
1338
                          ).configure(members_table)
1339

    
1340
        else:
1341
            members_table = None
1342

    
1343
    else: # is application
1344
        application_id = chain_or_app_id
1345
        application = get_object_or_404(ProjectApplication, pk=application_id)
1346
        members_table = None
1347
        addmembers_form = None
1348

    
1349
    modifications_table = None
1350

    
1351
    user = request.user
1352
    is_project_admin = user.is_project_admin(application_id=application.id)
1353
    is_owner = user.owns_application(application)
1354
    if not (is_owner or is_project_admin) and not project_view:
1355
        m = _(astakos_messages.NOT_ALLOWED)
1356
        raise PermissionDenied(m)
1357

    
1358
    if (not (is_owner or is_project_admin) and project_view and
1359
        not user.non_owner_can_view(project)):
1360
        m = _(astakos_messages.NOT_ALLOWED)
1361
        raise PermissionDenied(m)
1362

    
1363
    following_applications = list(application.pending_modifications())
1364
    following_applications.reverse()
1365
    modifications_table = (
1366
        tables.ProjectModificationApplicationsTable(following_applications,
1367
                                                    user=request.user,
1368
                                                    prefix="modifications_"))
1369

    
1370
    mem_display = user.membership_display(project) if project else None
1371
    can_join_req = can_join_request(project, user) if project else False
1372
    can_leave_req = can_leave_request(project, user) if project else False
1373

    
1374
    return object_detail(
1375
        request,
1376
        queryset=ProjectApplication.objects.select_related(),
1377
        object_id=application.id,
1378
        template_name='im/projects/project_detail.html',
1379
        extra_context={
1380
            'project_view': project_view,
1381
            'addmembers_form':addmembers_form,
1382
            'members_table': members_table,
1383
            'owner_mode': is_owner,
1384
            'admin_mode': is_project_admin,
1385
            'modifications_table': modifications_table,
1386
            'mem_display': mem_display,
1387
            'can_join_request': can_join_req,
1388
            'can_leave_request': can_leave_req,
1389
            })
1390

    
1391
@require_http_methods(["GET", "POST"])
1392
@valid_astakos_user_required
1393
def project_search(request):
1394
    q = request.GET.get('q', '')
1395
    form = ProjectSearchForm()
1396
    q = q.strip()
1397

    
1398
    if request.method == "POST":
1399
        form = ProjectSearchForm(request.POST)
1400
        if form.is_valid():
1401
            q = form.cleaned_data['q'].strip()
1402
        else:
1403
            q = None
1404

    
1405
    if q is None:
1406
        projects = ProjectApplication.objects.none()
1407
    else:
1408
        accepted_projects = request.user.projectmembership_set.filter(
1409
            ~Q(acceptance_date__isnull=True)).values_list('project', flat=True)
1410
        projects = ProjectApplication.objects.search_by_name(q)
1411
        projects = projects.filter(~Q(project__last_approval_date__isnull=True))
1412
        projects = projects.exclude(project__in=accepted_projects)
1413

    
1414
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1415
                                                prefix="my_projects_")
1416
    if request.method == "POST":
1417
        table.caption = _('SEARCH RESULTS')
1418
    else:
1419
        table.caption = _('ALL PROJECTS')
1420

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

    
1423
    return object_list(
1424
        request,
1425
        projects,
1426
        template_name='im/projects/project_list.html',
1427
        extra_context={
1428
          'form': form,
1429
          'is_search': True,
1430
          'q': q,
1431
          'table': table
1432
        })
1433

    
1434
@require_http_methods(["POST"])
1435
@valid_astakos_user_required
1436
def project_join(request, chain_id):
1437
    next = request.GET.get('next')
1438
    if not next:
1439
        next = reverse('astakos.im.views.project_detail',
1440
                       args=(chain_id,))
1441

    
1442
    with ExceptionHandler(request):
1443
        _project_join(request, chain_id)
1444

    
1445

    
1446
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1447
    return redirect(next)
1448

    
1449

    
1450
@commit_on_success_strict()
1451
def _project_join(request, chain_id):
1452
    try:
1453
        chain_id = int(chain_id)
1454
        auto_accepted = join_project(chain_id, request.user)
1455
        if auto_accepted:
1456
            m = _(astakos_messages.USER_JOINED_PROJECT)
1457
        else:
1458
            m = _(astakos_messages.USER_JOIN_REQUEST_SUBMITTED)
1459
        messages.success(request, m)
1460
    except (IOError, PermissionDenied), e:
1461
        messages.error(request, e)
1462

    
1463

    
1464
@require_http_methods(["POST"])
1465
@valid_astakos_user_required
1466
def project_leave(request, chain_id):
1467
    next = request.GET.get('next')
1468
    if not next:
1469
        next = reverse('astakos.im.views.project_list')
1470

    
1471
    with ExceptionHandler(request):
1472
        _project_leave(request, chain_id)
1473

    
1474
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1475
    return redirect(next)
1476

    
1477

    
1478
@commit_on_success_strict()
1479
def _project_leave(request, chain_id):
1480
    try:
1481
        chain_id = int(chain_id)
1482
        auto_accepted = leave_project(chain_id, request.user)
1483
        if auto_accepted:
1484
            m = _(astakos_messages.USER_LEFT_PROJECT)
1485
        else:
1486
            m = _(astakos_messages.USER_LEAVE_REQUEST_SUBMITTED)
1487
        messages.success(request, m)
1488
    except (IOError, PermissionDenied), e:
1489
        messages.error(request, e)
1490

    
1491

    
1492
@require_http_methods(["POST"])
1493
@valid_astakos_user_required
1494
def project_cancel(request, chain_id):
1495
    next = request.GET.get('next')
1496
    if not next:
1497
        next = reverse('astakos.im.views.project_list')
1498

    
1499
    with ExceptionHandler(request):
1500
        _project_cancel(request, chain_id)
1501

    
1502
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1503
    return redirect(next)
1504

    
1505

    
1506
@commit_on_success_strict()
1507
def _project_cancel(request, chain_id):
1508
    try:
1509
        chain_id = int(chain_id)
1510
        cancel_membership(chain_id, request.user)
1511
        m = _(astakos_messages.USER_REQUEST_CANCELLED)
1512
        messages.success(request, m)
1513
    except (IOError, PermissionDenied), e:
1514
        messages.error(request, e)
1515

    
1516

    
1517

    
1518
@require_http_methods(["POST"])
1519
@valid_astakos_user_required
1520
def project_accept_member(request, chain_id, memb_id):
1521

    
1522
    with ExceptionHandler(request):
1523
        _project_accept_member(request, chain_id, memb_id)
1524

    
1525
    return redirect(reverse('project_detail', args=(chain_id,)))
1526

    
1527

    
1528
@commit_on_success_strict()
1529
def _project_accept_member(request, chain_id, memb_id):
1530
    try:
1531
        chain_id = int(chain_id)
1532
        memb_id = int(memb_id)
1533
        m = accept_membership(chain_id, memb_id, request.user)
1534
    except (IOError, PermissionDenied), e:
1535
        messages.error(request, e)
1536

    
1537
    else:
1538
        email = escape(m.person.email)
1539
        msg = _(astakos_messages.USER_MEMBERSHIP_ACCEPTED) % email
1540
        messages.success(request, msg)
1541

    
1542

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

    
1547
    with ExceptionHandler(request):
1548
        _project_remove_member(request, chain_id, memb_id)
1549

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

    
1552

    
1553
@commit_on_success_strict()
1554
def _project_remove_member(request, chain_id, memb_id):
1555
    try:
1556
        chain_id = int(chain_id)
1557
        memb_id = int(memb_id)
1558
        m = remove_membership(chain_id, memb_id, request.user)
1559
    except (IOError, PermissionDenied), e:
1560
        messages.error(request, e)
1561
    else:
1562
        email = escape(m.person.email)
1563
        msg = _(astakos_messages.USER_MEMBERSHIP_REMOVED) % email
1564
        messages.success(request, msg)
1565

    
1566

    
1567
@require_http_methods(["POST"])
1568
@valid_astakos_user_required
1569
def project_reject_member(request, chain_id, memb_id):
1570

    
1571
    with ExceptionHandler(request):
1572
        _project_reject_member(request, chain_id, memb_id)
1573

    
1574
    return redirect(reverse('project_detail', args=(chain_id,)))
1575

    
1576

    
1577
@commit_on_success_strict()
1578
def _project_reject_member(request, chain_id, memb_id):
1579
    try:
1580
        chain_id = int(chain_id)
1581
        memb_id = int(memb_id)
1582
        m = reject_membership(chain_id, memb_id, request.user)
1583
    except (IOError, PermissionDenied), e:
1584
        messages.error(request, e)
1585
    else:
1586
        email = escape(m.person.email)
1587
        msg = _(astakos_messages.USER_MEMBERSHIP_REJECTED) % email
1588
        messages.success(request, msg)
1589

    
1590

    
1591
@require_http_methods(["POST"])
1592
@signed_terms_required
1593
@login_required
1594
def project_app_approve(request, application_id):
1595

    
1596
    if not request.user.is_project_admin():
1597
        m = _(astakos_messages.NOT_ALLOWED)
1598
        raise PermissionDenied(m)
1599

    
1600
    try:
1601
        app = ProjectApplication.objects.get(id=application_id)
1602
    except ProjectApplication.DoesNotExist:
1603
        raise Http404
1604

    
1605
    with ExceptionHandler(request):
1606
        _project_app_approve(request, application_id)
1607

    
1608
    chain_id = get_related_project_id(application_id)
1609
    return redirect(reverse('project_detail', args=(chain_id,)))
1610

    
1611

    
1612
@commit_on_success_strict()
1613
def _project_app_approve(request, application_id):
1614
    approve_application(application_id)
1615

    
1616

    
1617
@require_http_methods(["POST"])
1618
@signed_terms_required
1619
@login_required
1620
def project_app_deny(request, application_id):
1621

    
1622
    reason = request.POST.get('reason', None)
1623
    if not reason:
1624
        reason = None
1625

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

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

    
1635
    with ExceptionHandler(request):
1636
        _project_app_deny(request, application_id, reason)
1637

    
1638
    return redirect(reverse('project_list'))
1639

    
1640

    
1641
@commit_on_success_strict()
1642
def _project_app_deny(request, application_id, reason):
1643
    deny_application(application_id, reason=reason)
1644

    
1645

    
1646
@require_http_methods(["POST"])
1647
@signed_terms_required
1648
@login_required
1649
def project_app_dismiss(request, application_id):
1650
    try:
1651
        app = ProjectApplication.objects.get(id=application_id)
1652
    except ProjectApplication.DoesNotExist:
1653
        raise Http404
1654

    
1655
    if not request.user.owns_application(app):
1656
        m = _(astakos_messages.NOT_ALLOWED)
1657
        raise PermissionDenied(m)
1658

    
1659
    with ExceptionHandler(request):
1660
        _project_app_dismiss(request, application_id)
1661

    
1662
    chain_id = None
1663
    chain_id = get_related_project_id(application_id)
1664
    if chain_id:
1665
        next = reverse('project_detail', args=(chain_id,))
1666
    else:
1667
        next = reverse('project_list')
1668
    return redirect(next)
1669

    
1670

    
1671
def _project_app_dismiss(request, application_id):
1672
    # XXX: dismiss application also does authorization
1673
    dismiss_application(application_id, request_user=request.user)
1674

    
1675

    
1676
@require_http_methods(["GET"])
1677
@required_auth_methods_assigned(allow_access=True)
1678
@login_required
1679
@signed_terms_required
1680
def landing(request):
1681
    context = {'services': Service.catalog(orderfor='dashboard')}
1682
    return render_response(
1683
        'im/landing.html',
1684
        context_instance=get_context(request), **context)
1685

    
1686

    
1687
def api_access(request):
1688
    return render_response(
1689
        'im/api_access.html',
1690
        context_instance=get_context(request))