Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (56.4 kB)

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

    
34
import logging
35
import calendar
36
import inflect
37

    
38
engine = inflect.engine()
39

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

    
45
from django_tables2 import RequestConfig
46

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

    
621

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

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

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

    
662

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

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

    
681
    if user.is_active:
682
        message = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE)
683
        messages.error(request, message)
684
        return index(request)
685

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

    
707

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

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

    
734
    terms = f.read()
735

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

    
760

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

    
770

    
771
    if not astakos_settings.EMAILCHANGE_ENABLED:
772
        raise PermissionDenied
773

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

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

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

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

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

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

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

    
830

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

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

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

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

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

    
855

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

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

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

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

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

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

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

    
916
    if provider.can_remove():
917
        provider.delete()
918
        message = astakos_messages.AUTH_PROVIDER_REMOVED % \
919
                            provider.settings.get_method_prompt_display
920
        messages.success(request, message)
921
        return HttpResponseRedirect(reverse('edit_profile'))
922
    else:
923
        raise PermissionDenied
924

    
925

    
926
def how_it_works(request):
927
    return render_response(
928
        'im/how_it_works.html',
929
        context_instance=get_context(request))
930

    
931
@project_transaction_context()
932
def _create_object(request, model=None, template_name=None,
933
        template_loader=template_loader, extra_context=None, post_save_redirect=None,
934
        login_required=False, context_processors=None, form_class=None,
935
        msg=None, ctx=None):
936
    """
937
    Based of django.views.generic.create_update.create_object which displays a
938
    summary page before creating the object.
939
    """
940
    response = None
941

    
942
    if extra_context is None: extra_context = {}
943
    if login_required and not request.user.is_authenticated():
944
        return redirect_to_login(request.path)
945
    try:
946

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

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

    
999
    if extra_context is None: extra_context = {}
1000
    if login_required and not request.user.is_authenticated():
1001
        return redirect_to_login(request.path)
1002

    
1003
    try:
1004
        model, form_class = get_model_and_form_class(model, form_class)
1005
        obj = lookup_object(model, object_id, slug, slug_field)
1006

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

    
1045
@require_http_methods(["GET", "POST"])
1046
@signed_terms_required
1047
@login_required
1048
def project_add(request):
1049

    
1050
    user = request.user
1051
    reached, limit = reached_pending_application_limit(user.id)
1052
    if reached:
1053
        m = _(astakos_messages.PENDING_APPLICATION_LIMIT_ADD) % limit
1054
        messages.error(request, m)
1055
        next = reverse('astakos.im.views.project_list')
1056
        next = restrict_next(next, domain=COOKIE_DOMAIN)
1057
        return redirect(next)
1058

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

    
1076
    # order resources
1077
    groups_order = RESOURCES_PRESENTATION_DATA.get('groups_order')
1078
    resources_order = RESOURCES_PRESENTATION_DATA.get('resources_order')
1079
    resource_catalog = sorted(resource_catalog, key=lambda g:groups_order.index(g[0]))
1080

    
1081
    resource_groups_list = sorted([(k,v) for k,v in resource_groups.items()],
1082
                                  key=lambda f:groups_order.index(f[0]))
1083
    resource_groups = OrderedDict(resource_groups_list)
1084
    for index, group in enumerate(resource_catalog):
1085
        resource_catalog[index][1] = sorted(resource_catalog[index][1],
1086
                                            key=lambda r: resources_order.index(r['str_repr']))
1087

    
1088

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

    
1104

    
1105
@require_http_methods(["GET"])
1106
@signed_terms_required
1107
@login_required
1108
def project_list(request):
1109
    projects = ProjectApplication.objects.user_accessible_projects(request.user).select_related()
1110
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1111
                                                prefix="my_projects_")
1112
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1113

    
1114
    return object_list(
1115
        request,
1116
        projects,
1117
        template_name='im/projects/project_list.html',
1118
        extra_context={
1119
            'is_search':False,
1120
            'table': table,
1121
        })
1122

    
1123

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

    
1145
    next = request.GET.get('next')
1146
    if not next:
1147
        if chain_id:
1148
            next = reverse('astakos.im.views.project_detail', args=(chain_id,))
1149
        else:
1150
            next = reverse('astakos.im.views.project_list')
1151

    
1152
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1153
    return redirect(next)
1154

    
1155

    
1156
@require_http_methods(["GET", "POST"])
1157
@signed_terms_required
1158
@login_required
1159
def project_modify(request, application_id):
1160

    
1161
    try:
1162
        app = ProjectApplication.objects.get(id=application_id)
