Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 85d444db

History | View | Annotate | Download (50.6 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

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

    
68
import astakos.im.messages as astakos_messages
69

    
70
from astakos.im.activation_backends import get_backend, SimpleBackend
71
from astakos.im.models import (
72
    AstakosUser, ApprovalTerms,
73
#     AstakosGroup, Membership
74
    EmailChange, GroupKind,
75
    RESOURCE_SEPARATOR, AstakosUserAuthProvider,
76
    PendingThirdPartyUser,
77
    ProjectApplication, ProjectMembership, Project)
78
from astakos.im.util import (
79
    get_context, prepare_response, get_query, restrict_next)
80
from astakos.im.forms import (
81
    LoginForm, InvitationForm, ProfileForm,
82
    FeedbackForm, SignApprovalTermsForm,
83
    EmailChangeForm,
84
#     AstakosGroupCreationForm, AstakosGroupSearchForm,
85
#     AstakosGroupUpdateForm, AddGroupMembersForm,
86
#     MembersSortForm, AstakosGroupSortForm,
87
#     TimelineForm, PickResourceForm,
88
#     AstakosGroupCreationSummaryForm,
89
    ProjectApplicationForm, ProjectSortForm,
90
    AddProjectMembersForm, ProjectSearchForm,
91
    ProjectMembersSortForm)
92
from astakos.im.functions import (
93
    send_feedback, SendMailError,
94
    logout as auth_logout,
95
    activate as activate_func,
96
    invite,
97
    send_activation as send_activation_func,
98
#     send_group_creation_notification,
99
    SendNotificationError,
100
    accept_membership, reject_membership, remove_membership,
101
    leave_project, join_project, enroll_member)
102
# from astakos.im.endpoints.qh import timeline_charge
103
from astakos.im.settings import (
104
    COOKIE_DOMAIN, LOGOUT_NEXT,
105
    LOGGING_LEVEL, PAGINATE_BY,
106
    RESOURCES_PRESENTATION_DATA, PAGINATE_BY_ALL,
107
    MODERATION_ENABLED)
108
#from astakos.im.tasks import request_billing
109
from astakos.im.api.callpoint import AstakosCallpoint
110
from astakos.im import auth_providers
111
from astakos.im.templatetags.filters import ResourcePresentation
112

    
113
logger = logging.getLogger(__name__)
114

    
115
callpoint = AstakosCallpoint()
116

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

    
131
def requires_auth_provider(provider_id, **perms):
132
    """
133
    """
134
    def decorator(func, *args, **kwargs):
135
        @wraps(func)
136
        def wrapper(request, *args, **kwargs):
137
            provider = auth_providers.get_provider(provider_id)
138

    
139
            if not provider or not provider.is_active():
140
                raise PermissionDenied
141

    
142
            if provider:
143
                for pkey, value in perms.iteritems():
144
                    attr = 'is_available_for_%s' % pkey.lower()
145
                    if getattr(provider, attr)() != value:
146
                        #TODO: add session message
147
                        return HttpResponseRedirect(reverse('login'))
148
            return func(request, *args)
149
        return wrapper
150
    return decorator
151

    
152

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

    
167

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

    
183

    
184
def required_auth_methods_assigned(only_warn=False):
185
    """
186
    Decorator that checks whether the request.user has all required auth providers
187
    assigned.
188
    """
189
    required_providers = auth_providers.REQUIRED_PROVIDERS.keys()
190

    
191
    def decorator(func):
192
        if not required_providers:
193
            return func
194

    
195
        @wraps(func)
196
        def wrapper(request, *args, **kwargs):
197
            if request.user.is_authenticated():
198
                for required in required_providers:
199
                    if not request.user.has_auth_provider(required):
200
                        provider = auth_providers.get_provider(required)
201
                        if only_warn:
202
                            messages.error(request,
203
                                           _(astakos_messages.AUTH_PROVIDER_REQUIRED  % {
204
                                               'provider': provider.get_title_display}))
205
                        else:
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(["GET", "POST"])
257
@valid_astakos_user_required
258
@transaction.commit_manually
259
def invite(request, template_name='im/invitations.html', extra_context=None):
260
    """
261
    Allows a user to invite somebody else.
262

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

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

270
    If the user isn't logged in, redirects to settings.LOGIN_URL.
271

272
    **Arguments**
273

274
    ``template_name``
275
        A custom template to use. This is optional; if not specified,
276
        this will default to ``im/invitations.html``.
277

278
    ``extra_context``
279
        An dictionary of variables to add to the template context.
280

281
    **Template:**
282

283
    im/invitations.html or ``template_name`` keyword argument.
284

285
    **Settings:**
286

287
    The view expectes the following settings are defined:
288

289
    * LOGIN_URL: login uri
290
    """
291
    extra_context = extra_context or {}
292
    status = None
293
    message = None
294
    form = InvitationForm()
295

    
296
    inviter = request.user
297
    if request.method == 'POST':
298
        form = InvitationForm(request.POST)
299
        if inviter.invitations > 0:
300
            if form.is_valid():
301
                try:
302
                    email = form.cleaned_data.get('username')
303
                    realname = form.cleaned_data.get('realname')
304
                    invite(inviter, email, realname)
305
                    message = _(astakos_messages.INVITATION_SENT) % locals()
306
                    messages.success(request, message)
307
                except SendMailError, e:
308
                    message = e.message
309
                    messages.error(request, message)
310
                    transaction.rollback()
311
                except BaseException, e:
312
                    message = _(astakos_messages.GENERIC_ERROR)
313
                    messages.error(request, message)
314
                    logger.exception(e)
315
                    transaction.rollback()
316
                else:
317
                    transaction.commit()
318
        else:
319
            message = _(astakos_messages.MAX_INVITATION_NUMBER_REACHED)
320
            messages.error(request, message)
321

    
322
    sent = [{'email': inv.username,
323
             'realname': inv.realname,
324
             'is_consumed': inv.is_consumed}
325
            for inv in request.user.invitations_sent.all()]
326
    kwargs = {'inviter': inviter,
327
              'sent': sent}
328
    context = get_context(request, extra_context, **kwargs)
329
    return render_response(template_name,
330
                           invitation_form=form,
331
                           context_instance=context)
332

    
333

    
334
@require_http_methods(["GET", "POST"])
335
@required_auth_methods_assigned(only_warn=True)
336
@login_required
337
@signed_terms_required
338
def edit_profile(request, template_name='im/profile.html', extra_context=None):
339
    """
340
    Allows a user to edit his/her profile.
341

342
    In case of GET request renders a form for displaying the user information.
343
    In case of POST updates the user informantion and redirects to ``next``
344
    url parameter if exists.
345

346
    If the user isn't logged in, redirects to settings.LOGIN_URL.
347

348
    **Arguments**
349

350
    ``template_name``
351
        A custom template to use. This is optional; if not specified,
352
        this will default to ``im/profile.html``.
353

354
    ``extra_context``
355
        An dictionary of variables to add to the template context.
356

357
    **Template:**
358

359
    im/profile.html or ``template_name`` keyword argument.
360

361
    **Settings:**
362

363
    The view expectes the following settings are defined:
364

365
    * LOGIN_URL: login uri
366
    """
367
    extra_context = extra_context or {}
368
    form = ProfileForm(
369
        instance=request.user,
370
        session_key=request.session.session_key
371
    )
372
    extra_context['next'] = request.GET.get('next')
373
    if request.method == 'POST':
374
        form = ProfileForm(
375
            request.POST,
376
            instance=request.user,
377
            session_key=request.session.session_key
378
        )
379
        if form.is_valid():
380
            try:
381
                prev_token = request.user.auth_token
382
                user = form.save()
383
                form = ProfileForm(
384
                    instance=user,
385
                    session_key=request.session.session_key
386
                )
387
                next = restrict_next(
388
                    request.POST.get('next'),
389
                    domain=COOKIE_DOMAIN
390
                )
391
                if next:
392
                    return redirect(next)
393
                msg = _(astakos_messages.PROFILE_UPDATED)
394
                messages.success(request, msg)
395
            except ValueError, ve:
396
                messages.success(request, ve)
397
    elif request.method == "GET":
398
        request.user.is_verified = True
399
        request.user.save()
400

    
401
    # existing providers
402
    user_providers = request.user.get_active_auth_providers()
403

    
404
    # providers that user can add
405
    user_available_providers = request.user.get_available_auth_providers()
406

    
407
    return render_response(template_name,
408
                           profile_form = form,
409
                           user_providers = user_providers,
410
                           user_available_providers = user_available_providers,
411
                           context_instance = get_context(request,
412
                                                          extra_context))
413

    
414

    
415
@transaction.commit_manually
416
@require_http_methods(["GET", "POST"])
417
def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context=None, backend=None):
418
    """
419
    Allows a user to create a local account.
420

421
    In case of GET request renders a form for entering the user information.
422
    In case of POST handles the signup.
423

424
    The user activation will be delegated to the backend specified by the ``backend`` keyword argument
425
    if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
426
    if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
427
    (see activation_backends);
428

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

432
    On unsuccessful creation, renders ``template_name`` with an error message.
433

434
    **Arguments**
435

436
    ``template_name``
437
        A custom template to render. This is optional;
438
        if not specified, this will default to ``im/signup.html``.
439

440
    ``on_success``
441
        A custom template to render in case of success. This is optional;
442
        if not specified, this will default to ``im/signup_complete.html``.
443

444
    ``extra_context``
445
        An dictionary of variables to add to the template context.
446

447
    **Template:**
448

449
    im/signup.html or ``template_name`` keyword argument.
450
    im/signup_complete.html or ``on_success`` keyword argument.
451
    """
