Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 9770ba6c

History | View | Annotate | Download (57.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, AstakosUserAuthProvider, PendingThirdPartyUser,
82
    ProjectApplication, ProjectMembership, Project, Service)
83
from astakos.im.util import (
84
    get_context, prepare_response, get_query, restrict_next)
85
from astakos.im.forms import (
86
    LoginForm, InvitationForm,
87
    FeedbackForm, SignApprovalTermsForm,
88
    EmailChangeForm,
89
    ProjectApplicationForm, ProjectSortForm,
90
    AddProjectMembersForm, ProjectSearchForm,
91
    ProjectMembersSortForm)
92
from astakos.im.forms import ExtendedProfileForm as ProfileForm
93
from astakos.im.functions import (
94
    send_feedback, SendMailError,
95
    logout as auth_logout,
96
    activate as activate_func,
97
    invite as invite_func,
98
    send_activation as send_activation_func,
99
    SendNotificationError,
100
    qh_add_pending_app,
101
    accept_membership, reject_membership, remove_membership, cancel_membership,
102
    leave_project, join_project, enroll_member, can_join_request, can_leave_request,
103
    get_related_project_id, get_by_chain_or_404,
104
    approve_application, deny_application,
105
    cancel_application, dismiss_application)
106
from astakos.im.settings import (
107
    COOKIE_DOMAIN, LOGOUT_NEXT,
108
    LOGGING_LEVEL, PAGINATE_BY,
109
    PAGINATE_BY_ALL,
110
    ACTIVATION_REDIRECT_URL,
111
    MODERATION_ENABLED)
112
from astakos.im import presentation
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 snf_django.lib.db.transaction import commit_on_success_strict
118
from astakos.im.ctx import ExceptionHandler
119
from astakos.im import quotas
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.get_provider(provider_id)
146

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

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

    
159

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

    
174

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

    
190

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

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

    
212

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

    
216

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

223
    **Arguments**
224

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

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

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

236
    **Template:**
237

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

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

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

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

    
256

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

    
269

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

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

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

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

286
    **Arguments**
287

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

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

295
    **Template:**
296

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

299
    **Settings:**
300

301
    The view expectes the following settings are defined:
302

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

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

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

    
347

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

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

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

362
    **Arguments**
363

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

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

371
    **Template:**
372

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

375
    **Settings:**
376

377
    The view expectes the following settings are defined:
378

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

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

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

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

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

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

    
437

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

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

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

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

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

457
    **Arguments**
458

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

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

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

469
    **Template:**
470

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

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

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

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

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

    
499
        if unverified and request.method == 'GET':
500
            messages.warning(request, unverified.get_pending_registration_msg)
501
            if unverified.user.activation_sent:
502
                messages.warning(request,
503
                                 unverified.get_pending_resend_activation_msg)
504
            else:
505
                messages.warning(request,
506
                                 unverified.get_pending_moderation_msg)
507

    
508
    try:
509
        if not backend:
510
            backend = get_backend(request)
511
        form = backend.get_signup_form(provider, instance)
512
    except Exception, e:
513
        form = SimpleBackend(request).get_signup_form(provider)
514
        messages.error(request, e)
515

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

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

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

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

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

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

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

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

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

    
569

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

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

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

583
    **Arguments**
584

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

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

592
    **Template:**
593

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

596
    **Settings:**
597

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

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

    
624

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

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

    
646
    if next:
647
        response['Location'] = next
648
        response.status_code = 302
649
    elif LOGOUT_NEXT:
650
        response['Location'] = LOGOUT_NEXT
651
        response.status_code = 301
652
    else:
653
        last_provider = request.COOKIES.get('astakos_last_login_method', 'local')
654
        provider = auth.get_provider(last_provider)
655
        message = provider.get_logout_success_msg
656
        extra = provider.get_logout_success_extra_msg
657
        if extra:
658
            message += "<br />"  + extra
659
        messages.success(request, message)
660
        response['Location'] = reverse('index')
661
        response.status_code = 301
662
    return response
663

    
664

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

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

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

    
688
    if not user.activation_sent:
689
        provider = user.get_auth_provider()
690
        message = user.get_inactive_message(provider.module)
691
        messages.error(request, message)
692
        return HttpResponseRedirect(reverse('index'))
693

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

    
715

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

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

    
742
    terms = f.read()
