Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 79b5d61b

History | View | Annotate | Download (58.9 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 import settings as astakos_settings
114
from astakos.im.api.callpoint import AstakosCallpoint
115
from astakos.im import auth_providers as auth
116
from snf_django.lib.db.transaction import commit_on_success_strict
117
from astakos.im.ctx import ExceptionHandler
118
from astakos.im import quotas
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'] = Service.catalog().values()
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
    resources_meta = presentation.RESOURCES
866

    
867
    current_usage = quotas.get_user_quotas(request.user)
868
    current_usage = json.dumps(current_usage['system'])
869
    resource_catalog, resource_groups = _resources_catalog(for_usage=True)
870
    if resource_catalog is False:
871
        # on fail resource_groups contains the result object
872
        result = resource_groups
873
        messages.error(request, 'Unable to retrieve system resources: %s' %
874
                       result.reason)
875

    
876
    resource_catalog = json.dumps(resource_catalog)
877
    resource_groups = json.dumps(resource_groups)
878
    resources_order = json.dumps(resources_meta.get('resources_order'))
879

    
880
    return render_response('im/resource_usage.html',
881
                           context_instance=get_context(request),
882
                           resource_catalog=resource_catalog,
883
                           resource_groups=resource_groups,
884
                           resources_order=resources_order,
885
                           current_usage=current_usage,
886
                           token_cookie_name=astakos_settings.COOKIE_NAME,
887
                           usage_update_interval=
888
                           astakos_settings.USAGE_UPDATE_INTERVAL)
889

    
890

    
891
# TODO: action only on POST and user should confirm the removal
892
@require_http_methods(["GET", "POST"])
893
@valid_astakos_user_required
894
def remove_auth_provider(request, pk):
895
    try:
896
        provider = request.user.auth_providers.get(pk=int(pk)).settings
897
    except AstakosUserAuthProvider.DoesNotExist:
898
        raise Http404
899

    
900
    if provider.get_remove_policy:
901
        messages.success(request, provider.get_removed_msg)
902
        provider.remove_from_user()
903
        return HttpResponseRedirect(reverse('edit_profile'))
904
    else:
905
        raise PermissionDenied
906

    
907

    
908
def how_it_works(request):
909
    return render_response(
910
        'im/how_it_works.html',
911
        context_instance=get_context(request))
912

    
913

    
914
@commit_on_success_strict()
915
def _create_object(request, model=None, template_name=None,
916
        template_loader=template_loader, extra_context=None, post_save_redirect=None,
917
        login_required=False, context_processors=None, form_class=None,
918
        msg=None):
919
    """
920
    Based of django.views.generic.create_update.create_object which displays a
921
    summary page before creating the object.
922
    """
923
    response = None
924

    
925
    if extra_context is None: extra_context = {}
926
    if login_required and not request.user.is_authenticated():
927
        return redirect_to_login(request.path)
928
    try:
929

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

    
968
@commit_on_success_strict()
969
def _update_object(request, model=None, object_id=None, slug=None,
970
        slug_field='slug', template_name=None, template_loader=template_loader,
971
        extra_context=None, post_save_redirect=None, login_required=False,
972
        context_processors=None, template_object_name='object',
973
        form_class=None, msg=None):
974
    """
975
    Based of django.views.generic.create_update.update_object which displays a
976
    summary page before updating the object.
977
    """
978
    response = None
979

    
980
    if extra_context is None: extra_context = {}
981
    if login_required and not request.user.is_authenticated():
982
        return redirect_to_login(request.path)
983

    
984
    try:
985
        model, form_class = get_model_and_form_class(model, form_class)
986
        obj = lookup_object(model, object_id, slug, slug_field)
987

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

    
1025

    
1026
def _resources_catalog(for_project=False, for_usage=False):
1027
    """
1028
    `resource_catalog` contains a list of tuples. Each tuple contains the group
1029
    key the resource is assigned to and resources list of dicts that contain
1030
    resource information.
1031
    `resource_groups` contains information about the groups
1032
    """
1033
    # presentation data
1034
    resources_meta = presentation.RESOURCES
1035
    resource_groups = resources_meta.get('groups', {})
1036
    resource_catalog = ()
1037
    resource_keys = []
1038

    
1039
    # resources in database
1040
    result = callpoint.list_resources()
1041
    if not result.is_success:
1042
        return False, result
1043
    else:
1044
        # initialize resource_catalog to contain all group/resource information
1045
        for r in result.data:
1046
            if not r.get('group') in resource_groups:
1047
                resource_groups[r.get('group')] = {'icon': 'unknown'}
1048

    
1049
        resource_keys = [r.get('str_repr') for r in result.data]
1050
        resource_catalog = [[g, filter(lambda r: r.get('group', '') == g,
1051
                                       result.data)] for g in resource_groups]
1052

    
1053
    # order groups, also include unknown groups
1054
    groups_order = resources_meta.get('groups_order')
1055
    for g in resource_groups.keys():
1056
        if not g in groups_order:
1057
            groups_order.append(g)
1058

    
1059
    # order resources, also include unknown resources
1060
    resources_order = resources_meta.get('resources_order')
1061
    for r in resource_keys:
1062
        if not r in resources_order:
1063
            resources_order.append(r)
1064

    
1065
    # sort catalog groups
1066
    resource_catalog = sorted(resource_catalog,
1067
                              key=lambda g: groups_order.index(g[0]))
1068

    
1069
    # sort groups
1070
    def groupindex(g):
1071
        return groups_order.index(g[0])
1072
    resource_groups_list = sorted([(k, v) for k, v in resource_groups.items()],
1073
                                  key=groupindex)
1074
    resource_groups = OrderedDict(resource_groups_list)
1075

    
1076
    # sort resources
1077
    def resourceindex(r):
1078
        return resources_order.index(r['str_repr'])
1079

    
1080
    for index, group in enumerate(resource_catalog):
1081
        resource_catalog[index][1] = sorted(resource_catalog[index][1],
1082
                                            key=resourceindex)
1083
        if len(resource_catalog[index][1]) == 0:
1084
            resource_catalog.pop(index)
1085
            for gindex, g in enumerate(resource_groups):
1086
                if g[0] == group[0]:
1087
                    resource_groups.pop(gindex)
1088

    
1089
    # filter out resources which user cannot request in a project application
1090
    exclude = resources_meta.get('exclude_from_usage', [])
1091
    for group_index, group_resources in enumerate(list(resource_catalog)):
1092
        group, resources = group_resources
1093
        for index, resource in list(enumerate(resources)):
1094
            if for_project and not resource.get('allow_in_projects'):
1095
                resources.remove(resource)
1096
            if resource.get('str_repr') in exclude and for_usage:
1097
                resources.remove(resource)
1098

    
1099
    # cleanup empty groups
1100
    for group_index, group_resources in enumerate(list(resource_catalog)):
1101
        group, resources = group_resources
1102
        if len(resources) == 0:
1103
            resource_catalog.pop(group_index)
1104
            resource_groups.pop(group)
1105

    
1106

    
1107
    return resource_catalog, resource_groups
1108

    
1109

    
1110
@require_http_methods(["GET", "POST"])
1111
@valid_astakos_user_required
1112
def project_add(request):
1113
    user = request.user
1114
    if not user.is_project_admin():
1115
        ok, limit = qh_add_pending_app(user, dry_run=True)
1116
        if not ok:
1117
            m = _(astakos_messages.PENDING_APPLICATION_LIMIT_ADD) % limit
1118
            messages.error(request, m)
1119
            next = reverse('astakos.im.views.project_list')
1120
            next = restrict_next(next, domain=COOKIE_DOMAIN)
1121
            return redirect(next)
1122

    
1123
    details_fields = ["name", "homepage", "description", "start_date",
1124
                      "end_date", "comments"]
1125
    membership_fields = ["member_join_policy", "member_leave_policy",
1126
                         "limit_on_members_number"]
1127
    resource_catalog, resource_groups = _resources_catalog(for_project=True)
1128
    if resource_catalog is False:
1129
        # on fail resource_groups contains the result object
1130
        result = resource_groups
1131
        messages.error(request, 'Unable to retrieve system resources: %s' %
1132
                       result.reason)
1133
    extra_context = {
1134
        'resource_catalog': resource_catalog,
1135
        'resource_groups': resource_groups,
1136
        'show_form': True,
1137
        'details_fields': details_fields,
1138
        'membership_fields': membership_fields}
1139

    
1140
    response = None
1141
    with ExceptionHandler(request):
1142
        response = _create_object(
1143
            request,
1144
            template_name='im/projects/projectapplication_form.html',
1145
            extra_context=extra_context,
1146
            post_save_redirect=reverse('project_list'),
1147
            form_class=ProjectApplicationForm,
1148
            msg=_("The %(verbose_name)s has been received and "
1149
                  "is under consideration."),
1150
            )
1151

    
1152
    if response is not None:
1153
        return response
1154

    
1155
    next = reverse('astakos.im.views.project_list')
1156
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1157
    return redirect(next)
1158

    
1159

    
1160
@require_http_methods(["GET"])
1161
@valid_astakos_user_required
1162
def project_list(request):
1163
    projects = ProjectApplication.objects.user_accessible_projects(request.user).select_related()
1164
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1165
                                                prefix="my_projects_")
