Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (55.7 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.activation_backends import get_backend, SimpleBackend
78
from astakos.im import tables
79
from astakos.im.models import (
80
    AstakosUser, ApprovalTerms,
81
    EmailChange, RESOURCE_SEPARATOR,
82
    AstakosUserAuthProvider, PendingThirdPartyUser,
83
    PendingMembershipError,
84
    ProjectApplication, ProjectMembership, Project)
85
from astakos.im.util import (
86
    get_context, prepare_response, get_query, restrict_next)
87
from astakos.im.forms import (
88
    LoginForm, InvitationForm,
89
    FeedbackForm, SignApprovalTermsForm,
90
    EmailChangeForm,
91
    ProjectApplicationForm, ProjectSortForm,
92
    AddProjectMembersForm, ProjectSearchForm,
93
    ProjectMembersSortForm)
94
from astakos.im.forms import ExtendedProfileForm as ProfileForm
95
from astakos.im.functions import (
96
    send_feedback, SendMailError,
97
    logout as auth_logout,
98
    activate as activate_func,
99
    invite,
100
    send_activation as send_activation_func,
101
    SendNotificationError,
102
    accept_membership, reject_membership, remove_membership, cancel_membership,
103
    leave_project, join_project, enroll_member, can_join_request, can_leave_request,
104
    get_related_project_id, get_by_chain_or_404,
105
    approve_application, deny_application,
106
    cancel_application, dismiss_application)
107
from astakos.im.settings import (
108
    COOKIE_DOMAIN, LOGOUT_NEXT,
109
    LOGGING_LEVEL, PAGINATE_BY,
110
    RESOURCES_PRESENTATION_DATA, PAGINATE_BY_ALL,
111
    ACTIVATION_REDIRECT_URL,
112
    MODERATION_ENABLED)
113
from astakos.im.api import get_services_dict
114
from astakos.im import settings as astakos_settings
115
from astakos.im.api.callpoint import AstakosCallpoint
116
from astakos.im import auth_providers
117
from astakos.im.project_xctx import project_transaction_context
118
from astakos.im.retry_xctx import RetryException
119

    
120
logger = logging.getLogger(__name__)
121

    
122
callpoint = AstakosCallpoint()
123

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

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

    
146
            if not provider or not provider.is_active():
147
                raise PermissionDenied
148

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

    
159

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

    
174

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

    
190

    
191
def required_auth_methods_assigned(only_warn=False):
192
    """
193
    Decorator that checks whether the request.user has all required auth providers
194
    assigned.
195
    """
196
    required_providers = auth_providers.REQUIRED_PROVIDERS.keys()
197

    
198
    def decorator(func):
199
        if not required_providers:
200
            return func
201

    
202
        @wraps(func)
203
        def wrapper(request, *args, **kwargs):
204
            if request.user.is_authenticated():
205
                for required in required_providers:
206
                    if not request.user.has_auth_provider(required):
207
                        provider = auth_providers.get_provider(required)
208
                        if only_warn:
209
                            messages.error(request,
210
                                           _(astakos_messages.AUTH_PROVIDER_REQUIRED  % {
211
                                               'provider': provider.get_title_display}))
212
                        else:
213
                            return HttpResponseRedirect(reverse('edit_profile'))
214
            return func(request, *args, **kwargs)
215
        return wrapper
216
    return decorator
217

    
218

    
219
def valid_astakos_user_required(func):
220
    return signed_terms_required(required_auth_methods_assigned()(login_required(func)))
221

    
222

    
223
@require_http_methods(["GET", "POST"])
224
@signed_terms_required
225
def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context=None):
226
    """
227
    If there is logged on user renders the profile page otherwise renders login page.
228

229
    **Arguments**
230

231
    ``login_template_name``
232
        A custom login template to use. This is optional; if not specified,
233
        this will default to ``im/login.html``.
234

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

239
    ``extra_context``
240
        An dictionary of variables to add to the template context.
241

242
    **Template:**
243

244
    im/profile.html or im/login.html or ``template_name`` keyword argument.
245

246
    """
247
    extra_context = extra_context or {}
248
    template_name = login_template_name
249
    if request.user.is_authenticated():
250
        return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
251

    
252
    third_party_token = request.GET.get('key', False)
253
    if third_party_token:
254
        messages.info(request, astakos_messages.AUTH_PROVIDER_LOGIN_TO_ADD)
255

    
256
    return render_response(
257
        template_name,
258
        login_form = LoginForm(request=request),
259
        context_instance = get_context(request, extra_context)
260
    )
261

    
262

    
263
@require_http_methods(["POST"])
264
@valid_astakos_user_required
265
def update_token(request):
266
    """
267
    Update api token view.
268
    """
269
    user = request.user
270
    user.renew_token()
271
    user.save()
272
    messages.success(request, astakos_messages.TOKEN_UPDATED)
273
    return HttpResponseRedirect(reverse('edit_profile'))
274

    
275

    
276
@require_http_methods(["GET", "POST"])
277
@valid_astakos_user_required
278
@transaction.commit_manually
279
def invite(request, template_name='im/invitations.html', extra_context=None):
280
    """
281
    Allows a user to invite somebody else.
282

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

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

290
    If the user isn't logged in, redirects to settings.LOGIN_URL.
291

292
    **Arguments**
293

294
    ``template_name``
295
        A custom template to use. This is optional; if not specified,
296
        this will default to ``im/invitations.html``.
297

298
    ``extra_context``
299
        An dictionary of variables to add to the template context.
300

301
    **Template:**
302

303
    im/invitations.html or ``template_name`` keyword argument.
304

305
    **Settings:**
306

307
    The view expectes the following settings are defined:
308

309
    * LOGIN_URL: login uri
310
    """
