Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 9cdb86fd

History | View | Annotate | Download (57 kB)

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

    
34
import logging
35
import calendar
36
import inflect
37

    
38
engine = inflect.engine()
39

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

    
45
from django_tables2 import RequestConfig
46

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

    
75
import astakos.im.messages as astakos_messages
76

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

    
121
logger = logging.getLogger(__name__)
122

    
123
callpoint = AstakosCallpoint()
124

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

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

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

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

    
160

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

    
175

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

    
191

    
192
def required_auth_methods_assigned(only_warn=False):
193
    """
194
    Decorator that checks whether the request.user has all required auth providers
195
    assigned.
196
    """
197
    required_providers = auth_providers.REQUIRED_PROVIDERS.keys()
198

    
199
    def decorator(func):
200
        if not required_providers:
201
            return func
202

    
203
        @wraps(func)
204
        def wrapper(request, *args, **kwargs):
205
            if request.user.is_authenticated():
206
                for required in required_providers:
207
                    if not request.user.has_auth_provider(required):
208
                        provider = auth_providers.get_provider(required)
209
                        if only_warn:
210
                            messages.error(request,
211
                                           _(astakos_messages.AUTH_PROVIDER_REQUIRED  % {
212
                                               'provider': provider.get_title_display}))
213
                        else:
214
                            return HttpResponseRedirect(reverse('edit_profile'))
215
            return func(request, *args, **kwargs)
216
        return wrapper
217
    return decorator
218

    
219

    
220
def valid_astakos_user_required(func):
221
    return signed_terms_required(required_auth_methods_assigned()(login_required(func)))
222

    
223

    
224
@require_http_methods(["GET", "POST"])
225
@signed_terms_required
226
def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context=None):
227
    """
228
    If there is logged on user renders the profile page otherwise renders login page.
229

230
    **Arguments**
231

232
    ``login_template_name``
233
        A custom login template to use. This is optional; if not specified,
234
        this will default to ``im/login.html``.
235

236
    ``profile_template_name``
237
        A custom profile template to use. This is optional; if not specified,
238
        this will default to ``im/profile.html``.
239

240
    ``extra_context``
241
        An dictionary of variables to add to the template context.
242

243
    **Template:**
244

245
    im/profile.html or im/login.html or ``template_name`` keyword argument.
246

247
    """
248
    extra_context = extra_context or {}
249
    template_name = login_template_name
250
    if request.user.is_authenticated():
251
        return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
252

    
253
    third_party_token = request.GET.get('key', False)
254
    if third_party_token:
255
        messages.info(request, astakos_messages.AUTH_PROVIDER_LOGIN_TO_ADD)
256

    
257
    return render_response(
258
        template_name,
259
        login_form = LoginForm(request=request),
260
        context_instance = get_context(request, extra_context)
261
    )
262

    
263

    
264
@require_http_methods(["POST"])
265
@valid_astakos_user_required
266
def update_token(request):
267
    """
268
    Update api token view.
269
    """
270
    user = request.user
271
    user.renew_token()
272
    user.save()
273
    messages.success(request, astakos_messages.TOKEN_UPDATED)
274
    return HttpResponseRedirect(reverse('edit_profile'))
275

    
276

    
277
@require_http_methods(["GET", "POST"])
278
@valid_astakos_user_required
279
@transaction.commit_manually
280
def invite(request, template_name='im/invitations.html', extra_context=None):
281
    """
282
    Allows a user to invite somebody else.
283

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

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

291
    If the user isn't logged in, redirects to settings.LOGIN_URL.
292

293
    **Arguments**
294

295
    ``template_name``
296
        A custom template to use. This is optional; if not specified,
297
        this will default to ``im/invitations.html``.
298

299
    ``extra_context``
300
        An dictionary of variables to add to the template context.
301

302
    **Template:**
303

304
    im/invitations.html or ``template_name`` keyword argument.
305

306
    **Settings:**
307

308
    The view expectes the following settings are defined:
309

310
    * LOGIN_URL: login uri
311
    """
312
    extra_context = extra_context or {}
313
    status = None
314
    message = None
315
    form = InvitationForm()
316

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

    
343
    sent = [{'email': inv.username,
344
             'realname': inv.realname,
345
             'is_consumed': inv.is_consumed}
346
            for inv in request.user.invitations_sent.all()]