452
    extra_context = extra_context or {}
453
    if request.user.is_authenticated():
454
        return HttpResponseRedirect(reverse('edit_profile'))
455

    
456
    provider = get_query(request).get('provider', 'local')
457
    if not auth_providers.get_provider(provider).is_available_for_create():
458
        raise PermissionDenied
459

    
460
    id = get_query(request).get('id')
461
    try:
462
        instance = AstakosUser.objects.get(id=id) if id else None
463
    except AstakosUser.DoesNotExist:
464
        instance = None
465

    
466
    third_party_token = request.REQUEST.get('third_party_token', None)
467
    if third_party_token:
468
        pending = get_object_or_404(PendingThirdPartyUser,
469
                                    token=third_party_token)
470
        provider = pending.provider
471
        instance = pending.get_user_instance()
472

    
473
    try:
474
        if not backend:
475
            backend = get_backend(request)
476
        form = backend.get_signup_form(provider, instance)
477
    except Exception, e:
478
        form = SimpleBackend(request).get_signup_form(provider)
479
        messages.error(request, e)
480
    if request.method == 'POST':
481
        if form.is_valid():
482
            user = form.save(commit=False)
483
            try:
484
                result = backend.handle_activation(user)
485
                status = messages.SUCCESS
486
                message = result.message