311
    extra_context = extra_context or {}
312
    status = None
313
    message = None
314
    form = InvitationForm()
315

    
316
    inviter = request.user
317
    if request.method == 'POST':
318
        form = InvitationForm(request.POST)
319
        if inviter.invitations > 0:
320
            if form.is_valid():
321
                try:
322
                    email = form.cleaned_data.get('username')
323
                    realname = form.cleaned_data.get('realname')
324
                    invite(inviter, email, realname)
325
                    message = _(astakos_messages.INVITATION_SENT) % locals()
326
                    messages.success(request, message)
327
                except SendMailError, e:
328
                    message = e.message
329
                    messages.error(request, message)
330
                    transaction.rollback()
331
                except BaseException, e:
332
                    message = _(astakos_messages.GENERIC_ERROR)
333
                    messages.error(request, message)
334
                    logger.exception(e)
335
                    transaction.rollback()
336
                else:
337
                    transaction.commit()
338
        else:
339
            message = _(astakos_messages.MAX_INVITATION_NUMBER_REACHED)
340
            messages.error(request, message)
341

    
342
    sent = [{'email': inv.username,
343
             'realname': inv.realname,
344
             'is_consumed': inv.is_consumed}
345
            for inv in request.user.invitations_sent.all()]
346
    kwargs = {'inviter': inviter,
347
              'sent': sent}
348
    context = get_context(request, extra_context, **kwargs)
349
    return render_response(template_name,
350
                           invitation_form=form,
351
                           context_instance=context)
352

    
353

    
354
@require_http_methods(["GET", "POST"])
355
@required_auth_methods_assigned(only_warn=True)
356
@login_required
357
@signed_terms_required
358
def edit_profile(request, template_name='im/profile.html', extra_context=None):
359
    """
360
    Allows a user to edit his/her profile.
361

362
    In case of GET request renders a form for displaying the user information.
363
    In case of POST updates the user informantion and redirects to ``next``
364
    url parameter if exists.
365

366
    If the user isn't logged in, redirects to settings.LOGIN_URL.
367

368
    **Arguments**
369

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

374
    ``extra_context``
375
        An dictionary of variables to add to the template context.
376

377
    **Template:**
378

379
    im/profile.html or ``template_name`` keyword argument.
380

381
    **Settings:**
382

383
    The view expectes the following settings are defined:
384

385
    * LOGIN_URL: login uri
386
    """
387
    extra_context = extra_context or {}
388
    form = ProfileForm(
389
        instance=request.user,
390
        session_key=request.session.session_key
391
    )
392
    extra_context['next'] = request.GET.get('next')
393
    if request.method == 'POST':
394
        form = ProfileForm(
395
            request.POST,
396
            instance=request.user,
397
            session_key=request.session.session_key
398
        )
399
        if form.is_valid():
400
            try:
401
                prev_token = request.user.auth_token
402
                user = form.save(request=request)
403
                next = restrict_next(
404
                    request.POST.get('next'),
405
                    domain=COOKIE_DOMAIN
406
                )
407
                msg = _(astakos_messages.PROFILE_UPDATED)
408
                messages.success(request, msg)
409

    
410
                if form.email_changed:
411
                    msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
412
                    messages.success(request, msg)
413
                if form.password_changed:
414
                    msg = _(astakos_messages.PASSWORD_CHANGED)
415
                    messages.success(request, msg)
416

    
417
                if next:
418
                    return redirect(next)
419
                else:
420
                    return redirect(reverse('edit_profile'))
421
            except ValueError, ve:
422
                messages.success(request, ve)
423
    elif request.method == "GET":
424
        request.user.is_verified = True
425
        request.user.save()
426

    
427
    # existing providers
428
    user_providers = request.user.get_active_auth_providers()
429

    
430
    # providers that user can add
431
    user_available_providers = request.user.get_available_auth_providers()
432

    
433
    extra_context['services'] = get_services_dict()
434
    return render_response(template_name,
435
                           profile_form = form,
436
                           user_providers = user_providers,
437
                           user_available_providers = user_available_providers,
438
                           context_instance = get_context(request,
439
                                                          extra_context))
440

    
441

    
442
@transaction.commit_manually
443
@require_http_methods(["GET", "POST"])
444
def signup(request, template_name='im/signup.html', on_success='index', extra_context=None, backend=None):
445
    """
446
    Allows a user to create a local account.
447

448
    In case of GET request renders a form for entering the user information.
449
    In case of POST handles the signup.
450

451
    The user activation will be delegated to the backend specified by the ``backend`` keyword argument
452
    if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
453
    if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
454
    (see activation_backends);
455

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

459
    On unsuccessful creation, renders ``template_name`` with an error message.
460

461
    **Arguments**
462

463
    ``template_name``
464
        A custom template to render. This is optional;
465
        if not specified, this will default to ``im/signup.html``.
466

467
    ``extra_context``
468
        An dictionary of variables to add to the template context.
469

470
    ``on_success``
471
        Resolvable view name to redirect on registration success.
472

473
    **Template:**
474

475
    im/signup.html or ``template_name`` keyword argument.
476
    """
477
    extra_context = extra_context or {}
478
    if request.user.is_authenticated():
479
        return HttpResponseRedirect(reverse('edit_profile'))
480

    
481
    provider = get_query(request).get('provider', 'local')
482
    if not auth_providers.get_provider(provider).is_available_for_create():
