Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (50 kB)

1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
import logging
35
import calendar
36
import inflect
37

    
38
engine = inflect.engine()
39

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

    
44
from django_tables2 import RequestConfig
45

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

    
72
import astakos.im.messages as astakos_messages
73

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

    
113
logger = logging.getLogger(__name__)
114

    
115
callpoint = AstakosCallpoint()
116

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

    
131
def requires_auth_provider(provider_id, **perms):
132
    """
133
    """
134
    def decorator(func, *args, **kwargs):
135
        @wraps(func)
136
        def wrapper(request, *args, **kwargs):
137
            provider = auth_providers.get_provider(provider_id)
138

    
139
            if not provider or not provider.is_active():
140
                raise PermissionDenied
141

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

    
152

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

    
167

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

    
183

    
184
def required_auth_methods_assigned(only_warn=False):
185
    """
186
    Decorator that checks whether the request.user has all required auth providers
187
    assigned.
188
    """
189
    required_providers = auth_providers.REQUIRED_PROVIDERS.keys()
190

    
191
    def decorator(func):
192
        if not required_providers:
193
            return func
194

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

    
211

    
212
def valid_astakos_user_required(func):
213
    return signed_terms_required(required_auth_methods_assigned()(login_required(func)))
214

    
215

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

222
    **Arguments**
223

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

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

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

235
    **Template:**
236

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

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

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

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

    
255

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

    
268

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

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

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

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

285
    **Arguments**
286

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

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

294
    **Template:**
295

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

298
    **Settings:**
299

300
    The view expectes the following settings are defined:
301

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

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

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

    
346

    
347
@require_http_methods(["GET", "POST"])
348
@required_auth_methods_assigned(only_warn=True)
349
@login_required
350
@signed_terms_required
351
def edit_profile(request, template_name='im/profile.html', extra_context=None):
352
    """
353
    Allows a user to edit his/her profile.
354

355
    In case of GET request renders a form for displaying the user information.
356
    In case of POST updates the user informantion and redirects to ``next``
357
    url parameter if exists.
358

359
    If the user isn't logged in, redirects to settings.LOGIN_URL.
360

361
    **Arguments**
362

363
    ``template_name``
364
        A custom template to use. This is optional; if not specified,
365
        this will default to ``im/profile.html``.
366

367
    ``extra_context``
368
        An dictionary of variables to add to the template context.
369

370
    **Template:**
371

372
    im/profile.html or ``template_name`` keyword argument.
373

374
    **Settings:**
375

376
    The view expectes the following settings are defined:
377

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

    
403
                if form.email_changed:
404
                    msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
405
                    messages.success(request, msg)
406
                if form.password_changed:
407
                    msg = _(astakos_messages.PASSWORD_CHANGED)
408
                    messages.success(request, msg)
409

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

    
420
    # existing providers
421
    user_providers = request.user.get_active_auth_providers()
422

    
423
    # providers that user can add
424
    user_available_providers = request.user.get_available_auth_providers()
425

    
426
    extra_context['services'] = get_services_dict()
427
    return render_response(template_name,
428
                           profile_form = form,
429
                           user_providers = user_providers,
430
                           user_available_providers = user_available_providers,
431
                           context_instance = get_context(request,
432
                                                          extra_context))
433

    
434

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

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

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

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

452
    On unsuccessful creation, renders ``template_name`` with an error message.
453

454
    **Arguments**
455

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

460
    ``extra_context``
461
        An dictionary of variables to add to the template context.
462

463
    ``on_success``
464
        Resolvable view name to redirect on registration success.
465

466
    **Template:**
467

468
    im/signup.html or ``template_name`` keyword argument.
469
    """
470
    extra_context = extra_context or {}
471
    if request.user.is_authenticated():
472
        return HttpResponseRedirect(reverse('edit_profile'))
473

    
474
    provider = get_query(request).get('provider', 'local')
475
    if not auth_providers.get_provider(provider).is_available_for_create():
476
        raise PermissionDenied
477

    
478
    id = get_query(request).get('id')
479
    try:
480
        instance = AstakosUser.objects.get(id=id) if id else None
481
    except AstakosUser.DoesNotExist:
482
        instance = None
483

    
484
    third_party_token = request.REQUEST.get('third_party_token', None)
485
    if third_party_token:
486
        pending = get_object_or_404(PendingThirdPartyUser,
487
                                    token=third_party_token)
488
        provider = pending.provider
489
        instance = pending.get_user_instance()
490

    
491
    try:
492
        if not backend:
493
            backend = get_backend(request)
494
        form = backend.get_signup_form(provider, instance)
495
    except Exception, e:
496
        form = SimpleBackend(request).get_signup_form(provider)
497
        messages.error(request, e)
498
    if request.method == 'POST':
499
        if form.is_valid():
500
            user = form.save(commit=False)
501

    
502
            # delete previously unverified accounts
503
            if AstakosUser.objects.user_exists(user.email):
504
                AstakosUser.objects.get_by_identifier(user.email).delete()
505

    
506
            try:
507
                result = backend.handle_activation(user)
508
                status = messages.SUCCESS
509
                message = result.message
510

    
511
                form.store_user(user, request)
512

    
513
                if 'additional_email' in form.cleaned_data:
514
                    additional_email = form.cleaned_data['additional_email']
515
                    if additional_email != user.email:
516
                        user.additionalmail_set.create(email=additional_email)
517
                        msg = 'Additional email: %s saved for user %s.' % (
518
                            additional_email,
519
                            user.email
520
                        )
521
                        logger._log(LOGGING_LEVEL, msg, [])
522

    
523
                if user and user.is_active:
524
                    next = request.POST.get('next', '')
525
                    response = prepare_response(request, user, next=next)
526
                    transaction.commit()
527
                    return response
528

    
529
                transaction.commit()
530
                messages.add_message(request, status, message)
531
                return HttpResponseRedirect(reverse(on_success))
532

    
533
            except SendMailError, e:
534
                logger.exception(e)
535
                status = messages.ERROR
536
                message = e.message
537
                messages.error(request, message)
538
                transaction.rollback()
539
            except BaseException, e:
540
                logger.exception(e)
541
                message = _(astakos_messages.GENERIC_ERROR)
542
                messages.error(request, message)
543
                logger.exception(e)
544
                transaction.rollback()
545

    
546
    return render_response(template_name,
547
                           signup_form=form,
548
                           third_party_token=third_party_token,
549
                           provider=provider,
550
                           context_instance=get_context(request, extra_context))
551

    
552

    
553
@require_http_methods(["GET", "POST"])
554
@required_auth_methods_assigned(only_warn=True)
555
@login_required
556
@signed_terms_required
557
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
558
    """
559
    Allows a user to send feedback.
560

561
    In case of GET request renders a form for providing the feedback information.
562
    In case of POST sends an email to support team.
563

564
    If the user isn't logged in, redirects to settings.LOGIN_URL.
565

566
    **Arguments**
567

568
    ``template_name``
569
        A custom template to use. This is optional; if not specified,
570
        this will default to ``im/feedback.html``.
571

572
    ``extra_context``
573
        An dictionary of variables to add to the template context.
574

575
    **Template:**
576

577
    im/signup.html or ``template_name`` keyword argument.
578

579
    **Settings:**
580

581
    * LOGIN_URL: login uri
582
    """
583
    extra_context = extra_context or {}
584
    if request.method == 'GET':
585
        form = FeedbackForm()
586
    if request.method == 'POST':
587
        if not request.user:
588
            return HttpResponse('Unauthorized', status=401)
589

    
590
        form = FeedbackForm(request.POST)
591
        if form.is_valid():
592
            msg = form.cleaned_data['feedback_msg']
593
            data = form.cleaned_data['feedback_data']
594
            try:
595
                send_feedback(msg, data, request.user, email_template_name)
596
            except SendMailError, e:
597
                messages.error(request, message)
598
            else:
599
                message = _(astakos_messages.FEEDBACK_SENT)
600
                messages.success(request, message)
