Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 0156e40c

History | View | Annotate | Download (58 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 synnefo.lib.db.transaction import commit_on_success_strict
118
from astakos.im.ctx import ExceptionHandler
119

    
120
logger = logging.getLogger(__name__)
121

    
122
callpoint = AstakosCallpoint()
123

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

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

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

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

    
158

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

    
173

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

    
189

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

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

    
211

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

    
215

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

222
    **Arguments**
223

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

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

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

235
    **Template:**
236

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

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

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

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

    
255

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

    
268

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

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

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

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

285
    **Arguments**
286

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

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

294
    **Template:**
295

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

298
    **Settings:**
299

300
    The view expectes the following settings are defined:
301

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

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

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

    
346

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

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

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

361
    **Arguments**
362

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

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

370
    **Template:**
371

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

374
    **Settings:**
375

376
    The view expectes the following settings are defined:
377

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

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

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

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

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

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

    
436

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

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

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

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

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

456
    **Arguments**
457

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

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

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

468
    **Template:**
469

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
568

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

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

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

582
    **Arguments**
583

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

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

591
    **Template:**
592

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

595
    **Settings:**
596

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

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

    
623

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

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

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

    
663

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

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

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

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

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

    
714

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

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

    
741
    terms = f.read()
742

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

    
767

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

    
777

    
778
    if not astakos_settings.EMAILCHANGE_ENABLED:
779
        raise PermissionDenied
780

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

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

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

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

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

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

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

    
837

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

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

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

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

    
860

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

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

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

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

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

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

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

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

    
927

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

    
933

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

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

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

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

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

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

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

    
1045

    
1046
def _resources_catalog(request):
1047
    """
1048
    `resource_catalog` contains a list of tuples. Each tuple contains the group
1049
    key the resource is assigned to and resources list of dicts that contain
1050
    resource information.
1051
    `resource_groups` contains information about the groups
1052
    """
1053
    # presentation data
1054
    resource_groups = presentation.RESOURCES.get('groups', {})
1055
    resource_catalog = ()
1056
    resource_keys = []
1057

    
1058
    # resources in database
1059
    result = callpoint.list_resources()
1060
    if not result.is_success:
1061
        messages.error(request, 'Unable to retrieve system resources: %s' %
1062
                                result.reason)
1063
    else:
1064
        # initialize resource_catalog to contain all group/resource information
1065
        for r in result.data:
1066
            if not r.get('group') in resource_groups:
1067
                resource_groups[r.get('group')] = {'icon': 'unknown'}
1068

    
1069
        resource_keys = [r.get('str_repr') for r in result.data]
1070
        resource_catalog = [[g, filter(lambda r: r.get('group', '') == g,
1071
                                       result.data)] for g in resource_groups]
1072

    
1073
    # order groups, also include unknown groups
1074
    groups_order = presentation.RESOURCES.get('groups_order')
1075
    for g in resource_groups.keys():
1076
        if not g in groups_order:
1077
            groups_order.append(g)
1078

    
1079
    # order resources, also include unknown resources
1080
    resources_order = presentation.RESOURCES.get('resources_order')
1081
    for r in resource_keys:
1082
        if not r in resources_order:
1083
            resources_order.append(r)
1084

    
1085
    # sort catalog groups
1086
    resource_catalog = sorted(resource_catalog,
1087
                              key=lambda g: groups_order.index(g[0]))
1088

    
1089
    # sort groups
1090
    def groupindex(g):
1091
        return groups_order.index(g[0])
1092
    resource_groups_list = sorted([(k, v) for k, v in resource_groups.items()],
1093
                                  key=groupindex)
1094
    resource_groups = OrderedDict(resource_groups_list)
1095

    
1096
    # sort resources
1097
    def resourceindex(r):
1098
        return resources_order.index(r['str_repr'])
1099
    for index, group in enumerate(resource_catalog):
1100
        resource_catalog[index][1] = sorted(resource_catalog[index][1],
1101
                                            key=resourceindex)
1102
        if len(resource_catalog[index][1]) == 0:
1103
            resource_catalog.pop(index)
1104
            for gindex, g in enumerate(resource_groups):
1105
                if g[0] == group[0]:
1106
                    resource_groups.pop(gindex)
1107

    
1108
    return resource_catalog, resource_groups
1109

    
1110

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

    
1123
    details_fields = ["name", "homepage", "description", "start_date",
1124
                      "end_date", "comments"]
1125
    membership_fields = ["member_join_policy", "member_leave_policy",
1126
                         "limit_on_members_number"]
1127
    resource_catalog, resource_groups = _resources_catalog(request)
1128
    extra_context = {
1129
        'resource_catalog': resource_catalog,
1130
        'resource_groups': resource_groups,
1131
        'show_form': True,
1132
        'details_fields': details_fields,
1133
        'membership_fields': membership_fields}
1134

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

    
1147
    if response is not None:
1148
        return response
1149

    
1150
    next = reverse('astakos.im.views.project_list')
1151
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1152
    return redirect(next)
1153

    
1154

    
1155
@require_http_methods(["GET"])
1156
@valid_astakos_user_required
1157
def project_list(request):
1158
    projects = ProjectApplication.objects.user_accessible_projects(request.user).select_related()
1159
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1160
                                                prefix="my_projects_")