483
        raise PermissionDenied
484

    
485
    id = get_query(request).get('id')
486
    try:
487
        instance = AstakosUser.objects.get(id=id) if id else None
488
    except AstakosUser.DoesNotExist:
489
        instance = None
490

    
491
    pending_user = None
492
    third_party_token = request.REQUEST.get('third_party_token', None)
493
    if third_party_token:
494
        pending = get_object_or_404(PendingThirdPartyUser,
495
                                    token=third_party_token)
496
        provider = pending.provider
497
        instance = pending.get_user_instance()
498
        if pending.existing_user().count() > 0:
499
            pending_user = pending.existing_user().get()
500
            if request.method == "GET":
501
                messages.warning(request, pending_user.get_inactive_message())
502

    
503

    
504
    extra_context['pending_user_exists'] = pending_user
505

    
506
    try:
507
        if not backend:
508
            backend = get_backend(request)
509
        form = backend.get_signup_form(provider, instance)
510
    except Exception, e:
511
        form = SimpleBackend(request).get_signup_form(provider)
512
        messages.error(request, e)
513
    if request.method == 'POST':
514
        if form.is_valid():
515
            user = form.save(commit=False)
516

    
517
            # delete previously unverified accounts
518
            if AstakosUser.objects.user_exists(user.email):
519
                AstakosUser.objects.get_by_identifier(user.email).delete()
520

    
521
            try:
522
                result = backend.handle_activation(user)
523
                status = messages.SUCCESS
524
                message = result.message
525

    
526
                form.store_user(user, request)
527

    
528
                if 'additional_email' in form.cleaned_data:
529
                    additional_email = form.cleaned_data['additional_email']
530
                    if additional_email != user.email:
531
                        user.additionalmail_set.create(email=additional_email)
532
                        msg = 'Additional email: %s saved for user %s.' % (
533
                            additional_email,
534
                            user.email
535
                        )
536
                        logger._log(LOGGING_LEVEL, msg, [])
537

    
538
                if user and user.is_active:
539
                    next = request.POST.get('next', '')
540
                    response = prepare_response(request, user, next=next)
541
                    transaction.commit()
542
                    return response
543

    
544
                transaction.commit()
545
                messages.add_message(request, status, message)
546
                return HttpResponseRedirect(reverse(on_success))
547

    
548
            except SendMailError, e:
549
                logger.exception(e)
550
                status = messages.ERROR
551
                message = e.message
552
                messages.error(request, message)
553
                transaction.rollback()
554
            except BaseException, e:
555
                logger.exception(e)
556
                message = _(astakos_messages.GENERIC_ERROR)
557
                messages.error(request, message)
558
                logger.exception(e)
559
                transaction.rollback()
560

    
561
    return render_response(template_name,
562
                           signup_form=form,
563
                           third_party_token=third_party_token,
564
                           provider=provider,
565
                           context_instance=get_context(request, extra_context))
566

    
567

    
568
@require_http_methods(["GET", "POST"])
569
@required_auth_methods_assigned(only_warn=True)
570
@login_required
571
@signed_terms_required
572
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
573
    """
574
    Allows a user to send feedback.
575

576
    In case of GET request renders a form for providing the feedback information.
577
    In case of POST sends an email to support team.
578

579
    If the user isn't logged in, redirects to settings.LOGIN_URL.
580

581
    **Arguments**
582

583
    ``template_name``
584
        A custom template to use. This is optional; if not specified,
585
        this will default to ``im/feedback.html``.
586

587
    ``extra_context``
588
        An dictionary of variables to add to the template context.
589

590
    **Template:**
591

592
    im/signup.html or ``template_name`` keyword argument.
593

594
    **Settings:**
595

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

    
605
        form = FeedbackForm(request.POST)
606
        if form.is_valid():
607
            msg = form.cleaned_data['feedback_msg']
608
            data = form.cleaned_data['feedback_data']
609
            try:
610
                send_feedback(msg, data, request.user, email_template_name)
611
            except SendMailError, e:
612
                messages.error(request, message)
613
            else:
614
                message = _(astakos_messages.FEEDBACK_SENT)
615
                messages.success(request, message)
616
    return render_response(template_name,
617
                           feedback_form=form,
618
                           context_instance=get_context(request, extra_context))
619

    
620

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

    
637
    next = restrict_next(
638
        request.GET.get('next'),
639
        domain=COOKIE_DOMAIN
640
    )
641

    
642
    if next:
643
        response['Location'] = next
644
        response.status_code = 302
645
    elif LOGOUT_NEXT:
646
        response['Location'] = LOGOUT_NEXT
647
        response.status_code = 301
648
    else:
649
        message = _(astakos_messages.LOGOUT_SUCCESS)
650
        last_provider = request.COOKIES.get('astakos_last_login_method', None)
651
        if last_provider:
652
            provider = auth_providers.get_provider(last_provider)
653
            extra_message = provider.get_logout_message_display
654
            if extra_message:
655
                message += '<br />' + extra_message
656
        messages.success(request, message)
657
        response['Location'] = reverse('index')
658
        response.status_code = 301
659
    return response
660

    
661

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

670
    The view uses commit_manually decorator in order to ensure the user state will be updated
671
    only if the email will be send successfully.
672
    """
673
    token = request.GET.get('auth')
674
    next = request.GET.get('next')
675
    try:
676
        user = AstakosUser.objects.get(auth_token=token)
677
    except AstakosUser.DoesNotExist:
678
        return HttpResponseBadRequest(_(astakos_messages.ACCOUNT_UNKNOWN))
