Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 39b2cb50

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
    ProjectApplication, ProjectMembership, Project)
84
from astakos.im.util import (
85
    get_context, prepare_response, get_query, restrict_next)
86
from astakos.im.forms import (
87
    LoginForm, InvitationForm,
88
    FeedbackForm, SignApprovalTermsForm,
89
    EmailChangeForm,
90
    ProjectApplicationForm, ProjectSortForm,
91
    AddProjectMembersForm, ProjectSearchForm,
92
    ProjectMembersSortForm)
93
from astakos.im.forms import ExtendedProfileForm as ProfileForm
94
from astakos.im.functions import (
95
    send_feedback, SendMailError,
96
    logout as auth_logout,
97
    activate as activate_func,
98
    invite as invite_func,
99
    send_activation as send_activation_func,
100
    SendNotificationError,
101
    reached_pending_application_limit,
102
    accept_membership, reject_membership, remove_membership, cancel_membership,
103
    leave_project, join_project, enroll_member, can_join_request, can_leave_request,
104
    get_related_project_id, get_by_chain_or_404,
105
    approve_application, deny_application,
106
    cancel_application, dismiss_application)
107
from astakos.im.settings import (
108
    COOKIE_DOMAIN, LOGOUT_NEXT,
109
    LOGGING_LEVEL, PAGINATE_BY,
110
    RESOURCES_PRESENTATION_DATA, PAGINATE_BY_ALL,
111
    ACTIVATION_REDIRECT_URL,
112
    MODERATION_ENABLED)
113
from astakos.im.api import get_services_dict
114
from astakos.im import settings as astakos_settings
115
from astakos.im.api.callpoint import AstakosCallpoint
116
from astakos.im import auth_providers as auth
117
from synnefo.lib.db.transaction import commit_on_success_strict
118
from astakos.im.ctx import ExceptionHandler
119

    
120
logger = logging.getLogger(__name__)
121

    
122
callpoint = AstakosCallpoint()
123

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

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

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

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

    
158

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

    
173

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

    
189

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

    
196
    def decorator(func):
197
        @wraps(func)
198
        def wrapper(request, *args, **kwargs):
199
            if request.user.is_authenticated():
200
                missing = request.user.missing_required_providers()
201
                if missing:
202
                    for provider in missing:
203
                        messages.error(request,
204
                                       provider.get_required_msg)
205
                    if not allow_access:
206
                        return HttpResponseRedirect(reverse('edit_profile'))
207
            return func(request, *args, **kwargs)
208
        return wrapper
209
    return decorator
210

    
211

    
212
def valid_astakos_user_required(func):
213
    return signed_terms_required(required_auth_methods_assigned()(login_required(func)))
214

    
215

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

222
    **Arguments**
223

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

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

232
    ``extra_context``
233
        An dictionary of variables to add to the template context.
234

235
    **Template:**
236

237
    im/profile.html or im/login.html or ``template_name`` keyword argument.
238

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

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

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

    
255

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

    
268

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

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

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

283
    If the user isn't logged in, redirects to settings.LOGIN_URL.
284

285
    **Arguments**
286

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

291
    ``extra_context``
292
        An dictionary of variables to add to the template context.
293

294
    **Template:**
295

296
    im/invitations.html or ``template_name`` keyword argument.
297

298
    **Settings:**
299

300
    The view expectes the following settings are defined:
301

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

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

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

    
346

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

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

359
    If the user isn't logged in, redirects to settings.LOGIN_URL.
360

361
    **Arguments**
362

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

367
    ``extra_context``
368
        An dictionary of variables to add to the template context.
369

370
    **Template:**
371

372
    im/profile.html or ``template_name`` keyword argument.
373

374
    **Settings:**
375

376
    The view expectes the following settings are defined:
377

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

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

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

    
420
    # existing providers
421
    user_providers = request.user.get_enabled_auth_providers()
422
    user_disabled_providers = request.user.get_disabled_auth_providers()
