Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 3f8570dc

History | View | Annotate | Download (48.3 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_tables2 import RequestConfig
45

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

    
72
import astakos.im.messages as astakos_messages
73

    
74
from astakos.im.activation_backends import get_backend, SimpleBackend
75
from astakos.im import tables
76
from astakos.im.models import (
77
    AstakosUser, ApprovalTerms,
78
    EmailChange, RESOURCE_SEPARATOR,
79
    AstakosUserAuthProvider, PendingThirdPartyUser,
80
    ProjectApplication, ProjectMembership, Project)
81
from astakos.im.util import (
82
    get_context, prepare_response, get_query, restrict_next)
83
from astakos.im.forms import (
84
    LoginForm, InvitationForm,
85
    FeedbackForm, SignApprovalTermsForm,
86
    EmailChangeForm,
87
    ProjectApplicationForm, ProjectSortForm,
88
    AddProjectMembersForm, ProjectSearchForm,
89
    ProjectMembersSortForm)
90
from astakos.im.forms import ExtendedProfileForm as ProfileForm
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
    SendNotificationError,
98
    accept_membership, reject_membership, remove_membership,
99
    leave_project, join_project, enroll_member)
100
from astakos.im.settings import (
101
    COOKIE_DOMAIN, LOGOUT_NEXT,
102
    LOGGING_LEVEL, PAGINATE_BY,
103
    RESOURCES_PRESENTATION_DATA, PAGINATE_BY_ALL,
104
    ACTIVATION_REDIRECT_URL,
105
    MODERATION_ENABLED)
106
from astakos.im.api import get_services_dict
107
from astakos.im import settings as astakos_settings
108
from astakos.im.api.callpoint import AstakosCallpoint
109
from astakos.im import auth_providers
110

    
111
logger = logging.getLogger(__name__)
112

    
113
callpoint = AstakosCallpoint()
114

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

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

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

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

    
150

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

    
165

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

    
181

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

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

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

    
209

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

    
213

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

220
    **Arguments**
221

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

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

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

233
    **Template:**
234

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

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

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

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

    
253

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

    
266

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

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

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

281
    If the user isn't logged in, redirects to settings.LOGIN_URL.
282

283
    **Arguments**
284

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

289
    ``extra_context``
290
        An dictionary of variables to add to the template context.
291

292
    **Template:**
293

294
    im/invitations.html or ``template_name`` keyword argument.
295

296
    **Settings:**
297

298
    The view expectes the following settings are defined:
299

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

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

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

    
344

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

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

357
    If the user isn't logged in, redirects to settings.LOGIN_URL.
358

359
    **Arguments**
360

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

365
    ``extra_context``
366
        An dictionary of variables to add to the template context.
367

368
    **Template:**
369

370
    im/profile.html or ``template_name`` keyword argument.
371

372
    **Settings:**
373

374
    The view expectes the following settings are defined:
375

376
    * LOGIN_URL: login uri
377
    """
378
    extra_context = extra_context or {}
379
    form = ProfileForm(
380
        instance=request.user,
381
        session_key=request.session.session_key
382
    )
383
    extra_context['next'] = request.GET.get('next')
384
    if request.method == 'POST':
385
        form = ProfileForm(
386
            request.POST,
387
            instance=request.user,
388
            session_key=request.session.session_key
389
        )
390
        if form.is_valid():
391
            try:
392
                prev_token = request.user.auth_token
393
                user = form.save(request=request)
394
                next = restrict_next(
395
                    request.POST.get('next'),
396
                    domain=COOKIE_DOMAIN
397
                )
398
                msg = _(astakos_messages.PROFILE_UPDATED)
399
                messages.success(request, msg)
400
                if next:
401
                    return redirect(next)
402
                else:
403
                    return redirect(reverse('edit_profile'))
404
            except ValueError, ve:
405
                messages.success(request, ve)
406
    elif request.method == "GET":
407
        request.user.is_verified = True
408
        request.user.save()
409

    
410
    # existing providers
411
    user_providers = request.user.get_active_auth_providers()
412

    
413
    # providers that user can add
414
    user_available_providers = request.user.get_available_auth_providers()
415

    
416
    extra_context['services'] = get_services_dict()
417
    return render_response(template_name,
418
                           profile_form = form,
419
                           user_providers = user_providers,
420
                           user_available_providers = user_available_providers,
421
                           context_instance = get_context(request,
422
                                                          extra_context))
423

    
424

    
425
@transaction.commit_manually
426
@require_http_methods(["GET", "POST"])
427
def signup(request, template_name='im/signup.html', on_success='index', extra_context=None, backend=None):
428
    """