743

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

    
768

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

    
778

    
779
    if not astakos_settings.EMAILCHANGE_ENABLED:
780
        raise PermissionDenied
781

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

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

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

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

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

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

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

    
838

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

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

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

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

    
861

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

    
866
    current_usage = quotas.get_user_quotas(request.user)
867
    current_usage = json.dumps(current_usage['system'])
868
    resource_catalog, resource_groups = _resources_catalog(request)
869
    resource_catalog = json.dumps(resource_catalog)
870
    resource_groups = json.dumps(resource_groups)
871
    resources_order = json.dumps(presentation.RESOURCES.get('resources_order'))
872

    
873
    return render_response('im/resource_usage.html',
874
                           context_instance=get_context(request),
875
                           resource_catalog=resource_catalog,
876
                           resource_groups=resource_groups,
877
                           resources_order=resources_order,
878
                           current_usage=current_usage,
879
                           token_cookie_name=astakos_settings.COOKIE_NAME,
880
                           usage_update_interval=
881
                           astakos_settings.USAGE_UPDATE_INTERVAL)
882

    
883

    
884
# TODO: action only on POST and user should confirm the removal
885
@require_http_methods(["GET", "POST"])
886
@valid_astakos_user_required
887
def remove_auth_provider(request, pk):
888
    try:
889
        provider = request.user.auth_providers.get(pk=int(pk)).settings
890
    except AstakosUserAuthProvider.DoesNotExist:
891
        raise Http404
892

    
893
    if provider.get_remove_policy:
894
        messages.success(request, provider.get_removed_msg)
895
        provider.remove_from_user()
896
        return HttpResponseRedirect(reverse('edit_profile'))
897
    else:
898
        raise PermissionDenied
899

    
900

    
901
def how_it_works(request):
902
    return render_response(
903
        'im/how_it_works.html',
904
        context_instance=get_context(request))
905

    
906

    
907
@commit_on_success_strict()
908
def _create_object(request, model=None, template_name=None,
909
        template_loader=template_loader, extra_context=None, post_save_redirect=None,
910
        login_required=False, context_processors=None, form_class=None,
911
        msg=None):
912
    """
913
    Based of django.views.generic.create_update.create_object which displays a
914
    summary page before creating the object.
915
    """
916
    response = None
917

    
918
    if extra_context is None: extra_context = {}
919
    if login_required and not request.user.is_authenticated():
920
        return redirect_to_login(request.path)
921
    try:
922

    
923
        model, form_class = get_model_and_form_class(model, form_class)
924
        extra_context['edit'] = 0
925
        if request.method == 'POST':
926
            form = form_class(request.POST, request.FILES)
927
            if form.is_valid():
928
                verify = request.GET.get('verify')
929
                edit = request.GET.get('edit')
930
                if verify == '1':
931
                    extra_context['show_form'] = False
932
                    extra_context['form_data'] = form.cleaned_data
933
                elif edit == '1':
934
                    extra_context['show_form'] = True
935
                else:
936
                    new_object = form.save()
937
                    if not msg:
938
                        msg = _("The %(verbose_name)s was created successfully.")
939
                    msg = msg % model._meta.__dict__
940
                    messages.success(request, msg, fail_silently=True)
941
                    response = redirect(post_save_redirect, new_object)
942
        else:
943
            form = form_class()
944
    except (IOError, PermissionDenied), e:
945
        messages.error(request, e)
946
        return None
947
    else:
948
        if response == None:
949
            # Create the template, context, response
950
            if not template_name:
951
                template_name = "%s/%s_form.html" %\
952
                     (model._meta.app_label, model._meta.object_name.lower())
953
            t = template_loader.get_template(template_name)
954
            c = RequestContext(request, {
955
                'form': form
956
            }, context_processors)
957
            apply_extra_context(extra_context, c)
958
            response = HttpResponse(t.render(c))
959
        return response
960

    
961
@commit_on_success_strict()
962
def _update_object(request, model=None, object_id=None, slug=None,
963
        slug_field='slug', template_name=None, template_loader=template_loader,
964
        extra_context=None, post_save_redirect=None, login_required=False,
965
        context_processors=None, template_object_name='object',
966
        form_class=None, msg=None):
967
    """
968
    Based of django.views.generic.create_update.update_object which displays a
969
    summary page before updating the object.
970
    """
971
    response = None
