Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (55.4 kB)

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

    
34
import logging
35
import calendar
36
import inflect
37

    
38
engine = inflect.engine()
39

    
40
from urllib import quote
41
from functools import wraps
42
from datetime import datetime
43
from synnefo.lib.ordereddict import OrderedDict
44

    
45
from django_tables2 import RequestConfig
46

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

    
74
import astakos.im.messages as astakos_messages
75

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

    
119
logger = logging.getLogger(__name__)
120

    
121
callpoint = AstakosCallpoint()
122

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

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

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

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

    
158

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

    
173

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

    
189

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

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

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

    
217

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

    
221

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

228
    **Arguments**
229

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

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

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

241
    **Template:**
242

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

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

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

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

    
261

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

    
274

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

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

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

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

291
    **Arguments**
292

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

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

300
    **Template:**
301

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

304
    **Settings:**
305

306
    The view expectes the following settings are defined:
307

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

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

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

    
352

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

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

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

367
    **Arguments**
368

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

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

376
    **Template:**
377

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

380
    **Settings:**
381

382
    The view expectes the following settings are defined:
383

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

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

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

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

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

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

    
440

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

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

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

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

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

460
    **Arguments**
461

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

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

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

472
    **Template:**
473

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

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

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

    
490
    third_party_token = request.REQUEST.get('third_party_token', None)
491
    if third_party_token:
492
        pending = get_object_or_404(PendingThirdPartyUser,
493
                                    token=third_party_token)
494
        provider = pending.provider
495
        instance = pending.get_user_instance()
496

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

    
508
            # delete previously unverified accounts
509
            if AstakosUser.objects.user_exists(user.email):
510
                AstakosUser.objects.get_by_identifier(user.email).delete()
511

    
512
            try:
513
                result = backend.handle_activation(user)
514
                status = messages.SUCCESS
515
                message = result.message
516

    
517
                form.store_user(user, request)
518

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

    
529
                if user and user.is_active:
530
                    next = request.POST.get('next', '')
531
                    response = prepare_response(request, user, next=next)
532
                    transaction.commit()
533
                    return response
534

    
535
                transaction.commit()
536
                messages.add_message(request, status, message)
537
                return HttpResponseRedirect(reverse(on_success))
538

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

    
552
    return render_response(template_name,
553
                           signup_form=form,
554
                           third_party_token=third_party_token,
555
                           provider=provider,
556
                           context_instance=get_context(request, extra_context))
557

    
558

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

567
    In case of GET request renders a form for providing the feedback information.
568
    In case of POST sends an email to support team.
569

570
    If the user isn't logged in, redirects to settings.LOGIN_URL.
571

572
    **Arguments**
573

574
    ``template_name``
575
        A custom template to use. This is optional; if not specified,
576
        this will default to ``im/feedback.html``.
577

578
    ``extra_context``
579
        An dictionary of variables to add to the template context.
580

581
    **Template:**
582

583
    im/signup.html or ``template_name`` keyword argument.
584

585
    **Settings:**
586

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

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

    
611

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

    
628
    next = restrict_next(
629
        request.GET.get('next'),
630
        domain=COOKIE_DOMAIN
631
    )
632

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

    
652

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

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

    
671
    if user.is_active:
672
        message = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE)
673
        messages.error(request, message)
674
        return index(request)
675

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

    
697

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

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

    
724
    terms = f.read()
725

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

    
750

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

    
760

    
761
    if not astakos_settings.EMAILCHANGE_ENABLED:
762
        raise PermissionDenied
763

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

    
778
        return render_response(confirm_template_name,
779
                               modified_user=user if 'user' in locals() \
780
                               else None, context_instance=get_context(request,
781
                                                            extra_context))
782

    
783
    if not request.user.is_authenticated():
784
        path = quote(request.get_full_path())
785
        url = request.build_absolute_uri(reverse('index'))
786
        return HttpResponseRedirect(url + '?next=' + path)
787

    
788
    # clean up expired email changes
789
    if request.user.email_change_is_pending():
790
        change = request.user.emailchanges.get()
791
        if change.activation_key_expired():
792
            change.delete()
793
            transaction.commit()
794
            return HttpResponseRedirect(reverse('email_change'))
795

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

    
811
    if request.user.email_change_is_pending():
812
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
813

    
814
    return render_response(
815
        form_template_name,
816
        form=form,
817
        context_instance=get_context(request, extra_context)
818
    )
819

    
820

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

    
823
    if request.user.is_authenticated():
824
        messages.error(request, _(astakos_messages.ALREADY_LOGGED_IN))
825
        return HttpResponseRedirect(reverse('edit_profile'))
826

    
827
    # TODO: check if moderation is only enabled for local login
828
    if astakos_settings.MODERATION_ENABLED:
829
        raise PermissionDenied
