Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 61edd5cd

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

    
121
logger = logging.getLogger(__name__)
122

    
123
callpoint = AstakosCallpoint()
124

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

    
139
def requires_auth_provider(provider_id, **perms):
140
    """
141
    """
142
    def decorator(func, *args, **kwargs):
143
        @wraps(func)
144
        def wrapper(request, *args, **kwargs):
145
            provider = auth.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
    def with_class(entry):
867
         entry['load_class'] = 'red'
868
         max_value = float(entry['maxValue'])
869
         curr_value = float(entry['currValue'])
870
         entry['ratio_limited']= 0
871
         if max_value > 0 :
872
             entry['ratio'] = (curr_value / max_value) * 100
873
         else:
874
             entry['ratio'] = 0
875
         if entry['ratio'] < 66:
876
             entry['load_class'] = 'yellow'
877
         if entry['ratio'] < 33:
878
             entry['load_class'] = 'green'
879
         if entry['ratio']<0:
880
             entry['ratio'] = 0
881
         if entry['ratio']>100:
882
             entry['ratio_limited'] = 100
883
         else:
884
             entry['ratio_limited'] = entry['ratio']
885
         return entry
886

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

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

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

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

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

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

    
928

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

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

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

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

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

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

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

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

    
1048
@require_http_methods(["GET", "POST"])
1049
@valid_astakos_user_required
1050
def project_add(request):
1051

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

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

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

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

    
1090

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

    
1106

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

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

    
1124

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

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

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

    
1155

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

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

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

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

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

    
1211

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

    
1217
@require_http_methods(["GET", "POST"])
1218
@valid_astakos_user_required
1219
def project_detail(request, chain_id):
1220
    return common_detail(request, chain_id)
1221

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

    
1239
def common_detail(request, chain_or_app_id, project_view=True, 
1240
                  template_name='im/projects/project_detail.html',
1241
                  members_status_filter=None):
1242
    project = None
1243
    if project_view:
1244
        chain_id = chain_or_app_id
1245
        if request.method == 'POST':
1246
            addmembers_form = AddProjectMembersForm(
1247
                request.POST,
1248
                chain_id=int(chain_id),
1249
                request_user=request.user)
1250
            addmembers(request, chain_id, addmembers_form)
1251
            if addmembers_form.is_valid():
1252
                addmembers_form = AddProjectMembersForm()  # clear form data
1253
        else:
1254
            addmembers_form = AddProjectMembersForm()  # initialize form
1255
        approved_members_count = 0
1256
        pending_members_count = 0
1257
        remaining_memberships_count = 0
1258
        project, application = get_by_chain_or_404(chain_id)
1259
        if project:
1260
            members = project.projectmembership_set.select_related()
1261
            approved_members_count = project.count_actually_accepted_memberships()
1262
            pending_members_count = project.count_pending_memberships()
1263
            if members_status_filter in (ProjectMembership.REQUESTED,
1264
                ProjectMembership.ACCEPTED) :
1265
                members = members.filter(state=members_status_filter)
1266
            members_table = tables.ProjectMembersTable(project,
1267
                                                       members,
1268
                                                       user=request.user,
1269
                                                       prefix="members_")
1270

    
1271
            RequestConfig(request, paginate={"per_page": PAGINATE_BY}
1272
                          ).configure(members_table)
1273

    
1274
        else:
1275
            members_table = None
1276

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

    
1283
    modifications_table = None
1284

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

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

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

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

    
1308
    return object_detail(
1309
        request,
1310
        queryset=ProjectApplication.objects.select_related(),
1311
        object_id=application.id,
1312
        template_name= template_name,
1313
        extra_context={
1314
            'project_view': project_view,
1315
            'addmembers_form':addmembers_form,
1316
            'approved_members_count':approved_members_count,
1317
            'pending_members_count':pending_members_count,
1318
            'members_table': members_table,
1319
            'owner_mode': is_owner,
1320
            'admin_mode': is_project_admin,
1321
            'modifications_table': modifications_table,
1322
            'mem_display': mem_display,
1323
            'can_join_request': can_join_req,
1324
            'can_leave_request': can_leave_req,
1325
            'members_status_filter':members_status_filter,
1326
            })
1327

    
1328
@require_http_methods(["GET", "POST"])
1329
@valid_astakos_user_required
1330
def project_search(request):
1331
    q = request.GET.get('q', '')
1332
    form = ProjectSearchForm()
1333
    q = q.strip()
1334

    
1335
    if request.method == "POST":
1336
        form = ProjectSearchForm(request.POST)
1337
        if form.is_valid():
1338
            q = form.cleaned_data['q'].strip()
1339
        else:
1340
            q = None
1341

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

    
1351
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1352
                                                prefix="my_projects_")
1353
    if request.method == "POST":
1354
        table.caption = _('SEARCH RESULTS')
1355
    else:
1356
        table.caption = _('ALL PROJECTS')
1357

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

    
1360
    return object_list(
1361
        request,
1362
        projects,
1363
        template_name='im/projects/project_list.html',
1364
        extra_context={
1365
          'form': form,
1366
          'is_search': True,
1367
          'q': q,
1368
          'table': table
1369
        })
1370

    
1371
@require_http_methods(["POST"])
1372
@valid_astakos_user_required
1373
@project_transaction_context(sync=True)
1374
def project_join(request, chain_id, ctx=None):
1375
    next = request.GET.get('next')
1376
    if not next:
1377
        next = reverse('astakos.im.views.project_detail',
1378
                       args=(chain_id,))
1379

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1546
    reason = request.POST.get('reason', None)
1547
    if not reason:
1548
        reason = None
1549

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

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

    
1559
    deny_application(application_id, reason=reason)
1560
    return redirect(reverse('project_list'))
1561

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

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

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

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

    
1587
@require_http_methods(["GET"])
1588
@required_auth_methods_assigned(allow_access=True)
1589
@login_required
1590
@signed_terms_required
1591
def landing(request):
1592
    return render_response(
1593
        'im/landing.html',
1594
        context_instance=get_context(request))
1595

    
1596

    
1597
def api_access(request):
1598
    return render_response(
1599
        'im/api_access.html',
1600
        context_instance=get_context(request))
1601

    
1602

    
1603
@require_http_methods(["GET", "POST"])
1604
@valid_astakos_user_required
1605
def project_members(request, chain_id, members_status_filter=None, 
1606
                    template_name='im/projects/project_members.html'):
1607
    return common_detail(request, chain_id, 
1608
        members_status_filter=members_status_filter, 
1609
        template_name=template_name)