487

    
488
                form.store_user(user, request)
489

    
490
                if 'additional_email' in form.cleaned_data:
491
                    additional_email = form.cleaned_data['additional_email']
492
                    if additional_email != user.email:
493
                        user.additionalmail_set.create(email=additional_email)
494
                        msg = 'Additional email: %s saved for user %s.' % (
495
                            additional_email,
496
                            user.email
497
                        )
498
                        logger._log(LOGGING_LEVEL, msg, [])
499
                if user and user.is_active:
500
                    next = request.POST.get('next', '')
501
                    response = prepare_response(request, user, next=next)
502
                    transaction.commit()
503
                    return response
504
                messages.add_message(request, status, message)
505
                transaction.commit()
506
                return render_response(
507
                    on_success,
508
                    context_instance=get_context(
509
                        request,
510
                        extra_context
511
                    )
512
                )
513
            except SendMailError, e:
514
                logger.exception(e)
515
                status = messages.ERROR
516
                message = e.message
517
                messages.error(request, message)
518
                transaction.rollback()
519
            except BaseException, e:
520
                logger.exception(e)
521
                message = _(astakos_messages.GENERIC_ERROR)
522
                messages.error(request, message)
523
                logger.exception(e)
524
                transaction.rollback()
525
    return render_response(template_name,
526
                           signup_form=form,
527
                           third_party_token=third_party_token,
528
                           provider=provider,
529
                           context_instance=get_context(request, extra_context))
530

    
531

    
532
@require_http_methods(["GET", "POST"])
533
@required_auth_methods_assigned(only_warn=True)
534
@login_required
535
@signed_terms_required
536
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
537
    """
538
    Allows a user to send feedback.
539

540
    In case of GET request renders a form for providing the feedback information.
541
    In case of POST sends an email to support team.
542

543
    If the user isn't logged in, redirects to settings.LOGIN_URL.
544

545
    **Arguments**
546

547
    ``template_name``
548
        A custom template to use. This is optional; if not specified,
549
        this will default to ``im/feedback.html``.
550

551
    ``extra_context``
552
        An dictionary of variables to add to the template context.
553

554
    **Template:**
555

556
    im/signup.html or ``template_name`` keyword argument.
557

558
    **Settings:**
559

560
    * LOGIN_URL: login uri
561
    """
562
    extra_context = extra_context or {}
563
    if request.method == 'GET':
564
        form = FeedbackForm()
565
    if request.method == 'POST':
566
        if not request.user:
567
            return HttpResponse('Unauthorized', status=401)
568

    
569
        form = FeedbackForm(request.POST)
570
        if form.is_valid():
571
            msg = form.cleaned_data['feedback_msg']
572
            data = form.cleaned_data['feedback_data']
573
            try:
574
                send_feedback(msg, data, request.user, email_template_name)
575
            except SendMailError, e:
576
                messages.error(request, message)
577
            else:
578
                message = _(astakos_messages.FEEDBACK_SENT)
579
                messages.success(request, message)
580
    return render_response(template_name,
581
                           feedback_form=form,
582
                           context_instance=get_context(request, extra_context))
583

    
584

    
585
@require_http_methods(["GET"])
586
@signed_terms_required
587
def logout(request, template='registration/logged_out.html', extra_context=None):
588
    """
589
    Wraps `django.contrib.auth.logout`.
590
    """
591
    extra_context = extra_context or {}
592
    response = HttpResponse()
593
    if request.user.is_authenticated():
594
        email = request.user.email
595
        auth_logout(request)
596
    else:
597
        response['Location'] = reverse('index')
598
        response.status_code = 301
599
        return response
600

    
601
    next = restrict_next(
602
        request.GET.get('next'),
603
        domain=COOKIE_DOMAIN
604
    )
605

    
606
    if next:
607
        response['Location'] = next
608
        response.status_code = 302
609
    elif LOGOUT_NEXT:
610
        response['Location'] = LOGOUT_NEXT
611
        response.status_code = 301
612
    else:
613
        messages.add_message(request, messages.SUCCESS, _(astakos_messages.LOGOUT_SUCCESS))
614
        response['Location'] = reverse('index')
615
        response.status_code = 301
616
    return response
617

    
618

    
619
@require_http_methods(["GET", "POST"])
620
@transaction.commit_manually
621
def activate(request, greeting_email_template_name='im/welcome_email.txt',
622
             helpdesk_email_template_name='im/helpdesk_notification.txt'):
623
    """
624
    Activates the user identified by the ``auth`` request parameter, sends a welcome email
625
    and renews the user token.
626

627
    The view uses commit_manually decorator in order to ensure the user state will be updated
628
    only if the email will be send successfully.
629
    """
630
    token = request.GET.get('auth')
631
    next = request.GET.get('next')
632
    try:
633
        user = AstakosUser.objects.get(auth_token=token)
634
    except AstakosUser.DoesNotExist:
635
        return HttpResponseBadRequest(_(astakos_messages.ACCOUNT_UNKNOWN))
636

    
637
    if user.is_active:
638
        message = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE)
639
        messages.error(request, message)
640
        return index(request)
641

    
642
    try:
643
        activate_func(user, greeting_email_template_name, helpdesk_email_template_name, verify_email=True)
644
        response = prepare_response(request, user, next, renew=True)
645
        transaction.commit()
646
        return response
647
    except SendMailError, e:
648
        message = e.message
649
        messages.add_message(request, messages.ERROR, message)
650
        transaction.rollback()
651
        return index(request)
652
    except BaseException, e:
653
        status = messages.ERROR
654
        message = _(astakos_messages.GENERIC_ERROR)
655
        messages.add_message(request, messages.ERROR, message)
656
        logger.exception(e)
657
        transaction.rollback()
658
        return index(request)
659

    
660

    
661
@require_http_methods(["GET", "POST"])
662
def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
663
    extra_context = extra_context or {}
664
    term = None
665
    terms = None
666
    if not term_id:
667
        try:
668
            term = ApprovalTerms.objects.order_by('-id')[0]
669
        except IndexError:
670
            pass
671
    else:
672
        try:
673
            term = ApprovalTerms.objects.get(id=term_id)
674
        except ApprovalTerms.DoesNotExist, e:
675
            pass
676

    
677
    if not term:
678
        messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
679
        return HttpResponseRedirect(reverse('index'))
680
    f = open(term.location, 'r')
681
    terms = f.read()
682

    
683
    if request.method == 'POST':
684
        next = restrict_next(
685
            request.POST.get('next'),
686
            domain=COOKIE_DOMAIN
687
        )
688
        if not next:
689
            next = reverse('index')
690
        form = SignApprovalTermsForm(request.POST, instance=request.user)
691
        if not form.is_valid():
692
            return render_response(template_name,
693
                                   terms=terms,
694
                                   approval_terms_form=form,
695
                                   context_instance=get_context(request, extra_context))
696
        user = form.save()
697
        return HttpResponseRedirect(next)
698
    else:
699
        form = None
700
        if request.user.is_authenticated() and not request.user.signed_terms:
701
            form = SignApprovalTermsForm(instance=request.user)
702
        return render_response(template_name,
703
                               terms=terms,
704
                               approval_terms_form=form,
705
                               context_instance=get_context(request, extra_context))
706

    
707

    
708
@require_http_methods(["GET", "POST"])
709
@valid_astakos_user_required
710
@transaction.commit_manually
711
def change_email(request, activation_key=None,
712
                 email_template_name='registration/email_change_email.txt',
713
                 form_template_name='registration/email_change_form.html',
714
                 confirm_template_name='registration/email_change_done.html',
715
                 extra_context=None):
716
    extra_context = extra_context or {}
717

    
718

    
719
    if activation_key:
720
        try:
721
            user = EmailChange.objects.change_email(activation_key)
722
            if request.user.is_authenticated() and request.user == user:
723
                msg = _(astakos_messages.EMAIL_CHANGED)
724
                messages.success(request, msg)
725
                auth_logout(request)
726
                response = prepare_response(request, user)
727
                transaction.commit()
728
                return HttpResponseRedirect(reverse('edit_profile'))
729
        except ValueError, e:
730
            messages.error(request, e)
731
            transaction.rollback()
732
            return HttpResponseRedirect(reverse('index'))
733

    
734
        return render_response(confirm_template_name,
735
                               modified_user=user if 'user' in locals() \
736
                               else None, context_instance=get_context(request,
737
                                                            extra_context))
738

    
739
    if not request.user.is_authenticated():
740
        path = quote(request.get_full_path())
741
        url = request.build_absolute_uri(reverse('index'))
742
        return HttpResponseRedirect(url + '?next=' + path)
743

    
744
    # clean up expired email changes
745
    if request.user.email_change_is_pending():
746
        change = request.user.emailchanges.get()
747
        if change.activation_key_expired():
748
            change.delete()
749
            transaction.commit()
750
            return HttpResponseRedirect(reverse('email_change'))
751

    
752
    form = EmailChangeForm(request.POST or None)
753
    if request.method == 'POST' and form.is_valid():
754
        try:
755
            # delete pending email changes
756
            request.user.emailchanges.all().delete()
757
            ec = form.save(email_template_name, request)
758
        except SendMailError, e:
759
            msg = e
760
            messages.error(request, msg)
761
            transaction.rollback()
762
            return HttpResponseRedirect(reverse('edit_profile'))
763
        else:
764
            msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
765
            messages.success(request, msg)
766
            transaction.commit()
767
            return HttpResponseRedirect(reverse('edit_profile'))
768

    
769
    if request.user.email_change_is_pending():
770
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
771

    
772
    return render_response(
773
        form_template_name,
774
        form=form,
775
        context_instance=get_context(request, extra_context)
776
    )
777

    
778

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

    
781
    if request.user.is_authenticated():
782
        messages.error(request, _(astakos_messages.ALREADY_LOGGED_IN))
783
        return HttpResponseRedirect(reverse('edit_profile'))
784

    
785
    if MODERATION_ENABLED:
786
        raise PermissionDenied
787

    
788
    extra_context = extra_context or {}
789
    try:
790
        u = AstakosUser.objects.get(id=user_id)
791
    except AstakosUser.DoesNotExist:
792
        messages.error(request, _(astakos_messages.ACCOUNT_UNKNOWN))
793
    else:
794
        try:
795
            send_activation_func(u)
796
            msg = _(astakos_messages.ACTIVATION_SENT)
797
            messages.success(request, msg)
798
        except SendMailError, e:
799
            messages.error(request, e)
800
    return render_response(
801
        template_name,
802
        login_form = LoginForm(request=request),
803
        context_instance = get_context(
804
            request,
805
            extra_context
806
        )
807
    )