679

    
680
    if user.is_active:
681
        message = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE)
682
        messages.error(request, message)
683
        return index(request)
684

    
685
    try:
686
        activate_func(user, greeting_email_template_name,
687
                      helpdesk_email_template_name, verify_email=True)
688
        messages.success(request, _(astakos_messages.ACCOUNT_ACTIVATED))
689
        next = ACTIVATION_REDIRECT_URL or next
690
        response = prepare_response(request, user, next, renew=True)
691
        transaction.commit()
692
        return response
693
    except SendMailError, e:
694
        message = e.message
695
        messages.add_message(request, messages.ERROR, message)
696
        transaction.rollback()
697
        return index(request)
698
    except BaseException, e:
699
        status = messages.ERROR
700
        message = _(astakos_messages.GENERIC_ERROR)
701
        messages.add_message(request, messages.ERROR, message)
702
        logger.exception(e)
703
        transaction.rollback()
704
        return index(request)
705

    
706

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

    
723
    if not term:
724
        messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
725
        return HttpResponseRedirect(reverse('index'))
726
    try:
727
        f = open(term.location, 'r')
728
    except IOError:
729
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
730
        return render_response(
731
            template_name, context_instance=get_context(request, extra_context))
732

    
733
    terms = f.read()
734

    
735
    if request.method == 'POST':
736
        next = restrict_next(
737
            request.POST.get('next'),
738
            domain=COOKIE_DOMAIN
739
        )
740
        if not next:
741
            next = reverse('index')
742
        form = SignApprovalTermsForm(request.POST, instance=request.user)
743
        if not form.is_valid():
744
            return render_response(template_name,
745
                                   terms=terms,
746
                                   approval_terms_form=form,
747
                                   context_instance=get_context(request, extra_context))
748
        user = form.save()
749
        return HttpResponseRedirect(next)
750
    else:
751
        form = None
752
        if request.user.is_authenticated() and not request.user.signed_terms:
753
            form = SignApprovalTermsForm(instance=request.user)
754
        return render_response(template_name,
755
                               terms=terms,
756
                               approval_terms_form=form,
757
                               context_instance=get_context(request, extra_context))
758

    
759

    
760
@require_http_methods(["GET", "POST"])
761
@transaction.commit_manually
762
def change_email(request, activation_key=None,
763
                 email_template_name='registration/email_change_email.txt',
764
                 form_template_name='registration/email_change_form.html',
765
                 confirm_template_name='registration/email_change_done.html',
766
                 extra_context=None):
767
    extra_context = extra_context or {}
768

    
769

    
770
    if not astakos_settings.EMAILCHANGE_ENABLED:
771
        raise PermissionDenied
772

    
773
    if activation_key:
774
        try:
775
            user = EmailChange.objects.change_email(activation_key)
776
            if request.user.is_authenticated() and request.user == user or not \
777
                    request.user.is_authenticated():
778
                msg = _(astakos_messages.EMAIL_CHANGED)
779
                messages.success(request, msg)
780
                transaction.commit()
781
                return HttpResponseRedirect(reverse('edit_profile'))
782
        except ValueError, e:
783
            messages.error(request, e)
784
            transaction.rollback()
785
            return HttpResponseRedirect(reverse('index'))
786

    
787
        return render_response(confirm_template_name,
788
                               modified_user=user if 'user' in locals() \
789
                               else None, context_instance=get_context(request,
790
                                                            extra_context))
791

    
792
    if not request.user.is_authenticated():
793
        path = quote(request.get_full_path())
794
        url = request.build_absolute_uri(reverse('index'))
795
        return HttpResponseRedirect(url + '?next=' + path)
796

    
797
    # clean up expired email changes
798
    if request.user.email_change_is_pending():
799
        change = request.user.emailchanges.get()
800
        if change.activation_key_expired():
801
            change.delete()
802
            transaction.commit()
803
            return HttpResponseRedirect(reverse('email_change'))
804

    
805
    form = EmailChangeForm(request.POST or None)
806
    if request.method == 'POST' and form.is_valid():
807
        try:
808
            ec = form.save(email_template_name, request)
809
        except SendMailError, e:
810
            msg = e
811
            messages.error(request, msg)
812
            transaction.rollback()
813
            return HttpResponseRedirect(reverse('edit_profile'))
814
        else:
815
            msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
816
            messages.success(request, msg)
817
            transaction.commit()
818
            return HttpResponseRedirect(reverse('edit_profile'))
819

    
820
    if request.user.email_change_is_pending():
821
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
822

    
823
    return render_response(
824
        form_template_name,
825
        form=form,
826
        context_instance=get_context(request, extra_context)
827
    )
828

    
829

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

    
832
    if request.user.is_authenticated():
833
        return HttpResponseRedirect(reverse('edit_profile'))
834

    
835
    # TODO: check if moderation is only enabled for local login
836
    if astakos_settings.MODERATION_ENABLED:
837
        raise PermissionDenied
838

    
839
    extra_context = extra_context or {}
840
    try:
841
        u = AstakosUser.objects.get(id=user_id)
842
    except AstakosUser.DoesNotExist:
843
        messages.error(request, _(astakos_messages.ACCOUNT_UNKNOWN))
844
    else:
845
        try:
846
            send_activation_func(u)
847
            msg = _(astakos_messages.ACTIVATION_SENT)
848
            messages.success(request, msg)
849
        except SendMailError, e:
850
            messages.error(request, e)
851

    
852
    return HttpResponseRedirect(reverse('index'))