423

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

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

    
436

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

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

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

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

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

456
    **Arguments**
457

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

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

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

468
    **Template:**
469

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

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

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

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

    
492
        provider = pending.provider
493
        instance = pending.get_user_instance()
494
        get_unverified = AstakosUserAuthProvider.objects.unverified
495
        unverified = get_unverified(pending.provider,
496
                                    identifier=pending.third_party_identifier)
497

    
498
        if unverified and request.method == 'GET':
499
            messages.warning(request, unverified.get_pending_registration_msg)
500
            if unverified.user.activation_sent:
501
                messages.warning(request,
502
                                 unverified.get_pending_resend_activation_msg)
503
            else:
504
                messages.warning(request,
505
                                 unverified.get_pending_moderation_msg)
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

    
515
    if request.method == 'POST':
516
        if form.is_valid():
517
            user = form.save(commit=False)
518

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

    
523
            try:
524
                form.store_user(user, request)
525

    
526
                result = backend.handle_activation(user)
527
                status = messages.SUCCESS
528
                message = result.message
529

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

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

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

    
550
            except SendMailError, 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(allow_access=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
                message = e.message
614
                messages.error(request, message)
615
            else:
616
                message = _(astakos_messages.FEEDBACK_SENT)
617
                messages.success(request, message)
618
            return HttpResponseRedirect(reverse('feedback'))
619
    return render_response(template_name,
620
                           feedback_form=form,
621
                           context_instance=get_context(request, extra_context))
622

    
623

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

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

    
645
    if next:
646
        response['Location'] = next
647
        response.status_code = 302
648
    elif LOGOUT_NEXT:
649
        response['Location'] = LOGOUT_NEXT
650
        response.status_code = 301
651
    else:
652
        last_provider = request.COOKIES.get('astakos_last_login_method', 'local')
653
        provider = auth.get_provider(last_provider)
654
        message = provider.get_logout_success_msg
655
        extra = provider.get_logout_success_extra_msg
656
        if extra:
657
            message += "<br />"  + extra
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
    if not user.activation_sent:
688
        provider = user.get_auth_provider()
689
        message = user.get_inactive_message(provider.module)
690
        messages.error(request, message)
691
        return HttpResponseRedirect(reverse('index'))
692

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

    
714

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

    
731
    if not term:
732
        messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
733
        return HttpResponseRedirect(reverse('index'))
734
    try:
735
        f = open(term.location, 'r')
736
    except IOError:
737
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
738
        return render_response(
739
            template_name, context_instance=get_context(request, extra_context))
740

    
741
    terms = f.read()
742

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

    
767

    
768
@require_http_methods(["GET", "POST"])
769
@transaction.commit_manually
770
def change_email(request, activation_key=None,
771
                 email_template_name='registration/email_change_email.txt',
772
                 form_template_name='registration/email_change_form.html',
773
                 confirm_template_name='registration/email_change_done.html',
774
                 extra_context=None):
775
    extra_context = extra_context or {}
776

    
777

    
778
    if not astakos_settings.EMAILCHANGE_ENABLED:
779
        raise PermissionDenied
780

    
781
    if activation_key:
782
        try:
783
            user = EmailChange.objects.change_email(activation_key)
784
            if request.user.is_authenticated() and request.user == user or not \
785
                    request.user.is_authenticated():
786
                msg = _(astakos_messages.EMAIL_CHANGED)
787
                messages.success(request, msg)
788
                transaction.commit()
789
                return HttpResponseRedirect(reverse('edit_profile'))
790
        except ValueError, e:
791
            messages.error(request, e)
792
            transaction.rollback()
793
            return HttpResponseRedirect(reverse('index'))
794

    
795
        return render_response(confirm_template_name,
796
                               modified_user=user if 'user' in locals() \
797
                               else None, context_instance=get_context(request,
798
                                                            extra_context))
799

    
800
    if not request.user.is_authenticated():
801
        path = quote(request.get_full_path())
802
        url = request.build_absolute_uri(reverse('index'))
803
        return HttpResponseRedirect(url + '?next=' + path)
804

    
805
    # clean up expired email changes
806
    if request.user.email_change_is_pending():
807
        change = request.user.emailchanges.get()
808
        if change.activation_key_expired():
809
            change.delete()
810
            transaction.commit()
811
            return HttpResponseRedirect(reverse('email_change'))
812

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

    
828
    if request.user.email_change_is_pending():
829
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
830

    
831
    return render_response(
832
        form_template_name,
833
        form=form,
834
        context_instance=get_context(request, extra_context)
835
    )
836

    
837

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

    
840
    if request.user.is_authenticated():
841
        return HttpResponseRedirect(reverse('edit_profile'))
842

    
843
    extra_context = extra_context or {}
844
    try:
845
        u = AstakosUser.objects.get(id=user_id)
846
    except AstakosUser.DoesNotExist:
847
        messages.error(request, _(astakos_messages.ACCOUNT_UNKNOWN))
848
    else:
849
        if not u.activation_sent and astakos_settings.MODERATION_ENABLED:
850
            raise PermissionDenied
851
        try:
852
            send_activation_func(u)
853
            msg = _(astakos_messages.ACTIVATION_SENT)
854
            messages.success(request, msg)
855
        except SendMailError, e:
856
            messages.error(request, e)
857

    
858
    return HttpResponseRedirect(reverse('index'))
859

    
860

    
861
@require_http_methods(["GET"])
862
@valid_astakos_user_required
863
def resource_usage(request):
864

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

    
886
    def pluralize(entry):
887
        entry['plural'] = engine.plural(entry.get('name'))
888
        return entry
889

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

    
901
    if request.REQUEST.get('json', None):
902
        return HttpResponse(json.dumps(backenddata),
903
                            mimetype="application/json")
904

    
905
    return render_response('im/resource_usage.html',
906
                           context_instance=get_context(request),
907
                           resource_usage=backenddata,
908
                           usage_update_interval=astakos_settings.USAGE_UPDATE_INTERVAL,
909
                           result=result)
910

    
911
# TODO: action only on POST and user should confirm the removal
912
@require_http_methods(["GET", "POST"])
913
@valid_astakos_user_required
914
def remove_auth_provider(request, pk):
915
    try:
916
        provider = request.user.auth_providers.get(pk=int(pk)).settings
917
    except AstakosUserAuthProvider.DoesNotExist:
918
        raise Http404
919

    
920
    if provider.get_remove_policy:
921
        messages.success(request, provider.get_removed_msg)
922
        provider.remove_from_user()
923
        return HttpResponseRedirect(reverse('edit_profile'))
924
    else:
925
        raise PermissionDenied
926

    
927

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

    
933

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

    
945
    if extra_context is None: extra_context = {}
946
    if login_required and not request.user.is_authenticated():
947
        return redirect_to_login(request.path)
948
    try:
949

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

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

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

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

    
1008
        if request.method == 'POST':
1009
            form = form_class(request.POST, request.FILES, instance=obj)
1010
            if form.is_valid():
1011
                verify = request.GET.get('verify')
1012
                edit = request.GET.get('edit')
1013
                if verify == '1':
1014
                    extra_context['show_form'] = False
1015
                    extra_context['form_data'] = form.cleaned_data
1016
                elif edit == '1':
1017
                    extra_context['show_form'] = True
1018
                else:
1019
                    obj = form.save()
1020
                    if not msg:
1021
                        msg = _("The %(verbose_name)s was created successfully.")
1022
                    msg = msg % model._meta.__dict__
1023
                    messages.success(request, msg, fail_silently=True)
1024
                    response = redirect(post_save_redirect, obj)
1025
        else:
1026
            form = form_class(instance=obj)
1027
    except (IOError, PermissionDenied), e:
1028
        messages.error(request, e)
1029
        return None
1030
    else:
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
@valid_astakos_user_required
1047
def project_add(request):
1048

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

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

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

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

    
1087

    
1088
    extra_context = {
1089
        'resource_catalog':resource_catalog,
1090
        'resource_groups':resource_groups,
1091
        'show_form':True,
1092
        'details_fields':details_fields,
1093
        'membership_fields':membership_fields}
1094

    
1095
    response = None
1096
    with ExceptionHandler(request):
1097
        response = _create_object(
1098
            request,
1099
            template_name='im/projects/projectapplication_form.html',
1100
            extra_context=extra_context,
1101
            post_save_redirect=reverse('project_list'),
1102
            form_class=ProjectApplicationForm,
1103
            msg=_("The %(verbose_name)s has been received and "
1104
                  "is under consideration."),
1105
            )
1106

    
1107
    if response is not None:
1108
        return response
1109

    
1110
    next = reverse('astakos.im.views.project_list')
1111
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1112
    return redirect(next)
1113

    
1114

    
1115
@require_http_methods(["GET"])
1116
@valid_astakos_user_required
1117
def project_list(request):
1118
    projects = ProjectApplication.objects.user_accessible_projects(request.user).select_related()
1119
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1120
                                                prefix="my_projects_")