1161
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1162

    
1163
    return object_list(
1164
        request,
1165
        projects,
1166
        template_name='im/projects/project_list.html',
1167
        extra_context={
1168
            'is_search':False,
1169
            'table': table,
1170
        })
1171

    
1172

    
1173
@require_http_methods(["POST"])
1174
@valid_astakos_user_required
1175
def project_app_cancel(request, application_id):
1176
    next = request.GET.get('next')
1177
    chain_id = None
1178

    
1179
    with ExceptionHandler(request):
1180
        chain_id = _project_app_cancel(request, application_id)
1181

    
1182
    if not next:
1183
        if chain_id:
1184
            next = reverse('astakos.im.views.project_detail', args=(chain_id,))
1185
        else:
1186
            next = reverse('astakos.im.views.project_list')
1187

    
1188
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1189
    return redirect(next)
1190

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

    
1205

    
1206
@require_http_methods(["GET", "POST"])
1207
@valid_astakos_user_required
1208
def project_modify(request, application_id):
1209

    
1210
    try:
1211
        app = ProjectApplication.objects.get(id=application_id)
1212
    except ProjectApplication.DoesNotExist:
1213
        raise Http404
1214

    
1215
    user = request.user
1216
    if not (user.owns_application(app) or user.is_project_admin(app.id)):
1217
        m = _(astakos_messages.NOT_ALLOWED)
1218
        raise PermissionDenied(m)
1219

    
1220
    owner_id = app.owner_id
1221
    reached, limit = reached_pending_application_limit(owner_id, app)
1222
    if not user.is_project_admin() and reached:
1223
        m = _(astakos_messages.PENDING_APPLICATION_LIMIT_MODIFY) % limit
1224
        messages.error(request, m)
1225
        next = reverse('astakos.im.views.project_list')
1226
        next = restrict_next(next, domain=COOKIE_DOMAIN)
1227
        return redirect(next)
1228

    
1229
    details_fields = ["name", "homepage", "description", "start_date",
1230
                      "end_date", "comments"]
1231
    membership_fields = ["member_join_policy", "member_leave_policy",
1232
                         "limit_on_members_number"]
1233
    resource_catalog, resource_groups = _resources_catalog(request)
1234
    extra_context = {
1235
        'resource_catalog': resource_catalog,
1236
        'resource_groups': resource_groups,
1237
        'show_form': True,
1238
        'details_fields': details_fields,
1239
        'update_form': True,
1240
        'membership_fields': membership_fields
1241
    }
1242

    
1243
    response = None
1244
    with ExceptionHandler(request):