853

    
854

    
855
@require_http_methods(["GET"])
856
@valid_astakos_user_required
857
def resource_usage(request):
858

    
859
    def with_class(entry):
860
         entry['load_class'] = 'red'
861
         max_value = float(entry['maxValue'])
862
         curr_value = float(entry['currValue'])
863
         entry['ratio_limited']= 0
864
         if max_value > 0 :
865
             entry['ratio'] = (curr_value / max_value) * 100
866
         else:
867
             entry['ratio'] = 0
868
         if entry['ratio'] < 66:
869
             entry['load_class'] = 'yellow'
870
         if entry['ratio'] < 33:
871
             entry['load_class'] = 'green'
872
         if entry['ratio']<0:
873
             entry['ratio'] = 0
874
         if entry['ratio']>100:
875
             entry['ratio_limited'] = 100
876
         else:
877
             entry['ratio_limited'] = entry['ratio']
878
         return entry
879

    
880
    def pluralize(entry):
881
        entry['plural'] = engine.plural(entry.get('name'))
882
        return entry
883

    
884
    resource_usage = None
885
    result = callpoint.get_user_usage(request.user.id)
886
    if result.is_success:
887
        resource_usage = result.data
888
        backenddata = map(with_class, result.data)
889
        backenddata = map(pluralize , backenddata)
890
    else:
891
        messages.error(request, result.reason)
892
        backenddata = []
893
        resource_usage = []
894

    
895
    if request.REQUEST.get('json', None):
896
        return HttpResponse(json.dumps(backenddata),
897
                            mimetype="application/json")
898

    
899
    return render_response('im/resource_usage.html',
900
                           context_instance=get_context(request),
901
                           resource_usage=backenddata,
902
                           usage_update_interval=astakos_settings.USAGE_UPDATE_INTERVAL,
903
                           result=result)
904

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

    
915
    if provider.can_remove():
916
        provider.delete()
917
        message = astakos_messages.AUTH_PROVIDER_REMOVED % \
918
                            provider.settings.get_method_prompt_display
919
        messages.success(request, message)
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
@project_transaction_context()
931
def _create_object(request, model=None, template_name=None,
932
        template_loader=template_loader, extra_context=None, post_save_redirect=None,
933
        login_required=False, context_processors=None, form_class=None,
934
        msg=None, ctx=None):
935
    """
936
    Based of django.views.generic.create_update.create_object which displays a
937
    summary page before creating the object.
938
    """
939
    response = None
940

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

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

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

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

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

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

    
1044
@require_http_methods(["GET", "POST"])
1045
@signed_terms_required
1046
@login_required
1047
def project_add(request):
1048
    resource_groups = RESOURCES_PRESENTATION_DATA.get('groups', {})
1049
    resource_catalog = ()
1050
    result = callpoint.list_resources()
1051
    details_fields = [
1052
        "name", "homepage", "description","start_date","end_date", "comments"]
1053
    membership_fields =[
1054
        "member_join_policy", "member_leave_policy", "limit_on_members_number"]
1055
    if not result.is_success:
1056
        messages.error(
1057
            request,
1058
            'Unable to retrieve system resources: %s' % result.reason
1059
    )
1060
    else:
1061
        resource_catalog = [
1062
            [g, filter(lambda r: r.get('group', '') == g, result.data)] \
1063
                for g in resource_groups]
1064

    
1065
    # order resources
1066
    groups_order = RESOURCES_PRESENTATION_DATA.get('groups_order')
1067
    resources_order = RESOURCES_PRESENTATION_DATA.get('resources_order')
1068
    resource_catalog = sorted(resource_catalog, key=lambda g:groups_order.index(g[0]))
1069

    
1070
    resource_groups_list = sorted([(k,v) for k,v in resource_groups.items()],
1071
                                  key=lambda f:groups_order.index(f[0]))
1072
    resource_groups = OrderedDict(resource_groups_list)
1073
    for index, group in enumerate(resource_catalog):
1074
        resource_catalog[index][1] = sorted(resource_catalog[index][1],
1075
                                            key=lambda r: resources_order.index(r['str_repr']))
1076

    
1077

    
1078
    extra_context = {
1079
        'resource_catalog':resource_catalog,
1080
        'resource_groups':resource_groups,
1081
        'show_form':True,
1082
        'details_fields':details_fields,
1083
        'membership_fields':membership_fields}