601
    return render_response(template_name,
602
                           feedback_form=form,
603
                           context_instance=get_context(request, extra_context))
604

    
605

    
606
@require_http_methods(["GET"])
607
@signed_terms_required
608
def logout(request, template='registration/logged_out.html', extra_context=None):
609
    """
610
    Wraps `django.contrib.auth.logout`.
611
    """
612
    extra_context = extra_context or {}
613
    response = HttpResponse()
614
    if request.user.is_authenticated():
615
        email = request.user.email
616
        auth_logout(request)
617
    else:
618
        response['Location'] = reverse('index')
619
        response.status_code = 301
620
        return response
621

    
622
    next = restrict_next(
623
        request.GET.get('next'),
624
        domain=COOKIE_DOMAIN
625
    )
626

    
627
    if next:
628
        response['Location'] = next
629
        response.status_code = 302
630
    elif LOGOUT_NEXT:
631
        response['Location'] = LOGOUT_NEXT
632
        response.status_code = 301
633
    else:
634
        message = _(astakos_messages.LOGOUT_SUCCESS)
635
        last_provider = request.COOKIES.get('astakos_last_login_method', None)
636
        if last_provider:
637
            provider = auth_providers.get_provider(last_provider)
638
            extra_message = provider.get_logout_message_display
639
            if extra_message:
640
                message += '<br />' + extra_message
641
        messages.add_message(request, messages.SUCCESS, mark_safe(message))
642
        response['Location'] = reverse('index')
643
        response.status_code = 301
644
    return response
645

    
646

    
647
@require_http_methods(["GET", "POST"])
648
@transaction.commit_manually
649
def activate(request, greeting_email_template_name='im/welcome_email.txt',
650
             helpdesk_email_template_name='im/helpdesk_notification.txt'):
651
    """
652
    Activates the user identified by the ``auth`` request parameter, sends a welcome email
653
    and renews the user token.
654

655
    The view uses commit_manually decorator in order to ensure the user state will be updated
656
    only if the email will be send successfully.
657
    """
658
    token = request.GET.get('auth')
659
    next = request.GET.get('next')
660
    try:
661
        user = AstakosUser.objects.get(auth_token=token)
662
    except AstakosUser.DoesNotExist:
663
        return HttpResponseBadRequest(_(astakos_messages.ACCOUNT_UNKNOWN))
664

    
665
    if user.is_active:
666
        message = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE)
667
        messages.error(request, message)
668
        return index(request)
669

    
670
    try:
671
        activate_func(user, greeting_email_template_name,
672
                      helpdesk_email_template_name, verify_email=True)
673
        messages.success(request, _(astakos_messages.ACCOUNT_ACTIVATED))
674
        next = ACTIVATION_REDIRECT_URL or next
675
        response = prepare_response(request, user, next, renew=True)
676
        transaction.commit()
677
        return response
678
    except SendMailError, e:
679
        message = e.message
680
        messages.add_message(request, messages.ERROR, message)
681
        transaction.rollback()
682
        return index(request)
683
    except BaseException, e:
684
        status = messages.ERROR
685
        message = _(astakos_messages.GENERIC_ERROR)
686
        messages.add_message(request, messages.ERROR, message)
687
        logger.exception(e)
688
        transaction.rollback()
689
        return index(request)
690

    
691

    
692
@require_http_methods(["GET", "POST"])
693
def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
694
    extra_context = extra_context or {}
695
    term = None
696
    terms = None
697
    if not term_id:
698
        try:
699
            term = ApprovalTerms.objects.order_by('-id')[0]
700
        except IndexError:
701
            pass
702
    else:
703
        try:
704
            term = ApprovalTerms.objects.get(id=term_id)
705
        except ApprovalTerms.DoesNotExist, e:
706
            pass
707

    
708
    if not term:
709
        messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
710
        return HttpResponseRedirect(reverse('index'))
711
    try:
712
        f = open(term.location, 'r')
713
    except IOError:
714
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
715
        return render_response(
716
            template_name, context_instance=get_context(request, extra_context))