1245
        response = _update_object(
1246
            request,
1247
            object_id=application_id,
1248
            template_name='im/projects/projectapplication_form.html',
1249
            extra_context=extra_context,
1250
            post_save_redirect=reverse('project_list'),
1251
            form_class=ProjectApplicationForm,
1252
            msg=_("The %(verbose_name)s has been received and is under "
1253
                  "consideration."))
1254

    
1255
    if response is not None:
1256
        return response
1257

    
1258
    next = reverse('astakos.im.views.project_list')
1259
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1260
    return redirect(next)
1261

    
1262
@require_http_methods(["GET", "POST"])
1263
@valid_astakos_user_required
1264
def project_app(request, application_id):
1265
    return common_detail(request, application_id, project_view=False)
1266

    
1267
@require_http_methods(["GET", "POST"])
1268
@valid_astakos_user_required
1269
def project_detail(request, chain_id):
1270
    return common_detail(request, chain_id)
1271

    
1272
@commit_on_success_strict()
1273
def addmembers(request, chain_id, addmembers_form):
1274
    if addmembers_form.is_valid():
1275
        try:
1276
            chain_id = int(chain_id)
1277
            map(lambda u: enroll_member(
1278
                    chain_id,
1279
                    u,
1280
                    request_user=request.user),
1281
                addmembers_form.valid_users)
1282
        except (IOError, PermissionDenied), e:
1283
            messages.error(request, e)
1284

    
1285
def common_detail(request, chain_or_app_id, project_view=True):
1286
    project = None
1287
    if project_view:
1288
        chain_id = chain_or_app_id
1289
        if request.method == 'POST':
1290
            addmembers_form = AddProjectMembersForm(
1291
                request.POST,
1292
                chain_id=int(chain_id),
1293
                request_user=request.user)
1294
            with ExceptionHandler(request):
1295
                addmembers(request, chain_id, addmembers_form)
1296

    
1297
            if addmembers_form.is_valid():
1298
                addmembers_form = AddProjectMembersForm()  # clear form data
1299
        else:
1300
            addmembers_form = AddProjectMembersForm()  # initialize form
1301

    
1302
        project, application = get_by_chain_or_404(chain_id)
1303
        if project:
1304
            members = project.projectmembership_set.select_related()
1305
            members_table = tables.ProjectMembersTable(project,
1306
                                                       members,
1307
                                                       user=request.user,
1308
                                                       prefix="members_")
1309
            RequestConfig(request, paginate={"per_page": PAGINATE_BY}
1310
                          ).configure(members_table)
1311

    
1312
        else:
1313
            members_table = None
1314

    
1315
    else: # is application
1316
        application_id = chain_or_app_id
1317
        application = get_object_or_404(ProjectApplication, pk=application_id)
1318
        members_table = None
1319
        addmembers_form = None
1320

    
1321
    modifications_table = None
1322

    
1323
    user = request.user
1324
    is_project_admin = user.is_project_admin(application_id=application.id)
1325
    is_owner = user.owns_application(application)
1326
    if not (is_owner or is_project_admin) and not project_view:
1327
        m = _(astakos_messages.NOT_ALLOWED)
1328
        raise PermissionDenied(m)
1329

    
1330
    if (not (is_owner or is_project_admin) and project_view and
1331
        not user.non_owner_can_view(project)):
1332
        m = _(astakos_messages.NOT_ALLOWED)
1333
        raise PermissionDenied(m)
1334

    
1335
    following_applications = list(application.pending_modifications())
1336
    following_applications.reverse()
1337
    modifications_table = (
1338
        tables.ProjectModificationApplicationsTable(following_applications,
1339
                                                    user=request.user,
1340
                                                    prefix="modifications_"))
1341

    
1342
    mem_display = user.membership_display(project) if project else None
1343
    can_join_req = can_join_request(project, user) if project else False
1344
    can_leave_req = can_leave_request(project, user) if project else False