1121
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1122

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

    
1132

    
1133
@require_http_methods(["POST"])
1134
@valid_astakos_user_required
1135
def project_app_cancel(request, application_id):
1136
    next = request.GET.get('next')
1137
    chain_id = None
1138

    
1139
    with ExceptionHandler(request):
1140
        chain_id = _project_app_cancel(request, application_id)
1141

    
1142
    if not next:
1143
        if chain_id:
1144
            next = reverse('astakos.im.views.project_detail', args=(chain_id,))
1145
        else:
1146
            next = reverse('astakos.im.views.project_list')
1147

    
1148
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1149
    return redirect(next)
1150

    
1151
@commit_on_success_strict()
1152
def _project_app_cancel(request, application_id):
1153
    chain_id = None
1154
    try:
1155
        application_id = int(application_id)
1156
        chain_id = get_related_project_id(application_id)
1157
        cancel_application(application_id, request.user)
1158
    except (IOError, PermissionDenied), e:
1159
        messages.error(request, e)
1160
    else:
1161
        msg = _(astakos_messages.APPLICATION_CANCELLED)
1162
        messages.success(request, msg)
1163
        return chain_id
1164

    
1165

    
1166
@require_http_methods(["GET", "POST"])
1167
@valid_astakos_user_required
1168
def project_modify(request, application_id):
1169

    
1170
    try:
1171
        app = ProjectApplication.objects.get(id=application_id)
1172
    except ProjectApplication.DoesNotExist:
1173
        raise Http404
1174

    
1175
    user = request.user
1176
    if not (user.owns_application(app) or user.is_project_admin(app.id)):
1177
        m = _(astakos_messages.NOT_ALLOWED)
1178
        raise PermissionDenied(m)
1179

    
1180
    owner_id = app.owner_id
1181
    reached, limit = reached_pending_application_limit(owner_id, app)
1182
    if not user.is_project_admin() and reached:
1183
        m = _(astakos_messages.PENDING_APPLICATION_LIMIT_MODIFY) % limit
1184
        messages.error(request, m)
1185
        next = reverse('astakos.im.views.project_list')
1186
        next = restrict_next(next, domain=COOKIE_DOMAIN)
1187
        return redirect(next)
1188

    
1189
    resource_groups = RESOURCES_PRESENTATION_DATA.get('groups', {})
1190
    resource_catalog = ()
1191
    result = callpoint.list_resources()
1192
    details_fields = [
1193
        "name", "homepage", "description","start_date","end_date", "comments"]
1194
    membership_fields =[
1195
        "member_join_policy", "member_leave_policy", "limit_on_members_number"]
1196
    if not result.is_success:
1197
        messages.error(
1198
            request,
1199
            'Unable to retrieve system resources: %s' % result.reason
1200
    )
1201
    else:
1202
        resource_catalog = [
1203
            (g, filter(lambda r: r.get('group', '') == g, result.data)) \
1204
                for g in resource_groups]
1205
    extra_context = {
1206
        'resource_catalog':resource_catalog,
1207
        'resource_groups':resource_groups,
1208
        'show_form':True,
1209
        'details_fields':details_fields,
1210
        'update_form': True,
1211
        'membership_fields':membership_fields}
1212

    
1213
    response = None
1214
    with ExceptionHandler(request):
1215
        response =_update_object(
1216
            request,
1217
            object_id=application_id,
1218
            template_name='im/projects/projectapplication_form.html',
1219
            extra_context=extra_context, post_save_redirect=reverse('project_list'),
1220
            form_class=ProjectApplicationForm,
1221
            msg = _("The %(verbose_name)s has been received and "
1222
                    "is under consideration."),
1223
            )
1224

    
1225
    if response is not None:
1226
        return response
1227

    
1228
    next = reverse('astakos.im.views.project_list')
1229
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1230
    return redirect(next)
1231

    
1232
@require_http_methods(["GET", "POST"])
1233
@valid_astakos_user_required
1234
def project_app(request, application_id):
1235
    return common_detail(request, application_id, project_view=False)
1236

    
1237
@require_http_methods(["GET", "POST"])
1238
@valid_astakos_user_required
1239
def project_detail(request, chain_id):
1240
    return common_detail(request, chain_id)
1241

    
1242
@commit_on_success_strict()
1243
def addmembers(request, chain_id, addmembers_form):
1244
    if addmembers_form.is_valid():
1245
        try:
1246
            chain_id = int(chain_id)
1247
            map(lambda u: enroll_member(
1248
                    chain_id,
1249
                    u,
1250
                    request_user=request.user),
1251
                addmembers_form.valid_users)
1252
        except (IOError, PermissionDenied), e:
1253
            messages.error(request, e)
1254

    
1255
def common_detail(request, chain_or_app_id, project_view=True):
1256
    project = None
1257
    if project_view:
1258
        chain_id = chain_or_app_id
1259
        if request.method == 'POST':
1260
            addmembers_form = AddProjectMembersForm(
1261
                request.POST,
1262
                chain_id=int(chain_id),
1263
                request_user=request.user)
1264
            with ExceptionHandler(request):
1265
                addmembers(request, chain_id, addmembers_form)
1266

    
1267
            if addmembers_form.is_valid():
1268
                addmembers_form = AddProjectMembersForm()  # clear form data
1269
        else:
1270
            addmembers_form = AddProjectMembersForm()  # initialize form
1271

    
1272
        project, application = get_by_chain_or_404(chain_id)
1273
        if project:
1274
            members = project.projectmembership_set.select_related()
1275
            members_table = tables.ProjectMembersTable(project,
1276
                                                       members,
1277
                                                       user=request.user,
1278
                                                       prefix="members_")
1279
            RequestConfig(request, paginate={"per_page": PAGINATE_BY}
1280
                          ).configure(members_table)
1281

    
1282
        else:
1283
            members_table = None
1284

    
1285
    else: # is application
1286
        application_id = chain_or_app_id
1287
        application = get_object_or_404(ProjectApplication, pk=application_id)
1288
        members_table = None
1289
        addmembers_form = None
1290

    
1291
    modifications_table = None
1292

    
1293
    user = request.user
1294
    is_project_admin = user.is_project_admin(application_id=application.id)
1295
    is_owner = user.owns_application(application)
1296
    if not (is_owner or is_project_admin) and not project_view:
1297
        m = _(astakos_messages.NOT_ALLOWED)
1298
        raise PermissionDenied(m)
1299

    
1300
    if (not (is_owner or is_project_admin) and project_view and
1301
        not user.non_owner_can_view(project)):