1166
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1167

    
1168
    return object_list(
1169
        request,
1170
        projects,
1171
        template_name='im/projects/project_list.html',
1172
        extra_context={
1173
            'is_search':False,
1174
            'table': table,
1175
        })
1176

    
1177

    
1178
@require_http_methods(["POST"])
1179
@valid_astakos_user_required
1180
def project_app_cancel(request, application_id):
1181
    next = request.GET.get('next')
1182
    chain_id = None
1183

    
1184
    with ExceptionHandler(request):
1185
        chain_id = _project_app_cancel(request, application_id)
1186

    
1187
    if not next:
1188
        if chain_id:
1189
            next = reverse('astakos.im.views.project_detail', args=(chain_id,))
1190
        else:
1191
            next = reverse('astakos.im.views.project_list')
1192

    
1193
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1194
    return redirect(next)
1195

    
1196
@commit_on_success_strict()
1197
def _project_app_cancel(request, application_id):
1198
    chain_id = None
1199
    try:
1200
        application_id = int(application_id)
1201
        chain_id = get_related_project_id(application_id)
1202
        cancel_application(application_id, request.user)
1203
    except (IOError, PermissionDenied), e:
1204
        messages.error(request, e)
1205
    else:
1206
        msg = _(astakos_messages.APPLICATION_CANCELLED)