347
    kwargs = {'inviter': inviter,
348
              'sent': sent}
349
    context = get_context(request, extra_context, **kwargs)
350
    return render_response(template_name,
351
                           invitation_form=form,
352
                           context_instance=context)
353

    
354

    
355
@require_http_methods(["GET", "POST"])
356
@required_auth_methods_assigned(only_warn=True)
357
@login_required
358
@signed_terms_required
359
def edit_profile(request, template_name='im/profile.html', extra_context=None):
360
    """
361
    Allows a user to edit his/her profile.
362

363
    In case of GET request renders a form for displaying the user information.
364
    In case of POST updates the user informantion and redirects to ``next``
365
    url parameter if exists.
366

367
    If the user isn't logged in, redirects to settings.LOGIN_URL.
368

369
    **Arguments**
370

371
    ``template_name``
372
        A custom template to use. This is optional; if not specified,
373
        this will default to ``im/profile.html``.
374

375
    ``extra_context``
376
        An dictionary of variables to add to the template context.
377

378
    **Template:**
379

380
    im/profile.html or ``template_name`` keyword argument.
381

382
    **Settings:**
383

384
    The view expectes the following settings are defined:
385

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

    
411
                if form.email_changed:
412
                    msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
413
                    messages.success(request, msg)
414
                if form.password_changed:
415
                    msg = _(astakos_messages.PASSWORD_CHANGED)
416
                    messages.success(request, msg)
417

    
418
                if next:
419
                    return redirect(next)
420
                else:
421
                    return redirect(reverse('edit_profile'))
422
            except ValueError, ve:
423
                messages.success(request, ve)
424
    elif request.method == "GET":
425
        request.user.is_verified = True
426
        request.user.save()
427

    
428
    # existing providers
429
    user_providers = request.user.get_active_auth_providers()
430

    
431
    # providers that user can add
432
    user_available_providers = request.user.get_available_auth_providers()
433

    
434
    extra_context['services'] = get_services_dict()
435
    return render_response(template_name,
436
                           profile_form = form,
437
                           user_providers = user_providers,
438
                           user_available_providers = user_available_providers,
439
                           context_instance = get_context(request,
440
                                                          extra_context))
441

    
442

    
443
@transaction.commit_manually
444
@require_http_methods(["GET", "POST"])
445
def signup(request, template_name='im/signup.html', on_success='index', extra_context=None, backend=None):
446
    """
447
    Allows a user to create a local account.
448

449
    In case of GET request renders a form for entering the user information.
450
    In case of POST handles the signup.
451

452
    The user activation will be delegated to the backend specified by the ``backend`` keyword argument
453
    if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
454
    if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
455
    (see activation_backends);
456

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

460
    On unsuccessful creation, renders ``template_name`` with an error message.
461

462
    **Arguments**
463

464
    ``template_name``
465
        A custom template to render. This is optional;
466
        if not specified, this will default to ``im/signup.html``.
467

468
    ``extra_context``
469
        An dictionary of variables to add to the template context.
470

471
    ``on_success``
472
        Resolvable view name to redirect on registration success.
473

474
    **Template:**
475

476
    im/signup.html or ``template_name`` keyword argument.