1302
        m = _(astakos_messages.NOT_ALLOWED)
1303
        raise PermissionDenied(m)
1304

    
1305
    following_applications = list(application.pending_modifications())
1306
    following_applications.reverse()
1307
    modifications_table = (
1308
        tables.ProjectModificationApplicationsTable(following_applications,
1309
                                                    user=request.user,
1310
                                                    prefix="modifications_"))
1311

    
1312
    mem_display = user.membership_display(project) if project else None
1313
    can_join_req = can_join_request(project, user) if project else False
1314
    can_leave_req = can_leave_request(project, user) if project else False
1315

    
1316
    return object_detail(
1317
        request,
1318
        queryset=ProjectApplication.objects.select_related(),
1319
        object_id=application.id,
1320
        template_name='im/projects/project_detail.html',
1321
        extra_context={
1322
            'project_view': project_view,
1323
            'addmembers_form':addmembers_form,
1324
            'members_table': members_table,
1325
            'owner_mode': is_owner,
1326
            'admin_mode': is_project_admin,
1327
            'modifications_table': modifications_table,
1328
            'mem_display': mem_display,
1329
            'can_join_request': can_join_req,
1330
            'can_leave_request': can_leave_req,
1331
            })
1332

    
1333
@require_http_methods(["GET", "POST"])
1334
@valid_astakos_user_required
1335
def project_search(request):
1336
    q = request.GET.get('q', '')
1337
    form = ProjectSearchForm()
1338
    q = q.strip()
1339

    
1340
    if request.method == "POST":
1341
        form = ProjectSearchForm(request.POST)
1342
        if form.is_valid():
1343
            q = form.cleaned_data['q'].strip()
1344
        else:
1345
            q = None
1346

    
1347
    if q is None:
1348
        projects = ProjectApplication.objects.none()
1349
    else:
1350
        accepted_projects = request.user.projectmembership_set.filter(
1351
            ~Q(acceptance_date__isnull=True)).values_list('project', flat=True)
1352
        projects = ProjectApplication.objects.search_by_name(q)
1353
        projects = projects.filter(~Q(project__last_approval_date__isnull=True))
1354
        projects = projects.exclude(project__in=accepted_projects)
1355

    
1356
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1357
                                                prefix="my_projects_")
1358
    if request.method == "POST":
1359
        table.caption = _('SEARCH RESULTS')
1360
    else:
1361
        table.caption = _('ALL PROJECTS')
1362

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

    
1365
    return object_list(
1366
        request,
1367
        projects,
1368
        template_name='im/projects/project_list.html',
1369
        extra_context={
1370
          'form': form,
1371
          'is_search': True,
1372
          'q': q,
1373
          'table': table
1374
        })
1375

    
1376
@require_http_methods(["POST"])
1377
@valid_astakos_user_required
1378
def project_join(request, chain_id):
1379
    next = request.GET.get('next')
1380
    if not next:
1381
        next = reverse('astakos.im.views.project_detail',
1382
                       args=(chain_id,))
1383

    
1384
    with ExceptionHandler(request):
1385
        _project_join(request, chain_id)
1386

    
1387

    
1388
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1389
    return redirect(next)
1390

    
1391

    
1392
@commit_on_success_strict()
1393
def _project_join(request, chain_id):
1394
    try:
1395
        chain_id = int(chain_id)
1396
        auto_accepted = join_project(chain_id, request.user)
1397
        if auto_accepted:
1398
            m = _(astakos_messages.USER_JOINED_PROJECT)
1399
        else:
1400
            m = _(astakos_messages.USER_JOIN_REQUEST_SUBMITTED)
1401
        messages.success(request, m)
1402
    except (IOError, PermissionDenied), e:
1403
        messages.error(request, e)
1404

    
1405

    
1406
@require_http_methods(["POST"])
1407
@valid_astakos_user_required
1408
def project_leave(request, chain_id):
1409
    next = request.GET.get('next')