717

    
718
    terms = f.read()
719

    
720
    if request.method == 'POST':
721
        next = restrict_next(
722
            request.POST.get('next'),
723
            domain=COOKIE_DOMAIN
724
        )
725
        if not next:
726
            next = reverse('index')
727
        form = SignApprovalTermsForm(request.POST, instance=request.user)
728
        if not form.is_valid():
729
            return render_response(template_name,
730
                                   terms=terms,
731
                                   approval_terms_form=form,
732
                                   context_instance=get_context(request, extra_context))
733
        user = form.save()
734
        return HttpResponseRedirect(next)
735
    else:
736
        form = None
737
        if request.user.is_authenticated() and not request.user.signed_terms:
738
            form = SignApprovalTermsForm(instance=request.user)
739
        return render_response(template_name,
740
                               terms=terms,
741
                               approval_terms_form=form,
742
                               context_instance=get_context(request, extra_context))
743

    
744

    
745
@require_http_methods(["GET", "POST"])
746
@transaction.commit_manually
747
def change_email(request, activation_key=None,
748
                 email_template_name='registration/email_change_email.txt',
749
                 form_template_name='registration/email_change_form.html',
750
                 confirm_template_name='registration/email_change_done.html',
751
                 extra_context=None):
752
    extra_context = extra_context or {}
753

    
754

    
755
    if not astakos_settings.EMAILCHANGE_ENABLED:
756
        raise PermissionDenied
757

    
758
    if activation_key:
759
        try:
760
            user = EmailChange.objects.change_email(activation_key)
761
            if request.user.is_authenticated() and request.user == user or not \
762
                    request.user.is_authenticated():
763
                msg = _(astakos_messages.EMAIL_CHANGED)
764
                messages.success(request, msg)
765
                transaction.commit()
766
                return HttpResponseRedirect(reverse('edit_profile'))
767
        except ValueError, e:
768
            messages.error(request, e)
769
            transaction.rollback()
770
            return HttpResponseRedirect(reverse('index'))
771

    
772
        return render_response(confirm_template_name,
773
                               modified_user=user if 'user' in locals() \
774
                               else None, context_instance=get_context(request,
775
                                                            extra_context))
776

    
777
    if not request.user.is_authenticated():
778
        path = quote(request.get_full_path())
779
        url = request.build_absolute_uri(reverse('index'))
780
        return HttpResponseRedirect(url + '?next=' + path)
781

    
782
    # clean up expired email changes
783
    if request.user.email_change_is_pending():
784
        change = request.user.emailchanges.get()
785
        if change.activation_key_expired():
786
            change.delete()
787
            transaction.commit()
788
            return HttpResponseRedirect(reverse('email_change'))
789

    
790
    form = EmailChangeForm(request.POST or None)
791
    if request.method == 'POST' and form.is_valid():
792
        try:
793
            ec = form.save(email_template_name, request)
794
        except SendMailError, e:
795
            msg = e
796
            messages.error(request, msg)
797
            transaction.rollback()
798
            return HttpResponseRedirect(reverse('edit_profile'))
799
        else:
800
            msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
801
            messages.success(request, msg)
802
            transaction.commit()
803
            return HttpResponseRedirect(reverse('edit_profile'))
804

    
805
    if request.user.email_change_is_pending():
806
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
807

    
808
    return render_response(
809
        form_template_name,
810
        form=form,
811
        context_instance=get_context(request, extra_context)
812
    )
813

    
814

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

    
817
    if request.user.is_authenticated():
818
        messages.error(request, _(astakos_messages.ALREADY_LOGGED_IN))
819
        return HttpResponseRedirect(reverse('edit_profile'))
820

    
821
    # TODO: check if moderation is only enabled for local login
822
    if astakos_settings.MODERATION_ENABLED:
823
        raise PermissionDenied
824

    
825
    extra_context = extra_context or {}
826
    try:
827
        u = AstakosUser.objects.get(id=user_id)
828
    except AstakosUser.DoesNotExist:
829
        messages.error(request, _(astakos_messages.ACCOUNT_UNKNOWN))