429
    Allows a user to create a local account.
430

431
    In case of GET request renders a form for entering the user information.
432
    In case of POST handles the signup.
433

434
    The user activation will be delegated to the backend specified by the ``backend`` keyword argument
435
    if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
436
    if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
437
    (see activation_backends);
438

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

442
    On unsuccessful creation, renders ``template_name`` with an error message.
443

444
    **Arguments**
445

446
    ``template_name``
447
        A custom template to render. This is optional;
448
        if not specified, this will default to ``im/signup.html``.
449

450
    ``extra_context``
451
        An dictionary of variables to add to the template context.
452

453
    ``on_success``
454
        Resolvable view name to redirect on registration success.
455

456
    **Template:**
457

458
    im/signup.html or ``template_name`` keyword argument.
459
    """
460
    extra_context = extra_context or {}
461
    if request.user.is_authenticated():
462
        return HttpResponseRedirect(reverse('edit_profile'))
463

    
464
    provider = get_query(request).get('provider', 'local')
465
    if not auth_providers.get_provider(provider).is_available_for_create():
466
        raise PermissionDenied
467

    
468
    id = get_query(request).get('id')
469
    try:
470
        instance = AstakosUser.objects.get(id=id) if id else None
471
    except AstakosUser.DoesNotExist:
472
        instance = None
473

    
474
    third_party_token = request.REQUEST.get('third_party_token', None)
475
    if third_party_token:
476
        pending = get_object_or_404(PendingThirdPartyUser,
477
                                    token=third_party_token)
478
        provider = pending.provider
479
        instance = pending.get_user_instance()
480

    
481
    try:
482
        if not backend:
483
            backend = get_backend(request)
484
        form = backend.get_signup_form(provider, instance)
485
    except Exception, e:
486
        form = SimpleBackend(request).get_signup_form(provider)
487
        messages.error(request, e)
488
    if request.method == 'POST':
489
        if form.is_valid():
490
            user = form.save(commit=False)
491

    
492
            # delete previously unverified accounts
493
            if AstakosUser.objects.user_exists(user.email):
494
                AstakosUser.objects.get_by_identifier(user.email).delete()
495

    
496
            try:
497
                result = backend.handle_activation(user)
498
                status = messages.SUCCESS
499
                message = result.message
500

    
501
                form.store_user(user, request)
502

    
503
                if 'additional_email' in form.cleaned_data:
504
                    additional_email = form.cleaned_data['additional_email']
505
                    if additional_email != user.email:
506
                        user.additionalmail_set.create(email=additional_email)
507
                        msg = 'Additional email: %s saved for user %s.' % (
508
                            additional_email,
509
                            user.email
510
                        )
511
                        logger._log(LOGGING_LEVEL, msg, [])
512

    
513
                if user and user.is_active:
514
                    next = request.POST.get('next', '')
515
                    response = prepare_response(request, user, next=next)
516
                    transaction.commit()
517
                    return response
518

    
519
                transaction.commit()
520
                messages.add_message(request, status, message)
521
                return HttpResponseRedirect(reverse(on_success))
522

    
523
            except SendMailError, e:
524
                logger.exception(e)
525
                status = messages.ERROR
526
                message = e.message
527
                messages.error(request, message)
528
                transaction.rollback()
529
            except BaseException, e:
530
                logger.exception(e)
531
                message = _(astakos_messages.GENERIC_ERROR)
532
                messages.error(request, message)
533
                logger.exception(e)
534
                transaction.rollback()
535

    
536
    return render_response(template_name,
537
                           signup_form=form,
538
                           third_party_token=third_party_token,
539
                           provider=provider,
540
                           context_instance=get_context(request, extra_context))
541

    
542

    
543
@require_http_methods(["GET", "POST"])
544
@required_auth_methods_assigned(only_warn=True)
545
@login_required
546
@signed_terms_required
547
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
548
    """
549
    Allows a user to send feedback.
550

551
    In case of GET request renders a form for providing the feedback information.
552
    In case of POST sends an email to support team.
553

554
    If the user isn't logged in, redirects to settings.LOGIN_URL.
555

556
    **Arguments**
557

558
    ``template_name``
559
        A custom template to use. This is optional; if not specified,
