Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 1ebea3d3

History | View | Annotate | Download (46.7 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.translation import ugettext as _
60
from django.views.generic.create_update import (
61
    apply_extra_context, lookup_object, delete_object, get_model_and_form_class)
62
from django.views.generic.list_detail import object_list, object_detail
63
from django.core.xheaders import populate_xheaders
64
from django.core.exceptions import ValidationError, PermissionDenied
65
from django.template.loader import render_to_string
66
from django.views.decorators.http import require_http_methods
67
from django.db.models import Q
68
from django.core.exceptions import PermissionDenied
69
from django.utils import simplejson as json
70

    
71
import astakos.im.messages as astakos_messages
72

    
73
from astakos.im.activation_backends import get_backend, SimpleBackend
74
from astakos.im import tables
75
from astakos.im.models import (
76
    AstakosUser, ApprovalTerms,
77
    EmailChange, RESOURCE_SEPARATOR,
78
    AstakosUserAuthProvider, PendingThirdPartyUser,
79
    ProjectApplication, ProjectMembership, Project)
80
from astakos.im.util import (
81
    get_context, prepare_response, get_query, restrict_next)
82
from astakos.im.forms import (
83
    LoginForm, InvitationForm, ProfileForm,
84
    FeedbackForm, SignApprovalTermsForm,
85
    EmailChangeForm,
86
    ProjectApplicationForm, ProjectSortForm,
87
    AddProjectMembersForm, ProjectSearchForm,
88
    ProjectMembersSortForm)
89
from astakos.im.functions import (
90
    send_feedback, SendMailError,
91
    logout as auth_logout,
92
    activate as activate_func,
93
    invite,
94
    send_activation as send_activation_func,
95
    SendNotificationError,
96
    accept_membership, reject_membership, remove_membership,
97
    leave_project, join_project, enroll_member)
98
from astakos.im.settings import (
99
    COOKIE_DOMAIN, LOGOUT_NEXT,
100
    LOGGING_LEVEL, PAGINATE_BY,
101
    RESOURCES_PRESENTATION_DATA, PAGINATE_BY_ALL,
102
    MODERATION_ENABLED)
103
from astakos.im.api import get_services_dict
104
from astakos.im import settings as astakos_settings
105
from astakos.im.api.callpoint import AstakosCallpoint
106
from astakos.im import auth_providers
107

    
108
logger = logging.getLogger(__name__)
109

    
110
callpoint = AstakosCallpoint()
111

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

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

    
134
            if not provider or not provider.is_active():
135
                raise PermissionDenied
136

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

    
147

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

    
162

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

    
178

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

    
186
    def decorator(func):
187
        if not required_providers:
188
            return func
189

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

    
206

    
207
def valid_astakos_user_required(func):
208
    return signed_terms_required(required_auth_methods_assigned()(login_required(func)))
209

    
210

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

217
    **Arguments**
218

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

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

227
    ``extra_context``
228
        An dictionary of variables to add to the template context.
229

230
    **Template:**
231

232
    im/profile.html or im/login.html or ``template_name`` keyword argument.
233

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

    
240
    third_party_token = request.GET.get('key', False)
241
    if third_party_token:
242
        messages.info(request, astakos_messages.AUTH_PROVIDER_LOGIN_TO_ADD)
243

    
244
    return render_response(
245
        template_name,
246
        login_form = LoginForm(request=request),
247
        context_instance = get_context(request, extra_context)
248
    )
249

    
250

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

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

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

265
    If the user isn't logged in, redirects to settings.LOGIN_URL.
266

267
    **Arguments**
268

269
    ``template_name``
270
        A custom template to use. This is optional; if not specified,
271
        this will default to ``im/invitations.html``.
272

273
    ``extra_context``
274
        An dictionary of variables to add to the template context.
275

276
    **Template:**
277

278
    im/invitations.html or ``template_name`` keyword argument.
279

280
    **Settings:**
281

282
    The view expectes the following settings are defined:
283

284
    * LOGIN_URL: login uri
285
    """
286
    extra_context = extra_context or {}
287
    status = None
288
    message = None
289
    form = InvitationForm()
290

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

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

    
328

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

337
    In case of GET request renders a form for displaying the user information.
338
    In case of POST updates the user informantion and redirects to ``next``
339
    url parameter if exists.
340

341
    If the user isn't logged in, redirects to settings.LOGIN_URL.
342

343
    **Arguments**
344

345
    ``template_name``
346
        A custom template to use. This is optional; if not specified,
347
        this will default to ``im/profile.html``.
348

349
    ``extra_context``
350
        An dictionary of variables to add to the template context.
351

352
    **Template:**
353

354
    im/profile.html or ``template_name`` keyword argument.
355

356
    **Settings:**
357

358
    The view expectes the following settings are defined:
359

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

    
396
    # existing providers
397
    user_providers = request.user.get_active_auth_providers()
398

    
399
    # providers that user can add
400
    user_available_providers = request.user.get_available_auth_providers()
401

    
402
    extra_context['services'] = get_services_dict()
403
    return render_response(template_name,
404
                           profile_form = form,
405
                           user_providers = user_providers,
406
                           user_available_providers = user_available_providers,
407
                           context_instance = get_context(request,
408
                                                          extra_context))
409

    
410

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

417
    In case of GET request renders a form for entering the user information.
418
    In case of POST handles the signup.
419

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

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

428
    On unsuccessful creation, renders ``template_name`` with an error message.
429

430
    **Arguments**
431

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

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

440
    ``extra_context``
441
        An dictionary of variables to add to the template context.
442

443
    **Template:**
444

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

    
452
    provider = get_query(request).get('provider', 'local')
453
    if not auth_providers.get_provider(provider).is_available_for_create():
454
        raise PermissionDenied
455

    
456
    id = get_query(request).get('id')
457
    try:
458
        instance = AstakosUser.objects.get(id=id) if id else None
459
    except AstakosUser.DoesNotExist:
460
        instance = None
461

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

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

    
484
                form.store_user(user, request)
485

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

    
527

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

536
    In case of GET request renders a form for providing the feedback information.
537
    In case of POST sends an email to support team.
538

539
    If the user isn't logged in, redirects to settings.LOGIN_URL.
540

541
    **Arguments**
542

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

547
    ``extra_context``
548
        An dictionary of variables to add to the template context.
549

550
    **Template:**
551

552
    im/signup.html or ``template_name`` keyword argument.
553

554
    **Settings:**
555

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

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

    
580

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

    
597
    next = restrict_next(
598
        request.GET.get('next'),
599
        domain=COOKIE_DOMAIN
600
    )
601

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

    
614

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

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

    
633
    if user.is_active:
634
        message = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE)
635
        messages.error(request, message)
636
        return index(request)
637

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

    
656

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

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

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

    
703

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

    
714

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

    
730
        return render_response(confirm_template_name,
731
                               modified_user=user if 'user' in locals() \
732
                               else None, context_instance=get_context(request,
733
                                                            extra_context))
734

    
735
    if not request.user.is_authenticated():
736
        path = quote(request.get_full_path())
737
        url = request.build_absolute_uri(reverse('index'))
738
        return HttpResponseRedirect(url + '?next=' + path)
739

    
740
    # clean up expired email changes
741
    if request.user.email_change_is_pending():
742
        change = request.user.emailchanges.get()
743
        if change.activation_key_expired():
744
            change.delete()
745
            transaction.commit()
746
            return HttpResponseRedirect(reverse('email_change'))
747

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

    
765
    if request.user.email_change_is_pending():
766
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
767

    
768
    return render_response(
769
        form_template_name,
770
        form=form,
771
        context_instance=get_context(request, extra_context)
772
    )
773

    
774

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

    
777
    if request.user.is_authenticated():
778
        messages.error(request, _(astakos_messages.ALREADY_LOGGED_IN))
779
        return HttpResponseRedirect(reverse('edit_profile'))
780

    
781
    if astakos_settings.MODERATION_ENABLED:
782
        raise PermissionDenied
783

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

    
805

    
806
@require_http_methods(["GET"])
807
@valid_astakos_user_required
808
def resource_usage(request):
809

    
810
    def with_class(entry):
811
         entry['load_class'] = 'red'
812
         max_value = float(entry['maxValue'])
813
         curr_value = float(entry['currValue'])
814
         entry['ratio_limited']= 0
815
         if max_value > 0 :
816
             entry['ratio'] = (curr_value / max_value) * 100
817
         else:
818
             entry['ratio'] = 0
819
         if entry['ratio'] < 66:
820
             entry['load_class'] = 'yellow'
821
         if entry['ratio'] < 33:
822
             entry['load_class'] = 'green'
823
         if entry['ratio']<0:
824
             entry['ratio'] = 0
825
         if entry['ratio']>100:
826
             entry['ratio_limited'] = 100
827
         else:
828
             entry['ratio_limited'] = entry['ratio']
829
         return entry
830

    
831
    def pluralize(entry):
832
        entry['plural'] = engine.plural(entry.get('name'))
833
        return entry
834

    
835
    resource_usage = None
836
    result = callpoint.get_user_usage(request.user.id)
837
    if result.is_success:
838
        resource_usage = result.data
839
        backenddata = map(with_class, result.data)
840
        backenddata = map(pluralize , backenddata)
841
    else:
842
        messages.error(request, result.reason)
843
        backenddata = []
844
    return render_response('im/resource_usage.html',
845
                           context_instance=get_context(request),
846
                           resource_usage=backenddata,
847
                           result=result)
848

    
849
# TODO: action only on POST and user should confirm the removal
850
@require_http_methods(["GET", "POST"])
851
@login_required
852
@signed_terms_required
853
def remove_auth_provider(request, pk):
854
    try:
855
        provider = request.user.auth_providers.get(pk=pk)
856
    except AstakosUserAuthProvider.DoesNotExist:
857
        raise Http404
858

    
859
    if provider.can_remove():
860
        provider.delete()
861
        return HttpResponseRedirect(reverse('edit_profile'))
862
    else:
863
        raise PermissionDenied
864

    
865

    
866
def how_it_works(request):
867
    return render_response(
868
        'im/how_it_works.html',
869
        context_instance=get_context(request))
870

    
871
@transaction.commit_manually
872
def _create_object(request, model=None, template_name=None,
873
        template_loader=template_loader, extra_context=None, post_save_redirect=None,
874
        login_required=False, context_processors=None, form_class=None,
875
        msg=None):
876
    """
877
    Based of django.views.generic.create_update.create_object which displays a
878
    summary page before creating the object.
879
    """
880
    rollback = False
881
    response = None
882

    
883
    if extra_context is None: extra_context = {}
884
    if login_required and not request.user.is_authenticated():
885
        return redirect_to_login(request.path)
886
    try:
887

    
888
        model, form_class = get_model_and_form_class(model, form_class)
889
        extra_context['edit'] = 0
890
        if request.method == 'POST':
891
            form = form_class(request.POST, request.FILES)
892
            if form.is_valid():
893
                verify = request.GET.get('verify')
894
                edit = request.GET.get('edit')
895
                if verify == '1':
896
                    extra_context['show_form'] = False
897
                    extra_context['form_data'] = form.cleaned_data
898
                elif edit == '1':
899
                    extra_context['show_form'] = True
900
                else:
901
                    new_object = form.save()
902
                    if not msg:
903
                        msg = _("The %(verbose_name)s was created successfully.")
904
                    msg = msg % model._meta.__dict__
905
                    messages.success(request, msg, fail_silently=True)
906
                    response = redirect(post_save_redirect, new_object)
907
        else:
908
            form = form_class()
909
    except BaseException, e:
910
        logger.exception(e)
911
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
912
        rollback = True
913
    finally:
914
        if rollback:
915
            transaction.rollback()
916
        else:
917
            transaction.commit()
918

    
919
        if response == None:
920
            # Create the template, context, response
921
            if not template_name:
922
                template_name = "%s/%s_form.html" %\
923
                     (model._meta.app_label, model._meta.object_name.lower())
924
            t = template_loader.get_template(template_name)
925
            c = RequestContext(request, {
926
                'form': form
927
            }, context_processors)
928
            apply_extra_context(extra_context, c)
929
            response = HttpResponse(t.render(c))
930
        return response
931

    
932
@transaction.commit_manually
933
def _update_object(request, model=None, object_id=None, slug=None,
934
        slug_field='slug', template_name=None, template_loader=template_loader,
935
        extra_context=None, post_save_redirect=None, login_required=False,
936
        context_processors=None, template_object_name='object',
937
        form_class=None, msg=None):
938
    """
939
    Based of django.views.generic.create_update.update_object which displays a
940
    summary page before updating the object.
941
    """
942
    rollback = False
943
    response = None
944

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

    
949
    try:
950
        model, form_class = get_model_and_form_class(model, form_class)
951
        obj = lookup_object(model, object_id, slug, slug_field)
952

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

    
995
@require_http_methods(["GET", "POST"])
996
@signed_terms_required
997
@login_required
998
def project_add(request):
999
    resource_groups = RESOURCES_PRESENTATION_DATA.get('groups', {})
1000
    resource_catalog = ()
1001
    result = callpoint.list_resources()
1002
    details_fields = [
1003
        "name", "homepage", "description","start_date","end_date", "comments"]
1004
    membership_fields =[
1005
        "member_join_policy", "member_leave_policy", "limit_on_members_number"]
1006
    if not result.is_success:
1007
        messages.error(
1008
            request,
1009
            'Unable to retrieve system resources: %s' % result.reason
1010
    )
1011
    else:
1012
        resource_catalog = [
1013
            (g, filter(lambda r: r.get('group', '') == g, result.data)) \
1014
                for g in resource_groups]
1015
    extra_context = {
1016
        'resource_catalog':resource_catalog,
1017
        'resource_groups':resource_groups,
1018
        'show_form':True,
1019
        'details_fields':details_fields,
1020
        'membership_fields':membership_fields}
1021
    return _create_object(
1022
        request,
1023
        template_name='im/projects/projectapplication_form.html',
1024
        extra_context=extra_context,
1025
        post_save_redirect=reverse('project_list'),
1026
        form_class=ProjectApplicationForm,
1027
        msg=_("The %(verbose_name)s has been received and \
1028
                 is under consideration."))
1029

    
1030

    
1031
@require_http_methods(["GET"])
1032
@signed_terms_required
1033
@login_required
1034
def project_list(request):
1035
    projects = ProjectApplication.objects.user_projects(request.user).select_related()
1036
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1037
                                                prefix="my_projects_")
1038
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1039

    
1040
    return object_list(
1041
        request,
1042
        projects,
1043
        template_name='im/projects/project_list.html',
1044
        extra_context={
1045
            'is_search':False,
1046
            'table': table,
1047
        })
1048

    
1049

    
1050
@require_http_methods(["GET", "POST"])
1051
@signed_terms_required
1052
@login_required
1053
def project_update(request, application_id):
1054
    resource_groups = RESOURCES_PRESENTATION_DATA.get('groups', {})
1055
    resource_catalog = ()
1056
    result = callpoint.list_resources()
1057
    details_fields = [
1058
        "name", "homepage", "description","start_date","end_date", "comments"]
1059
    membership_fields =[
1060
        "member_join_policy", "member_leave_policy", "limit_on_members_number"]
1061
    if not result.is_success:
1062
        messages.error(
1063
            request,
1064
            'Unable to retrieve system resources: %s' % result.reason
1065
    )
1066
    else:
1067
        resource_catalog = [
1068
            (g, filter(lambda r: r.get('group', '') == g, result.data)) \
1069
                for g in resource_groups]
1070
    extra_context = {
1071
        'resource_catalog':resource_catalog,
1072
        'resource_groups':resource_groups,
1073
        'show_form':True,
1074
        'details_fields':details_fields,
1075
        'membership_fields':membership_fields}
1076
    return _update_object(
1077
        request,
1078
        object_id=application_id,
1079
        template_name='im/projects/projectapplication_form.html',
1080
        extra_context=extra_context, post_save_redirect=reverse('project_list'),
1081
        form_class=ProjectApplicationForm,
1082
        msg = _("The %(verbose_name)s has been received and \
1083
                    is under consideration."))
1084

    
1085

    
1086
@require_http_methods(["GET", "POST"])
1087
@signed_terms_required
1088
@login_required
1089
@transaction.commit_on_success
1090
def project_detail(request, application_id):
1091
    addmembers_form = AddProjectMembersForm()
1092
    if request.method == 'POST':
1093
        addmembers_form = AddProjectMembersForm(request.POST)
1094
        if addmembers_form.is_valid():
1095
            try:
1096
                rollback = False
1097
                application_id = int(application_id)
1098
                map(lambda u: enroll_member(
1099
                        application_id,
1100
                        u,
1101
                        request_user=request.user),
1102
                    addmembers_form.valid_users)
1103
            except (IOError, PermissionDenied), e:
1104
                messages.error(request, e)
1105
            except BaseException, e:
1106
                rollback = True
1107
                messages.error(request, e)
1108
            finally:
1109
                if rollback == True:
1110
                    transaction.rollback()
1111
                else:
1112
                    transaction.commit()
1113
            addmembers_form = AddProjectMembersForm()
1114

    
1115
    rollback = False
1116

    
1117
    application = get_object_or_404(ProjectApplication, pk=application_id)
1118
    try:
1119
        members = application.project.projectmembership_set.select_related()
1120
    except Project.DoesNotExist:
1121
        members = ProjectMembership.objects.none()
1122

    
1123
    members_table = tables.ProjectApplicationMembersTable(application,
1124
                                                          members,
1125
                                                          user=request.user,
1126
                                                          prefix="members_")
1127
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(members_table)
1128

    
1129
    return object_detail(
1130
        request,
1131
        queryset=ProjectApplication.objects.select_related(),
1132
        object_id=application_id,
1133
        template_name='im/projects/project_detail.html',
1134
        extra_context={
1135
            'addmembers_form':addmembers_form,
1136
            'members_table': members_table,
1137
            'user_owns_project': request.user.owns_project(application)
1138
            })
1139

    
1140
@require_http_methods(["GET", "POST"])
1141
@signed_terms_required
1142
@login_required
1143
def project_search(request):
1144
    q = request.GET.get('q', '')
1145
    form = ProjectSearchForm()
1146
    q = q.strip()
1147

    
1148
    if request.method == "POST":
1149
        form = ProjectSearchForm(request.POST)
1150
        if form.is_valid():
1151
            q = form.cleaned_data['q'].strip()
1152
        else:
1153
            q = None
1154

    
1155
    if q is None:
1156
        projects = ProjectApplication.objects.none()
1157
    else:
1158
        accepted_projects = request.user.projectmembership_set.filter(
1159
            ~Q(acceptance_date__isnull=True)).values_list('project', flat=True)
1160
        projects = ProjectApplication.objects.search_by_name(q)
1161
        projects = projects.filter(~Q(project__last_approval_date__isnull=True))
1162
        projects = projects.exclude(project__in=accepted_projects)
1163

    
1164
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1165
                                                prefix="my_projects_")
1166
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1167

    
1168
    return object_list(
1169
        request,
1170
        projects,
1171
        template_name='im/projects/project_list.html',
1172
        extra_context={
1173
          'form': form,
1174
          'is_search': True,
1175
          'q': q,
1176
          'table': table
1177
        })
1178

    
1179
@require_http_methods(["POST"])
1180
@signed_terms_required
1181
@login_required
1182
@transaction.commit_manually
1183
def project_join(request, application_id):
1184
    next = request.GET.get('next')
1185
    if not next:
1186
        next = reverse('astakos.im.views.project_detail',
1187
                       args=(application_id,))
1188

    
1189
    rollback = False
1190
    try:
1191
        application_id = int(application_id)
1192
        join_project(application_id, request.user)
1193
        # TODO: distinct messages for request/auto accept ???
1194
        messages.success(request, _(astakos_messages.USER_JOIN_REQUEST_SUBMITED))
1195
    except (IOError, PermissionDenied), e:
1196
        messages.error(request, e)
1197
    except BaseException, e:
1198
        logger.exception(e)
1199
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1200
        rollback = True
1201
    finally:
1202
        if rollback:
1203
            transaction.rollback()
1204
        else:
1205
            transaction.commit()
1206
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1207
    return redirect(next)
1208

    
1209
@require_http_methods(["POST"])
1210
@signed_terms_required
1211
@login_required
1212
@transaction.commit_manually
1213
def project_leave(request, application_id):
1214
    next = request.GET.get('next')
1215
    if not next:
1216
        next = reverse('astakos.im.views.project_list')
1217

    
1218
    rollback = False
1219
    try:
1220
        application_id = int(application_id)
1221
        leave_project(application_id, request.user)
1222
    except (IOError, PermissionDenied), e:
1223
        messages.error(request, e)
1224
    except BaseException, e:
1225
        logger.exception(e)
1226
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1227
        rollback = True
1228
    finally:
1229
        if rollback:
1230
            transaction.rollback()
1231
        else:
1232
            transaction.commit()
1233

    
1234
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1235
    return redirect(next)
1236

    
1237
@require_http_methods(["POST"])
1238
@signed_terms_required
1239
@login_required
1240
@transaction.commit_manually
1241
def project_accept_member(request, application_id, user_id):
1242
    rollback = False
1243
    try:
1244
        application_id = int(application_id)
1245
        user_id = int(user_id)
1246
        m = accept_membership(application_id, user_id, request.user)
1247
    except (IOError, PermissionDenied), e:
1248
        messages.error(request, e)
1249
    except BaseException, e:
1250
        logger.exception(e)
1251
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1252
        rollback = True
1253
    else:
1254
        realname = m.person.realname
1255
        msg = _(astakos_messages.USER_JOINED_PROJECT) % locals()
1256
        messages.success(request, msg)
1257
    finally:
1258
        if rollback:
1259
            transaction.rollback()
1260
        else:
1261
            transaction.commit()
1262
    return redirect(reverse('project_detail', args=(application_id,)))
1263

    
1264
@require_http_methods(["POST"])
1265
@signed_terms_required
1266
@login_required
1267
@transaction.commit_manually
1268
def project_remove_member(request, application_id, user_id):
1269
    rollback = False
1270
    try:
1271
        application_id = int(application_id)
1272
        user_id = int(user_id)
1273
        m = remove_membership(application_id, user_id, request.user)
1274
    except (IOError, PermissionDenied), e:
1275
        messages.error(request, e)
1276
    except BaseException, e:
1277
        logger.exception(e)
1278
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1279
        rollback = True
1280
    else:
1281
        realname = m.person.realname
1282
        msg = _(astakos_messages.USER_LEFT_PROJECT) % locals()
1283
        messages.success(request, msg)
1284
    finally:
1285
        if rollback:
1286
            transaction.rollback()
1287
        else:
1288
            transaction.commit()
1289
    return redirect(reverse('project_detail', args=(application_id,)))
1290

    
1291
@require_http_methods(["POST"])
1292
@signed_terms_required
1293
@login_required
1294
@transaction.commit_manually
1295
def project_reject_member(request, application_id, user_id):
1296
    rollback = False
1297
    try:
1298
        application_id = int(application_id)
1299
        user_id = int(user_id)
1300
        m = reject_membership(application_id, user_id, request.user)
1301
    except (IOError, PermissionDenied), e:
1302
        messages.error(request, e)
1303
    except BaseException, e:
1304
        logger.exception(e)
1305
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1306
        rollback = True
1307
    else:
1308
        realname = m.person.realname
1309
        msg = _(astakos_messages.USER_LEFT_PROJECT) % locals()
1310
        messages.success(request, msg)
1311
    finally:
1312
        if rollback:
1313
            transaction.rollback()
1314
        else:
1315
            transaction.commit()
1316
    return redirect(reverse('project_detail', args=(application_id,)))
1317

    
1318