1410
    if not next:
1411
        next = reverse('astakos.im.views.project_list')
1412

    
1413
    with ExceptionHandler(request):
1414
        _project_leave(request, chain_id)
1415

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

    
1419

    
1420
@commit_on_success_strict()
1421
def _project_leave(request, chain_id):
1422
    try:
1423
        chain_id = int(chain_id)
1424
        auto_accepted = leave_project(chain_id, request.user)
1425
        if auto_accepted:
1426
            m = _(astakos_messages.USER_LEFT_PROJECT)
1427
        else:
1428
            m = _(astakos_messages.USER_LEAVE_REQUEST_SUBMITTED)
1429
        messages.success(request, m)
1430
    except (IOError, PermissionDenied), e:
1431
        messages.error(request, e)
1432

    
1433

    
1434
@require_http_methods(["POST"])
1435
@valid_astakos_user_required
1436
def project_cancel(request, chain_id):
1437
    next = request.GET.get('next')
1438
    if not next:
1439
        next = reverse('astakos.im.views.project_list')
1440

    
1441
    with ExceptionHandler(request):
1442
        _project_cancel(request, chain_id)
1443

    
1444
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1445
    return redirect(next)
1446

    
1447

    
1448
@commit_on_success_strict()
1449
def _project_cancel(request, chain_id):
1450
    try:
1451
        chain_id = int(chain_id)
1452
        cancel_membership(chain_id, request.user)
1453
        m = _(astakos_messages.USER_REQUEST_CANCELLED)
1454
        messages.success(request, m)
1455
    except (IOError, PermissionDenied), e:
1456
        messages.error(request, e)
1457

    
1458

    
1459
@require_http_methods(["POST"])
1460
@valid_astakos_user_required
1461
def project_accept_member(request, chain_id, user_id):
1462

    
1463
    with ExceptionHandler(request):
1464
        _project_accept_member(request, chain_id, user_id)
1465

    
1466
    return redirect(reverse('project_detail', args=(chain_id,)))
1467

    
1468

    
1469
@commit_on_success_strict()
1470
def _project_accept_member(request, chain_id, user_id):
1471
    try:
1472
        chain_id = int(chain_id)
1473
        user_id = int(user_id)
1474
        m = accept_membership(chain_id, user_id, request.user)
1475
    except (IOError, PermissionDenied), e:
1476
        messages.error(request, e)
1477
    else:
1478
        email = escape(m.person.email)
1479
        msg = _(astakos_messages.USER_MEMBERSHIP_ACCEPTED) % email
1480
        messages.success(request, msg)
1481

    
1482

    
1483
@require_http_methods(["POST"])
1484
@valid_astakos_user_required
1485
def project_remove_member(request, chain_id, user_id):
1486

    
1487
    with ExceptionHandler(request):
1488
        _project_remove_member(request, chain_id, user_id)
1489

    
1490
    return redirect(reverse('project_detail', args=(chain_id,)))
1491

    
1492

    
1493
@commit_on_success_strict()
1494
def _project_remove_member(request, chain_id, user_id):
1495
    try:
1496
        chain_id = int(chain_id)
1497
        user_id = int(user_id)
1498
        m = remove_membership(chain_id, user_id, request.user)
1499
    except (IOError, PermissionDenied), e:
1500
        messages.error(request, e)
1501
    else:
1502
        email = escape(m.person.email)
1503
        msg = _(astakos_messages.USER_MEMBERSHIP_REMOVED) % email
1504
        messages.success(request, msg)
1505

    
1506

    
1507
@require_http_methods(["POST"])
1508
@valid_astakos_user_required
1509
def project_reject_member(request, chain_id, user_id):
1510

    
1511
    with ExceptionHandler(request):
1512
        _project_reject_member(request, chain_id, user_id)
1513

    
1514
    return redirect(reverse('project_detail', args=(chain_id,)))