1163
    except ProjectApplication.DoesNotExist:
1164
        raise Http404
1165

    
1166
    user = request.user
1167
    if not (user.owns_application(app) or user.is_project_admin(app.id)):
1168
        m = _(astakos_messages.NOT_ALLOWED)
1169
        raise PermissionDenied(m)
1170

    
1171
    reached, limit = reached_pending_application_limit(user.id, app)
1172
    if reached:
1173
        m = _(astakos_messages.PENDING_APPLICATION_LIMIT_MODIFY) % limit
1174
        messages.error(request, m)
1175
        next = reverse('astakos.im.views.project_list')
1176
        next = restrict_next(next, domain=COOKIE_DOMAIN)
1177
        return redirect(next)
1178

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

    
1211

    
1212
@require_http_methods(["GET", "POST"])
1213
@signed_terms_required
1214
@login_required
1215
def project_app(request, application_id):
1216
    return common_detail(request, application_id, project_view=False)
1217

    
1218
@require_http_methods(["GET", "POST"])
1219
@signed_terms_required
1220
@login_required
1221
def project_detail(request, chain_id):
1222
    return common_detail(request, chain_id)
1223

    
1224
@project_transaction_context(sync=True)
1225
def addmembers(request, chain_id, addmembers_form, ctx=None):
1226
    if addmembers_form.is_valid():
1227
        try:
1228
            chain_id = int(chain_id)
1229
            map(lambda u: enroll_member(
1230
                    chain_id,
1231
                    u,
1232
                    request_user=request.user),
1233
                addmembers_form.valid_users)
1234
        except (IOError, PermissionDenied), e:
1235
            messages.error(request, e)
1236
        except BaseException, e:
1237
            if ctx:
1238
                ctx.mark_rollback()
1239
            messages.error(request, e)
1240

    
1241
def common_detail(request, chain_or_app_id, project_view=True):
1242
    project = None
1243
    if project_view:
1244
        chain_id = chain_or_app_id
1245
        if request.method == 'POST':
1246
            addmembers_form = AddProjectMembersForm(
1247
                request.POST,
1248
                chain_id=int(chain_id),
1249
                request_user=request.user)
1250
            addmembers(request, chain_id, addmembers_form)
1251
            if addmembers_form.is_valid():
1252
                addmembers_form = AddProjectMembersForm()  # clear form data
1253
        else:
1254
            addmembers_form = AddProjectMembersForm()  # initialize form
1255

    
1256
        project, application = get_by_chain_or_404(chain_id)
1257
        if project:
1258
            members = project.projectmembership_set.select_related()
1259
            members_table = tables.ProjectMembersTable(project,
1260
                                                       members,
1261
                                                       user=request.user,
1262
                                                       prefix="members_")
1263
            RequestConfig(request, paginate={"per_page": PAGINATE_BY}
1264
                          ).configure(members_table)
1265

    
1266
        else:
1267
            members_table = None
1268

    
1269
    else: # is application
1270
        application_id = chain_or_app_id
1271
        application = get_object_or_404(ProjectApplication, pk=application_id)
1272
        members_table = None
1273
        addmembers_form = None
1274

    
1275
    modifications_table = None
1276

    
1277
    user = request.user
1278
    is_project_admin = user.is_project_admin(application_id=application.id)
1279
    is_owner = user.owns_application(application)
1280
    if not (is_owner or is_project_admin) and not project_view:
1281
        m = _(astakos_messages.NOT_ALLOWED)
1282
        raise PermissionDenied(m)
1283

    
1284
    if (not (is_owner or is_project_admin) and project_view and
1285
        not user.non_owner_can_view(project)):
1286
        m = _(astakos_messages.NOT_ALLOWED)
1287
        raise PermissionDenied(m)
1288

    
1289
    following_applications = list(application.pending_modifications())
1290
    following_applications.reverse()
1291
    modifications_table = (
1292
        tables.ProjectModificationApplicationsTable(following_applications,
1293
                                                    user=request.user,
1294
                                                    prefix="modifications_"))
1295

    
1296
    mem_display = user.membership_display(project) if project else None
1297
    can_join_req = can_join_request(project, user) if project else False
1298
    can_leave_req = can_leave_request(project, user) if project else False
1299

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

    
1317
@require_http_methods(["GET", "POST"])
1318
@signed_terms_required
1319
@login_required
1320
def project_search(request):
1321
    q = request.GET.get('q', '')
1322
    form = ProjectSearchForm()
1323
    q = q.strip()
1324

    
1325
    if request.method == "POST":
1326
        form = ProjectSearchForm(request.POST)