808

    
809

    
810
# def handle_membership(func):
811
#     @wraps(func)
812
#     def wrapper(request, group_id, user_id):
813
#         try:
814
#             m = Membership.objects.select_related().get(
815
#                 group__id=group_id,
816
#                 person__id=user_id)
817
#         except Membership.DoesNotExist:
818
#             return HttpResponseBadRequest(_(astakos_messages.NOT_MEMBER))
819
#         else:
820
#             if request.user not in m.group.owner.all():
821
#                 return HttpResponseForbidden(_(astakos_messages.NOT_OWNER))
822
#             func(request, m)
823
#             return group_detail(request, group_id)
824
#     return wrapper
825

    
826

    
827
#@require_http_methods(["POST"])
828
# @require_http_methods(["POST", "GET"])
829
# @signed_terms_required
830
# @login_required
831
# @handle_membership
832
# def approve_member(request, membership):
833
#     try:
834
#         membership.approve()
835
#         realname = membership.person.realname
836
#         msg = _(astakos_messages.MEMBER_JOINED_GROUP) % locals()
837
#         messages.success(request, msg)
838
#     except AssertionError:
839
#         msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
840
#         messages.error(request, msg)
841
#     except BaseException, e:
842
#         logger.exception(e)
843
#         realname = membership.person.realname
844
#         msg = _(astakos_messages.GENERIC_ERROR)
845
#         messages.error(request, msg)
846

    
847

    
848
# @signed_terms_required
849
# @login_required
850
# @handle_membership
851
# def disapprove_member(request, membership):
852
#     try:
853
#         membership.disapprove()
854
#         realname = membership.person.realname
855
#         msg = astakos_messages.MEMBER_REMOVED % locals()
856
#         messages.success(request, msg)
857
#     except BaseException, e:
858
#         logger.exception(e)
859
#         msg = _(astakos_messages.GENERIC_ERROR)
860
#         messages.error(request, msg)
861

    
862

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

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

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

    
892
    resource_usage = None
893
    result = callpoint.get_user_usage(request.user.id)
894
    if result.is_success:
895
        resource_usage = result.data
896
        backenddata = map(with_class, result.data)
897
        backenddata = map(with_class, backenddata)
898

    
899
    else:
900
        messages.error(request, result.reason)
901
    return render_response('im/resource_usage.html',
902
                           context_instance=get_context(request),
903
                           resource_usage=backenddata,
904
                           result=result)
905

    
906

    
907
# def group_create_list(request):
908
#     form = PickResourceForm()
909
#     return render_response(
910
#         template='im/astakosgroup_create_list.html',
911
#         context_instance=get_context(request),)
912

    
913

    
914
##@require_http_methods(["GET"])
915
#@require_http_methods(["POST", "GET"])
916
#@signed_terms_required
917
#@login_required
918
#def billing(request):
919
#
920
#    today = datetime.today()
921
#    month_last_day = calendar.monthrange(today.year, today.month)[1]
922
#    start = request.POST.get('datefrom', None)
923
#    if start:
924
#        today = datetime.fromtimestamp(int(start))
925
#        month_last_day = calendar.monthrange(today.year, today.month)[1]
926
#
927
#    start = datetime(today.year, today.month, 1).strftime("%s")
928
#    end = datetime(today.year, today.month, month_last_day).strftime("%s")
929
#    r = request_billing.apply(args=('pgerakios@grnet.gr',
930
#                                    int(start) * 1000,
931
#                                    int(end) * 1000))
932
#    data = {}
933
#
934
#    try:
935
#        status, data = r.result
936
#        data = _clear_billing_data(data)
937
#        if status != 200:
938
#            messages.error(request, _(astakos_messages.BILLING_ERROR) % status)
939
#    except:
940
#        messages.error(request, r.result)
941
#
942
#    return render_response(
943
#        template='im/billing.html',
944
#        context_instance=get_context(request),
945
#        data=data,
946
#        zerodate=datetime(month=1, year=1970, day=1),
947
#        today=today,
948
#        start=int(start),
949
#        month_last_day=month_last_day)
950

    
951

    
952
#def _clear_billing_data(data):
953
#
954
#    # remove addcredits entries
955
#    def isnotcredit(e):
956
#        return e['serviceName'] != "addcredits"
957
#
958
#    # separate services
959
#    def servicefilter(service_name):
960
#        service = service_name
961
#
962
#        def fltr(e):
963
#            return e['serviceName'] == service
964
#        return fltr
965
#
966
#    data['bill_nocredits'] = filter(isnotcredit, data['bill'])
967
#    data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
968
#    data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
969
#    data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
970
#
971
#    return data
972

    
973

    
974
# #@require_http_methods(["GET"])
975
# @require_http_methods(["POST", "GET"])
976
# @signed_terms_required
977
# @login_required
978
# def timeline(request):
979
# #    data = {'entity':request.user.email}
980
#     timeline_body = ()
981
#     timeline_header = ()
982
# #    form = TimelineForm(data)
983
#     form = TimelineForm()
984
#     if request.method == 'POST':
985
#         data = request.POST
986
#         form = TimelineForm(data)
987
#         if form.is_valid():
988
#             data = form.cleaned_data
989
#             timeline_header = ('entity', 'resource',
990
#                                'event name', 'event date',
991
#                                'incremental cost', 'total cost')
992
#             timeline_body = timeline_charge(
993
#                 data['entity'], data['resource'],
994
#                 data['start_date'], data['end_date'],
995
#                 data['details'], data['operation'])
996
#
997
#     return render_response(template='im/timeline.html',
998
#                            context_instance=get_context(request),
999
#                            form=form,
1000
#                            timeline_header=timeline_header,
1001
#                            timeline_body=timeline_body)
1002
#     return data
1003

    
1004

    
1005
# TODO: action only on POST and user should confirm the removal
1006
@require_http_methods(["GET", "POST"])
1007
@login_required
1008
@signed_terms_required
1009
def remove_auth_provider(request, pk):
1010
    try:
1011
        provider = request.user.auth_providers.get(pk=pk)
1012
    except AstakosUserAuthProvider.DoesNotExist:
1013
        raise Http404
1014

    
1015
    if provider.can_remove():
1016
        provider.delete()
1017
        return HttpResponseRedirect(reverse('edit_profile'))
1018
    else:
1019
        raise PermissionDenied
1020

    
1021

    
1022
def how_it_works(request):
1023
    return render_response(
1024
        'im/how_it_works.html',
1025
        context_instance=get_context(request))
1026

    
1027
@transaction.commit_manually
1028
def _create_object(request, model=None, template_name=None,
1029
        template_loader=template_loader, extra_context=None, post_save_redirect=None,
1030
        login_required=False, context_processors=None, form_class=None):
1031
    """
1032
    Based of django.views.generic.create_update.create_object which displays a
1033
    summary page before creating the object.
1034
    """
1035
    rollback = False
1036
    response = None
1037

    
1038
    if extra_context is None: extra_context = {}
1039
    if login_required and not request.user.is_authenticated():
1040
        return redirect_to_login(request.path)
1041
    try:
1042

    
1043
        model, form_class = get_model_and_form_class(model, form_class)
1044
        extra_context['edit'] = 0
1045
        if request.method == 'POST':
1046
            form = form_class(request.POST, request.FILES)
1047
            if form.is_valid():
1048
                verify = request.GET.get('verify')
1049
                edit = request.GET.get('edit')
1050
                if verify == '1':
1051
                    extra_context['show_form'] = False
1052
                    extra_context['form_data'] = form.cleaned_data
1053
                elif edit == '1':
1054
                    extra_context['show_form'] = True
1055
                else:
1056
                    new_object = form.save()
1057

    
1058
                    msg = _("The %(verbose_name)s was created successfully.") %\
1059
                                {"verbose_name": model._meta.verbose_name}
1060
                    messages.success(request, msg, fail_silently=True)
1061
                    response = redirect(post_save_redirect, new_object)
1062
        else:
1063
            form = form_class()
1064
    except BaseException, e:
1065
        logger.exception(e)
1066
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1067
        rollback = True
1068
    finally:
1069
        if rollback:
1070
            transaction.rollback()
1071
        else:
1072
            transaction.commit()
1073

    
1074
        if response == None:
1075
            # Create the template, context, response
1076
            if not template_name:
1077
                template_name = "%s/%s_form.html" %\
1078
                     (model._meta.app_label, model._meta.object_name.lower())
1079
            t = template_loader.get_template(template_name)
1080
            c = RequestContext(request, {
1081
                'form': form
1082
            }, context_processors)
1083
            apply_extra_context(extra_context, c)
1084
            response = HttpResponse(t.render(c))
1085
        return response
1086

    
1087
@transaction.commit_manually
1088
def _update_object(request, model=None, object_id=None, slug=None,
1089
        slug_field='slug', template_name=None, template_loader=template_loader,
1090
        extra_context=None, post_save_redirect=None, login_required=False,
1091
        context_processors=None, template_object_name='object',
1092
        form_class=None):
1093
    """
1094
    Based of django.views.generic.create_update.update_object which displays a
1095
    summary page before updating the object.
1096
    """
1097
    rollback = False
1098
    response = None
1099

    
1100
    if extra_context is None: extra_context = {}
1101
    if login_required and not request.user.is_authenticated():
1102
        return redirect_to_login(request.path)
1103

    
1104
    try:
1105
        model, form_class = get_model_and_form_class(model, form_class)
1106
        obj = lookup_object(model, object_id, slug, slug_field)
1107

    
1108
        if request.method == 'POST':
1109
            form = form_class(request.POST, request.FILES, instance=obj)
1110
            if form.is_valid():
1111
                verify = request.GET.get('verify')
1112
                edit = request.GET.get('edit')
1113
                if verify == '1':
1114
                    extra_context['show_form'] = False
1115
                    extra_context['form_data'] = form.cleaned_data
1116
                elif edit == '1':
1117
                    extra_context['show_form'] = True
1118
                else:
1119
                    obj = form.save()
1120
                    msg = _("The %(verbose_name)s was updated successfully.") %\
1121
                                {"verbose_name": model._meta.verbose_name}
1122
                    messages.success(request, msg, fail_silently=True)
1123
                    response = redirect(post_save_redirect, obj)
1124
        else:
1125
            form = form_class(instance=obj)
1126
    except BaseException, e:
1127
        logger.exception(e)
1128
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1129
        rollback = True
1130
    finally:
1131
        if rollback:
1132
            transaction.rollback()
1133
        else:
1134
            transaction.commit()
1135
        if response == None:
1136
            if not template_name:
1137
                template_name = "%s/%s_form.html" %\
1138
                    (model._meta.app_label, model._meta.object_name.lower())
1139
            t = template_loader.get_template(template_name)
1140
            c = RequestContext(request, {
1141
                'form': form,
1142
                template_object_name: obj,
1143
            }, context_processors)
