Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / views.py @ fcde23e4

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
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
    try:
403
        resp = get_services(request)
404
    except Exception, e:
405
        services = ()
406
    else:
407
        services = json.loads(resp.content)
408
    extra_context['services'] = services
409
    return render_response(template_name,
410
                           profile_form = form,
411
                           user_providers = user_providers,
412
                           user_available_providers = user_available_providers,
413
                           context_instance = get_context(request,
414
                                                          extra_context))
415

    
416

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

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

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

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

434
    On unsuccessful creation, renders ``template_name`` with an error message.
435

436
    **Arguments**
437

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

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

446
    ``extra_context``
447
        An dictionary of variables to add to the template context.
448

449
    **Template:**
450

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

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

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

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

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

    
490
                form.store_user(user, request)
491

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

    
533

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

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

545
    If the user isn't logged in, redirects to settings.LOGIN_URL.
546

547
    **Arguments**
548

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

553
    ``extra_context``
554
        An dictionary of variables to add to the template context.
555

556
    **Template:**
557

558
    im/signup.html or ``template_name`` keyword argument.
559

560
    **Settings:**
561

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

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

    
586

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

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

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

    
620

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

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

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

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

    
662

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

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

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

    
709

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

    
720

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

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

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

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

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

    
771
    if request.user.email_change_is_pending():
772
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
773

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

    
780

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

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

    
787
    if astakos_settings.MODERATION_ENABLED:
788
        raise PermissionDenied
789

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

    
811

    
812
@require_http_methods(["GET"])
813
@valid_astakos_user_required
814
def resource_usage(request):
815

    
816
    def with_class(entry):
817
         entry['load_class'] = 'red'
818
         max_value = float(entry['maxValue'])
819
         curr_value = float(entry['currValue'])
820
         entry['ratio_limited']= 0
821
         if max_value > 0 :
822
             entry['ratio'] = (curr_value / max_value) * 100
823
         else:
824
             entry['ratio'] = 0
825
         if entry['ratio'] < 66:
826
             entry['load_class'] = 'yellow'
827
         if entry['ratio'] < 33:
828
             entry['load_class'] = 'green'
829
         if entry['ratio']<0:
830
             entry['ratio'] = 0
831
         if entry['ratio']>100:
832
             entry['ratio_limited'] = 100
833
         else:
834
             entry['ratio_limited'] = entry['ratio']
835
         return entry
836

    
837
    def pluralize(entry):
838
        entry['plural'] = engine.plural(entry.get('name'))
839
        return entry
840

    
841
    resource_usage = None
842
    result = callpoint.get_user_usage(request.user.id)
843
    if result.is_success:
844
        resource_usage = result.data
845
        backenddata = map(with_class, result.data)
846
        backenddata = map(pluralize , backenddata)
847
    else:
848
        messages.error(request, result.reason)
849
        backenddata = []
850
    return render_response('im/resource_usage.html',
851
                           context_instance=get_context(request),
852
                           resource_usage=backenddata,
853
                           result=result)
854

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

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

    
871

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

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

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

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

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

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

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

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

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

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

    
1036

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

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

    
1055

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

    
1091

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

    
1121
    rollback = False
1122

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

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

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

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

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

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

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

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

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

    
1194
    rollback = False
1195
    try:
1196
        application_id = int(application_id)
1197
        join_project(application_id, request.user)
1198
    except (IOError, PermissionDenied), e:
1199
        messages.error(request, e)
1200
    except BaseException, e:
1201
        logger.exception(e)
1202
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1203
        rollback = True
1204
    finally:
1205
        if rollback:
1206
            transaction.rollback()
1207
        else:
1208
            transaction.commit()
1209
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1210
    return redirect(next)
1211

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

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

    
1237
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1238
    return redirect(next)
1239

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

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

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

    
1321