560
        this will default to ``im/feedback.html``.
561

562
    ``extra_context``
563
        An dictionary of variables to add to the template context.
564

565
    **Template:**
566

567
    im/signup.html or ``template_name`` keyword argument.
568

569
    **Settings:**
570

571
    * LOGIN_URL: login uri
572
    """
573
    extra_context = extra_context or {}
574
    if request.method == 'GET':
575
        form = FeedbackForm()
576
    if request.method == 'POST':
577
        if not request.user:
578
            return HttpResponse('Unauthorized', status=401)
579

    
580
        form = FeedbackForm(request.POST)
581
        if form.is_valid():
582
            msg = form.cleaned_data['feedback_msg']
583
            data = form.cleaned_data['feedback_data']
584
            try:
585
                send_feedback(msg, data, request.user, email_template_name)
586
            except SendMailError, e:
587
                messages.error(request, message)
588
            else:
589
                message = _(astakos_messages.FEEDBACK_SENT)
590
                messages.success(request, message)
591
    return render_response(template_name,
592
                           feedback_form=form,
593
                           context_instance=get_context(request, extra_context))
594

    
595

    
596
@require_http_methods(["GET"])
597
@signed_terms_required
598
def logout(request, template='registration/logged_out.html', extra_context=None):
599
    """
600
    Wraps `django.contrib.auth.logout`.
601
    """
602
    extra_context = extra_context or {}
603
    response = HttpResponse()
604
    if request.user.is_authenticated():
605
        email = request.user.email
606
        auth_logout(request)
607
    else:
608
        response['Location'] = reverse('index')
609
        response.status_code = 301
610
        return response
611

    
612
    next = restrict_next(
613
        request.GET.get('next'),
614
        domain=COOKIE_DOMAIN
615
    )
616

    
617
    if next:
618
        response['Location'] = next
619
        response.status_code = 302
620
    elif LOGOUT_NEXT:
621
        response['Location'] = LOGOUT_NEXT
622
        response.status_code = 301
623
    else:
624
        message = _(astakos_messages.LOGOUT_SUCCESS)
625
        last_provider = request.COOKIES.get('astakos_last_login_method', None)
626
        if last_provider:
627
            provider = auth_providers.get_provider(last_provider)
628
            extra_message = provider.get_logout_message_display
629
            if extra_message:
630
                message += '<br />' + extra_message
631
        messages.add_message(request, messages.SUCCESS, mark_safe(message))
632
        response['Location'] = reverse('index')
633
        response.status_code = 301
634
    return response
635

    
636

    
637
@require_http_methods(["GET", "POST"])
638
@transaction.commit_manually
639
def activate(request, greeting_email_template_name='im/welcome_email.txt',
640
             helpdesk_email_template_name='im/helpdesk_notification.txt'):
641
    """
642
    Activates the user identified by the ``auth`` request parameter, sends a welcome email
643
    and renews the user token.
644

645
    The view uses commit_manually decorator in order to ensure the user state will be updated
646
    only if the email will be send successfully.