1327
        if form.is_valid():
1328
            q = form.cleaned_data['q'].strip()
1329
        else:
1330
            q = None
1331

    
1332
    if q is None:
1333
        projects = ProjectApplication.objects.none()
1334
    else:
1335
        accepted_projects = request.user.projectmembership_set.filter(
1336
            ~Q(acceptance_date__isnull=True)).values_list('project', flat=True)
1337
        projects = ProjectApplication.objects.search_by_name(q)
1338
        projects = projects.filter(~Q(project__last_approval_date__isnull=True))
1339
        projects = projects.exclude(project__in=accepted_projects)
1340

    
1341
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1342
                                                prefix="my_projects_")
1343
    if request.method == "POST":
1344
        table.caption = _('SEARCH RESULTS')
1345
    else:
1346
        table.caption = _('ALL PROJECTS')
1347

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

    
1350
    return object_list(
1351
        request,
1352
        projects,
1353
        template_name='im/projects/project_list.html',
1354
        extra_context={
1355
          'form': form,
1356
          'is_search': True,
1357
          'q': q,
1358
          'table': table
1359
        })
1360

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

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

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

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

    
1418
@require_http_methods(["POST"])
1419
@signed_terms_required
1420
@login_required
1421
@project_transaction_context()
1422
def project_cancel(request, chain_id, ctx=None):
1423
    next = request.GET.get('next')
1424
    if not next:
1425
        next = reverse('astakos.im.views.project_list')
1426

    
1427
    try:
1428
        chain_id = int(chain_id)
1429
        cancel_membership(chain_id, request.user)
1430
        m = _(astakos_messages.USER_REQUEST_CANCELLED)
1431
        messages.success(request, m)
1432
    except (IOError, PermissionDenied), e:
1433
        messages.error(request, e)
1434
    except PendingMembershipError as e:
1435
        raise RetryException()
1436
    except BaseException, e:
1437
        logger.exception(e)
1438
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1439
        if ctx:
1440
            ctx.mark_rollback()
1441

    
1442
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1443
    return redirect(next)
1444

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

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

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

    
1517
@require_http_methods(["POST", "GET"])
1518
@signed_terms_required
1519
@login_required
1520
@project_transaction_context(sync=True)
1521
def project_app_approve(request, application_id, ctx=None):
1522

    
1523
    if not request.user.is_project_admin():
1524
        m = _(astakos_messages.NOT_ALLOWED)
1525
        raise PermissionDenied(m)
1526

    
1527
    try:
1528
        app = ProjectApplication.objects.get(id=application_id)
1529
    except ProjectApplication.DoesNotExist:
1530
        raise Http404
1531

    
1532
    approve_application(application_id)
1533
    chain_id = get_related_project_id(application_id)
1534
    return redirect(reverse('project_detail', args=(chain_id,)))
1535

    
1536
@require_http_methods(["POST", "GET"])
1537
@signed_terms_required
1538
@login_required
1539
@project_transaction_context()
1540
def project_app_deny(request, application_id, ctx=None):
1541

    
1542
    if not request.user.is_project_admin():
1543
        m = _(astakos_messages.NOT_ALLOWED)
1544
        raise PermissionDenied(m)
1545

    
1546
    try:
1547
        app = ProjectApplication.objects.get(id=application_id)
1548
    except ProjectApplication.DoesNotExist:
1549
        raise Http404
1550

    
1551
    deny_application(application_id)
1552
    return redirect(reverse('project_list'))
1553

    
1554
@require_http_methods(["POST", "GET"])
1555
@signed_terms_required
1556
@login_required
1557
@project_transaction_context()
1558
def project_app_dismiss(request, application_id, ctx=None):
1559
    try:
1560
        app = ProjectApplication.objects.get(id=application_id)
1561
    except ProjectApplication.DoesNotExist:
1562
        raise Http404
1563

    
1564
    if not request.user.owns_application(app):
1565
        m = _(astakos_messages.NOT_ALLOWED)
1566
        raise PermissionDenied(m)
1567

    
1568
    # XXX: dismiss application also does authorization
1569
    dismiss_application(application_id, request_user=request.user)
1570

    
1571
    chain_id = None
1572
    chain_id = get_related_project_id(application_id)
1573
    if chain_id:
1574
        next = reverse('project_detail', args=(chain_id,))
1575
    else:
1576
        next = reverse('project_list')
1577
    return redirect(next)
1578

    
1579

    
1580
def landing(request):
1581
    return render_response(
1582
        'im/landing.html',
1583
        context_instance=get_context(request))
1584

    
1585

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