Statistics
| Branch: | Tag: | Revision:

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

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

    
120
logger = logging.getLogger(__name__)
121

    
122
callpoint = AstakosCallpoint()
123

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

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

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

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

    
158

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

    
173

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

    
189

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

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

    
211

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

    
215

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

222
    **Arguments**
223

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

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

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

235
    **Template:**
236

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

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

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

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

    
255

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

    
268

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

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

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

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

285
    **Arguments**
286

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

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

294
    **Template:**
295

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

298
    **Settings:**
299

300
    The view expectes the following settings are defined:
301

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

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

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

    
346

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

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

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

361
    **Arguments**
362

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

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

370
    **Template:**
371

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

374
    **Settings:**
375

376
    The view expectes the following settings are defined:
377

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

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

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

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

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

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

    
436

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

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

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

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

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

456
    **Arguments**
457

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

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

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

468
    **Template:**
469

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
568

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

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

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

582
    **Arguments**
583

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

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

591
    **Template:**
592

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

595
    **Settings:**
596

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

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

    
623

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

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

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

    
663

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

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

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

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

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

    
714

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

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

    
741
    terms = f.read()
742

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

    
767

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

    
777

    
778
    if not astakos_settings.EMAILCHANGE_ENABLED:
779
        raise PermissionDenied
780

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

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

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

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

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

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

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

    
837

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

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

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

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

    
860

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

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

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

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

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

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

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

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

    
927

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

    
933

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

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

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

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

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

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

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

    
1045

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

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

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

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

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

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

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

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

    
1107
    return resource_catalog, resource_groups
1108

    
1109

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

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

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

    
1146
    if response is not None:
1147
        return response
1148

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

    
1153

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

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

    
1171

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

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

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

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

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

    
1204

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

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

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

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

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

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

    
1254
    if response is not None:
1255
        return response
1256

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

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

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

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

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

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

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

    
1311
        else:
1312
            members_table = None
1313

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

    
1320
    modifications_table = None
1321

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1416

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

    
1420

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

    
1434

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

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

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

    
1448

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

    
1462

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

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

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

    
1476

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

    
1487

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

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

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

    
1497

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

    
1511

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

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

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

    
1521

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

    
1535

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

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

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

    
1545

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

    
1559

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

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

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

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

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

    
1580

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

    
1585

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

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

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

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

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

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

    
1609

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

    
1614

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

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

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

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

    
1639

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

    
1644

    
1645
@require_http_methods(["GET"])
1646
@required_auth_methods_assigned(allow_access=True)
1647
@login_required
1648
@signed_terms_required
1649
def landing(request):
1650
    return render_response(
1651
        'im/landing.html',
1652
        context_instance=get_context(request))
1653

    
1654

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