1345

    
1346
    return object_detail(
1347
        request,
1348
        queryset=ProjectApplication.objects.select_related(),
1349
        object_id=application.id,
1350
        template_name='im/projects/project_detail.html',
1351
        extra_context={
1352
            'project_view': project_view,
1353
            'addmembers_form':addmembers_form,
1354
            'members_table': members_table,
1355
            'owner_mode': is_owner,
1356
            'admin_mode': is_project_admin,
1357
            'modifications_table': modifications_table,
1358
            'mem_display': mem_display,
1359
            'can_join_request': can_join_req,
1360
            'can_leave_request': can_leave_req,
1361
            })
1362

    
1363
@require_http_methods(["GET", "POST"])
1364
@valid_astakos_user_required
1365
def project_search(request):
1366
    q = request.GET.get('q', '')
1367
    form = ProjectSearchForm()
1368
    q = q.strip()
1369

    
1370
    if request.method == "POST":
1371
        form = ProjectSearchForm(request.POST)
1372
        if form.is_valid():
1373
            q = form.cleaned_data['q'].strip()
1374
        else:
1375
            q = None
1376

    
1377
    if q is None:
1378
        projects = ProjectApplication.objects.none()
1379
    else:
1380
        accepted_projects = request.user.projectmembership_set.filter(
1381
            ~Q(acceptance_date__isnull=True)).values_list('project', flat=True)
1382
        projects = ProjectApplication.objects.search_by_name(q)
1383
        projects = projects.filter(~Q(project__last_approval_date__isnull=True))
1384
        projects = projects.exclude(project__in=accepted_projects)
1385

    
1386
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1387
                                                prefix="my_projects_")
1388
    if request.method == "POST":
1389
        table.caption = _('SEARCH RESULTS')
1390
    else:
1391
        table.caption = _('ALL PROJECTS')
1392

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

    
1395
    return object_list(
1396
        request,
1397
        projects,
1398
        template_name='im/projects/project_list.html',
1399
        extra_context={
1400
          'form': form,
1401
          'is_search': True,
1402
          'q': q,
1403
          'table': table
1404
        })
1405

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

    
1414
    with ExceptionHandler(request):
1415
        _project_join(request, chain_id)
1416

    
1417

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

    
1421

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

    
1435

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

    
1443
    with ExceptionHandler(request):
1444
        _project_leave(request, chain_id)
1445

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

    
1449

    
1450
@commit_on_success_strict()
1451
def _project_leave(request, chain_id):
1452
    try:
1453
        chain_id = int(chain_id)
1454
        auto_accepted = leave_project(chain_id, request.user.id)
1455
        if auto_accepted:
1456
            m = _(astakos_messages.USER_LEFT_PROJECT)
1457
        else:
1458
            m = _(astakos_messages.USER_LEAVE_REQUEST_SUBMITTED)
1459
        messages.success(request, m)
1460
    except (IOError, PermissionDenied), e:
1461
        messages.error(request, e)
1462

    
1463

    
1464
@require_http_methods(["POST"])
1465
@valid_astakos_user_required
1466
def project_cancel(request, chain_id):
1467
    next = request.GET.get('next')
1468
    if not next:
1469
        next = reverse('astakos.im.views.project_list')
1470

    
1471
    with ExceptionHandler(request):
1472
        _project_cancel(request, chain_id)
1473

    
1474
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1475
    return redirect(next)
1476

    
1477

    
1478
@commit_on_success_strict()
1479
def _project_cancel(request, chain_id):
1480
    try:
1481
        chain_id = int(chain_id)
1482
        cancel_membership(chain_id, request.user)
1483
        m = _(astakos_messages.USER_REQUEST_CANCELLED)
1484
        messages.success(request, m)
1485
    except (IOError, PermissionDenied), e:
1486
        messages.error(request, e)
1487

    
1488

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

    
1493
    with ExceptionHandler(request):
1494
        _project_accept_member(request, chain_id, user_id)
1495

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

    
1498

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

    
1512

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

    
1517
    with ExceptionHandler(request):
1518
        _project_remove_member(request, chain_id, user_id)
