Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 7f31a7a3

History | View | Annotate | Download (52.3 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.safestring import mark_safe
61
from django.utils.translation import ugettext as _
62
from django.views.generic.create_update import (
63
    apply_extra_context, lookup_object, delete_object, get_model_and_form_class)
64
from django.views.generic.list_detail import object_list, object_detail
65
from django.core.xheaders import populate_xheaders
66
from django.core.exceptions import ValidationError, PermissionDenied
67
from django.template.loader import render_to_string
68
from django.views.decorators.http import require_http_methods
69
from django.db.models import Q
70
from django.core.exceptions import PermissionDenied
71
from django.utils import simplejson as json
72

    
73
import astakos.im.messages as astakos_messages
74

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

    
115
logger = logging.getLogger(__name__)
116

    
117
callpoint = AstakosCallpoint()
118

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

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

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

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

    
154

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

    
169

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

    
185

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

    
193
    def decorator(func):
194
        if not required_providers:
195
            return func
196

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

    
213

    
214
def valid_astakos_user_required(func):
215
    return signed_terms_required(required_auth_methods_assigned()(login_required(func)))
216

    
217

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

224
    **Arguments**
225

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

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

234
    ``extra_context``
235
        An dictionary of variables to add to the template context.
236

237
    **Template:**
238

239
    im/profile.html or im/login.html or ``template_name`` keyword argument.
240

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

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

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

    
257

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

    
270

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

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

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

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

287
    **Arguments**
288

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

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

296
    **Template:**
297

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

300
    **Settings:**
301

302
    The view expectes the following settings are defined:
303

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

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

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

    
348

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

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

361
    If the user isn't logged in, redirects to settings.LOGIN_URL.
362

363
    **Arguments**
364

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

369
    ``extra_context``
370
        An dictionary of variables to add to the template context.
371

372
    **Template:**
373

374
    im/profile.html or ``template_name`` keyword argument.
375

376
    **Settings:**
377

378
    The view expectes the following settings are defined:
379

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

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

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

    
422
    # existing providers
423
    user_providers = request.user.get_active_auth_providers()
424

    
425
    # providers that user can add
426
    user_available_providers = request.user.get_available_auth_providers()
427

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

    
436

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

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

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

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

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

456
    **Arguments**
457

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

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

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

468
    **Template:**
469

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

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

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

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

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

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

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

    
513
                form.store_user(user, request)
514

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

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

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

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

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

    
554

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

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

566
    If the user isn't logged in, redirects to settings.LOGIN_URL.
567

568
    **Arguments**
569

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

574
    ``extra_context``
575
        An dictionary of variables to add to the template context.
576

577
    **Template:**
578

579
    im/signup.html or ``template_name`` keyword argument.
580

581
    **Settings:**
582

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

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

    
607

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

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

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

    
648

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

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

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

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

    
693

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

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

    
720
    terms = f.read()
721

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

    
746

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

    
756

    
757
    if not astakos_settings.EMAILCHANGE_ENABLED:
758
        raise PermissionDenied
759

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

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

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

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

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

    
807
    if request.user.email_change_is_pending():
808
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
809

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

    
816

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

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

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

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

    
840
    return HttpResponseRedirect(reverse('index'))
841

    
842

    
843
@require_http_methods(["GET"])
844
@valid_astakos_user_required
845
def resource_usage(request):
846

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

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

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

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

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

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

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

    
912

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

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

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

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

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

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

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

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

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

    
1053
    # order resources
1054
    groups_order = RESOURCES_PRESENTATION_DATA.get('groups_order')
1055
    resources_order = RESOURCES_PRESENTATION_DATA.get('resources_order')
1056
    resource_catalog = sorted(resource_catalog, key=lambda g:groups_order.index(g[0]))
1057

    
1058
    resource_groups_list = sorted([(k,v) for k,v in resource_groups.items()],
1059
                                  key=lambda f:groups_order.index(f[0]))
1060
    resource_groups = OrderedDict(resource_groups_list)
1061
    for index, group in enumerate(resource_catalog):
1062
        resource_catalog[index][1] = sorted(resource_catalog[index][1],
1063
                                            key=lambda r: resources_order.index(r['str_repr']))
1064

    
1065

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

    
1081

    
1082
@require_http_methods(["GET"])
1083
@signed_terms_required
1084
@login_required
1085
def project_list(request):
1086
    projects = ProjectApplication.objects.user_accessible_projects(request.user).select_related()
1087
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1088
                                                prefix="my_projects_")
