Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 120b4498

History | View | Annotate | Download (50.1 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
    ProjectApplication, ProjectMembership, Project)
77
from astakos.im.util import (
78
    get_context, prepare_response, get_query, restrict_next)
79
from astakos.im.forms import (
80
    LoginForm, InvitationForm, ProfileForm,
81
    FeedbackForm, SignApprovalTermsForm,
82
    EmailChangeForm,
83
#     AstakosGroupCreationForm, AstakosGroupSearchForm,
84
#     AstakosGroupUpdateForm, AddGroupMembersForm,
85
#     MembersSortForm, AstakosGroupSortForm,
86
#     TimelineForm, PickResourceForm,
87
#     AstakosGroupCreationSummaryForm,
88
    ProjectApplicationForm, ProjectSortForm,
89
    AddProjectMembersForm, ProjectSearchForm,
90
    ProjectMembersSortForm)
91
from astakos.im.functions import (
92
    send_feedback, SendMailError,
93
    logout as auth_logout,
94
    activate as activate_func,
95
    invite,
96
    send_activation as send_activation_func,
97
#     send_group_creation_notification,
98
    SendNotificationError,
99
    accept_membership, reject_membership, remove_membership,
100
    leave_project, join_project)
101
# from astakos.im.endpoints.qh import timeline_charge
102
from astakos.im.settings import (
103
    COOKIE_DOMAIN, LOGOUT_NEXT,
104
    LOGGING_LEVEL, PAGINATE_BY,
105
    RESOURCES_PRESENTATION_DATA, PAGINATE_BY_ALL)
106
#from astakos.im.tasks import request_billing
107
from astakos.im.api.callpoint import AstakosCallpoint
108

    
109
from astakos.im import settings
110
from astakos.im import auth_providers
111

    
112
logger = logging.getLogger(__name__)
113

    
114
callpoint = AstakosCallpoint()
115

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

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

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

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

    
151

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

    
166

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

    
182

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

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

    
194
        @wraps(func)
195
        def wrapper(request, *args, **kwargs):
196
            if request.user.is_authenticated():
197
                for required in required_providers:
198
                    if not request.user.has_auth_provider(required):
199
                        provider = auth_providers.get_provider(required)
200
                        if only_warn:
201
                            messages.error(request,
202
                                           _(astakos_messages.AUTH_PROVIDER_REQUIRED  % {
203
                                               'provider': provider.get_title_display}))
204
                        else:
205
                            return HttpResponseRedirect(reverse('edit_profile'))
206
            return func(request, *args, **kwargs)
207
        return wrapper
208
    return decorator
209

    
210

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

    
214

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

221
    **Arguments**
222

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

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

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

234
    **Template:**