1144
            apply_extra_context(extra_context, c)
1145
            response = HttpResponse(t.render(c))
1146
            populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.attname))
1147
        return response
1148

    
1149
@require_http_methods(["GET", "POST"])
1150
@signed_terms_required
1151
@login_required
1152
def project_add(request):
1153
    result = callpoint.list_resources()
1154
    if not result.is_success:
1155
        messages.error(
1156
            request,
1157
            'Unable to retrieve system resources: %s' % result.reason
1158
    )
1159
    else:
1160
        resource_catalog = result.data
1161
    extra_context = {'resource_catalog':resource_catalog, 'show_form':True}
1162
    return _create_object(request, template_name='im/projects/projectapplication_form.html',
1163
        extra_context=extra_context, post_save_redirect='/im/project/list/',
1164
        form_class=ProjectApplicationForm)
1165

    
1166

    
1167
@require_http_methods(["GET"])
1168
@signed_terms_required
1169
@login_required
1170
def project_list(request):
1171
    q = ProjectApplication.objects.filter(owner=request.user)
1172
    q |= ProjectApplication.objects.filter(applicant=request.user)
1173
    q |= ProjectApplication.objects.filter(
1174
        project__in=request.user.projectmembership_set.values_list('project', flat=True)
1175
    )
1176
    q = q.select_related()
1177
    sorting = 'name'
1178
    sort_form = ProjectSortForm(request.GET)
1179
    if sort_form.is_valid():
1180
        sorting = sort_form.cleaned_data.get('sorting')
1181
    q = q.order_by(sorting)
1182

    
1183
    return object_list(
1184
        request,
1185
        q,
1186
        paginate_by=PAGINATE_BY,
1187
        page=request.GET.get('page') or 1,
1188
        template_name='im/projects/project_list.html',
1189
        extra_context={
1190
            'is_search':False,
1191
            'sorting':sorting
1192
        }
1193
    )
1194

    
1195
@require_http_methods(["GET", "POST"])
1196
@signed_terms_required
1197
@login_required
1198
def project_update(request, application_id):
1199
    result = callpoint.list_resources()
1200
    if not result.is_success:
1201
        messages.error(
1202
            request,
1203
            'Unable to retrieve system resources: %s' % result.reason
1204
    )
1205
    else:
1206
        resource_catalog = result.data
1207
    extra_context = {'resource_catalog':resource_catalog, 'show_form':True}
1208
    return _update_object(
1209
        request,
1210
        object_id=application_id,
1211
        template_name='im/projects/projectapplication_form.html',
1212
        extra_context=extra_context, post_save_redirect='/im/project/list/',
1213
        form_class=ProjectApplicationForm)
1214

    
1215

    
1216
@require_http_methods(["GET", "POST"])
1217
@signed_terms_required
1218
@login_required
1219
@transaction.commit_manually
1220
def project_detail(request, application_id):
1221
    resource_catalog = None
1222
    result = callpoint.list_resources()
1223
    if not result.is_success:
1224
        messages.error(
1225
            request,
1226
            'Unable to retrieve system resources: %s' % result.reason
1227
    )
1228
    else:
1229
        resource_catalog = result.data
1230

    
1231
    addmembers_form = AddProjectMembersForm()
1232
    if request.method == 'POST':
1233
        addmembers_form = AddProjectMembersForm(request.POST)
1234
        if addmembers_form.is_valid():
1235
            try:
1236
                rollback = False
1237
                application_id = int(application_id)
1238
                map(lambda u: enroll_member(
1239
                        application_id,
1240
                        u,
1241
                        request_user=request.user),
1242
                    addmembers_form.valid_users)
1243
            except (IOError, PermissionDenied), e:
1244
                messages.error(request, e)
1245
            except BaseException, e:
1246
                rollback = True
1247
                messages.error(request, e)
1248
            finally:
1249
                if rollback == True:
1250
                    transaction.rollback()
1251
                else:
1252
                    transaction.commit()
1253
            addmembers_form = AddProjectMembersForm()
1254

    
1255
    # validate sorting
1256
    sorting = 'person__email'
1257
    form = ProjectMembersSortForm(request.GET or request.POST)
1258
    if form.is_valid():
1259
        sorting = form.cleaned_data.get('sorting')
1260

    
1261
    rollback = False
1262
    try:
1263
        return object_detail(
1264
            request,
1265
            queryset=ProjectApplication.objects.select_related(),
1266
            object_id=application_id,
1267
            template_name='im/projects/project_detail.html',
1268
            extra_context={
1269
                'resource_catalog':resource_catalog,
1270
                'sorting':sorting,
1271
                'addmembers_form':addmembers_form
1272
                }
1273
            )
1274
    except:
1275
        rollback = True
1276
    finally:
1277
        if rollback == True:
1278
            transaction.rollback()
1279
        else:
1280
            transaction.commit()
1281

    
1282
@require_http_methods(["GET", "POST"])
1283
@signed_terms_required
1284
@login_required
1285
def project_search(request):
1286
    user_projects = request.user.projectmembership_set.filter(
1287
        ~Q(acceptance_date__isnull=True)).values('project')
1288
    queryset = ProjectApplication.objects.filter(state=ProjectApplication.APPROVED)
1289
    queryset = queryset.filter(~Q(project__last_approval_date__isnull=True))
1290
    queryset = queryset.exclude(project__in=user_projects)
1291
    queryset = queryset.select_related()
