Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ b095201e

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

    
119
logger = logging.getLogger(__name__)
120

    
121

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

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

    
144
            if not provider or not provider.is_active():
145
                raise PermissionDenied
146

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

    
156

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

    
171

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

    
187

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

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

    
209

    
210
def valid_astakos_user_required(func):
211
    return signed_terms_required(required_auth_methods_assigned()(login_required(func)))
212

    
213

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

220
    **Arguments**
221

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

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

230
    ``extra_context``
231
        An dictionary of variables to add to the template context.
232

233
    **Template:**
234

235
    im/profile.html or im/login.html or ``template_name`` keyword argument.
236

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

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

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

    
253

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

    
266

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

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

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

281
    If the user isn't logged in, redirects to settings.LOGIN_URL.
282

283
    **Arguments**
284

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

289
    ``extra_context``
290
        An dictionary of variables to add to the template context.
291

292
    **Template:**
293

294
    im/invitations.html or ``template_name`` keyword argument.
295

296
    **Settings:**
297

298
    The view expectes the following settings are defined:
299

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

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

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

    
344

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

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

357
    If the user isn't logged in, redirects to settings.LOGIN_URL.
358

359
    **Arguments**
360

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

365
    ``extra_context``
366
        An dictionary of variables to add to the template context.
367

368
    **Template:**
369

370
    im/profile.html or ``template_name`` keyword argument.
371

372
    **Settings:**
373

374
    The view expectes the following settings are defined:
375

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

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

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

    
418
    # existing providers
419
    user_providers = request.user.get_enabled_auth_providers()
420
    user_disabled_providers = request.user.get_disabled_auth_providers()
421

    
422
    # providers that user can add
423
    user_available_providers = request.user.get_available_auth_providers()
424

    
425
    extra_context['services'] = Service.catalog().values()
426
    return render_response(template_name,
427
                           profile_form = form,
428
                           user_providers = user_providers,
429
                           user_disabled_providers = user_disabled_providers,
430
                           user_available_providers = user_available_providers,
431
                           context_instance = get_context(request,
432
                                                          extra_context))
433

    
434

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

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

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

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

452
    On unsuccessful creation, renders ``template_name`` with an error message.
453

454
    **Arguments**
455

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

460
    ``extra_context``
461
        An dictionary of variables to add to the template context.
462

463
    ``on_success``
464
        Resolvable view name to redirect on registration success.
465

466
    **Template:**
467

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

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

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

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

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

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

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

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

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

    
521
            try:
522
                form.store_user(user, request)
523

    
524
                result = backend.handle_activation(user)
525
                status = messages.SUCCESS
526
                message = result.message
527

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

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

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

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

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

    
566

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

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

578
    If the user isn't logged in, redirects to settings.LOGIN_URL.
579

580
    **Arguments**
581

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

586
    ``extra_context``
587
        An dictionary of variables to add to the template context.
588

589
    **Template:**
590

591
    im/signup.html or ``template_name`` keyword argument.
592

593
    **Settings:**
594

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

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

    
621

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

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

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

    
661

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

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

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

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

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

    
712

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

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

    
739
    terms = f.read()
740

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

    
765

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

    
775

    
776
    if not astakos_settings.EMAILCHANGE_ENABLED:
777
        raise PermissionDenied
778

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

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

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

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

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

    
826
    if request.user.email_change_is_pending():
827
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
828

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

    
835

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

    
838
    if request.user.is_authenticated():
839
        return HttpResponseRedirect(reverse('edit_profile'))
840

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

    
856
    return HttpResponseRedirect(reverse('index'))
857

    
858

    
859
@require_http_methods(["GET"])
860
@valid_astakos_user_required
861
def resource_usage(request):
862

    
863
    resources_meta = presentation.RESOURCES
864

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

    
874
    resource_catalog = json.dumps(resource_catalog)
875
    resource_groups = json.dumps(resource_groups)
876
    resources_order = json.dumps(resources_meta.get('resources_order'))
877

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

    
888

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

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

    
905

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

    
911

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

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

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

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

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

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

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

    
1023

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

    
1037
    # resources in database
1038
    resource_details = map(lambda obj: model_to_dict(obj, exclude=[]),
1039
                           Resource.objects.all())
1040
    # initialize resource_catalog to contain all group/resource information
1041
    for r in resource_details:
1042
        if not r.get('group') in resource_groups:
1043
            resource_groups[r.get('group')] = {'icon': 'unknown'}
1044

    
1045
    resource_keys = [r.get('str_repr') for r in resource_details]
1046
    resource_catalog = [[g, filter(lambda r: r.get('group', '') == g,
1047
                                   resource_details)] for g in resource_groups]
1048

    
1049
    # order groups, also include unknown groups