1207
        messages.success(request, msg)
1208
        return chain_id
1209

    
1210

    
1211
@require_http_methods(["GET", "POST"])
1212
@valid_astakos_user_required
1213
def project_modify(request, application_id):
1214

    
1215
    try:
1216
        app = ProjectApplication.objects.get(id=application_id)
1217
    except ProjectApplication.DoesNotExist:
1218
        raise Http404
1219

    
1220
    user = request.user
1221
    if not (user.owns_application(app) or user.is_project_admin(app.id)):
1222
        m = _(astakos_messages.NOT_ALLOWED)
1223
        raise PermissionDenied(m)
1224

    
1225
    if not user.is_project_admin():
1226
        owner = app.owner
1227
        ok, limit = qh_add_pending_app(owner, precursor=app, dry_run=True)
1228
        if not ok:
1229
            m = _(astakos_messages.PENDING_APPLICATION_LIMIT_MODIFY) % limit
1230
            messages.error(request, m)
1231
            next = reverse('astakos.im.views.project_list')
1232
            next = restrict_next(next, domain=COOKIE_DOMAIN)
1233
            return redirect(next)
1234

    
1235
    details_fields = ["name", "homepage", "description", "start_date",
1236
                      "end_date", "comments"]
1237
    membership_fields = ["member_join_policy", "member_leave_policy",
1238
                         "limit_on_members_number"]