1515

    
1516

    
1517
@commit_on_success_strict()
1518
def _project_reject_member(request, chain_id, user_id):
1519
    try:
1520
        chain_id = int(chain_id)
1521
        user_id = int(user_id)
1522
        m = reject_membership(chain_id, user_id, request.user)
1523
    except (IOError, PermissionDenied), e:
1524
        messages.error(request, e)
1525
    else:
1526
        email = escape(m.person.email)
1527
        msg = _(astakos_messages.USER_MEMBERSHIP_REJECTED) % email
1528
        messages.success(request, msg)
1529

    
1530

    
1531
@require_http_methods(["POST"])
1532
@signed_terms_required
1533
@login_required
1534
def project_app_approve(request, application_id):
1535

    
1536
    if not request.user.is_project_admin():
1537
        m = _(astakos_messages.NOT_ALLOWED)
1538
        raise PermissionDenied(m)
1539

    
1540
    try:
1541
        app = ProjectApplication.objects.get(id=application_id)
1542
    except ProjectApplication.DoesNotExist:
1543
        raise Http404
1544

    
1545
    with ExceptionHandler(request):
1546
        _project_app_approve(request, application_id)
1547

    
1548
    chain_id = get_related_project_id(application_id)
1549
    return redirect(reverse('project_detail', args=(chain_id,)))
1550

    
1551

    
1552
@commit_on_success_strict()
1553
def _project_app_approve(request, application_id):
1554
    approve_application(application_id)
1555

    
1556

    
1557
@require_http_methods(["POST"])
1558
@signed_terms_required
1559
@login_required
1560
def project_app_deny(request, application_id):
1561

    
1562
    reason = request.POST.get('reason', None)
1563
    if not reason:
1564
        reason = None
1565

    
1566
    if not request.user.is_project_admin():
1567
        m = _(astakos_messages.NOT_ALLOWED)
1568
        raise PermissionDenied(m)
1569

    
1570
    try:
1571
        app = ProjectApplication.objects.get(id=application_id)
1572
    except ProjectApplication.DoesNotExist:
1573
        raise Http404
1574

    
1575
    with ExceptionHandler(request):
1576
        _project_app_deny(request, application_id, reason)
1577

    
1578
    return redirect(reverse('project_list'))
1579

    
1580

    
1581
@commit_on_success_strict()
1582
def _project_app_deny(request, application_id, reason):
1583
    deny_application(application_id, reason=reason)
1584

    
1585

    
1586
@require_http_methods(["POST"])
1587
@signed_terms_required
1588
@login_required
1589
def project_app_dismiss(request, application_id):
1590
    try:
1591
        app = ProjectApplication.objects.get(id=application_id)
1592
    except ProjectApplication.DoesNotExist:
1593
        raise Http404
1594

    
1595
    if not request.user.owns_application(app):
1596
        m = _(astakos_messages.NOT_ALLOWED)
1597
        raise PermissionDenied(m)
1598

    
1599
    with ExceptionHandler(request):
1600
        _project_app_dismiss(request, application_id)
1601

    
1602
    chain_id = None
1603
    chain_id = get_related_project_id(application_id)
1604
    if chain_id:
1605
        next = reverse('project_detail', args=(chain_id,))
1606
    else:
1607
        next = reverse('project_list')
1608
    return redirect(next)
1609

    
1610

    
1611
def _project_app_dismiss(request, application_id):
1612
    # XXX: dismiss application also does authorization
1613
    dismiss_application(application_id, request_user=request.user)
1614

    
1615

    
1616
@require_http_methods(["GET"])
1617
@required_auth_methods_assigned(allow_access=True)
1618
@login_required
1619
@signed_terms_required
1620
def landing(request):
1621
    return render_response(
1622
        'im/landing.html',
1623
        context_instance=get_context(request))
1624

    
1625

    
1626
def api_access(request):
1627
    return render_response(
1628
        'im/api_access.html',
1629
        context_instance=get_context(request))