1089
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1090

    
1091
    return object_list(
1092
        request,
1093
        projects,
1094
        template_name='im/projects/project_list.html',
1095
        extra_context={
1096
            'is_search':False,
1097
            'table': table,
1098
        })
1099

    
1100

    
1101
@require_http_methods(["GET", "POST"])
1102
@signed_terms_required
1103
@login_required
1104
@project_transaction_context()
1105
def project_app_cancel(request, application_id, ctx=None):
1106
    chain_id = None
1107
    try:
1108
        application_id = int(application_id)
1109
        chain_id = get_related_project_id(application_id)
1110
        cancel_application(application_id, request.user)
1111
    except (IOError, PermissionDenied), e:
1112
        messages.error(request, e)
1113
    except BaseException, e:
1114
        logger.exception(e)
1115
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1116
        if ctx:
1117
            ctx.mark_rollback()
1118
    else:
1119
        msg = _(astakos_messages.APPLICATION_CANCELLED)
1120
        messages.success(request, msg)
1121

    
1122
    next = request.GET.get('next')
1123
    if not next:
1124
        if chain_id:
1125
            next = reverse('astakos.im.views.project_detail', args=(chain_id,))
1126
        else:
1127
            next = reverse('astakos.im.views.project_list')
1128

    
1129
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1130
    return redirect(next)
1131

    
1132

    
1133
@require_http_methods(["GET", "POST"])
1134
@signed_terms_required
1135
@login_required
1136
def project_modify(request, application_id):
1137

    
1138
    try:
1139
        app = ProjectApplication.objects.get(id=application_id)
1140
    except ProjectApplication.DoesNotExist:
1141
        raise Http404
1142

    
1143
    if not request.user.owns_application(app):
1144
        m = _(astakos_messages.NOT_ALLOWED)
1145
        raise PermissionDenied(m)
1146

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

    
1179

    
1180
@require_http_methods(["GET", "POST"])
1181
@signed_terms_required
1182
@login_required
1183
def project_app(request, application_id):
1184
    return common_detail(request, application_id, project_view=False)
1185

    
1186
@require_http_methods(["GET", "POST"])
1187
@signed_terms_required
1188
@login_required
1189
def project_detail(request, chain_id):
1190
    return common_detail(request, chain_id)
1191

    
1192
@project_transaction_context(sync=True)
1193
def addmembers(request, chain_id, ctx=None):
1194
    addmembers_form = AddProjectMembersForm(
1195
        request.POST,
1196
        chain_id=int(chain_id),
1197
        request_user=request.user)
1198
    if addmembers_form.is_valid():
1199
        try:
1200
            chain_id = int(chain_id)
1201
            map(lambda u: enroll_member(
1202
                    chain_id,
1203
                    u,
1204
                    request_user=request.user),
1205
                addmembers_form.valid_users)
1206
        except (IOError, PermissionDenied), e:
1207
            messages.error(request, e)
1208
        except BaseException, e:
1209
            if ctx:
1210
                ctx.mark_rollback()
1211
            messages.error(request, e)
1212

    
1213
def common_detail(request, chain_or_app_id, project_view=True):
1214
    project = None
1215
    if project_view:
1216
        chain_id = chain_or_app_id
1217
        if request.method == 'POST':
1218
            addmembers(request, chain_id)
1219

    
1220
        addmembers_form = AddProjectMembersForm()
1221

    
1222
        project, application = get_by_chain_or_404(chain_id)
1223
        if project:
1224
            members = project.projectmembership_set.select_related()
1225
            members_table = tables.ProjectMembersTable(project,
1226
                                                       members,
1227
                                                       user=request.user,
1228
                                                       prefix="members_")
1229
            RequestConfig(request, paginate={"per_page": PAGINATE_BY}
1230
                          ).configure(members_table)