1239
    resource_catalog, resource_groups = _resources_catalog(for_project=True)
1240
    if resource_catalog is False:
1241
        # on fail resource_groups contains the result object
1242
        result = resource_groups
1243
        messages.error(request, 'Unable to retrieve system resources: %s' %
1244
                       result.reason)
1245
    extra_context = {
1246
        'resource_catalog': resource_catalog,
1247
        'resource_groups': resource_groups,
1248
        'show_form': True,
1249
        'details_fields': details_fields,
1250
        'update_form': True,
1251
        'membership_fields': membership_fields
1252
    }
1253

    
1254
    response = None
1255
    with ExceptionHandler(request):
1256
        response = _update_object(
1257
            request,
1258
            object_id=application_id,
1259
            template_name='im/projects/projectapplication_form.html',
1260
            extra_context=extra_context,
1261
            post_save_redirect=reverse('project_list'),
1262
            form_class=ProjectApplicationForm,
1263
            msg=_("The %(verbose_name)s has been received and is under "
1264
                  "consideration."))
1265

    
1266
    if response is not None:
1267
        return response
1268

    
1269
    next = reverse('astakos.im.views.project_list')
1270
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1271
    return redirect(next)
1272

    
1273
@require_http_methods(["GET", "POST"])
1274
@valid_astakos_user_required
1275
def project_app(request, application_id):
1276
    return common_detail(request, application_id, project_view=False)
1277

    
1278
@require_http_methods(["GET", "POST"])
1279
@valid_astakos_user_required
1280
def project_detail(request, chain_id):
1281
    return common_detail(request, chain_id)
1282

    
1283
@commit_on_success_strict()
1284
def addmembers(request, chain_id, addmembers_form):
1285
    if addmembers_form.is_valid():
1286
        try:
1287
            chain_id = int(chain_id)
1288
            map(lambda u: enroll_member(
1289
                    chain_id,
1290
                    u,
1291
                    request_user=request.user),
1292
                addmembers_form.valid_users)
1293
        except (IOError, PermissionDenied), e:
1294
            messages.error(request, e)
1295

    
1296
def common_detail(request, chain_or_app_id, project_view=True):
1297
    project = None
1298
    if project_view:
1299
        chain_id = chain_or_app_id
1300
        if request.method == 'POST':
1301
            addmembers_form = AddProjectMembersForm(
1302
                request.POST,
1303
                chain_id=int(chain_id),
1304
                request_user=request.user)
1305
            with ExceptionHandler(request):
1306
                addmembers(request, chain_id, addmembers_form)
1307

    
1308
            if addmembers_form.is_valid():
1309
                addmembers_form = AddProjectMembersForm()  # clear form data
1310
        else:
1311
            addmembers_form = AddProjectMembersForm()  # initialize form
1312

    
1313
        project, application = get_by_chain_or_404(chain_id)
1314
        if project:
1315
            members = project.projectmembership_set.select_related()
1316
            members_table = tables.ProjectMembersTable(project,
1317
                                                       members,
1318
                                                       user=request.user,
1319
                                                       prefix="members_")
1320
            RequestConfig(request, paginate={"per_page": PAGINATE_BY}
1321
                          ).configure(members_table)
1322

    
1323
        else:
1324
            members_table = None
1325

    
1326
    else: # is application
1327
        application_id = chain_or_app_id
1328
        application = get_object_or_404(ProjectApplication, pk=application_id)
1329
        members_table = None
1330
        addmembers_form = None
1331

    
1332
    modifications_table = None
1333

    
1334
    user = request.user
1335
    is_project_admin = user.is_project_admin(application_id=application.id)
1336
    is_owner = user.owns_application(application)
1337
    if not (is_owner or is_project_admin) and not project_view:
1338
        m = _(astakos_messages.NOT_ALLOWED)
1339
        raise PermissionDenied(m)