1519

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

    
1522

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

    
1536

    
1537
@require_http_methods(["POST"])
1538
@valid_astakos_user_required
1539
def project_reject_member(request, chain_id, user_id):
1540

    
1541
    with ExceptionHandler(request):
1542
        _project_reject_member(request, chain_id, user_id)
1543

    
1544
    return redirect(reverse('project_detail', args=(chain_id,)))
1545

    
1546

    
1547
@commit_on_success_strict()
1548
def _project_reject_member(request, chain_id, user_id):
1549
    try:
1550
        chain_id = int(chain_id)
1551
        user_id = int(user_id)
1552
        m = reject_membership(chain_id, user_id, request.user)
1553
    except (IOError, PermissionDenied), e:
1554
        messages.error(request, e)
1555
    else:
1556
        email = escape(m.person.email)
1557
        msg = _(astakos_messages.USER_MEMBERSHIP_REJECTED) % email
1558
        messages.success(request, msg)
1559

    
1560

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

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

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

    
1575
    with ExceptionHandler(request):
1576
        _project_app_approve(request, application_id)
1577

    
1578
    chain_id = get_related_project_id(application_id)
1579
    return redirect(reverse('project_detail', args=(chain_id,)))
1580

    
1581

    
1582
@commit_on_success_strict()
1583
def _project_app_approve(request, application_id):
1584
    approve_application(application_id)
1585

    
1586

    
1587
@require_http_methods(["POST"])
1588
@signed_terms_required
1589
@login_required
1590
def project_app_deny(request, application_id):
1591

    
1592
    reason = request.POST.get('reason', None)
1593
    if not reason:
1594
        reason = None
1595

    
1596
    if not request.user.is_project_admin():
1597
        m = _(astakos_messages.NOT_ALLOWED)
1598
        raise PermissionDenied(m)
1599

    
1600
    try:
1601
        app = ProjectApplication.objects.get(id=application_id)
1602
    except ProjectApplication.DoesNotExist:
1603
        raise Http404
1604

    
1605
    with ExceptionHandler(request):
1606
        _project_app_deny(request, application_id, reason)
1607

    
1608
    return redirect(reverse('project_list'))
1609

    
1610

    
1611
@commit_on_success_strict()
1612
def _project_app_deny(request, application_id, reason):
1613
    deny_application(application_id, reason=reason)
1614

    
1615

    
1616
@require_http_methods(["POST"])
1617
@signed_terms_required
1618
@login_required
1619
def project_app_dismiss(request, application_id):
1620
    try:
1621
        app = ProjectApplication.objects.get(id=application_id)
1622
    except ProjectApplication.DoesNotExist:
1623
        raise Http404
1624

    
1625
    if not request.user.owns_application(app):
1626
        m = _(astakos_messages.NOT_ALLOWED)
1627
        raise PermissionDenied(m)
1628

    
1629
    with ExceptionHandler(request):
1630
        _project_app_dismiss(request, application_id)
1631

    
1632
    chain_id = None
1633
    chain_id = get_related_project_id(application_id)
1634
    if chain_id:
1635
        next = reverse('project_detail', args=(chain_id,))
1636
    else:
1637
        next = reverse('project_list')
1638
    return redirect(next)
1639

    
1640

    
1641
def _project_app_dismiss(request, application_id):
1642
    # XXX: dismiss application also does authorization
1643
    dismiss_application(application_id, request_user=request.user)
1644

    
1645

    
1646
@require_http_methods(["GET"])
1647
@required_auth_methods_assigned(allow_access=True)
1648
@login_required
1649
@signed_terms_required
1650
def landing(request):
1651
    context = {'services': Service.catalog(orderfor='dashboard')}
1652
    return render_response(
1653
        'im/landing.html',
1654
        context_instance=get_context(request), **context)
1655

    
1656

    
1657
def api_access(request):
1658
    return render_response(
1659
        'im/api_access.html',
1660
        context_instance=get_context(request))