235

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

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

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

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

    
254

    
255
@require_http_methods(["GET", "POST"])
256
@valid_astakos_user_required
257
@transaction.commit_manually
258
def invite(request, template_name='im/invitations.html', extra_context=None):
259
    """
260
    Allows a user to invite somebody else.
261

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

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

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

271
    **Arguments**
272

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

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

280
    **Template:**
281

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

284
    **Settings:**
285

286
    The view expectes the following settings are defined:
287

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

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

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

    
332

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

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

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

347
    **Arguments**
348

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

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

356
    **Template:**
357

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

360
    **Settings:**
361

362
    The view expectes the following settings are defined:
363

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

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

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

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

    
413

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

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

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

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

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

433
    **Arguments**
434

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

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

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

446
    **Template:**
447

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

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

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

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

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

    
487
                form.store_user(user, request)
488

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

    
530

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

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

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

544
    **Arguments**
545

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

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

553
    **Template:**
554

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

557
    **Settings:**
558

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

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

    
583

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

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

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

    
617

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

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

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

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

    
659

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

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

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

    
706

    
707
@require_http_methods(["GET", "POST"])
708
@valid_astakos_user_required
709
@transaction.commit_manually
710
def change_email(request, activation_key=None,
711
                 email_template_name='registration/email_change_email.txt',
712
                 form_template_name='registration/email_change_form.html',
713
                 confirm_template_name='registration/email_change_done.html',
714
                 extra_context=None):
715
    extra_context = extra_context or {}
716
    if activation_key:
717
        try:
718
            user = EmailChange.objects.change_email(activation_key)
719
            if request.user.is_authenticated() and request.user == user:
720
                msg = _(astakos_messages.EMAIL_CHANGED)
721
                messages.success(request, msg)
722
                auth_logout(request)
723
                response = prepare_response(request, user)
724
                transaction.commit()
725
                return response
726
        except ValueError, e:
727
            messages.error(request, e)
728
        return render_response(confirm_template_name,
729
                               modified_user=user if 'user' in locals(
730
                               ) else None,
731
                               context_instance=get_context(request,
732
                                                            extra_context))
733

    
734
    if not request.user.is_authenticated():
735
        path = quote(request.get_full_path())
736
        url = request.build_absolute_uri(reverse('index'))
737
        return HttpResponseRedirect(url + '?next=' + path)
738
    form = EmailChangeForm(request.POST or None)
739
    if request.method == 'POST' and form.is_valid():
740
        try:
741
            ec = form.save(email_template_name, request)
742
        except SendMailError, e:
743
            msg = e
744
            messages.error(request, msg)
745
            transaction.rollback()
746
        except IntegrityError, e:
747
            msg = _(astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
748
            messages.error(request, msg)
749
        else:
750
            msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
751
            messages.success(request, msg)
752
            transaction.commit()
753
    return render_response(
754
        form_template_name,
755
        form=form,
756
        context_instance=get_context(request, extra_context)
757
    )
758

    
759

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

    
762
    if request.user.is_authenticated():
763
        messages.error(request, _(astakos_messages.ALREADY_LOGGED_IN))
764
        return HttpResponseRedirect(reverse('edit_profile'))
765

    
766
    if settings.MODERATION_ENABLED:
767
        raise PermissionDenied
768

    
769
    extra_context = extra_context or {}
770
    try:
771
        u = AstakosUser.objects.get(id=user_id)
772
    except AstakosUser.DoesNotExist:
773
        messages.error(request, _(astakos_messages.ACCOUNT_UNKNOWN))
774
    else:
775
        try:
776
            send_activation_func(u)
777
            msg = _(astakos_messages.ACTIVATION_SENT)
778
            messages.success(request, msg)
779
        except SendMailError, e:
780
            messages.error(request, e)
781
    return render_response(
782
        template_name,
783
        login_form = LoginForm(request=request),
784
        context_instance = get_context(
785
            request,
786
            extra_context
787
        )
788
    )
789

    
790

    
791
# def handle_membership(func):
792
#     @wraps(func)
793
#     def wrapper(request, group_id, user_id):
794
#         try:
795
#             m = Membership.objects.select_related().get(
796
#                 group__id=group_id,
797
#                 person__id=user_id)
798
#         except Membership.DoesNotExist:
799
#             return HttpResponseBadRequest(_(astakos_messages.NOT_MEMBER))
800
#         else:
801
#             if request.user not in m.group.owner.all():
802
#                 return HttpResponseForbidden(_(astakos_messages.NOT_OWNER))
803
#             func(request, m)
804
#             return group_detail(request, group_id)
805
#     return wrapper
806

    
807

    
808
#@require_http_methods(["POST"])
809
# @require_http_methods(["POST", "GET"])
810
# @signed_terms_required
811
# @login_required
812
# @handle_membership
813
# def approve_member(request, membership):
814
#     try:
815
#         membership.approve()
816
#         realname = membership.person.realname
817
#         msg = _(astakos_messages.MEMBER_JOINED_GROUP) % locals()
818
#         messages.success(request, msg)
819
#     except AssertionError:
820
#         msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
821
#         messages.error(request, msg)
822
#     except BaseException, e:
823
#         logger.exception(e)
824
#         realname = membership.person.realname
825
#         msg = _(astakos_messages.GENERIC_ERROR)
826
#         messages.error(request, msg)
827

    
828

    
829
# @signed_terms_required
830
# @login_required
831
# @handle_membership
832
# def disapprove_member(request, membership):
833
#     try:
834
#         membership.disapprove()
835
#         realname = membership.person.realname
836
#         msg = astakos_messages.MEMBER_REMOVED % locals()
837
#         messages.success(request, msg)
838
#     except BaseException, e:
839
#         logger.exception(e)
840
#         msg = _(astakos_messages.GENERIC_ERROR)
841
#         messages.error(request, msg)
842

    
843

    
844
@require_http_methods(["GET"])
845
@require_http_methods(["POST", "GET"])
846
@valid_astakos_user_required
847
def resource_usage(request):
848
    def with_class(entry):
849
        entry['load_class'] = 'red'
850
        max_value = float(entry['maxValue'])
851
        curr_value = float(entry['currValue'])
852
        entry['ratio_limited']= 0
853
        if max_value > 0 :
854
            entry['ratio'] = (curr_value / max_value) * 100
855
        else:
856
            entry['ratio'] = 0
857
        if entry['ratio'] < 66:
858
            entry['load_class'] = 'yellow'
859
        if entry['ratio'] < 33:
860
            entry['load_class'] = 'green'
861
        if entry['ratio']<0:
862
            entry['ratio'] = 0
863
        if entry['ratio']>100:
864
            entry['ratio_limited'] = 100
865
        else:
866
            entry['ratio_limited'] = entry['ratio']
867

    
868
        return entry
869

    
870
    def pluralize(entry):
871
        entry['plural'] = engine.plural(entry.get('name'))
872
        return entry
873

    
874
    result = callpoint.get_user_usage(request.user.id)
875
    if result.is_success:
876
        backenddata = map(with_class, result.data)
877
        data = map(pluralize, result.data)
878
    else:
879
        data = None
880
        messages.error(request, result.reason)
881
    resource_catalog = result.data
882
#     resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
883
#     resource_catalog.update_from_result_report(result)
884
    return render_response('im/resource_usage.html',
885
                           data=data,
886
                           context_instance=get_context(request),
887
                           resource_catalog=resource_catalog,
888
                           result=result)
889

    
890

    
891
# def group_create_list(request):
892
#     form = PickResourceForm()
893
#     return render_response(
894
#         template='im/astakosgroup_create_list.html',
895
#         context_instance=get_context(request),)
896

    
897

    
898
##@require_http_methods(["GET"])
899
#@require_http_methods(["POST", "GET"])
900
#@signed_terms_required
901
#@login_required
902
#def billing(request):
903
#
904
#    today = datetime.today()
905
#    month_last_day = calendar.monthrange(today.year, today.month)[1]
906
#    start = request.POST.get('datefrom', None)
907
#    if start:
908
#        today = datetime.fromtimestamp(int(start))
909
#        month_last_day = calendar.monthrange(today.year, today.month)[1]
910
#
911
#    start = datetime(today.year, today.month, 1).strftime("%s")
912
#    end = datetime(today.year, today.month, month_last_day).strftime("%s")
913
#    r = request_billing.apply(args=('pgerakios@grnet.gr',
914
#                                    int(start) * 1000,
915
#                                    int(end) * 1000))
916
#    data = {}
917
#
918
#    try:
919
#        status, data = r.result
920
#        data = _clear_billing_data(data)
921
#        if status != 200:
922
#            messages.error(request, _(astakos_messages.BILLING_ERROR) % status)
923
#    except:
924
#        messages.error(request, r.result)
925
#
926
#    return render_response(
927
#        template='im/billing.html',
928
#        context_instance=get_context(request),
929
#        data=data,
930
#        zerodate=datetime(month=1, year=1970, day=1),
931
#        today=today,
932
#        start=int(start),
933
#        month_last_day=month_last_day)
934

    
935

    
936
#def _clear_billing_data(data):
937
#
938
#    # remove addcredits entries
939
#    def isnotcredit(e):
940
#        return e['serviceName'] != "addcredits"
941
#
942
#    # separate services
943
#    def servicefilter(service_name):
944
#        service = service_name
945
#
946
#        def fltr(e):
947
#            return e['serviceName'] == service
948
#        return fltr
949
#
950
#    data['bill_nocredits'] = filter(isnotcredit, data['bill'])
951
#    data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
952
#    data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
953
#    data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
954
#
955
#    return data
956

    
957

    
958
# #@require_http_methods(["GET"])
959
# @require_http_methods(["POST", "GET"])
960
# @signed_terms_required
961
# @login_required
962
# def timeline(request):
963
# #    data = {'entity':request.user.email}
964
#     timeline_body = ()
965
#     timeline_header = ()
966
# #    form = TimelineForm(data)
967
#     form = TimelineForm()
968
#     if request.method == 'POST':
969
#         data = request.POST
970
#         form = TimelineForm(data)
971
#         if form.is_valid():
972
#             data = form.cleaned_data
973
#             timeline_header = ('entity', 'resource',
974
#                                'event name', 'event date',
975
#                                'incremental cost', 'total cost')
976
#             timeline_body = timeline_charge(
977
#                 data['entity'], data['resource'],
978
#                 data['start_date'], data['end_date'],
979
#                 data['details'], data['operation'])
980
#
981
#     return render_response(template='im/timeline.html',
982
#                            context_instance=get_context(request),
983
#                            form=form,
984
#                            timeline_header=timeline_header,
985
#                            timeline_body=timeline_body)
986
#     return data
987

    
988

    
989
# TODO: action only on POST and user should confirm the removal
990
@require_http_methods(["GET", "POST"])
991
@login_required
992
@signed_terms_required
993
def remove_auth_provider(request, pk):
994
    try:
995
        provider = request.user.auth_providers.get(pk=pk)
996
    except AstakosUserAuthProvider.DoesNotExist:
997
        raise Http404
998

    
999
    if provider.can_remove():
1000
        provider.delete()
1001
        return HttpResponseRedirect(reverse('edit_profile'))
1002
    else:
1003
        raise PermissionDenied
1004

    
1005

    
1006
def how_it_works(request):
1007
    return render_response(
1008
        'im/how_it_works.html',
1009
        context_instance=get_context(request))
1010

    
1011
@transaction.commit_manually
1012
def _create_object(request, model=None, template_name=None,
1013
        template_loader=template_loader, extra_context=None, post_save_redirect=None,
1014
        login_required=False, context_processors=None, form_class=None):
1015
    """