1340

    
1341
    if (not (is_owner or is_project_admin) and project_view and
1342
        not user.non_owner_can_view(project)):
1343
        m = _(astakos_messages.NOT_ALLOWED)
1344
        raise PermissionDenied(m)
1345

    
1346
    following_applications = list(application.pending_modifications())
1347
    following_applications.reverse()
1348
    modifications_table = (
1349
        tables.ProjectModificationApplicationsTable(following_applications,
1350
                                                    user=request.user,
1351
                                                    prefix="modifications_"))
1352

    
1353
    mem_display = user.membership_display(project) if project else None
1354
    can_join_req = can_join_request(project, user) if project else False
1355
    can_leave_req = can_leave_request(project, user) if project else False
1356

    
1357
    return object_detail(
1358
        request,
1359
        queryset=ProjectApplication.objects.select_related(),
1360
        object_id=application.id,
1361
        template_name='im/projects/project_detail.html',
1362
        extra_context={
1363
            'project_view': project_view,
1364
            'addmembers_form':addmembers_form,
1365
            'members_table': members_table,
1366
            'owner_mode': is_owner,
1367
            'admin_mode': is_project_admin,
1368
            'modifications_table': modifications_table,
1369
            'mem_display': mem_display,
1370
            'can_join_request': can_join_req,
1371
            'can_leave_request': can_leave_req,
1372
            })
1373

    
1374
@require_http_methods(["GET", "POST"])
1375
@valid_astakos_user_required
1376
def project_search(request):
1377
    q = request.GET.get('q', '')
1378
    form = ProjectSearchForm()
1379
    q = q.strip()
1380

    
1381
    if request.method == "POST":
1382
        form = ProjectSearchForm(request.POST)
1383
        if form.is_valid():
1384
            q = form.cleaned_data['q'].strip()
1385
        else:
1386
            q = None
1387

    
1388
    if q is None:
1389
        projects = ProjectApplication.objects.none()
1390
    else:
1391
        accepted_projects = request.user.projectmembership_set.filter(
1392
            ~Q(acceptance_date__isnull=True)).values_list('project', flat=True)
1393
        projects = ProjectApplication.objects.search_by_name(q)
1394
        projects = projects.filter(~Q(project__last_approval_date__isnull=True))
1395
        projects = projects.exclude(project__in=accepted_projects)
1396

    
1397
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1398
                                                prefix="my_projects_")
1399
    if request.method == "POST":
1400
        table.caption = _('SEARCH RESULTS')
1401
    else:
1402
        table.caption = _('ALL PROJECTS')
1403

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

    
1406
    return object_list(
1407
        request,
1408
        projects,
1409
        template_name='im/projects/project_list.html',
1410
        extra_context={
1411
          'form': form,
1412
          'is_search': True,
1413
          'q': q,
1414
          'table': table
1415
        })
1416

    
1417
@require_http_methods(["POST"])
1418
@valid_astakos_user_required
1419
def project_join(request, chain_id):
1420
    next = request.GET.get('next')
1421
    if not next:
1422
        next = reverse('astakos.im.views.project_detail',
1423
                       args=(chain_id,))
1424

    
1425
    with ExceptionHandler(request):
1426
        _project_join(request, chain_id)
1427

    
1428

    
1429
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1430
    return redirect(next)
1431

    
1432

    
1433
@commit_on_success_strict()
1434
def _project_join(request, chain_id):
1435
    try:
1436
        chain_id = int(chain_id)
1437
        auto_accepted = join_project(chain_id, request.user)
1438
        if auto_accepted:
1439
            m = _(astakos_messages.USER_JOINED_PROJECT)
1440
        else:
1441
            m = _(astakos_messages.USER_JOIN_REQUEST_SUBMITTED)
1442
        messages.success(request, m)
1443
    except (IOError, PermissionDenied), e:
1444
        messages.error(request, e)
1445

    
1446

    
1447
@require_http_methods(["POST"])
1448
@valid_astakos_user_required
1449
def project_leave(request, chain_id):
1450
    next = request.GET.get('next')