972

    
973
    if extra_context is None: extra_context = {}
974
    if login_required and not request.user.is_authenticated():
975
        return redirect_to_login(request.path)
976

    
977
    try:
978
        model, form_class = get_model_and_form_class(model, form_class)
979
        obj = lookup_object(model, object_id, slug, slug_field)
980

    
981
        if request.method == 'POST':
982
            form = form_class(request.POST, request.FILES, instance=obj)
983
            if form.is_valid():
984
                verify = request.GET.get('verify')
985
                edit = request.GET.get('edit')
986
                if verify == '1':
987
                    extra_context['show_form'] = False
988
                    extra_context['form_data'] = form.cleaned_data
989
                elif edit == '1':
990
                    extra_context['show_form'] = True
991
                else:
992
                    obj = form.save()
993
                    if not msg:
994
                        msg = _("The %(verbose_name)s was created successfully.")
995
                    msg = msg % model._meta.__dict__
996
                    messages.success(request, msg, fail_silently=True)
997
                    response = redirect(post_save_redirect, obj)
998
        else:
999
            form = form_class(instance=obj)
1000
    except (IOError, PermissionDenied), e:
1001
        messages.error(request, e)
1002
        return None
1003
    else:
1004
        if response == None:
1005
            if not template_name:
1006
                template_name = "%s/%s_form.html" %\
1007
                    (model._meta.app_label, model._meta.object_name.lower())
1008
            t = template_loader.get_template(template_name)
1009
            c = RequestContext(request, {
1010
                'form': form,
1011
                template_object_name: obj,
1012
            }, context_processors)
1013
            apply_extra_context(extra_context, c)
1014
            response = HttpResponse(t.render(c))
1015
            populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.attname))
1016
        return response
1017

    
1018

    
1019
def _resources_catalog(request):
1020
    """
1021
    `resource_catalog` contains a list of tuples. Each tuple contains the group
1022
    key the resource is assigned to and resources list of dicts that contain
1023
    resource information.
1024
    `resource_groups` contains information about the groups
1025
    """
1026
    # presentation data
1027
    resource_groups = presentation.RESOURCES.get('groups', {})
1028
    resource_catalog = ()
1029
    resource_keys = []
1030

    
1031
    # resources in database
1032
    result = callpoint.list_resources()
1033
    if not result.is_success:
1034
        messages.error(request, 'Unable to retrieve system resources: %s' %
1035
                                result.reason)
1036
    else:
1037
        # initialize resource_catalog to contain all group/resource information
1038
        for r in result.data:
1039
            if not r.get('group') in resource_groups:
1040
                resource_groups[r.get('group')] = {'icon': 'unknown'}
1041

    
1042
        resource_keys = [r.get('str_repr') for r in result.data]
1043
        resource_catalog = [[g, filter(lambda r: r.get('group', '') == g,
1044
                                       result.data)] for g in resource_groups]
1045

    
1046
    # order groups, also include unknown groups
1047
    groups_order = presentation.RESOURCES.get('groups_order')
1048
    for g in resource_groups.keys():
1049
        if not g in groups_order:
1050
            groups_order.append(g)
1051

    
1052
    # order resources, also include unknown resources
1053
    resources_order = presentation.RESOURCES.get('resources_order')
1054
    for r in resource_keys:
1055
        if not r in resources_order:
1056
            resources_order.append(r)
1057

    
1058
    # sort catalog groups
1059
    resource_catalog = sorted(resource_catalog,
1060
                              key=lambda g: groups_order.index(g[0]))
1061

    
1062
    # sort groups
1063
    def groupindex(g):
1064
        return groups_order.index(g[0])
1065
    resource_groups_list = sorted([(k, v) for k, v in resource_groups.items()],
1066
                                  key=groupindex)
1067
    resource_groups = OrderedDict(resource_groups_list)
1068

    
1069
    # sort resources
1070
    def resourceindex(r):
1071
        return resources_order.index(r['str_repr'])
1072
    for index, group in enumerate(resource_catalog):
1073
        resource_catalog[index][1] = sorted(resource_catalog[index][1],
1074
                                            key=resourceindex)
1075
        if len(resource_catalog[index][1]) == 0:
1076
            resource_catalog.pop(index)
1077
            for gindex, g in enumerate(resource_groups):
1078
                if g[0] == group[0]:
1079
                    resource_groups.pop(gindex)
1080

    
1081
    return resource_catalog, resource_groups
1082

    
1083

    
1084
@require_http_methods(["GET", "POST"])
1085
@valid_astakos_user_required
1086
def project_add(request):
1087
    user = request.user
1088
    if not user.is_project_admin():
1089
        ok, limit = qh_add_pending_app(user, dry_run=True)
1090
        if not ok:
1091
            m = _(astakos_messages.PENDING_APPLICATION_LIMIT_ADD) % limit
1092
            messages.error(request, m)
1093
            next = reverse('astakos.im.views.project_list')
1094
            next = restrict_next(next, domain=COOKIE_DOMAIN)
1095
            return redirect(next)
1096

    
1097
    details_fields = ["name", "homepage", "description", "start_date",
1098
                      "end_date", "comments"]
1099
    membership_fields = ["member_join_policy", "member_leave_policy",
1100
                         "limit_on_members_number"]
1101
    resource_catalog, resource_groups = _resources_catalog(request)
1102
    extra_context = {
1103
        'resource_catalog': resource_catalog,
1104
        'resource_groups': resource_groups,
1105
        'show_form': True,
1106
        'details_fields': details_fields,
1107
        'membership_fields': membership_fields}
1108

    
1109
    response = None
1110
    with ExceptionHandler(request):
1111
        response = _create_object(
1112
            request,
1113
            template_name='im/projects/projectapplication_form.html',
1114
            extra_context=extra_context,
1115
            post_save_redirect=reverse('project_list'),
1116
            form_class=ProjectApplicationForm,
1117
            msg=_("The %(verbose_name)s has been received and "
1118
                  "is under consideration."),
1119
            )
1120

    
1121
    if response is not None:
1122
        return response
1123

    
1124
    next = reverse('astakos.im.views.project_list')
1125
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1126
    return redirect(next)
1127

    
1128

    
1129
@require_http_methods(["GET"])
1130
@valid_astakos_user_required
1131
def project_list(request):
1132
    projects = ProjectApplication.objects.user_accessible_projects(request.user).select_related()
1133
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1134
                                                prefix="my_projects_")
1135
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1136

    
1137
    return object_list(
1138
        request,
1139
        projects,
1140
        template_name='im/projects/project_list.html',
1141
        extra_context={
1142
            'is_search':False,
1143
            'table': table,
1144
        })
1145

    
1146

    
1147
@require_http_methods(["POST"])
1148
@valid_astakos_user_required
1149
def project_app_cancel(request, application_id):
1150
    next = request.GET.get('next')
1151
    chain_id = None
1152

    
1153
    with ExceptionHandler(request):
1154
        chain_id = _project_app_cancel(request, application_id)
1155

    
1156
    if not next:
1157
        if chain_id:
1158
            next = reverse('astakos.im.views.project_detail', args=(chain_id,))
1159
        else:
1160
            next = reverse('astakos.im.views.project_list')
1161

    
1162
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1163
    return redirect(next)
1164

    
1165
@commit_on_success_strict()
1166
def _project_app_cancel(request, application_id):
1167
    chain_id = None
1168
    try:
1169
        application_id = int(application_id)
1170
        chain_id = get_related_project_id(application_id)
1171
        cancel_application(application_id, request.user)
1172
    except (IOError, PermissionDenied), e:
1173
        messages.error(request, e)
1174
    else:
1175
        msg = _(astakos_messages.APPLICATION_CANCELLED)
1176
        messages.success(request, msg)
1177
        return chain_id
1178

    
1179

    
1180
@require_http_methods(["GET", "POST"])
1181
@valid_astakos_user_required
1182
def project_modify(request, application_id):
1183

    
1184
    try:
1185
        app = ProjectApplication.objects.get(id=application_id)
1186
    except ProjectApplication.DoesNotExist:
1187
        raise Http404
1188

    
1189
    user = request.user
1190
    if not (user.owns_application(app) or user.is_project_admin(app.id)):
1191
        m = _(astakos_messages.NOT_ALLOWED)
1192
        raise PermissionDenied(m)
1193

    
1194
    if not user.is_project_admin():
1195
        owner = app.owner
1196
        ok, limit = qh_add_pending_app(owner, precursor=app, dry_run=True)
1197
        if not ok:
1198
            m = _(astakos_messages.PENDING_APPLICATION_LIMIT_MODIFY) % limit
1199
            messages.error(request, m)
1200
            next = reverse('astakos.im.views.project_list')
1201
            next = restrict_next(next, domain=COOKIE_DOMAIN)
1202
            return redirect(next)
1203

    
1204
    details_fields = ["name", "homepage", "description", "start_date",
1205
                      "end_date", "comments"]
1206
    membership_fields = ["member_join_policy", "member_leave_policy",
1207
                         "limit_on_members_number"]
1208
    resource_catalog, resource_groups = _resources_catalog(request)
1209
    extra_context = {
1210
        'resource_catalog': resource_catalog,
1211
        'resource_groups': resource_groups,
1212
        'show_form': True,
1213
        'details_fields': details_fields,
1214
        'update_form': True,
1215
        'membership_fields': membership_fields
1216
    }
1217

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

    
1230
    if response is not None:
1231
        return response
1232

    
1233
    next = reverse('astakos.im.views.project_list')
1234
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1235
    return redirect(next)
1236

    
1237
@require_http_methods(["GET", "POST"])
1238
@valid_astakos_user_required
1239
def project_app(request, application_id):
1240
    return common_detail(request, application_id, project_view=False)
1241

    
1242
@require_http_methods(["GET", "POST"])
1243
@valid_astakos_user_required
1244
def project_detail(request, chain_id):
1245
    return common_detail(request, chain_id)
1246

    
1247
@commit_on_success_strict()
1248
def addmembers(request, chain_id, addmembers_form):
1249
    if addmembers_form.is_valid():
1250
        try:
1251
            chain_id = int(chain_id)
1252
            map(lambda u: enroll_member(
1253
                    chain_id,
1254
                    u,
1255
                    request_user=request.user),
1256
                addmembers_form.valid_users)
1257
        except (IOError, PermissionDenied), e:
1258
            messages.error(request, e)
1259

    
1260
def common_detail(request, chain_or_app_id, project_view=True):
1261
    project = None
1262
    if project_view:
1263
        chain_id = chain_or_app_id
1264
        if request.method == 'POST':
1265
            addmembers_form = AddProjectMembersForm(
1266
                request.POST,
1267
                chain_id=int(chain_id),
1268
                request_user=request.user)
1269
            with ExceptionHandler(request):
1270
                addmembers(request, chain_id, addmembers_form)
1271

    
1272
            if addmembers_form.is_valid():
1273
                addmembers_form = AddProjectMembersForm()  # clear form data
1274
        else:
1275
            addmembers_form = AddProjectMembersForm()  # initialize form
1276

    
1277
        project, application = get_by_chain_or_404(chain_id)
1278
        if project:
1279
            members = project.projectmembership_set.select_related()
1280
            members_table = tables.ProjectMembersTable(project,
1281
                                                       members,
1282
                                                       user=request.user,
1283
                                                       prefix="members_")
1284
            RequestConfig(request, paginate={"per_page": PAGINATE_BY}
1285
                          ).configure(members_table)
1286

    
1287
        else:
1288
            members_table = None
1289

    
1290
    else: # is application
1291
        application_id = chain_or_app_id
1292
        application = get_object_or_404(ProjectApplication, pk=application_id)
1293
        members_table = None
1294
        addmembers_form = None
1295

    
1296
    modifications_table = None
1297

    
1298
    user = request.user
1299
    is_project_admin = user.is_project_admin(application_id=application.id)
1300
    is_owner = user.owns_application(application)
1301
    if not (is_owner or is_project_admin) and not project_view:
1302
        m = _(astakos_messages.NOT_ALLOWED)
1303
        raise PermissionDenied(m)
1304

    
1305
    if (not (is_owner or is_project_admin) and project_view and
1306
        not user.non_owner_can_view(project)):
1307
        m = _(astakos_messages.NOT_ALLOWED)
1308
        raise PermissionDenied(m)
1309

    
1310
    following_applications = list(application.pending_modifications())
1311
    following_applications.reverse()
1312
    modifications_table = (
1313
        tables.ProjectModificationApplicationsTable(following_applications,
1314
                                                    user=request.user,
1315
                                                    prefix="modifications_"))
1316

    
1317
    mem_display = user.membership_display(project) if project else None
1318
    can_join_req = can_join_request(project, user) if project else False
