Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (56.8 kB)

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

    
34
import logging
35
import calendar
36
import inflect
37

    
38
engine = inflect.engine()
39

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

    
45
from django_tables2 import RequestConfig
46

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

    
75
import astakos.im.messages as astakos_messages
76

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

    
121
logger = logging.getLogger(__name__)
122

    
123
callpoint = AstakosCallpoint()
124

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

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

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

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

    
160

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

    
175

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

    
191

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

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

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

    
219

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

    
223

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

230
    **Arguments**
231

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

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

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

243
    **Template:**
244

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

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

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

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

    
263

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

    
276

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

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

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

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

293
    **Arguments**
294

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

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

302
    **Template:**
303

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

306
    **Settings:**
307

308
    The view expectes the following settings are defined:
309

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

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

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

    
354

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

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

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

369
    **Arguments**
370

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

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

378
    **Template:**
379

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

382
    **Settings:**
383

384
    The view expectes the following settings are defined:
385

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

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

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

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

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

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

    
442

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

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

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

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

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

462
    **Arguments**
463

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

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

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

474
    **Template:**
475

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

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

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

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

    
504

    
505
    extra_context['pending_user_exists'] = pending_user
506

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

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

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

    
527
                form.store_user(user, request)
528

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

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

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

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

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

    
568

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

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

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

582
    **Arguments**
583

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

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

591
    **Template:**
592

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

595
    **Settings:**
596

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

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

    
622

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

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

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

    
663

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

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

    
682
    if user.is_active or user.email_verified:
683
        message = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE)
684
        messages.error(request, message)
685
        return HttpResponseRedirect(reverse('index'))
686

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

    
708

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

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

    
735
    terms = f.read()
736

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

    
761

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

    
771

    
772
    if not astakos_settings.EMAILCHANGE_ENABLED:
773
        raise PermissionDenied
774

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

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

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

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

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

    
822
    if request.user.email_change_is_pending():
823
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
824

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

    
831

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

    
834
    if request.user.is_authenticated():
835
        return HttpResponseRedirect(reverse('edit_profile'))
836

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

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

    
854
    return HttpResponseRedirect(reverse('index'))
855

    
856

    
857
@require_http_methods(["GET"])
858
@valid_astakos_user_required
859
def resource_usage(request):
860

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

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

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

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

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

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

    
917
    if provider.can_remove():
918
        provider.delete()
919
        message = astakos_messages.AUTH_PROVIDER_REMOVED % \
920
                            provider.settings.get_method_prompt_display
921
        user = request.user
922
        logger.info("%s deleted %s provider (%d): %r" % (user.log_display,
923
                                                         provider.module,
924
                                                         int(pk),
925
                                                         provider.info))
926
        messages.success(request, message)
927
        return HttpResponseRedirect(reverse('edit_profile'))
928
    else:
929
        raise PermissionDenied
930

    
931

    
932
def how_it_works(request):
933
    return render_response(
934
        'im/how_it_works.html',
935
        context_instance=get_context(request))
936

    
937
@project_transaction_context()
938
def _create_object(request, model=None, template_name=None,
939
        template_loader=template_loader, extra_context=None, post_save_redirect=None,
940
        login_required=False, context_processors=None, form_class=None,
941
        msg=None, ctx=None):
942
    """
943
    Based of django.views.generic.create_update.create_object which displays a
944
    summary page before creating the object.
945
    """
946
    response = None
947

    
948
    if extra_context is None: extra_context = {}
949
    if login_required and not request.user.is_authenticated():
950
        return redirect_to_login(request.path)
951
    try:
952

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

    
993
@project_transaction_context()
994
def _update_object(request, model=None, object_id=None, slug=None,
995
        slug_field='slug', template_name=None, template_loader=template_loader,
996
        extra_context=None, post_save_redirect=None, login_required=False,
997
        context_processors=None, template_object_name='object',
998
        form_class=None, msg=None, ctx=None):
