Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ 401089d8

History | View | Annotate | Download (47 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
        resource_usage = []
845

    
846
    if request.REQUEST.get('json', None):
847
        return HttpResponse(json.dumps(backenddata),
848
                            mimetype="application/json")
849

    
850
    return render_response('im/resource_usage.html',
851
                           context_instance=get_context(request),
852
                           resource_usage=backenddata,
853
                           usage_update_interval=astakos_settings.USAGE_UPDATE_INTERVAL,
854
                           result=result)
855

    
856
# TODO: action only on POST and user should confirm the removal
857
@require_http_methods(["GET", "POST"])
858
@login_required
859
@signed_terms_required
860
def remove_auth_provider(request, pk):
861
    try:
862
        provider = request.user.auth_providers.get(pk=pk)
863
    except AstakosUserAuthProvider.DoesNotExist:
864
        raise Http404
865

    
866
    if provider.can_remove():
867
        provider.delete()
868
        return HttpResponseRedirect(reverse('edit_profile'))
869
    else:
870
        raise PermissionDenied
871

    
872

    
873
def how_it_works(request):
874
    return render_response(
875
        'im/how_it_works.html',
876
        context_instance=get_context(request))
877

    
878
@transaction.commit_manually
879
def _create_object(request, model=None, template_name=None,
880
        template_loader=template_loader, extra_context=None, post_save_redirect=None,
881
        login_required=False, context_processors=None, form_class=None,
882
        msg=None):
883
    """
884
    Based of django.views.generic.create_update.create_object which displays a
885
    summary page before creating the object.
886
    """
887
    rollback = False
888
    response = None
889

    
890
    if extra_context is None: extra_context = {}
891
    if login_required and not request.user.is_authenticated():
892
        return redirect_to_login(request.path)
893
    try:
894

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

    
926
        if response == None:
927
            # Create the template, context, response
928
            if not template_name:
929
                template_name = "%s/%s_form.html" %\
930
                     (model._meta.app_label, model._meta.object_name.lower())
931
            t = template_loader.get_template(template_name)
932
            c = RequestContext(request, {
933
                'form': form
934
            }, context_processors)
935
            apply_extra_context(extra_context, c)
936
            response = HttpResponse(t.render(c))
937
        return response
938

    
939
@transaction.commit_manually
940
def _update_object(request, model=None, object_id=None, slug=None,
941
        slug_field='slug', template_name=None, template_loader=template_loader,
942
        extra_context=None, post_save_redirect=None, login_required=False,
943
        context_processors=None, template_object_name='object',
944
        form_class=None, msg=None):
945
    """
946
    Based of django.views.generic.create_update.update_object which displays a
947
    summary page before updating the object.
948
    """
949
    rollback = False
950
    response = None
951

    
952
    if extra_context is None: extra_context = {}
953
    if login_required and not request.user.is_authenticated():
954
        return redirect_to_login(request.path)
955

    
956
    try:
957
        model, form_class = get_model_and_form_class(model, form_class)
958
        obj = lookup_object(model, object_id, slug, slug_field)
959

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

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

    
1037

    
1038
@require_http_methods(["GET"])
1039
@signed_terms_required
1040
@login_required
1041
def project_list(request):
1042
    projects = ProjectApplication.objects.user_projects(request.user).select_related()
1043
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1044
                                                prefix="my_projects_")
1045
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1046

    
1047
    return object_list(
1048
        request,
1049
        projects,
1050
        template_name='im/projects/project_list.html',
1051
        extra_context={
1052
            'is_search':False,
1053
            'table': table,
1054
        })
1055

    
1056

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

    
1092

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

    
1122
    rollback = False
1123

    
1124
    application = get_object_or_404(ProjectApplication, pk=application_id)
1125
    try:
1126
        members = application.project.projectmembership_set.select_related()
1127
    except Project.DoesNotExist:
1128
        members = ProjectMembership.objects.none()
1129

    
1130
    members_table = tables.ProjectApplicationMembersTable(application,
1131
                                                          members,
1132
                                                          user=request.user,
1133
                                                          prefix="members_")
1134
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(members_table)
1135

    
1136
    return object_detail(
1137
        request,
1138
        queryset=ProjectApplication.objects.select_related(),
1139
        object_id=application_id,
1140
        template_name='im/projects/project_detail.html',
1141
        extra_context={
1142
            'addmembers_form':addmembers_form,
1143
            'members_table': members_table,
1144
            'user_owns_project': request.user.owns_project(application)
1145
            })
1146

    
1147
@require_http_methods(["GET", "POST"])
1148
@signed_terms_required
1149
@login_required
1150
def project_search(request):
1151
    q = request.GET.get('q', '')
1152
    form = ProjectSearchForm()
1153
    q = q.strip()
1154

    
1155
    if request.method == "POST":
1156
        form = ProjectSearchForm(request.POST)
1157
        if form.is_valid():
1158
            q = form.cleaned_data['q'].strip()
1159
        else:
1160
            q = None
1161

    
1162
    if q is None:
1163
        projects = ProjectApplication.objects.none()
1164
    else:
1165
        accepted_projects = request.user.projectmembership_set.filter(
1166
            ~Q(acceptance_date__isnull=True)).values_list('project', flat=True)
1167
        projects = ProjectApplication.objects.search_by_name(q)
1168
        projects = projects.filter(~Q(project__last_approval_date__isnull=True))
1169
        projects = projects.exclude(project__in=accepted_projects)
1170

    
1171
    table = tables.UserProjectApplicationsTable(projects, user=request.user,
1172
                                                prefix="my_projects_")
1173
    RequestConfig(request, paginate={"per_page": PAGINATE_BY}).configure(table)
1174

    
1175
    return object_list(
1176
        request,
1177
        projects,
1178
        template_name='im/projects/project_list.html',
1179
        extra_context={
1180
          'form': form,
1181
          'is_search': True,
1182
          'q': q,
1183
          'table': table
1184
        })
1185

    
1186
@require_http_methods(["POST"])
1187
@signed_terms_required
1188
@login_required
1189
@transaction.commit_manually
1190
def project_join(request, application_id):
1191
    next = request.GET.get('next')
1192
    if not next:
1193
        next = reverse('astakos.im.views.project_detail',
1194
                       args=(application_id,))
1195

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

    
1216
@require_http_methods(["POST"])
1217
@signed_terms_required
1218
@login_required
1219
@transaction.commit_manually
1220
def project_leave(request, application_id):
1221
    next = request.GET.get('next')
1222
    if not next:
1223
        next = reverse('astakos.im.views.project_list')
1224

    
1225
    rollback = False
1226
    try:
1227
        application_id = int(application_id)
1228
        leave_project(application_id, request.user)
1229
    except (IOError, PermissionDenied), e:
1230
        messages.error(request, e)
1231
    except BaseException, e:
1232
        logger.exception(e)
1233
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1234
        rollback = True
1235
    finally:
1236
        if rollback:
1237
            transaction.rollback()
1238
        else:
1239
            transaction.commit()
1240

    
1241
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1242
    return redirect(next)
1243

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

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

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

    
1325