647
    """
648
    token = request.GET.get('auth')
649
    next = request.GET.get('next')
650
    try:
651
        user = AstakosUser.objects.get(auth_token=token)
652
    except AstakosUser.DoesNotExist:
653
        return HttpResponseBadRequest(_(astakos_messages.ACCOUNT_UNKNOWN))
654

    
655
    if user.is_active:
656
        message = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE)
657
        messages.error(request, message)
658
        return index(request)
659

    
660
    try:
661
        activate_func(user, greeting_email_template_name,
662
                      helpdesk_email_template_name, verify_email=True)
663
        next = ACTIVATION_REDIRECT_URL or next
664
        response = prepare_response(request, user, next, renew=True)
665
        transaction.commit()
666
        return response
667
    except SendMailError, e:
668
        message = e.message
669
        messages.add_message(request, messages.ERROR, message)
670
        transaction.rollback()
671
        return index(request)
672
    except BaseException, e:
673
        status = messages.ERROR
674
        message = _(astakos_messages.GENERIC_ERROR)
675
        messages.add_message(request, messages.ERROR, message)
676
        logger.exception(e)
677
        transaction.rollback()
678
        return index(request)
679

    
680

    
681
@require_http_methods(["GET", "POST"])
682
def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
683
    extra_context = extra_context or {}
684
    term = None
685
    terms = None
686
    if not term_id:
687
        try:
688
            term = ApprovalTerms.objects.order_by('-id')[0]
689
        except IndexError:
690
            pass
691
    else:
692
        try:
693
            term = ApprovalTerms.objects.get(id=term_id)
694
        except ApprovalTerms.DoesNotExist, e:
695
            pass
696

    
697
    if not term:
698
        messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
699
        return HttpResponseRedirect(reverse('index'))
700
    f = open(term.location, 'r')
701
    terms = f.read()
702

    
703
    if request.method == 'POST':
704
        next = restrict_next(
705
            request.POST.get('next'),
706
            domain=COOKIE_DOMAIN
707
        )
708
        if not next:
709
            next = reverse('index')
710
        form = SignApprovalTermsForm(request.POST, instance=request.user)
711
        if not form.is_valid():
712
            return render_response(template_name,
713
                                   terms=terms,
714
                                   approval_terms_form=form,
715
                                   context_instance=get_context(request, extra_context))
716
        user = form.save()
717
        return HttpResponseRedirect(next)
718
    else:
719
        form = None
720
        if request.user.is_authenticated() and not request.user.signed_terms:
721
            form = SignApprovalTermsForm(instance=request.user)
722
        return render_response(template_name,
723
                               terms=terms,
724
                               approval_terms_form=form,
725
                               context_instance=get_context(request, extra_context))
726

    
727

    
728
@require_http_methods(["GET", "POST"])
729
@transaction.commit_manually
730
def change_email(request, activation_key=None,
731
                 email_template_name='registration/email_change_email.txt',
732
                 form_template_name='registration/email_change_form.html',
733
                 confirm_template_name='registration/email_change_done.html',
734
                 extra_context=None):
735
    extra_context = extra_context or {}
736

    
737

    
738
    if not astakos_settings.EMAILCHANGE_ENABLED:
739
        raise PermissionDenied
740

    
741
    if activation_key:
742
        try:
743
            user = EmailChange.objects.change_email(activation_key)
744
            if request.user.is_authenticated() and request.user == user or not \
745
                    request.user.is_authenticated():
746
                msg = _(astakos_messages.EMAIL_CHANGED)
747
                messages.success(request, msg)
748
                transaction.commit()
749
                return HttpResponseRedirect(reverse('edit_profile'))
750
        except ValueError, e:
751
            messages.error(request, e)
752
            transaction.rollback()
753
            return HttpResponseRedirect(reverse('index'))
754

    
755
        return render_response(confirm_template_name,
756
                               modified_user=user if 'user' in locals() \
757
                               else None, context_instance=get_context(request,
758
                                                            extra_context))
759

    
760
    if not request.user.is_authenticated():
761
        path = quote(request.get_full_path())
762
        url = request.build_absolute_uri(reverse('index'))
763
        return HttpResponseRedirect(url + '?next=' + path)
764

    
765
    # clean up expired email changes
766
    if request.user.email_change_is_pending():
767
        change = request.user.emailchanges.get()
768
        if change.activation_key_expired():
769
            change.delete()
770
            transaction.commit()
771
            return HttpResponseRedirect(reverse('email_change'))
772

    
773
    form = EmailChangeForm(request.POST or None)
774
    if request.method == 'POST' and form.is_valid():
775
        try:
776
            ec = form.save(email_template_name, request)
777
        except SendMailError, e:
778
            msg = e
779
            messages.error(request, msg)
780
            transaction.rollback()
781
            return HttpResponseRedirect(reverse('edit_profile'))
782
        else:
783
            msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
784
            messages.success(request, msg)
785
            transaction.commit()
786
            return HttpResponseRedirect(reverse('edit_profile'))
787

    
788
    if request.user.email_change_is_pending():
789
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
790

    
791
    return render_response(
792
        form_template_name,
793
        form=form,
794
        context_instance=get_context(request, extra_context)
795
    )
796

    
797

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

    
800
    if request.user.is_authenticated():
801
        messages.error(request, _(astakos_messages.ALREADY_LOGGED_IN))
802
        return HttpResponseRedirect(reverse('edit_profile'))
803

    
804
    if astakos_settings.MODERATION_ENABLED:
805
        raise PermissionDenied
806

    
807
    extra_context = extra_context or {}
808
    try:
809
        u = AstakosUser.objects.get(id=user_id)
810
    except AstakosUser.DoesNotExist:
811
        messages.error(request, _(astakos_messages.ACCOUNT_UNKNOWN))
812
    else:
813
        try:
814
            send_activation_func(u)
815
            msg = _(astakos_messages.ACTIVATION_SENT)
816
            messages.success(request, msg)
817
        except SendMailError, e:
818
            messages.error(request, e)
819
    return render_response(
820
        template_name,
821
        login_form = LoginForm(request=request),
822
        context_instance = get_context(
823
            request,
824
            extra_context
825
        )
826
    )
827

    
828

    
829
@require_http_methods(["GET"])
830
@valid_astakos_user_required
831
def resource_usage(request):
832

    
833
    def with_class(entry):
834
         entry['load_class'] = 'red'
835
         max_value = float(entry['maxValue'])
836
         curr_value = float(entry['currValue'])
837
         entry['ratio_limited']= 0
838
         if max_value > 0 :
839
             entry['ratio'] = (curr_value / max_value) * 100
840
         else:
841
             entry['ratio'] = 0
842
         if entry['ratio'] < 66:
843
             entry['load_class'] = 'yellow'
844
         if entry['ratio'] < 33:
845
             entry['load_class'] = 'green'
846
         if entry['ratio']<0:
847
             entry['ratio'] = 0
848
         if entry['ratio']>100:
849
             entry['ratio_limited'] = 100
850
         else:
851
             entry['ratio_limited'] = entry['ratio']
852
         return entry
853

    
854
    def pluralize(entry):
855
        entry['plural'] = engine.plural(entry.get('name'))
856
        return entry
857

    
858
    resource_usage = None
859
    result = callpoint.get_user_usage(request.user.id)
860
    if result.is_success:
861
        resource_usage = result.data
862
        backenddata = map(with_class, result.data)
863
        backenddata = map(pluralize , backenddata)
864
    else:
865
        messages.error(request, result.reason)
866
        backenddata = []
867
        resource_usage = []
868

    
869
    if request.REQUEST.get('json', None):
870
        return HttpResponse(json.dumps(backenddata),
871
                            mimetype="application/json")
872

    
873
    return render_response('im/resource_usage.html',
874
                           context_instance=get_context(request),
875
                           resource_usage=backenddata,
876
                           usage_update_interval=astakos_settings.USAGE_UPDATE_INTERVAL,
877
                           result=result)
878

    
879
# TODO: action only on POST and user should confirm the removal
880
@require_http_methods(["GET", "POST"])
881
@login_required
882
@signed_terms_required
883
def remove_auth_provider(request, pk):
884
    try:
885
        provider = request.user.auth_providers.get(pk=pk)
886
    except AstakosUserAuthProvider.DoesNotExist:
887
        raise Http404
888

    
889
    if provider.can_remove():
890
        provider.delete()
891
        return HttpResponseRedirect(reverse('edit_profile'))
892
    else:
893
        raise PermissionDenied
894

    
895

    
896
def how_it_works(request):
897
    return render_response(
898
        'im/how_it_works.html',
899
        context_instance=get_context(request))
900

    
901
@transaction.commit_manually
902
def _create_object(request, model=None, template_name=None,
903
        template_loader=template_loader, extra_context=None, post_save_redirect=None,
904
        login_required=False, context_processors=None, form_class=None,
905
        msg=None):
906
    """