830
    else:
831
        try:
832
            send_activation_func(u)
833
            msg = _(astakos_messages.ACTIVATION_SENT)
834
            messages.success(request, msg)
835
        except SendMailError, e:
836
            messages.error(request, e)
837

    
838
    return HttpResponseRedirect(reverse('index'))
839

    
840

    
841
@require_http_methods(["GET"])
842
@valid_astakos_user_required
843
def resource_usage(request):
844

    
845
    def with_class(entry):
846
         entry['load_class'] = 'red'
847
         max_value = float(entry['maxValue'])
848
         curr_value = float(entry['currValue'])
849
         entry['ratio_limited']= 0
850
         if max_value > 0 :
851
             entry['ratio'] = (curr_value / max_value) * 100
852
         else:
853
             entry['ratio'] = 0
854
         if entry['ratio'] < 66:
855
             entry['load_class'] = 'yellow'
856
         if entry['ratio'] < 33:
857
             entry['load_class'] = 'green'
858
         if entry['ratio']<0:
859
             entry['ratio'] = 0
860
         if entry['ratio']>100:
861
             entry['ratio_limited'] = 100
862
         else:
863
             entry['ratio_limited'] = entry['ratio']
864
         return entry
865

    
866
    def pluralize(entry):
867
        entry['plural'] = engine.plural(entry.get('name'))
868
        return entry
869

    
870
    resource_usage = None
871
    result = callpoint.get_user_usage(request.user.id)
872
    if result.is_success:
873
        resource_usage = result.data
874
        backenddata = map(with_class, result.data)
875
        backenddata = map(pluralize , backenddata)
876
    else:
877
        messages.error(request, result.reason)
878
        backenddata = []
879
        resource_usage = []
880

    
881
    if request.REQUEST.get('json', None):
882
        return HttpResponse(json.dumps(backenddata),
883
                            mimetype="application/json")
884

    
885
    return render_response('im/resource_usage.html',
886
                           context_instance=get_context(request),
887
                           resource_usage=backenddata,
888
                           usage_update_interval=astakos_settings.USAGE_UPDATE_INTERVAL,
889
                           result=result)
890

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

    
901
    if provider.can_remove():
902
        provider.delete()
903
        message = astakos_messages.AUTH_PROVIDER_REMOVED % \
904
                            provider.settings.get_method_prompt_display
905
        messages.success(request, message)
906
        return HttpResponseRedirect(reverse('edit_profile'))
907
    else:
908
        raise PermissionDenied
909

    
910

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

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

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

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

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

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

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

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

    
1030
@require_http_methods(["GET", "POST"])
1031
@signed_terms_required
1032
@login_required
1033
def project_add(request):
1034
    resource_groups = RESOURCES_PRESENTATION_DATA.get('groups', {})
1035
    resource_catalog = ()
1036
    result = callpoint.list_resources()
1037
    details_fields = [
1038
        "name", "homepage", "description","start_date","end_date", "comments"]
1039
    membership_fields =[
1040
        "member_join_policy", "member_leave_policy", "limit_on_members_number"]
1041
    if not result.is_success:
1042
        messages.error(
1043
            request,
1044
            'Unable to retrieve system resources: %s' % result.reason
1045
    )
1046
    else:
1047
        resource_catalog = [
1048
            [g, filter(lambda r: r.get('group', '') == g, result.data)] \
1049
                for g in resource_groups]
1050

    
1051
    # order resources
1052
    groups_order = RESOURCES_PRESENTATION_DATA.get('groups_order')
1053
    resources_order = RESOURCES_PRESENTATION_DATA.get('resources_order')
1054
    resource_catalog = sorted(resource_catalog, lambda g,rs:groups_order.index(g[0]))
1055
    for index, group in enumerate(resource_catalog):
1056
        resource_catalog[index][1] = sorted(resource_catalog[index][1],
1057
                                            key=lambda r: resources_order.index(r['str_repr']))