1231

    
1232
        else:
1233
            members_table = None
1234

    
1235
    else: # is application
1236
        application_id = chain_or_app_id
1237
        application = get_object_or_404(ProjectApplication, pk=application_id)
1238
        members_table = None
1239
        addmembers_form = None
1240

    
1241
    modifications_table = None
1242

    
1243
    user = request.user
1244
    is_owner = user.owns_application(application)
1245
    if not is_owner and not project_view:
1246
        m = _(astakos_messages.NOT_ALLOWED)
1247
        raise PermissionDenied(m)
1248

    
1249
    if (not is_owner and project_view and
1250
        not user.non_owner_can_view(project)):
1251
        m = _(astakos_messages.NOT_ALLOWED)
1252
        raise PermissionDenied(m)
1253

    
1254
    following_applications = list(application.pending_modifications())
1255
    following_applications.reverse()
1256
    modifications_table = (
1257
        tables.ProjectModificationApplicationsTable(following_applications,
1258
                                                    user=request.user,
1259
                                                    prefix="modifications_"))
1260

    
1261
    mem_display = user.membership_display(project) if project else None
1262
    can_join_req = can_join_request(project, user) if project else False
1263
    can_leave_req = can_leave_request(project, user) if project else False
1264

    
1265
    return object_detail(
1266
        request,
1267
        queryset=ProjectApplication.objects.select_related(),
1268
        object_id=application.id,
1269
        template_name='im/projects/project_detail.html',
1270
        extra_context={
1271
            'project_view': project_view,
1272
            'addmembers_form':addmembers_form,
1273
            'members_table': members_table,
1274
            'owner_mode': is_owner,
1275
            'modifications_table': modifications_table,
1276
            'mem_display': mem_display,
1277
            'can_join_request': can_join_req,
1278
            'can_leave_request': can_leave_req,
1279
            })
1280

    
1281
@require_http_methods(["GET", "POST"])
1282
@signed_terms_required
1283
@login_required
1284
def project_search(request):
1285
    q = request.GET.get('q', '')
1286
    form = ProjectSearchForm()
1287
    q = q.strip()
1288

    
1289
    if request.method == "POST":
1290
        form = ProjectSearchForm(request.POST)
1291
        if form.is_valid():
1292
            q = form.cleaned_data['q'].strip()
1293
        else:
1294
            q = None
1295

    
1296
    if q is None:
1297
        projects = ProjectApplication.objects.none()
1298
    else:
1299
        accepted_projects = request.user.projectmembership_set.filter(
1300
            ~Q(acceptance_date__isnull=True)).values_list('project', flat=True)
1301
        projects = ProjectApplication.objects.search_by_name(q)
1302
        projects = projects.filter(~Q(project__last_approval_date__isnull=True))
1303
        projects = projects.exclude(project__in=accepted_projects)
1304

    
1305
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1306
                                                prefix="my_projects_")
1307
    if request.method == "POST":
1308
        table.caption = _('SEARCH RESULTS')
1309
    else:
1310
        table.caption = _('ALL PROJECTS')
1311

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

    
1314
    return object_list(
1315
        request,
1316
        projects,
1317
        template_name='im/projects/project_list.html',
1318
        extra_context={
1319
          'form': form,
1320
          'is_search': True,
1321
          'q': q,
1322
          'table': table
1323
        })
1324

    
1325
@require_http_methods(["POST", "GET"])
1326
@signed_terms_required
1327
@login_required
1328
@project_transaction_context(sync=True)
1329
def project_join(request, chain_id, ctx=None):
1330
    next = request.GET.get('next')
1331
    if not next:
1332
        next = reverse('astakos.im.views.project_detail',
1333
                       args=(chain_id,))
1334

    
1335
    try:
1336
        chain_id = int(chain_id)
1337
        join_project(chain_id, request.user)
1338
        # TODO: distinct messages for request/auto accept ???
1339
        messages.success(request, _(astakos_messages.USER_JOIN_REQUEST_SUBMITED))
1340
    except (IOError, PermissionDenied), e:
1341
        messages.error(request, e)
1342
    except BaseException, e:
1343
        logger.exception(e)