999
    """
1000
    Based of django.views.generic.create_update.update_object which displays a
1001
    summary page before updating the object.
1002
    """
1003
    response = None
1004

    
1005
    if extra_context is None: extra_context = {}
1006
    if login_required and not request.user.is_authenticated():
1007
        return redirect_to_login(request.path)
1008

    
1009
    try:
1010
        model, form_class = get_model_and_form_class(model, form_class)
1011
        obj = lookup_object(model, object_id, slug, slug_field)
1012

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

    
1051
@require_http_methods(["GET", "POST"])
1052
@signed_terms_required
1053
@login_required
1054
def project_add(request):
1055

    
1056
    user = request.user
1057
    reached, limit = reached_pending_application_limit(user.id)
1058
    if reached:
1059
        m = _(astakos_messages.PENDING_APPLICATION_LIMIT_ADD) % limit
1060
        messages.error(request, m)
1061
        next = reverse('astakos.im.views.project_list')
1062
        next = restrict_next(next, domain=COOKIE_DOMAIN)
1063
        return redirect(next)
1064

    
1065
    resource_groups = RESOURCES_PRESENTATION_DATA.get('groups', {})
1066
    resource_catalog = ()
1067
    result = callpoint.list_resources()
1068
    details_fields = [
1069
        "name", "homepage", "description","start_date","end_date", "comments"]
1070
    membership_fields =[
1071
        "member_join_policy", "member_leave_policy", "limit_on_members_number"]
1072
    if not result.is_success:
1073
        messages.error(
1074
            request,
1075
            'Unable to retrieve system resources: %s' % result.reason
1076
    )
1077
    else:
1078
        resource_catalog = [
1079
            [g, filter(lambda r: r.get('group', '') == g, result.data)] \
1080
                for g in resource_groups]
1081

    
1082
    # order resources
1083
    groups_order = RESOURCES_PRESENTATION_DATA.get('groups_order')
1084
    resources_order = RESOURCES_PRESENTATION_DATA.get('resources_order')
1085
    resource_catalog = sorted(resource_catalog, key=lambda g:groups_order.index(g[0]))
1086

    
1087
    resource_groups_list = sorted([(k,v) for k,v in resource_groups.items()],
1088
                                  key=lambda f:groups_order.index(f[0]))
1089
    resource_groups = OrderedDict(resource_groups_list)
1090
    for index, group in enumerate(resource_catalog):
1091
        resource_catalog[index][1] = sorted(resource_catalog[index][1],
1092
                                            key=lambda r: resources_order.index(r['str_repr']))
1093

    
1094

    
1095
    extra_context = {
1096
        'resource_catalog':resource_catalog,
1097
        'resource_groups':resource_groups,
1098
        'show_form':True,
1099
        'details_fields':details_fields,
1100
        'membership_fields':membership_fields}