1050
    groups_order = resources_meta.get('groups_order')
1051
    for g in resource_groups.keys():
1052
        if not g in groups_order:
1053
            groups_order.append(g)
1054

    
1055
    # order resources, also include unknown resources
1056
    resources_order = resources_meta.get('resources_order')
1057
    for r in resource_keys:
1058
        if not r in resources_order:
1059
            resources_order.append(r)
1060

    
1061
    # sort catalog groups
1062
    resource_catalog = sorted(resource_catalog,
1063
                              key=lambda g: groups_order.index(g[0]))
1064

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

    
1072
    # sort resources
1073
    def resourceindex(r):
1074
        return resources_order.index(r['str_repr'])
1075

    
1076
    for index, group in enumerate(resource_catalog):
1077
        resource_catalog[index][1] = sorted(resource_catalog[index][1],
1078
                                            key=resourceindex)
1079
        if len(resource_catalog[index][1]) == 0:
1080
            resource_catalog.pop(index)
1081
            for gindex, g in enumerate(resource_groups):
1082
                if g[0] == group[0]:
1083
                    resource_groups.pop(gindex)
1084

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

    
1095
    # cleanup empty groups
1096
    for group_index, group_resources in enumerate(list(resource_catalog)):
1097
        group, resources = group_resources
1098
        if len(resources) == 0:
1099
            resource_catalog.pop(group_index)
1100
            resource_groups.pop(group)
1101

    
1102

    
1103
    return resource_catalog, resource_groups
1104

    
1105

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

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

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

    
1148
    if response is not None:
1149
        return response
1150

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

    
1155

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

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

    
1173

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

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

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

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

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

    
1206

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

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

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

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

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

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

    
1262
    if response is not None:
1263
        return response
1264

    
1265
    next = reverse('astakos.im.views.project_list')
1266
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1267
    return redirect(next)
1268

    
1269
@require_http_methods(["GET", "POST"])
1270
@valid_astakos_user_required
1271
def project_app(request, application_id):
1272
    return common_detail(request, application_id, project_view=False)
1273

    
1274
@require_http_methods(["GET", "POST"])
1275
@valid_astakos_user_required
1276
def project_detail(request, chain_id):
1277
    return common_detail(request, chain_id)
1278

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

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

    
1304
            if addmembers_form.is_valid():
1305
                addmembers_form = AddProjectMembersForm()  # clear form data
1306
        else:
1307
            addmembers_form = AddProjectMembersForm()  # initialize form
1308

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

    
1319
        else:
1320
            members_table = None
1321

    
1322
    else: # is application
1323
        application_id = chain_or_app_id
1324
        application = get_object_or_404(ProjectApplication, pk=application_id)
1325
        members_table = None
1326
        addmembers_form = None
1327

    
1328
    modifications_table = None
1329

    
1330
    user = request.user
1331
    is_project_admin = user.is_project_admin(application_id=application.id)
1332
    is_owner = user.owns_application(application)
1333
    if not (is_owner or is_project_admin) and not project_view:
1334
        m = _(astakos_messages.NOT_ALLOWED)
1335
        raise PermissionDenied(m)
1336

    
1337
    if (not (is_owner or is_project_admin) and project_view and
1338
        not user.non_owner_can_view(project)):
1339
        m = _(astakos_messages.NOT_ALLOWED)
1340
        raise PermissionDenied(m)
1341

    
1342
    following_applications = list(application.pending_modifications())
1343
    following_applications.reverse()
1344
    modifications_table = (
1345
        tables.ProjectModificationApplicationsTable(following_applications,
1346
                                                    user=request.user,
1347
                                                    prefix="modifications_"))
1348

    
1349
    mem_display = user.membership_display(project) if project else None
1350
    can_join_req = can_join_request(project, user) if project else False
1351
    can_leave_req = can_leave_request(project, user) if project else False
1352

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

    
1370
@require_http_methods(["GET", "POST"])
1371
@valid_astakos_user_required
1372
def project_search(request):
1373
    q = request.GET.get('q', '')
1374
    form = ProjectSearchForm()
1375
    q = q.strip()
1376

    
1377
    if request.method == "POST":
1378
        form = ProjectSearchForm(request.POST)
1379
        if form.is_valid():
1380
            q = form.cleaned_data['q'].strip()
1381
        else:
1382
            q = None
1383

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

    
1393
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1394
                                                prefix="my_projects_")
1395
    if request.method == "POST":
1396
        table.caption = _('SEARCH RESULTS')
1397
    else:
1398
        table.caption = _('ALL PROJECTS')
1399

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

    
1402
    return object_list(
1403
        request,
1404
        projects,
1405
        template_name='im/projects/project_list.html',
1406
        extra_context={
1407
          'form': form,
1408
          'is_search': True,
1409
          'q': q,
1410
          'table': table
1411
        })