1451
    if not next:
1452
        next = reverse('astakos.im.views.project_list')
1453

    
1454
    with ExceptionHandler(request):
1455
        _project_leave(request, chain_id)
1456

    
1457
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1458
    return redirect(next)
1459

    
1460

    
1461
@commit_on_success_strict()
1462
def _project_leave(request, chain_id):
1463
    try:
1464
        chain_id = int(chain_id)
1465
        auto_accepted = leave_project(chain_id, request.user)
1466
        if auto_accepted:
1467
            m = _(astakos_messages.USER_LEFT_PROJECT)
1468
        else:
1469
            m = _(astakos_messages.USER_LEAVE_REQUEST_SUBMITTED)
1470
        messages.success(request, m)
1471
    except (IOError, PermissionDenied), e:
1472
        messages.error(request, e)
1473

    
1474

    
1475
@require_http_methods(["POST"])
1476
@valid_astakos_user_required
1477
def project_cancel(request, chain_id):
1478
    next = request.GET.get('next')
1479
    if not next:
1480
        next = reverse('astakos.im.views.project_list')
1481

    
1482
    with ExceptionHandler(request):
1483
        _project_cancel(request, chain_id)
1484

    
1485
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1486
    return redirect(next)
1487

    
1488

    
1489
@commit_on_success_strict()
1490
def _project_cancel(request, chain_id):
1491
    try:
1492
        chain_id = int(chain_id)
1493
        cancel_membership(chain_id, request.user)
1494
        m = _(astakos_messages.USER_REQUEST_CANCELLED)
1495
        messages.success(request, m)
1496
    except (IOError, PermissionDenied), e:
1497
        messages.error(request, e)
1498

    
1499

    
1500
@require_http_methods(["POST"])
1501
@valid_astakos_user_required
1502
def project_accept_member(request, chain_id, memb_id):
1503

    
1504
    with ExceptionHandler(request):
1505
        _project_accept_member(request, chain_id, memb_id)
1506

    
1507
    return redirect(reverse('project_detail', args=(chain_id,)))
1508

    
1509

    
1510
@commit_on_success_strict()
1511
def _project_accept_member(request, chain_id, memb_id):
1512
    try:
1513
        chain_id = int(chain_id)
1514
        memb_id = int(memb_id)
1515
        m = accept_membership(chain_id, memb_id, request.user)
1516
    except (IOError, PermissionDenied), e:
1517
        messages.error(request, e)
1518
    else:
1519
        email = escape(m.person.email)
1520
        msg = _(astakos_messages.USER_MEMBERSHIP_ACCEPTED) % email
1521
        messages.success(request, msg)
1522

    
1523

    
1524
@require_http_methods(["POST"])
1525
@valid_astakos_user_required
1526
def project_remove_member(request, chain_id, memb_id):
1527

    
1528
    with ExceptionHandler(request):
1529
        _project_remove_member(request, chain_id, memb_id)
1530

    
1531
    return redirect(reverse('project_detail', args=(chain_id,)))
1532

    
1533

    
1534
@commit_on_success_strict()
1535
def _project_remove_member(request, chain_id, memb_id):
1536
    try:
1537
        chain_id = int(chain_id)
1538
        memb_id = int(memb_id)
1539
        m = remove_membership(chain_id, memb_id, request.user)
1540
    except (IOError, PermissionDenied), e:
1541
        messages.error(request, e)
1542
    else:
1543
        email = escape(m.person.email)
1544
        msg = _(astakos_messages.USER_MEMBERSHIP_REMOVED) % email
1545
        messages.success(request, msg)
1546

    
1547

    
1548
@require_http_methods(["POST"])
1549
@valid_astakos_user_required
1550
def project_reject_member(request, chain_id, memb_id):
1551

    
1552
    with ExceptionHandler(request):
1553
        _project_reject_member(request, chain_id, memb_id)
1554

    
1555
    return redirect(reverse('project_detail', args=(chain_id,)))
