Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 7f3c4920

History | View | Annotate | Download (58.9 kB)

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

    
34
import logging
35
import calendar
36
import inflect
37

    
38
engine = inflect.engine()
39

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

    
45
from django_tables2 import RequestConfig
46

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

    
75
import astakos.im.messages as astakos_messages
76

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

    
121
logger = logging.getLogger(__name__)
122

    
123
callpoint = AstakosCallpoint()
124

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

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

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

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

    
159

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

    
174

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

    
190

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

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

    
212

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

    
216

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

223
    **Arguments**
224

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

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

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

236
    **Template:**
237

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

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

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

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

    
256

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

    
269

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

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

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

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

286
    **Arguments**
287

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

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

295
    **Template:**
296

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

299
    **Settings:**
300

301
    The view expectes the following settings are defined:
302

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

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

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

    
347

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

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

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

362
    **Arguments**
363

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

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

371
    **Template:**
372

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

375
    **Settings:**
376

377
    The view expectes the following settings are defined:
378

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

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

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

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

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

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

    
437

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

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

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

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

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

457
    **Arguments**
458

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

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

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

469
    **Template:**
470

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
569

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

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

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

583
    **Arguments**
584

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

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

592
    **Template:**
593

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

596
    **Settings:**
597

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

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

    
624

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

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

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

    
664

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

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

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

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

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

    
715

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

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

    
742
    terms = f.read()
743

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

    
768

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

    
778

    
779
    if not astakos_settings.EMAILCHANGE_ENABLED:
780
        raise PermissionDenied
781

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

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

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

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

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

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

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

    
838

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

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

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

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

    
861

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

    
866
    resources_meta = presentation.RESOURCES
867

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

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

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

    
891

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

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

    
908

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

    
914

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

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

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

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

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

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

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

    
1026

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

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

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

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

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

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

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

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

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

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

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

    
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
    if not user.is_project_admin():
1116
        ok, limit = qh_add_pending_app(user, dry_run=True)
1117
        if not ok:
1118
            m = _(astakos_messages.PENDING_APPLICATION_LIMIT_ADD) % limit
1119
            messages.error(request, m)
1120
            next = reverse('astakos.im.views.project_list')
1121
            next = restrict_next(next, domain=COOKIE_DOMAIN)
1122
            return redirect(next)
1123

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

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

    
1153
    if response is not None:
1154
        return response
1155

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

    
1160

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

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

    
1178

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

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

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

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

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

    
1211

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

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

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

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

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

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

    
1267
    if response is not None:
1268
        return response
1269

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

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

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

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

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

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

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

    
1324
        else:
1325
            members_table = None
1326

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

    
1333
    modifications_table = None
1334

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1429

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

    
1433

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

    
1447

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

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

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

    
1461

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

    
1475

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

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

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

    
1489

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

    
1500

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

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

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

    
1510

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

    
1524

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

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

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

    
1534

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

    
1548

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

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

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

    
1558

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

    
1572

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

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

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

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

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

    
1593

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

    
1598

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

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

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

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

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

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

    
1622

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

    
1627

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

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

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

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

    
1652

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

    
1657

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

    
1668

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