1016
    Based of django.views.generic.create_update.create_object which displays a
1017
    summary page before creating the object.
1018
    """
1019
    rollback = False
1020
    response = None
1021

    
1022
    if extra_context is None: extra_context = {}
1023
    if login_required and not request.user.is_authenticated():
1024
        return redirect_to_login(request.path)
1025
    try:
1026

    
1027
        model, form_class = get_model_and_form_class(model, form_class)
1028
        extra_context['edit'] = 0
1029
        if request.method == 'POST':
1030
            form = form_class(request.POST, request.FILES)
1031
            if form.is_valid():
1032
                verify = request.GET.get('verify')
1033
                edit = request.GET.get('edit')
1034
                if verify == '1':
1035
                    extra_context['show_form'] = False
1036
                    extra_context['form_data'] = form.cleaned_data
1037
                elif edit == '1':
1038
                    extra_context['show_form'] = True
1039
                else:
1040
                    new_object = form.save()
1041

    
1042
                    msg = _("The %(verbose_name)s was created successfully.") %\
1043
                                {"verbose_name": model._meta.verbose_name}
1044
                    messages.success(request, msg, fail_silently=True)
1045
                    response = redirect(post_save_redirect, new_object)
1046
        else:
1047
            form = form_class()
1048
    except BaseException, e:
1049
        logger.exception(e)
1050
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1051
        rollback = True
1052
    finally:
1053
        if rollback:
1054
            transaction.rollback()
1055
        else:
1056
            transaction.commit()
1057

    
1058
        if response == None:
1059
            # Create the template, context, response
1060
            if not template_name:
1061
                template_name = "%s/%s_form.html" %\
1062
                     (model._meta.app_label, model._meta.object_name.lower())
1063
            t = template_loader.get_template(template_name)
1064
            c = RequestContext(request, {
1065
                'form': form
1066
            }, context_processors)
1067
            apply_extra_context(extra_context, c)
1068
            response = HttpResponse(t.render(c))
1069
        return response
1070

    
1071
@transaction.commit_manually
1072
def _update_object(request, model=None, object_id=None, slug=None,
1073
        slug_field='slug', template_name=None, template_loader=template_loader,
1074
        extra_context=None, post_save_redirect=None, login_required=False,
1075
        context_processors=None, template_object_name='object',
1076
        form_class=None):
1077
    """