1084
    return _create_object(
1085
        request,
1086
        template_name='im/projects/projectapplication_form.html',
1087
        extra_context=extra_context,
1088
        post_save_redirect=reverse('project_list'),
1089
        form_class=ProjectApplicationForm,
1090
        msg=_("The %(verbose_name)s has been received and \
1091
                 is under consideration."))
1092

    
1093

    
1094
@require_http_methods(["GET"])
1095
@signed_terms_required
1096
@login_required
1097
def project_list(request):
1098
    projects = ProjectApplication.objects.user_accessible_projects(request.user).select_related()
1099
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1100
                                                prefix="my_projects_")
1101
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1102

    
1103
    return object_list(
1104
        request,
1105
        projects,
1106
        template_name='im/projects/project_list.html',
1107
        extra_context={
1108
            'is_search':False,
1109
            'table': table,
1110
        })
1111

    
1112

    
1113
@require_http_methods(["GET", "POST"])
1114
@signed_terms_required
1115
@login_required
1116
@project_transaction_context()
1117
def project_app_cancel(request, application_id, ctx=None):
1118
    chain_id = None
1119
    try:
1120
        application_id = int(application_id)
1121
        chain_id = get_related_project_id(application_id)
1122
        cancel_application(application_id, request.user)
1123
    except (IOError, PermissionDenied), e:
1124
        messages.error(request, e)
1125
    except BaseException, e:
1126
        logger.exception(e)
1127
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1128
        if ctx:
1129
            ctx.mark_rollback()
1130
    else:
1131
        msg = _(astakos_messages.APPLICATION_CANCELLED)
1132
        messages.success(request, msg)
1133

    
1134
    next = request.GET.get('next')
1135
    if not next:
1136
        if chain_id:
1137
            next = reverse('astakos.im.views.project_detail', args=(chain_id,))
1138
        else:
1139
            next = reverse('astakos.im.views.project_list')
1140

    
1141
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1142
    return redirect(next)
1143

    
1144

    
1145
@require_http_methods(["GET", "POST"])
1146
@signed_terms_required
1147
@login_required
1148
def project_modify(request, application_id):
1149

    
1150
    try:
1151
        app = ProjectApplication.objects.get(id=application_id)
1152
    except ProjectApplication.DoesNotExist:
1153
        raise Http404
1154

    
1155
    user = request.user
1156
    if not (user.owns_application(app) or user.is_project_admin(app.id)):
1157
        m = _(astakos_messages.NOT_ALLOWED)
1158
        raise PermissionDenied(m)
1159

    
1160
    resource_groups = RESOURCES_PRESENTATION_DATA.get('groups', {})
1161
    resource_catalog = ()
1162
    result = callpoint.list_resources()
1163
    details_fields = [
1164
        "name", "homepage", "description","start_date","end_date", "comments"]
1165
    membership_fields =[
1166
        "member_join_policy", "member_leave_policy", "limit_on_members_number"]
1167
    if not result.is_success:
1168
        messages.error(
1169
            request,
1170
            'Unable to retrieve system resources: %s' % result.reason
1171
    )
1172
    else:
1173
        resource_catalog = [
1174
            (g, filter(lambda r: r.get('group', '') == g, result.data)) \
1175
                for g in resource_groups]
1176
    extra_context = {
1177
        'resource_catalog':resource_catalog,
1178
        'resource_groups':resource_groups,
1179
        'show_form':True,
1180
        'details_fields':details_fields,
1181
        'update_form': True,
1182
        'membership_fields':membership_fields}
1183
    return _update_object(
1184
        request,
1185
        object_id=application_id,
1186
        template_name='im/projects/projectapplication_form.html',
1187
        extra_context=extra_context, post_save_redirect=reverse('project_list'),
1188
        form_class=ProjectApplicationForm,
1189
        msg = _("The %(verbose_name)s has been received and \
1190
                    is under consideration."))
1191

    
1192

    
1193
@require_http_methods(["GET", "POST"])
1194
@signed_terms_required
1195
@login_required
1196
def project_app(request, application_id):
1197
    return common_detail(request, application_id, project_view=False)
1198

    
1199
@require_http_methods(["GET", "POST"])
1200
@signed_terms_required
1201
@login_required
1202
def project_detail(request, chain_id):
1203
    return common_detail(request, chain_id)
1204

    
1205
@project_transaction_context(sync=True)
1206
def addmembers(request, chain_id, addmembers_form, ctx=None):
1207
    if addmembers_form.is_valid():
1208
        try:
1209
            chain_id = int(chain_id)
1210
            map(lambda u: enroll_member(
1211
                    chain_id,
1212
                    u,
1213
                    request_user=request.user),
1214
                addmembers_form.valid_users)
1215
        except (IOError, PermissionDenied), e:
1216
            messages.error(request, e)
1217
        except BaseException, e:
1218
            if ctx:
1219
                ctx.mark_rollback()
1220
            messages.error(request, e)
1221

    
1222
def common_detail(request, chain_or_app_id, project_view=True):
1223
    project = None
1224
    if project_view:
1225
        chain_id = chain_or_app_id
1226
        if request.method == 'POST':
1227
            addmembers_form = AddProjectMembersForm(
1228
                request.POST,
1229
                chain_id=int(chain_id),
1230
                request_user=request.user)
1231
            addmembers(request, chain_id, addmembers_form)
1232
            if addmembers_form.is_valid():
1233
                addmembers_form = AddProjectMembersForm()  # clear form data
1234
        else:
1235
            addmembers_form = AddProjectMembersForm()  # initialize form
1236

    
1237
        project, application = get_by_chain_or_404(chain_id)
1238
        if project:
1239
            members = project.projectmembership_set.select_related()
1240
            members_table = tables.ProjectMembersTable(project,
1241
                                                       members,
1242
                                                       user=request.user,
1243
                                                       prefix="members_")
1244
            RequestConfig(request, paginate={"per_page": PAGINATE_BY}
1245
                          ).configure(members_table)
1246

    
1247
        else:
1248
            members_table = None
1249

    
1250
    else: # is application
1251
        application_id = chain_or_app_id
1252
        application = get_object_or_404(ProjectApplication, pk=application_id)
1253
        members_table = None
1254
        addmembers_form = None
1255

    
1256
    modifications_table = None
1257

    
1258
    user = request.user
1259
    is_project_admin = user.is_project_admin(application_id=application.id)
1260
    is_owner = user.owns_application(application)
1261
    if not (is_owner or is_project_admin) and not project_view:
1262
        m = _(astakos_messages.NOT_ALLOWED)
1263
        raise PermissionDenied(m)
1264

    
1265
    if (not (is_owner or is_project_admin) and project_view and
1266
        not user.non_owner_can_view(project)):
1267
        m = _(astakos_messages.NOT_ALLOWED)
1268
        raise PermissionDenied(m)
1269

    
1270
    following_applications = list(application.pending_modifications())
1271
    following_applications.reverse()
1272
    modifications_table = (
1273
        tables.ProjectModificationApplicationsTable(following_applications,
1274
                                                    user=request.user,
1275
                                                    prefix="modifications_"))
1276

    
1277
    mem_display = user.membership_display(project) if project else None
1278
    can_join_req = can_join_request(project, user) if project else False
1279
    can_leave_req = can_leave_request(project, user) if project else False
1280

    
1281
    return object_detail(
1282
        request,
1283
        queryset=ProjectApplication.objects.select_related(),
1284
        object_id=application.id,
1285
        template_name='im/projects/project_detail.html',
1286
        extra_context={
1287
            'project_view': project_view,
1288
            'addmembers_form':addmembers_form,
1289
            'members_table': members_table,
1290
            'owner_mode': is_owner,
1291
            'admin_mode': is_project_admin,
1292
            'modifications_table': modifications_table,
1293
            'mem_display': mem_display,
1294
            'can_join_request': can_join_req,
1295
            'can_leave_request': can_leave_req,
1296
            })
1297

    
1298
@require_http_methods(["GET", "POST"])
1299
@signed_terms_required
1300
@login_required
1301
def project_search(request):
1302
    q = request.GET.get('q', '')
1303
    form = ProjectSearchForm()
1304
    q = q.strip()
1305

    
1306
    if request.method == "POST":
1307
        form = ProjectSearchForm(request.POST)
1308
        if form.is_valid():
1309
            q = form.cleaned_data['q'].strip()
1310
        else:
1311
            q = None
1312

    
1313
    if q is None:
1314
        projects = ProjectApplication.objects.none()
1315
    else:
1316
        accepted_projects = request.user.projectmembership_set.filter(
1317
            ~Q(acceptance_date__isnull=True)).values_list('project', flat=True)
1318
        projects = ProjectApplication.objects.search_by_name(q)
1319
        projects = projects.filter(~Q(project__last_approval_date__isnull=True))
1320
        projects = projects.exclude(project__in=accepted_projects)
1321

    
1322
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1323
                                                prefix="my_projects_")
1324
    if request.method == "POST":
1325
        table.caption = _('SEARCH RESULTS')
1326
    else:
1327
        table.caption = _('ALL PROJECTS')
1328

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

    
1331
    return object_list(
1332
        request,
1333
        projects,
1334
        template_name='im/projects/project_list.html',
1335
        extra_context={
1336
          'form': form,
1337
          'is_search': True,
1338
          'q': q,
1339
          'table': table
1340
        })
1341

    
1342
@require_http_methods(["POST", "GET"])
1343
@signed_terms_required
1344
@login_required
1345
@project_transaction_context(sync=True)
1346
def project_join(request, chain_id, ctx=None):
1347
    next = request.GET.get('next')
1348
    if not next:
1349
        next = reverse('astakos.im.views.project_detail',
1350
                       args=(chain_id,))
1351

    
1352
    try:
1353
        chain_id = int(chain_id)
1354
        auto_accepted = join_project(chain_id, request.user)
1355
        if auto_accepted:
1356
            m = _(astakos_messages.USER_JOINED_PROJECT)
1357
        else:
1358
            m = _(astakos_messages.USER_JOIN_REQUEST_SUBMITTED)
1359
        messages.success(request, m)
1360
    except (IOError, PermissionDenied), e:
1361
        messages.error(request, e)
1362
    except BaseException, e:
1363
        logger.exception(e)
1364
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1365
        if ctx:
1366
            ctx.mark_rollback()
1367
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1368
    return redirect(next)
1369

    
1370
@require_http_methods(["POST", "GET"])
1371
@signed_terms_required
1372
@login_required
1373
@project_transaction_context(sync=True)
1374
def project_leave(request, chain_id, ctx=None):
1375
    next = request.GET.get('next')
1376
    if not next:
1377
        next = reverse('astakos.im.views.project_list')
1378

    
1379
    try:
1380
        chain_id = int(chain_id)
1381
        auto_accepted = leave_project(chain_id, request.user)
1382
        if auto_accepted:
1383
            m = _(astakos_messages.USER_LEFT_PROJECT)
1384
        else:
1385
            m = _(astakos_messages.USER_LEAVE_REQUEST_SUBMITTED)
1386
        messages.success(request, m)
1387
    except (IOError, PermissionDenied), e:
1388
        messages.error(request, e)
1389
    except PendingMembershipError as e:
1390
        raise RetryException()
1391
    except BaseException, e:
1392
        logger.exception(e)
1393
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1394
        if ctx:
1395
            ctx.mark_rollback()
1396
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1397
    return redirect(next)
1398

    
1399
@require_http_methods(["POST"])
1400
@signed_terms_required
1401
@login_required
1402
@project_transaction_context()
1403
def project_cancel(request, chain_id, ctx=None):
1404
    next = request.GET.get('next')
1405
    if not next:
1406
        next = reverse('astakos.im.views.project_list')
1407

    
1408
    try:
1409
        chain_id = int(chain_id)
1410
        cancel_membership(chain_id, request.user)
1411
        m = _(astakos_messages.USER_REQUEST_CANCELLED)
1412
        messages.success(request, m)
1413
    except (IOError, PermissionDenied), e:
1414
        messages.error(request, e)
1415
    except PendingMembershipError as e:
1416
        raise RetryException()
1417
    except BaseException, e:
1418
        logger.exception(e)
1419
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1420
        if ctx:
1421
            ctx.mark_rollback()
1422

    
1423
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1424
    return redirect(next)
1425

    
1426
@require_http_methods(["POST"])
1427
@signed_terms_required
1428
@login_required
1429
@project_transaction_context(sync=True)
1430
def project_accept_member(request, chain_id, user_id, ctx=None):
1431
    try:
1432
        chain_id = int(chain_id)
1433
        user_id = int(user_id)
1434
        m = accept_membership(chain_id, user_id, request.user)
1435
    except (IOError, PermissionDenied), e:
1436
        messages.error(request, e)
1437
    except PendingMembershipError as e:
1438
        raise RetryException()
1439
    except BaseException, e:
1440
        logger.exception(e)
1441
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1442
        if ctx:
1443
            ctx.mark_rollback()
1444
    else:
1445
        email = escape(m.person.email)
1446
        msg = _(astakos_messages.USER_MEMBERSHIP_ACCEPTED) % email
1447
        messages.success(request, msg)
1448
    return redirect(reverse('project_detail', args=(chain_id,)))
1449

    
1450
@require_http_methods(["POST"])
1451
@signed_terms_required
1452
@login_required
1453
@project_transaction_context(sync=True)
1454
def project_remove_member(request, chain_id, user_id, ctx=None):
1455
    try:
1456
        chain_id = int(chain_id)
1457
        user_id = int(user_id)
1458
        m = remove_membership(chain_id, user_id, request.user)
1459
    except (IOError, PermissionDenied), e:
1460
        messages.error(request, e)
1461
    except PendingMembershipError as e:
1462
        raise RetryException()
1463
    except BaseException, e:
1464
        logger.exception(e)
1465
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1466
        if ctx:
1467
            ctx.mark_rollback()
1468
    else:
1469
        email = escape(m.person.email)
1470
        msg = _(astakos_messages.USER_MEMBERSHIP_REMOVED) % email
1471
        messages.success(request, msg)
1472
    return redirect(reverse('project_detail', args=(chain_id,)))
1473

    
1474
@require_http_methods(["POST"])
1475
@signed_terms_required
1476
@login_required
1477
@project_transaction_context()
1478
def project_reject_member(request, chain_id, user_id, ctx=None):
1479
    try:
1480
        chain_id = int(chain_id)
1481
        user_id = int(user_id)
1482
        m = reject_membership(chain_id, user_id, request.user)
1483
    except (IOError, PermissionDenied), e:
1484
        messages.error(request, e)
1485
    except PendingMembershipError as e:
1486
        raise RetryException()
1487
    except BaseException, e:
1488
        logger.exception(e)
1489
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1490
        if ctx:
1491
            ctx.mark_rollback()
1492
    else:
1493
        email = escape(m.person.email)
1494
        msg = _(astakos_messages.USER_MEMBERSHIP_REJECTED) % email
1495
        messages.success(request, msg)
1496
    return redirect(reverse('project_detail', args=(chain_id,)))
1497

    
1498
@require_http_methods(["POST", "GET"])
1499
@signed_terms_required
1500
@login_required
1501
@project_transaction_context(sync=True)
1502
def project_app_approve(request, application_id, ctx=None):
1503

    
1504
    if not request.user.is_project_admin():
1505
        m = _(astakos_messages.NOT_ALLOWED)
1506
        raise PermissionDenied(m)
1507

    
1508
    try:
1509
        app = ProjectApplication.objects.get(id=application_id)
1510
    except ProjectApplication.DoesNotExist:
1511
        raise Http404
1512

    
1513
    approve_application(application_id)
1514
    chain_id = get_related_project_id(application_id)
1515
    return redirect(reverse('project_detail', args=(chain_id,)))
1516

    
1517
@require_http_methods(["POST", "GET"])
1518
@signed_terms_required
1519
@login_required
1520
@project_transaction_context()
1521
def project_app_deny(request, application_id, ctx=None):
1522

    
1523
    if not request.user.is_project_admin():
1524
        m = _(astakos_messages.NOT_ALLOWED)
1525
        raise PermissionDenied(m)
1526

    
1527
    try:
1528
        app = ProjectApplication.objects.get(id=application_id)
1529
    except ProjectApplication.DoesNotExist:
1530
        raise Http404
1531

    
1532
    deny_application(application_id)
1533
    return redirect(reverse('project_list'))
1534

    
1535
@require_http_methods(["POST", "GET"])
1536
@signed_terms_required
1537
@login_required
1538
@project_transaction_context()
1539
def project_app_dismiss(request, application_id, ctx=None):
1540
    try:
1541
        app = ProjectApplication.objects.get(id=application_id)
1542
    except ProjectApplication.DoesNotExist:
1543
        raise Http404
1544

    
1545
    if not request.user.owns_application(app):
1546
        m = _(astakos_messages.NOT_ALLOWED)
1547
        raise PermissionDenied(m)
1548

    
1549
    # XXX: dismiss application also does authorization
1550
    dismiss_application(application_id, request_user=request.user)
1551

    
1552
    chain_id = None
1553
    chain_id = get_related_project_id(application_id)
1554
    if chain_id:
1555
        next = reverse('project_detail', args=(chain_id,))
1556
    else:
1557
        next = reverse('project_list')
1558
    return redirect(next)
1559

    
1560

    
1561
def landing(request):
1562
    return render_response(
1563
        'im/landing.html',
1564
        context_instance=get_context(request))
1565

    
1566

    
1567
def api_access(request):
1568
    return render_response(
1569
        'im/api_access.html',
1570
        context_instance=get_context(request))