1344
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1345
        if ctx:
1346
            ctx.mark_rollback()
1347
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1348
    return redirect(next)
1349

    
1350
@require_http_methods(["POST"])
1351
@signed_terms_required
1352
@login_required
1353
@project_transaction_context(sync=True)
1354
def project_leave(request, chain_id, ctx=None):
1355
    next = request.GET.get('next')
1356
    if not next:
1357
        next = reverse('astakos.im.views.project_list')
1358

    
1359
    try:
1360
        chain_id = int(chain_id)
1361
        leave_project(chain_id, request.user)
1362
    except (IOError, PermissionDenied), e:
1363
        messages.error(request, e)
1364
    except BaseException, e:
1365
        logger.exception(e)
1366
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1367
        if ctx:
1368
            ctx.mark_rollback()
1369
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1370
    return redirect(next)
1371

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

    
1381
    try:
1382
        chain_id = int(chain_id)
1383
        cancel_membership(chain_id, request.user)
1384
    except (IOError, PermissionDenied), e:
1385
        messages.error(request, e)
1386
    except BaseException, e:
1387
        logger.exception(e)
1388
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1389
        if ctx:
1390
            ctx.mark_rollback()
1391

    
1392
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1393
    return redirect(next)
1394

    
1395
@require_http_methods(["POST"])
1396
@signed_terms_required
1397
@login_required
1398
@project_transaction_context(sync=True)
1399
def project_accept_member(request, chain_id, user_id, ctx=None):
1400
    try:
1401
        chain_id = int(chain_id)
1402
        user_id = int(user_id)
1403
        m = accept_membership(chain_id, user_id, request.user)
1404
    except (IOError, PermissionDenied), e:
1405
        messages.error(request, e)
1406
    except BaseException, e:
1407
        logger.exception(e)
1408
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1409
        if ctx:
1410
            ctx.mark_rollback()
1411
    else:
1412
        realname = m.person.realname
1413
        msg = _(astakos_messages.USER_JOINED_PROJECT) % locals()
1414
        messages.success(request, msg)
1415
    return redirect(reverse('project_detail', args=(chain_id,)))
1416

    
1417
@require_http_methods(["POST"])
1418
@signed_terms_required
1419
@login_required
1420
@project_transaction_context(sync=True)
1421
def project_remove_member(request, chain_id, user_id, ctx=None):
1422
    try:
1423
        chain_id = int(chain_id)
1424
        user_id = int(user_id)
1425
        m = remove_membership(chain_id, user_id, request.user)
1426
    except (IOError, PermissionDenied), e:
1427
        messages.error(request, e)
1428
    except BaseException, e:
1429
        logger.exception(e)
1430
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1431
        if ctx:
1432
            ctx.mark_rollback()
1433
    else:
1434
        realname = m.person.realname
1435
        msg = _(astakos_messages.USER_LEFT_PROJECT) % locals()
1436
        messages.success(request, msg)
1437
    return redirect(reverse('project_detail', args=(chain_id,)))
1438

    
1439
@require_http_methods(["POST"])
1440
@signed_terms_required
1441
@login_required
1442
@project_transaction_context()
1443
def project_reject_member(request, chain_id, user_id, ctx=None):
1444
    try:
1445
        chain_id = int(chain_id)
1446
        user_id = int(user_id)
1447
        m = reject_membership(chain_id, user_id, request.user)
1448
    except (IOError, PermissionDenied), e:
1449
        messages.error(request, e)
1450
    except BaseException, e:
1451
        logger.exception(e)
1452
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1453
        if ctx:
1454
            ctx.mark_rollback()
1455
    else:
1456
        realname = m.person.realname
1457
        msg = _(astakos_messages.USER_LEFT_PROJECT) % locals()
1458
        messages.success(request, msg)
1459
    return redirect(reverse('project_detail', args=(chain_id,)))
1460

    
1461
def landing(request):
1462
    return render_response(
1463
        'im/landing.html',
1464
        context_instance=get_context(request))
1465

    
1466

    
1467
def api_access(request):
1468
    return render_response(
1469
        'im/api_access.html',
1470
        context_instance=get_context(request))