907
    Based of django.views.generic.create_update.create_object which displays a
908
    summary page before creating the object.
909
    """
910
    rollback = False
911
    response = None
912

    
913
    if extra_context is None: extra_context = {}
914
    if login_required and not request.user.is_authenticated():
915
        return redirect_to_login(request.path)
916
    try:
917

    
918
        model, form_class = get_model_and_form_class(model, form_class)
919
        extra_context['edit'] = 0
920
        if request.method == 'POST':
921
            form = form_class(request.POST, request.FILES)
922
            if form.is_valid():
923
                verify = request.GET.get('verify')
924
                edit = request.GET.get('edit')
925
                if verify == '1':
926
                    extra_context['show_form'] = False
927
                    extra_context['form_data'] = form.cleaned_data
928
                elif edit == '1':
929
                    extra_context['show_form'] = True
930
                else:
931
                    new_object = form.save()
932
                    if not msg:
933
                        msg = _("The %(verbose_name)s was created successfully.")
934
                    msg = msg % model._meta.__dict__
935
                    messages.success(request, msg, fail_silently=True)
936
                    response = redirect(post_save_redirect, new_object)
937
        else:
938
            form = form_class()
939
    except BaseException, e:
940
        logger.exception(e)
941
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
942
        rollback = True
943
    finally:
944
        if rollback:
945
            transaction.rollback()
946
        else:
947
            transaction.commit()
948

    
949
        if response == None:
950
            # Create the template, context, response
951
            if not template_name:
952
                template_name = "%s/%s_form.html" %\
953
                     (model._meta.app_label, model._meta.object_name.lower())
954
            t = template_loader.get_template(template_name)
955
            c = RequestContext(request, {
956
                'form': form
957
            }, context_processors)
958
            apply_extra_context(extra_context, c)
959
            response = HttpResponse(t.render(c))
960
        return response
961

    
962
@transaction.commit_manually
963
def _update_object(request, model=None, object_id=None, slug=None,
964
        slug_field='slug', template_name=None, template_loader=template_loader,
965
        extra_context=None, post_save_redirect=None, login_required=False,
966
        context_processors=None, template_object_name='object',
967
        form_class=None, msg=None):
968
    """