830

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

    
844
    return HttpResponseRedirect(reverse('index'))
845

    
846

    
847
@require_http_methods(["GET"])
848
@valid_astakos_user_required
849
def resource_usage(request):
850

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

    
872
    def pluralize(entry):
873
        entry['plural'] = engine.plural(entry.get('name'))
874
        return entry
875

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

    
887
    if request.REQUEST.get('json', None):
888
        return HttpResponse(json.dumps(backenddata),
889
                            mimetype="application/json")
890

    
891
    return render_response('im/resource_usage.html',
892
                           context_instance=get_context(request),
893
                           resource_usage=backenddata,
894
                           usage_update_interval=astakos_settings.USAGE_UPDATE_INTERVAL,
895
                           result=result)
896

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

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

    
916

    
917
def how_it_works(request):
918
    return render_response(
919
        'im/how_it_works.html',
920
        context_instance=get_context(request))
921

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

    
933
    if extra_context is None: extra_context = {}
934
    if login_required and not request.user.is_authenticated():
935
        return redirect_to_login(request.path)
936
    try:
937

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

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

    
990
    if extra_context is None: extra_context = {}
991
    if login_required and not request.user.is_authenticated():
992
        return redirect_to_login(request.path)
993

    
994
    try:
995
        model, form_class = get_model_and_form_class(model, form_class)
996
        obj = lookup_object(model, object_id, slug, slug_field)
997

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

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

    
1057
    # order resources
1058
    groups_order = RESOURCES_PRESENTATION_DATA.get('groups_order')
1059
    resources_order = RESOURCES_PRESENTATION_DATA.get('resources_order')
1060
    resource_catalog = sorted(resource_catalog, key=lambda g:groups_order.index(g[0]))
1061

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

    
1069

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

    
1085

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

    
1095
    return object_list(
1096
        request,
1097
        projects,
1098
        template_name='im/projects/project_list.html',
1099
        extra_context={
1100
            'is_search':False,
1101
            'table': table,
1102
        })
1103

    
1104

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

    
1126
    next = request.GET.get('next')
1127
    if not next:
1128
        if chain_id:
1129
            next = reverse('astakos.im.views.project_detail', args=(chain_id,))
1130
        else:
1131
            next = reverse('astakos.im.views.project_list')
1132

    
1133
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1134
    return redirect(next)
1135

    
1136

    
1137
@require_http_methods(["GET", "POST"])
1138
@signed_terms_required
1139
@login_required
1140
def project_modify(request, application_id):
1141

    
1142
    try:
1143
        app = ProjectApplication.objects.get(id=application_id)
1144
    except ProjectApplication.DoesNotExist:
1145
        raise Http404
1146

    
1147
    user = request.user
1148
    if not (user.owns_application(app) or user.is_project_admin(app.id)):
1149
        m = _(astakos_messages.NOT_ALLOWED)
1150
        raise PermissionDenied(m)
1151

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

    
1184

    
1185
@require_http_methods(["GET", "POST"])
1186
@signed_terms_required
1187
@login_required
1188
def project_app(request, application_id):
1189
    return common_detail(request, application_id, project_view=False)
1190

    
1191
@require_http_methods(["GET", "POST"])
1192
@signed_terms_required
1193
@login_required
1194
def project_detail(request, chain_id):
1195
    return common_detail(request, chain_id)
1196

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

    
1214
def common_detail(request, chain_or_app_id, project_view=True):
1215
    project = None
1216
    if project_view:
1217
        chain_id = chain_or_app_id
1218
        if request.method == 'POST':
1219
            addmembers_form = AddProjectMembersForm(
1220
                request.POST,
1221
                chain_id=int(chain_id),
1222
                request_user=request.user)
1223
            addmembers(request, chain_id, addmembers_form)
1224
            if addmembers_form.is_valid():
1225
                addmembers_form = AddProjectMembersForm()  # clear form data
1226
        else:
1227
            addmembers_form = AddProjectMembersForm()  # initialize form
1228

    
1229
        project, application = get_by_chain_or_404(chain_id)
1230
        if project:
1231
            members = project.projectmembership_set.select_related()
1232
            members_table = tables.ProjectMembersTable(project,
1233
                                                       members,
1234
                                                       user=request.user,
1235
                                                       prefix="members_")
1236
            RequestConfig(request, paginate={"per_page": PAGINATE_BY}
1237
                          ).configure(members_table)
1238

    
1239
        else:
1240
            members_table = None
1241

    
1242
    else: # is application
1243
        application_id = chain_or_app_id
1244
        application = get_object_or_404(ProjectApplication, pk=application_id)
1245
        members_table = None
1246
        addmembers_form = None
1247

    
1248
    modifications_table = None
1249

    
1250
    user = request.user
1251
    is_project_admin = user.is_project_admin(application_id=application.id)
1252
    is_owner = user.owns_application(application)