1319
    can_leave_req = can_leave_request(project, user) if project else False
1320

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

    
1338
@require_http_methods(["GET", "POST"])
1339
@valid_astakos_user_required
1340
def project_search(request):
1341
    q = request.GET.get('q', '')
1342
    form = ProjectSearchForm()
1343
    q = q.strip()
1344

    
1345
    if request.method == "POST":
1346
        form = ProjectSearchForm(request.POST)
1347
        if form.is_valid():
1348
            q = form.cleaned_data['q'].strip()
1349
        else:
1350
            q = None
1351

    
1352
    if q is None:
1353
        projects = ProjectApplication.objects.none()
1354
    else:
1355
        accepted_projects = request.user.projectmembership_set.filter(
1356
            ~Q(acceptance_date__isnull=True)).values_list('project', flat=True)
1357
        projects = ProjectApplication.objects.search_by_name(q)
1358
        projects = projects.filter(~Q(project__last_approval_date__isnull=True))
1359
        projects = projects.exclude(project__in=accepted_projects)
1360

    
1361
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1362
                                                prefix="my_projects_")
1363
    if request.method == "POST":
1364
        table.caption = _('SEARCH RESULTS')
1365
    else:
1366
        table.caption = _('ALL PROJECTS')
1367

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

    
1370
    return object_list(
1371
        request,
1372
        projects,
1373
        template_name='im/projects/project_list.html',
1374
        extra_context={
1375
          'form': form,
1376
          'is_search': True,
1377
          'q': q,
1378
          'table': table
1379
        })
1380

    
1381
@require_http_methods(["POST"])
1382
@valid_astakos_user_required
1383
def project_join(request, chain_id):
1384
    next = request.GET.get('next')
1385
    if not next:
1386
        next = reverse('astakos.im.views.project_detail',
1387
                       args=(chain_id,))
1388

    
1389
    with ExceptionHandler(request):
1390
        _project_join(request, chain_id)
1391

    
1392

    
1393
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1394
    return redirect(next)
1395

    
1396

    
1397
@commit_on_success_strict()
1398
def _project_join(request, chain_id):
1399
    try:
1400
        chain_id = int(chain_id)
1401
        auto_accepted = join_project(chain_id, request.user)
1402
        if auto_accepted:
1403
            m = _(astakos_messages.USER_JOINED_PROJECT)
1404
        else:
1405
            m = _(astakos_messages.USER_JOIN_REQUEST_SUBMITTED)
1406
        messages.success(request, m)
1407
    except (IOError, PermissionDenied), e:
1408
        messages.error(request, e)
1409

    
1410

    
1411
@require_http_methods(["POST"])
1412
@valid_astakos_user_required
1413
def project_leave(request, chain_id):
1414
    next = request.GET.get('next')
1415
    if not next:
1416
        next = reverse('astakos.im.views.project_list')
1417

    
1418
    with ExceptionHandler(request):
1419
        _project_leave(request, chain_id)
1420

    
1421
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1422
    return redirect(next)
1423

    
1424

    
1425
@commit_on_success_strict()
1426
def _project_leave(request, chain_id):
1427
    try:
1428
        chain_id = int(chain_id)
1429
        auto_accepted = leave_project(chain_id, request.user)
1430
        if auto_accepted:
1431
            m = _(astakos_messages.USER_LEFT_PROJECT)
1432
        else:
1433
            m = _(astakos_messages.USER_LEAVE_REQUEST_SUBMITTED)
1434
        messages.success(request, m)
1435
    except (IOError, PermissionDenied), e:
1436
        messages.error(request, e)
1437

    
1438

    
1439
@require_http_methods(["POST"])
1440
@valid_astakos_user_required
1441
def project_cancel(request, chain_id):
1442
    next = request.GET.get('next')
1443
    if not next:
1444
        next = reverse('astakos.im.views.project_list')
1445

    
1446
    with ExceptionHandler(request):
1447
        _project_cancel(request, chain_id)
1448

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

    
1452

    
1453
@commit_on_success_strict()
1454
def _project_cancel(request, chain_id):
1455
    try:
1456
        chain_id = int(chain_id)
1457
        cancel_membership(chain_id, request.user)
1458
        m = _(astakos_messages.USER_REQUEST_CANCELLED)
1459
        messages.success(request, m)
1460
    except (IOError, PermissionDenied), e:
1461
        messages.error(request, e)
1462

    
1463

    
1464
@require_http_methods(["POST"])
1465
@valid_astakos_user_required
1466
def project_accept_member(request, chain_id, user_id):
1467

    
1468
    with ExceptionHandler(request):
1469
        _project_accept_member(request, chain_id, user_id)
1470

    
1471
    return redirect(reverse('project_detail', args=(chain_id,)))
1472

    
1473

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

    
1487

    
1488
@require_http_methods(["POST"])
1489
@valid_astakos_user_required
1490
def project_remove_member(request, chain_id, user_id):
1491

    
1492
    with ExceptionHandler(request):
1493
        _project_remove_member(request, chain_id, user_id)
1494

    
1495
    return redirect(reverse('project_detail', args=(chain_id,)))
1496

    
1497

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

    
1511

    
1512
@require_http_methods(["POST"])
1513
@valid_astakos_user_required
1514
def project_reject_member(request, chain_id, user_id):
1515

    
1516
    with ExceptionHandler(request):
1517
        _project_reject_member(request, chain_id, user_id)
1518

    
1519
    return redirect(reverse('project_detail', args=(chain_id,)))
1520

    
1521

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

    
1535

    
1536
@require_http_methods(["POST"])
1537
@signed_terms_required
1538
@login_required
1539
def project_app_approve(request, application_id):
1540

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

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

    
1550
    with ExceptionHandler(request):
1551
        _project_app_approve(request, application_id)
1552

    
1553
    chain_id = get_related_project_id(application_id)
1554
    return redirect(reverse('project_detail', args=(chain_id,)))
1555

    
1556

    
1557
@commit_on_success_strict()
1558
def _project_app_approve(request, application_id):
1559
    approve_application(application_id)
1560

    
1561

    
1562
@require_http_methods(["POST"])
1563
@signed_terms_required
1564
@login_required
1565
def project_app_deny(request, application_id):
1566

    
1567
    reason = request.POST.get('reason', None)
1568
    if not reason:
1569
        reason = None
1570

    
1571
    if not request.user.is_project_admin():
1572
        m = _(astakos_messages.NOT_ALLOWED)
1573
        raise PermissionDenied(m)
1574

    
1575
    try:
1576
        app = ProjectApplication.objects.get(id=application_id)
1577
    except ProjectApplication.DoesNotExist:
1578
        raise Http404
1579

    
1580
    with ExceptionHandler(request):
1581
        _project_app_deny(request, application_id, reason)
1582

    
1583
    return redirect(reverse('project_list'))
1584

    
1585

    
1586
@commit_on_success_strict()
1587
def _project_app_deny(request, application_id, reason):
1588
    deny_application(application_id, reason=reason)
1589

    
1590

    
1591
@require_http_methods(["POST"])
1592
@signed_terms_required
1593
@login_required
1594
def project_app_dismiss(request, application_id):
1595
    try:
1596
        app = ProjectApplication.objects.get(id=application_id)
1597
    except ProjectApplication.DoesNotExist:
1598
        raise Http404
1599

    
1600
    if not request.user.owns_application(app):
1601
        m = _(astakos_messages.NOT_ALLOWED)
1602
        raise PermissionDenied(m)
1603

    
1604
    with ExceptionHandler(request):
1605
        _project_app_dismiss(request, application_id)
1606

    
1607
    chain_id = None
1608
    chain_id = get_related_project_id(application_id)
1609
    if chain_id:
1610
        next = reverse('project_detail', args=(chain_id,))
1611
    else:
1612
        next = reverse('project_list')
1613
    return redirect(next)
1614

    
1615

    
1616
def _project_app_dismiss(request, application_id):
1617
    # XXX: dismiss application also does authorization
1618
    dismiss_application(application_id, request_user=request.user)
1619

    
1620

    
1621
@require_http_methods(["GET"])
1622
@required_auth_methods_assigned(allow_access=True)
1623
@login_required
1624
@signed_terms_required
1625
def landing(request):
1626
    context = {'services': Service.catalog(orderfor='dashboard')}
1627
    return render_response(
1628
        'im/landing.html',
1629
        context_instance=get_context(request), **context)
1630

    
1631

    
1632
def api_access(request):
1633
    return render_response(
1634
        'im/api_access.html',
1635
        context_instance=get_context(request))