1412

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

    
1421
    with ExceptionHandler(request):
1422
        _project_join(request, chain_id)
1423

    
1424

    
1425
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1426
    return redirect(next)
1427

    
1428

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

    
1442

    
1443
@require_http_methods(["POST"])
1444
@valid_astakos_user_required
1445
def project_leave(request, chain_id):
1446
    next = request.GET.get('next')
1447
    if not next:
1448
        next = reverse('astakos.im.views.project_list')
1449

    
1450
    with ExceptionHandler(request):
1451
        _project_leave(request, chain_id)
1452

    
1453
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1454
    return redirect(next)
1455

    
1456

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

    
1470

    
1471
@require_http_methods(["POST"])
1472
@valid_astakos_user_required
1473
def project_cancel(request, chain_id):
1474
    next = request.GET.get('next')
1475
    if not next:
1476
        next = reverse('astakos.im.views.project_list')
1477

    
1478
    with ExceptionHandler(request):
1479
        _project_cancel(request, chain_id)
1480

    
1481
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1482
    return redirect(next)
1483

    
1484

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

    
1495

    
1496
@require_http_methods(["POST"])
1497
@valid_astakos_user_required
1498
def project_accept_member(request, chain_id, memb_id):
1499

    
1500
    with ExceptionHandler(request):
1501
        _project_accept_member(request, chain_id, memb_id)
1502

    
1503
    return redirect(reverse('project_detail', args=(chain_id,)))
1504

    
1505

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

    
1519

    
1520
@require_http_methods(["POST"])
1521
@valid_astakos_user_required
1522
def project_remove_member(request, chain_id, memb_id):
1523

    
1524
    with ExceptionHandler(request):
1525
        _project_remove_member(request, chain_id, memb_id)
1526

    
1527
    return redirect(reverse('project_detail', args=(chain_id,)))
1528

    
1529

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

    
1543

    
1544
@require_http_methods(["POST"])
1545
@valid_astakos_user_required
1546
def project_reject_member(request, chain_id, memb_id):
1547

    
1548
    with ExceptionHandler(request):
1549
        _project_reject_member(request, chain_id, memb_id)
1550

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

    
1553

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

    
1567

    
1568
@require_http_methods(["POST"])
1569
@signed_terms_required
1570
@login_required
1571
def project_app_approve(request, application_id):
1572

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

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

    
1582
    with ExceptionHandler(request):
1583
        _project_app_approve(request, application_id)
1584

    
1585
    chain_id = get_related_project_id(application_id)
1586
    return redirect(reverse('project_detail', args=(chain_id,)))
1587

    
1588

    
1589
@commit_on_success_strict()
1590
def _project_app_approve(request, application_id):
1591
    approve_application(application_id)
1592

    
1593

    
1594
@require_http_methods(["POST"])
1595
@signed_terms_required
1596
@login_required
1597
def project_app_deny(request, application_id):
1598

    
1599
    reason = request.POST.get('reason', None)
1600
    if not reason:
1601
        reason = None
1602

    
1603
    if not request.user.is_project_admin():
1604
        m = _(astakos_messages.NOT_ALLOWED)
1605
        raise PermissionDenied(m)
1606

    
1607
    try:
1608
        app = ProjectApplication.objects.get(id=application_id)
1609
    except ProjectApplication.DoesNotExist:
1610
        raise Http404
1611

    
1612
    with ExceptionHandler(request):
1613
        _project_app_deny(request, application_id, reason)
1614

    
1615
    return redirect(reverse('project_list'))
1616

    
1617

    
1618
@commit_on_success_strict()
1619
def _project_app_deny(request, application_id, reason):
1620
    deny_application(application_id, reason=reason)
1621

    
1622

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

    
1632
    if not request.user.owns_application(app):
1633
        m = _(astakos_messages.NOT_ALLOWED)
1634
        raise PermissionDenied(m)
1635

    
1636
    with ExceptionHandler(request):
1637
        _project_app_dismiss(request, application_id)
1638

    
1639
    chain_id = None
1640
    chain_id = get_related_project_id(application_id)
1641
    if chain_id:
1642
        next = reverse('project_detail', args=(chain_id,))
1643
    else:
1644
        next = reverse('project_list')
1645
    return redirect(next)
1646

    
1647

    
1648
def _project_app_dismiss(request, application_id):
1649
    # XXX: dismiss application also does authorization
1650
    dismiss_application(application_id, request_user=request.user)
1651

    
1652

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

    
1663

    
1664
def api_access(request):
1665
    return render_response(
1666
        'im/api_access.html',
1667
        context_instance=get_context(request))