1253
    if not (is_owner or is_project_admin) and not project_view:
1254
        m = _(astakos_messages.NOT_ALLOWED)
1255
        raise PermissionDenied(m)
1256

    
1257
    if (not (is_owner or is_project_admin) and project_view and
1258
        not user.non_owner_can_view(project)):
1259
        m = _(astakos_messages.NOT_ALLOWED)
1260
        raise PermissionDenied(m)
1261

    
1262
    following_applications = list(application.pending_modifications())
1263
    following_applications.reverse()
1264
    modifications_table = (
1265
        tables.ProjectModificationApplicationsTable(following_applications,
1266
                                                    user=request.user,
1267
                                                    prefix="modifications_"))
1268

    
1269
    mem_display = user.membership_display(project) if project else None
1270
    can_join_req = can_join_request(project, user) if project else False
1271
    can_leave_req = can_leave_request(project, user) if project else False
1272

    
1273
    return object_detail(
1274
        request,
1275
        queryset=ProjectApplication.objects.select_related(),
1276
        object_id=application.id,
1277
        template_name='im/projects/project_detail.html',
1278
        extra_context={
1279
            'project_view': project_view,
1280
            'addmembers_form':addmembers_form,
1281
            'members_table': members_table,
1282
            'owner_mode': is_owner,
1283
            'admin_mode': is_project_admin,
1284
            'modifications_table': modifications_table,
1285
            'mem_display': mem_display,
1286
            'can_join_request': can_join_req,
1287
            'can_leave_request': can_leave_req,
1288
            })
1289

    
1290
@require_http_methods(["GET", "POST"])
1291
@signed_terms_required
1292
@login_required
1293
def project_search(request):
1294
    q = request.GET.get('q', '')
1295
    form = ProjectSearchForm()
1296
    q = q.strip()
1297

    
1298
    if request.method == "POST":
1299
        form = ProjectSearchForm(request.POST)
1300
        if form.is_valid():
1301
            q = form.cleaned_data['q'].strip()
1302
        else:
1303
            q = None
1304

    
1305
    if q is None:
1306
        projects = ProjectApplication.objects.none()
1307
    else:
1308
        accepted_projects = request.user.projectmembership_set.filter(
1309
            ~Q(acceptance_date__isnull=True)).values_list('project', flat=True)
1310
        projects = ProjectApplication.objects.search_by_name(q)
1311
        projects = projects.filter(~Q(project__last_approval_date__isnull=True))
1312
        projects = projects.exclude(project__in=accepted_projects)
1313

    
1314
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1315
                                                prefix="my_projects_")
1316
    if request.method == "POST":
1317
        table.caption = _('SEARCH RESULTS')
1318
    else:
1319
        table.caption = _('ALL PROJECTS')
1320

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

    
1323
    return object_list(
1324
        request,
1325
        projects,
1326
        template_name='im/projects/project_list.html',
1327
        extra_context={
1328
          'form': form,
1329
          'is_search': True,
1330
          'q': q,
1331
          'table': table
1332
        })
1333

    
1334
@require_http_methods(["POST", "GET"])
1335
@signed_terms_required
1336
@login_required
1337
@project_transaction_context(sync=True)
1338
def project_join(request, chain_id, ctx=None):
1339
    next = request.GET.get('next')
1340
    if not next:
1341
        next = reverse('astakos.im.views.project_detail',
1342
                       args=(chain_id,))
1343

    
1344
    try:
1345
        chain_id = int(chain_id)
1346
        auto_accepted = join_project(chain_id, request.user)
1347
        if auto_accepted:
1348
            m = _(astakos_messages.USER_JOINED_PROJECT)
1349
        else:
1350
            m = _(astakos_messages.USER_JOIN_REQUEST_SUBMITTED)
1351
        messages.success(request, m)
1352
    except (IOError, PermissionDenied), e:
1353
        messages.error(request, e)
1354
    except BaseException, e:
1355
        logger.exception(e)
1356
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1357
        if ctx:
1358
            ctx.mark_rollback()
1359
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1360
    return redirect(next)
1361

    
1362
@require_http_methods(["POST", "GET"])
1363
@signed_terms_required
1364
@login_required
1365
@project_transaction_context(sync=True)
1366
def project_leave(request, chain_id, ctx=None):
1367
    next = request.GET.get('next')
1368
    if not next:
1369
        next = reverse('astakos.im.views.project_list')
1370

    
1371
    try:
1372
        chain_id = int(chain_id)
1373
        auto_accepted = leave_project(chain_id, request.user)
1374
        if auto_accepted:
1375
            m = _(astakos_messages.USER_LEFT_PROJECT)
1376
        else:
1377
            m = _(astakos_messages.USER_LEAVE_REQUEST_SUBMITTED)
1378
        messages.success(request, m)