1101
    return _create_object(
1102
        request,
1103
        template_name='im/projects/projectapplication_form.html',
1104
        extra_context=extra_context,
1105
        post_save_redirect=reverse('project_list'),
1106
        form_class=ProjectApplicationForm,
1107
        msg=_("The %(verbose_name)s has been received and \
1108
                 is under consideration."))
1109

    
1110

    
1111
@require_http_methods(["GET"])
1112
@signed_terms_required
1113
@login_required
1114
def project_list(request):
1115
    projects = ProjectApplication.objects.user_accessible_projects(request.user).select_related()
1116
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1117
                                                prefix="my_projects_")
1118
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1119

    
1120
    return object_list(
1121
        request,
1122
        projects,
1123
        template_name='im/projects/project_list.html',
1124
        extra_context={
1125
            'is_search':False,
1126
            'table': table,
1127
        })
1128

    
1129

    
1130
@require_http_methods(["GET", "POST"])
1131
@signed_terms_required
1132
@login_required
1133
@project_transaction_context()
1134
def project_app_cancel(request, application_id, ctx=None):
1135
    chain_id = None
1136
    try:
1137
        application_id = int(application_id)
1138
        chain_id = get_related_project_id(application_id)
1139
        cancel_application(application_id, request.user)
1140
    except (IOError, PermissionDenied), e:
1141
        messages.error(request, e)
1142
    except BaseException, e:
1143
        logger.exception(e)
1144
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1145
        if ctx:
1146
            ctx.mark_rollback()
1147
    else:
1148
        msg = _(astakos_messages.APPLICATION_CANCELLED)
1149
        messages.success(request, msg)
1150

    
1151
    next = request.GET.get('next')
1152
    if not next:
1153
        if chain_id:
1154
            next = reverse('astakos.im.views.project_detail', args=(chain_id,))
1155
        else:
1156
            next = reverse('astakos.im.views.project_list')
1157

    
1158
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1159
    return redirect(next)
1160

    
1161

    
1162
@require_http_methods(["GET", "POST"])
1163
@signed_terms_required
1164
@login_required
1165
def project_modify(request, application_id):
1166

    
1167
    try:
1168
        app = ProjectApplication.objects.get(id=application_id)
1169
    except ProjectApplication.DoesNotExist:
1170
        raise Http404
1171

    
1172
    user = request.user
1173
    if not (user.owns_application(app) or user.is_project_admin(app.id)):
1174
        m = _(astakos_messages.NOT_ALLOWED)
1175
        raise PermissionDenied(m)
1176

    
1177
    reached, limit = reached_pending_application_limit(user.id, app)
1178
    if reached:
1179
        m = _(astakos_messages.PENDING_APPLICATION_LIMIT_MODIFY) % limit
1180
        messages.error(request, m)
1181
        next = reverse('astakos.im.views.project_list')
1182
        next = restrict_next(next, domain=COOKIE_DOMAIN)
1183
        return redirect(next)
1184

    
1185
    resource_groups = RESOURCES_PRESENTATION_DATA.get('groups', {})
1186
    resource_catalog = ()
1187
    result = callpoint.list_resources()
1188
    details_fields = [
1189
        "name", "homepage", "description","start_date","end_date", "comments"]
1190
    membership_fields =[
1191
        "member_join_policy", "member_leave_policy", "limit_on_members_number"]
1192
    if not result.is_success:
1193
        messages.error(
1194
            request,
1195
            'Unable to retrieve system resources: %s' % result.reason
1196
    )
1197
    else:
1198
        resource_catalog = [
1199
            (g, filter(lambda r: r.get('group', '') == g, result.data)) \
1200
                for g in resource_groups]
1201
    extra_context = {
1202
        'resource_catalog':resource_catalog,
1203
        'resource_groups':resource_groups,
1204
        'show_form':True,
1205
        'details_fields':details_fields,
1206
        'update_form': True,
1207
        'membership_fields':membership_fields}
1208
    return _update_object(
1209
        request,
1210
        object_id=application_id,
1211
        template_name='im/projects/projectapplication_form.html',
1212
        extra_context=extra_context, post_save_redirect=reverse('project_list'),
1213
        form_class=ProjectApplicationForm,
1214
        msg = _("The %(verbose_name)s has been received and \
1215
                    is under consideration."))
1216

    
1217

    
1218
@require_http_methods(["GET", "POST"])
1219
@signed_terms_required
1220
@login_required
1221
def project_app(request, application_id):
1222
    return common_detail(request, application_id, project_view=False)
1223

    
1224
@require_http_methods(["GET", "POST"])
1225
@signed_terms_required
1226
@login_required
1227
def project_detail(request, chain_id):
1228
    return common_detail(request, chain_id)
1229

    
1230
@project_transaction_context(sync=True)
1231
def addmembers(request, chain_id, addmembers_form, ctx=None):
1232
    if addmembers_form.is_valid():
1233
        try:
1234
            chain_id = int(chain_id)
1235
            map(lambda u: enroll_member(
1236
                    chain_id,
1237
                    u,
1238
                    request_user=request.user),
1239
                addmembers_form.valid_users)
1240
        except (IOError, PermissionDenied), e:
1241
            messages.error(request, e)
1242
        except BaseException, e:
1243
            if ctx:
1244
                ctx.mark_rollback()
1245
            messages.error(request, e)
1246

    
1247
def common_detail(request, chain_or_app_id, project_view=True):
1248
    project = None
1249
    if project_view:
1250
        chain_id = chain_or_app_id
1251
        if request.method == 'POST':
1252
            addmembers_form = AddProjectMembersForm(
1253
                request.POST,
1254
                chain_id=int(chain_id),
1255
                request_user=request.user)
1256
            addmembers(request, chain_id, addmembers_form)
1257
            if addmembers_form.is_valid():
1258
                addmembers_form = AddProjectMembersForm()  # clear form data
1259
        else:
1260
            addmembers_form = AddProjectMembersForm()  # initialize form
1261

    
1262
        project, application = get_by_chain_or_404(chain_id)
1263
        if project:
1264
            members = project.projectmembership_set.select_related()
1265
            members_table = tables.ProjectMembersTable(project,
1266
                                                       members,
1267
                                                       user=request.user,
1268
                                                       prefix="members_")
1269
            RequestConfig(request, paginate={"per_page": PAGINATE_BY}
1270
                          ).configure(members_table)
1271

    
1272
        else:
1273
            members_table = None
1274

    
1275
    else: # is application
1276
        application_id = chain_or_app_id
1277
        application = get_object_or_404(ProjectApplication, pk=application_id)
1278
        members_table = None
1279
        addmembers_form = None
1280

    
1281
    modifications_table = None
1282

    
1283
    user = request.user
1284
    is_project_admin = user.is_project_admin(application_id=application.id)
1285
    is_owner = user.owns_application(application)
1286
    if not (is_owner or is_project_admin) and not project_view:
1287
        m = _(astakos_messages.NOT_ALLOWED)
1288
        raise PermissionDenied(m)
1289

    
1290
    if (not (is_owner or is_project_admin) and project_view and
1291
        not user.non_owner_can_view(project)):
1292
        m = _(astakos_messages.NOT_ALLOWED)
1293
        raise PermissionDenied(m)
1294

    
1295
    following_applications = list(application.pending_modifications())
1296
    following_applications.reverse()
1297
    modifications_table = (
1298
        tables.ProjectModificationApplicationsTable(following_applications,
1299
                                                    user=request.user,
1300
                                                    prefix="modifications_"))
1301

    
1302
    mem_display = user.membership_display(project) if project else None
1303
    can_join_req = can_join_request(project, user) if project else False
1304
    can_leave_req = can_leave_request(project, user) if project else False
1305

    
1306
    return object_detail(
1307
        request,
1308
        queryset=ProjectApplication.objects.select_related(),
1309
        object_id=application.id,
1310
        template_name='im/projects/project_detail.html',
1311
        extra_context={
1312
            'project_view': project_view,
1313
            'addmembers_form':addmembers_form,
1314
            'members_table': members_table,
1315
            'owner_mode': is_owner,
1316
            'admin_mode': is_project_admin,
1317
            'modifications_table': modifications_table,
1318
            'mem_display': mem_display,
1319
            'can_join_request': can_join_req,
1320
            'can_leave_request': can_leave_req,
1321
            })
1322

    
1323
@require_http_methods(["GET", "POST"])
1324
@signed_terms_required
1325
@login_required
1326
def project_search(request):
1327
    q = request.GET.get('q', '')
1328
    form = ProjectSearchForm()
1329
    q = q.strip()
1330

    
1331
    if request.method == "POST":
1332
        form = ProjectSearchForm(request.POST)
1333
        if form.is_valid():
1334
            q = form.cleaned_data['q'].strip()
1335
        else:
1336
            q = None
1337

    
1338
    if q is None:
1339
        projects = ProjectApplication.objects.none()
1340
    else:
1341
        accepted_projects = request.user.projectmembership_set.filter(
1342
            ~Q(acceptance_date__isnull=True)).values_list('project', flat=True)
1343
        projects = ProjectApplication.objects.search_by_name(q)
1344
        projects = projects.filter(~Q(project__last_approval_date__isnull=True))
1345
        projects = projects.exclude(project__in=accepted_projects)
1346

    
1347
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1348
                                                prefix="my_projects_")
1349
    if request.method == "POST":
1350
        table.caption = _('SEARCH RESULTS')
1351
    else:
1352
        table.caption = _('ALL PROJECTS')
1353

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

    
1356
    return object_list(
1357
        request,
1358
        projects,
1359
        template_name='im/projects/project_list.html',
1360
        extra_context={
1361
          'form': form,
1362
          'is_search': True,
1363
          'q': q,
1364
          'table': table
1365
        })
1366

    
1367
@require_http_methods(["POST", "GET"])
1368
@signed_terms_required
1369
@login_required
1370
@project_transaction_context(sync=True)
1371
def project_join(request, chain_id, ctx=None):
1372
    next = request.GET.get('next')
1373
    if not next:
1374
        next = reverse('astakos.im.views.project_detail',
1375
                       args=(chain_id,))
1376

    
1377
    try:
1378
        chain_id = int(chain_id)
1379
        auto_accepted = join_project(chain_id, request.user)
1380
        if auto_accepted:
1381
            m = _(astakos_messages.USER_JOINED_PROJECT)
1382
        else:
1383
            m = _(astakos_messages.USER_JOIN_REQUEST_SUBMITTED)
1384
        messages.success(request, m)
1385
    except (IOError, PermissionDenied), e:
1386
        messages.error(request, e)
1387
    except BaseException, e:
1388
        logger.exception(e)
1389
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1390
        if ctx:
1391
            ctx.mark_rollback()
1392
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1393
    return redirect(next)
1394

    
1395
@require_http_methods(["POST", "GET"])
1396
@signed_terms_required
1397
@login_required
1398
@project_transaction_context(sync=True)
1399
def project_leave(request, chain_id, ctx=None):
1400
    next = request.GET.get('next')
1401
    if not next:
1402
        next = reverse('astakos.im.views.project_list')
1403

    
1404
    try:
1405
        chain_id = int(chain_id)
1406
        auto_accepted = leave_project(chain_id, request.user)
1407
        if auto_accepted:
1408
            m = _(astakos_messages.USER_LEFT_PROJECT)
1409
        else:
1410
            m = _(astakos_messages.USER_LEAVE_REQUEST_SUBMITTED)
1411
        messages.success(request, m)
1412
    except (IOError, PermissionDenied), e:
1413
        messages.error(request, e)
1414
    except PendingMembershipError as e:
1415
        raise RetryException()
1416
    except BaseException, e:
1417
        logger.exception(e)
1418
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1419
        if ctx:
1420
            ctx.mark_rollback()
1421
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1422
    return redirect(next)
1423

    
1424
@require_http_methods(["POST"])
1425
@signed_terms_required
1426
@login_required
1427
@project_transaction_context()
1428
def project_cancel(request, chain_id, ctx=None):
1429
    next = request.GET.get('next')
1430
    if not next:
1431
        next = reverse('astakos.im.views.project_list')
1432

    
1433
    try:
1434
        chain_id = int(chain_id)
1435
        cancel_membership(chain_id, request.user)
1436
        m = _(astakos_messages.USER_REQUEST_CANCELLED)
1437
        messages.success(request, m)
1438
    except (IOError, PermissionDenied), e:
1439
        messages.error(request, e)
1440
    except PendingMembershipError as e:
1441
        raise RetryException()
1442
    except BaseException, e:
1443
        logger.exception(e)
1444
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1445
        if ctx:
1446
            ctx.mark_rollback()
1447

    
1448
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1449
    return redirect(next)
1450

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

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

    
1499
@require_http_methods(["POST"])
1500
@signed_terms_required
1501
@login_required
1502
@project_transaction_context()
1503
def project_reject_member(request, chain_id, user_id, ctx=None):
1504
    try:
1505
        chain_id = int(chain_id)
1506
        user_id = int(user_id)
1507
        m = reject_membership(chain_id, user_id, request.user)
1508
    except (IOError, PermissionDenied), e:
1509
        messages.error(request, e)
1510
    except PendingMembershipError as e:
1511
        raise RetryException()
1512
    except BaseException, e:
1513
        logger.exception(e)
1514
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1515
        if ctx:
1516
            ctx.mark_rollback()
1517
    else:
1518
        email = escape(m.person.email)
1519
        msg = _(astakos_messages.USER_MEMBERSHIP_REJECTED) % email
1520
        messages.success(request, msg)
1521
    return redirect(reverse('project_detail', args=(chain_id,)))
1522

    
1523
@require_http_methods(["POST", "GET"])
1524
@signed_terms_required
1525
@login_required
1526
@project_transaction_context(sync=True)
1527
def project_app_approve(request, application_id, ctx=None):
1528

    
1529
    if not request.user.is_project_admin():
1530
        m = _(astakos_messages.NOT_ALLOWED)
1531
        raise PermissionDenied(m)
1532

    
1533
    try:
1534
        app = ProjectApplication.objects.get(id=application_id)
1535
    except ProjectApplication.DoesNotExist:
1536
        raise Http404
1537

    
1538
    approve_application(application_id)
1539
    chain_id = get_related_project_id(application_id)
1540
    return redirect(reverse('project_detail', args=(chain_id,)))
1541

    
1542
@require_http_methods(["POST", "GET"])
1543
@signed_terms_required
1544
@login_required
1545
@project_transaction_context()
1546
def project_app_deny(request, application_id, ctx=None):
1547

    
1548
    if not request.user.is_project_admin():
1549
        m = _(astakos_messages.NOT_ALLOWED)
1550
        raise PermissionDenied(m)
1551

    
1552
    try:
1553
        app = ProjectApplication.objects.get(id=application_id)
1554
    except ProjectApplication.DoesNotExist:
1555
        raise Http404
1556

    
1557
    deny_application(application_id)
1558
    return redirect(reverse('project_list'))
1559

    
1560
@require_http_methods(["POST", "GET"])
1561
@signed_terms_required
1562
@login_required
1563
@project_transaction_context()
1564
def project_app_dismiss(request, application_id, ctx=None):
1565
    try:
1566
        app = ProjectApplication.objects.get(id=application_id)
1567
    except ProjectApplication.DoesNotExist:
1568
        raise Http404
1569

    
1570
    if not request.user.owns_application(app):
1571
        m = _(astakos_messages.NOT_ALLOWED)
1572
        raise PermissionDenied(m)
1573

    
1574
    # XXX: dismiss application also does authorization
1575
    dismiss_application(application_id, request_user=request.user)
1576

    
1577
    chain_id = None
1578
    chain_id = get_related_project_id(application_id)
1579
    if chain_id:
1580
        next = reverse('project_detail', args=(chain_id,))
1581
    else:
1582
        next = reverse('project_list')
1583
    return redirect(next)
1584

    
1585

    
1586
def landing(request):
1587
    return render_response(
1588
        'im/landing.html',
1589
        context_instance=get_context(request))
1590

    
1591

    
1592
def api_access(request):
1593
    return render_response(
1594
        'im/api_access.html',
1595
        context_instance=get_context(request))