477
    """
478
    extra_context = extra_context or {}
479
    if request.user.is_authenticated():
480
        return HttpResponseRedirect(reverse('edit_profile'))
481

    
482
    provider = get_query(request).get('provider', 'local')
483
    if not auth_providers.get_provider(provider).is_available_for_create():
484
        raise PermissionDenied
485

    
486
    id = get_query(request).get('id')
487
    try:
488
        instance = AstakosUser.objects.get(id=id) if id else None
489
    except AstakosUser.DoesNotExist:
490
        instance = None
491

    
492
    pending_user = None
493
    third_party_token = request.REQUEST.get('third_party_token', None)
494
    if third_party_token:
495
        pending = get_object_or_404(PendingThirdPartyUser,
496
                                    token=third_party_token)
497
        provider = pending.provider
498
        instance = pending.get_user_instance()
499
        if pending.existing_user().count() > 0:
500
            pending_user = pending.existing_user().get()
501
            if request.method == "GET":
502
                messages.warning(request, pending_user.get_inactive_message())
503

    
504

    
505
    extra_context['pending_user_exists'] = pending_user
506

    
507
    try:
508
        if not backend:
509
            backend = get_backend(request)
510
        form = backend.get_signup_form(provider, instance)
511
    except Exception, e:
512
        form = SimpleBackend(request).get_signup_form(provider)
513
        messages.error(request, e)
514
    if request.method == 'POST':
515
        if form.is_valid():
516
            user = form.save(commit=False)
517

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

    
522
            try:
523
                result = backend.handle_activation(user)
524
                status = messages.SUCCESS
525
                message = result.message
526

    
527
                form.store_user(user, request)
528

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

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

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

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

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

    
567

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

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

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

581
    **Arguments**
582

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

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

590
    **Template:**
591

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

594
    **Settings:**
595

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

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

    
622

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

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

    
644
    if next:
645
        response['Location'] = next
646
        response.status_code = 302
647
    elif LOGOUT_NEXT:
648
        response['Location'] = LOGOUT_NEXT
649
        response.status_code = 301
650
    else:
651
        message = _(astakos_messages.LOGOUT_SUCCESS)
652
        last_provider = request.COOKIES.get('astakos_last_login_method', None)
653
        if last_provider:
654
            provider = auth_providers.get_provider(last_provider)
655
            extra_message = provider.get_logout_message_display
656
            if extra_message:
657
                message += '<br />' + extra_message
658
        messages.success(request, message)
659
        response['Location'] = reverse('index')
660
        response.status_code = 301
661
    return response
662

    
663

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

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

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

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

    
708

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

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

    
735
    terms = f.read()
736

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

    
761

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

    
771

    
772
    if not astakos_settings.EMAILCHANGE_ENABLED:
773
        raise PermissionDenied
774

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

    
789
        return render_response(confirm_template_name,
790
                               modified_user=user if 'user' in locals() \
791
                               else None, context_instance=get_context(request,
792
                                                            extra_context))
793

    
794
    if not request.user.is_authenticated():
795
        path = quote(request.get_full_path())
796
        url = request.build_absolute_uri(reverse('index'))
797
        return HttpResponseRedirect(url + '?next=' + path)
798

    
799
    # clean up expired email changes
800
    if request.user.email_change_is_pending():
801
        change = request.user.emailchanges.get()
802
        if change.activation_key_expired():
803
            change.delete()
804
            transaction.commit()
805
            return HttpResponseRedirect(reverse('email_change'))
806

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

    
822
    if request.user.email_change_is_pending():
823
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
824

    
825
    return render_response(
826
        form_template_name,
827
        form=form,
828
        context_instance=get_context(request, extra_context)
829
    )
830

    
831

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

    
834
    if request.user.is_authenticated():
835
        return HttpResponseRedirect(reverse('edit_profile'))
836

    
837
    # TODO: check if moderation is only enabled for local login
838
    if astakos_settings.MODERATION_ENABLED:
839
        raise PermissionDenied
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
        try:
848
            send_activation_func(u)
849
            msg = _(astakos_messages.ACTIVATION_SENT)
850
            messages.success(request, msg)
851
        except SendMailError, e:
852
            messages.error(request, e)
853

    
854
    return HttpResponseRedirect(reverse('index'))
855

    
856

    
857
@require_http_methods(["GET"])
858
@valid_astakos_user_required
859
def resource_usage(request):
860

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

    
882
    def pluralize(entry):
883
        entry['plural'] = engine.plural(entry.get('name'))
884
        return entry
885

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

    
897
    if request.REQUEST.get('json', None):
898
        return HttpResponse(json.dumps(backenddata),
899
                            mimetype="application/json")
900

    
901
    return render_response('im/resource_usage.html',
902
                           context_instance=get_context(request),
903
                           resource_usage=backenddata,
904
                           usage_update_interval=astakos_settings.USAGE_UPDATE_INTERVAL,
905
                           result=result)
906

    
907
# TODO: action only on POST and user should confirm the removal
908
@require_http_methods(["GET", "POST"])
909
@login_required
910
@signed_terms_required
911
def remove_auth_provider(request, pk):
912
    try:
913
        provider = request.user.auth_providers.get(pk=pk)
914
    except AstakosUserAuthProvider.DoesNotExist:
915
        raise Http404
916

    
917
    if provider.can_remove():
918
        provider.delete()
919
        message = astakos_messages.AUTH_PROVIDER_REMOVED % \
920
                            provider.settings.get_method_prompt_display
921
        user = request.user
922
        logger.info("%s deleted %s provider (%d): %r" % (user.log_display,
923
                                                         provider.module,
924
                                                         int(pk),
925
                                                         provider.info))
926
        messages.success(request, message)
927
        return HttpResponseRedirect(reverse('edit_profile'))
928
    else:
929
        raise PermissionDenied
930

    
931

    
932
def how_it_works(request):
933
    return render_response(
934
        'im/how_it_works.html',
935
        context_instance=get_context(request))
936

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

    
948
    if extra_context is None: extra_context = {}
949
    if login_required and not request.user.is_authenticated():
950
        return redirect_to_login(request.path)
951
    try:
952

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

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

    
1005
    if extra_context is None: extra_context = {}
1006
    if login_required and not request.user.is_authenticated():
1007
        return redirect_to_login(request.path)
1008

    
1009
    try:
1010
        model, form_class = get_model_and_form_class(model, form_class)
1011
        obj = lookup_object(model, object_id, slug, slug_field)
1012

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

    
1051
@require_http_methods(["GET", "POST"])
1052
@signed_terms_required
1053
@login_required
1054
def project_add(request):
1055

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

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

    
1082
    # order resources
1083
    groups_order = RESOURCES_PRESENTATION_DATA.get('groups_order')
1084
    resources_order = RESOURCES_PRESENTATION_DATA.get('resources_order')
1085
    resource_catalog = sorted(resource_catalog, key=lambda g:groups_order.index(g[0]))
1086

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

    
1094

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

    
1110

    
1111
@require_http_methods(["GET"])
1112
@signed_terms_required
1113
@login_required
1114
def project_list(request):
1115
    projects = ProjectApplication.objects.user_accessible_projects(request.user).select_related()
1116
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1117
                                                prefix="my_projects_")
1118
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1119

    
1120
    return object_list(
1121
        request,
1122
        projects,
1123
        template_name='im/projects/project_list.html',
1124
        extra_context={
1125
            'is_search':False,
1126
            'table': table,
1127
        })
1128

    
1129

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

    
1151
    next = request.GET.get('next')
1152
    if not next:
1153
        if chain_id:
1154
            next = reverse('astakos.im.views.project_detail', args=(chain_id,))
1155
        else:
1156
            next = reverse('astakos.im.views.project_list')
1157

    
1158
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1159
    return redirect(next)
1160

    
1161

    
1162
@require_http_methods(["GET", "POST"])
1163
@signed_terms_required
1164
@login_required
1165
def project_modify(request, application_id):
1166

    
1167
    try:
1168
        app = ProjectApplication.objects.get(id=application_id)
1169
    except ProjectApplication.DoesNotExist:
1170
        raise Http404
1171

    
1172
    user = request.user
1173
    if not (user.owns_application(app) or user.is_project_admin(app.id)):
1174
        m = _(astakos_messages.NOT_ALLOWED)
1175
        raise PermissionDenied(m)
1176

    
1177
    owner_id = app.owner_id
1178
    reached, limit = reached_pending_application_limit(owner_id, app)
1179
    if not user.is_project_admin() and reached:
1180
        m = _(astakos_messages.PENDING_APPLICATION_LIMIT_MODIFY) % limit
1181
        messages.error(request, m)
1182
        next = reverse('astakos.im.views.project_list')
1183
        next = restrict_next(next, domain=COOKIE_DOMAIN)
1184
        return redirect(next)
1185

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

    
1218

    
1219
@require_http_methods(["GET", "POST"])
1220
@signed_terms_required
1221
@login_required
1222
def project_app(request, application_id):
1223
    return common_detail(request, application_id, project_view=False)
1224

    
1225
@require_http_methods(["GET", "POST"])
1226
@signed_terms_required
1227
@login_required
1228
def project_detail(request, chain_id):
1229
    return common_detail(request, chain_id)
1230

    
1231
@project_transaction_context(sync=True)
1232
def addmembers(request, chain_id, addmembers_form, ctx=None):
1233
    if addmembers_form.is_valid():
1234
        try:
1235
            chain_id = int(chain_id)
1236
            map(lambda u: enroll_member(
1237
                    chain_id,
1238
                    u,
1239
                    request_user=request.user),
1240
                addmembers_form.valid_users)
1241
        except (IOError, PermissionDenied), e:
1242
            messages.error(request, e)
1243
        except BaseException, e:
1244
            if ctx:
1245
                ctx.mark_rollback()
1246
            messages.error(request, e)
1247

    
1248
def common_detail(request, chain_or_app_id, project_view=True):
1249
    project = None
1250
    if project_view:
1251
        chain_id = chain_or_app_id
1252
        if request.method == 'POST':
1253
            addmembers_form = AddProjectMembersForm(
1254
                request.POST,
1255
                chain_id=int(chain_id),
1256
                request_user=request.user)
1257
            addmembers(request, chain_id, addmembers_form)
1258
            if addmembers_form.is_valid():
1259
                addmembers_form = AddProjectMembersForm()  # clear form data
1260
        else:
1261
            addmembers_form = AddProjectMembersForm()  # initialize form
1262

    
1263
        project, application = get_by_chain_or_404(chain_id)
1264
        if project:
1265
            members = project.projectmembership_set.select_related()
1266
            members_table = tables.ProjectMembersTable(project,
1267
                                                       members,
1268
                                                       user=request.user,
1269
                                                       prefix="members_")
1270
            RequestConfig(request, paginate={"per_page": PAGINATE_BY}
1271
                          ).configure(members_table)
1272

    
1273
        else:
1274
            members_table = None
1275

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

    
1282
    modifications_table = None
1283

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

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

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

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

    
1307
    return object_detail(
1308
        request,
1309
        queryset=ProjectApplication.objects.select_related(),
1310
        object_id=application.id,
1311
        template_name='im/projects/project_detail.html',
1312
        extra_context={
1313
            'project_view': project_view,
1314
            'addmembers_form':addmembers_form,
1315
            'members_table': members_table,
1316
            'owner_mode': is_owner,
1317
            'admin_mode': is_project_admin,
1318
            'modifications_table': modifications_table,
1319
            'mem_display': mem_display,
1320
            'can_join_request': can_join_req,
1321
            'can_leave_request': can_leave_req,
1322
            })
1323

    
1324
@require_http_methods(["GET", "POST"])
1325
@signed_terms_required
1326
@login_required
1327
def project_search(request):
1328
    q = request.GET.get('q', '')
1329
    form = ProjectSearchForm()
1330
    q = q.strip()
1331

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1530
    if not request.user.is_project_admin():
1531
        m = _(astakos_messages.NOT_ALLOWED)
1532
        raise PermissionDenied(m)
1533

    
1534
    try:
1535
        app = ProjectApplication.objects.get(id=application_id)
1536
    except ProjectApplication.DoesNotExist:
1537
        raise Http404
1538

    
1539
    approve_application(application_id)
1540
    chain_id = get_related_project_id(application_id)
1541
    return redirect(reverse('project_detail', args=(chain_id,)))
1542

    
1543
@require_http_methods(["POST"])
1544
@signed_terms_required
1545
@login_required
1546
@project_transaction_context()
1547
def project_app_deny(request, application_id, ctx=None):
1548

    
1549
    reason = request.POST.get('reason', None)
1550
    if not reason:
1551
        reason = None
1552

    
1553
    if not request.user.is_project_admin():
1554
        m = _(astakos_messages.NOT_ALLOWED)
1555
        raise PermissionDenied(m)
1556

    
1557
    try:
1558
        app = ProjectApplication.objects.get(id=application_id)
1559
    except ProjectApplication.DoesNotExist:
1560
        raise Http404
1561

    
1562
    deny_application(application_id, reason=reason)
1563
    return redirect(reverse('project_list'))
1564

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

    
1575
    if not request.user.owns_application(app):
1576
        m = _(astakos_messages.NOT_ALLOWED)
1577
        raise PermissionDenied(m)
1578

    
1579
    # XXX: dismiss application also does authorization
1580
    dismiss_application(application_id, request_user=request.user)
1581

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

    
1590

    
1591
def landing(request):
1592
    return render_response(
1593
        'im/landing.html',
1594
        context_instance=get_context(request))
1595

    
1596

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