1379
    except (IOError, PermissionDenied), e:
1380
        messages.error(request, e)
1381
    except PendingMembershipError as e:
1382
        raise RetryException()
1383
    except BaseException, e:
1384
        logger.exception(e)
1385
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1386
        if ctx:
1387
            ctx.mark_rollback()
1388
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1389
    return redirect(next)
1390

    
1391
@require_http_methods(["POST"])
1392
@signed_terms_required
1393
@login_required
1394
@project_transaction_context()
1395
def project_cancel(request, chain_id, ctx=None):
1396
    next = request.GET.get('next')
1397
    if not next:
1398
        next = reverse('astakos.im.views.project_list')
1399

    
1400
    try:
1401
        chain_id = int(chain_id)
1402
        cancel_membership(chain_id, request.user)
1403
        m = _(astakos_messages.USER_REQUEST_CANCELLED)
1404
        messages.success(request, m)
1405
    except (IOError, PermissionDenied), e:
1406
        messages.error(request, e)
1407
    except PendingMembershipError as e:
1408
        raise RetryException()
1409
    except BaseException, e:
1410
        logger.exception(e)
1411
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1412
        if ctx:
1413
            ctx.mark_rollback()
1414

    
1415
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1416
    return redirect(next)
1417

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

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

    
1466
@require_http_methods(["POST"])
1467
@signed_terms_required
1468
@login_required
1469
@project_transaction_context()
1470
def project_reject_member(request, chain_id, user_id, ctx=None):
1471
    try:
1472
        chain_id = int(chain_id)
1473
        user_id = int(user_id)
1474
        m = reject_membership(chain_id, user_id, request.user)
1475
    except (IOError, PermissionDenied), e:
1476
        messages.error(request, e)
1477
    except PendingMembershipError as e:
1478
        raise RetryException()
1479
    except BaseException, e:
1480
        logger.exception(e)
1481
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1482
        if ctx:
1483
            ctx.mark_rollback()
1484
    else:
1485
        realname = escape(m.person.realname)
1486
        msg = _(astakos_messages.USER_MEMBERSHIP_REJECTED) % locals()
1487
        messages.success(request, msg)
1488
    return redirect(reverse('project_detail', args=(chain_id,)))
1489

    
1490
@require_http_methods(["POST", "GET"])
1491
@signed_terms_required
1492
@login_required
1493
@project_transaction_context(sync=True)
1494
def project_app_approve(request, application_id, ctx=None):
1495

    
1496
    if not request.user.is_project_admin():
1497
        m = _(astakos_messages.NOT_ALLOWED)
1498
        raise PermissionDenied(m)
1499

    
1500
    try:
1501
        app = ProjectApplication.objects.get(id=application_id)
1502
    except ProjectApplication.DoesNotExist:
1503
        raise Http404
1504

    
1505
    approve_application(application_id)
1506
    chain_id = get_related_project_id(application_id)
1507
    return redirect(reverse('project_detail', args=(chain_id,)))
1508

    
1509
@require_http_methods(["POST", "GET"])
1510
@signed_terms_required
1511
@login_required
1512
@project_transaction_context()
1513
def project_app_deny(request, application_id, ctx=None):
1514

    
1515
    if not request.user.is_project_admin():
1516
        m = _(astakos_messages.NOT_ALLOWED)
1517
        raise PermissionDenied(m)
1518

    
1519
    try:
1520
        app = ProjectApplication.objects.get(id=application_id)
1521
    except ProjectApplication.DoesNotExist:
1522
        raise Http404
1523

    
1524
    deny_application(application_id)
1525
    return redirect(reverse('project_list'))
1526

    
1527
@require_http_methods(["POST", "GET"])
1528
@signed_terms_required
1529
@login_required
1530
@project_transaction_context()
1531
def project_app_dismiss(request, application_id, ctx=None):
1532
    try:
1533
        app = ProjectApplication.objects.get(id=application_id)
1534
    except ProjectApplication.DoesNotExist:
1535
        raise Http404
1536

    
1537
    if not request.user.owns_application(app):
1538
        m = _(astakos_messages.NOT_ALLOWED)
1539
        raise PermissionDenied(m)
1540

    
1541
    # XXX: dismiss application also does authorization
1542
    dismiss_application(application_id, request_user=request.user)
1543

    
1544
    chain_id = None
1545
    chain_id = get_related_project_id(application_id)
1546
    if chain_id:
1547
        next = reverse('project_detail', args=(chain_id,))
1548
    else:
1549
        next = reverse('project_list')
1550
    return redirect(next)
1551

    
1552

    
1553
def landing(request):
1554
    return render_response(
1555
        'im/landing.html',
1556
        context_instance=get_context(request))
1557

    
1558

    
1559
def api_access(request):
1560
    return render_response(
1561
        'im/api_access.html',
1562
        context_instance=get_context(request))