1058

    
1059

    
1060
    extra_context = {
1061
        'resource_catalog':resource_catalog,
1062
        'resource_groups':resource_groups,
1063
        'show_form':True,
1064
        'details_fields':details_fields,
1065
        'membership_fields':membership_fields}
1066
    return _create_object(
1067
        request,
1068
        template_name='im/projects/projectapplication_form.html',
1069
        extra_context=extra_context,
1070
        post_save_redirect=reverse('project_list'),
1071
        form_class=ProjectApplicationForm,
1072
        msg=_("The %(verbose_name)s has been received and \
1073
                 is under consideration."))
1074

    
1075

    
1076
@require_http_methods(["GET"])
1077
@signed_terms_required
1078
@login_required
1079
def project_list(request):
1080
    projects = ProjectApplication.objects.user_accessible_projects(request.user).select_related()
1081
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1082
                                                prefix="my_projects_")
1083
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1084

    
1085
    return object_list(
1086
        request,
1087
        projects,
1088
        template_name='im/projects/project_list.html',
1089
        extra_context={
1090
            'is_search':False,
1091
            'table': table,
1092
        })
1093

    
1094

    
1095
@require_http_methods(["GET", "POST"])
1096
@signed_terms_required
1097
@login_required
1098
def project_modify(request, application_id):
1099
    resource_groups = RESOURCES_PRESENTATION_DATA.get('groups', {})
1100
    resource_catalog = ()
1101
    result = callpoint.list_resources()
1102
    details_fields = [
1103
        "name", "homepage", "description","start_date","end_date", "comments"]
1104
    membership_fields =[
1105
        "member_join_policy", "member_leave_policy", "limit_on_members_number"]
1106
    if not result.is_success:
1107
        messages.error(
1108
            request,
1109
            'Unable to retrieve system resources: %s' % result.reason
1110
    )
1111
    else:
1112
        resource_catalog = [
1113
            (g, filter(lambda r: r.get('group', '') == g, result.data)) \
1114
                for g in resource_groups]
1115
    extra_context = {
1116
        'resource_catalog':resource_catalog,
1117
        'resource_groups':resource_groups,
1118
        'show_form':True,
1119
        'details_fields':details_fields,
1120
        'update_form': True,
1121
        'membership_fields':membership_fields}
1122
    return _update_object(
1123
        request,
1124
        object_id=application_id,
1125
        template_name='im/projects/projectapplication_form.html',
1126
        extra_context=extra_context, post_save_redirect=reverse('project_list'),
1127
        form_class=ProjectApplicationForm,
1128
        msg = _("The %(verbose_name)s has been received and \
1129
                    is under consideration."))
1130

    
1131

    
1132
@require_http_methods(["GET", "POST"])
1133
@signed_terms_required
1134
@login_required
1135
def project_app(request, application_id):
1136
    return common_detail(request, application_id, is_chain=False)
1137

    
1138
@require_http_methods(["GET", "POST"])
1139
@signed_terms_required
1140
@login_required
1141
def project_detail(request, chain_id):
1142
    return common_detail(request, chain_id)
1143

    
1144
@project_transaction_context(sync=True)
1145
def addmembers(request, chain_id, ctx=None):
1146
    addmembers_form = AddProjectMembersForm(
1147
        request.POST,
1148
        chain_id=int(chain_id),
1149
        request_user=request.user)
1150
    if addmembers_form.is_valid():
1151
        try:
1152
            chain_id = int(chain_id)
1153
            map(lambda u: enroll_member(
1154
                    chain_id,
1155
                    u,
1156
                    request_user=request.user),
1157
                addmembers_form.valid_users)
1158
        except (IOError, PermissionDenied), e:
1159
            messages.error(request, e)
1160
        except BaseException, e:
1161
            if ctx:
1162
                ctx.mark_rollback()
1163
            messages.error(request, e)
1164

    
1165
def common_detail(request, chain_or_app_id, is_chain=True):
1166
    if is_chain:
1167
        chain_id = chain_or_app_id
1168
        if request.method == 'POST':
1169
            addmembers(request, chain_id)
1170

    
1171
        addmembers_form = AddProjectMembersForm()
1172

    
1173
        project, application = get_by_chain_or_404(chain_id)
1174
        if project:
1175
            members = project.projectmembership_set.select_related()
1176
            members_table = tables.ProjectMembersTable(project,
1177
                                                       members,
1178
                                                       user=request.user,
1179
                                                       prefix="members_")
1180
            RequestConfig(request, paginate={"per_page": PAGINATE_BY}
1181
                          ).configure(members_table)
1182

    
1183
        else:
1184
            members_table = None
1185

    
1186
    else: # is application
1187
        application_id = chain_or_app_id
1188
        application = get_object_or_404(ProjectApplication, pk=application_id)
1189
        members_table = None
1190
        addmembers_form = None
1191

    
1192
    modifications_table = None
1193

    
1194
    following_applications = list(application.pending_modifications())
1195
    following_applications.reverse()
1196
    modifications_table = (
1197
        tables.ProjectModificationApplicationsTable(following_applications,
1198
                                                    user=request.user,
1199
                                                    prefix="modifications_"))
1200

    
1201
    return object_detail(
1202
        request,
1203
        queryset=ProjectApplication.objects.select_related(),
1204
        object_id=application.id,
1205
        template_name='im/projects/project_detail.html',
1206
        extra_context={
1207
            'project_view': is_chain,
1208
            'addmembers_form':addmembers_form,
1209
            'members_table': members_table,
1210
            'user_owns_project': request.user.owns_application(application),
1211
            'modifications_table': modifications_table,
1212
            'member_status': application.user_status(request.user)
1213
            })
1214

    
1215
@require_http_methods(["GET", "POST"])
1216
@signed_terms_required
1217
@login_required
1218
def project_search(request):
1219
    q = request.GET.get('q', '')
1220
    form = ProjectSearchForm()
1221
    q = q.strip()
1222

    
1223
    if request.method == "POST":
1224
        form = ProjectSearchForm(request.POST)
1225
        if form.is_valid():
1226
            q = form.cleaned_data['q'].strip()
1227
        else:
1228
            q = None
1229

    
1230
    if q is None:
1231
        projects = ProjectApplication.objects.none()
1232
    else:
1233
        accepted_projects = request.user.projectmembership_set.filter(
1234
            ~Q(acceptance_date__isnull=True)).values_list('project', flat=True)
1235
        projects = ProjectApplication.objects.search_by_name(q)
1236
        projects = projects.filter(~Q(project__last_approval_date__isnull=True))
1237
        projects = projects.exclude(project__in=accepted_projects)
1238

    
1239
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1240
                                                prefix="my_projects_")
1241
    if request.method == "POST":
1242
        table.caption = _('SEARCH RESULTS')
1243
    else:
1244
        table.caption = _('ALL PROJECTS')
1245

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

    
1248
    return object_list(
1249
        request,
1250
        projects,
1251
        template_name='im/projects/project_list.html',
1252
        extra_context={
1253
          'form': form,
1254
          'is_search': True,
1255
          'q': q,
1256
          'table': table
1257
        })
1258

    
1259
@require_http_methods(["POST", "GET"])
1260
@signed_terms_required
1261
@login_required
1262
@project_transaction_context(sync=True)
1263
def project_join(request, chain_id, ctx=None):
1264
    next = request.GET.get('next')
1265
    if not next:
1266
        next = reverse('astakos.im.views.project_detail',
1267
                       args=(chain_id,))
1268

    
1269
    try:
1270
        chain_id = int(chain_id)
1271
        join_project(chain_id, request.user)
1272
        # TODO: distinct messages for request/auto accept ???
1273
        messages.success(request, _(astakos_messages.USER_JOIN_REQUEST_SUBMITED))
1274
    except (IOError, PermissionDenied), e:
1275
        messages.error(request, e)
1276
    except BaseException, e:
1277
        logger.exception(e)
1278
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1279
        if ctx:
1280
            ctx.mark_rollback()
1281
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1282
    return redirect(next)
1283

    
1284
@require_http_methods(["POST"])
1285
@signed_terms_required
1286
@login_required
1287
@project_transaction_context(sync=True)
1288
def project_leave(request, chain_id, ctx=None):
1289
    next = request.GET.get('next')
1290
    if not next:
1291
        next = reverse('astakos.im.views.project_list')
1292

    
1293
    try:
1294
        chain_id = int(chain_id)
1295
        leave_project(chain_id, request.user)
1296
    except (IOError, PermissionDenied), e:
1297
        messages.error(request, e)
1298
    except BaseException, e:
1299
        logger.exception(e)
1300
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1301
        if ctx:
1302
            ctx.mark_rollback()
1303
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1304
    return redirect(next)
1305

    
1306
@require_http_methods(["POST"])
1307
@signed_terms_required
1308
@login_required
1309
@project_transaction_context()
1310
def project_cancel(request, chain_id, ctx=None):
1311
    next = request.GET.get('next')
1312
    if not next:
1313
        next = reverse('astakos.im.views.project_list')
1314

    
1315
    try:
1316
        chain_id = int(chain_id)
1317
        cancel_membership(chain_id, request.user)
1318
    except (IOError, PermissionDenied), e:
1319
        messages.error(request, e)
1320
    except BaseException, e:
1321
        logger.exception(e)
1322
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1323
        if ctx:
1324
            ctx.mark_rollback()
1325

    
1326
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1327
    return redirect(next)
1328

    
1329
@require_http_methods(["POST"])
1330
@signed_terms_required
1331
@login_required
1332
@project_transaction_context(sync=True)
1333
def project_accept_member(request, chain_id, user_id, ctx=None):
1334
    try:
1335
        chain_id = int(chain_id)
1336
        user_id = int(user_id)
1337
        m = accept_membership(chain_id, user_id, request.user)
1338
    except (IOError, PermissionDenied), e:
1339
        messages.error(request, e)
1340
    except BaseException, e:
1341
        logger.exception(e)
1342
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1343
        if ctx:
1344
            ctx.mark_rollback()
1345
    else:
1346
        realname = m.person.realname
1347
        msg = _(astakos_messages.USER_JOINED_PROJECT) % locals()
1348
        messages.success(request, msg)
1349
    return redirect(reverse('project_detail', args=(chain_id,)))
1350

    
1351
@require_http_methods(["POST"])
1352
@signed_terms_required
1353
@login_required
1354
@project_transaction_context(sync=True)
1355
def project_remove_member(request, chain_id, user_id, ctx=None):
1356
    try:
1357
        chain_id = int(chain_id)
1358
        user_id = int(user_id)
1359
        m = remove_membership(chain_id, user_id, request.user)
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
    else:
1368
        realname = m.person.realname
1369
        msg = _(astakos_messages.USER_LEFT_PROJECT) % locals()
1370
        messages.success(request, msg)
1371
    return redirect(reverse('project_detail', args=(chain_id,)))
1372

    
1373
@require_http_methods(["POST"])
1374
@signed_terms_required
1375
@login_required
1376
@project_transaction_context()
1377
def project_reject_member(request, chain_id, user_id, ctx=None):
1378
    try:
1379
        chain_id = int(chain_id)
1380
        user_id = int(user_id)
1381
        m = reject_membership(chain_id, user_id, request.user)
1382
    except (IOError, PermissionDenied), e:
1383
        messages.error(request, e)
1384
    except BaseException, e:
1385
        logger.exception(e)
1386
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1387
        if ctx:
1388
            ctx.mark_rollback()
1389
    else:
1390
        realname = m.person.realname
1391
        msg = _(astakos_messages.USER_LEFT_PROJECT) % locals()
1392
        messages.success(request, msg)
1393
    return redirect(reverse('project_detail', args=(chain_id,)))
1394

    
1395
def landing(request):
1396
    return render_response(
1397
        'im/landing.html',
1398
        context_instance=get_context(request))
1399

    
1400

    
1401
def api_access(request):
1402
    return render_response(
1403
        'im/api_access.html',
1404
        context_instance=get_context(request))