Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 4e03ba30

History | View | Annotate | Download (57.4 kB)

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

    
34
import logging
35
import calendar
36
import inflect
37

    
38
engine = inflect.engine()
39

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

    
45
from django_tables2 import RequestConfig
46

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

    
75
import astakos.im.messages as astakos_messages
76

    
77
from astakos.im.activation_backends import get_backend, SimpleBackend
78
from astakos.im import tables
79
from astakos.im.models import (
80
    AstakosUser, ApprovalTerms,
81
    EmailChange, AstakosUserAuthProvider, PendingThirdPartyUser,
82
    ProjectApplication, ProjectMembership, Project, Service)
83
from astakos.im.util import (
84
    get_context, prepare_response, get_query, restrict_next)
85
from astakos.im.forms import (
86
    LoginForm, InvitationForm,
87
    FeedbackForm, SignApprovalTermsForm,
88
    EmailChangeForm,
89
    ProjectApplicationForm, ProjectSortForm,
90
    AddProjectMembersForm, ProjectSearchForm,
91
    ProjectMembersSortForm)
92
from astakos.im.forms import ExtendedProfileForm as ProfileForm
93
from astakos.im.functions import (
94
    send_feedback, SendMailError,
95
    logout as auth_logout,
96
    activate as activate_func,
97
    invite as invite_func,
98
    send_activation as send_activation_func,
99
    SendNotificationError,
100
    reached_pending_application_limit,
101
    accept_membership, reject_membership, remove_membership, cancel_membership,
102
    leave_project, join_project, enroll_member, can_join_request, can_leave_request,
103
    get_related_project_id, get_by_chain_or_404,
104
    approve_application, deny_application,
105
    cancel_application, dismiss_application)
106
from astakos.im.settings import (
107
    COOKIE_DOMAIN, LOGOUT_NEXT,
108
    LOGGING_LEVEL, PAGINATE_BY,
109
    PAGINATE_BY_ALL,
110
    ACTIVATION_REDIRECT_URL,
111
    MODERATION_ENABLED)
112
from astakos.im import presentation
113
from astakos.im.api import get_services_dict
114
from astakos.im import settings as astakos_settings
115
from astakos.im.api.callpoint import AstakosCallpoint
116
from astakos.im import auth_providers as auth
117
from snf_django.lib.db.transaction import commit_on_success_strict
118
from astakos.im.ctx import ExceptionHandler
119
from astakos.im import quotas
120

    
121
logger = logging.getLogger(__name__)
122

    
123
callpoint = AstakosCallpoint()
124

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

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

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

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

    
159

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

    
174

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

    
190

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

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

    
212

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

    
216

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

223
    **Arguments**
224

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

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

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

236
    **Template:**
237

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

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

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

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

    
256

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

    
269

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

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

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

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

286
    **Arguments**
287

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

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

295
    **Template:**
296

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

299
    **Settings:**
300

301
    The view expectes the following settings are defined:
302

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

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

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

    
347

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

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

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

362
    **Arguments**
363

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

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

371
    **Template:**
372

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

375
    **Settings:**
376

377
    The view expectes the following settings are defined:
378

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

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

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

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

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

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

    
437

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

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

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

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

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

457
    **Arguments**
458

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

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

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

469
    **Template:**
470

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
569

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

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

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

583
    **Arguments**
584

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

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

592
    **Template:**
593

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

596
    **Settings:**
597

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

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

    
624

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

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

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

    
664

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

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

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

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

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

    
715

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

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

    
742
    terms = f.read()
743

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

    
768

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

    
778

    
779
    if not astakos_settings.EMAILCHANGE_ENABLED:
780
        raise PermissionDenied
781

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

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

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

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

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

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

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

    
838

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

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

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

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

    
861

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

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

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

    
883

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

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

    
900

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

    
906

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

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

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

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

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

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

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

    
1018

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

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

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

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

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

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

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

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

    
1081
    return resource_catalog, resource_groups
1082

    
1083

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

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

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

    
1120
    if response is not None:
1121
        return response
1122

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

    
1127

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

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

    
1145

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

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

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

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

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

    
1178

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

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

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

    
1193
    owner_id = app.owner_id
1194
    reached, limit = reached_pending_application_limit(owner_id, app)
1195
    if not user.is_project_admin() and reached:
1196
        m = _(astakos_messages.PENDING_APPLICATION_LIMIT_MODIFY) % limit
1197
        messages.error(request, m)
1198
        next = reverse('astakos.im.views.project_list')
1199
        next = restrict_next(next, domain=COOKIE_DOMAIN)
1200
        return redirect(next)
1201

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

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

    
1228
    if response is not None:
1229
        return response
1230

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

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

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

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

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

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

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

    
1285
        else:
1286
            members_table = None
1287

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

    
1294
    modifications_table = None
1295

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

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

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

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

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

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

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

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

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

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

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

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

    
1387
    with ExceptionHandler(request):
1388
        _project_join(request, chain_id)
1389

    
1390

    
1391
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1392
    return redirect(next)
1393

    
1394

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

    
1408

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

    
1416
    with ExceptionHandler(request):
1417
        _project_leave(request, chain_id)
1418

    
1419
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1420
    return redirect(next)
1421

    
1422

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

    
1436

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

    
1444
    with ExceptionHandler(request):
1445
        _project_cancel(request, chain_id)
1446

    
1447
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1448
    return redirect(next)
1449

    
1450

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

    
1461

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

    
1466
    with ExceptionHandler(request):
1467
        _project_accept_member(request, chain_id, user_id)
1468

    
1469
    return redirect(reverse('project_detail', args=(chain_id,)))
1470

    
1471

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

    
1485

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

    
1490
    with ExceptionHandler(request):
1491
        _project_remove_member(request, chain_id, user_id)
1492

    
1493
    return redirect(reverse('project_detail', args=(chain_id,)))
1494

    
1495

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

    
1509

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

    
1514
    with ExceptionHandler(request):
1515
        _project_reject_member(request, chain_id, user_id)
1516

    
1517
    return redirect(reverse('project_detail', args=(chain_id,)))
1518

    
1519

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

    
1533

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

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

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

    
1548
    with ExceptionHandler(request):
1549
        _project_app_approve(request, application_id)
1550

    
1551
    chain_id = get_related_project_id(application_id)
1552
    return redirect(reverse('project_detail', args=(chain_id,)))
1553

    
1554

    
1555
@commit_on_success_strict()
1556
def _project_app_approve(request, application_id):
1557
    approve_application(application_id)
1558

    
1559

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

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

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

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

    
1578
    with ExceptionHandler(request):
1579
        _project_app_deny(request, application_id, reason)
1580

    
1581
    return redirect(reverse('project_list'))
1582

    
1583

    
1584
@commit_on_success_strict()
1585
def _project_app_deny(request, application_id, reason):
1586
    deny_application(application_id, reason=reason)
1587

    
1588

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

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

    
1602
    with ExceptionHandler(request):
1603
        _project_app_dismiss(request, application_id)
1604

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

    
1613

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

    
1618

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

    
1629

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