1078
    Based of django.views.generic.create_update.update_object which displays a
1079
    summary page before updating the object.
1080
    """
1081
    rollback = False
1082
    response = None
1083

    
1084
    if extra_context is None: extra_context = {}
1085
    if login_required and not request.user.is_authenticated():
1086
        return redirect_to_login(request.path)
1087

    
1088
    try:
1089
        model, form_class = get_model_and_form_class(model, form_class)
1090
        obj = lookup_object(model, object_id, slug, slug_field)
1091

    
1092
        if request.method == 'POST':
1093
            form = form_class(request.POST, request.FILES, instance=obj)
1094
            if form.is_valid():
1095
                verify = request.GET.get('verify')
1096
                edit = request.GET.get('edit')
1097
                if verify == '1':
1098
                    extra_context['show_form'] = False
1099
                    extra_context['form_data'] = form.cleaned_data
1100
                elif edit == '1':
1101
                    extra_context['show_form'] = True
1102
                else:
1103
                    obj = form.save()
1104
                    msg = _("The %(verbose_name)s was updated successfully.") %\
1105
                                {"verbose_name": model._meta.verbose_name}
1106
                    messages.success(request, msg, fail_silently=True)
1107
                    response = redirect(post_save_redirect, obj)
1108
        else:
1109
            form = form_class(instance=obj)
1110
    except BaseException, e:
1111
        logger.exception(e)
1112
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1113
        rollback = True
1114
    finally:
1115
        if rollback:
1116
            transaction.rollback()
1117
        else:
1118
            transaction.commit()
1119
        if response == None:
1120
            if not template_name:
1121
                template_name = "%s/%s_form.html" %\
1122
                    (model._meta.app_label, model._meta.object_name.lower())
1123
            t = template_loader.get_template(template_name)
1124
            c = RequestContext(request, {
1125
                'form': form,
1126
                template_object_name: obj,
1127
            }, context_processors)
1128
            apply_extra_context(extra_context, c)
1129
            response = HttpResponse(t.render(c))
1130
            populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.attname))
1131
        return response
1132

    
1133
@require_http_methods(["GET", "POST"])
1134
@signed_terms_required
1135
@login_required
1136
def project_add(request):
1137
    result = callpoint.list_resources()
1138
    if not result.is_success:
1139
        messages.error(
1140
            request,
1141
            'Unable to retrieve system resources: %s' % result.reason
1142
    )
1143
    else:
1144
        resource_catalog = result.data
1145
    extra_context = {'resource_catalog':resource_catalog, 'show_form':True}
1146
    return _create_object(request, template_name='im/projects/projectapplication_form.html',
1147
        extra_context=extra_context, post_save_redirect='/im/project/list/',
1148
        form_class=ProjectApplicationForm)
1149

    
1150

    
1151
@require_http_methods(["GET"])
1152
@signed_terms_required
1153
@login_required
1154
def project_list(request):
1155
    q = ProjectApplication.objects.filter(owner=request.user)
1156
    q |= ProjectApplication.objects.filter(
1157
        project__in=request.user.projectmembership_set.values_list('project', flat=True)
1158
    )
1159
    q = q.select_related()
1160
    sorting = 'name'
1161
    sort_form = ProjectSortForm(request.GET)
1162
    if sort_form.is_valid():
1163
        sorting = sort_form.cleaned_data.get('sorting')
1164
    q = q.order_by(sorting)
1165

    
1166
    return object_list(
1167
        request,
1168
        q,
1169
        paginate_by=PAGINATE_BY_ALL,
1170
        page=request.GET.get('page') or 1,
1171
        template_name='im/projects/project_list.html',
1172
        extra_context={
1173
            'is_search':False,
1174
            'sorting':sorting
1175
        }
1176
    )
1177

    
1178
@require_http_methods(["GET", "POST"])
1179
@signed_terms_required
1180
@login_required
1181
def project_update(request, application_id):
1182
    result = callpoint.list_resources()
1183
    if not result.is_success:
1184
        messages.error(
1185
            request,
1186
            'Unable to retrieve system resources: %s' % result.reason
1187
    )
1188
    else:
1189
        resource_catalog = result.data
1190
    extra_context = {'resource_catalog':resource_catalog, 'show_form':True}
1191
    return _update_object(
1192
        request,
1193
        object_id=application_id,
1194
        template_name='im/projects/projectapplication_form.html',
1195
        extra_context=extra_context, post_save_redirect='/im/project/list/',
1196
        form_class=ProjectApplicationForm)
1197

    
1198

    
1199
@require_http_methods(["GET", "POST"])
1200
@signed_terms_required
1201
@login_required
1202
@transaction.commit_manually
1203
def project_detail(request, application_id):
1204
    result = callpoint.list_resources()
1205
    if not result.is_success:
1206
        messages.error(
1207
            request,
1208
            'Unable to retrieve system resources: %s' % result.reason
1209
    )
1210
    else:
1211
        resource_catalog = result.data
1212
#     resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
1213
#     resource_catalog.update_from_result(result)
1214

    
1215
    addmembers_form = AddProjectMembersForm()
1216
    if request.method == 'POST':
1217
        addmembers_form = AddProjectMembersForm(request.POST)
1218
        if addmembers_form.is_valid():
1219
            try:
1220
                rollback = False
1221
                map(lambda u: accept_membership(
1222
                        application_id,
1223
                        u,
1224
                        request_user=request.user),
1225
                    addmembers_form.valid_users)
1226
            except (IOError, PermissionDenied), e:
1227
                messages.error(request, e)
1228
            except BaseException, e:
1229
                rollback = True
1230
                messages.error(request, e)
1231
            finally:
1232
                if rollback == True:
1233
                    transaction.rollback()
1234
                else:
1235
                    transaction.commit()
1236
            addmembers_form = AddProjectMembersForm()
1237

    
1238
    # validate sorting
1239
    sorting = 'person__email'
1240
    form = ProjectMembersSortForm(request.GET or request.POST)
1241
    if form.is_valid():
1242
        sorting = form.cleaned_data.get('sorting')
1243

    
1244
    return object_detail(
1245
        request,
1246
        queryset=ProjectApplication.objects.select_related(),
1247
        object_id=application_id,
1248
        template_name='im/projects/project_detail.html',
1249
        extra_context={
1250
            'resource_catalog':resource_catalog,
1251
            'sorting':sorting,
1252
            'addmembers_form':addmembers_form
1253
        }
1254
    )
1255

    
1256
@require_http_methods(["GET", "POST"])
1257
@signed_terms_required
1258
@login_required
1259
def project_search(request):
1260
    queryset = ProjectApplication.objects
1261
    if request.method == 'GET':
1262
        form = ProjectSearchForm()
1263
        queryset = queryset.none()
1264
    else:
1265
        form = ProjectSearchForm(request.POST)
1266
        if form.is_valid():
1267
            q = form.cleaned_data['q'].strip()
1268
            queryset = queryset.filter(~Q(project__last_approval_date__isnull=True))
1269
            queryset = queryset.filter(name__contains=q)
1270
    sorting = 'name'
1271
    # validate sorting
1272
    sort_form = ProjectSortForm(request.GET)
1273
    if sort_form.is_valid():
1274
        sorting = sort_form.cleaned_data.get('sorting')
1275
    queryset = queryset.order_by(sorting)
1276
    return object_list(
1277
        request,
1278
        queryset,
1279
        paginate_by=PAGINATE_BY_ALL,
1280
        page=request.GET.get('page') or 1,
1281
        template_name='im/projects/project_list.html',
1282
        extra_context=dict(
1283
            form=form,
1284
            is_search=True,
1285
            sorting=sorting
1286
        )
1287
    )
1288

    
1289

    
1290
@require_http_methods(["GET"])
1291
@signed_terms_required
1292
@login_required
1293
def project_all(request):
1294
    q = ProjectApplication.objects.filter(
1295
        ~Q(project__last_approval_date__isnull=True))
1296
    q = q.select_related()
1297
    sorting = 'name'
1298
    sort_form = ProjectSortForm(request.GET)
1299
    if sort_form.is_valid():
1300
        sorting = sort_form.cleaned_data.get('sorting')
1301
    q = q.order_by(sorting)
1302

    
1303
    return object_list(
1304
        request,
1305
        q,
1306
        paginate_by=PAGINATE_BY_ALL,
1307
        page=request.GET.get('page') or 1,
1308
        template_name='im/projects/project_list.html',
1309
        extra_context={
1310
            'form':ProjectSearchForm(),
1311
            'is_search':True,
1312
            'sorting':sorting
1313
        }
1314
    )
1315

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

    
1326
    rollback = False
1327
    try:
1328
        join_project(application_id, request.user)
1329
    except (IOError, PermissionDenied), e:
1330
        messages.error(request, e)
1331
    except BaseException, e:
1332
        logger.exception(e)
1333
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1334
        rollback = True
1335
    else:
1336
        return project_detail(request, id)
1337
    finally:
1338
        if rollback:
1339
            transaction.rollback()
1340
        else:
1341
            transaction.commit()
1342

    
1343
    next = restrict_next(
1344
        request.POST.get('next'),
1345
        domain=COOKIE_DOMAIN)
1346
    return redirect(next)
1347

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

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

    
1373
    next = restrict_next(
1374
        request.POST.get('next'),
1375
        domain=COOKIE_DOMAIN)
1376
    return redirect(next)
1377

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

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

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

    
1453