969
    Based of django.views.generic.create_update.update_object which displays a
970
    summary page before updating the object.
971
    """
972
    rollback = False
973
    response = None
974

    
975
    if extra_context is None: extra_context = {}
976
    if login_required and not request.user.is_authenticated():
977
        return redirect_to_login(request.path)
978

    
979
    try:
980
        model, form_class = get_model_and_form_class(model, form_class)
981
        obj = lookup_object(model, object_id, slug, slug_field)
982

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

    
1025
@require_http_methods(["GET", "POST"])
1026
@signed_terms_required
1027
@login_required
1028
def project_add(request):
1029
    resource_groups = RESOURCES_PRESENTATION_DATA.get('groups', {})
1030
    resource_catalog = ()
1031
    result = callpoint.list_resources()
1032
    details_fields = [
1033
        "name", "homepage", "description","start_date","end_date", "comments"]
1034
    membership_fields =[
1035
        "member_join_policy", "member_leave_policy", "limit_on_members_number"]
1036
    if not result.is_success:
1037
        messages.error(
1038
            request,
1039
            'Unable to retrieve system resources: %s' % result.reason
1040
    )
1041
    else:
1042
        resource_catalog = [
1043
            (g, filter(lambda r: r.get('group', '') == g, result.data)) \
1044
                for g in resource_groups]
1045
    extra_context = {
1046
        'resource_catalog':resource_catalog,
1047
        'resource_groups':resource_groups,
1048
        'show_form':True,
1049
        'details_fields':details_fields,
1050
        'membership_fields':membership_fields}
1051
    return _create_object(
1052
        request,
1053
        template_name='im/projects/projectapplication_form.html',
1054
        extra_context=extra_context,
1055
        post_save_redirect=reverse('project_list'),
1056
        form_class=ProjectApplicationForm,
1057
        msg=_("The %(verbose_name)s has been received and \
1058
                 is under consideration."))
1059

    
1060

    
1061
@require_http_methods(["GET"])
1062
@signed_terms_required
1063
@login_required
1064
def project_list(request):
1065
    projects = ProjectApplication.objects.user_accessible_projects(request.user).select_related()
1066
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1067
                                                prefix="my_projects_")
1068
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1069

    
1070
    return object_list(
1071
        request,
1072
        projects,
1073
        template_name='im/projects/project_list.html',
1074
        extra_context={
1075
            'is_search':False,
1076
            'table': table,
1077
        })
1078

    
1079

    
1080
@require_http_methods(["GET", "POST"])
1081
@signed_terms_required
1082
@login_required
1083
def project_update(request, application_id):
1084
    resource_groups = RESOURCES_PRESENTATION_DATA.get('groups', {})
1085
    resource_catalog = ()
1086
    result = callpoint.list_resources()
1087
    details_fields = [
1088
        "name", "homepage", "description","start_date","end_date", "comments"]
1089
    membership_fields =[
1090
        "member_join_policy", "member_leave_policy", "limit_on_members_number"]
1091
    if not result.is_success:
1092
        messages.error(
1093
            request,
1094
            'Unable to retrieve system resources: %s' % result.reason
1095
    )
1096
    else:
1097
        resource_catalog = [
1098
            (g, filter(lambda r: r.get('group', '') == g, result.data)) \
1099
                for g in resource_groups]
1100
    extra_context = {
1101
        'resource_catalog':resource_catalog,
1102
        'resource_groups':resource_groups,
1103
        'show_form':True,
1104
        'details_fields':details_fields,
1105
        'update_form': True,
1106
        'membership_fields':membership_fields}
1107
    return _update_object(
1108
        request,
1109
        object_id=application_id,
1110
        template_name='im/projects/projectapplication_form.html',
1111
        extra_context=extra_context, post_save_redirect=reverse('project_list'),
1112
        form_class=ProjectApplicationForm,
1113
        msg = _("The %(verbose_name)s has been received and \
1114
                    is under consideration."))
1115

    
1116

    
1117
@require_http_methods(["GET", "POST"])
1118
@signed_terms_required
1119
@login_required
1120
@transaction.commit_on_success
1121
def project_detail(request, application_id):
1122
    addmembers_form = AddProjectMembersForm()
1123
    if request.method == 'POST':
1124
        addmembers_form = AddProjectMembersForm(
1125
            request.POST,
1126
            application_id=int(application_id),
1127
            request_user=request.user)
1128
        if addmembers_form.is_valid():
1129
            try:
1130
                rollback = False
1131
                application_id = int(application_id)
1132
                map(lambda u: enroll_member(
1133
                        application_id,
1134
                        u,
1135
                        request_user=request.user),
1136
                    addmembers_form.valid_users)
1137
            except (IOError, PermissionDenied), e:
1138
                messages.error(request, e)
1139
            except BaseException, e:
1140
                rollback = True
1141
                messages.error(request, e)
1142
            finally:
1143
                if rollback == True:
1144
                    transaction.rollback()
1145
                else:
1146
                    transaction.commit()
1147
            addmembers_form = AddProjectMembersForm()
1148

    
1149
    rollback = False
1150

    
1151
    application = get_object_or_404(ProjectApplication, pk=application_id)
1152
    try:
1153
        members = application.project.projectmembership_set.select_related()
1154
    except Project.DoesNotExist:
1155
        members = ProjectMembership.objects.none()
1156

    
1157
    members_table = tables.ProjectApplicationMembersTable(application,
1158
                                                          members,
1159
                                                          user=request.user,
1160
                                                          prefix="members_")
1161
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(members_table)
1162

    
1163
    modifications_table = None
1164
    if application.follower:
1165
        following_applications = list(application.followers())
1166
        following_applications.reverse()
1167
        modifications_table = \
1168
            tables.ProjectModificationApplicationsTable(following_applications,
1169
                                                       user=request.user,
1170
                                                       prefix="modifications_")
1171

    
1172
    return object_detail(
1173
        request,
1174
        queryset=ProjectApplication.objects.select_related(),
1175
        object_id=application_id,
1176
        template_name='im/projects/project_detail.html',
1177
        extra_context={
1178
            'addmembers_form':addmembers_form,
1179
            'members_table': members_table,
1180
            'user_owns_project': request.user.owns_project(application),
1181
            'modifications_table': modifications_table,
1182
            'member_status': application.user_status(request.user)
1183
            })
1184

    
1185
@require_http_methods(["GET", "POST"])
1186
@signed_terms_required
1187
@login_required
1188
def project_search(request):
1189
    q = request.GET.get('q', '')
1190
    form = ProjectSearchForm()
1191
    q = q.strip()
1192

    
1193
    if request.method == "POST":
1194
        form = ProjectSearchForm(request.POST)
1195
        if form.is_valid():
1196
            q = form.cleaned_data['q'].strip()
1197
        else:
1198
            q = None
1199

    
1200
    if q is None:
1201
        projects = ProjectApplication.objects.none()
1202
    else:
1203
        accepted_projects = request.user.projectmembership_set.filter(
1204
            ~Q(acceptance_date__isnull=True)).values_list('project', flat=True)
1205
        projects = ProjectApplication.objects.search_by_name(q)
1206
        projects = projects.filter(~Q(project__last_approval_date__isnull=True))
1207
        projects = projects.exclude(project__in=accepted_projects)
1208

    
1209
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1210
                                                prefix="my_projects_")
1211
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1212

    
1213
    return object_list(
1214
        request,
1215
        projects,
1216
        template_name='im/projects/project_list.html',
1217
        extra_context={
1218
          'form': form,
1219
          'is_search': True,
1220
          'q': q,
1221
          'table': table
1222
        })
1223

    
1224
@require_http_methods(["POST", "GET"])
1225
@signed_terms_required
1226
@login_required
1227
@transaction.commit_manually
1228
def project_join(request, application_id):
1229
    next = request.GET.get('next')
1230
    if not next:
1231
        next = reverse('astakos.im.views.project_detail',
1232
                       args=(application_id,))
1233

    
1234
    rollback = False
1235
    try:
1236
        application_id = int(application_id)
1237
        join_project(application_id, request.user)
1238
        # TODO: distinct messages for request/auto accept ???
1239
        messages.success(request, _(astakos_messages.USER_JOIN_REQUEST_SUBMITED))
1240
    except (IOError, PermissionDenied), e:
1241
        messages.error(request, e)
1242
    except BaseException, e:
1243
        logger.exception(e)
1244
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1245
        rollback = True
1246
    finally:
1247
        if rollback:
1248
            transaction.rollback()
1249
        else:
1250
            transaction.commit()
1251
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1252
    return redirect(next)
1253

    
1254
@require_http_methods(["POST"])
1255
@signed_terms_required
1256
@login_required
1257
@transaction.commit_manually
1258
def project_leave(request, application_id):
1259
    next = request.GET.get('next')
1260
    if not next:
1261
        next = reverse('astakos.im.views.project_list')
1262

    
1263
    rollback = False
1264
    try:
1265
        application_id = int(application_id)
1266
        leave_project(application_id, request.user)
1267
    except (IOError, PermissionDenied), e:
1268
        messages.error(request, e)
1269
    except BaseException, e:
1270
        logger.exception(e)
1271
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1272
        rollback = True
1273
    finally:
1274
        if rollback:
1275
            transaction.rollback()
1276
        else:
1277
            transaction.commit()
1278

    
1279
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1280
    return redirect(next)
1281

    
1282
@require_http_methods(["POST"])
1283
@signed_terms_required
1284
@login_required
1285
@transaction.commit_manually
1286
def project_accept_member(request, application_id, user_id):
1287
    rollback = False
1288
    try:
1289
        application_id = int(application_id)
1290
        user_id = int(user_id)
1291
        m = accept_membership(application_id, user_id, request.user)
1292
    except (IOError, PermissionDenied), e:
1293
        messages.error(request, e)
1294
    except BaseException, e:
1295
        logger.exception(e)
1296
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1297
        rollback = True
1298
    else:
1299
        realname = m.person.realname
1300
        msg = _(astakos_messages.USER_JOINED_PROJECT) % locals()
1301
        messages.success(request, msg)
1302
    finally:
1303
        if rollback:
1304
            transaction.rollback()
1305
        else:
1306
            transaction.commit()
1307
    return redirect(reverse('project_detail', args=(application_id,)))
1308

    
1309
@require_http_methods(["POST"])
1310
@signed_terms_required
1311
@login_required
1312
@transaction.commit_manually
1313
def project_remove_member(request, application_id, user_id):
1314
    rollback = False
1315
    try:
1316
        application_id = int(application_id)
1317
        user_id = int(user_id)
1318
        m = remove_membership(application_id, user_id, request.user)
1319
    except (IOError, PermissionDenied), e:
1320
        messages.error(request, e)
1321
    except BaseException, e:
1322
        logger.exception(e)
1323
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1324
        rollback = True
1325
    else:
1326
        realname = m.person.realname
1327
        msg = _(astakos_messages.USER_LEFT_PROJECT) % locals()
1328
        messages.success(request, msg)
1329
    finally:
1330
        if rollback:
1331
            transaction.rollback()
1332
        else:
1333
            transaction.commit()
1334
    return redirect(reverse('project_detail', args=(application_id,)))
1335

    
1336
@require_http_methods(["POST"])
1337
@signed_terms_required
1338
@login_required
1339
@transaction.commit_manually
1340
def project_reject_member(request, application_id, user_id):
1341
    rollback = False
1342
    try:
1343
        application_id = int(application_id)
1344
        user_id = int(user_id)
1345
        m = reject_membership(application_id, user_id, request.user)
1346
    except (IOError, PermissionDenied), e:
1347
        messages.error(request, e)
1348
    except BaseException, e:
1349
        logger.exception(e)
1350
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1351
        rollback = True
1352
    else:
1353
        realname = m.person.realname
1354
        msg = _(astakos_messages.USER_LEFT_PROJECT) % locals()
1355
        messages.success(request, msg)
1356
    finally:
1357
        if rollback:
1358
            transaction.rollback()
1359
        else:
1360
            transaction.commit()
1361
    return redirect(reverse('project_detail', args=(application_id,)))
1362

    
1363