Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 022cc8e2

History | View | Annotate | Download (55.1 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.add_message(request, messages.SUCCESS, mark_safe(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
        join_project(chain_id, request.user)
1347
        # TODO: distinct messages for request/auto accept ???
1348
        messages.success(request, _(astakos_messages.USER_JOIN_REQUEST_SUBMITED))
1349
    except (IOError, PermissionDenied), e:
1350
        messages.error(request, e)
1351
    except BaseException, e:
1352
        logger.exception(e)
1353
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1354
        if ctx:
1355
            ctx.mark_rollback()
1356
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1357
    return redirect(next)
1358

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

    
1368
    try:
1369
        chain_id = int(chain_id)
1370
        leave_project(chain_id, request.user)
1371
    except (IOError, PermissionDenied), e:
1372
        messages.error(request, e)
1373
    except PendingMembershipError as e:
1374
        raise RetryException()
1375
    except BaseException, e:
1376
        logger.exception(e)
1377
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1378
        if ctx:
1379
            ctx.mark_rollback()
1380
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1381
    return redirect(next)
1382

    
1383
@require_http_methods(["POST"])
1384
@signed_terms_required
1385
@login_required
1386
@project_transaction_context()
1387
def project_cancel(request, chain_id, ctx=None):
1388
    next = request.GET.get('next')
1389
    if not next:
1390
        next = reverse('astakos.im.views.project_list')
1391

    
1392
    try:
1393
        chain_id = int(chain_id)
1394
        cancel_membership(chain_id, request.user)
1395
    except (IOError, PermissionDenied), e:
1396
        messages.error(request, e)
1397
    except PendingMembershipError as e:
1398
        raise RetryException()
1399
    except BaseException, e:
1400
        logger.exception(e)
1401
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1402
        if ctx:
1403
            ctx.mark_rollback()
1404

    
1405
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1406
    return redirect(next)
1407

    
1408
@require_http_methods(["POST"])
1409
@signed_terms_required
1410
@login_required
1411
@project_transaction_context(sync=True)
1412
def project_accept_member(request, chain_id, user_id, ctx=None):
1413
    try:
1414
        chain_id = int(chain_id)
1415
        user_id = int(user_id)
1416
        m = accept_membership(chain_id, user_id, request.user)
1417
    except (IOError, PermissionDenied), e:
1418
        messages.error(request, e)
1419
    except PendingMembershipError as e:
1420
        raise RetryException()
1421
    except BaseException, e:
1422
        logger.exception(e)
1423
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1424
        if ctx:
1425
            ctx.mark_rollback()
1426
    else:
1427
        realname = escape(m.person.realname)
1428
        msg = _(astakos_messages.USER_JOINED_PROJECT) % locals()
1429
        messages.success(request, msg)
1430
    return redirect(reverse('project_detail', args=(chain_id,)))
1431

    
1432
@require_http_methods(["POST"])
1433
@signed_terms_required
1434
@login_required
1435
@project_transaction_context(sync=True)
1436
def project_remove_member(request, chain_id, user_id, ctx=None):
1437
    try:
1438
        chain_id = int(chain_id)
1439
        user_id = int(user_id)
1440
        m = remove_membership(chain_id, user_id, request.user)
1441
    except (IOError, PermissionDenied), e:
1442
        messages.error(request, e)
1443
    except PendingMembershipError as e:
1444
        raise RetryException()
1445
    except BaseException, e:
1446
        logger.exception(e)
1447
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1448
        if ctx:
1449
            ctx.mark_rollback()
1450
    else:
1451
        realname = escape(m.person.realname)
1452
        msg = _(astakos_messages.USER_LEFT_PROJECT) % locals()
1453
        messages.success(request, msg)
1454
    return redirect(reverse('project_detail', args=(chain_id,)))
1455

    
1456
@require_http_methods(["POST"])
1457
@signed_terms_required
1458
@login_required
1459
@project_transaction_context()
1460
def project_reject_member(request, chain_id, user_id, ctx=None):
1461
    try:
1462
        chain_id = int(chain_id)
1463
        user_id = int(user_id)
1464
        m = reject_membership(chain_id, user_id, request.user)
1465
    except (IOError, PermissionDenied), e:
1466
        messages.error(request, e)
1467
    except PendingMembershipError as e:
1468
        raise RetryException()
1469
    except BaseException, e:
1470
        logger.exception(e)
1471
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1472
        if ctx:
1473
            ctx.mark_rollback()
1474
    else:
1475
        realname = escape(m.person.realname)
1476
        msg = _(astakos_messages.USER_LEFT_PROJECT) % locals()
1477
        messages.success(request, msg)
1478
    return redirect(reverse('project_detail', args=(chain_id,)))
1479

    
1480
@require_http_methods(["POST", "GET"])
1481
@signed_terms_required
1482
@login_required
1483
@project_transaction_context(sync=True)
1484
def project_app_approve(request, application_id, ctx=None):
1485

    
1486
    if not request.user.is_project_admin():
1487
        m = _(astakos_messages.NOT_ALLOWED)
1488
        raise PermissionDenied(m)
1489

    
1490
    try:
1491
        app = ProjectApplication.objects.get(id=application_id)
1492
    except ProjectApplication.DoesNotExist:
1493
        raise Http404
1494

    
1495
    approve_application(application_id)
1496
    chain_id = get_related_project_id(application_id)
1497
    return redirect(reverse('project_detail', args=(chain_id,)))
1498

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

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

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

    
1514
    deny_application(application_id)
1515
    return redirect(reverse('project_list'))
1516

    
1517
@require_http_methods(["POST", "GET"])
1518
@signed_terms_required
1519
@login_required
1520
@project_transaction_context()
1521
def project_app_dismiss(request, application_id, ctx=None):
1522
    try:
1523
        app = ProjectApplication.objects.get(id=application_id)
1524
    except ProjectApplication.DoesNotExist:
1525
        raise Http404
1526

    
1527
    if not request.user.owns_application(app):
1528
        m = _(astakos_messages.NOT_ALLOWED)
1529
        raise PermissionDenied(m)
1530

    
1531
    # XXX: dismiss application also does authorization
1532
    dismiss_application(application_id, request_user=request.user)
1533

    
1534
    chain_id = None
1535
    chain_id = get_related_project_id(application_id)
1536
    if chain_id:
1537
        next = reverse('project_detail', args=(chain_id,))
1538
    else:
1539
        next = reverse('project_list')
1540
    return redirect(next)
1541

    
1542

    
1543
def landing(request):
1544
    return render_response(
1545
        'im/landing.html',
1546
        context_instance=get_context(request))
1547

    
1548

    
1549
def api_access(request):
1550
    return render_response(
1551
        'im/api_access.html',
1552
        context_instance=get_context(request))