1292
    form = ProjectSearchForm(request.POST or request.GET)
1293
    q = None
1294
    if form.is_valid():
1295
        q = form.cleaned_data['q'].strip()
1296
        queryset = queryset.filter(name__contains=q)
1297
    sorting = 'name'
1298
    # validate sorting
1299
    sort_form = ProjectSortForm(request.GET)
1300
    if sort_form.is_valid():
1301
        sorting = sort_form.cleaned_data.get('sorting')
1302
    queryset = queryset.order_by(sorting)
1303
    
1304
    return object_list(
1305
        request,
1306
        queryset,
1307
        paginate_by=PAGINATE_BY_ALL,
1308
        page=request.GET.get('page') or 1,
1309
        template_name='im/projects/project_list.html',
1310
        extra_context=dict(
1311
            form=form,
1312
            is_search=True,
1313
            sorting=sorting,
1314
            q=q,
1315
        )
1316
    )
1317

    
1318
@require_http_methods(["POST"])
1319
@signed_terms_required
1320
@login_required
1321
@transaction.commit_manually
1322
def project_join(request, application_id):
1323
    next = request.GET.get('next')
1324
    if not next:
1325
        return HttpResponseBadRequest(
1326
            _(astakos_messages.MISSING_NEXT_PARAMETER))
1327

    
1328
    rollback = False
1329
    try:
1330
        application_id = int(application_id)
1331
        join_project(application_id, request.user)
1332
    except (IOError, PermissionDenied), e:
1333
        messages.error(request, e)
1334
    except BaseException, e:
1335
        logger.exception(e)
1336
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1337
        rollback = True
1338
    finally:
1339
        if rollback:
1340
            transaction.rollback()
1341
        else:
1342
            transaction.commit()
1343
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1344
    return redirect(next)
1345

    
1346
@require_http_methods(["POST"])
1347
@signed_terms_required
1348
@login_required
1349
@transaction.commit_manually
1350
def project_leave(request, application_id):
1351
    next = request.GET.get('next')
1352
    if not next:
1353
        return HttpResponseBadRequest(
1354
            _(astakos_messages.MISSING_NEXT_PARAMETER))
1355

    
1356
    rollback = False
1357
    try:
1358
        application_id = int(application_id)
1359
        leave_project(application_id, request.user)
1360
    except (IOError, PermissionDenied), e:
1361
        messages.error(request, e)
1362
    except BaseException, e:
1363
        logger.exception(e)
1364
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1365
        rollback = True
1366
    finally:
1367
        if rollback:
1368
            transaction.rollback()
1369
        else:
1370
            transaction.commit()
1371

    
1372
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1373
    return redirect(next)
1374

    
1375
@require_http_methods(["GET"])
1376
@signed_terms_required
1377
@login_required
1378
@transaction.commit_manually
1379
def project_accept_member(request, application_id, user_id):
1380
    rollback = False
1381
    try:
1382
        application_id = int(application_id)
1383
        user_id = int(user_id)
1384
        m = accept_membership(application_id, user_id, request.user)
1385
    except (IOError, PermissionDenied), e:
1386
        messages.error(request, e)
1387
    except BaseException, e:
1388
        logger.exception(e)
1389
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1390
        rollback = True
1391
    else:
1392
        realname = m.person.realname
1393
        msg = _(astakos_messages.USER_JOINED_PROJECT) % locals()
1394
        messages.success(request, msg)
1395
    finally:
1396
        if rollback:
1397
            transaction.rollback()
1398
        else:
1399
            transaction.commit()
1400
    return redirect(reverse('project_detail', args=(application_id,)))
1401

    
1402
@require_http_methods(["GET"])
1403
@signed_terms_required
1404
@login_required
1405
@transaction.commit_manually
1406
def project_remove_member(request, application_id, user_id):
1407
    rollback = False
1408
    try:
1409
        application_id = int(application_id)
1410
        user_id = int(user_id)
1411
        m = remove_membership(application_id, user_id, request.user)
1412
    except (IOError, PermissionDenied), e:
1413
        messages.error(request, e)
1414
    except BaseException, e:
1415
        logger.exception(e)
1416
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1417
        rollback = True
1418
    else:
1419
        realname = m.person.realname
1420
        msg = _(astakos_messages.USER_LEFT_PROJECT) % locals()
1421
        messages.success(request, msg)
1422
    finally:
1423
        if rollback:
1424
            transaction.rollback()
1425
        else:
1426
            transaction.commit()
1427
    return redirect(reverse('project_detail', args=(application_id,)))
1428

    
1429
@require_http_methods(["GET"])
1430
@signed_terms_required
1431
@login_required
1432
@transaction.commit_manually
1433
def project_reject_member(request, application_id, user_id):
1434
    rollback = False
1435
    try:
1436
        application_id = int(application_id)
1437
        user_id = int(user_id)
1438
        m = reject_membership(application_id, user_id, request.user)
1439
    except (IOError, PermissionDenied), e:
1440
        messages.error(request, e)
1441
    except BaseException, e:
1442
        logger.exception(e)
1443
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1444
        rollback = True
1445
    else:
1446
        realname = m.person.realname
1447
        msg = _(astakos_messages.USER_LEFT_PROJECT) % locals()
1448
        messages.success(request, msg)
1449
    finally:
1450
        if rollback:
1451
            transaction.rollback()
1452
        else:
1453
            transaction.commit()
1454
    return redirect(reverse('project_detail', args=(application_id,)))
1455

    
1456