1556

    
1557

    
1558
@commit_on_success_strict()
1559
def _project_reject_member(request, chain_id, memb_id):
1560
    try:
1561
        chain_id = int(chain_id)
1562
        memb_id = int(memb_id)
1563
        m = reject_membership(chain_id, memb_id, request.user)
1564
    except (IOError, PermissionDenied), e:
1565
        messages.error(request, e)
1566
    else:
1567
        email = escape(m.person.email)
1568
        msg = _(astakos_messages.USER_MEMBERSHIP_REJECTED) % email
1569
        messages.success(request, msg)
1570

    
1571

    
1572
@require_http_methods(["POST"])
1573
@signed_terms_required
1574
@login_required
1575
def project_app_approve(request, application_id):
1576

    
1577
    if not request.user.is_project_admin():
1578
        m = _(astakos_messages.NOT_ALLOWED)
1579
        raise PermissionDenied(m)
1580

    
1581
    try:
1582
        app = ProjectApplication.objects.get(id=application_id)
1583
    except ProjectApplication.DoesNotExist:
1584
        raise Http404
1585

    
1586
    with ExceptionHandler(request):
1587
        _project_app_approve(request, application_id)
1588

    
1589
    chain_id = get_related_project_id(application_id)
1590
    return redirect(reverse('project_detail', args=(chain_id,)))
1591

    
1592

    
1593
@commit_on_success_strict()
1594
def _project_app_approve(request, application_id):
1595
    approve_application(application_id)
1596

    
1597

    
1598
@require_http_methods(["POST"])
1599
@signed_terms_required
1600
@login_required
1601
def project_app_deny(request, application_id):
1602

    
1603
    reason = request.POST.get('reason', None)
1604
    if not reason:
1605
        reason = None
1606

    
1607
    if not request.user.is_project_admin():
1608
        m = _(astakos_messages.NOT_ALLOWED)
1609
        raise PermissionDenied(m)
1610

    
1611
    try:
1612
        app = ProjectApplication.objects.get(id=application_id)
1613
    except ProjectApplication.DoesNotExist:
1614
        raise Http404
1615

    
1616
    with ExceptionHandler(request):
1617
        _project_app_deny(request, application_id, reason)
1618

    
1619
    return redirect(reverse('project_list'))
1620

    
1621

    
1622
@commit_on_success_strict()
1623
def _project_app_deny(request, application_id, reason):
1624
    deny_application(application_id, reason=reason)
1625

    
1626

    
1627
@require_http_methods(["POST"])
1628
@signed_terms_required
1629
@login_required
1630
def project_app_dismiss(request, application_id):
1631
    try:
1632
        app = ProjectApplication.objects.get(id=application_id)
1633
    except ProjectApplication.DoesNotExist:
1634
        raise Http404
1635

    
1636
    if not request.user.owns_application(app):
1637
        m = _(astakos_messages.NOT_ALLOWED)
1638
        raise PermissionDenied(m)
1639

    
1640
    with ExceptionHandler(request):
1641
        _project_app_dismiss(request, application_id)
1642

    
1643
    chain_id = None
1644
    chain_id = get_related_project_id(application_id)
1645
    if chain_id:
1646
        next = reverse('project_detail', args=(chain_id,))
1647
    else:
1648
        next = reverse('project_list')
1649
    return redirect(next)
1650

    
1651

    
1652
def _project_app_dismiss(request, application_id):
1653
    # XXX: dismiss application also does authorization
1654
    dismiss_application(application_id, request_user=request.user)
1655

    
1656

    
1657
@require_http_methods(["GET"])
1658
@required_auth_methods_assigned(allow_access=True)
1659
@login_required
1660
@signed_terms_required
1661
def landing(request):
1662
    context = {'services': Service.catalog(orderfor='dashboard')}
1663
    return render_response(
1664
        'im/landing.html',
1665
        context_instance=get_context(request), **context)
1666

    
1667

    
1668
def api_access(request):
1669
    return render_response(
1670
        'im/api_access.html',
1671
        context_instance=get_context(request))